feat: finished new logging feature

This commit is contained in:
Anton Stubenbord
2023-10-12 17:50:13 +02:00
parent f0c3ced804
commit 7d1c0dffe4
37 changed files with 1446 additions and 720 deletions

View 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();
}
}

View 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;
}

View 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---"
],
];
}
}

View 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,
);
}
}

View 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,
);
}
}
});
}
}

View File

@@ -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();
}
}
}

View 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';
}
}

View 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,
);
}
}

View 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';
}

View File

@@ -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/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:logger/logger.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:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:path/path.dart' as p; import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
import 'package:rxdart/subjects.dart';
final _fileNameFormat = DateFormat("yyyy-MM-dd");
class AppLogsPage extends StatefulWidget { class AppLogsPage extends StatefulWidget {
const AppLogsPage({super.key}); const AppLogsPage({super.key});
@@ -23,304 +17,181 @@ class AppLogsPage extends StatefulWidget {
} }
class _AppLogsPageState extends State<AppLogsPage> { class _AppLogsPageState extends State<AppLogsPage> {
final _fileContentStream = BehaviorSubject();
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
StreamSubscription? _fileChangesSubscription;
late DateTime _date;
File? file;
bool autoScroll = true; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final locale = Localizations.localeOf(context).toString(); final locale = Localizations.localeOf(context).toString();
final theme = Theme.of(context); final theme = Theme.of(context);
return Scaffold( return BlocBuilder<AppLogsCubit, AppLogsState>(
appBar: AppBar( builder: (context, state) {
title: Row( final formattedDate = DateFormat.yMMMd(locale).format(state.date);
mainAxisSize: MainAxisSize.min, return Scaffold(
children: [ bottomNavigationBar: BottomAppBar(
Text("Logs"), child: Row(
SizedBox(width: 16), mainAxisAlignment: MainAxisAlignment.start,
DropdownButton<DateTime>( children: switch (state) {
AppLogsStateInitial() => [],
value: _date, AppLogsStateLoading() => [],
items: [ AppLogsStateLoaded() => [
for (var date in _availableLogs ?? []) IconButton(
DropdownMenuItem( tooltip: S.of(context)!.copyToClipboard,
child: Text(DateFormat.yMMMd(locale).format(date)), onPressed: () {
value: date, context
), .read<AppLogsCubit>()
], .copyToClipboard(state.date);
onChanged: (value) { },
if (value != null) { icon: const Icon(Icons.copy),
setState(() { ).padded(),
_date = value; IconButton(
}); tooltip: S.of(context)!.saveLogsToFile,
_initFile(); 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(),
],
_ => [],
}, },
), ),
], ),
), appBar: AppBar(
actions: file != null title: Text(S.of(context)!.appLogs(formattedDate)),
? [ actions: [
if (state is AppLogsStateLoaded)
IconButton( IconButton(
tooltip: "Save log file to selected directory", tooltip: MaterialLocalizations.of(context).datePickerHelpText,
onPressed: () => _saveFile(locale), onPressed: () async {
icon: const Icon(Icons.download), final selectedDate = await showDatePicker(
), context: context,
IconButton( initialDate: state.date,
tooltip: "Copy logs to clipboard", firstDate: state.availableLogs.first,
onPressed: _copyToClipboard, lastDate: state.availableLogs.last,
icon: const Icon(Icons.copy), 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(), ).padded(),
] ],
: null, ),
), body: switch (state) {
body: Builder( AppLogsStateLoaded(
builder: (context) { logs: var logs,
if (_availableLogs == null) { ) =>
return Center( Builder(
child: Text("No logs available."), builder: (context) {
); if (state.logs.isEmpty) {
} return Center(
return StreamBuilder( child: Text(S.of(context)!.noLogsFoundOn(formattedDate)),
stream: _fileContentStream, );
builder: (context, snapshot) { }
if (!snapshot.hasData || file == null) { return ListView.builder(
return const Center( reverse: true,
child: Text( controller: _scrollController,
"Initializing logs...", itemBuilder: (context, index) {
), if (index == 0) {
); return Center(
} child: Text(S.of(context)!.logfileBottomReached,
final messages = _transformLog(snapshot.data!).reversed.toList(); style: theme.textTheme.bodySmall?.copyWith(
return ColoredBox( color: theme.disabledColor,
color: theme.colorScheme.background, )),
child: Column( ).padded(24);
children: [ }
Expanded( final messages = state.logs;
child: ListView.builder( final logMessage = messages[index - 1];
reverse: true, final altColor = CupertinoDynamicColor.withBrightness(
controller: _scrollController, color: Colors.grey.shade200,
itemBuilder: (context, index) { darkColor: Colors.grey.shade800,
if (index == 0) { ).resolveFrom(context);
return Center( return ParsedLogMessageTile(
child: Text( message: logMessage,
"End of logs.", backgroundColor: (index % 2 == 0)
style: theme.textTheme.labelLarge?.copyWith( ? theme.colorScheme.background
fontStyle: FontStyle.italic, : altColor,
), );
), },
).padded(24); itemCount: logs.length + 1,
} );
final logMessage = messages[index - 1]; },
final altColor = CupertinoDynamicColor.withBrightness( ),
color: Colors.grey.shade200, AppLogsStateError() => Center(
darkColor: Colors.grey.shade800, child: Text(
).resolveFrom(context); S.of(context)!.couldNotLoadLogfileFrom(formattedDate),
return _LogMessageWidget(
message: logMessage,
backgroundColor: (index % 2 == 0)
? theme.colorScheme.background
: altColor,
);
},
itemCount: messages.length + 1,
),
),
],
), ),
); ),
}, _ => _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 { class ParsedLogMessageTile extends StatelessWidget {
static final RegExp pattern = RegExp( final ParsedLogMessage message;
r'(?<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s*(?<level>[A-Z]*)'
r'\s+---\s*(?:\[\s*(?<className>.*)\]\s*-\s*(?<methodName>.*)\s*)?:\s*(?<message>.+)',
);
final Level? level;
final String message;
final String? className;
final String? methodName;
final DateTime? timestamp;
bool get isFormatted => level != null;
const _LogMessage({
this.level,
required this.message,
this.className,
this.methodName,
this.timestamp,
});
static bool hasMatch(String message) => pattern.hasMatch(message);
factory _LogMessage.fromMessage(String message) {
final match = pattern.firstMatch(message);
if (match == null) {
return _LogMessage(message: message);
}
return _LogMessage(
level: Level.values.byName(match.namedGroup('level')!.toLowerCase()),
message: match.namedGroup('message')!,
className: match.namedGroup('className'),
methodName: match.namedGroup('methodName'),
timestamp: DateTime.tryParse(match.namedGroup('timestamp') ?? ''),
);
}
}
class _LogMessageWidget extends StatelessWidget {
final _LogMessage message;
final Color backgroundColor; final Color backgroundColor;
const _LogMessageWidget({
const ParsedLogMessageTile({
super.key,
required this.message, required this.message,
required this.backgroundColor, required this.backgroundColor,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final c = Theme.of(context).colorScheme; return switch (message) {
if (!message.isFormatted) { ParsedFormattedLogMessage m => FormattedLogMessageWidget(
return Text( message: m,
message.message, backgroundColor: backgroundColor,
style: Theme.of(context).textTheme.bodySmall?.copyWith( ),
fontSize: 5, UnformattedLogMessage(message: var m) => Text(m),
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,
}; };
}
}
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) { final icon = switch (message.level) {
Level.trace => Icons.troubleshoot, Level.trace => Icons.troubleshoot,
Level.debug => Icons.bug_report, Level.debug => Icons.bug_report,
@@ -330,31 +201,83 @@ class _LogMessageWidget extends StatelessWidget {
Level.fatal => Icons.error_outline, Level.fatal => Icons.error_outline,
_ => null, _ => 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( 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( trailing: Icon(
icon, icon,
color: color, color: color,
), ),
tileColor: backgroundColor, expandedCrossAxisAlignment: CrossAxisAlignment.start,
title: Text( childrenPadding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
message.message, expandedAlignment: Alignment.topLeft,
style: TextStyle(color: color), children: source.isNotEmpty
), ? [
subtitle: message.className != null Row(
? Text( children: [
"${message.className ?? ''} ${message.methodName ?? ''}", const Icon(Icons.arrow_right),
style: TextStyle( Flexible(
color: color.withOpacity(0.75), child: Text(
fontSize: 10, 'In $source',
fontFamily: "monospace", style: logStyle?.copyWith(fontSize: 14),
),
),
],
), ),
) ..._buildErrorWidgets(context),
: null, ]
leading: message.timestamp != null : _buildErrorWidgets(context),
? Text(DateFormat("HH:mm:ss.SSS").format(message.timestamp!))
: null,
), ),
); );
} }
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 [];
}
}
} }

View File

@@ -1,166 +1,260 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/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:paperless_mobile/helpers/format_helpers.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class FileService { 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, Uint8List bytes,
String filename, String filename,
) async { ) async {
final dir = await documentsDirectory; File file = File(p.join(_logDirectory.path, filename));
File file = File("${dir.path}/$filename"); logger.fd(
"Writing bytes to file $filename",
methodName: 'saveToFile',
className: runtimeType.toString(),
);
return file..writeAsBytes(bytes); return file..writeAsBytes(bytes);
} }
static Future<Directory?> getDirectory(PaperlessDirectoryType type) { Directory getDirectory(PaperlessDirectoryType type) {
return switch (type) { return switch (type) {
PaperlessDirectoryType.documents => documentsDirectory, PaperlessDirectoryType.documents => _documentsDirectory,
PaperlessDirectoryType.temporary => temporaryDirectory, PaperlessDirectoryType.temporary => _temporaryDirectory,
PaperlessDirectoryType.scans => temporaryScansDirectory, PaperlessDirectoryType.scans => _temporaryScansDirectory,
PaperlessDirectoryType.download => downloadsDirectory, PaperlessDirectoryType.download => _downloadsDirectory,
PaperlessDirectoryType.upload => uploadDirectory, 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, { PaperlessDirectoryType type, {
required String extension, required String extension,
String? fileName, String? fileName,
bool create = false,
}) async { }) async {
final dir = await getDirectory(type); final dir = getDirectory(type);
final _fileName = (fileName ?? const Uuid().v1()) + '.$extension'; final filename = (fileName ?? const Uuid().v1()) + '.$extension';
return File('${dir?.path}/$_fileName'); final file = File(p.join(dir.path, filename));
} if (create) {
await file.create(recursive: true);
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.");
} }
return file;
} }
static Future<Directory> get logDirectory async { Future<Directory> getConsumptionDirectory({required String userId}) async {
if (Platform.isAndroid) { return Directory(p.join(_uploadDirectory.path, userId))
return getExternalStorageDirectories(type: StorageDirectory.documents) .create(recursive: true);
.then((directory) async =>
directory?.firstOrNull ??
await getApplicationDocumentsDirectory())
.then((directory) =>
Directory('${directory.path}/logs').create(recursive: true));
} else if (Platform.isIOS) {
return getApplicationDocumentsDirectory().then(
(value) => Directory('${value.path}/logs').create(recursive: true));
}
throw UnsupportedError("Platform not supported.");
} }
static Future<Directory> get downloadsDirectory async { Future<void> clearUserData({required String userId}) async {
if (Platform.isAndroid) { final redactedId = redactUserId(userId);
var directory = Directory('/storage/emulated/0/Download'); logger.fd(
if (!directory.existsSync()) { "Clearing data for user $redactedId...",
final downloadsDir = await getExternalStorageDirectories( className: runtimeType.toString(),
type: StorageDirectory.downloads, methodName: "clearUserData",
); );
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.");
}
}
static Future<Directory> get uploadDirectory async { final scanDirSize =
final dir = await getApplicationDocumentsDirectory() formatBytes(await getDirSizeInBytes(_temporaryScansDirectory));
.then((dir) => Directory('${dir.path}/upload')); final tempDirSize =
return dir.create(recursive: true); formatBytes(await getDirSizeInBytes(_temporaryDirectory));
}
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 consumptionDir = await getConsumptionDirectory(userId: userId); final consumptionDir = await getConsumptionDirectory(userId: userId);
final consumptionDirSize = final consumptionDirSize =
formatBytes(await getDirSizeInBytes(consumptionDir)); formatBytes(await getDirSizeInBytes(consumptionDir));
logger.t("FileService#clearUserData(): Removing scans..."); logger.ft(
await scanDir.delete(recursive: true); "Removing scans...",
logger.t("FileService#clearUserData(): Removed $scanDirSize..."); 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( await _temporaryDirectory.delete(recursive: true);
"FileService#clearUserData(): Removing temporary files and cache content..."); logger.ft(
"Removed $tempDirSize...",
className: runtimeType.toString(),
methodName: "clearUserData",
);
await tempDir.delete(recursive: true); logger.ft(
logger.t("FileService#clearUserData(): Removed $tempDirSize..."); "Removing files waiting for consumption...",
className: runtimeType.toString(),
logger.t( methodName: "clearUserData",
"FileService#clearUserData(): Removing files waiting for consumption..."); );
await consumptionDir.delete(recursive: true); await consumptionDir.delete(recursive: true);
logger.t("FileService#clearUserData(): Removed $consumptionDirSize..."); logger.ft(
} "Removed $consumptionDirSize...",
className: runtimeType.toString(),
static Future<void> clearDirectoryContent(PaperlessDirectoryType type) async { methodName: "clearUserData",
final dir = await getDirectory(type);
if (dir == null || !(await dir.exists())) {
return;
}
await Future.wait(
dir.listSync().map((item) => item.delete(recursive: true)),
); );
} }
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(); 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(); return directory.list().whereType<Directory>().toList();
} }
static Future<int> getDirSizeInBytes(Directory dir) async { Future<int> getDirSizeInBytes(Directory dir) async {
return dir return dir
.list(recursive: true) .list(recursive: true)
.fold(0, (previous, element) => previous + element.statSync().size); .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 { enum PaperlessDirectoryType {
@@ -168,5 +262,6 @@ enum PaperlessDirectoryType {
temporary, temporary,
scans, scans,
download, download,
upload; upload,
logs;
} }

View File

@@ -35,6 +35,10 @@ extension DateHelpers on DateTime {
yesterday.month == month && yesterday.month == month &&
yesterday.year == year; yesterday.year == year;
} }
bool isOnSameDayAs(DateTime other) {
return other.day == day && other.month == month && other.year == year;
}
} }
extension StringNormalizer on String { extension StringNormalizer on String {

View File

@@ -2,12 +2,9 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.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/constants.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/global/asset_images.dart';
import 'package:paperless_mobile/core/logging/view/app_logs_page.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/core/widgets/paperless_logo.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.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/saved_views_route.dart';
import 'package:paperless_mobile/routes/typed/branches/upload_queue_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/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/changelog_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart'; import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -185,12 +183,9 @@ class AppDrawer extends StatelessWidget {
ListTile( ListTile(
dense: true, dense: true,
leading: const Icon(Icons.subject), leading: const Icon(Icons.subject),
title: const Text('Logs'), //TODO: INTL title: Text(S.of(context)!.appLogs('')),
onTap: () { onTap: () {
Navigator.of(context) AppLogsRoute().push(context);
.push(MaterialPageRoute(builder: (context) {
return const AppLogsPage();
}));
}, },
), ),
ListTile( ListTile(

View File

@@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:open_filex/open_filex.dart'; import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/core/logging/data/logger.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
@@ -85,7 +85,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
} }
Future<ResultType> openDocumentInSystemViewer() async { Future<ResultType> openDocumentInSystemViewer() async {
final cacheDir = await FileService.temporaryDirectory; final cacheDir = FileService.instance.temporaryDirectory;
if (state.metaData == null) { if (state.metaData == null) {
await loadMetaData(); await loadMetaData();
} }
@@ -121,7 +121,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
} }
String targetPath = _buildDownloadFilePath( String targetPath = _buildDownloadFilePath(
downloadOriginal, downloadOriginal,
await FileService.downloadsDirectory, FileService.instance.downloadsDirectory,
); );
if (!await File(targetPath).exists()) { if (!await File(targetPath).exists()) {
@@ -170,7 +170,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
locale: locale, locale: locale,
userId: userId, 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 { Future<void> shareDocument({bool shareOriginal = false}) async {
@@ -179,7 +179,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
} }
String filePath = _buildDownloadFilePath( String filePath = _buildDownloadFilePath(
shareOriginal, shareOriginal,
await FileService.temporaryDirectory, FileService.instance.temporaryDirectory,
); );
await _api.downloadToFile( await _api.downloadToFile(
state.document, state.document,
@@ -204,7 +204,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
await loadMetaData(); await loadMetaData();
} }
final filePath = final filePath =
_buildDownloadFilePath(false, await FileService.temporaryDirectory); _buildDownloadFilePath(false, FileService.instance.temporaryDirectory);
await _api.downloadToFile( await _api.downloadToFile(
state.document, state.document,
filePath, filePath,

View File

@@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/core/logging/data/logger.dart';
import 'package:paperless_mobile/core/model/info_message_exception.dart'; import 'package:paperless_mobile/core/model/info_message_exception.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
@@ -19,13 +19,21 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
: super(const InitialDocumentScannerState()); : super(const InitialDocumentScannerState());
Future<void> initialize() async { Future<void> initialize() async {
logger.t("Restoring scans..."); logger.fd(
"Restoring scans...",
className: runtimeType.toString(),
methodName: "initialize",
);
emit(const RestoringDocumentScannerState()); emit(const RestoringDocumentScannerState());
final tempDir = await FileService.temporaryScansDirectory; final tempDir = FileService.instance.temporaryScansDirectory;
final allFiles = tempDir.list().whereType<File>(); final allFiles = tempDir.list().whereType<File>();
final scans = final scans =
await allFiles.where((event) => event.path.endsWith(".jpeg")).toList(); await allFiles.where((event) => event.path.endsWith(".jpeg")).toList();
logger.t("Restored ${scans.length} scans."); logger.fd(
"Restored ${scans.length} scans.",
className: runtimeType.toString(),
methodName: "initialize",
);
emit( emit(
scans.isEmpty scans.isEmpty
? const InitialDocumentScannerState() ? const InitialDocumentScannerState()
@@ -75,7 +83,7 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
String fileName, String fileName,
String locale, String locale,
) async { ) async {
var file = await FileService.saveToFile(bytes, fileName); var file = await FileService.instance.saveToFile(bytes, fileName);
_notificationService.notifyFileSaved( _notificationService.notifyFileSaved(
filename: fileName, filename: fileName,
filePath: file.path, filePath: file.path,

View File

@@ -227,9 +227,10 @@ class _ScannerPageState extends State<ScannerPage>
if (!isGranted) { if (!isGranted) {
return; return;
} }
final file = await FileService.allocateTemporaryFile( final file = await FileService.instance.allocateTemporaryFile(
PaperlessDirectoryType.scans, PaperlessDirectoryType.scans,
extension: 'jpeg', extension: 'jpeg',
create: true,
); );
if (kDebugMode) { if (kDebugMode) {
dev.log('[ScannerPage] Created temporary file: ${file.path}'); dev.log('[ScannerPage] Created temporary file: ${file.path}');

View File

@@ -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/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/core/logging/data/logger.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/future_or_builder.dart'; import 'package:paperless_mobile/core/widgets/future_or_builder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
@@ -378,8 +378,10 @@ class _DocumentUploadPreparationPageState
} on PaperlessFormValidationException catch (exception) { } on PaperlessFormValidationException catch (exception) {
setState(() => _errors = exception.validationMessages); setState(() => _errors = exception.validationMessages);
} catch (error, stackTrace) { } catch (error, stackTrace) {
logger.e( logger.fe(
"An unknown error occurred during document upload.", "An unknown error occurred during document upload.",
className: runtimeType.toString(),
methodName: "_onSubmit",
error: error, error: error,
stackTrace: stackTrace, stackTrace: stackTrace,
); );

View File

@@ -1,16 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/core/logging/data/logger.dart';
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/label_repository_state.dart'; import 'package:paperless_mobile/core/repository/label_repository_state.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.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/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_cubit.g.dart';
part 'inbox_state.dart'; part 'inbox_state.dart';
@@ -50,18 +49,12 @@ class InboxCubit extends HydratedCubit<InboxState>
final wasInInboxBeforeUpdate = final wasInInboxBeforeUpdate =
state.documents.map((e) => e.id).contains(document.id); state.documents.map((e) => e.id).contains(document.id);
if (!hasInboxTag && wasInInboxBeforeUpdate) { if (!hasInboxTag && wasInInboxBeforeUpdate) {
print(
"INBOX: Removing document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
remove(document); remove(document);
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1)); emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
} else if (hasInboxTag) { } else if (hasInboxTag) {
if (wasInInboxBeforeUpdate) { if (wasInInboxBeforeUpdate) {
print(
"INBOX: Replacing document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
replace(document); replace(document);
} else { } else {
print(
"INBOX: Adding document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
_addDocument(document); _addDocument(document);
emit( emit(
state.copyWith(itemsInInboxCount: state.itemsInInboxCount + 1)); state.copyWith(itemsInInboxCount: state.itemsInInboxCount + 1));
@@ -84,17 +77,26 @@ class InboxCubit extends HydratedCubit<InboxState>
} }
Future<void> refreshItemsInInboxCount([bool shouldLoadInbox = true]) async { Future<void> refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {
logger.t( logger.fi(
"InboxCubit#refreshItemsInInboxCount(): Checking for new documents in inbox..."); "Checking for new documents in inbox...",
className: runtimeType.toString(),
methodName: "refreshItemsInInboxCount",
);
final stats = await _statsApi.getServerStatistics(); final stats = await _statsApi.getServerStatistics();
if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) { if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) {
logger.t( logger.fi(
"InboxCubit#refreshItemsInInboxCount(): New documents found in inbox, reloading inbox."); "New documents found in inbox, reloading.",
className: runtimeType.toString(),
methodName: "refreshItemsInInboxCount",
);
await loadInbox(); await loadInbox();
} else { } else {
logger.t( logger.fi(
"InboxCubit#refreshItemsInInboxCount(): No new documents found in inbox."); "No new documents found in inbox.",
className: runtimeType.toString(),
methodName: "refreshItemsInInboxCount",
);
} }
emit(state.copyWith(itemsInInboxCount: stats.documentsInInbox)); emit(state.copyWith(itemsInInboxCount: stats.documentsInInbox));
} }

View File

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

View File

@@ -1,5 +1,6 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hive_flutter/adapters.dart'; import 'package:hive_flutter/adapters.dart';
import 'package:hydrated_bloc/hydrated_bloc.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/database/tables/user_credentials.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/core/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/model/info_message_exception.dart';
import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
@@ -56,7 +58,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
} }
emit(const AuthenticatingState(AuthenticatingStage.authenticating)); emit(const AuthenticatingState(AuthenticatingStage.authenticating));
final localUserId = "${credentials.username}@$serverUrl"; final localUserId = "${credentials.username}@$serverUrl";
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 { try {
await _addUser( await _addUser(
localUserId, localUserId,
@@ -95,15 +103,22 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
await globalSettings.save(); await globalSettings.save();
emit(AuthenticatedState(localUserId: localUserId)); emit(AuthenticatedState(localUserId: localUserId));
logger.t( logger.fd(
'AuthenticationCubit#login(): User $localUserId successfully logged in.'); 'User $redactedId successfully logged in.',
className: runtimeType.toString(),
methodName: 'login',
);
} }
/// Switches to another account if it exists. /// Switches to another account if it exists.
Future<void> switchAccount(String localUserId) async { Future<void> switchAccount(String localUserId) async {
emit(const SwitchingAccountsState()); emit(const SwitchingAccountsState());
logger.t( final redactedId = redactUserId(localUserId);
'AuthenticationCubit#switchAccount(): Trying to switch to user $localUserId...'); logger.fd(
'Trying to switch to user $redactedId...',
className: runtimeType.toString(),
methodName: 'switchAccount',
);
final globalSettings = final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
@@ -111,9 +126,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final userAccountBox = Hive.localUserAccountBox; final userAccountBox = Hive.localUserAccountBox;
if (!userAccountBox.containsKey(localUserId)) { if (!userAccountBox.containsKey(localUserId)) {
logger.w( logger.fw(
'AuthenticationCubit#switchAccount(): User $localUserId not yet registered. ' 'User $redactedId not yet registered. '
'This should never be the case!', 'This should never be the case!',
className: runtimeType.toString(),
methodName: 'switchAccount',
); );
return; return;
} }
@@ -124,8 +141,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final authenticated = await _localAuthService final authenticated = await _localAuthService
.authenticateLocalUser("Authenticate to switch your account."); .authenticateLocalUser("Authenticate to switch your account.");
if (!authenticated) { if (!authenticated) {
logger.w( logger.fw(
"AuthenticationCubit#switchAccount(): User could not be authenticated."); "User could not be authenticated.",
className: runtimeType.toString(),
methodName: 'switchAccount',
);
emit(VerifyIdentityState(userId: localUserId)); emit(VerifyIdentityState(userId: localUserId));
return; return;
} }
@@ -138,8 +158,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
HiveBoxes.localUserCredentials, (credentialsBox) async { HiveBoxes.localUserCredentials, (credentialsBox) async {
if (!credentialsBox.containsKey(localUserId)) { if (!credentialsBox.containsKey(localUserId)) {
await credentialsBox.close(); await credentialsBox.close();
logger.w( logger.fw(
"AuthenticationCubit#switchAccount(): Invalid authentication for $localUserId."); "Invalid authentication for $redactedId.",
className: runtimeType.toString(),
methodName: 'switchAccount',
);
return; return;
} }
final credentials = credentialsBox.get(localUserId); final credentials = credentialsBox.get(localUserId);
@@ -176,8 +199,12 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}) async { }) async {
assert(credentials.password != null && credentials.username != null); assert(credentials.password != null && credentials.username != null);
final localUserId = "${credentials.username}@$serverUrl"; final localUserId = "${credentials.username}@$serverUrl";
logger final redactedId = redactUserId(localUserId);
.d("AuthenticationCubit#addAccount(): Adding account $localUserId..."); logger.fd(
"Adding account $redactedId...",
className: runtimeType.toString(),
methodName: 'switchAccount',
);
final sessionManager = SessionManager([ final sessionManager = SessionManager([
LanguageHeaderInterceptor(locale), LanguageHeaderInterceptor(locale),
@@ -194,12 +221,16 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
} }
Future<void> removeAccount(String userId) async { Future<void> removeAccount(String userId) async {
logger final redactedId = redactUserId(userId);
.t("AuthenticationCubit#removeAccount(): Removing account $userId..."); logger.fd(
"Trying to remove account $redactedId...",
className: runtimeType.toString(),
methodName: 'removeAccount',
);
final userAccountBox = Hive.localUserAccountBox; final userAccountBox = Hive.localUserAccountBox;
final userAppStateBox = Hive.localUserAppStateBox; final userAppStateBox = Hive.localUserAppStateBox;
await FileService.clearUserData(userId: userId); await FileService.instance.clearUserData(userId: userId);
await userAccountBox.delete(userId); await userAccountBox.delete(userId);
await userAppStateBox.delete(userId); await userAppStateBox.delete(userId);
await withEncryptedBox<UserCredentials, void>( await withEncryptedBox<UserCredentials, void>(
@@ -213,15 +244,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
/// ///
Future<void> restoreSession([String? userId]) async { Future<void> restoreSession([String? userId]) async {
emit(const RestoringSessionState()); emit(const RestoringSessionState());
logger.t( logger.fd(
"AuthenticationCubit#restoreSessionState(): Trying to restore previous session..."); "Trying to restore previous session...",
className: runtimeType.toString(),
methodName: 'restoreSession',
);
final globalSettings = final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final restoreSessionForUser = userId ?? globalSettings.loggedInUserId; final restoreSessionForUser = userId ?? globalSettings.loggedInUserId;
// final localUserId = globalSettings.loggedInUserId; // final localUserId = globalSettings.loggedInUserId;
if (restoreSessionForUser == null) { if (restoreSessionForUser == null) {
logger.t( logger.fd(
"AuthenticationCubit#restoreSessionState(): There is nothing to restore."); "There is nothing to restore.",
className: runtimeType.toString(),
methodName: 'restoreSession',
);
final otherAccountsExist = Hive.localUserAccountBox.isNotEmpty; final otherAccountsExist = Hive.localUserAccountBox.isNotEmpty;
// If there is nothing to restore, we can quit here. // If there is nothing to restore, we can quit here.
emit( emit(
@@ -233,24 +270,36 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount); Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final localUserAccount = localUserAccountBox.get(restoreSessionForUser)!; final localUserAccount = localUserAccountBox.get(restoreSessionForUser)!;
if (localUserAccount.settings.isBiometricAuthenticationEnabled) { if (localUserAccount.settings.isBiometricAuthenticationEnabled) {
logger.t( logger.fd(
"AuthenticationCubit#restoreSessionState(): Verifying user identity..."); "Verifying user identity...",
className: runtimeType.toString(),
methodName: 'restoreSession',
);
final authenticationMesage = final authenticationMesage =
(await S.delegate.load(Locale(globalSettings.preferredLocaleSubtag))) (await S.delegate.load(Locale(globalSettings.preferredLocaleSubtag)))
.verifyYourIdentity; .verifyYourIdentity;
final localAuthSuccess = final localAuthSuccess =
await _localAuthService.authenticateLocalUser(authenticationMesage); await _localAuthService.authenticateLocalUser(authenticationMesage);
if (!localAuthSuccess) { if (!localAuthSuccess) {
logger.w( logger.fw(
"AuthenticationCubit#restoreSessionState(): Identity could not be verified."); "Identity could not be verified.",
className: runtimeType.toString(),
methodName: 'restoreSession',
);
emit(VerifyIdentityState(userId: restoreSessionForUser)); emit(VerifyIdentityState(userId: restoreSessionForUser));
return; return;
} }
logger.t( logger.fd(
"AuthenticationCubit#restoreSessionState(): Identity successfully verified."); "Identity successfully verified.",
className: runtimeType.toString(),
methodName: 'restoreSession',
);
} }
logger.t( logger.fd(
"AuthenticationCubit#restoreSessionState(): Reading encrypted credentials..."); "Reading encrypted credentials...",
className: runtimeType.toString(),
methodName: 'restoreSession',
);
final authentication = final authentication =
await withEncryptedBox<UserCredentials, UserCredentials>( await withEncryptedBox<UserCredentials, UserCredentials>(
HiveBoxes.localUserCredentials, (box) { HiveBoxes.localUserCredentials, (box) {
@@ -258,33 +307,48 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}); });
if (authentication == null) { if (authentication == null) {
logger.e( logger.fe(
"AuthenticationCubit#restoreSessionState(): Credentials could not be read!"); "Credentials could not be read!",
className: runtimeType.toString(),
methodName: 'restoreSession',
);
throw Exception( throw Exception(
"User should be authenticated but no authentication information was found.", "User should be authenticated but no authentication information was found.",
); );
} }
logger.t( logger.fd(
"AuthenticationCubit#restoreSessionState(): Credentials successfully retrieved."); "Credentials successfully retrieved.",
className: runtimeType.toString(),
methodName: 'restoreSession',
);
logger.t( logger.fd(
"AuthenticationCubit#restoreSessionState(): Updating security context..."); "Updating security context...",
className: runtimeType.toString(),
methodName: 'restoreSession',
);
_sessionManager.updateSettings( _sessionManager.updateSettings(
clientCertificate: authentication.clientCertificate, clientCertificate: authentication.clientCertificate,
authToken: authentication.token, authToken: authentication.token,
baseUrl: localUserAccount.serverUrl, baseUrl: localUserAccount.serverUrl,
); );
logger.t( logger.fd(
"AuthenticationCubit#restoreSessionState(): Security context successfully updated."); "Security context successfully updated.",
className: runtimeType.toString(),
methodName: 'restoreSession',
);
final isPaperlessServerReachable = final isPaperlessServerReachable =
await _connectivityService.isPaperlessServerReachable( await _connectivityService.isPaperlessServerReachable(
localUserAccount.serverUrl, localUserAccount.serverUrl,
authentication.clientCertificate, authentication.clientCertificate,
) == ) ==
ReachabilityStatus.reachable; ReachabilityStatus.reachable;
logger.t( logger.fd(
"AuthenticationCubit#restoreSessionState(): Trying to update remote paperless user..."); "Trying to update remote paperless user...",
className: runtimeType.toString(),
methodName: 'restoreSession',
);
if (isPaperlessServerReachable) { if (isPaperlessServerReachable) {
final apiVersion = await _getApiVersion(_sessionManager.client); final apiVersion = await _getApiVersion(_sessionManager.client);
await _updateRemoteUser( await _updateRemoteUser(
@@ -292,51 +356,83 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
localUserAccount, localUserAccount,
apiVersion, apiVersion,
); );
logger.t( logger.fd(
"AuthenticationCubit#restoreSessionState(): Successfully updated remote paperless user."); "Successfully updated remote paperless user.",
className: runtimeType.toString(),
methodName: 'restoreSession',
);
} else { } else {
logger.w( logger.fw(
"AuthenticationCubit#restoreSessionState(): Could not update remote paperless user. Server could not be reached. The app might behave unexpected!"); "Could not update remote paperless user - "
"Server could not be reached. The app might behave unexpected!",
className: runtimeType.toString(),
methodName: 'restoreSession',
);
} }
globalSettings.loggedInUserId = restoreSessionForUser; globalSettings.loggedInUserId = restoreSessionForUser;
await globalSettings.save(); await globalSettings.save();
emit(AuthenticatedState(localUserId: restoreSessionForUser)); emit(AuthenticatedState(localUserId: restoreSessionForUser));
logger.t( logger.fd(
"AuthenticationCubit#restoreSessionState(): Previous session successfully restored."); "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()); emit(const LoggingOutState());
final globalSettings = Hive.globalSettingsBox.getValue()!; final globalSettings = Hive.globalSettingsBox.getValue()!;
final userId = globalSettings.loggedInUserId!; final userId = globalSettings.loggedInUserId!;
logger.t( final redactedId = redactUserId(userId);
"AuthenticationCubit#logout(): Logging out current user ($userId)...");
logger.fd(
"Logging out $redactedId...",
className: runtimeType.toString(),
methodName: 'logout',
);
await _resetExternalState(); await _resetExternalState();
await _notificationService.cancelUserNotifications(userId); await _notificationService.cancelUserNotifications(userId);
final otherAccountsExist = Hive.localUserAccountBox.length > 1; final otherAccountsExist = Hive.localUserAccountBox.length > 1;
emit(UnauthenticatedState(redirectToAccountSelection: otherAccountsExist)); emit(UnauthenticatedState(redirectToAccountSelection: otherAccountsExist));
if (removeAccount) { if (shouldRemoveAccount) {
await this.removeAccount(userId); await removeAccount(userId);
} }
globalSettings.loggedInUserId = null; globalSettings.loggedInUserId = null;
await globalSettings.save(); 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 { Future<void> _resetExternalState() async {
logger.t( logger.fd(
"AuthenticationCubit#_resetExternalState(): Resetting security context..."); "Resetting security context...",
className: runtimeType.toString(),
methodName: '_resetExternalState',
);
_sessionManager.resetSettings(); _sessionManager.resetSettings();
logger.t( logger.fd(
"AuthenticationCubit#_resetExternalState(): Security context reset."); "Security context reset.",
logger.t( className: runtimeType.toString(),
"AuthenticationCubit#_resetExternalState(): Clearing local state..."); methodName: '_resetExternalState',
);
logger.fd(
"Clearing local state...",
className: runtimeType.toString(),
methodName: '_resetExternalState',
);
await HydratedBloc.storage.clear(); await HydratedBloc.storage.clear();
logger.t("AuthenticationCubit#_resetExternalState(): Local state cleard."); logger.fd(
"Local state cleard.",
className: runtimeType.toString(),
methodName: '_resetExternalState',
);
} }
Future<int> _addUser( Future<int> _addUser(
@@ -350,8 +446,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
_FutureVoidCallback? onFetchUserInformation, _FutureVoidCallback? onFetchUserInformation,
}) async { }) async {
assert(credentials.username != null && credentials.password != null); assert(credentials.username != null && credentials.password != null);
logger final redactedId = redactUserId(localUserId);
.t("AuthenticationCubit#_addUser(): Adding new user $localUserId....");
logger.fd(
"Adding new user $redactedId..",
className: runtimeType.toString(),
methodName: '_addUser',
);
sessionManager.updateSettings( sessionManager.updateSettings(
baseUrl: serverUrl, baseUrl: serverUrl,
@@ -360,8 +461,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final authApi = _apiFactory.createAuthenticationApi(sessionManager.client); final authApi = _apiFactory.createAuthenticationApi(sessionManager.client);
logger.t( logger.fd(
"AuthenticationCubit#_addUser(): Fetching bearer token from the server..."); "Fetching bearer token from the server...",
className: runtimeType.toString(),
methodName: '_addUser',
);
await onPerformLogin?.call(); await onPerformLogin?.call();
@@ -370,8 +474,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
password: credentials.password!, password: credentials.password!,
); );
logger.t( logger.fd(
"AuthenticationCubit#_addUser(): Bearer token successfully retrieved."); "Bearer token successfully retrieved.",
className: runtimeType.toString(),
methodName: '_addUser',
);
sessionManager.updateSettings( sessionManager.updateSettings(
baseUrl: serverUrl, baseUrl: serverUrl,
@@ -385,14 +492,20 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState); Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
if (userAccountBox.containsKey(localUserId)) { if (userAccountBox.containsKey(localUserId)) {
logger.w( logger.fw(
"AuthenticationCubit#_addUser(): The user $localUserId already exists."); "The user $redactedId already exists.",
className: runtimeType.toString(),
methodName: '_addUser',
);
throw InfoMessageException(code: ErrorCode.userAlreadyExists); throw InfoMessageException(code: ErrorCode.userAlreadyExists);
} }
await onFetchUserInformation?.call(); await onFetchUserInformation?.call();
final apiVersion = await _getApiVersion(sessionManager.client); final apiVersion = await _getApiVersion(sessionManager.client);
logger.t( logger.fd(
"AuthenticationCubit#_addUser(): Trying to fetch remote paperless user for $localUserId."); "Trying to fetch remote paperless user for $redactedId.",
className: runtimeType.toString(),
methodName: '_addUser',
);
late UserModel serverUser; late UserModel serverUser;
try { try {
@@ -403,19 +516,27 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
) )
.findCurrentUser(); .findCurrentUser();
} on DioException catch (error, stackTrace) { } on DioException catch (error, stackTrace) {
logger.e( logger.fe(
"AuthenticationCubit#_addUser(): An error occurred while fetching the remote paperless user.", "An error occurred while fetching the remote paperless user.",
className: runtimeType.toString(),
methodName: '_addUser',
error: error, error: error,
stackTrace: stackTrace, stackTrace: stackTrace,
); );
rethrow; rethrow;
} }
logger.t( logger.fd(
"AuthenticationCubit#_addUser(): Remote paperless user successfully fetched."); "Remote paperless user successfully fetched.",
className: runtimeType.toString(),
methodName: '_addUser',
);
logger.t( logger.fd(
"AuthenticationCubit#_addUser(): Persisting user account information..."); "Persisting user account information...",
className: runtimeType.toString(),
methodName: '_addUser',
);
await onPersistLocalUserData?.call(); await onPersistLocalUserData?.call();
// Create user account // Create user account
@@ -429,20 +550,33 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
apiVersion: apiVersion, apiVersion: apiVersion,
), ),
); );
logger.t( logger.fd(
"AuthenticationCubit#_addUser(): User account information successfully persisted."); "User account information successfully persisted.",
logger.t("AuthenticationCubit#_addUser(): Persisting user app state..."); className: runtimeType.toString(),
methodName: '_addUser',
);
logger.fd(
"Persisting user app state...",
className: runtimeType.toString(),
methodName: '_addUser',
);
// Create user state // Create user state
await userStateBox.put( await userStateBox.put(
localUserId, localUserId,
LocalUserAppState(userId: localUserId), LocalUserAppState(userId: localUserId),
); );
logger.t( logger.fd(
"AuthenticationCubit#_addUser(): User state successfully persisted."); "User state successfully persisted.",
className: runtimeType.toString(),
methodName: '_addUser',
);
// Save credentials in encrypted box // Save credentials in encrypted box
await withEncryptedBox(HiveBoxes.localUserCredentials, (box) async { await withEncryptedBox(HiveBoxes.localUserCredentials, (box) async {
logger.t( logger.fd(
"AuthenticationCubit#_addUser(): Saving user credentials inside encrypted storage..."); "Saving user credentials inside encrypted storage...",
className: runtimeType.toString(),
methodName: '_addUser',
);
await box.put( await box.put(
localUserId, localUserId,
@@ -451,12 +585,20 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
clientCertificate: clientCert, clientCertificate: clientCert,
), ),
); );
logger.t( logger.fd(
"AuthenticationCubit#_addUser(): User credentials successfully saved."); "User credentials successfully saved.",
className: runtimeType.toString(),
methodName: '_addUser',
);
}); });
final hostsBox = Hive.box<String>(HiveBoxes.hosts); final hostsBox = Hive.box<String>(HiveBoxes.hosts);
if (!hostsBox.values.contains(serverUrl)) { if (!hostsBox.values.contains(serverUrl)) {
await hostsBox.add(serverUrl); await hostsBox.add(serverUrl);
logger.fd(
"Added new url to list of hosts.",
className: runtimeType.toString(),
methodName: '_addUser',
);
} }
return serverUser.id; return serverUser.id;
@@ -467,8 +609,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
Duration? timeout, Duration? timeout,
int defaultValue = 2, int defaultValue = 2,
}) async { }) async {
logger.t( logger.fd(
"AuthenticationCubit#_getApiVersion(): Trying to fetch API version..."); "Trying to fetch API version...",
className: runtimeType.toString(),
methodName: '_getApiVersion',
);
try { try {
final response = await dio.get( final response = await dio.get(
"/api/", "/api/",
@@ -478,13 +623,19 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
); );
final apiVersion = final apiVersion =
int.parse(response.headers.value('x-api-version') ?? "3"); int.parse(response.headers.value('x-api-version') ?? "3");
logger.t( logger.fd(
"AuthenticationCubit#_getApiVersion(): Successfully retrieved API version ($apiVersion)."); "Successfully retrieved API version ($apiVersion).",
className: runtimeType.toString(),
methodName: '_getApiVersion',
);
return apiVersion; return apiVersion;
} on DioException catch (_) { } on DioException catch (_) {
logger.w( logger.fw(
"AuthenticationCubit#_getApiVersion(): Could not retrieve API version."); "Could not retrieve API version, using default ($defaultValue).",
className: runtimeType.toString(),
methodName: '_getApiVersion',
);
return defaultValue; return defaultValue;
} }
} }
@@ -495,18 +646,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
LocalUserAccount localUserAccount, LocalUserAccount localUserAccount,
int apiVersion, int apiVersion,
) async { ) async {
logger.t( logger.fd(
"AuthenticationCubit#_updateRemoteUser(): Trying to update remote user object..."); "Trying to update remote user object...",
className: runtimeType.toString(),
methodName: '_updateRemoteUser',
);
final updatedPaperlessUser = await _apiFactory final updatedPaperlessUser = await _apiFactory
.createUserApi( .createUserApi(sessionManager.client, apiVersion: apiVersion)
sessionManager.client,
apiVersion: apiVersion,
)
.findCurrentUser(); .findCurrentUser();
localUserAccount.paperlessUser = updatedPaperlessUser; localUserAccount.paperlessUser = updatedPaperlessUser;
await localUserAccount.save(); await localUserAccount.save();
logger.t( logger.fd(
"AuthenticationCubit#_updateRemoteUser(): Successfully updated remote user object."); "Successfully updated remote user object.",
className: runtimeType.toString(),
methodName: '_updateRemoteUser',
);
} }
} }

View File

@@ -18,43 +18,25 @@ class _ClearCacheSettingState extends State<ClearCacheSetting> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
title: Text(S.of(context)!.clearCache), title: Text(S.of(context)!.clearCache),
subtitle: FutureBuilder<String>( subtitle: FutureBuilder<int>(
future: FileService.temporaryDirectory.then(_dirSize), future: FileService.instance
.getDirSizeInBytes(FileService.instance.temporaryDirectory),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return Text(S.of(context)!.calculatingDots); 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 { onTap: () async {
final dir = await FileService.temporaryDirectory; final freedBytes = await FileService.instance
final deletedSize = await _dirSize(dir); .clearDirectoryContent(PaperlessDirectoryType.temporary);
await dir.delete(recursive: true);
showSnackBar( showSnackBar(
context, 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);
}

View File

@@ -32,7 +32,7 @@ class ConsumptionChangeNotifier extends ChangeNotifier {
return []; return [];
} }
final consumptionDirectory = final consumptionDirectory =
await FileService.getConsumptionDirectory(userId: userId); await FileService.instance.getConsumptionDirectory(userId: userId);
final List<File> localFiles = []; final List<File> localFiles = [];
for (final file in files) { for (final file in files) {
if (!file.path.startsWith(consumptionDirectory.path)) { if (!file.path.startsWith(consumptionDirectory.path)) {
@@ -53,7 +53,7 @@ class ConsumptionChangeNotifier extends ChangeNotifier {
required String userId, required String userId,
}) async { }) async {
final consumptionDirectory = final consumptionDirectory =
await FileService.getConsumptionDirectory(userId: userId); await FileService.instance.getConsumptionDirectory(userId: userId);
if (file.path.startsWith(consumptionDirectory.path)) { if (file.path.startsWith(consumptionDirectory.path)) {
await file.delete(); await file.delete();
} }
@@ -70,8 +70,8 @@ class ConsumptionChangeNotifier extends ChangeNotifier {
} }
Future<List<File>> _getCurrentFiles(String userId) async { Future<List<File>> _getCurrentFiles(String userId) async {
final directory = await FileService.getConsumptionDirectory(userId: userId); final directory =
final files = await FileService.getAllFiles(directory); await FileService.instance.getConsumptionDirectory(userId: userId);
return files; return await FileService.instance.getAllFiles(directory);
} }
} }

View File

@@ -85,7 +85,6 @@ class _EventListenerShellState extends State<EventListenerShell>
if (!currentUser.paperlessUser.canViewInbox || _inboxTimer != null) { if (!currentUser.paperlessUser.canViewInbox || _inboxTimer != null) {
return; return;
} }
cubit.refreshItemsInInboxCount(false);
_inboxTimer = Timer.periodic(30.seconds, (_) { _inboxTimer = Timer.periodic(30.seconds, (_) {
cubit.refreshItemsInInboxCount(false); cubit.refreshItemsInInboxCount(false);
}); });

View File

@@ -1001,5 +1001,13 @@
"@discardChangesWarning": { "@discardChangesWarning": {
"description": "Warning message shown when the user tries to close a route without saving the changes." "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}"
} }

View File

@@ -1001,5 +1001,13 @@
"@discardChangesWarning": { "@discardChangesWarning": {
"description": "Warning message shown when the user tries to close a route without saving the changes." "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}"
} }

View File

@@ -1001,5 +1001,13 @@
"@discardChangesWarning": { "@discardChangesWarning": {
"description": "Warning message shown when the user tries to close a route without saving the changes." "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"
} }

View File

@@ -1001,5 +1001,13 @@
"@discardChangesWarning": { "@discardChangesWarning": {
"description": "Warning message shown when the user tries to close a route without saving the changes." "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}"
} }

View File

@@ -1001,5 +1001,13 @@
"@discardChangesWarning": { "@discardChangesWarning": {
"description": "Warning message shown when the user tries to close a route without saving the changes." "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}"
} }

View File

@@ -1001,5 +1001,13 @@
"@discardChangesWarning": { "@discardChangesWarning": {
"description": "Warning message shown when the user tries to close a route without saving the changes." "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}"
} }

View File

@@ -1001,5 +1001,13 @@
"@discardChangesWarning": { "@discardChangesWarning": {
"description": "Warning message shown when the user tries to close a route without saving the changes." "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}"
} }

View File

@@ -1001,5 +1001,13 @@
"@discardChangesWarning": { "@discardChangesWarning": {
"description": "Warning message shown when the user tries to close a route without saving the changes." "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}"
} }

View File

@@ -1001,5 +1001,13 @@
"@discardChangesWarning": { "@discardChangesWarning": {
"description": "Warning message shown when the user tries to close a route without saving the changes." "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}"
} }

View File

@@ -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.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart';
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/core/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/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
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/cubit/authentication_cubit.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart'; import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_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/branches/landing_route.dart';
import 'package:paperless_mobile/routes/typed/shells/authenticated_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/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/changelog_route.dart';
import 'package:paperless_mobile/routes/typed/top_level/logging_out_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'; import 'package:paperless_mobile/routes/typed/top_level/login_route.dart';
@@ -85,7 +89,11 @@ Future<void> performMigrations() async {
final requiresMigrationForCurrentVersion = final requiresMigrationForCurrentVersion =
!performedMigrations.contains(currentVersion); !performedMigrations.contains(currentVersion);
if (requiresMigrationForCurrentVersion) { if (requiresMigrationForCurrentVersion) {
logger.t("Applying migration scripts for version $currentVersion"); logger.fd(
"Applying migration scripts for version $currentVersion",
className: "",
methodName: "performMigrations",
);
await migrationProcedure(); await migrationProcedure();
await sp.setStringList( await sp.setStringList(
'performed_migrations', 'performed_migrations',
@@ -115,7 +123,15 @@ Future<void> _initHive() async {
void main() async { void main() async {
runZonedGuarded(() async { runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await FileService.instance.initialize();
logger = l.Logger(
output: MirroredFileOutput(),
printer: FormattedPrinter(),
level: l.Level.trace,
);
Paint.enableDithering = true; Paint.enableDithering = true;
// if (kDebugMode) { // if (kDebugMode) {
// // URL: http://localhost:3131 // // URL: http://localhost:3131
// // Login: admin:test // // Login: admin:test
@@ -128,12 +144,6 @@ void main() async {
// .start(); // .start();
// } // }
logger = l.Logger(
output: MirroredFileOutput(),
printer: SpringBootLikePrinter(),
level: l.Level.trace,
);
packageInfo = await PackageInfo.fromPlatform(); packageInfo = await PackageInfo.fromPlatform();
if (Platform.isAndroid) { if (Platform.isAndroid) {
@@ -168,7 +178,6 @@ void main() async {
); );
// Manages security context, required for self signed client certificates // Manages security context, required for self signed client certificates
final sessionManager = SessionManager([ final sessionManager = SessionManager([
languageHeaderInterceptor,
PrettyDioLogger( PrettyDioLogger(
compact: true, compact: true,
responseBody: false, responseBody: false,
@@ -178,6 +187,7 @@ void main() async {
requestHeader: false, requestHeader: false,
logPrint: (object) => logger.t, logPrint: (object) => logger.t,
), ),
languageHeaderInterceptor,
]); ]);
// Initialize Blocs/Cubits // 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. // 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(), PaperlessApiException e => e.details ?? error.toString(),
ServerMessageException e => e.message, 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 = final DisplayMode mostOptimalMode =
sameResolution.isNotEmpty ? sameResolution.first : active; 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); await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
} }
@@ -336,6 +351,7 @@ class _GoRouterShellState extends State<GoRouterShell> {
$loggingOutRoute, $loggingOutRoute,
$addAccountRoute, $addAccountRoute,
$changelogRoute, $changelogRoute,
$appLogsRoute,
$authenticatedRoute, $authenticatedRoute,
], ],
), ),

View 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),
),
);
}
}

View File

@@ -6,7 +6,7 @@ import 'package:paperless_mobile/routes/utils/dialog_page.dart';
part 'changelog_route.g.dart'; part 'changelog_route.g.dart';
@TypedGoRoute<ChangelogRoute>(path: '/changelogs)') @TypedGoRoute<ChangelogRoute>(path: '/changelogs')
class ChangelogRoute extends GoRouteData { class ChangelogRoute extends GoRouteData {
static final $parentNavigatorKey = rootNavigatorKey; static final $parentNavigatorKey = rootNavigatorKey;
@override @override

View File

@@ -1657,7 +1657,7 @@ packages:
source: hosted source: hosted
version: "0.3.1" version: "0.3.1"
synchronized: synchronized:
dependency: transitive dependency: "direct main"
description: description:
name: synchronized name: synchronized
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"

View File

@@ -99,6 +99,7 @@ dependencies:
shared_preferences: ^2.2.1 shared_preferences: ^2.2.1
flutter_markdown: ^0.6.18 flutter_markdown: ^0.6.18
logger: ^2.0.2+1 logger: ^2.0.2+1
synchronized: ^3.1.0
# camerawesome: ^2.0.0-dev.1 # camerawesome: ^2.0.0-dev.1
dependency_overrides: dependency_overrides: