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