mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2026-01-31 12:24:58 -06:00
feat: Add improved date input, fix bugs, restructurings
This commit is contained in:
@@ -2,23 +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/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/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;
|
||||
@@ -29,24 +29,46 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
this._labelRepository,
|
||||
this._notifier,
|
||||
this._notificationService, {
|
||||
required DocumentModel initialDocument,
|
||||
}) : super(DocumentDetailsState(document: initialDocument)) {
|
||||
required this.id,
|
||||
}) : super(const DocumentDetailsInitial()) {
|
||||
_notifier.addListener(this, onUpdated: (document) {
|
||||
if (document.id == state.document.id) {
|
||||
replace(document);
|
||||
if (state is DocumentDetailsLoaded) {
|
||||
final currentState = state as DocumentDetailsLoaded;
|
||||
if (document.id == currentState.document.id) {
|
||||
replace(document);
|
||||
}
|
||||
}
|
||||
});
|
||||
_labelRepository.addListener(
|
||||
this,
|
||||
onChanged: (labels) => emit(
|
||||
state.copyWith(
|
||||
correspondents: labels.correspondents,
|
||||
documentTypes: labels.documentTypes,
|
||||
tags: labels.tags,
|
||||
storagePaths: labels.storagePaths,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -54,20 +76,6 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
_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,
|
||||
@@ -87,11 +95,15 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
}
|
||||
|
||||
Future<ResultType> openDocumentInSystemViewer() async {
|
||||
final cacheDir = FileService.instance.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");
|
||||
@@ -99,7 +111,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
if (!file.existsSync()) {
|
||||
file.createSync();
|
||||
await _api.downloadToFile(
|
||||
state.document,
|
||||
s.document,
|
||||
file.path,
|
||||
);
|
||||
}
|
||||
@@ -110,7 +122,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({
|
||||
@@ -118,10 +137,12 @@ 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,
|
||||
FileService.instance.downloadsDirectory,
|
||||
);
|
||||
@@ -130,7 +151,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
await File(targetPath).create();
|
||||
} else {
|
||||
await _notificationService.notifyDocumentDownload(
|
||||
document: state.document,
|
||||
document: s.document,
|
||||
filename: p.basename(targetPath),
|
||||
filePath: targetPath,
|
||||
finished: true,
|
||||
@@ -149,12 +170,12 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
// );
|
||||
|
||||
await _api.downloadToFile(
|
||||
state.document,
|
||||
s.document,
|
||||
targetPath,
|
||||
original: downloadOriginal,
|
||||
onProgressChanged: (progress) {
|
||||
_notificationService.notifyDocumentDownload(
|
||||
document: state.document,
|
||||
document: s.document,
|
||||
filename: p.basename(targetPath),
|
||||
filePath: targetPath,
|
||||
finished: true,
|
||||
@@ -165,26 +186,28 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
},
|
||||
);
|
||||
await _notificationService.notifyDocumentDownload(
|
||||
document: state.document,
|
||||
document: s.document,
|
||||
filename: p.basename(targetPath),
|
||||
filePath: targetPath,
|
||||
finished: true,
|
||||
locale: locale,
|
||||
userId: userId,
|
||||
);
|
||||
logger.fi("Document '${state.document.title}' saved 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,
|
||||
FileService.instance.temporaryDirectory,
|
||||
);
|
||||
await _api.downloadToFile(
|
||||
state.document,
|
||||
s.document,
|
||||
filePath,
|
||||
original: shareOriginal,
|
||||
);
|
||||
@@ -192,23 +215,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, FileService.instance.temporaryDirectory);
|
||||
final filePath = _buildDownloadFilePath(
|
||||
s.metaData,
|
||||
false,
|
||||
FileService.instance.temporaryDirectory,
|
||||
);
|
||||
await _api.downloadToFile(
|
||||
state.document,
|
||||
s.document,
|
||||
filePath,
|
||||
original: false,
|
||||
);
|
||||
@@ -217,13 +244,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/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/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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/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';
|
||||
|
||||
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),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -4,87 +4,73 @@ 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/features/document_details/cubit/document_details_cubit.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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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/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)
|
||||
|
||||
Reference in New Issue
Block a user