mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2026-01-31 04:25:02 -06:00
Merge pull request #289 from astubenbord/feat/better-logging
Better Logging
This commit is contained in:
@@ -72,6 +72,10 @@ android {
|
||||
release {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
* Neu: App-Logs werden in Dateien geschrieben und können auch direkt in der App eingesehen werden
|
||||
* Optimierung der Datums-Eingabe durch neues Eingabemaske
|
||||
* Schneller Wechsel zwischen Dokumenten-PDF Ansicht und Bearbeitungsmaske
|
||||
* Kleinere Visuelle Anpassungen und Bugfixes
|
||||
@@ -0,0 +1,4 @@
|
||||
* New: App-Logs are written to local files and can also be viewed in-app
|
||||
* New and optimized date input fields
|
||||
* Quickly switch between editing your document and a PDF-View
|
||||
* Minor visual changes and bug fixes
|
||||
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
|
||||
class ThemeModeAdapter extends TypeAdapter<ThemeMode> {
|
||||
@override
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/custom_adapters/theme_mode_adapter.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/custom_adapters/theme_mode_adapter.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_credentials.dart';
|
||||
+1
-1
@@ -4,7 +4,7 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/file_download_type.dart';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
|
||||
|
||||
part 'local_user_account.g.dart';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
|
||||
part 'local_user_settings.g.dart';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
|
||||
part 'user_credentials.g.dart';
|
||||
|
||||
@@ -35,6 +35,10 @@ extension DateHelpers on DateTime {
|
||||
yesterday.month == month &&
|
||||
yesterday.year == year;
|
||||
}
|
||||
|
||||
bool isOnSameDayAs(DateTime other) {
|
||||
return other.day == day && other.month == month && other.year == year;
|
||||
}
|
||||
}
|
||||
|
||||
extension StringNormalizer on String {
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
|
||||
extension DocumentModelIterableExtension on Iterable<DocumentModel> {
|
||||
Iterable<int> get ids => map((e) => e.id);
|
||||
|
||||
Iterable<DocumentModel> withDocumentreplaced(DocumentModel document) {
|
||||
return map((e) => e.id == document.id ? document : e);
|
||||
}
|
||||
|
||||
bool containsDocument(DocumentModel document) {
|
||||
return ids.contains(document.id);
|
||||
}
|
||||
|
||||
Iterable<DocumentModel> withDocumentRemoved(DocumentModel document) {
|
||||
return whereNot((element) => element.id == document.id);
|
||||
}
|
||||
}
|
||||
|
||||
extension SessionAwareDownloadIdExtension on DocumentModel {
|
||||
String buildThumbnailUrl(BuildContext context) =>
|
||||
context.read<PaperlessDocumentsApi>().getThumbnailUrl(id);
|
||||
}
|
||||
@@ -38,6 +38,8 @@ class DioHttpErrorInterceptor extends Interceptor {
|
||||
const PaperlessApiException(ErrorCode.missingClientCertificate),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
handler.reject(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:rxdart/subjects.dart';
|
||||
|
||||
@@ -17,12 +16,10 @@ class DocumentChangedNotifier {
|
||||
Stream<DocumentModel> get $deleted => _deleted.asBroadcastStream();
|
||||
|
||||
void notifyUpdated(DocumentModel updated) {
|
||||
debugPrint("Notifying updated document ${updated.id}");
|
||||
_updated.add(updated);
|
||||
}
|
||||
|
||||
void notifyDeleted(DocumentModel deleted) {
|
||||
debugPrint("Notifying deleted document ${deleted.id}");
|
||||
_deleted.add(deleted);
|
||||
}
|
||||
|
||||
@@ -30,14 +27,15 @@ class DocumentChangedNotifier {
|
||||
Object subscriber, {
|
||||
DocumentChangedCallback? onUpdated,
|
||||
DocumentChangedCallback? onDeleted,
|
||||
Iterable<int>? ids,
|
||||
}) {
|
||||
_subscribers.putIfAbsent(
|
||||
subscriber,
|
||||
() => [
|
||||
_updated.listen((value) {
|
||||
_updated.where((doc) => ids?.contains(doc.id) ?? true).listen((value) {
|
||||
onUpdated?.call(value);
|
||||
}),
|
||||
_deleted.listen((value) {
|
||||
_deleted.where((doc) => ids?.contains(doc.id) ?? true).listen((value) {
|
||||
onDeleted?.call(value);
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository_state.dart';
|
||||
import 'package:paperless_mobile/core/repository/persistent_repository.dart';
|
||||
@@ -11,19 +10,12 @@ class LabelRepository extends PersistentRepository<LabelRepositoryState> {
|
||||
LabelRepository(this._api) : super(const LabelRepositoryState());
|
||||
|
||||
Future<void> initialize() async {
|
||||
debugPrint("[LabelRepository] initialize() called.");
|
||||
try {
|
||||
await Future.wait([
|
||||
findAllCorrespondents(),
|
||||
findAllDocumentTypes(),
|
||||
findAllStoragePaths(),
|
||||
findAllTags(),
|
||||
]);
|
||||
} catch (error, stackTrace) {
|
||||
debugPrint(
|
||||
"[LabelRepository] An error occurred in initialize(): ${error.toString()}");
|
||||
debugPrintStack(stackTrace: stackTrace);
|
||||
}
|
||||
await Future.wait([
|
||||
findAllCorrespondents(),
|
||||
findAllDocumentTypes(),
|
||||
findAllStoragePaths(),
|
||||
findAllTags(),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<Tag> createTag(Tag object) async {
|
||||
@@ -95,9 +87,7 @@ class LabelRepository extends PersistentRepository<LabelRepositoryState> {
|
||||
|
||||
Future<Iterable<Correspondent>> findAllCorrespondents(
|
||||
[Iterable<int>? ids]) async {
|
||||
debugPrint("Loading correspondents...");
|
||||
final correspondents = await _api.getCorrespondents(ids);
|
||||
debugPrint("${correspondents.length} correspondents successfully loaded.");
|
||||
final updatedState = {
|
||||
...state.correspondents,
|
||||
}..addAll({for (var element in correspondents) element.id!: element});
|
||||
|
||||
@@ -39,14 +39,6 @@ class SessionManager extends ValueNotifier<Dio> {
|
||||
DioUnauthorizedInterceptor(),
|
||||
DioHttpErrorInterceptor(),
|
||||
DioOfflineInterceptor(),
|
||||
PrettyDioLogger(
|
||||
compact: true,
|
||||
responseBody: false,
|
||||
responseHeader: false,
|
||||
request: false,
|
||||
requestBody: false,
|
||||
requestHeader: false,
|
||||
),
|
||||
RetryOnConnectionChangeInterceptor(dio: dio)
|
||||
]);
|
||||
return dio;
|
||||
|
||||
@@ -1,125 +1,260 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:paperless_mobile/features/logging/data/logger.dart';
|
||||
import 'package:paperless_mobile/features/logging/utils/redaction_utils.dart';
|
||||
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class FileService {
|
||||
const FileService._();
|
||||
FileService._();
|
||||
|
||||
static Future<File> saveToFile(
|
||||
static FileService? _singleton;
|
||||
|
||||
late final Directory _logDirectory;
|
||||
late final Directory _temporaryDirectory;
|
||||
late final Directory _documentsDirectory;
|
||||
late final Directory _downloadsDirectory;
|
||||
late final Directory _uploadDirectory;
|
||||
late final Directory _temporaryScansDirectory;
|
||||
|
||||
Directory get logDirectory => _logDirectory;
|
||||
Directory get temporaryDirectory => _temporaryDirectory;
|
||||
Directory get documentsDirectory => _documentsDirectory;
|
||||
Directory get downloadsDirectory => _downloadsDirectory;
|
||||
Directory get uploadDirectory => _uploadDirectory;
|
||||
Directory get temporaryScansDirectory => _temporaryScansDirectory;
|
||||
|
||||
Future<void> initialize() async {
|
||||
try {
|
||||
await _initTemporaryDirectory();
|
||||
await _initTemporaryScansDirectory();
|
||||
await _initUploadDirectory();
|
||||
await _initLogDirectory();
|
||||
await _initDownloadsDirectory();
|
||||
await _initializeDocumentsDirectory();
|
||||
} catch (error, stackTrace) {
|
||||
debugPrint("Could not initialize directories.");
|
||||
debugPrint(error.toString());
|
||||
debugPrintStack(stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// Make sure to call and await initialize before accessing any of the instance members.
|
||||
static FileService get instance {
|
||||
_singleton ??= FileService._();
|
||||
return _singleton!;
|
||||
}
|
||||
|
||||
Future<File> saveToFile(
|
||||
Uint8List bytes,
|
||||
String filename,
|
||||
) async {
|
||||
final dir = await documentsDirectory;
|
||||
File file = File("${dir.path}/$filename");
|
||||
File file = File(p.join(_logDirectory.path, filename));
|
||||
logger.fd(
|
||||
"Writing bytes to file $filename",
|
||||
methodName: 'saveToFile',
|
||||
className: runtimeType.toString(),
|
||||
);
|
||||
return file..writeAsBytes(bytes);
|
||||
}
|
||||
|
||||
static Future<Directory?> getDirectory(PaperlessDirectoryType type) {
|
||||
Directory getDirectory(PaperlessDirectoryType type) {
|
||||
return switch (type) {
|
||||
PaperlessDirectoryType.documents => documentsDirectory,
|
||||
PaperlessDirectoryType.temporary => temporaryDirectory,
|
||||
PaperlessDirectoryType.scans => temporaryScansDirectory,
|
||||
PaperlessDirectoryType.download => downloadsDirectory,
|
||||
PaperlessDirectoryType.upload => uploadDirectory,
|
||||
PaperlessDirectoryType.documents => _documentsDirectory,
|
||||
PaperlessDirectoryType.temporary => _temporaryDirectory,
|
||||
PaperlessDirectoryType.scans => _temporaryScansDirectory,
|
||||
PaperlessDirectoryType.download => _downloadsDirectory,
|
||||
PaperlessDirectoryType.upload => _uploadDirectory,
|
||||
PaperlessDirectoryType.logs => _logDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
static Future<File> allocateTemporaryFile(
|
||||
///
|
||||
/// Returns a [File] pointing to a temporary file in the directory specified by [type].
|
||||
/// If [create] is true, the file will be created.
|
||||
/// If [fileName] is left blank, a random UUID will be generated.
|
||||
///
|
||||
Future<File> allocateTemporaryFile(
|
||||
PaperlessDirectoryType type, {
|
||||
required String extension,
|
||||
String? fileName,
|
||||
bool create = false,
|
||||
}) async {
|
||||
final dir = await getDirectory(type);
|
||||
final _fileName = (fileName ?? const Uuid().v1()) + '.$extension';
|
||||
return File('${dir?.path}/$_fileName');
|
||||
}
|
||||
|
||||
static Future<Directory> get temporaryDirectory => getTemporaryDirectory();
|
||||
|
||||
static Future<Directory> get documentsDirectory async {
|
||||
if (Platform.isAndroid) {
|
||||
return (await getExternalStorageDirectories(
|
||||
type: StorageDirectory.documents,
|
||||
))!
|
||||
.first;
|
||||
} else if (Platform.isIOS) {
|
||||
final dir = await getApplicationDocumentsDirectory()
|
||||
.then((dir) => Directory('${dir.path}/documents'));
|
||||
return dir.create(recursive: true);
|
||||
} else {
|
||||
throw UnsupportedError("Platform not supported.");
|
||||
final dir = getDirectory(type);
|
||||
final filename = (fileName ?? const Uuid().v1()) + '.$extension';
|
||||
final file = File(p.join(dir.path, filename));
|
||||
if (create) {
|
||||
await file.create(recursive: true);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
static Future<Directory> get downloadsDirectory async {
|
||||
if (Platform.isAndroid) {
|
||||
Directory directory = Directory('/storage/emulated/0/Download');
|
||||
if (!directory.existsSync()) {
|
||||
final downloadsDir = await getExternalStorageDirectories(
|
||||
type: StorageDirectory.downloads,
|
||||
);
|
||||
directory = downloadsDir!.first;
|
||||
}
|
||||
return directory;
|
||||
} else if (Platform.isIOS) {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final dir = Directory('${appDir.path}/downloads');
|
||||
return dir.create(recursive: true);
|
||||
} else {
|
||||
throw UnsupportedError("Platform not supported.");
|
||||
}
|
||||
Future<Directory> getConsumptionDirectory({required String userId}) async {
|
||||
return Directory(p.join(_uploadDirectory.path, userId))
|
||||
.create(recursive: true);
|
||||
}
|
||||
|
||||
static Future<Directory> get uploadDirectory async {
|
||||
final dir = await getApplicationDocumentsDirectory()
|
||||
.then((dir) => Directory('${dir.path}/upload'));
|
||||
return dir.create(recursive: true);
|
||||
}
|
||||
Future<void> clearUserData({required String userId}) async {
|
||||
final redactedId = redactUserId(userId);
|
||||
logger.fd(
|
||||
"Clearing data for user $redactedId...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
|
||||
static Future<Directory> 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 {
|
||||
final scanDir = await temporaryScansDirectory;
|
||||
final tempDir = await temporaryDirectory;
|
||||
final scanDirSize =
|
||||
formatBytes(await getDirSizeInBytes(_temporaryScansDirectory));
|
||||
final tempDirSize =
|
||||
formatBytes(await getDirSizeInBytes(_temporaryDirectory));
|
||||
final consumptionDir = await getConsumptionDirectory(userId: userId);
|
||||
await scanDir.delete(recursive: true);
|
||||
await tempDir.delete(recursive: true);
|
||||
final consumptionDirSize =
|
||||
formatBytes(await getDirSizeInBytes(consumptionDir));
|
||||
|
||||
logger.ft(
|
||||
"Removing scans...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
await _temporaryScansDirectory.delete(recursive: true);
|
||||
logger.ft(
|
||||
"Removed $scanDirSize...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
logger.ft(
|
||||
"Removing temporary files and cache content...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
|
||||
await _temporaryDirectory.delete(recursive: true);
|
||||
logger.ft(
|
||||
"Removed $tempDirSize...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
|
||||
logger.ft(
|
||||
"Removing files waiting for consumption...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
await consumptionDir.delete(recursive: true);
|
||||
}
|
||||
|
||||
static Future<void> clearDirectoryContent(PaperlessDirectoryType type) async {
|
||||
final dir = await getDirectory(type);
|
||||
|
||||
if (dir == null || !(await dir.exists())) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.wait(
|
||||
dir.listSync().map((item) => item.delete(recursive: true)),
|
||||
logger.ft(
|
||||
"Removed $consumptionDirSize...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<File>> getAllFiles(Directory directory) {
|
||||
Future<int> clearDirectoryContent(
|
||||
PaperlessDirectoryType type, {
|
||||
bool filesOnly = false,
|
||||
}) async {
|
||||
final dir = getDirectory(type);
|
||||
final dirSize = await getDirSizeInBytes(dir);
|
||||
if (!await dir.exists()) {
|
||||
return 0;
|
||||
}
|
||||
final streamedEntities = filesOnly
|
||||
? dir.list().whereType<File>().cast<FileSystemEntity>()
|
||||
: dir.list();
|
||||
|
||||
final entities = await streamedEntities.toList();
|
||||
await Future.wait([
|
||||
for (var entity in entities) entity.delete(recursive: !filesOnly),
|
||||
]);
|
||||
return dirSize;
|
||||
}
|
||||
|
||||
Future<List<File>> getAllFiles(Directory directory) {
|
||||
return directory.list().whereType<File>().toList();
|
||||
}
|
||||
|
||||
static Future<List<Directory>> getAllSubdirectories(Directory directory) {
|
||||
Future<List<Directory>> getAllSubdirectories(Directory directory) {
|
||||
return directory.list().whereType<Directory>().toList();
|
||||
}
|
||||
|
||||
Future<int> getDirSizeInBytes(Directory dir) async {
|
||||
return dir
|
||||
.list(recursive: true)
|
||||
.fold(0, (previous, element) => previous + element.statSync().size);
|
||||
}
|
||||
|
||||
Future<void> _initTemporaryDirectory() async {
|
||||
_temporaryDirectory = await getTemporaryDirectory();
|
||||
}
|
||||
|
||||
Future<void> _initializeDocumentsDirectory() async {
|
||||
if (Platform.isAndroid) {
|
||||
final dirs =
|
||||
await getExternalStorageDirectories(type: StorageDirectory.documents);
|
||||
_documentsDirectory = dirs!.first;
|
||||
return;
|
||||
} else if (Platform.isIOS) {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
_documentsDirectory = await Directory(p.join(dir.path, 'documents'))
|
||||
.create(recursive: true);
|
||||
return;
|
||||
} else {
|
||||
throw UnsupportedError("Platform not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initLogDirectory() async {
|
||||
if (Platform.isAndroid) {
|
||||
_logDirectory =
|
||||
await getExternalStorageDirectories(type: StorageDirectory.documents)
|
||||
.then((directory) async =>
|
||||
directory?.firstOrNull ??
|
||||
await getApplicationDocumentsDirectory())
|
||||
.then((directory) =>
|
||||
Directory('${directory.path}/logs').create(recursive: true));
|
||||
return;
|
||||
} else if (Platform.isIOS) {
|
||||
_logDirectory = await getApplicationDocumentsDirectory().then(
|
||||
(value) => Directory('${value.path}/logs').create(recursive: true));
|
||||
return;
|
||||
}
|
||||
throw UnsupportedError("Platform not supported.");
|
||||
}
|
||||
|
||||
Future<void> _initDownloadsDirectory() async {
|
||||
if (Platform.isAndroid) {
|
||||
var directory = Directory('/storage/emulated/0/Download');
|
||||
if (!await directory.exists()) {
|
||||
final downloadsDir = await getExternalStorageDirectories(
|
||||
type: StorageDirectory.downloads,
|
||||
);
|
||||
directory = await downloadsDir!.first.create(recursive: true);
|
||||
}
|
||||
_downloadsDirectory = directory;
|
||||
return;
|
||||
} 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 {
|
||||
@@ -127,5 +262,6 @@ enum PaperlessDirectoryType {
|
||||
temporary,
|
||||
scans,
|
||||
download,
|
||||
upload;
|
||||
upload,
|
||||
logs;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/core/model/github_error_report.model.dart';
|
||||
import 'package:paperless_mobile/core/widgets/error_report_page.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/dart_extensions.dart';
|
||||
|
||||
class GithubIssueService {
|
||||
static void openCreateGithubIssue({
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:paperless_mobile/core/model/github_error_report.model.dart';
|
||||
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
|
||||
class ErrorReportPage extends StatefulWidget {
|
||||
final StackTrace? stackTrace;
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_relative_date_range_field.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class ExtendedDateRangeDialog extends StatefulWidget {
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
// ignore_for_file: invalid_use_of_protected_member
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/landing/view/widgets/mime_types_pie_chart.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class FormDateTime {
|
||||
final int? day;
|
||||
final int? month;
|
||||
final int? year;
|
||||
|
||||
FormDateTime({this.day, this.month, this.year});
|
||||
|
||||
FormDateTime.fromDateTime(DateTime date)
|
||||
: day = date.day,
|
||||
month = date.month,
|
||||
year = date.year;
|
||||
|
||||
FormDateTime copyWith({int? day, int? month, int? year}) {
|
||||
return FormDateTime(
|
||||
day: day ?? this.day,
|
||||
month: month ?? this.month,
|
||||
year: year ?? this.year,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isComplete => day != null && month != null && year != null;
|
||||
|
||||
DateTime? toDateTime() {
|
||||
if (day == null && month == null && year == null) {
|
||||
return null;
|
||||
}
|
||||
if (!isComplete) {
|
||||
throw ArgumentError.notNull("day, month and year must be set together");
|
||||
}
|
||||
return DateTime(year!, month!, day!);
|
||||
}
|
||||
}
|
||||
|
||||
/// A localized, segmented date input field.
|
||||
class FormBuilderLocalizedDatePicker extends StatefulWidget {
|
||||
final String name;
|
||||
final Locale locale;
|
||||
final String labelText;
|
||||
final Widget? prefixIcon;
|
||||
final DateTime? initialValue;
|
||||
final DateTime firstDate;
|
||||
final DateTime lastDate;
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// If set to true, the field will not throw any validation errors when empty.
|
||||
final bool allowUnset;
|
||||
|
||||
const FormBuilderLocalizedDatePicker({
|
||||
super.key,
|
||||
required this.name,
|
||||
this.initialValue,
|
||||
required this.firstDate,
|
||||
required this.lastDate,
|
||||
required this.locale,
|
||||
required this.labelText,
|
||||
this.prefixIcon,
|
||||
this.allowUnset = false,
|
||||
this.focusNode,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FormBuilderLocalizedDatePicker> createState() =>
|
||||
_FormBuilderLocalizedDatePickerState();
|
||||
}
|
||||
|
||||
class _FormBuilderLocalizedDatePickerState
|
||||
extends State<FormBuilderLocalizedDatePicker> {
|
||||
late final String _separator;
|
||||
late final String _format;
|
||||
|
||||
final _textFieldControls =
|
||||
LinkedList<_NeighbourAwareDateInputSegmentControls>();
|
||||
String? _error;
|
||||
bool _temporarilyDisableListeners = false;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final format =
|
||||
DateFormat.yMd(widget.locale.toString()).format(DateTime(1000, 11, 22));
|
||||
_separator = format.replaceAll(RegExp(r'\d'), '').characters.first;
|
||||
_format = format
|
||||
.replaceAll("1000", "yyyy")
|
||||
.replaceAll("11", "MM")
|
||||
.replaceAll("22", "dd");
|
||||
|
||||
final components = _format.split(_separator);
|
||||
for (int i = 0; i < components.length; i++) {
|
||||
final formatString = components[i];
|
||||
final initialText = widget.initialValue != null
|
||||
? DateFormat(formatString).format(widget.initialValue!)
|
||||
: null;
|
||||
final defaultFocusNode = FocusNode(debugLabel: formatString);
|
||||
final focusNode =
|
||||
i == 0 ? (widget.focusNode ?? defaultFocusNode) : defaultFocusNode;
|
||||
final controls = _NeighbourAwareDateInputSegmentControls(
|
||||
node: focusNode,
|
||||
controller: TextEditingController(text: initialText),
|
||||
format: formatString,
|
||||
position: i,
|
||||
type: _DateInputSegment.fromPattern(formatString),
|
||||
);
|
||||
_textFieldControls.add(controls);
|
||||
controls.controller.addListener(() {
|
||||
if (_temporarilyDisableListeners) {
|
||||
return;
|
||||
}
|
||||
if (controls.controller.selection.isCollapsed &&
|
||||
controls.controller.text.length == controls.format.length) {
|
||||
controls.next?.node.requestFocus();
|
||||
}
|
||||
});
|
||||
controls.node.addListener(() {
|
||||
if (_temporarilyDisableListeners || !controls.node.hasFocus) {
|
||||
return;
|
||||
}
|
||||
controls.controller.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: controls.controller.text.length,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var controls in _textFieldControls) {
|
||||
controls.node.dispose();
|
||||
controls.controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RawKeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKey: (value) {
|
||||
if (value.logicalKey == LogicalKeyboardKey.backspace &&
|
||||
value is RawKeyDownEvent) {
|
||||
final currentFocus = _textFieldControls
|
||||
.where((element) => element.node.hasFocus)
|
||||
.firstOrNull;
|
||||
if (currentFocus == null) {
|
||||
return;
|
||||
}
|
||||
if (currentFocus.controller.text.isEmpty) {
|
||||
currentFocus.previous?.node.requestFocus();
|
||||
final endOffset = currentFocus.previous?.controller.text.length;
|
||||
currentFocus.previous?.controller.selection =
|
||||
TextSelection.collapsed(offset: endOffset ?? 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: FormBuilderField<FormDateTime>(
|
||||
name: widget.name,
|
||||
validator: _validateDate,
|
||||
onChanged: (value) {
|
||||
assert(!widget.allowUnset && value != null);
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
// When the change is requested from external sources, such as calling
|
||||
// field.didChange(value), then we want to update the text fields individually
|
||||
// without causing the either field to gain focus (as defined above).
|
||||
final isChangeRequestedFromOutside =
|
||||
_textFieldControls.none((element) => element.node.hasFocus);
|
||||
|
||||
if (isChangeRequestedFromOutside) {
|
||||
_updateInputsWithDate(value, disableListeners: true);
|
||||
}
|
||||
// Imitate the functionality of the validator function in "normal" form fields.
|
||||
// The error is shown on the outer decorator as if this was a regular text input.
|
||||
// Errors are cleared after the next user interaction.
|
||||
final error = _validateDate(value);
|
||||
setState(() {
|
||||
_error = error;
|
||||
});
|
||||
},
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
initialValue: widget.initialValue != null
|
||||
? FormDateTime.fromDateTime(widget.initialValue!)
|
||||
: null,
|
||||
builder: (field) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_textFieldControls.first.node.requestFocus();
|
||||
},
|
||||
child: InputDecorator(
|
||||
textAlignVertical: TextAlignVertical.bottom,
|
||||
decoration: InputDecoration(
|
||||
errorText: _error,
|
||||
labelText: widget.labelText,
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.calendar_month_outlined),
|
||||
onPressed: () async {
|
||||
final selectedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate:
|
||||
field.value?.toDateTime() ?? DateTime.now(),
|
||||
firstDate: widget.firstDate,
|
||||
lastDate: widget.lastDate,
|
||||
initialEntryMode: DatePickerEntryMode.calendarOnly,
|
||||
);
|
||||
if (selectedDate != null) {
|
||||
final formDate =
|
||||
FormDateTime.fromDateTime(selectedDate);
|
||||
_temporarilyDisableListeners = true;
|
||||
_updateInputsWithDate(formDate);
|
||||
field.didChange(formDate);
|
||||
_temporarilyDisableListeners = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
if (widget.allowUnset)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
for (var c in _textFieldControls) {
|
||||
c.controller.clear();
|
||||
}
|
||||
_textFieldControls.first.node.requestFocus();
|
||||
field.didChange(null);
|
||||
},
|
||||
icon: const Icon(Icons.clear),
|
||||
),
|
||||
],
|
||||
).paddedOnly(right: 4),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
for (var s in _textFieldControls) ...[
|
||||
IntrinsicWidth(
|
||||
child: _buildDateSegmentInput(s, context, field),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? _validateDate(FormDateTime? date) {
|
||||
if (widget.allowUnset && date == null) {
|
||||
return null;
|
||||
}
|
||||
if (date == null) {
|
||||
return S.of(context)!.thisFieldIsRequired;
|
||||
}
|
||||
final d = date.toDateTime();
|
||||
if (d == null) {
|
||||
return S.of(context)!.thisFieldIsRequired;
|
||||
}
|
||||
if (d.day != date.day && d.month != date.month && d.year != date.year) {
|
||||
return "Invalid date.";
|
||||
}
|
||||
if (d.isBefore(widget.firstDate)) {
|
||||
final formattedDateHint =
|
||||
DateFormat.yMd(widget.locale.toString()).format(widget.firstDate);
|
||||
return "Date must be after $formattedDateHint.";
|
||||
}
|
||||
if (d.isAfter(widget.lastDate)) {
|
||||
final formattedDateHint =
|
||||
DateFormat.yMd(widget.locale.toString()).format(widget.lastDate);
|
||||
return "Date must be before $formattedDateHint.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _updateInputsWithDate(
|
||||
FormDateTime date, {
|
||||
bool disableListeners = false,
|
||||
}) {
|
||||
if (disableListeners) {
|
||||
_temporarilyDisableListeners = true;
|
||||
}
|
||||
for (var controls in _textFieldControls) {
|
||||
final value = DateFormat(controls.format).format(date.toDateTime()!);
|
||||
controls.controller.text = value;
|
||||
}
|
||||
_temporarilyDisableListeners = false;
|
||||
}
|
||||
|
||||
Widget _buildDateSegmentInput(
|
||||
_NeighbourAwareDateInputSegmentControls controls,
|
||||
BuildContext context,
|
||||
FormFieldState<FormDateTime> field,
|
||||
) {
|
||||
return TextFormField(
|
||||
onFieldSubmitted: (value) {
|
||||
if (value.length < controls.format.length) {
|
||||
controls.controller.text = value.padLeft(controls.format.length, '0');
|
||||
}
|
||||
controls.next?.node.requestFocus();
|
||||
},
|
||||
style: const TextStyle(fontFamily: 'RobotoMono'),
|
||||
keyboardType: TextInputType.datetime,
|
||||
textInputAction:
|
||||
controls.position < 2 ? TextInputAction.next : TextInputAction.done,
|
||||
controller: controls.controller,
|
||||
focusNode: _textFieldControls.elementAt(controls.position).node,
|
||||
maxLength: controls.format.length,
|
||||
maxLengthEnforcement: MaxLengthEnforcement.enforced,
|
||||
enableInteractiveSelection: false,
|
||||
onChanged: (value) {
|
||||
if (value.length == controls.format.length) {
|
||||
final number = int.tryParse(value);
|
||||
if (number == null) {
|
||||
return;
|
||||
}
|
||||
final fieldValue = field.value ?? FormDateTime();
|
||||
final newValue = switch (controls.type) {
|
||||
_DateInputSegment.day => fieldValue.copyWith(day: number),
|
||||
_DateInputSegment.month => fieldValue.copyWith(month: number),
|
||||
_DateInputSegment.year => fieldValue.copyWith(year: number),
|
||||
};
|
||||
field.setValue(newValue);
|
||||
}
|
||||
},
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
RangeLimitedInputFormatter(
|
||||
1,
|
||||
switch (controls.type) {
|
||||
_DateInputSegment.day => 31,
|
||||
_DateInputSegment.month => 12,
|
||||
_DateInputSegment.year => 9999,
|
||||
},
|
||||
),
|
||||
],
|
||||
onEditingComplete: () {
|
||||
if (field.value != null) {
|
||||
_updateInputsWithDate(field.value!, disableListeners: true);
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
suffixIcon: controls.position < 2
|
||||
? Text(
|
||||
_separator,
|
||||
style: const TextStyle(fontFamily: 'RobotoMono'),
|
||||
).paddedSymmetrically(horizontal: 2)
|
||||
: null,
|
||||
suffixIconConstraints: const BoxConstraints.tightFor(),
|
||||
fillColor: Colors.blue.values[controls.position],
|
||||
counterText: '',
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintText: controls.format,
|
||||
hintStyle: const TextStyle(fontFamily: "RobotoMono"),
|
||||
border: Theme.of(context).inputDecorationTheme.border?.copyWith(
|
||||
borderSide: const BorderSide(
|
||||
width: 0,
|
||||
style: BorderStyle.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _DateInputSegment {
|
||||
day,
|
||||
month,
|
||||
year;
|
||||
|
||||
static _DateInputSegment fromPattern(String pattern) {
|
||||
final char = pattern.characters.first;
|
||||
return switch (char) {
|
||||
'd' => day,
|
||||
'M' => month,
|
||||
'y' => year,
|
||||
_ => throw ArgumentError.value(pattern),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
final class _NeighbourAwareDateInputSegmentControls
|
||||
with LinkedListEntry<_NeighbourAwareDateInputSegmentControls> {
|
||||
final FocusNode node;
|
||||
final TextEditingController controller;
|
||||
final int position;
|
||||
final String format;
|
||||
final _DateInputSegment type;
|
||||
|
||||
_NeighbourAwareDateInputSegmentControls({
|
||||
required this.node,
|
||||
required this.controller,
|
||||
required this.format,
|
||||
required this.position,
|
||||
required this.type,
|
||||
});
|
||||
}
|
||||
|
||||
class RangeLimitedInputFormatter extends TextInputFormatter {
|
||||
RangeLimitedInputFormatter(
|
||||
this.minimum,
|
||||
this.maximum,
|
||||
) : assert(minimum < maximum);
|
||||
|
||||
final int minimum;
|
||||
final int maximum;
|
||||
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
if (newValue.text.length < 2) {
|
||||
return newValue;
|
||||
}
|
||||
var value = int.parse(newValue.text);
|
||||
final lastCharacter = newValue.text.characters.last;
|
||||
if (value < minimum || value > maximum) {
|
||||
return TextEditingValue(
|
||||
text: lastCharacter,
|
||||
selection: TextSelection.collapsed(offset: 1),
|
||||
);
|
||||
}
|
||||
return newValue;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class FullscreenSelectionForm extends StatefulWidget {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class HintCard extends StatelessWidget {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:paperless_mobile/generated/assets.gen.dart';
|
||||
|
||||
class PaperlessLogo extends StatelessWidget {
|
||||
static const _paperlessGreen = Color(0xFF18541F);
|
||||
final double? height;
|
||||
final double? width;
|
||||
final Color _color;
|
||||
|
||||
const PaperlessLogo.white({
|
||||
super.key,
|
||||
this.height,
|
||||
this.width,
|
||||
}) : _color = Colors.white;
|
||||
|
||||
const PaperlessLogo.green({super.key, this.height, this.width})
|
||||
: _color = _paperlessGreen;
|
||||
|
||||
const PaperlessLogo.black({super.key, this.height, this.width})
|
||||
: _color = Colors.black;
|
||||
|
||||
const PaperlessLogo.colored(Color color, {super.key, this.height, this.width})
|
||||
: _color = color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: height ?? Theme.of(context).iconTheme.size ?? 32,
|
||||
maxWidth: width ?? Theme.of(context).iconTheme.size ?? 32,
|
||||
),
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Assets.logos.paperlessLogoWhiteSvg.svg(
|
||||
colorFilter: ColorFilter.mode(
|
||||
_color,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,10 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:paperless_mobile/constants.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/global/asset_images.dart';
|
||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/logging/view/app_logs_page.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
||||
@@ -18,6 +15,7 @@ import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/saved_views_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/upload_queue_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/app_logs_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/changelog_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -39,10 +37,10 @@ class AppDrawer extends StatelessWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const PaperlessLogo.green(
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
const $AssetsLogosGen()
|
||||
.paperlessLogoGreenSvg
|
||||
.svg(width: 32, height: 32),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
"Paperless Mobile",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
@@ -108,14 +106,6 @@ class AppDrawer extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.history),
|
||||
title: Text(S.of(context)!.changelog),
|
||||
onTap: () {
|
||||
ChangelogRoute().push(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.bug_report_outlined),
|
||||
@@ -131,7 +121,7 @@ class AppDrawer extends StatelessWidget {
|
||||
),
|
||||
onTap: () {
|
||||
launchUrlString(
|
||||
'https://github.com/astubenbord/paperless-mobile/issues/new',
|
||||
'https://github.com/astubenbord/paperless-mobile/issues/new?assignees=astubenbord&labels=bug%2Ctriage&projects=&template=bug-report.yml&title=%5BBug%5D%3A+',
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/theme.dart';
|
||||
|
||||
@@ -63,6 +63,7 @@ class ChangelogDialog extends StatelessWidget {
|
||||
}
|
||||
|
||||
const _versionNumbers = {
|
||||
"54": "3.0.7",
|
||||
"53": "3.0.6",
|
||||
"52": "3.0.5",
|
||||
"51": "3.0.4",
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart';
|
||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/dart_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/confirm_bulk_modify_label_dialog.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/widgets/form_fields/fullscreen_selection_form.dart';
|
||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/dart_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
|
||||
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/confirm_bulk_modify_tags_dialog.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
@@ -2,22 +2,23 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||
import 'package:paperless_mobile/features/logging/data/logger.dart';
|
||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:printing/printing.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:cross_file/cross_file.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
part 'document_details_cubit.freezed.dart';
|
||||
|
||||
part 'document_details_state.dart';
|
||||
|
||||
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
final int id;
|
||||
final PaperlessDocumentsApi _api;
|
||||
final DocumentChangedNotifier _notifier;
|
||||
final LocalNotificationService _notificationService;
|
||||
@@ -28,43 +29,52 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
this._labelRepository,
|
||||
this._notifier,
|
||||
this._notificationService, {
|
||||
required DocumentModel initialDocument,
|
||||
}) : super(DocumentDetailsState(
|
||||
document: initialDocument,
|
||||
)) {
|
||||
_notifier.addListener(this, onUpdated: replace);
|
||||
_labelRepository.addListener(
|
||||
required this.id,
|
||||
}) : super(const DocumentDetailsInitial()) {
|
||||
_notifier.addListener(
|
||||
this,
|
||||
onChanged: (labels) => emit(
|
||||
state.copyWith(
|
||||
correspondents: labels.correspondents,
|
||||
documentTypes: labels.documentTypes,
|
||||
tags: labels.tags,
|
||||
storagePaths: labels.storagePaths,
|
||||
),
|
||||
),
|
||||
onUpdated: (document) {
|
||||
replace(document);
|
||||
},
|
||||
ids: [id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
debugPrint("Initialize called");
|
||||
emit(const DocumentDetailsLoading());
|
||||
try {
|
||||
final (document, metaData) = await Future.wait([
|
||||
_api.find(id),
|
||||
_api.getMetaData(id),
|
||||
]).then((value) => (
|
||||
value[0] as DocumentModel,
|
||||
value[1] as DocumentMetaData,
|
||||
));
|
||||
// final document = await _api.find(id);
|
||||
// final metaData = await _api.getMetaData(id);
|
||||
debugPrint("Document data loaded for $id");
|
||||
emit(DocumentDetailsLoaded(
|
||||
document: document,
|
||||
metaData: metaData,
|
||||
));
|
||||
} catch (error, stackTrace) {
|
||||
logger.fe(
|
||||
"An error occurred while loading data for document $id.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'initialize',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
emit(const DocumentDetailsError());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> delete(DocumentModel document) async {
|
||||
await _api.delete(document);
|
||||
_notifier.notifyDeleted(document);
|
||||
}
|
||||
|
||||
Future<void> loadMetaData() async {
|
||||
final metaData = await _api.getMetaData(state.document);
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(metaData: metaData));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadFullContent() async {
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
final doc = await _api.find(state.document.id);
|
||||
_notifier.notifyUpdated(doc);
|
||||
emit(state.copyWith(isFullContentLoaded: true));
|
||||
}
|
||||
|
||||
Future<void> assignAsn(
|
||||
DocumentModel document, {
|
||||
int? asn,
|
||||
@@ -84,11 +94,15 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
}
|
||||
|
||||
Future<ResultType> openDocumentInSystemViewer() async {
|
||||
final cacheDir = await FileService.temporaryDirectory;
|
||||
if (state.metaData == null) {
|
||||
await loadMetaData();
|
||||
final s = state;
|
||||
if (s is! DocumentDetailsLoaded) {
|
||||
throw Exception(
|
||||
"Document cannot be opened in system viewer "
|
||||
"if document information has not yet been loaded.",
|
||||
);
|
||||
}
|
||||
final filePath = state.metaData!.mediaFilename.replaceAll("/", " ");
|
||||
final cacheDir = FileService.instance.temporaryDirectory;
|
||||
final filePath = s.metaData.mediaFilename.replaceAll("/", " ");
|
||||
|
||||
final fileName = "${p.basenameWithoutExtension(filePath)}.pdf";
|
||||
final file = File("${cacheDir.path}/$fileName");
|
||||
@@ -96,7 +110,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
if (!file.existsSync()) {
|
||||
file.createSync();
|
||||
await _api.downloadToFile(
|
||||
state.document,
|
||||
s.document,
|
||||
file.path,
|
||||
);
|
||||
}
|
||||
@@ -107,7 +121,14 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
}
|
||||
|
||||
void replace(DocumentModel document) {
|
||||
emit(state.copyWith(document: document));
|
||||
final s = state;
|
||||
if (s is! DocumentDetailsLoaded) {
|
||||
return;
|
||||
}
|
||||
emit(DocumentDetailsLoaded(
|
||||
document: document,
|
||||
metaData: s.metaData,
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> downloadDocument({
|
||||
@@ -115,19 +136,21 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
required String locale,
|
||||
required String userId,
|
||||
}) async {
|
||||
if (state.metaData == null) {
|
||||
await loadMetaData();
|
||||
final s = state;
|
||||
if (s is! DocumentDetailsLoaded) {
|
||||
return;
|
||||
}
|
||||
String targetPath = _buildDownloadFilePath(
|
||||
s.metaData,
|
||||
downloadOriginal,
|
||||
await FileService.downloadsDirectory,
|
||||
FileService.instance.downloadsDirectory,
|
||||
);
|
||||
|
||||
if (!await File(targetPath).exists()) {
|
||||
await File(targetPath).create();
|
||||
} else {
|
||||
await _notificationService.notifyFileDownload(
|
||||
document: state.document,
|
||||
await _notificationService.notifyDocumentDownload(
|
||||
document: s.document,
|
||||
filename: p.basename(targetPath),
|
||||
filePath: targetPath,
|
||||
finished: true,
|
||||
@@ -146,12 +169,12 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
// );
|
||||
|
||||
await _api.downloadToFile(
|
||||
state.document,
|
||||
s.document,
|
||||
targetPath,
|
||||
original: downloadOriginal,
|
||||
onProgressChanged: (progress) {
|
||||
_notificationService.notifyFileDownload(
|
||||
document: state.document,
|
||||
_notificationService.notifyDocumentDownload(
|
||||
document: s.document,
|
||||
filename: p.basename(targetPath),
|
||||
filePath: targetPath,
|
||||
finished: true,
|
||||
@@ -161,27 +184,29 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
);
|
||||
},
|
||||
);
|
||||
await _notificationService.notifyFileDownload(
|
||||
document: state.document,
|
||||
await _notificationService.notifyDocumentDownload(
|
||||
document: s.document,
|
||||
filename: p.basename(targetPath),
|
||||
filePath: targetPath,
|
||||
finished: true,
|
||||
locale: locale,
|
||||
userId: userId,
|
||||
);
|
||||
debugPrint("Downloaded file to $targetPath");
|
||||
logger.fi("Document '${s.document.title}' saved to $targetPath.");
|
||||
}
|
||||
|
||||
Future<void> shareDocument({bool shareOriginal = false}) async {
|
||||
if (state.metaData == null) {
|
||||
await loadMetaData();
|
||||
final s = state;
|
||||
if (s is! DocumentDetailsLoaded) {
|
||||
return;
|
||||
}
|
||||
String filePath = _buildDownloadFilePath(
|
||||
s.metaData,
|
||||
shareOriginal,
|
||||
await FileService.temporaryDirectory,
|
||||
FileService.instance.temporaryDirectory,
|
||||
);
|
||||
await _api.downloadToFile(
|
||||
state.document,
|
||||
s.document,
|
||||
filePath,
|
||||
original: shareOriginal,
|
||||
);
|
||||
@@ -189,23 +214,27 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
[
|
||||
XFile(
|
||||
filePath,
|
||||
name: state.document.originalFileName,
|
||||
name: s.document.originalFileName,
|
||||
mimeType: "application/pdf",
|
||||
lastModified: state.document.modified,
|
||||
lastModified: s.document.modified,
|
||||
),
|
||||
],
|
||||
subject: state.document.title,
|
||||
subject: s.document.title,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> printDocument() async {
|
||||
if (state.metaData == null) {
|
||||
await loadMetaData();
|
||||
final s = state;
|
||||
if (s is! DocumentDetailsLoaded) {
|
||||
return;
|
||||
}
|
||||
final filePath =
|
||||
_buildDownloadFilePath(false, await FileService.temporaryDirectory);
|
||||
final filePath = _buildDownloadFilePath(
|
||||
s.metaData,
|
||||
false,
|
||||
FileService.instance.temporaryDirectory,
|
||||
);
|
||||
await _api.downloadToFile(
|
||||
state.document,
|
||||
s.document,
|
||||
filePath,
|
||||
original: false,
|
||||
);
|
||||
@@ -214,13 +243,14 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
throw Exception("An error occurred while downloading the document.");
|
||||
}
|
||||
Printing.layoutPdf(
|
||||
name: state.document.title,
|
||||
name: s.document.title,
|
||||
onLayout: (format) => file.readAsBytesSync(),
|
||||
);
|
||||
}
|
||||
|
||||
String _buildDownloadFilePath(bool original, Directory dir) {
|
||||
final normalizedPath = state.metaData!.mediaFilename.replaceAll("/", " ");
|
||||
String _buildDownloadFilePath(
|
||||
DocumentMetaData meta, bool original, Directory dir) {
|
||||
final normalizedPath = meta.mediaFilename.replaceAll("/", " ");
|
||||
final extension = original ? p.extension(normalizedPath) : '.pdf';
|
||||
return "${dir.path}/${p.basenameWithoutExtension(normalizedPath)}$extension";
|
||||
}
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
part of 'document_details_cubit.dart';
|
||||
|
||||
@freezed
|
||||
class DocumentDetailsState with _$DocumentDetailsState {
|
||||
const factory DocumentDetailsState({
|
||||
required DocumentModel document,
|
||||
DocumentMetaData? metaData,
|
||||
@Default(false) bool isFullContentLoaded,
|
||||
@Default({}) Map<int, Correspondent> correspondents,
|
||||
@Default({}) Map<int, DocumentType> documentTypes,
|
||||
@Default({}) Map<int, Tag> tags,
|
||||
@Default({}) Map<int, StoragePath> storagePaths,
|
||||
}) = _DocumentDetailsState;
|
||||
sealed class DocumentDetailsState {
|
||||
const DocumentDetailsState();
|
||||
}
|
||||
|
||||
class DocumentDetailsInitial extends DocumentDetailsState {
|
||||
const DocumentDetailsInitial();
|
||||
}
|
||||
|
||||
class DocumentDetailsLoading extends DocumentDetailsState {
|
||||
const DocumentDetailsLoading();
|
||||
}
|
||||
|
||||
class DocumentDetailsLoaded extends DocumentDetailsState {
|
||||
final DocumentModel document;
|
||||
final DocumentMetaData metaData;
|
||||
|
||||
const DocumentDetailsLoaded({
|
||||
required this.document,
|
||||
required this.metaData,
|
||||
});
|
||||
}
|
||||
|
||||
class DocumentDetailsError extends DocumentDetailsState {
|
||||
const DocumentDetailsError();
|
||||
}
|
||||
|
||||
|
||||
// @freezed
|
||||
// class DocumentDetailsState with _$DocumentDetailsState {
|
||||
// const factory DocumentDetailsState({
|
||||
// required DocumentModel document,
|
||||
// DocumentMetaData? metaData,
|
||||
// @Default(false) bool isFullContentLoaded,
|
||||
// @Default({}) Map<int, Correspondent> correspondents,
|
||||
// @Default({}) Map<int, DocumentType> documentTypes,
|
||||
// @Default({}) Map<int, Tag> tags,
|
||||
// @Default({}) Map<int, StoragePath> storagePaths,
|
||||
// }) = _DocumentDetailsState;
|
||||
// }
|
||||
|
||||
@@ -2,20 +2,18 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
|
||||
import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_content_widget.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_meta_data_widget.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_overview_widget.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_permissions_widget.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||
@@ -29,13 +27,21 @@ import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart';
|
||||
import 'package:paperless_mobile/theme.dart';
|
||||
|
||||
class DocumentDetailsPage extends StatefulWidget {
|
||||
final int id;
|
||||
final String? title;
|
||||
final bool isLabelClickable;
|
||||
final String? titleAndContentQueryString;
|
||||
final String? thumbnailUrl;
|
||||
final String? heroTag;
|
||||
|
||||
const DocumentDetailsPage({
|
||||
Key? key,
|
||||
this.isLabelClickable = true,
|
||||
this.titleAndContentQueryString,
|
||||
this.thumbnailUrl,
|
||||
required this.id,
|
||||
this.heroTag,
|
||||
this.title,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -57,152 +63,157 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
final hasMultiUserSupport =
|
||||
context.watch<LocalUserAccount>().hasMultiUserSupport;
|
||||
final tabLength = 4 + (hasMultiUserSupport && false ? 1 : 0);
|
||||
final title = context.watch<DocumentDetailsCubit>().state.document.title;
|
||||
return AnnotatedRegion(
|
||||
value: buildOverlayStyle(
|
||||
Theme.of(context),
|
||||
systemNavigationBarColor: Theme.of(context).bottomAppBarTheme.color,
|
||||
),
|
||||
child: WillPopScope(
|
||||
onWillPop: () async {
|
||||
Navigator.of(context)
|
||||
.pop(context.read<DocumentDetailsCubit>().state.document);
|
||||
return false;
|
||||
},
|
||||
child: DefaultTabController(
|
||||
length: tabLength,
|
||||
child: BlocListener<ConnectivityCubit, ConnectivityState>(
|
||||
listenWhen: (previous, current) =>
|
||||
!previous.isConnected && current.isConnected,
|
||||
listener: (context, state) {
|
||||
context.read<DocumentDetailsCubit>().loadMetaData();
|
||||
},
|
||||
child: BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||
builder: (context, state) {
|
||||
return DefaultTabController(
|
||||
length: tabLength,
|
||||
child: Scaffold(
|
||||
extendBodyBehindAppBar: false,
|
||||
floatingActionButtonLocation:
|
||||
FloatingActionButtonLocation.endDocked,
|
||||
floatingActionButton: _buildEditButton(),
|
||||
floatingActionButton: switch (state) {
|
||||
DocumentDetailsLoaded(document: var document) =>
|
||||
_buildEditButton(document),
|
||||
_ => null
|
||||
},
|
||||
bottomNavigationBar: _buildBottomAppBar(),
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
|
||||
context),
|
||||
sliver: SliverAppBar(
|
||||
title: Text(title),
|
||||
leading: const BackButton(),
|
||||
pinned: true,
|
||||
forceElevated: innerBoxIsScrolled,
|
||||
collapsedHeight: kToolbarHeight,
|
||||
expandedHeight: 250.0,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: BlocBuilder<DocumentDetailsCubit,
|
||||
DocumentDetailsState>(
|
||||
builder: (context, state) {
|
||||
return Hero(
|
||||
tag: "thumb_${state.document.id}",
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
DocumentPreviewRoute($extra: state.document)
|
||||
.push(context);
|
||||
},
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DocumentPreview(
|
||||
enableHero: false,
|
||||
document: state.document,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
stops: [0.2, 0.4],
|
||||
colors: [
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.6),
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.3),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
sliver:
|
||||
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||
builder: (context, state) {
|
||||
final title = switch (state) {
|
||||
DocumentDetailsLoaded(document: var document) =>
|
||||
document.title,
|
||||
_ => widget.title ?? '',
|
||||
};
|
||||
return SliverAppBar(
|
||||
title: Text(title),
|
||||
leading: const BackButton(),
|
||||
pinned: true,
|
||||
forceElevated: innerBoxIsScrolled,
|
||||
collapsedHeight: kToolbarHeight,
|
||||
expandedHeight: 250.0,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Builder(
|
||||
builder: (context) {
|
||||
return Hero(
|
||||
tag: widget.heroTag ?? "thumb_${widget.id}",
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
DocumentPreviewRoute(
|
||||
id: widget.id,
|
||||
title: title,
|
||||
).push(context);
|
||||
},
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DocumentPreview(
|
||||
documentId: widget.id,
|
||||
title: title,
|
||||
enableHero: false,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
stops: [0.2, 0.4],
|
||||
colors: [
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.6),
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.withOpacity(0.3),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
bottom: ColoredTabBar(
|
||||
tabBar: TabBar(
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(
|
||||
S.of(context)!.overview,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
S.of(context)!.content,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
S.of(context)!.metaData,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
S.of(context)!.similarDocuments,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasMultiUserSupport && false)
|
||||
Tab(
|
||||
child: Text(
|
||||
"Permissions",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
bottom: ColoredTabBar(
|
||||
tabBar: TabBar(
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(
|
||||
S.of(context)!.overview,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
S.of(context)!.content,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
S.of(context)!.metaData,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
S.of(context)!.similarDocuments,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
// if (hasMultiUserSupport && false)
|
||||
// Tab(
|
||||
// child: Text(
|
||||
// "Permissions",
|
||||
// style: TextStyle(
|
||||
// color: Theme.of(context)
|
||||
// .colorScheme
|
||||
// .onPrimaryContainer,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -214,7 +225,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
context.read(),
|
||||
context.read(),
|
||||
context.read(),
|
||||
documentId: state.document.id,
|
||||
documentId: widget.id,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -229,12 +240,19 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
DocumentOverviewWidget(
|
||||
document: state.document,
|
||||
itemSpacing: _itemSpacing,
|
||||
queryString:
|
||||
widget.titleAndContentQueryString,
|
||||
),
|
||||
switch (state) {
|
||||
DocumentDetailsLoaded(
|
||||
document: var document
|
||||
) =>
|
||||
DocumentOverviewWidget(
|
||||
document: document,
|
||||
itemSpacing: _itemSpacing,
|
||||
queryString:
|
||||
widget.titleAndContentQueryString,
|
||||
),
|
||||
DocumentDetailsError() => _buildErrorState(),
|
||||
_ => _buildLoadingState(),
|
||||
},
|
||||
],
|
||||
),
|
||||
CustomScrollView(
|
||||
@@ -243,13 +261,18 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
DocumentContentWidget(
|
||||
isFullContentLoaded:
|
||||
state.isFullContentLoaded,
|
||||
document: state.document,
|
||||
queryString:
|
||||
widget.titleAndContentQueryString,
|
||||
),
|
||||
switch (state) {
|
||||
DocumentDetailsLoaded(
|
||||
document: var document
|
||||
) =>
|
||||
DocumentContentWidget(
|
||||
document: document,
|
||||
queryString:
|
||||
widget.titleAndContentQueryString,
|
||||
),
|
||||
DocumentDetailsError() => _buildErrorState(),
|
||||
_ => _buildLoadingState(),
|
||||
}
|
||||
],
|
||||
),
|
||||
CustomScrollView(
|
||||
@@ -258,10 +281,19 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(context),
|
||||
),
|
||||
DocumentMetaDataWidget(
|
||||
document: state.document,
|
||||
itemSpacing: _itemSpacing,
|
||||
),
|
||||
switch (state) {
|
||||
DocumentDetailsLoaded(
|
||||
document: var document,
|
||||
metaData: var metaData,
|
||||
) =>
|
||||
DocumentMetaDataWidget(
|
||||
document: document,
|
||||
itemSpacing: _itemSpacing,
|
||||
metaData: metaData,
|
||||
),
|
||||
DocumentDetailsError() => _buildErrorState(),
|
||||
_ => _buildLoadingState(),
|
||||
},
|
||||
],
|
||||
),
|
||||
CustomScrollView(
|
||||
@@ -277,20 +309,20 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (hasMultiUserSupport && false)
|
||||
CustomScrollView(
|
||||
controller: _pagingScrollController,
|
||||
slivers: [
|
||||
SliverOverlapInjector(
|
||||
handle: NestedScrollView
|
||||
.sliverOverlapAbsorberHandleFor(
|
||||
context),
|
||||
),
|
||||
DocumentPermissionsWidget(
|
||||
document: state.document,
|
||||
),
|
||||
],
|
||||
),
|
||||
// if (hasMultiUserSupport && false)
|
||||
// CustomScrollView(
|
||||
// controller: _pagingScrollController,
|
||||
// slivers: [
|
||||
// SliverOverlapInjector(
|
||||
// handle: NestedScrollView
|
||||
// .sliverOverlapAbsorberHandleFor(
|
||||
// context),
|
||||
// ),
|
||||
// DocumentPermissionsWidget(
|
||||
// document: state.document,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -299,13 +331,13 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEditButton() {
|
||||
Widget _buildEditButton(DocumentModel document) {
|
||||
final currentUser = context.watch<LocalUserAccount>();
|
||||
|
||||
bool canEdit = context.watchInternetConnection &&
|
||||
@@ -313,7 +345,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
if (!canEdit) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final document = context.read<DocumentDetailsCubit>().state.document;
|
||||
return Tooltip(
|
||||
message: S.of(context)!.editDocumentTooltip,
|
||||
preferBelow: false,
|
||||
@@ -326,60 +357,80 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState() {
|
||||
return SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Text("Could not load document."),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState() {
|
||||
return SliverFillRemaining(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState> _buildBottomAppBar() {
|
||||
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||
builder: (context, state) {
|
||||
final currentUser = context.watch<LocalUserAccount>();
|
||||
return BottomAppBar(
|
||||
child: BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
||||
builder: (context, connectivityState) {
|
||||
final currentUser = context.watch<LocalUserAccount>();
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ConnectivityAwareActionWrapper(
|
||||
disabled: !currentUser.paperlessUser.canDeleteDocuments,
|
||||
offlineBuilder: (context, child) {
|
||||
return const IconButton(
|
||||
icon: Icon(Icons.delete),
|
||||
onPressed: null,
|
||||
).paddedSymmetrically(horizontal: 4);
|
||||
},
|
||||
child: IconButton(
|
||||
tooltip: S.of(context)!.deleteDocumentTooltip,
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => _onDelete(state.document),
|
||||
).paddedSymmetrically(horizontal: 4),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return switch (state) {
|
||||
DocumentDetailsLoaded(document: var document) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
ConnectivityAwareActionWrapper(
|
||||
disabled: !currentUser.paperlessUser.canDeleteDocuments,
|
||||
offlineBuilder: (context, child) {
|
||||
return const IconButton(
|
||||
icon: Icon(Icons.delete),
|
||||
onPressed: null,
|
||||
).paddedSymmetrically(horizontal: 4);
|
||||
},
|
||||
child: IconButton(
|
||||
tooltip: S.of(context)!.deleteDocumentTooltip,
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () => _onDelete(document),
|
||||
).paddedSymmetrically(horizontal: 4),
|
||||
),
|
||||
ConnectivityAwareActionWrapper(
|
||||
offlineBuilder: (context, child) =>
|
||||
const DocumentDownloadButton(
|
||||
document: null,
|
||||
enabled: false,
|
||||
),
|
||||
child: DocumentDownloadButton(
|
||||
document: document,
|
||||
),
|
||||
),
|
||||
ConnectivityAwareActionWrapper(
|
||||
offlineBuilder: (context, child) => const IconButton(
|
||||
icon: Icon(Icons.open_in_new),
|
||||
onPressed: null,
|
||||
),
|
||||
child: IconButton(
|
||||
tooltip: S.of(context)!.openInSystemViewer,
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
onPressed: _onOpenFileInSystemViewer,
|
||||
).paddedOnly(right: 4.0),
|
||||
),
|
||||
DocumentShareButton(document: document),
|
||||
IconButton(
|
||||
tooltip: S.of(context)!.print,
|
||||
onPressed: () => context
|
||||
.read<DocumentDetailsCubit>()
|
||||
.printDocument(),
|
||||
icon: const Icon(Icons.print),
|
||||
),
|
||||
],
|
||||
),
|
||||
ConnectivityAwareActionWrapper(
|
||||
offlineBuilder: (context, child) =>
|
||||
const DocumentDownloadButton(
|
||||
document: null,
|
||||
enabled: false,
|
||||
),
|
||||
child: DocumentDownloadButton(
|
||||
document: state.document,
|
||||
),
|
||||
),
|
||||
ConnectivityAwareActionWrapper(
|
||||
offlineBuilder: (context, child) => const IconButton(
|
||||
icon: Icon(Icons.open_in_new),
|
||||
onPressed: null,
|
||||
),
|
||||
child: IconButton(
|
||||
tooltip: S.of(context)!.openInSystemViewer,
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
onPressed: _onOpenFileInSystemViewer,
|
||||
).paddedOnly(right: 4.0),
|
||||
),
|
||||
DocumentShareButton(document: state.document),
|
||||
IconButton(
|
||||
tooltip: S.of(context)!.print,
|
||||
onPressed: () =>
|
||||
context.read<DocumentDetailsCubit>().printDocument(),
|
||||
icon: const Icon(Icons.print),
|
||||
),
|
||||
],
|
||||
);
|
||||
_ => SizedBox.shrink(),
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -423,11 +474,4 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onOpen(DocumentModel document) async {
|
||||
DocumentPreviewRoute(
|
||||
$extra: document,
|
||||
title: document.title,
|
||||
).push(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
@@ -50,11 +50,16 @@ class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
|
||||
context.watch<LocalUserAccount>().paperlessUser.canEditDocuments;
|
||||
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous is DocumentDetailsLoaded &&
|
||||
current is DocumentDetailsLoaded &&
|
||||
previous.document.archiveSerialNumber !=
|
||||
current.document.archiveSerialNumber,
|
||||
current.document.archiveSerialNumber,
|
||||
listener: (context, state) {
|
||||
_asnEditingController.text =
|
||||
state.document.archiveSerialNumber?.toString() ?? '';
|
||||
_asnEditingController.text = (state as DocumentDetailsLoaded)
|
||||
.document
|
||||
.archiveSerialNumber
|
||||
?.toString() ??
|
||||
'';
|
||||
setState(() {
|
||||
_canUpdate = false;
|
||||
});
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
|
||||
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class DocumentContentWidget extends StatelessWidget {
|
||||
final bool isFullContentLoaded;
|
||||
final String? queryString;
|
||||
final DocumentModel document;
|
||||
final String? queryString;
|
||||
const DocumentContentWidget({
|
||||
super.key,
|
||||
required this.isFullContentLoaded,
|
||||
required this.document,
|
||||
this.queryString,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
// if (document == null) {
|
||||
// final widths = [.3, .8, .9, .7, .6, .4, .8, .8, .6, .4];
|
||||
// return SliverToBoxAdapter(
|
||||
// child: ShimmerPlaceholder(
|
||||
// child: Column(
|
||||
// children: [
|
||||
// for (int i = 0; i < 10; i++)
|
||||
// Container(
|
||||
// width: MediaQuery.sizeOf(context).width * widths[i],
|
||||
// height: 14,
|
||||
// color: Colors.white,
|
||||
// margin: EdgeInsets.symmetric(vertical: 4),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
return SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -31,21 +42,6 @@ class DocumentContentWidget extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
caseSensitive: false,
|
||||
),
|
||||
if (!isFullContentLoaded)
|
||||
ShimmerPlaceholder(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (var scale in [0.5, 0.9, 0.5, 0.8, 0.9, 0.9])
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
width: screenWidth * scale,
|
||||
height: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
).paddedOnly(top: 4),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,9 +3,9 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
|
||||
@@ -3,88 +3,74 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/archive_serial_number_field.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
||||
|
||||
class DocumentMetaDataWidget extends StatefulWidget {
|
||||
class DocumentMetaDataWidget extends StatelessWidget {
|
||||
final DocumentModel document;
|
||||
final DocumentMetaData metaData;
|
||||
final double itemSpacing;
|
||||
const DocumentMetaDataWidget({
|
||||
super.key,
|
||||
required this.document,
|
||||
required this.metaData,
|
||||
required this.itemSpacing,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DocumentMetaDataWidget> createState() => _DocumentMetaDataWidgetState();
|
||||
}
|
||||
|
||||
class _DocumentMetaDataWidgetState extends State<DocumentMetaDataWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
|
||||
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||
builder: (context, state) {
|
||||
if (state.metaData == null) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildListDelegate(
|
||||
[
|
||||
if (currentUser.canEditDocuments)
|
||||
ArchiveSerialNumberField(
|
||||
document: widget.document,
|
||||
).paddedOnly(bottom: widget.itemSpacing),
|
||||
DetailsItem.text(
|
||||
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
|
||||
.format(widget.document.modified),
|
||||
context: context,
|
||||
label: S.of(context)!.modifiedAt,
|
||||
).paddedOnly(bottom: widget.itemSpacing),
|
||||
DetailsItem.text(
|
||||
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
|
||||
.format(widget.document.added),
|
||||
context: context,
|
||||
label: S.of(context)!.addedAt,
|
||||
).paddedOnly(bottom: widget.itemSpacing),
|
||||
DetailsItem.text(
|
||||
state.metaData!.mediaFilename,
|
||||
context: context,
|
||||
label: S.of(context)!.mediaFilename,
|
||||
).paddedOnly(bottom: widget.itemSpacing),
|
||||
if (state.document.originalFileName != null)
|
||||
DetailsItem.text(
|
||||
state.document.originalFileName!,
|
||||
context: context,
|
||||
label: S.of(context)!.originalMD5Checksum,
|
||||
).paddedOnly(bottom: widget.itemSpacing),
|
||||
DetailsItem.text(
|
||||
state.metaData!.originalChecksum,
|
||||
context: context,
|
||||
label: S.of(context)!.originalMD5Checksum,
|
||||
).paddedOnly(bottom: widget.itemSpacing),
|
||||
DetailsItem.text(
|
||||
formatBytes(state.metaData!.originalSize, 2),
|
||||
context: context,
|
||||
label: S.of(context)!.originalFileSize,
|
||||
).paddedOnly(bottom: widget.itemSpacing),
|
||||
DetailsItem.text(
|
||||
state.metaData!.originalMimeType,
|
||||
context: context,
|
||||
label: S.of(context)!.originalMIMEType,
|
||||
).paddedOnly(bottom: widget.itemSpacing),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
return SliverList(
|
||||
delegate: SliverChildListDelegate(
|
||||
[
|
||||
if (currentUser.canEditDocuments)
|
||||
ArchiveSerialNumberField(
|
||||
document: document,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
|
||||
.format(document.modified),
|
||||
context: context,
|
||||
label: S.of(context)!.modifiedAt,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
|
||||
.format(document.added),
|
||||
context: context,
|
||||
label: S.of(context)!.addedAt,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
metaData.mediaFilename,
|
||||
context: context,
|
||||
label: S.of(context)!.mediaFilename,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
if (document.originalFileName != null)
|
||||
DetailsItem.text(
|
||||
document.originalFileName!,
|
||||
context: context,
|
||||
label: S.of(context)!.originalMD5Checksum,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
metaData.originalChecksum,
|
||||
context: context,
|
||||
label: S.of(context)!.originalMD5Checksum,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
formatBytes(metaData.originalSize, 2),
|
||||
context: context,
|
||||
label: S.of(context)!.originalFileSize,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
DetailsItem.text(
|
||||
metaData.originalMimeType,
|
||||
context: context,
|
||||
label: S.of(context)!.originalMIMEType,
|
||||
).paddedOnly(bottom: itemSpacing),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/widgets/highlighted_text.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/widgets/details_item.dart';
|
||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.dart';
|
||||
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
|
||||
@@ -27,6 +28,7 @@ class DocumentOverviewWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final user = context.watch<LocalUserAccount>().paperlessUser;
|
||||
final availableLabels = context.watch<LabelRepository>().state;
|
||||
|
||||
return SliverList.list(
|
||||
children: [
|
||||
if (document.title.isNotEmpty)
|
||||
|
||||
@@ -4,9 +4,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/constants.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_details/cubit/document_details_cubit.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/dialogs/select_file_type_dialog.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/file_download_type.dart';
|
||||
|
||||
@@ -22,22 +22,12 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
|
||||
required DocumentModel document,
|
||||
}) : _initialDocument = document,
|
||||
super(DocumentEditState(document: document)) {
|
||||
_notifier.addListener(this, onUpdated: replace);
|
||||
_labelRepository.addListener(
|
||||
_notifier.addListener(
|
||||
this,
|
||||
onChanged: (labels) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
correspondents: labels.correspondents,
|
||||
documentTypes: labels.documentTypes,
|
||||
storagePaths: labels.storagePaths,
|
||||
tags: labels.tags,
|
||||
),
|
||||
);
|
||||
onUpdated: (doc) {
|
||||
emit(state.copyWith(document: doc));
|
||||
},
|
||||
ids: [document.id],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,14 +59,9 @@ class DocumentEditCubit extends Cubit<DocumentEditState> {
|
||||
emit(state.copyWith(suggestions: suggestions));
|
||||
}
|
||||
|
||||
void replace(DocumentModel document) {
|
||||
emit(state.copyWith(document: document));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_notifier.removeListener(this);
|
||||
_labelRepository.removeListener(this);
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,5 @@ class DocumentEditState with _$DocumentEditState {
|
||||
const factory DocumentEditState({
|
||||
required DocumentModel document,
|
||||
FieldSuggestions? suggestions,
|
||||
@Default({}) Map<int, Correspondent> correspondents,
|
||||
@Default({}) Map<int, DocumentType> documentTypes,
|
||||
@Default({}) Map<int, StoragePath> storagePaths,
|
||||
@Default({}) Map<int, Tag> tags,
|
||||
}) = _DocumentEditState;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
@@ -10,18 +9,19 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/widgets/dialog_utils/pop_with_unsaved_changes.dart';
|
||||
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart';
|
||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
|
||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart';
|
||||
import 'package:paperless_mobile/theme.dart';
|
||||
|
||||
class DocumentEditPage extends StatefulWidget {
|
||||
const DocumentEditPage({
|
||||
@@ -32,7 +32,8 @@ class DocumentEditPage extends StatefulWidget {
|
||||
State<DocumentEditPage> createState() => _DocumentEditPageState();
|
||||
}
|
||||
|
||||
class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
class _DocumentEditPageState extends State<DocumentEditPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const fkTitle = "title";
|
||||
static const fkCorrespondent = "correspondent";
|
||||
static const fkTags = "tags";
|
||||
@@ -43,42 +44,27 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
bool _isShowingPdf = false;
|
||||
|
||||
late final AnimationController _animationController;
|
||||
late final Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
vsync: this,
|
||||
);
|
||||
_animation =
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInCubic)
|
||||
.drive(Tween<double>(begin: 0, end: 1));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentUser = context.watch<LocalUserAccount>().paperlessUser;
|
||||
return BlocConsumer<DocumentEditCubit, DocumentEditState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.document.content != current.document.content,
|
||||
listener: (context, state) {
|
||||
final contentField = _formKey.currentState?.fields[fkContent];
|
||||
if (contentField == null) {
|
||||
return;
|
||||
}
|
||||
if (contentField.isDirty) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
//TODO: INTL
|
||||
title: Text("Content has changed!"),
|
||||
content: Text(
|
||||
"The content of this document has changed. This can happen if the full content was not yet loaded. By accepting the incoming changes, your changes will be overwritten and therefore lost! Do you want to discard your changes in favor of the full content?",
|
||||
),
|
||||
actions: [
|
||||
DialogCancelButton(),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
contentField.didChange(state.document.content);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(S.of(context)!.discard),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
contentField.didChange(state.document.content);
|
||||
}
|
||||
},
|
||||
return BlocBuilder<DocumentEditCubit, DocumentEditState>(
|
||||
builder: (context, state) {
|
||||
final filteredSuggestions = state.suggestions;
|
||||
return PopWithUnsavedChanges(
|
||||
@@ -107,212 +93,226 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
doc.created != createdAt ||
|
||||
(doc.content != content && isContentTouched);
|
||||
},
|
||||
child: DefaultTabController(
|
||||
length: 2,
|
||||
child: FormBuilder(
|
||||
key: _formKey,
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
heroTag: "fab_document_edit",
|
||||
onPressed: () => _onSubmit(state.document),
|
||||
icon: const Icon(Icons.save),
|
||||
label: Text(S.of(context)!.saveChanges),
|
||||
),
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context)!.editDocument),
|
||||
bottom: TabBar(
|
||||
tabs: [
|
||||
Tab(text: S.of(context)!.overview),
|
||||
Tab(text: S.of(context)!.content)
|
||||
],
|
||||
),
|
||||
),
|
||||
extendBody: true,
|
||||
body: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
top: 8,
|
||||
left: 8,
|
||||
right: 8,
|
||||
),
|
||||
child: FormBuilder(
|
||||
key: _formKey,
|
||||
child: TabBarView(
|
||||
children: [
|
||||
ListView(
|
||||
children: [
|
||||
_buildTitleFormField(state.document.title).padded(),
|
||||
_buildCreatedAtFormField(
|
||||
state.document.created,
|
||||
filteredSuggestions,
|
||||
).padded(),
|
||||
// Correspondent form field
|
||||
if (currentUser.canViewCorrespondents)
|
||||
Column(
|
||||
children: [
|
||||
LabelFormField<Correspondent>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
onAddLabel: (currentInput) =>
|
||||
CreateLabelRoute(
|
||||
LabelType.correspondent,
|
||||
name: currentInput,
|
||||
).push<Correspondent>(context),
|
||||
addLabelText:
|
||||
S.of(context)!.addCorrespondent,
|
||||
labelText: S.of(context)!.correspondent,
|
||||
options: context
|
||||
.watch<DocumentEditCubit>()
|
||||
.state
|
||||
.correspondents,
|
||||
initialValue: state
|
||||
.document.correspondent !=
|
||||
null
|
||||
? SetIdQueryParameter(
|
||||
id: state.document.correspondent!)
|
||||
: const UnsetIdQueryParameter(),
|
||||
name: fkCorrespondent,
|
||||
prefixIcon:
|
||||
const Icon(Icons.person_outlined),
|
||||
allowSelectUnassigned: true,
|
||||
canCreateNewLabel:
|
||||
currentUser.canCreateCorrespondents,
|
||||
suggestions:
|
||||
filteredSuggestions?.correspondents ??
|
||||
[],
|
||||
),
|
||||
],
|
||||
).padded(),
|
||||
// DocumentType form field
|
||||
if (currentUser.canViewDocumentTypes)
|
||||
Column(
|
||||
children: [
|
||||
LabelFormField<DocumentType>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
onAddLabel: (currentInput) =>
|
||||
CreateLabelRoute(
|
||||
LabelType.documentType,
|
||||
name: currentInput,
|
||||
).push<DocumentType>(context),
|
||||
canCreateNewLabel:
|
||||
currentUser.canCreateDocumentTypes,
|
||||
addLabelText:
|
||||
S.of(context)!.addDocumentType,
|
||||
labelText: S.of(context)!.documentType,
|
||||
initialValue: state.document.documentType !=
|
||||
null
|
||||
? SetIdQueryParameter(
|
||||
id: state.document.documentType!)
|
||||
: const UnsetIdQueryParameter(),
|
||||
options: state.documentTypes,
|
||||
name: _DocumentEditPageState.fkDocumentType,
|
||||
prefixIcon:
|
||||
const Icon(Icons.description_outlined),
|
||||
allowSelectUnassigned: true,
|
||||
suggestions:
|
||||
filteredSuggestions?.documentTypes ??
|
||||
[],
|
||||
),
|
||||
],
|
||||
).padded(),
|
||||
// StoragePath form field
|
||||
if (currentUser.canViewStoragePaths)
|
||||
Column(
|
||||
children: [
|
||||
LabelFormField<StoragePath>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
onAddLabel: (currentInput) =>
|
||||
CreateLabelRoute(
|
||||
LabelType.storagePath,
|
||||
name: currentInput,
|
||||
).push<StoragePath>(context),
|
||||
canCreateNewLabel:
|
||||
currentUser.canCreateStoragePaths,
|
||||
addLabelText: S.of(context)!.addStoragePath,
|
||||
labelText: S.of(context)!.storagePath,
|
||||
options: state.storagePaths,
|
||||
initialValue:
|
||||
state.document.storagePath != null
|
||||
? SetIdQueryParameter(
|
||||
id: state.document.storagePath!)
|
||||
: const UnsetIdQueryParameter(),
|
||||
name: fkStoragePath,
|
||||
prefixIcon:
|
||||
const Icon(Icons.folder_outlined),
|
||||
allowSelectUnassigned: true,
|
||||
),
|
||||
],
|
||||
).padded(),
|
||||
// Tag form field
|
||||
if (currentUser.canViewTags)
|
||||
TagsFormField(
|
||||
options: state.tags,
|
||||
name: fkTags,
|
||||
allowOnlySelection: true,
|
||||
allowCreation: true,
|
||||
allowExclude: false,
|
||||
suggestions: filteredSuggestions?.tags ?? [],
|
||||
initialValue: IdsTagsQuery(
|
||||
include: state.document.tags.toList(),
|
||||
),
|
||||
).padded(),
|
||||
if (filteredSuggestions?.tags
|
||||
.toSet()
|
||||
.difference(state.document.tags.toSet())
|
||||
.isNotEmpty ??
|
||||
false)
|
||||
const SizedBox(height: 64),
|
||||
],
|
||||
),
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: fkContent,
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
initialValue: state.document.content,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 84),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context)!.editDocument),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: _isShowingPdf
|
||||
? S.of(context)!.hidePdf
|
||||
: S.of(context)!.showPdf,
|
||||
padding: EdgeInsets.all(12),
|
||||
icon: AnimatedCrossFade(
|
||||
duration: _animationController.duration!,
|
||||
reverseDuration: _animationController.reverseDuration,
|
||||
crossFadeState: _isShowingPdf
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: Icon(Icons.visibility_off_outlined),
|
||||
secondChild: Icon(Icons.visibility_outlined),
|
||||
),
|
||||
onPressed: () {
|
||||
if (_isShowingPdf) {
|
||||
setState(() {
|
||||
_isShowingPdf = false;
|
||||
});
|
||||
_animationController.reverse();
|
||||
} else {
|
||||
setState(() {
|
||||
_isShowingPdf = true;
|
||||
});
|
||||
_animationController.forward();
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
DefaultTabController(
|
||||
length: 2,
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
floatingActionButton: !_isShowingPdf
|
||||
? FloatingActionButton.extended(
|
||||
heroTag: "fab_document_edit",
|
||||
onPressed: () => _onSubmit(state.document),
|
||||
icon: const Icon(Icons.save),
|
||||
label: Text(S.of(context)!.saveChanges),
|
||||
)
|
||||
: null,
|
||||
appBar: TabBar(
|
||||
tabs: [
|
||||
Tab(text: S.of(context)!.overview),
|
||||
Tab(text: S.of(context)!.content),
|
||||
],
|
||||
),
|
||||
extendBody: true,
|
||||
body: _buildEditForm(
|
||||
context,
|
||||
state,
|
||||
filteredSuggestions,
|
||||
currentUser,
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
alignment: Alignment.bottomLeft,
|
||||
scale: _animation.value,
|
||||
child: DocumentView(
|
||||
showAppBar: false,
|
||||
showControls: false,
|
||||
documentBytes: context
|
||||
.read<PaperlessDocumentsApi>()
|
||||
.downloadDocument(state.document.id),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool _isFieldDirty(DocumentModel document) {
|
||||
final fkState = _formKey.currentState;
|
||||
if (fkState == null) {
|
||||
return false;
|
||||
}
|
||||
fkState.save();
|
||||
final (
|
||||
title,
|
||||
correspondent,
|
||||
documentType,
|
||||
storagePath,
|
||||
tags,
|
||||
createdAt,
|
||||
content
|
||||
) = _currentValues;
|
||||
return document.title != title ||
|
||||
document.correspondent != correspondent ||
|
||||
document.documentType != documentType ||
|
||||
document.storagePath != storagePath ||
|
||||
const UnorderedIterableEquality().equals(document.tags, tags) ||
|
||||
document.created != createdAt ||
|
||||
document.content != content;
|
||||
Padding _buildEditForm(BuildContext context, DocumentEditState state,
|
||||
FieldSuggestions? filteredSuggestions, UserModel currentUser) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: TabBarView(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
ListView(
|
||||
children: [
|
||||
SizedBox(height: 16),
|
||||
_buildTitleFormField(state.document.title).padded(),
|
||||
_buildCreatedAtFormField(
|
||||
state.document.created,
|
||||
filteredSuggestions,
|
||||
).padded(),
|
||||
// Correspondent form field
|
||||
if (currentUser.canViewCorrespondents)
|
||||
Column(
|
||||
children: [
|
||||
LabelFormField<Correspondent>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
onAddLabel: (currentInput) => CreateLabelRoute(
|
||||
LabelType.correspondent,
|
||||
name: currentInput,
|
||||
).push<Correspondent>(context),
|
||||
addLabelText: S.of(context)!.addCorrespondent,
|
||||
labelText: S.of(context)!.correspondent,
|
||||
options:
|
||||
context.watch<LabelRepository>().state.correspondents,
|
||||
initialValue: state.document.correspondent != null
|
||||
? SetIdQueryParameter(
|
||||
id: state.document.correspondent!)
|
||||
: const UnsetIdQueryParameter(),
|
||||
name: fkCorrespondent,
|
||||
prefixIcon: const Icon(Icons.person_outlined),
|
||||
allowSelectUnassigned: true,
|
||||
canCreateNewLabel: currentUser.canCreateCorrespondents,
|
||||
suggestions: filteredSuggestions?.correspondents ?? [],
|
||||
),
|
||||
],
|
||||
).padded(),
|
||||
// DocumentType form field
|
||||
if (currentUser.canViewDocumentTypes)
|
||||
Column(
|
||||
children: [
|
||||
LabelFormField<DocumentType>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
onAddLabel: (currentInput) => CreateLabelRoute(
|
||||
LabelType.documentType,
|
||||
name: currentInput,
|
||||
).push<DocumentType>(context),
|
||||
canCreateNewLabel: currentUser.canCreateDocumentTypes,
|
||||
addLabelText: S.of(context)!.addDocumentType,
|
||||
labelText: S.of(context)!.documentType,
|
||||
initialValue: state.document.documentType != null
|
||||
? SetIdQueryParameter(
|
||||
id: state.document.documentType!)
|
||||
: const UnsetIdQueryParameter(),
|
||||
options:
|
||||
context.watch<LabelRepository>().state.documentTypes,
|
||||
name: _DocumentEditPageState.fkDocumentType,
|
||||
prefixIcon: const Icon(Icons.description_outlined),
|
||||
allowSelectUnassigned: true,
|
||||
suggestions: filteredSuggestions?.documentTypes ?? [],
|
||||
),
|
||||
],
|
||||
).padded(),
|
||||
// StoragePath form field
|
||||
if (currentUser.canViewStoragePaths)
|
||||
Column(
|
||||
children: [
|
||||
LabelFormField<StoragePath>(
|
||||
showAnyAssignedOption: false,
|
||||
showNotAssignedOption: false,
|
||||
onAddLabel: (currentInput) => CreateLabelRoute(
|
||||
LabelType.storagePath,
|
||||
name: currentInput,
|
||||
).push<StoragePath>(context),
|
||||
canCreateNewLabel: currentUser.canCreateStoragePaths,
|
||||
addLabelText: S.of(context)!.addStoragePath,
|
||||
labelText: S.of(context)!.storagePath,
|
||||
options:
|
||||
context.watch<LabelRepository>().state.storagePaths,
|
||||
initialValue: state.document.storagePath != null
|
||||
? SetIdQueryParameter(id: state.document.storagePath!)
|
||||
: const UnsetIdQueryParameter(),
|
||||
name: fkStoragePath,
|
||||
prefixIcon: const Icon(Icons.folder_outlined),
|
||||
allowSelectUnassigned: true,
|
||||
),
|
||||
],
|
||||
).padded(),
|
||||
// Tag form field
|
||||
if (currentUser.canViewTags)
|
||||
TagsFormField(
|
||||
options: context.watch<LabelRepository>().state.tags,
|
||||
name: fkTags,
|
||||
allowOnlySelection: true,
|
||||
allowCreation: true,
|
||||
allowExclude: false,
|
||||
suggestions: filteredSuggestions?.tags ?? [],
|
||||
initialValue: IdsTagsQuery(
|
||||
include: state.document.tags.toList(),
|
||||
),
|
||||
).padded(),
|
||||
|
||||
const SizedBox(height: 140),
|
||||
],
|
||||
),
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
FormBuilderTextField(
|
||||
name: fkContent,
|
||||
maxLines: null,
|
||||
keyboardType: TextInputType.multiline,
|
||||
initialValue: state.document.content,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 84),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
@@ -334,7 +334,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
fkState.getRawValue<IdQueryParameter?>(fkStoragePath);
|
||||
final tagsParam = fkState.getRawValue<TagsQuery?>(fkTags);
|
||||
final title = fkState.getRawValue<String?>(fkTitle);
|
||||
final created = fkState.getRawValue<DateTime?>(fkCreatedDate);
|
||||
final created = fkState.getRawValue<FormDateTime?>(fkCreatedDate);
|
||||
final correspondent = switch (correspondentParam) {
|
||||
SetIdQueryParameter(id: var id) => id,
|
||||
_ => null,
|
||||
@@ -359,7 +359,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
documentType,
|
||||
storagePath,
|
||||
tags,
|
||||
created,
|
||||
created?.toDateTime(),
|
||||
content
|
||||
);
|
||||
}
|
||||
@@ -401,6 +401,12 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
name: fkTitle,
|
||||
decoration: InputDecoration(
|
||||
label: Text(S.of(context)!.title),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_formKey.currentState?.fields[fkTitle]?.didChange(null);
|
||||
},
|
||||
),
|
||||
),
|
||||
initialValue: initialTitle,
|
||||
);
|
||||
@@ -409,18 +415,15 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
Widget _buildCreatedAtFormField(
|
||||
DateTime? initialCreatedAtDate, FieldSuggestions? filteredSuggestions) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FormBuilderDateTimePicker(
|
||||
inputType: InputType.date,
|
||||
FormBuilderLocalizedDatePicker(
|
||||
name: fkCreatedDate,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.calendar_month_outlined),
|
||||
label: Text(S.of(context)!.createdAt),
|
||||
),
|
||||
initialValue: initialCreatedAtDate,
|
||||
format: DateFormat.yMMMMd(Localizations.localeOf(context).toString()),
|
||||
initialEntryMode: DatePickerEntryMode.calendar,
|
||||
labelText: S.of(context)!.createdAt,
|
||||
firstDate: DateTime(1970, 1, 1),
|
||||
lastDate: DateTime.now(),
|
||||
locale: Localizations.localeOf(context),
|
||||
prefixIcon: Icon(Icons.calendar_today),
|
||||
),
|
||||
if (filteredSuggestions?.hasSuggestedDates ?? false)
|
||||
_buildSuggestionsSkeleton<DateTime>(
|
||||
@@ -430,7 +433,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
||||
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
|
||||
.format(itemData)),
|
||||
onPressed: () => _formKey.currentState?.fields[fkCreatedDate]
|
||||
?.didChange(itemData),
|
||||
?.didChange(FormDateTime.fromDateTime(itemData)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/logging/data/logger.dart';
|
||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||
@@ -18,13 +19,21 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
|
||||
: super(const InitialDocumentScannerState());
|
||||
|
||||
Future<void> initialize() async {
|
||||
debugPrint("Restoring scans...");
|
||||
logger.fd(
|
||||
"Restoring scans...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "initialize",
|
||||
);
|
||||
emit(const RestoringDocumentScannerState());
|
||||
final tempDir = await FileService.temporaryScansDirectory;
|
||||
final tempDir = FileService.instance.temporaryScansDirectory;
|
||||
final allFiles = tempDir.list().whereType<File>();
|
||||
final scans =
|
||||
await allFiles.where((event) => event.path.endsWith(".jpeg")).toList();
|
||||
debugPrint("Restored ${scans.length} scans.");
|
||||
logger.fd(
|
||||
"Restored ${scans.length} scans.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "initialize",
|
||||
);
|
||||
emit(
|
||||
scans.isEmpty
|
||||
? const InitialDocumentScannerState()
|
||||
@@ -74,7 +83,7 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
|
||||
String fileName,
|
||||
String locale,
|
||||
) async {
|
||||
var file = await FileService.saveToFile(bytes, fileName);
|
||||
var file = await FileService.instance.saveToFile(bytes, fileName);
|
||||
_notificationService.notifyFileSaved(
|
||||
filename: fileName,
|
||||
filePath: file.path,
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/constants.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/global/constants.dart';
|
||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||
@@ -227,9 +227,10 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
if (!isGranted) {
|
||||
return;
|
||||
}
|
||||
final file = await FileService.allocateTemporaryFile(
|
||||
final file = await FileService.instance.allocateTemporaryFile(
|
||||
PaperlessDirectoryType.scans,
|
||||
extension: 'jpeg',
|
||||
create: true,
|
||||
);
|
||||
if (kDebugMode) {
|
||||
dev.log('[ScannerPage] Created temporary file: ${file.path}');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
|
||||
|
||||
@@ -4,7 +4,8 @@ import 'dart:math' as math;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
|
||||
import 'package:paperless_mobile/features/document_search/view/remove_history_entry_dialog.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||
@@ -219,8 +220,12 @@ class _DocumentSearchPageState extends State<DocumentSearchPage> {
|
||||
hasLoaded: state.hasLoaded,
|
||||
enableHeroAnimation: false,
|
||||
onTap: (document) {
|
||||
DocumentDetailsRoute($extra: document, isLabelClickable: false)
|
||||
.push(context);
|
||||
DocumentDetailsRoute(
|
||||
title: document.title,
|
||||
id: document.id,
|
||||
isLabelClickable: false,
|
||||
thumbnailUrl: document.buildThumbnailUrl(context),
|
||||
).push(context);
|
||||
},
|
||||
)
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/features/document_search/view/document_search_bar.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/manage_accounts_page.dart';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
@@ -21,18 +20,7 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||
this._documentApi,
|
||||
this._connectivityStatusService,
|
||||
this._tasksNotifier,
|
||||
) : super(const DocumentUploadState()) {
|
||||
_labelRepository.addListener(
|
||||
this,
|
||||
onChanged: (labels) {
|
||||
emit(state.copyWith(
|
||||
correspondents: labels.correspondents,
|
||||
documentTypes: labels.documentTypes,
|
||||
tags: labels.tags,
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
) : super(const DocumentUploadState());
|
||||
|
||||
Future<String?> upload(
|
||||
Uint8List bytes, {
|
||||
@@ -44,7 +32,6 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||
Iterable<int> tags = const [],
|
||||
DateTime? createdAt,
|
||||
int? asn,
|
||||
void Function(double)? onProgressChanged,
|
||||
}) async {
|
||||
final taskId = await _documentApi.create(
|
||||
bytes,
|
||||
@@ -55,17 +42,15 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
|
||||
tags: tags,
|
||||
createdAt: createdAt,
|
||||
asn: asn,
|
||||
onProgressChanged: onProgressChanged,
|
||||
onProgressChanged: (progress) {
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(uploadProgress: progress));
|
||||
}
|
||||
},
|
||||
);
|
||||
if (taskId != null) {
|
||||
_tasksNotifier.listenToTaskChanges(taskId);
|
||||
}
|
||||
return taskId;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
_labelRepository.removeListener(this);
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,17 @@
|
||||
part of 'document_upload_cubit.dart';
|
||||
|
||||
@immutable
|
||||
class DocumentUploadState extends Equatable {
|
||||
final Map<int, Tag> tags;
|
||||
final Map<int, Correspondent> correspondents;
|
||||
final Map<int, DocumentType> documentTypes;
|
||||
|
||||
class DocumentUploadState {
|
||||
final double? uploadProgress;
|
||||
const DocumentUploadState({
|
||||
this.tags = const {},
|
||||
this.correspondents = const {},
|
||||
this.documentTypes = const {},
|
||||
this.uploadProgress,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [
|
||||
tags,
|
||||
correspondents,
|
||||
documentTypes,
|
||||
];
|
||||
|
||||
DocumentUploadState copyWith({
|
||||
Map<int, Tag>? tags,
|
||||
Map<int, Correspondent>? correspondents,
|
||||
Map<int, DocumentType>? documentTypes,
|
||||
double? uploadProgress,
|
||||
}) {
|
||||
return DocumentUploadState(
|
||||
tags: tags ?? this.tags,
|
||||
correspondents: correspondents ?? this.correspondents,
|
||||
documentTypes: documentTypes ?? this.documentTypes,
|
||||
uploadProgress: uploadProgress ?? this.uploadProgress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,27 +6,24 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_localized_date_picker.dart';
|
||||
import 'package:paperless_mobile/core/widgets/future_or_builder.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
|
||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
|
||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart';
|
||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||
import 'package:paperless_mobile/features/logging/data/logger.dart';
|
||||
import 'package:paperless_mobile/features/sharing/view/widgets/file_thumbnail.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DocumentUploadResult {
|
||||
final bool success;
|
||||
@@ -61,7 +58,6 @@ class _DocumentUploadPreparationPageState
|
||||
|
||||
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
||||
Map<String, String> _errors = {};
|
||||
bool _isUploadLoading = false;
|
||||
late bool _syncTitleAndFilename;
|
||||
bool _showDatePickerDeleteIcon = false;
|
||||
final _now = DateTime.now();
|
||||
@@ -74,21 +70,32 @@ class _DocumentUploadPreparationPageState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: false,
|
||||
resizeToAvoidBottomInset: true,
|
||||
floatingActionButton: Visibility(
|
||||
visible: MediaQuery.of(context).viewInsets.bottom == 0,
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: "fab_document_upload",
|
||||
onPressed: _onSubmit,
|
||||
label: Text(S.of(context)!.upload),
|
||||
icon: const Icon(Icons.upload),
|
||||
),
|
||||
),
|
||||
body: BlocBuilder<DocumentUploadCubit, DocumentUploadState>(
|
||||
builder: (context, state) {
|
||||
return FormBuilder(
|
||||
final labels = context.watch<LabelRepository>().state;
|
||||
return BlocBuilder<DocumentUploadCubit, DocumentUploadState>(
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: false,
|
||||
resizeToAvoidBottomInset: true,
|
||||
floatingActionButton: Visibility(
|
||||
visible: MediaQuery.of(context).viewInsets.bottom == 0,
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: "fab_document_upload",
|
||||
onPressed: state.uploadProgress == null ? _onSubmit : null,
|
||||
label: state.uploadProgress == null
|
||||
? Text(S.of(context)!.upload)
|
||||
: Text("Uploading..."), //TODO: INTL
|
||||
icon: state.uploadProgress == null
|
||||
? const Icon(Icons.upload)
|
||||
: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
value: state.uploadProgress,
|
||||
)).padded(4),
|
||||
),
|
||||
),
|
||||
body: FormBuilder(
|
||||
key: _formKey,
|
||||
child: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
@@ -96,7 +103,7 @@ class _DocumentUploadPreparationPageState
|
||||
handle:
|
||||
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||
sliver: SliverAppBar(
|
||||
leading: BackButton(),
|
||||
leading: const BackButton(),
|
||||
pinned: true,
|
||||
expandedHeight: 150,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
@@ -104,7 +111,7 @@ class _DocumentUploadPreparationPageState
|
||||
future: widget.fileBytes,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return SizedBox.shrink();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return FileThumbnail(
|
||||
bytes: snapshot.data!,
|
||||
@@ -116,12 +123,6 @@ class _DocumentUploadPreparationPageState
|
||||
title: Text(S.of(context)!.prepareDocument),
|
||||
collapseMode: CollapseMode.pin,
|
||||
),
|
||||
bottom: _isUploadLoading
|
||||
? PreferredSize(
|
||||
child: LinearProgressIndicator(),
|
||||
preferredSize: Size.fromHeight(4.0),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -218,32 +219,13 @@ class _DocumentUploadPreparationPageState
|
||||
),
|
||||
),
|
||||
// Created at
|
||||
FormBuilderDateTimePicker(
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
format: DateFormat.yMMMMd(
|
||||
Localizations.localeOf(context).toString()),
|
||||
inputType: InputType.date,
|
||||
FormBuilderLocalizedDatePicker(
|
||||
name: DocumentModel.createdKey,
|
||||
initialValue: null,
|
||||
onChanged: (value) {
|
||||
setState(() =>
|
||||
_showDatePickerDeleteIcon = value != null);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
prefixIcon:
|
||||
const Icon(Icons.calendar_month_outlined),
|
||||
labelText: S.of(context)!.createdAt + " *",
|
||||
suffixIcon: _showDatePickerDeleteIcon
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
_formKey.currentState!
|
||||
.fields[DocumentModel.createdKey]
|
||||
?.didChange(null);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
firstDate: DateTime(1970, 1, 1),
|
||||
lastDate: DateTime.now(),
|
||||
locale: Localizations.localeOf(context),
|
||||
labelText: S.of(context)!.createdAt + " *",
|
||||
allowUnset: true,
|
||||
),
|
||||
// Correspondent
|
||||
if (context
|
||||
@@ -260,7 +242,7 @@ class _DocumentUploadPreparationPageState
|
||||
addLabelText: S.of(context)!.addCorrespondent,
|
||||
labelText: S.of(context)!.correspondent + " *",
|
||||
name: DocumentModel.correspondentKey,
|
||||
options: state.correspondents,
|
||||
options: labels.correspondents,
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
allowSelectUnassigned: true,
|
||||
canCreateNewLabel: context
|
||||
@@ -283,7 +265,7 @@ class _DocumentUploadPreparationPageState
|
||||
addLabelText: S.of(context)!.addDocumentType,
|
||||
labelText: S.of(context)!.documentType + " *",
|
||||
name: DocumentModel.documentTypeKey,
|
||||
options: state.documentTypes,
|
||||
options: labels.documentTypes,
|
||||
prefixIcon:
|
||||
const Icon(Icons.description_outlined),
|
||||
allowSelectUnassigned: true,
|
||||
@@ -301,7 +283,7 @@ class _DocumentUploadPreparationPageState
|
||||
allowCreation: true,
|
||||
allowExclude: false,
|
||||
allowOnlySelection: true,
|
||||
options: state.tags,
|
||||
options: labels.tags,
|
||||
),
|
||||
Text(
|
||||
"* " + S.of(context)!.uploadInferValuesHint,
|
||||
@@ -317,9 +299,9 @@ class _DocumentUploadPreparationPageState
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -327,7 +309,6 @@ class _DocumentUploadPreparationPageState
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final cubit = context.read<DocumentUploadCubit>();
|
||||
try {
|
||||
setState(() => _isUploadLoading = true);
|
||||
final formValues = _formKey.currentState!.value;
|
||||
|
||||
final correspondentParam =
|
||||
@@ -335,7 +316,7 @@ class _DocumentUploadPreparationPageState
|
||||
final docTypeParam =
|
||||
formValues[DocumentModel.documentTypeKey] as IdQueryParameter?;
|
||||
final tagsParam = formValues[DocumentModel.tagsKey] as TagsQuery?;
|
||||
final createdAt = formValues[DocumentModel.createdKey] as DateTime?;
|
||||
final createdAt = formValues[DocumentModel.createdKey] as FormDateTime?;
|
||||
final title = formValues[DocumentModel.titleKey] as String;
|
||||
final correspondent = switch (correspondentParam) {
|
||||
SetIdQueryParameter(id: var id) => id,
|
||||
@@ -364,7 +345,7 @@ class _DocumentUploadPreparationPageState
|
||||
documentType: docType,
|
||||
correspondent: correspondent,
|
||||
tags: tags,
|
||||
createdAt: createdAt,
|
||||
createdAt: createdAt?.toDateTime(),
|
||||
asn: asn,
|
||||
);
|
||||
showSnackBar(
|
||||
@@ -376,14 +357,19 @@ class _DocumentUploadPreparationPageState
|
||||
showErrorMessage(context, error, stackTrace);
|
||||
} on PaperlessFormValidationException catch (exception) {
|
||||
setState(() => _errors = exception.validationMessages);
|
||||
} catch (unknownError, stackTrace) {
|
||||
debugPrint(unknownError.toString());
|
||||
} catch (error, stackTrace) {
|
||||
logger.fe(
|
||||
"An unknown error occurred during document upload.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "_onSubmit",
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
showErrorMessage(
|
||||
context, const PaperlessApiException.unknown(), stackTrace);
|
||||
} finally {
|
||||
setState(() {
|
||||
_isUploadLoading = false;
|
||||
});
|
||||
context,
|
||||
const PaperlessApiException.unknown(),
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
@@ -44,18 +45,15 @@ class DocumentsCubit extends Cubit<DocumentsState>
|
||||
replace(document);
|
||||
emit(
|
||||
state.copyWith(
|
||||
selection: state.selection
|
||||
.map((e) => e.id == document.id ? document : e)
|
||||
.toList(),
|
||||
),
|
||||
selection:
|
||||
state.selection.withDocumentreplaced(document).toList()),
|
||||
);
|
||||
},
|
||||
onDeleted: (document) {
|
||||
remove(document);
|
||||
emit(
|
||||
state.copyWith(
|
||||
selection:
|
||||
state.selection.where((e) => e.id != document.id).toList(),
|
||||
selection: state.selection.withDocumentRemoved(document).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -74,7 +72,6 @@ class DocumentsCubit extends Cubit<DocumentsState>
|
||||
}
|
||||
|
||||
Future<void> bulkDelete(List<DocumentModel> documents) async {
|
||||
debugPrint("[DocumentsCubit] bulkRemove");
|
||||
await api.bulkAction(
|
||||
BulkDeleteAction(documents.map((doc) => doc.id)),
|
||||
);
|
||||
@@ -85,7 +82,6 @@ class DocumentsCubit extends Cubit<DocumentsState>
|
||||
}
|
||||
|
||||
void toggleDocumentSelection(DocumentModel model) {
|
||||
debugPrint("[DocumentsCubit] toggleSelection");
|
||||
if (state.selectedIds.contains(model.id)) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -100,12 +96,10 @@ class DocumentsCubit extends Cubit<DocumentsState>
|
||||
}
|
||||
|
||||
void resetSelection() {
|
||||
debugPrint("[DocumentsCubit] resetSelection");
|
||||
emit(state.copyWith(selection: []));
|
||||
}
|
||||
|
||||
void reset() {
|
||||
debugPrint("[DocumentsCubit] reset");
|
||||
emit(const DocumentsState());
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,13 @@ import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||
class DocumentView extends StatefulWidget {
|
||||
final Future<Uint8List> documentBytes;
|
||||
final String? title;
|
||||
final bool showAppBar;
|
||||
final bool showControls;
|
||||
const DocumentView({
|
||||
Key? key,
|
||||
required this.documentBytes,
|
||||
this.showAppBar = true,
|
||||
this.showControls = true,
|
||||
this.title,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -27,43 +31,47 @@ class _DocumentViewState extends State<DocumentView> {
|
||||
final canGoToNextPage = isInitialized && _currentPage! + 1 < _totalPages!;
|
||||
final canGoToPreviousPage = isInitialized && _currentPage! > 0;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: widget.title != null ? Text(widget.title!) : null,
|
||||
),
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
appBar: widget.showAppBar
|
||||
? AppBar(
|
||||
title: widget.title != null ? Text(widget.title!) : null,
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: widget.showControls
|
||||
? BottomAppBar(
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton.filled(
|
||||
onPressed: canGoToPreviousPage
|
||||
? () {
|
||||
_controller?.setPage(_currentPage! - 1);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_left),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
IconButton.filled(
|
||||
onPressed: canGoToNextPage
|
||||
? () {
|
||||
_controller?.setPage(_currentPage! + 1);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_right),
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton.filled(
|
||||
onPressed: canGoToPreviousPage
|
||||
? () {
|
||||
_controller?.setPage(_currentPage! - 1);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_left),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
IconButton.filled(
|
||||
onPressed: canGoToNextPage
|
||||
? () {
|
||||
_controller?.setPage(_currentPage! + 1);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_right),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_currentPage != null && _totalPages != null)
|
||||
Text(
|
||||
"${_currentPage! + 1}/$_totalPages",
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_currentPage != null && _totalPages != null)
|
||||
Text(
|
||||
"${_currentPage! + 1}/$_totalPages",
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
body: FutureBuilder(
|
||||
future: widget.documentBytes,
|
||||
builder: (context, snapshot) {
|
||||
@@ -93,12 +101,7 @@ class _DocumentViewState extends State<DocumentView> {
|
||||
onViewCreated: (controller) {
|
||||
_controller = controller;
|
||||
},
|
||||
onError: (error) {
|
||||
print(error.toString());
|
||||
},
|
||||
onPageError: (page, error) {
|
||||
print('$page: ${error.toString()}');
|
||||
},
|
||||
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -6,7 +6,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.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/documents/cubit/documents_cubit.dart';
|
||||
@@ -109,7 +110,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
}
|
||||
|
||||
void _scrollExtentChangedListener() {
|
||||
const threshold = 400;
|
||||
const threshold = kToolbarHeight * 2;
|
||||
final offset =
|
||||
_nestedScrollViewKey.currentState!.innerController.position.pixels;
|
||||
if (offset < threshold && _showExtendedFab == false) {
|
||||
@@ -404,7 +405,11 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
return SliverAdaptiveDocumentsView(
|
||||
viewType: state.viewType,
|
||||
onTap: (document) {
|
||||
DocumentDetailsRoute($extra: document).push(context);
|
||||
DocumentDetailsRoute(
|
||||
title: document.title,
|
||||
id: document.id,
|
||||
thumbnailUrl: document.buildThumbnailUrl(context),
|
||||
).push(context);
|
||||
},
|
||||
onSelected:
|
||||
context.read<DocumentsCubit>().toggleDocumentSelection,
|
||||
@@ -424,6 +429,9 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
);
|
||||
},
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(height: 96),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/document_grid_loading_widget.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_detailed_item.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_grid_item.dart';
|
||||
@@ -159,7 +160,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 4,
|
||||
crossAxisSpacing: 4,
|
||||
mainAxisExtent: 356,
|
||||
mainAxisExtent: 324,
|
||||
),
|
||||
itemCount: documents.length,
|
||||
itemBuilder: (context, index) {
|
||||
@@ -176,7 +177,7 @@ class SliverAdaptiveDocumentsView extends AdaptiveDocumentsView {
|
||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||
onStoragePathSelected: onStoragePathSelected,
|
||||
enableHeroAnimation: enableHeroAnimation,
|
||||
);
|
||||
).paddedSymmetrically(horizontal: 4);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DateAndDocumentTypeLabelWidget extends StatelessWidget {
|
||||
const DateAndDocumentTypeLabelWidget({
|
||||
super.key,
|
||||
required this.document,
|
||||
required this.onDocumentTypeSelected,
|
||||
});
|
||||
|
||||
final DocumentModel document;
|
||||
final void Function(int? documentTypeId)? onDocumentTypeSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final subtitleStyle =
|
||||
Theme.of(context).textTheme.labelMedium?.apply(color: Colors.grey);
|
||||
return RichText(
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
text: DateFormat.yMMMMd(Localizations.localeOf(context).toString())
|
||||
.format(document.created),
|
||||
style: subtitleStyle,
|
||||
children: document.documentType != null
|
||||
? [
|
||||
const TextSpan(text: '\u30FB'),
|
||||
WidgetSpan(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
onTap: onDocumentTypeSelected != null
|
||||
? () => onDocumentTypeSelected!(document.documentType)
|
||||
: null,
|
||||
child: Text(
|
||||
context
|
||||
.watch<LabelRepository>()
|
||||
.state
|
||||
.documentTypes[document.documentType]!
|
||||
.name,
|
||||
style: subtitleStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import 'package:provider/provider.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class DocumentPreview extends StatelessWidget {
|
||||
final DocumentModel document;
|
||||
final int documentId;
|
||||
final String? title;
|
||||
final BoxFit fit;
|
||||
final Alignment alignment;
|
||||
final double borderRadius;
|
||||
@@ -19,13 +20,14 @@ class DocumentPreview extends StatelessWidget {
|
||||
|
||||
const DocumentPreview({
|
||||
super.key,
|
||||
required this.document,
|
||||
required this.documentId,
|
||||
this.fit = BoxFit.cover,
|
||||
this.alignment = Alignment.topCenter,
|
||||
this.borderRadius = 12.0,
|
||||
this.enableHero = true,
|
||||
this.scale = 1.1,
|
||||
this.isClickable = true,
|
||||
this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -34,12 +36,12 @@ class DocumentPreview extends StatelessWidget {
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: isClickable
|
||||
? () => DocumentPreviewRoute($extra: document).push(context)
|
||||
? () => DocumentPreviewRoute(id: documentId).push(context)
|
||||
: null,
|
||||
child: Builder(builder: (context) {
|
||||
if (enableHero) {
|
||||
return Hero(
|
||||
tag: "thumb_${document.id}",
|
||||
tag: "thumb_$documentId",
|
||||
child: _buildPreview(context),
|
||||
);
|
||||
}
|
||||
@@ -57,10 +59,9 @@ class DocumentPreview extends StatelessWidget {
|
||||
child: CachedNetworkImage(
|
||||
fit: fit,
|
||||
alignment: alignment,
|
||||
cacheKey: "thumb_${document.id}",
|
||||
imageUrl: context
|
||||
.read<PaperlessDocumentsApi>()
|
||||
.getThumbnailUrl(document.id),
|
||||
cacheKey: "thumb_$documentId",
|
||||
imageUrl:
|
||||
context.read<PaperlessDocumentsApi>().getThumbnailUrl(documentId),
|
||||
errorWidget: (ctxt, msg, __) => Text(msg),
|
||||
placeholder: (context, value) => Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/date_and_document_type_widget.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
|
||||
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
||||
@@ -56,6 +58,7 @@ class DocumentDetailedItem extends DocumentItem {
|
||||
final maxHeight = highlights != null
|
||||
? min(600.0, availableHeight)
|
||||
: min(500.0, availableHeight);
|
||||
final labels = context.watch<LabelRepository>().state;
|
||||
return Card(
|
||||
color: isSelected ? Theme.of(context).colorScheme.inversePrimary : null,
|
||||
child: InkWell(
|
||||
@@ -79,88 +82,59 @@ class DocumentDetailedItem extends DocumentItem {
|
||||
width: double.infinity,
|
||||
height: maxHeight / 2,
|
||||
),
|
||||
child: DocumentPreview(
|
||||
document: document,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
DocumentPreview(
|
||||
documentId: document.id,
|
||||
title: document.title,
|
||||
),
|
||||
if (paperlessUser.canViewTags)
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: TagsWidget(
|
||||
tags:
|
||||
document.tags.map((e) => labels.tags[e]!).toList(),
|
||||
onTagSelected: onTagSelected,
|
||||
).padded(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat.yMMMMd(Localizations.localeOf(context).toString())
|
||||
.format(document.created),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.apply(color: Theme.of(context).hintColor),
|
||||
),
|
||||
if (document.archiveSerialNumber != null)
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'#${document.archiveSerialNumber}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.apply(color: Theme.of(context).hintColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddedLTRB(8, 8, 8, 4),
|
||||
if (paperlessUser.canViewCorrespondents)
|
||||
CorrespondentWidget(
|
||||
onSelected: onCorrespondentSelected,
|
||||
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
correspondent: labels.correspondents[document.correspondent],
|
||||
).paddedLTRB(8, 8, 8, 0),
|
||||
Text(
|
||||
document.title.isEmpty ? '-' : document.title,
|
||||
document.title.isEmpty ? '(-)' : document.title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).paddedLTRB(8, 0, 8, 4),
|
||||
if (paperlessUser.canViewCorrespondents)
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.person_outline,
|
||||
size: 16,
|
||||
).paddedOnly(right: 4.0),
|
||||
CorrespondentWidget(
|
||||
onSelected: onCorrespondentSelected,
|
||||
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
correspondent: context
|
||||
.watch<LabelRepository>()
|
||||
.state
|
||||
.correspondents[document.correspondent],
|
||||
).paddedLTRB(8, 8, 8, 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: DateAndDocumentTypeLabelWidget(
|
||||
document: document,
|
||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||
),
|
||||
],
|
||||
).paddedLTRB(8, 0, 8, 4),
|
||||
if (paperlessUser.canViewDocumentTypes)
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.description_outlined,
|
||||
size: 16,
|
||||
).paddedOnly(right: 4.0),
|
||||
DocumentTypeWidget(
|
||||
onSelected: onDocumentTypeSelected,
|
||||
textStyle: Theme.of(context).textTheme.titleSmall?.apply(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
documentType: context
|
||||
.watch<LabelRepository>()
|
||||
.state
|
||||
.documentTypes[document.documentType],
|
||||
),
|
||||
if (document.archiveSerialNumber != null)
|
||||
Text(
|
||||
'#${document.archiveSerialNumber}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.apply(color: Theme.of(context).hintColor),
|
||||
),
|
||||
],
|
||||
).paddedLTRB(8, 0, 8, 4),
|
||||
if (paperlessUser.canViewTags)
|
||||
TagsWidget(
|
||||
tags: document.tags
|
||||
.map((e) => context.watch<LabelRepository>().state.tags[e]!)
|
||||
.toList(),
|
||||
onTagSelected: onTagSelected,
|
||||
).padded(),
|
||||
],
|
||||
).paddedLTRB(8, 4, 8, 8),
|
||||
if (highlights != null)
|
||||
Html(
|
||||
data: '<p>${highlights!}</p>',
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
|
||||
@@ -29,111 +30,133 @@ class DocumentGridItem extends DocumentItem {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var currentUser = context.watch<LocalUserAccount>().paperlessUser;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Card(
|
||||
elevation: 1.0,
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.inversePrimary
|
||||
: Theme.of(context).cardColor,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: _onTap,
|
||||
onLongPress: onSelected != null ? () => onSelected!(document) : null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DocumentPreview(
|
||||
document: document,
|
||||
borderRadius: 12.0,
|
||||
enableHero: enableHeroAnimation,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
// Prevents ancestor notification listeners to be notified when this widget scrolls
|
||||
onNotification: (notification) => true,
|
||||
child: CustomScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
slivers: [
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(width: 8),
|
||||
),
|
||||
if (currentUser.canViewTags)
|
||||
TagsWidget.sliver(
|
||||
tags: document.tags
|
||||
.map((e) => context
|
||||
.watch<LabelRepository>()
|
||||
.state
|
||||
.tags[e]!)
|
||||
.toList(),
|
||||
onTagSelected: onTagSelected,
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(width: 8),
|
||||
),
|
||||
],
|
||||
return Stack(
|
||||
children: [
|
||||
Card(
|
||||
elevation: 1.0,
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.inversePrimary
|
||||
: Theme.of(context).cardColor,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: _onTap,
|
||||
onLongPress:
|
||||
onSelected != null ? () => onSelected!(document) : null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: DocumentPreview(
|
||||
documentId: document.id,
|
||||
borderRadius: 12.0,
|
||||
enableHero: enableHeroAnimation,
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: SizedBox(
|
||||
height: kMinInteractiveDimension,
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
// Prevents ancestor notification listeners to be notified when this widget scrolls
|
||||
onNotification: (notification) => true,
|
||||
child: CustomScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
slivers: [
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(width: 8),
|
||||
),
|
||||
if (currentUser.canViewTags)
|
||||
TagsWidget.sliver(
|
||||
tags: document.tags
|
||||
.map((e) => context
|
||||
.watch<LabelRepository>()
|
||||
.state
|
||||
.tags[e]!)
|
||||
.toList(),
|
||||
onTagSelected: onTagSelected,
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(width: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (currentUser.canViewCorrespondents)
|
||||
CorrespondentWidget(
|
||||
correspondent: context
|
||||
.watch<LabelRepository>()
|
||||
.state
|
||||
.correspondents[document.correspondent],
|
||||
onSelected: onCorrespondentSelected,
|
||||
),
|
||||
if (currentUser.canViewDocumentTypes)
|
||||
DocumentTypeWidget(
|
||||
documentType: context
|
||||
.watch<LabelRepository>()
|
||||
.state
|
||||
.documentTypes[document.documentType],
|
||||
onSelected: onDocumentTypeSelected,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
document.title.isEmpty ? '-' : document.title,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
DateFormat.yMMMMd(
|
||||
Localizations.localeOf(context).toString())
|
||||
.format(document.created),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (currentUser.canViewCorrespondents)
|
||||
CorrespondentWidget(
|
||||
correspondent: context
|
||||
.watch<LabelRepository>()
|
||||
.state
|
||||
.correspondents[document.correspondent],
|
||||
onSelected: onCorrespondentSelected,
|
||||
),
|
||||
if (currentUser.canViewDocumentTypes)
|
||||
DocumentTypeWidget(
|
||||
documentType: context
|
||||
.watch<LabelRepository>()
|
||||
.state
|
||||
.documentTypes[document.documentType],
|
||||
onSelected: onDocumentTypeSelected,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
document.title.isEmpty ? '-' : document.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat.yMMMMd(
|
||||
Localizations.localeOf(context).toString(),
|
||||
).format(document.created),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
if (document.archiveSerialNumber != null)
|
||||
Text(
|
||||
'#' + document.archiveSerialNumber!.toString(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_api/src/models/document_model.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository_state.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/date_and_document_type_widget.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/items/document_item.dart';
|
||||
import 'package:paperless_mobile/features/labels/correspondent/view/widgets/correspondent_widget.dart';
|
||||
@@ -31,6 +33,7 @@ class DocumentListItem extends DocumentItem {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final labels = context.watch<LabelRepository>().state;
|
||||
|
||||
return ListTile(
|
||||
tileColor: backgroundColor,
|
||||
dense: true,
|
||||
@@ -77,30 +80,9 @@ class DocumentListItem extends DocumentItem {
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: RichText(
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
text: DateFormat.yMMMMd(Localizations.localeOf(context).toString())
|
||||
.format(document.created),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall
|
||||
?.apply(color: Colors.grey),
|
||||
children: document.documentType != null
|
||||
? [
|
||||
const TextSpan(text: '\u30FB'),
|
||||
TextSpan(
|
||||
text: labels.documentTypes[document.documentType]?.name,
|
||||
recognizer: onDocumentTypeSelected != null
|
||||
? (TapGestureRecognizer()
|
||||
..onTap = () =>
|
||||
onDocumentTypeSelected!(document.documentType))
|
||||
: null,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: DateAndDocumentTypeLabelWidget(
|
||||
document: document,
|
||||
onDocumentTypeSelected: onDocumentTypeSelected,
|
||||
),
|
||||
),
|
||||
isThreeLine: document.tags.isNotEmpty,
|
||||
@@ -108,7 +90,7 @@ class DocumentListItem extends DocumentItem {
|
||||
aspectRatio: _a4AspectRatio,
|
||||
child: GestureDetector(
|
||||
child: DocumentPreview(
|
||||
document: document,
|
||||
documentId: document.id,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
enableHero: enableHeroAnimation,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/text_placeholder.dart';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
|
||||
class TagsPlaceholder extends StatelessWidget {
|
||||
static const _lengths = <double>[90, 70, 130];
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/saved_views_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/saved_views/saved_view_chip.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/translation/sort_field_localization_mapper.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class SortFieldSelectionBottomSheet extends StatefulWidget {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
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_extensions.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_extensions.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/features/logging/data/logger.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository_state.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||
|
||||
part 'inbox_cubit.g.dart';
|
||||
part 'inbox_state.dart';
|
||||
@@ -49,18 +49,12 @@ class InboxCubit extends HydratedCubit<InboxState>
|
||||
final wasInInboxBeforeUpdate =
|
||||
state.documents.map((e) => e.id).contains(document.id);
|
||||
if (!hasInboxTag && wasInInboxBeforeUpdate) {
|
||||
print(
|
||||
"INBOX: Removing document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
|
||||
remove(document);
|
||||
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
|
||||
} else if (hasInboxTag) {
|
||||
if (wasInInboxBeforeUpdate) {
|
||||
print(
|
||||
"INBOX: Replacing document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
|
||||
replace(document);
|
||||
} else {
|
||||
print(
|
||||
"INBOX: Adding document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
|
||||
_addDocument(document);
|
||||
emit(
|
||||
state.copyWith(itemsInInboxCount: state.itemsInInboxCount + 1));
|
||||
@@ -83,11 +77,26 @@ class InboxCubit extends HydratedCubit<InboxState>
|
||||
}
|
||||
|
||||
Future<void> refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {
|
||||
debugPrint("Checking for new items in inbox...");
|
||||
logger.fi(
|
||||
"Checking for new documents in inbox...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "refreshItemsInInboxCount",
|
||||
);
|
||||
final stats = await _statsApi.getServerStatistics();
|
||||
|
||||
if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) {
|
||||
logger.fi(
|
||||
"New documents found in inbox, reloading.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "refreshItemsInInboxCount",
|
||||
);
|
||||
await loadInbox();
|
||||
} else {
|
||||
logger.fi(
|
||||
"No new documents found in inbox.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "refreshItemsInInboxCount",
|
||||
);
|
||||
}
|
||||
emit(state.copyWith(itemsInInboxCount: stats.documentsInInbox));
|
||||
}
|
||||
@@ -97,7 +106,6 @@ class InboxCubit extends HydratedCubit<InboxState>
|
||||
///
|
||||
Future<void> loadInbox() async {
|
||||
if (!isClosed) {
|
||||
debugPrint("Initializing inbox...");
|
||||
final inboxTags = await _labelRepository.findAllTags().then(
|
||||
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
|
||||
);
|
||||
|
||||
@@ -9,8 +9,8 @@ import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||
import 'package:paperless_mobile/extensions/dart_extensions.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/dart_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.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/inbox/cubit/inbox_cubit.dart';
|
||||
|
||||
@@ -3,9 +3,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
|
||||
import 'package:paperless_mobile/core/widgets/shimmer_placeholder.dart';
|
||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/delete_document_confirmation_dialog.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/document_preview.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/placeholder/tags_placeholder.dart';
|
||||
@@ -153,7 +154,9 @@ class _InboxItemState extends State<InboxItem> {
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
DocumentDetailsRoute(
|
||||
$extra: widget.document,
|
||||
title: widget.document.title,
|
||||
id: widget.document.id,
|
||||
thumbnailUrl: widget.document.buildThumbnailUrl(context),
|
||||
isLabelClickable: false,
|
||||
).push(context);
|
||||
},
|
||||
@@ -168,7 +171,8 @@ class _InboxItemState extends State<InboxItem> {
|
||||
AspectRatio(
|
||||
aspectRatio: InboxItem.a4AspectRatio,
|
||||
child: DocumentPreview(
|
||||
document: widget.document,
|
||||
documentId: widget.document.id,
|
||||
title: widget.document.title,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
enableHero: false,
|
||||
|
||||
@@ -21,15 +21,19 @@ class CorrespondentWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return AbsorbPointer(
|
||||
absorbing: !isClickable,
|
||||
child: GestureDetector(
|
||||
onTap: () => onSelected?.call(correspondent?.id),
|
||||
child: Text(
|
||||
correspondent?.name ?? "-",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
(textStyle ?? Theme.of(context).textTheme.bodyMedium)?.copyWith(
|
||||
color: textColor ?? Theme.of(context).colorScheme.primary,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
onTap: () => onSelected?.call(correspondent?.id),
|
||||
child: Text(
|
||||
correspondent?.name ?? "-",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
(textStyle ?? Theme.of(context).textTheme.bodyMedium)?.copyWith(
|
||||
color: textColor ?? Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -18,14 +18,18 @@ class DocumentTypeWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return AbsorbPointer(
|
||||
absorbing: !isClickable,
|
||||
child: GestureDetector(
|
||||
onTap: () => onSelected?.call(documentType?.id),
|
||||
child: Text(
|
||||
documentType?.toString() ?? "-",
|
||||
style: (textStyle ?? Theme.of(context).textTheme.bodyMedium)
|
||||
?.copyWith(color: Theme.of(context).colorScheme.tertiary),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
onTap: () => onSelected?.call(documentType?.id),
|
||||
child: Text(
|
||||
documentType?.toString() ?? "-",
|
||||
style: (textStyle ?? Theme.of(context).textTheme.bodyMedium)
|
||||
?.copyWith(color: Theme.of(context).colorScheme.tertiary),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_tag_page.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/fullscreen_tags_form.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart';
|
||||
import 'package:paperless_mobile/features/logging/data/logger.dart';
|
||||
import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart';
|
||||
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
|
||||
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
|
||||
@@ -212,17 +213,18 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
][_currentIndex]
|
||||
.call();
|
||||
} catch (error, stackTrace) {
|
||||
debugPrint(
|
||||
"[LabelsPage] RefreshIndicator.onRefresh "
|
||||
"${[
|
||||
"correspondents",
|
||||
"document types",
|
||||
"tags",
|
||||
"storage paths"
|
||||
][_currentIndex]}: "
|
||||
"An error occurred (${error.toString()})",
|
||||
);
|
||||
debugPrintStack(stackTrace: stackTrace);
|
||||
logger.fe(
|
||||
"An error ocurred while reloading "
|
||||
"${[
|
||||
"correspondents",
|
||||
"document types",
|
||||
"tags",
|
||||
"storage paths"
|
||||
][_currentIndex]}.",
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'onRefresh');
|
||||
}
|
||||
},
|
||||
child: TabBarView(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class FullscreenLabelForm<T extends Label> extends StatefulWidget {
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/labels/view/widgets/fullscreen_label_form.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/translation/matching_algorithm_localization_mapper.dart';
|
||||
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
|
||||
import 'package:paperless_mobile/features/labels/view/widgets/label_item.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
|
||||
class LabelTabView<T extends Label> extends StatelessWidget {
|
||||
final Map<int, T> labels;
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/constants.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.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/landing/view/widgets/expansion_card.dart';
|
||||
@@ -18,12 +18,6 @@ import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/changelog_route.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class Changelog {
|
||||
final int buildNumber;
|
||||
final String? changelog;
|
||||
Changelog(this.buildNumber, this.changelog);
|
||||
}
|
||||
|
||||
class LandingPage extends StatefulWidget {
|
||||
const LandingPage({super.key});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/extensions/document_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/adaptive_documents_view.dart';
|
||||
import 'package:paperless_mobile/features/documents/view/widgets/selection/view_type_selection_widget.dart';
|
||||
import 'package:paperless_mobile/features/linked_documents/cubit/linked_documents_cubit.dart';
|
||||
@@ -53,8 +54,10 @@ class _LinkedDocumentsPageState extends State<LinkedDocumentsPage>
|
||||
hasLoaded: state.hasLoaded,
|
||||
onTap: (document) {
|
||||
DocumentDetailsRoute(
|
||||
$extra: document,
|
||||
title: document.title,
|
||||
id: document.id,
|
||||
isLabelClickable: false,
|
||||
thumbnailUrl: document.buildThumbnailUrl(context),
|
||||
).push(context);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
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/features/logging/models/parsed_log_message.dart';
|
||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
part 'app_logs_state.dart';
|
||||
|
||||
final _fileNameFormat = DateFormat("yyyy-MM-dd");
|
||||
|
||||
class AppLogsCubit extends Cubit<AppLogsState> {
|
||||
StreamSubscription? _fileChangesSubscription;
|
||||
final LocalNotificationService _localNotificationService;
|
||||
AppLogsCubit(
|
||||
DateTime date,
|
||||
this._localNotificationService,
|
||||
) : 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 {
|
||||
_updateLogsFromFile(logFile, date, availableLogs);
|
||||
_fileChangesSubscription = logFile.watch().listen((event) async {
|
||||
if (!isClosed) {
|
||||
_updateLogsFromFile(logFile, date, availableLogs);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
emit(AppLogsStateError(
|
||||
error: e,
|
||||
date: date,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _updateLogsFromFile(
|
||||
File file, DateTime date, List<DateTime> availableLogs) async {
|
||||
final logs = await file.readAsLines();
|
||||
final parsedLogs = ParsedLogMessage.parse(logs).reversed.toList();
|
||||
emit(AppLogsStateLoaded(
|
||||
date: date,
|
||||
logs: parsedLogs,
|
||||
availableLogs: availableLogs,
|
||||
));
|
||||
}
|
||||
|
||||
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,
|
||||
// );
|
||||
// if (parentDir == null) {
|
||||
// return;
|
||||
// }
|
||||
final logFile = _getLogfile(date);
|
||||
final parentDir = FileService.instance.downloadsDirectory;
|
||||
final downloadedFile = await logFile.copy(p.join(parentDir.path, filename));
|
||||
_localNotificationService.notifyFileDownload(filePath: downloadedFile.path);
|
||||
}
|
||||
|
||||
File _getLogfile(DateTime date) {
|
||||
return File(p.join(FileService.instance.logDirectory.path,
|
||||
'${_fileNameFormat.format(date)}.log'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _fileChangesSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:paperless_mobile/features/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---"
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:paperless_mobile/features/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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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';
|
||||
|
||||
typedef f = FileOutput;
|
||||
|
||||
class MirroredFileOutput extends LogOutput {
|
||||
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}.");
|
||||
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);
|
||||
await file.writeAsString(
|
||||
"$line${Platform.lineTerminator}",
|
||||
mode: FileMode.append,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:paperless_mobile/features/logging/cubit/app_logs_cubit.dart';
|
||||
import 'package:paperless_mobile/features/logging/models/parsed_log_message.dart';
|
||||
import 'package:paperless_mobile/core/extensions/dart_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class AppLogsPage extends StatefulWidget {
|
||||
const AppLogsPage({super.key});
|
||||
|
||||
@override
|
||||
State<AppLogsPage> createState() => _AppLogsPageState();
|
||||
}
|
||||
|
||||
class _AppLogsPageState extends State<AppLogsPage> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
bool autoScroll = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
final theme = Theme.of(context);
|
||||
return BlocBuilder<AppLogsCubit, AppLogsState>(
|
||||
builder: (context, state) {
|
||||
final formattedDate = DateFormat.yMMMd(locale).format(state.date);
|
||||
return Scaffold(
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: switch (state) {
|
||||
AppLogsStateInitial() => [],
|
||||
AppLogsStateLoading() => [],
|
||||
AppLogsStateLoaded() => [
|
||||
IconButton(
|
||||
tooltip: S.of(context)!.copyToClipboard,
|
||||
onPressed: () {
|
||||
context
|
||||
.read<AppLogsCubit>()
|
||||
.copyToClipboard(state.date);
|
||||
},
|
||||
icon: const Icon(Icons.copy),
|
||||
).padded(),
|
||||
IconButton(
|
||||
tooltip: S.of(context)!.saveLogsToFile,
|
||||
onPressed: () {
|
||||
context
|
||||
.read<AppLogsCubit>()
|
||||
.saveLogs(state.date, locale);
|
||||
},
|
||||
icon: const Icon(Icons.download),
|
||||
).padded(),
|
||||
IconButton(
|
||||
tooltip: S.of(context)!.clearLogs(formattedDate),
|
||||
onPressed: () {
|
||||
context.read<AppLogsCubit>().clearLogs(state.date);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.delete_sweep,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
).padded(),
|
||||
],
|
||||
_ => [],
|
||||
},
|
||||
),
|
||||
),
|
||||
appBar: AppBar(
|
||||
title: Text(S
|
||||
.of(context)!
|
||||
.appLogs(formattedDate)), //TODO: CHange to App-Logs in german
|
||||
actions: [
|
||||
if (state is AppLogsStateLoaded)
|
||||
IconButton(
|
||||
tooltip: MaterialLocalizations.of(context).datePickerHelpText,
|
||||
onPressed: () async {
|
||||
final selectedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: state.date,
|
||||
firstDate: state.availableLogs.first,
|
||||
lastDate: state.availableLogs.last,
|
||||
selectableDayPredicate: (day) => state.availableLogs
|
||||
.any((date) => day.isOnSameDayAs(date)),
|
||||
initialEntryMode: DatePickerEntryMode.calendarOnly,
|
||||
);
|
||||
if (selectedDate != null) {
|
||||
context.read<AppLogsCubit>().loadLogs(selectedDate);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.calendar_today),
|
||||
).padded(),
|
||||
],
|
||||
),
|
||||
body: switch (state) {
|
||||
AppLogsStateLoaded(
|
||||
logs: var logs,
|
||||
) =>
|
||||
Builder(
|
||||
builder: (context) {
|
||||
if (state.logs.isEmpty) {
|
||||
return Center(
|
||||
child: Text(S.of(context)!.noLogsFoundOn(formattedDate)),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
reverse: true,
|
||||
controller: _scrollController,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return Center(
|
||||
child: Text(S.of(context)!.logfileBottomReached,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.disabledColor,
|
||||
)),
|
||||
).padded(24);
|
||||
}
|
||||
final messages = state.logs;
|
||||
final logMessage = messages[index - 1];
|
||||
final altColor = CupertinoDynamicColor.withBrightness(
|
||||
color: Colors.grey.shade200,
|
||||
darkColor: Colors.grey.shade800,
|
||||
).resolveFrom(context);
|
||||
return ParsedLogMessageTile(
|
||||
message: logMessage,
|
||||
backgroundColor: (index % 2 == 0)
|
||||
? theme.colorScheme.background
|
||||
: altColor,
|
||||
);
|
||||
},
|
||||
itemCount: logs.length + 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
AppLogsStateError() => Center(
|
||||
child:
|
||||
Text(S.of(context)!.couldNotLoadLogfileFrom(formattedDate)),
|
||||
),
|
||||
_ => _buildLoadingLogs(state.date)
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingLogs(DateTime date) {
|
||||
final formattedDate =
|
||||
DateFormat.yMd(Localizations.localeOf(context).toString()).format(date);
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
Text(S.of(context)!.loadingLogsFrom(formattedDate)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ParsedLogMessageTile extends StatelessWidget {
|
||||
final ParsedLogMessage message;
|
||||
final Color backgroundColor;
|
||||
|
||||
const ParsedLogMessageTile({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return switch (message) {
|
||||
ParsedFormattedLogMessage m => FormattedLogMessageWidget(
|
||||
message: m,
|
||||
backgroundColor: backgroundColor,
|
||||
),
|
||||
UnformattedLogMessage(message: var m) => Text(m),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class FormattedLogMessageWidget extends StatelessWidget {
|
||||
final ParsedFormattedLogMessage message;
|
||||
final Color backgroundColor;
|
||||
const FormattedLogMessageWidget(
|
||||
{super.key, required this.message, required this.backgroundColor});
|
||||
static final _timeFormat = DateFormat("HH:mm:ss.SSS");
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = Theme.of(context).colorScheme;
|
||||
|
||||
final icon = switch (message.level) {
|
||||
Level.trace => Icons.troubleshoot,
|
||||
Level.debug => Icons.bug_report,
|
||||
Level.info => Icons.info_outline,
|
||||
Level.warning => Icons.warning,
|
||||
Level.error => Icons.error,
|
||||
Level.fatal => Icons.error_outline,
|
||||
_ => null,
|
||||
};
|
||||
final color = switch (message.level) {
|
||||
Level.trace => c.onBackground.withOpacity(0.75),
|
||||
Level.warning => Colors.yellow.shade600,
|
||||
Level.error => Colors.red,
|
||||
Level.fatal => Colors.red.shade900,
|
||||
Level.info => Colors.blue,
|
||||
_ => c.onBackground,
|
||||
};
|
||||
|
||||
final logStyle = Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
);
|
||||
final formattedMethodName =
|
||||
message.methodName != null ? '${message.methodName!.trim()}()' : '';
|
||||
final source = switch (message.className) {
|
||||
'' || null => formattedMethodName,
|
||||
String className => '$className.$formattedMethodName',
|
||||
};
|
||||
return Material(
|
||||
color: backgroundColor,
|
||||
child: ExpansionTile(
|
||||
leading: Text(
|
||||
_timeFormat.format(message.timestamp),
|
||||
style: logStyle?.copyWith(color: color),
|
||||
),
|
||||
title: Text(
|
||||
message.message,
|
||||
style: logStyle?.copyWith(color: color),
|
||||
),
|
||||
trailing: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
),
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||
childrenPadding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
expandedAlignment: Alignment.topLeft,
|
||||
children: source.isNotEmpty
|
||||
? [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.arrow_right),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'In $source',
|
||||
style: logStyle?.copyWith(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
..._buildErrorWidgets(context),
|
||||
]
|
||||
: _buildErrorWidgets(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildErrorWidgets(BuildContext context) {
|
||||
if (message.error != null) {
|
||||
return [
|
||||
Divider(),
|
||||
Text(
|
||||
message.error!.error,
|
||||
style: TextStyle(color: Colors.red),
|
||||
).padded(),
|
||||
if (message.error?.stackTrace != null) ...[
|
||||
Text(
|
||||
message.error!.stackTrace!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
),
|
||||
).paddedOnly(left: 8),
|
||||
],
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
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_extensions.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_extensions.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_app_state.dart';
|
||||
@@ -13,6 +13,8 @@ import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_credentials.dart';
|
||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
||||
import 'package:paperless_mobile/features/logging/data/logger.dart';
|
||||
import 'package:paperless_mobile/features/logging/utils/redaction_utils.dart';
|
||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
@@ -55,9 +57,12 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
}
|
||||
emit(const AuthenticatingState(AuthenticatingStage.authenticating));
|
||||
final localUserId = "${credentials.username}@$serverUrl";
|
||||
_debugPrintMessage(
|
||||
"login",
|
||||
"Trying to login $localUserId...",
|
||||
final redactedId = redactUserId(localUserId);
|
||||
|
||||
logger.fd(
|
||||
"Trying to log in $redactedId...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'login',
|
||||
);
|
||||
try {
|
||||
await _addUser(
|
||||
@@ -97,35 +102,35 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
await globalSettings.save();
|
||||
|
||||
emit(AuthenticatedState(localUserId: localUserId));
|
||||
_debugPrintMessage(
|
||||
"login",
|
||||
"User successfully logged in.",
|
||||
logger.fd(
|
||||
'User $redactedId successfully logged in.',
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'login',
|
||||
);
|
||||
}
|
||||
|
||||
/// Switches to another account if it exists.
|
||||
Future<void> switchAccount(String localUserId) async {
|
||||
emit(const SwitchingAccountsState());
|
||||
_debugPrintMessage(
|
||||
"switchAccount",
|
||||
"Trying to switch to user $localUserId...",
|
||||
final redactedId = redactUserId(localUserId);
|
||||
logger.fd(
|
||||
'Trying to switch to user $redactedId...',
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'switchAccount',
|
||||
);
|
||||
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
// if (globalSettings.loggedInUserId == localUserId) {
|
||||
// _debugPrintMessage(
|
||||
// "switchAccount",
|
||||
// "User $localUserId is already logged in.",
|
||||
// );
|
||||
// emit(AuthenticatedState(localUserId: localUserId));
|
||||
// return;
|
||||
// }
|
||||
|
||||
final userAccountBox = Hive.localUserAccountBox;
|
||||
|
||||
if (!userAccountBox.containsKey(localUserId)) {
|
||||
debugPrint("User $localUserId not yet registered.");
|
||||
logger.fw(
|
||||
'User $redactedId not yet registered. '
|
||||
'This should never be the case!',
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'switchAccount',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,9 +140,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
final authenticated = await _localAuthService
|
||||
.authenticateLocalUser("Authenticate to switch your account.");
|
||||
if (!authenticated) {
|
||||
_debugPrintMessage(
|
||||
"switchAccount",
|
||||
logger.fw(
|
||||
"User could not be authenticated.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'switchAccount',
|
||||
);
|
||||
emit(VerifyIdentityState(userId: localUserId));
|
||||
return;
|
||||
@@ -151,7 +157,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
HiveBoxes.localUserCredentials, (credentialsBox) async {
|
||||
if (!credentialsBox.containsKey(localUserId)) {
|
||||
await credentialsBox.close();
|
||||
debugPrint("Invalid authentication for $localUserId");
|
||||
logger.fw(
|
||||
"Invalid authentication for $redactedId.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'switchAccount',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final credentials = credentialsBox.get(localUserId);
|
||||
@@ -188,6 +198,12 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
}) async {
|
||||
assert(credentials.password != null && credentials.username != null);
|
||||
final localUserId = "${credentials.username}@$serverUrl";
|
||||
final redactedId = redactUserId(localUserId);
|
||||
logger.fd(
|
||||
"Adding account $redactedId...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'switchAccount',
|
||||
);
|
||||
|
||||
final sessionManager = SessionManager([
|
||||
LanguageHeaderInterceptor(locale),
|
||||
@@ -204,9 +220,16 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
}
|
||||
|
||||
Future<void> removeAccount(String userId) async {
|
||||
final redactedId = redactUserId(userId);
|
||||
logger.fd(
|
||||
"Trying to remove account $redactedId...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'removeAccount',
|
||||
);
|
||||
final userAccountBox = Hive.localUserAccountBox;
|
||||
final userAppStateBox = Hive.localUserAppStateBox;
|
||||
await FileService.clearUserData(userId: userId);
|
||||
|
||||
await FileService.instance.clearUserData(userId: userId);
|
||||
await userAccountBox.delete(userId);
|
||||
await userAppStateBox.delete(userId);
|
||||
await withEncryptedBox<UserCredentials, void>(
|
||||
@@ -220,18 +243,20 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
///
|
||||
Future<void> restoreSession([String? userId]) async {
|
||||
emit(const RestoringSessionState());
|
||||
_debugPrintMessage(
|
||||
"restoreSessionState",
|
||||
logger.fd(
|
||||
"Trying to restore previous session...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
final restoreSessionForUser = userId ?? globalSettings.loggedInUserId;
|
||||
// final localUserId = globalSettings.loggedInUserId;
|
||||
if (restoreSessionForUser == null) {
|
||||
_debugPrintMessage(
|
||||
"restoreSessionState",
|
||||
logger.fd(
|
||||
"There is nothing to restore.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
final otherAccountsExist = Hive.localUserAccountBox.isNotEmpty;
|
||||
// If there is nothing to restore, we can quit here.
|
||||
@@ -243,14 +268,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
final localUserAccountBox =
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
final localUserAccount = localUserAccountBox.get(restoreSessionForUser)!;
|
||||
_debugPrintMessage(
|
||||
"restoreSessionState",
|
||||
"Checking if biometric authentication is required...",
|
||||
);
|
||||
if (localUserAccount.settings.isBiometricAuthenticationEnabled) {
|
||||
_debugPrintMessage(
|
||||
"restoreSessionState",
|
||||
"Biometric authentication required, waiting for user to authenticate...",
|
||||
logger.fd(
|
||||
"Verifying user identity...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
final authenticationMesage =
|
||||
(await S.delegate.load(Locale(globalSettings.preferredLocaleSubtag)))
|
||||
@@ -258,26 +280,24 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
final localAuthSuccess =
|
||||
await _localAuthService.authenticateLocalUser(authenticationMesage);
|
||||
if (!localAuthSuccess) {
|
||||
emit(VerifyIdentityState(userId: restoreSessionForUser));
|
||||
_debugPrintMessage(
|
||||
"restoreSessionState",
|
||||
"User could not be authenticated.",
|
||||
logger.fw(
|
||||
"Identity could not be verified.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
emit(VerifyIdentityState(userId: restoreSessionForUser));
|
||||
return;
|
||||
}
|
||||
_debugPrintMessage(
|
||||
"restoreSessionState",
|
||||
"User successfully autheticated.",
|
||||
);
|
||||
} else {
|
||||
_debugPrintMessage(
|
||||
"restoreSessionState",
|
||||
"Biometric authentication not configured, skipping.",
|
||||
logger.fd(
|
||||
"Identity successfully verified.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
}
|
||||
_debugPrintMessage(
|
||||
"restoreSessionState",
|
||||
"Trying to retrieve authentication credentials...",
|
||||
logger.fd(
|
||||
"Reading encrypted credentials...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
final authentication =
|
||||
await withEncryptedBox<UserCredentials, UserCredentials>(
|
||||
@@ -286,23 +306,25 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
});
|
||||
|
||||
if (authentication == null) {
|
||||
_debugPrintMessage(
|
||||
"restoreSessionState",
|
||||
"Could not retrieve existing authentication credentials.",
|
||||
logger.fe(
|
||||
"Credentials could not be read!",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
throw Exception(
|
||||
"User should be authenticated but no authentication information was found.",
|
||||
);
|
||||
}
|
||||
|
||||
_debugPrintMessage(
|
||||
"restoreSessionState",
|
||||
"Authentication credentials successfully retrieved.",
|
||||
logger.fd(
|
||||
"Credentials successfully retrieved.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
|
||||
_debugPrintMessage(
|
||||
"restoreSessionState",
|
||||
"Updating current session state...",
|
||||
logger.fd(
|
||||
"Updating security context...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
|
||||
_sessionManager.updateSettings(
|
||||
@@ -310,9 +332,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
authToken: authentication.token,
|
||||
baseUrl: localUserAccount.serverUrl,
|
||||
);
|
||||
_debugPrintMessage(
|
||||
"restoreSessionState",
|
||||
"Current session state successfully updated.",
|
||||
logger.fd(
|
||||
"Security context successfully updated.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
final isPaperlessServerReachable =
|
||||
await _connectivityService.isPaperlessServerReachable(
|
||||
@@ -320,65 +343,95 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
authentication.clientCertificate,
|
||||
) ==
|
||||
ReachabilityStatus.reachable;
|
||||
logger.fd(
|
||||
"Trying to update remote paperless user...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
if (isPaperlessServerReachable) {
|
||||
_debugPrintMessage(
|
||||
"restoreSessionMState",
|
||||
"Updating server user...",
|
||||
);
|
||||
final apiVersion = await _getApiVersion(_sessionManager.client);
|
||||
await _updateRemoteUser(
|
||||
_sessionManager,
|
||||
localUserAccount,
|
||||
apiVersion,
|
||||
);
|
||||
_debugPrintMessage(
|
||||
"restoreSessionMState",
|
||||
"Successfully updated server user.",
|
||||
logger.fd(
|
||||
"Successfully updated remote paperless user.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
} else {
|
||||
_debugPrintMessage(
|
||||
"restoreSessionMState",
|
||||
"Skipping update of server user (server could not be reached).",
|
||||
logger.fw(
|
||||
"Could not update remote paperless user - "
|
||||
"Server could not be reached. The app might behave unexpected!",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
}
|
||||
globalSettings.loggedInUserId = restoreSessionForUser;
|
||||
await globalSettings.save();
|
||||
emit(AuthenticatedState(localUserId: restoreSessionForUser));
|
||||
|
||||
_debugPrintMessage(
|
||||
"restoreSessionState",
|
||||
"Session was successfully restored.",
|
||||
logger.fd(
|
||||
"Previous session successfully restored.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> logout([bool removeAccount = false]) async {
|
||||
Future<void> logout([bool shouldRemoveAccount = false]) async {
|
||||
emit(const LoggingOutState());
|
||||
_debugPrintMessage(
|
||||
"logout",
|
||||
"Trying to log out current user...",
|
||||
);
|
||||
await _resetExternalState();
|
||||
final globalSettings = Hive.globalSettingsBox.getValue()!;
|
||||
final userId = globalSettings.loggedInUserId!;
|
||||
final redactedId = redactUserId(userId);
|
||||
|
||||
logger.fd(
|
||||
"Logging out $redactedId...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'logout',
|
||||
);
|
||||
|
||||
await _resetExternalState();
|
||||
await _notificationService.cancelUserNotifications(userId);
|
||||
|
||||
final otherAccountsExist = Hive.localUserAccountBox.length > 1;
|
||||
emit(UnauthenticatedState(redirectToAccountSelection: otherAccountsExist));
|
||||
if (removeAccount) {
|
||||
await this.removeAccount(userId);
|
||||
if (shouldRemoveAccount) {
|
||||
await removeAccount(userId);
|
||||
}
|
||||
globalSettings.loggedInUserId = null;
|
||||
await globalSettings.save();
|
||||
|
||||
_debugPrintMessage(
|
||||
"logout",
|
||||
logger.fd(
|
||||
"User successfully logged out.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'logout',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _resetExternalState() async {
|
||||
logger.fd(
|
||||
"Resetting security context...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_resetExternalState',
|
||||
);
|
||||
_sessionManager.resetSettings();
|
||||
logger.fd(
|
||||
"Security context reset.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_resetExternalState',
|
||||
);
|
||||
logger.fd(
|
||||
"Clearing local state...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_resetExternalState',
|
||||
);
|
||||
await HydratedBloc.storage.clear();
|
||||
logger.fd(
|
||||
"Local state cleard.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_resetExternalState',
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> _addUser(
|
||||
@@ -392,7 +445,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
_FutureVoidCallback? onFetchUserInformation,
|
||||
}) async {
|
||||
assert(credentials.username != null && credentials.password != null);
|
||||
_debugPrintMessage("_addUser", "Adding new user $localUserId...");
|
||||
final redactedId = redactUserId(localUserId);
|
||||
|
||||
logger.fd(
|
||||
"Adding new user $redactedId..",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
sessionManager.updateSettings(
|
||||
baseUrl: serverUrl,
|
||||
@@ -401,9 +460,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
|
||||
final authApi = _apiFactory.createAuthenticationApi(sessionManager.client);
|
||||
|
||||
_debugPrintMessage(
|
||||
"_addUser",
|
||||
"Trying to login user ${credentials.username} on $serverUrl...",
|
||||
logger.fd(
|
||||
"Fetching bearer token from the server...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
await onPerformLogin?.call();
|
||||
@@ -413,9 +473,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
password: credentials.password!,
|
||||
);
|
||||
|
||||
_debugPrintMessage(
|
||||
"_addUser",
|
||||
"Successfully acquired token.",
|
||||
logger.fd(
|
||||
"Bearer token successfully retrieved.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
sessionManager.updateSettings(
|
||||
@@ -430,18 +491,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||
|
||||
if (userAccountBox.containsKey(localUserId)) {
|
||||
_debugPrintMessage(
|
||||
"_addUser",
|
||||
"An error occurred! The user $localUserId already exists.",
|
||||
logger.fw(
|
||||
"The user $redactedId already exists.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
throw InfoMessageException(code: ErrorCode.userAlreadyExists);
|
||||
}
|
||||
await onFetchUserInformation?.call();
|
||||
final apiVersion = await _getApiVersion(sessionManager.client);
|
||||
_debugPrintMessage(
|
||||
"_addUser",
|
||||
"Trying to fetch user object for $localUserId...",
|
||||
logger.fd(
|
||||
"Trying to fetch remote paperless user for $redactedId.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
late UserModel serverUser;
|
||||
try {
|
||||
serverUser = await _apiFactory
|
||||
@@ -451,21 +515,28 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
)
|
||||
.findCurrentUser();
|
||||
} on DioException catch (error, stackTrace) {
|
||||
_debugPrintMessage(
|
||||
"_addUser",
|
||||
"An error occurred: ${error.message}",
|
||||
logger.fe(
|
||||
"An error occurred while fetching the remote paperless user.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
rethrow;
|
||||
}
|
||||
_debugPrintMessage(
|
||||
"_addUser",
|
||||
"User object successfully fetched.",
|
||||
logger.fd(
|
||||
"Remote paperless user successfully fetched.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
_debugPrintMessage(
|
||||
"_addUser",
|
||||
"Persisting local user account...",
|
||||
|
||||
logger.fd(
|
||||
"Persisting user account information...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
await onPersistLocalUserData?.call();
|
||||
// Create user account
|
||||
await userAccountBox.put(
|
||||
@@ -478,29 +549,34 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
apiVersion: apiVersion,
|
||||
),
|
||||
);
|
||||
_debugPrintMessage(
|
||||
"_addUser",
|
||||
"Local user account successfully persisted.",
|
||||
logger.fd(
|
||||
"User account information successfully persisted.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
_debugPrintMessage(
|
||||
"_addUser",
|
||||
"Persisting user state...",
|
||||
logger.fd(
|
||||
"Persisting user app state...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
// Create user state
|
||||
await userStateBox.put(
|
||||
localUserId,
|
||||
LocalUserAppState(userId: localUserId),
|
||||
);
|
||||
_debugPrintMessage(
|
||||
"_addUser",
|
||||
logger.fd(
|
||||
"User state successfully persisted.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
// Save credentials in encrypted box
|
||||
await withEncryptedBox(HiveBoxes.localUserCredentials, (box) async {
|
||||
_debugPrintMessage(
|
||||
"_addUser",
|
||||
logger.fd(
|
||||
"Saving user credentials inside encrypted storage...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
await box.put(
|
||||
localUserId,
|
||||
UserCredentials(
|
||||
@@ -508,14 +584,20 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
clientCertificate: clientCert,
|
||||
),
|
||||
);
|
||||
_debugPrintMessage(
|
||||
"_addUser",
|
||||
logger.fd(
|
||||
"User credentials successfully saved.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
});
|
||||
final hostsBox = Hive.box<String>(HiveBoxes.hosts);
|
||||
if (!hostsBox.values.contains(serverUrl)) {
|
||||
await hostsBox.add(serverUrl);
|
||||
logger.fd(
|
||||
"Added new url to list of hosts.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
}
|
||||
|
||||
return serverUser.id;
|
||||
@@ -526,9 +608,10 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
Duration? timeout,
|
||||
int defaultValue = 2,
|
||||
}) async {
|
||||
_debugPrintMessage(
|
||||
"_getApiVersion",
|
||||
logger.fd(
|
||||
"Trying to fetch API version...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_getApiVersion',
|
||||
);
|
||||
try {
|
||||
final response = await dio.get(
|
||||
@@ -539,12 +622,19 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
);
|
||||
final apiVersion =
|
||||
int.parse(response.headers.value('x-api-version') ?? "3");
|
||||
_debugPrintMessage(
|
||||
"_getApiVersion",
|
||||
"API version ($apiVersion) successfully retrieved.",
|
||||
logger.fd(
|
||||
"Successfully retrieved API version ($apiVersion).",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_getApiVersion',
|
||||
);
|
||||
|
||||
return apiVersion;
|
||||
} on DioException catch (_) {
|
||||
logger.fw(
|
||||
"Could not retrieve API version, using default ($defaultValue).",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_getApiVersion',
|
||||
);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -555,37 +645,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
LocalUserAccount localUserAccount,
|
||||
int apiVersion,
|
||||
) async {
|
||||
_debugPrintMessage(
|
||||
"_updateRemoteUser",
|
||||
"Updating paperless user object...",
|
||||
logger.fd(
|
||||
"Trying to update remote user object...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_updateRemoteUser',
|
||||
);
|
||||
final updatedPaperlessUser = await _apiFactory
|
||||
.createUserApi(
|
||||
sessionManager.client,
|
||||
apiVersion: apiVersion,
|
||||
)
|
||||
.createUserApi(sessionManager.client, apiVersion: apiVersion)
|
||||
.findCurrentUser();
|
||||
|
||||
localUserAccount.paperlessUser = updatedPaperlessUser;
|
||||
await localUserAccount.save();
|
||||
_debugPrintMessage(
|
||||
"_updateRemoteUser",
|
||||
"Paperless user object successfully updated.",
|
||||
logger.fd(
|
||||
"Successfully updated remote user object.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_updateRemoteUser',
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
|
||||
part 'authentication_information.g.dart';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
|
||||
part 'client_certificate.g.dart';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/exception/server_message_exception.dart';
|
||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
|
||||
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||
@@ -84,40 +84,42 @@ class _AddAccountPageState extends State<AddAccountPage> {
|
||||
),
|
||||
),
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
ServerAddressFormField(
|
||||
initialValue: widget.initialServerUrl,
|
||||
onSubmit: (address) {
|
||||
_updateReachability(address);
|
||||
},
|
||||
).padded(),
|
||||
ClientCertificateFormField(
|
||||
initialBytes: widget.initialClientCertificate?.bytes,
|
||||
initialPassphrase: widget.initialClientCertificate?.passphrase,
|
||||
onChanged: (_) => _updateReachability(),
|
||||
).padded(),
|
||||
_buildStatusIndicator(),
|
||||
if (_reachabilityStatus == ReachabilityStatus.reachable) ...[
|
||||
UserCredentialsFormField(
|
||||
formKey: _formKey,
|
||||
initialUsername: widget.initialUsername,
|
||||
initialPassword: widget.initialPassword,
|
||||
onFieldsSubmitted: _onSubmit,
|
||||
),
|
||||
Text(
|
||||
S.of(context)!.loginRequiredPermissionsHint,
|
||||
style: Theme.of(context).textTheme.bodySmall?.apply(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
).padded(16),
|
||||
]
|
||||
],
|
||||
body: AutofillGroup(
|
||||
child: FormBuilder(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
ServerAddressFormField(
|
||||
initialValue: widget.initialServerUrl,
|
||||
onSubmit: (address) {
|
||||
_updateReachability(address);
|
||||
},
|
||||
).padded(),
|
||||
ClientCertificateFormField(
|
||||
initialBytes: widget.initialClientCertificate?.bytes,
|
||||
initialPassphrase: widget.initialClientCertificate?.passphrase,
|
||||
onChanged: (_) => _updateReachability(),
|
||||
).padded(),
|
||||
_buildStatusIndicator(),
|
||||
if (_reachabilityStatus == ReachabilityStatus.reachable) ...[
|
||||
UserCredentialsFormField(
|
||||
formKey: _formKey,
|
||||
initialUsername: widget.initialUsername,
|
||||
initialPassword: widget.initialPassword,
|
||||
onFieldsSubmitted: _onSubmit,
|
||||
),
|
||||
Text(
|
||||
S.of(context)!.loginRequiredPermissionsHint,
|
||||
style: Theme.of(context).textTheme.bodySmall?.apply(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
).padded(16),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
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_extensions.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_extensions.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/users/view/widgets/user_account_list_tile.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart';
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/core/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate_form_model.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/hive/hive_config.dart';
|
||||
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user