mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-09 12:08:05 -06:00
feat: Improve notifications, add donation button, improved asn form field
This commit is contained in:
BIN
android/app/src/main/res/drawable/downloading.png
Normal file
BIN
android/app/src/main/res/drawable/downloading.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
android/app/src/main/res/drawable/file_download_done.png
Normal file
BIN
android/app/src/main/res/drawable/file_download_done.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 405 B |
BIN
android/app/src/main/res/drawable/paperless_logo_green.png
Normal file
BIN
android/app/src/main/res/drawable/paperless_logo_green.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
16
assets/images/bmc-logo.svg
Normal file
16
assets/images/bmc-logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.0 KiB |
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||||
|
|
||||||
@@ -45,6 +46,13 @@ class ConnectivityCubit extends Cubit<ConnectivityState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ConnectivityFromContext on BuildContext {
|
||||||
|
bool get watchInternetConnection =>
|
||||||
|
watch<ConnectivityCubit>().state.isConnected;
|
||||||
|
bool get readInternetConnection =>
|
||||||
|
read<ConnectivityCubit>().state.isConnected;
|
||||||
|
}
|
||||||
|
|
||||||
enum ConnectivityState {
|
enum ConnectivityState {
|
||||||
connected,
|
connected,
|
||||||
notConnected,
|
notConnected,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:paperless_mobile/constants.dart';
|
import 'package:paperless_mobile/constants.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
@@ -45,6 +46,21 @@ class AppDrawer extends StatelessWidget {
|
|||||||
'https://github.com/astubenbord/paperless-mobile/issues/new');
|
'https://github.com/astubenbord/paperless-mobile/issues/new');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
leading: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 3),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
'assets/images/bmc-logo.svg',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(S.of(context)!.donateCoffee),
|
||||||
|
onTap: () {
|
||||||
|
launchUrlString("https://www.buymeacoffee.com/astubenbord");
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
leading: const Icon(Icons.settings_outlined),
|
leading: const Icon(Icons.settings_outlined),
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:open_filex/open_filex.dart';
|
import 'package:open_filex/open_filex.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||||
|
import 'package:paperless_mobile/core/service/file_description.dart';
|
||||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||||
|
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
@@ -15,15 +18,18 @@ part 'document_details_state.dart';
|
|||||||
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||||
final PaperlessDocumentsApi _api;
|
final PaperlessDocumentsApi _api;
|
||||||
final DocumentChangedNotifier _notifier;
|
final DocumentChangedNotifier _notifier;
|
||||||
|
final LocalNotificationService _notificationService;
|
||||||
|
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
DocumentDetailsCubit(
|
DocumentDetailsCubit(
|
||||||
this._api,
|
this._api,
|
||||||
this._notifier, {
|
this._notifier,
|
||||||
|
this._notificationService, {
|
||||||
required DocumentModel initialDocument,
|
required DocumentModel initialDocument,
|
||||||
}) : super(DocumentDetailsState(document: initialDocument)) {
|
}) : super(DocumentDetailsState(document: initialDocument)) {
|
||||||
_notifier.subscribe(this, onUpdated: replace);
|
_notifier.subscribe(this, onUpdated: replace);
|
||||||
loadSuggestions();
|
loadSuggestions();
|
||||||
|
loadMetaData();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> delete(DocumentModel document) async {
|
Future<void> delete(DocumentModel document) async {
|
||||||
@@ -36,6 +42,11 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
emit(state.copyWith(suggestions: suggestions));
|
emit(state.copyWith(suggestions: suggestions));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadMetaData() async {
|
||||||
|
final metaData = await _api.getMetaData(state.document);
|
||||||
|
emit(state.copyWith(metaData: metaData));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> loadFullContent() async {
|
Future<void> loadFullContent() async {
|
||||||
final doc = await _api.find(state.document.id);
|
final doc = await _api.find(state.document.id);
|
||||||
if (doc == null) {
|
if (doc == null) {
|
||||||
@@ -47,11 +58,20 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> assignAsn(DocumentModel document) async {
|
Future<void> assignAsn(
|
||||||
if (document.archiveSerialNumber == null) {
|
DocumentModel document, {
|
||||||
final int asn = await _api.findNextAsn();
|
int? asn,
|
||||||
final updatedDocument =
|
bool autoAssign = false,
|
||||||
await _api.update(document.copyWith(archiveSerialNumber: asn));
|
}) async {
|
||||||
|
if (!autoAssign) {
|
||||||
|
final updatedDocument = await _api.update(
|
||||||
|
document.copyWith(archiveSerialNumber: () => asn),
|
||||||
|
);
|
||||||
|
_notifier.notifyUpdated(updatedDocument);
|
||||||
|
} else {
|
||||||
|
final int autoAsn = await _api.findNextAsn();
|
||||||
|
final updatedDocument = await _api
|
||||||
|
.update(document.copyWith(archiveSerialNumber: () => autoAsn));
|
||||||
_notifier.notifyUpdated(updatedDocument);
|
_notifier.notifyUpdated(updatedDocument);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,14 +79,19 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
Future<ResultType> openDocumentInSystemViewer() async {
|
Future<ResultType> openDocumentInSystemViewer() async {
|
||||||
final cacheDir = await FileService.temporaryDirectory;
|
final cacheDir = await FileService.temporaryDirectory;
|
||||||
|
|
||||||
final metaData = await _api.getMetaData(state.document);
|
if (state.metaData == null) {
|
||||||
final bytes = await _api.download(state.document);
|
await loadMetaData();
|
||||||
|
}
|
||||||
|
|
||||||
final file = File('${cacheDir.path}/${metaData.mediaFilename}')
|
await _api.downloadToFile(
|
||||||
..createSync(recursive: true)
|
state.document,
|
||||||
..writeAsBytesSync(bytes);
|
'${cacheDir.path}/${state.metaData!.mediaFilename}',
|
||||||
|
);
|
||||||
|
|
||||||
return OpenFilex.open(file.path, type: "application/pdf").then(
|
return OpenFilex.open(
|
||||||
|
'${cacheDir.path}/${state.metaData!.mediaFilename}',
|
||||||
|
type: "application/pdf",
|
||||||
|
).then(
|
||||||
(value) => value.type,
|
(value) => value.type,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -75,15 +100,60 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
emit(state.copyWith(document: document));
|
emit(state.copyWith(document: document));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> shareDocument() async {
|
Future<void> downloadDocument({
|
||||||
final documentBytes = await _api.download(state.document);
|
bool downloadOriginal = false,
|
||||||
final dir = await getTemporaryDirectory();
|
required String locale,
|
||||||
final String path = "${dir.path}/${state.document.originalFileName}";
|
}) async {
|
||||||
await File(path).writeAsBytes(documentBytes);
|
if (state.metaData == null) {
|
||||||
|
await loadMetaData();
|
||||||
|
}
|
||||||
|
String filePath = _buildDownloadFilePath(
|
||||||
|
downloadOriginal,
|
||||||
|
await FileService.downloadsDirectory,
|
||||||
|
);
|
||||||
|
final desc = FileDescription.fromPath(
|
||||||
|
state.metaData!.mediaFilename
|
||||||
|
.replaceAll("/", " "), // Flatten directory structure
|
||||||
|
);
|
||||||
|
await _notificationService.notifyFileDownload(
|
||||||
|
document: state.document,
|
||||||
|
filename: "${desc.filename}.${desc.extension}",
|
||||||
|
filePath: filePath,
|
||||||
|
finished: false,
|
||||||
|
locale: locale,
|
||||||
|
);
|
||||||
|
await _api.downloadToFile(
|
||||||
|
state.document,
|
||||||
|
filePath,
|
||||||
|
original: downloadOriginal,
|
||||||
|
);
|
||||||
|
await _notificationService.notifyFileDownload(
|
||||||
|
document: state.document,
|
||||||
|
filename: "${desc.filename}.${desc.extension}",
|
||||||
|
filePath: filePath,
|
||||||
|
finished: true,
|
||||||
|
locale: locale,
|
||||||
|
);
|
||||||
|
debugPrint("Downloaded file to $filePath");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> shareDocument({bool shareOriginal = false}) async {
|
||||||
|
if (state.metaData == null) {
|
||||||
|
await loadMetaData();
|
||||||
|
}
|
||||||
|
String filePath = _buildDownloadFilePath(
|
||||||
|
shareOriginal,
|
||||||
|
await FileService.temporaryDirectory,
|
||||||
|
);
|
||||||
|
await _api.downloadToFile(
|
||||||
|
state.document,
|
||||||
|
filePath,
|
||||||
|
original: shareOriginal,
|
||||||
|
);
|
||||||
Share.shareXFiles(
|
Share.shareXFiles(
|
||||||
[
|
[
|
||||||
XFile(
|
XFile(
|
||||||
path,
|
filePath,
|
||||||
name: state.document.originalFileName,
|
name: state.document.originalFileName,
|
||||||
mimeType: "application/pdf",
|
mimeType: "application/pdf",
|
||||||
lastModified: state.document.modified,
|
lastModified: state.document.modified,
|
||||||
@@ -93,12 +163,21 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _buildDownloadFilePath(bool original, Directory dir) {
|
||||||
|
final description = FileDescription.fromPath(
|
||||||
|
state.metaData!.mediaFilename
|
||||||
|
.replaceAll("/", " "), // Flatten directory structure
|
||||||
|
);
|
||||||
|
final extension = original ? description.extension : 'pdf';
|
||||||
|
return "${dir.path}/${description.filename}.$extension";
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() async {
|
||||||
for (final element in _subscriptions) {
|
for (final element in _subscriptions) {
|
||||||
element.cancel();
|
await element.cancel();
|
||||||
}
|
}
|
||||||
_notifier.unsubscribe(this);
|
_notifier.unsubscribe(this);
|
||||||
return super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ part of 'document_details_cubit.dart';
|
|||||||
|
|
||||||
class DocumentDetailsState with EquatableMixin {
|
class DocumentDetailsState with EquatableMixin {
|
||||||
final DocumentModel document;
|
final DocumentModel document;
|
||||||
|
final DocumentMetaData? metaData;
|
||||||
final bool isFullContentLoaded;
|
final bool isFullContentLoaded;
|
||||||
final String? fullContent;
|
final String? fullContent;
|
||||||
final FieldSuggestions suggestions;
|
final FieldSuggestions suggestions;
|
||||||
|
|
||||||
const DocumentDetailsState({
|
const DocumentDetailsState({
|
||||||
required this.document,
|
required this.document,
|
||||||
|
this.metaData,
|
||||||
this.suggestions = const FieldSuggestions(),
|
this.suggestions = const FieldSuggestions(),
|
||||||
this.isFullContentLoaded = false,
|
this.isFullContentLoaded = false,
|
||||||
this.fullContent,
|
this.fullContent,
|
||||||
@@ -19,6 +21,7 @@ class DocumentDetailsState with EquatableMixin {
|
|||||||
suggestions,
|
suggestions,
|
||||||
isFullContentLoaded,
|
isFullContentLoaded,
|
||||||
fullContent,
|
fullContent,
|
||||||
|
metaData,
|
||||||
];
|
];
|
||||||
|
|
||||||
DocumentDetailsState copyWith({
|
DocumentDetailsState copyWith({
|
||||||
@@ -26,12 +29,14 @@ class DocumentDetailsState with EquatableMixin {
|
|||||||
FieldSuggestions? suggestions,
|
FieldSuggestions? suggestions,
|
||||||
bool? isFullContentLoaded,
|
bool? isFullContentLoaded,
|
||||||
String? fullContent,
|
String? fullContent,
|
||||||
|
DocumentMetaData? metaData,
|
||||||
}) {
|
}) {
|
||||||
return DocumentDetailsState(
|
return DocumentDetailsState(
|
||||||
document: document ?? this.document,
|
document: document ?? this.document,
|
||||||
suggestions: suggestions ?? this.suggestions,
|
suggestions: suggestions ?? this.suggestions,
|
||||||
isFullContentLoaded: isFullContentLoaded ?? this.isFullContentLoaded,
|
isFullContentLoaded: isFullContentLoaded ?? this.isFullContentLoaded,
|
||||||
fullContent: fullContent ?? this.fullContent,
|
fullContent: fullContent ?? this.fullContent,
|
||||||
|
metaData: metaData ?? this.metaData,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
class SelectFileTypeDialog extends StatelessWidget {
|
||||||
|
const SelectFileTypeDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RadioSettingsDialog(
|
||||||
|
titleText: S.of(context)!.chooseFiletype,
|
||||||
|
options: [
|
||||||
|
RadioOption(
|
||||||
|
value: true,
|
||||||
|
label: S.of(context)!.original,
|
||||||
|
),
|
||||||
|
RadioOption(
|
||||||
|
value: false,
|
||||||
|
label: S.of(context)!.archivedPdf,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
initialValue: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import 'package:paperless_mobile/features/document_details/view/widgets/document
|
|||||||
import 'package:paperless_mobile/features/document_details/view/widgets/document_download_button.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_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_overview_widget.dart';
|
||||||
|
import 'package:paperless_mobile/features/document_details/view/widgets/document_share_button.dart';
|
||||||
import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart';
|
import 'package:paperless_mobile/features/document_edit/cubit/document_edit_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/document_edit/view/document_edit_page.dart';
|
import 'package:paperless_mobile/features/document_edit/view/document_edit_page.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
|
import 'package:paperless_mobile/features/documents/view/pages/document_view.dart';
|
||||||
@@ -40,7 +41,7 @@ class DocumentDetailsPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||||
late Future<DocumentMetaData> _metaData;
|
late Future<DocumentMetaData> _metaData;
|
||||||
static const double _itemPadding = 24;
|
static const double _itemSpacing = 24;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -71,6 +72,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
extendBodyBehindAppBar: false,
|
||||||
floatingActionButtonLocation:
|
floatingActionButtonLocation:
|
||||||
FloatingActionButtonLocation.endDocked,
|
FloatingActionButtonLocation.endDocked,
|
||||||
floatingActionButton: widget.allowEdit ? _buildEditButton() : null,
|
floatingActionButton: widget.allowEdit ? _buildEditButton() : null,
|
||||||
@@ -78,15 +80,47 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
|
title: Text(context
|
||||||
|
.watch<DocumentDetailsCubit>()
|
||||||
|
.state
|
||||||
|
.document
|
||||||
|
.title),
|
||||||
leading: const BackButton(),
|
leading: const BackButton(),
|
||||||
floating: true,
|
|
||||||
pinned: true,
|
pinned: true,
|
||||||
expandedHeight: 200.0,
|
forceElevated: innerBoxIsScrolled,
|
||||||
flexibleSpace:
|
collapsedHeight: kToolbarHeight,
|
||||||
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
expandedHeight: 250.0,
|
||||||
builder: (context, state) => DocumentPreview(
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
document: state.document,
|
background: Stack(
|
||||||
fit: BoxFit.cover,
|
alignment: Alignment.topCenter,
|
||||||
|
children: [
|
||||||
|
BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
|
builder: (context, state) => Positioned.fill(
|
||||||
|
child: DocumentPreview(
|
||||||
|
document: state.document,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
top: 0,
|
||||||
|
child: Container(
|
||||||
|
height: 100,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.black.withOpacity(0.7),
|
||||||
|
Colors.black.withOpacity(0.2),
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.transparent,
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bottom: ColoredTabBar(
|
bottom: ColoredTabBar(
|
||||||
@@ -150,7 +184,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
children: [
|
children: [
|
||||||
DocumentOverviewWidget(
|
DocumentOverviewWidget(
|
||||||
document: state.document,
|
document: state.document,
|
||||||
itemSpacing: _itemPadding,
|
itemSpacing: _itemSpacing,
|
||||||
queryString: widget.titleAndContentQueryString,
|
queryString: widget.titleAndContentQueryString,
|
||||||
),
|
),
|
||||||
DocumentContentWidget(
|
DocumentContentWidget(
|
||||||
@@ -161,8 +195,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
),
|
),
|
||||||
DocumentMetaDataWidget(
|
DocumentMetaDataWidget(
|
||||||
document: state.document,
|
document: state.document,
|
||||||
itemSpacing: _itemPadding,
|
itemSpacing: _itemSpacing,
|
||||||
metaData: _metaData,
|
|
||||||
),
|
),
|
||||||
const SimilarDocumentsView(),
|
const SimilarDocumentsView(),
|
||||||
],
|
],
|
||||||
@@ -230,13 +263,10 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
? () => _onDelete(state.document)
|
? () => _onDelete(state.document)
|
||||||
: null,
|
: null,
|
||||||
).paddedSymmetrically(horizontal: 4),
|
).paddedSymmetrically(horizontal: 4),
|
||||||
Tooltip(
|
DocumentDownloadButton(
|
||||||
message: S.of(context)!.downloadDocumentTooltip,
|
document: state.document,
|
||||||
child: DocumentDownloadButton(
|
enabled: isConnected,
|
||||||
document: state.document,
|
metaData: _metaData,
|
||||||
enabled: isConnected,
|
|
||||||
metaData: _metaData,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: S.of(context)!.previewTooltip,
|
tooltip: S.of(context)!.previewTooltip,
|
||||||
@@ -249,14 +279,7 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
|||||||
icon: const Icon(Icons.open_in_new),
|
icon: const Icon(Icons.open_in_new),
|
||||||
onPressed: isConnected ? _onOpenFileInSystemViewer : null,
|
onPressed: isConnected ? _onOpenFileInSystemViewer : null,
|
||||||
).paddedOnly(right: 4.0),
|
).paddedOnly(right: 4.0),
|
||||||
IconButton(
|
DocumentShareButton(document: state.document),
|
||||||
tooltip: S.of(context)!.shareTooltip,
|
|
||||||
icon: const Icon(Icons.share),
|
|
||||||
onPressed: isConnected
|
|
||||||
? () =>
|
|
||||||
context.read<DocumentDetailsCubit>().shareDocument()
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
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/type/types.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';
|
||||||
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
|
||||||
|
class ArchiveSerialNumberField extends StatefulWidget {
|
||||||
|
final DocumentModel document;
|
||||||
|
const ArchiveSerialNumberField({
|
||||||
|
super.key,
|
||||||
|
required this.document,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ArchiveSerialNumberField> createState() =>
|
||||||
|
_ArchiveSerialNumberFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ArchiveSerialNumberFieldState extends State<ArchiveSerialNumberField> {
|
||||||
|
late final TextEditingController _asnEditingController;
|
||||||
|
late bool _showClearButton;
|
||||||
|
bool _canUpdate = false;
|
||||||
|
Map<String, dynamic> _errors = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_asnEditingController = TextEditingController(
|
||||||
|
text: widget.document.archiveSerialNumber?.toString(),
|
||||||
|
)..addListener(_clearButtonListener);
|
||||||
|
_showClearButton = widget.document.archiveSerialNumber != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearButtonListener() {
|
||||||
|
setState(() {
|
||||||
|
_showClearButton = _asnEditingController.text.isNotEmpty;
|
||||||
|
_canUpdate = int.tryParse(_asnEditingController.text) !=
|
||||||
|
widget.document.archiveSerialNumber;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocListener<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
|
listenWhen: (previous, current) =>
|
||||||
|
previous.document.archiveSerialNumber !=
|
||||||
|
current.document.archiveSerialNumber,
|
||||||
|
listener: (context, state) {
|
||||||
|
_asnEditingController.text =
|
||||||
|
state.document.archiveSerialNumber?.toString() ?? '';
|
||||||
|
setState(() {
|
||||||
|
_canUpdate = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _asnEditingController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _errors = {});
|
||||||
|
},
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
|
],
|
||||||
|
onFieldSubmitted: (_) => _onSubmitted(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
suffixIcon: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (_showClearButton)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
onPressed: _asnEditingController.clear,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.plus_one_rounded),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
onPressed:
|
||||||
|
context.watchInternetConnection && !_showClearButton
|
||||||
|
? _onAutoAssign
|
||||||
|
: null,
|
||||||
|
).paddedOnly(right: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
errorText: _errors['archive_serial_number'],
|
||||||
|
errorMaxLines: 2,
|
||||||
|
labelText: S.of(context)!.archiveSerialNumber,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
icon: const Icon(Icons.done),
|
||||||
|
onPressed: context.watchInternetConnection && _canUpdate
|
||||||
|
? _onSubmitted
|
||||||
|
: null,
|
||||||
|
label: Text(S.of(context)!.save),
|
||||||
|
).padded(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSubmitted() async {
|
||||||
|
final value = _asnEditingController.text;
|
||||||
|
final asn = int.tryParse(value);
|
||||||
|
|
||||||
|
await context
|
||||||
|
.read<DocumentDetailsCubit>()
|
||||||
|
.assignAsn(widget.document, asn: asn)
|
||||||
|
.then((value) => _onAsnUpdated())
|
||||||
|
.onError<PaperlessServerException>(
|
||||||
|
(error, stackTrace) => showErrorMessage(context, error, stackTrace),
|
||||||
|
)
|
||||||
|
.onError<PaperlessValidationErrors>(
|
||||||
|
(error, stackTrace) => setState(() => _errors = error),
|
||||||
|
);
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onAutoAssign() async {
|
||||||
|
await context
|
||||||
|
.read<DocumentDetailsCubit>()
|
||||||
|
.assignAsn(
|
||||||
|
widget.document,
|
||||||
|
autoAssign: true,
|
||||||
|
)
|
||||||
|
.then((value) => _onAsnUpdated())
|
||||||
|
.onError<PaperlessServerException>(
|
||||||
|
(error, stackTrace) => showErrorMessage(context, error, stackTrace),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAsnUpdated() {
|
||||||
|
setState(() => _errors = {});
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
showSnackBar(context, S.of(context)!.archiveSerialNumberUpdated);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.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/dialogs/select_file_type_dialog.dart';
|
||||||
|
import 'package:paperless_mobile/features/settings/cubit/application_settings_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -34,6 +37,7 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
|
tooltip: S.of(context)!.downloadDocumentTooltip,
|
||||||
icon: _isDownloadPending
|
icon: _isDownloadPending
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
@@ -48,25 +52,10 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onDownload(DocumentModel document) async {
|
Future<void> _onDownload(DocumentModel document) async {
|
||||||
final api = context.read<PaperlessDocumentsApi>();
|
|
||||||
final meta = await widget.metaData;
|
|
||||||
try {
|
try {
|
||||||
final downloadOriginal = await showDialog<bool>(
|
final downloadOriginal = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => RadioSettingsDialog(
|
builder: (context) => const SelectFileTypeDialog(),
|
||||||
titleText: S.of(context)!.chooseFiletype,
|
|
||||||
options: [
|
|
||||||
RadioOption(
|
|
||||||
value: true,
|
|
||||||
label: S.of(context)!.original +
|
|
||||||
" (${meta.originalMimeType.split("/").last})"),
|
|
||||||
RadioOption(
|
|
||||||
value: false,
|
|
||||||
label: S.of(context)!.archivedPdf,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
initialValue: false,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (downloadOriginal == null) {
|
if (downloadOriginal == null) {
|
||||||
// Download was cancelled
|
// Download was cancelled
|
||||||
@@ -79,20 +68,14 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setState(() => _isDownloadPending = true);
|
setState(() => _isDownloadPending = true);
|
||||||
final bytes = await api.download(
|
await context.read<DocumentDetailsCubit>().downloadDocument(
|
||||||
document,
|
downloadOriginal: downloadOriginal,
|
||||||
original: downloadOriginal,
|
locale: context
|
||||||
);
|
.read<ApplicationSettingsCubit>()
|
||||||
final Directory dir = await FileService.downloadsDirectory;
|
.state
|
||||||
final fileExtension =
|
.preferredLocaleSubtag,
|
||||||
downloadOriginal ? meta.mediaFilename.split(".").last : 'pdf';
|
);
|
||||||
String filePath = "${dir.path}/${meta.mediaFilename}".split(".").first;
|
// showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
|
||||||
filePath += ".$fileExtension";
|
|
||||||
final createdFile = File(filePath);
|
|
||||||
createdFile.createSync(recursive: true);
|
|
||||||
createdFile.writeAsBytesSync(bytes);
|
|
||||||
debugPrint("Downloaded file to $filePath");
|
|
||||||
showSnackBar(context, S.of(context)!.documentSuccessfullyDownloaded);
|
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
showErrorMessage(context, error, stackTrace);
|
showErrorMessage(context, error, stackTrace);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,98 +2,82 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
|
||||||
import 'package:paperless_mobile/core/widgets/offline_widget.dart';
|
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.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/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/features/document_details/view/widgets/details_item.dart';
|
||||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
||||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
|
||||||
|
|
||||||
class DocumentMetaDataWidget extends StatelessWidget {
|
class DocumentMetaDataWidget extends StatefulWidget {
|
||||||
final Future<DocumentMetaData> metaData;
|
|
||||||
final DocumentModel document;
|
final DocumentModel document;
|
||||||
final double itemSpacing;
|
final double itemSpacing;
|
||||||
const DocumentMetaDataWidget({
|
const DocumentMetaDataWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.metaData,
|
|
||||||
required this.document,
|
required this.document,
|
||||||
required this.itemSpacing,
|
required this.itemSpacing,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<DocumentMetaDataWidget> createState() => _DocumentMetaDataWidgetState();
|
||||||
return BlocBuilder<ConnectivityCubit, ConnectivityState>(
|
}
|
||||||
builder: (context, connectivity) {
|
|
||||||
return FutureBuilder<DocumentMetaData>(
|
|
||||||
future: metaData,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!connectivity.isConnected && !snapshot.hasData) {
|
|
||||||
return OfflineWidget();
|
|
||||||
}
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
final meta = snapshot.data!;
|
class _DocumentMetaDataWidgetState extends State<DocumentMetaDataWidget> {
|
||||||
return ListView(
|
@override
|
||||||
padding: const EdgeInsets.symmetric(
|
Widget build(BuildContext context) {
|
||||||
vertical: 16,
|
return BlocBuilder<DocumentDetailsCubit, DocumentDetailsState>(
|
||||||
horizontal: 16,
|
builder: (context, state) {
|
||||||
),
|
debugPrint("Building state...");
|
||||||
|
if (state.metaData == null) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 16,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
DetailsItem(
|
ArchiveSerialNumberField(
|
||||||
label: S.of(context)!.archiveSerialNumber,
|
document: widget.document,
|
||||||
content: document.archiveSerialNumber != null
|
).paddedOnly(bottom: widget.itemSpacing),
|
||||||
? Text(document.archiveSerialNumber.toString())
|
|
||||||
: TextButton.icon(
|
|
||||||
icon: const Icon(Icons.archive_outlined),
|
|
||||||
label: Text(S.of(context)!.assignAsn),
|
|
||||||
onPressed: connectivity.isConnected
|
|
||||||
? () => _assignAsn(context)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
).paddedOnly(bottom: itemSpacing),
|
|
||||||
DetailsItem.text(DateFormat().format(document.modified),
|
|
||||||
label: S.of(context)!.modifiedAt, context: context)
|
|
||||||
.paddedOnly(bottom: itemSpacing),
|
|
||||||
DetailsItem.text(DateFormat().format(document.added),
|
|
||||||
label: S.of(context)!.addedAt, context: context)
|
|
||||||
.paddedOnly(bottom: itemSpacing),
|
|
||||||
DetailsItem.text(
|
DetailsItem.text(
|
||||||
meta.mediaFilename,
|
DateFormat().format(widget.document.modified),
|
||||||
|
context: context,
|
||||||
|
label: S.of(context)!.modifiedAt,
|
||||||
|
).paddedOnly(bottom: widget.itemSpacing),
|
||||||
|
DetailsItem.text(
|
||||||
|
DateFormat().format(widget.document.added),
|
||||||
|
context: context,
|
||||||
|
label: S.of(context)!.addedAt,
|
||||||
|
).paddedOnly(bottom: widget.itemSpacing),
|
||||||
|
DetailsItem.text(
|
||||||
|
state.metaData!.mediaFilename,
|
||||||
context: context,
|
context: context,
|
||||||
label: S.of(context)!.mediaFilename,
|
label: S.of(context)!.mediaFilename,
|
||||||
).paddedOnly(bottom: itemSpacing),
|
).paddedOnly(bottom: widget.itemSpacing),
|
||||||
DetailsItem.text(
|
DetailsItem.text(
|
||||||
meta.originalChecksum,
|
state.metaData!.originalChecksum,
|
||||||
context: context,
|
context: context,
|
||||||
label: S.of(context)!.originalMD5Checksum,
|
label: S.of(context)!.originalMD5Checksum,
|
||||||
).paddedOnly(bottom: itemSpacing),
|
).paddedOnly(bottom: widget.itemSpacing),
|
||||||
DetailsItem.text(formatBytes(meta.originalSize, 2),
|
|
||||||
label: S.of(context)!.originalFileSize,
|
|
||||||
context: context)
|
|
||||||
.paddedOnly(bottom: itemSpacing),
|
|
||||||
DetailsItem.text(
|
DetailsItem.text(
|
||||||
meta.originalMimeType,
|
formatBytes(state.metaData!.originalSize, 2),
|
||||||
label: S.of(context)!.originalMIMEType,
|
|
||||||
context: context,
|
context: context,
|
||||||
).paddedOnly(bottom: itemSpacing),
|
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),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _assignAsn(BuildContext context) async {
|
|
||||||
try {
|
|
||||||
await context.read<DocumentDetailsCubit>().assignAsn(document);
|
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
|
||||||
showErrorMessage(context, error, stackTrace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/constants.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/dialogs/select_file_type_dialog.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||||
|
import 'package:paperless_mobile/helpers/permission_helpers.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class DocumentShareButton extends StatefulWidget {
|
||||||
|
final DocumentModel? document;
|
||||||
|
final bool enabled;
|
||||||
|
const DocumentShareButton({
|
||||||
|
super.key,
|
||||||
|
required this.document,
|
||||||
|
this.enabled = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DocumentShareButton> createState() => _DocumentShareButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DocumentShareButtonState extends State<DocumentShareButton> {
|
||||||
|
bool _isDownloadPending = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
tooltip: S.of(context)!.shareTooltip,
|
||||||
|
icon: _isDownloadPending
|
||||||
|
? const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.share),
|
||||||
|
onPressed: widget.document != null && widget.enabled
|
||||||
|
? () => _onShare(widget.document!)
|
||||||
|
: null,
|
||||||
|
).paddedOnly(right: 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onShare(DocumentModel document) async {
|
||||||
|
try {
|
||||||
|
final shareOriginal = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const SelectFileTypeDialog(),
|
||||||
|
);
|
||||||
|
if (shareOriginal == null) {
|
||||||
|
// Download was cancelled
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Platform.isAndroid && androidInfo!.version.sdkInt! < 30) {
|
||||||
|
final isGranted = await askForPermission(Permission.storage);
|
||||||
|
if (!isGranted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() => _isDownloadPending = true);
|
||||||
|
await context
|
||||||
|
.read<DocumentDetailsCubit>()
|
||||||
|
.shareDocument(shareOriginal: shareOriginal);
|
||||||
|
} on PaperlessServerException catch (error, stackTrace) {
|
||||||
|
showErrorMessage(context, error, stackTrace);
|
||||||
|
} catch (error) {
|
||||||
|
showGenericError(context, error);
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isDownloadPending = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
static const fkDocumentType = "documentType";
|
static const fkDocumentType = "documentType";
|
||||||
static const fkCreatedDate = "createdAtDate";
|
static const fkCreatedDate = "createdAtDate";
|
||||||
static const fkStoragePath = 'storagePath';
|
static const fkStoragePath = 'storagePath';
|
||||||
|
static const fkContent = 'content';
|
||||||
|
|
||||||
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
final GlobalKey<FormBuilderState> _formKey = GlobalKey();
|
||||||
bool _isSubmitLoading = false;
|
bool _isSubmitLoading = false;
|
||||||
@@ -55,94 +56,131 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<DocumentEditCubit, DocumentEditState>(
|
return BlocBuilder<DocumentEditCubit, DocumentEditState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Scaffold(
|
return DefaultTabController(
|
||||||
resizeToAvoidBottomInset: false,
|
length: 2,
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
child: Scaffold(
|
||||||
onPressed: () => _onSubmit(state.document),
|
resizeToAvoidBottomInset: false,
|
||||||
icon: const Icon(Icons.save),
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
label: Text(S.of(context)!.saveChanges),
|
onPressed: () => _onSubmit(state.document),
|
||||||
),
|
icon: const Icon(Icons.save),
|
||||||
appBar: AppBar(
|
label: Text(S.of(context)!.saveChanges),
|
||||||
title: Text(S.of(context)!.editDocument),
|
|
||||||
bottom: _isSubmitLoading
|
|
||||||
? const PreferredSize(
|
|
||||||
preferredSize: Size.fromHeight(4),
|
|
||||||
child: LinearProgressIndicator(),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
extendBody: true,
|
|
||||||
body: Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
||||||
top: 8,
|
|
||||||
left: 8,
|
|
||||||
right: 8,
|
|
||||||
),
|
),
|
||||||
child: FormBuilder(
|
appBar: AppBar(
|
||||||
key: _formKey,
|
title: Text(S.of(context)!.editDocument),
|
||||||
child: ListView(
|
bottom: TabBar(
|
||||||
children: [
|
tabs: [
|
||||||
_buildTitleFormField(state.document.title).padded(),
|
Tab(
|
||||||
_buildCreatedAtFormField(state.document.created).padded(),
|
text: S.of(context)!.overview,
|
||||||
_buildCorrespondentFormField(
|
),
|
||||||
state.document.correspondent,
|
Tab(
|
||||||
state.correspondents,
|
text: S.of(context)!.content,
|
||||||
).padded(),
|
)
|
||||||
_buildDocumentTypeFormField(
|
|
||||||
state.document.documentType,
|
|
||||||
state.documentTypes,
|
|
||||||
).padded(),
|
|
||||||
_buildStoragePathFormField(
|
|
||||||
state.document.storagePath,
|
|
||||||
state.storagePaths,
|
|
||||||
).padded(),
|
|
||||||
TagFormField(
|
|
||||||
initialValue:
|
|
||||||
IdsTagsQuery.included(state.document.tags.toList()),
|
|
||||||
notAssignedSelectable: false,
|
|
||||||
anyAssignedSelectable: false,
|
|
||||||
excludeAllowed: false,
|
|
||||||
name: fkTags,
|
|
||||||
selectableOptions: state.tags,
|
|
||||||
suggestions: _filteredSuggestions.tags
|
|
||||||
.toSet()
|
|
||||||
.difference(state.document.tags.toSet())
|
|
||||||
.isNotEmpty
|
|
||||||
? _buildSuggestionsSkeleton<int>(
|
|
||||||
suggestions: _filteredSuggestions.tags,
|
|
||||||
itemBuilder: (context, itemData) {
|
|
||||||
final tag = state.tags[itemData]!;
|
|
||||||
return ActionChip(
|
|
||||||
label: Text(
|
|
||||||
tag.name,
|
|
||||||
style: TextStyle(color: tag.textColor),
|
|
||||||
),
|
|
||||||
backgroundColor: tag.color,
|
|
||||||
onPressed: () {
|
|
||||||
final currentTags = _formKey.currentState
|
|
||||||
?.fields[fkTags]?.value as TagsQuery;
|
|
||||||
if (currentTags is IdsTagsQuery) {
|
|
||||||
_formKey.currentState?.fields[fkTags]
|
|
||||||
?.didChange((IdsTagsQuery.fromIds(
|
|
||||||
{...currentTags.ids, itemData})));
|
|
||||||
} else {
|
|
||||||
_formKey.currentState?.fields[fkTags]
|
|
||||||
?.didChange((IdsTagsQuery.fromIds(
|
|
||||||
{itemData})));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
).padded(),
|
|
||||||
const SizedBox(
|
|
||||||
height: 64), // Prevent tags from being hidden by fab
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
));
|
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)
|
||||||
|
.padded(),
|
||||||
|
_buildCorrespondentFormField(
|
||||||
|
state.document.correspondent,
|
||||||
|
state.correspondents,
|
||||||
|
).padded(),
|
||||||
|
_buildDocumentTypeFormField(
|
||||||
|
state.document.documentType,
|
||||||
|
state.documentTypes,
|
||||||
|
).padded(),
|
||||||
|
_buildStoragePathFormField(
|
||||||
|
state.document.storagePath,
|
||||||
|
state.storagePaths,
|
||||||
|
).padded(),
|
||||||
|
TagFormField(
|
||||||
|
initialValue: IdsTagsQuery.included(
|
||||||
|
state.document.tags.toList()),
|
||||||
|
notAssignedSelectable: false,
|
||||||
|
anyAssignedSelectable: false,
|
||||||
|
excludeAllowed: false,
|
||||||
|
name: fkTags,
|
||||||
|
selectableOptions: state.tags,
|
||||||
|
suggestions: _filteredSuggestions.tags
|
||||||
|
.toSet()
|
||||||
|
.difference(state.document.tags.toSet())
|
||||||
|
.isNotEmpty
|
||||||
|
? _buildSuggestionsSkeleton<int>(
|
||||||
|
suggestions: _filteredSuggestions.tags,
|
||||||
|
itemBuilder: (context, itemData) {
|
||||||
|
final tag = state.tags[itemData]!;
|
||||||
|
return ActionChip(
|
||||||
|
label: Text(
|
||||||
|
tag.name,
|
||||||
|
style:
|
||||||
|
TextStyle(color: tag.textColor),
|
||||||
|
),
|
||||||
|
backgroundColor: tag.color,
|
||||||
|
onPressed: () {
|
||||||
|
final currentTags = _formKey
|
||||||
|
.currentState
|
||||||
|
?.fields[fkTags]
|
||||||
|
?.value as TagsQuery;
|
||||||
|
if (currentTags is IdsTagsQuery) {
|
||||||
|
_formKey
|
||||||
|
.currentState?.fields[fkTags]
|
||||||
|
?.didChange(
|
||||||
|
(IdsTagsQuery.fromIds({
|
||||||
|
...currentTags.ids,
|
||||||
|
itemData
|
||||||
|
})));
|
||||||
|
} else {
|
||||||
|
_formKey
|
||||||
|
.currentState?.fields[fkTags]
|
||||||
|
?.didChange(
|
||||||
|
(IdsTagsQuery.fromIds(
|
||||||
|
{itemData})));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
).padded(),
|
||||||
|
// Prevent tags from being hidden by fab
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -238,13 +276,13 @@ class _DocumentEditPageState extends State<DocumentEditPage> {
|
|||||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
final values = _formKey.currentState!.value;
|
final values = _formKey.currentState!.value;
|
||||||
var mergedDocument = document.copyWith(
|
var mergedDocument = document.copyWith(
|
||||||
title: values[fkTitle],
|
title: values[fkTitle],
|
||||||
created: values[fkCreatedDate],
|
created: values[fkCreatedDate],
|
||||||
documentType: () => (values[fkDocumentType] as IdQueryParameter).id,
|
documentType: () => (values[fkDocumentType] as IdQueryParameter).id,
|
||||||
correspondent: () => (values[fkCorrespondent] as IdQueryParameter).id,
|
correspondent: () => (values[fkCorrespondent] as IdQueryParameter).id,
|
||||||
storagePath: () => (values[fkStoragePath] as IdQueryParameter).id,
|
storagePath: () => (values[fkStoragePath] as IdQueryParameter).id,
|
||||||
tags: (values[fkTags] as IdsTagsQuery).includedIds,
|
tags: (values[fkTags] as IdsTagsQuery).includedIds,
|
||||||
);
|
content: values[fkContent]);
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSubmitLoading = true;
|
_isSubmitLoading = true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ class InboxCubit extends HydratedCubit<InboxState>
|
|||||||
if (document.archiveSerialNumber == null) {
|
if (document.archiveSerialNumber == null) {
|
||||||
final int asn = await _documentsApi.findNextAsn();
|
final int asn = await _documentsApi.findNextAsn();
|
||||||
final updatedDocument = await _documentsApi
|
final updatedDocument = await _documentsApi
|
||||||
.update(document.copyWith(archiveSerialNumber: asn));
|
.update(document.copyWith(archiveSerialNumber: () => asn));
|
||||||
|
|
||||||
replace(updatedDocument);
|
replace(updatedDocument);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:paperless_mobile/features/notifications/models/notification_actions.dart';
|
||||||
|
import 'package:paperless_mobile/features/notifications/models/notification_payloads/notification_tap/notification_tap_response_payload.dart';
|
||||||
|
import 'package:paperless_mobile/features/notifications/models/notification_payloads/notification_tap/open_downloaded_document_payload.dart';
|
||||||
|
|
||||||
|
class NotificationTapResponsePayloadConverter
|
||||||
|
implements
|
||||||
|
JsonConverter<NotificationTapResponsePayload, Map<String, dynamic>> {
|
||||||
|
const NotificationTapResponsePayloadConverter();
|
||||||
|
@override
|
||||||
|
NotificationTapResponsePayload fromJson(Map<String, dynamic> json) {
|
||||||
|
final type = NotificationResponseOpenAction.values.byName(json['type']);
|
||||||
|
switch (type) {
|
||||||
|
case NotificationResponseOpenAction.openDownloadedDocumentPath:
|
||||||
|
return OpenDownloadedDocumentPayload.fromJson(
|
||||||
|
json,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson(NotificationTapResponsePayload object) {
|
||||||
|
return object.toJson();
|
||||||
|
}
|
||||||
|
}
|
||||||
11
lib/features/notifications/models/notification_actions.dart
Normal file
11
lib/features/notifications/models/notification_actions.dart
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
enum NotificationResponseButtonAction {
|
||||||
|
openCreatedDocument,
|
||||||
|
acknowledgeCreatedDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonEnum()
|
||||||
|
enum NotificationResponseOpenAction {
|
||||||
|
openDownloadedDocumentPath;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
enum NotificationChannel {
|
enum NotificationChannel {
|
||||||
task("task_channel", "Paperless Tasks");
|
task("task_channel", "Paperless tasks"),
|
||||||
|
documentDownload("document_download_channel", "Document downloads");
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'create_document_success_payload.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CreateDocumentSuccessPayload {
|
||||||
|
final int documentId;
|
||||||
|
|
||||||
|
CreateDocumentSuccessPayload(this.documentId);
|
||||||
|
|
||||||
|
factory CreateDocumentSuccessPayload.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CreateDocumentSuccessPayloadFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CreateDocumentSuccessPayloadToJson(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:paperless_mobile/features/notifications/models/notification_actions.dart';
|
||||||
|
|
||||||
|
abstract class NotificationTapResponsePayload {
|
||||||
|
final NotificationResponseOpenAction type;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
NotificationTapResponsePayload({required this.type});
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:paperless_mobile/features/notifications/models/notification_actions.dart';
|
||||||
|
import 'package:paperless_mobile/features/notifications/models/notification_payloads/notification_tap/notification_tap_response_payload.dart';
|
||||||
|
|
||||||
|
part 'open_downloaded_document_payload.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class OpenDownloadedDocumentPayload extends NotificationTapResponsePayload {
|
||||||
|
final String filePath;
|
||||||
|
OpenDownloadedDocumentPayload({
|
||||||
|
required this.filePath,
|
||||||
|
super.type = NotificationResponseOpenAction.openDownloadedDocumentPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory OpenDownloadedDocumentPayload.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$OpenDownloadedDocumentPayloadFromJson(json);
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => _$OpenDownloadedDocumentPayloadToJson(this);
|
||||||
|
}
|
||||||
@@ -2,11 +2,16 @@ import 'dart:convert';
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:open_filex/open_filex.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/features/notifications/services/models/notification_payloads/open_created_document_notification_payload.dart';
|
import 'package:paperless_mobile/features/notifications/converters/notification_tap_response_payload.dart';
|
||||||
import 'package:paperless_mobile/features/notifications/services/notification_actions.dart';
|
import 'package:paperless_mobile/features/notifications/models/notification_payloads/notification_action/create_document_success_payload.dart';
|
||||||
import 'package:paperless_mobile/features/notifications/services/notification_channels.dart';
|
import 'package:paperless_mobile/features/notifications/models/notification_payloads/notification_tap/open_downloaded_document_payload.dart';
|
||||||
|
import 'package:paperless_mobile/features/notifications/models/notification_actions.dart';
|
||||||
|
import 'package:paperless_mobile/features/notifications/models/notification_channels.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class LocalNotificationService {
|
class LocalNotificationService {
|
||||||
final FlutterLocalNotificationsPlugin _plugin =
|
final FlutterLocalNotificationsPlugin _plugin =
|
||||||
@@ -16,7 +21,7 @@ class LocalNotificationService {
|
|||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||||
AndroidInitializationSettings('ic_stat_paperless_logo_green');
|
AndroidInitializationSettings('paperless_logo_green');
|
||||||
final DarwinInitializationSettings initializationSettingsDarwin =
|
final DarwinInitializationSettings initializationSettingsDarwin =
|
||||||
DarwinInitializationSettings(
|
DarwinInitializationSettings(
|
||||||
requestSoundPermission: false,
|
requestSoundPermission: false,
|
||||||
@@ -32,6 +37,8 @@ class LocalNotificationService {
|
|||||||
await _plugin.initialize(
|
await _plugin.initialize(
|
||||||
initializationSettings,
|
initializationSettings,
|
||||||
onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
|
onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
|
||||||
|
onDidReceiveBackgroundNotificationResponse:
|
||||||
|
onDidReceiveBackgroundNotificationResponse,
|
||||||
);
|
);
|
||||||
await _plugin
|
await _plugin
|
||||||
.resolvePlatformSpecificImplementation<
|
.resolvePlatformSpecificImplementation<
|
||||||
@@ -39,6 +46,51 @@ class LocalNotificationService {
|
|||||||
?.requestPermission();
|
?.requestPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> notifyFileDownload({
|
||||||
|
required DocumentModel document,
|
||||||
|
required String filename,
|
||||||
|
required String filePath,
|
||||||
|
required bool finished,
|
||||||
|
required String locale,
|
||||||
|
}) async {
|
||||||
|
final tr = await S.delegate.load(Locale(locale));
|
||||||
|
|
||||||
|
int id = document.id;
|
||||||
|
await _plugin.show(
|
||||||
|
id,
|
||||||
|
filename,
|
||||||
|
finished
|
||||||
|
? tr.notificationDownloadComplete
|
||||||
|
: tr.notificationDownloadingDocument,
|
||||||
|
NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
NotificationChannel.documentDownload.id + "_${document.id}",
|
||||||
|
NotificationChannel.documentDownload.name,
|
||||||
|
ongoing: !finished,
|
||||||
|
indeterminate: true,
|
||||||
|
importance: Importance.max,
|
||||||
|
priority: Priority.high,
|
||||||
|
showProgress: !finished,
|
||||||
|
when: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
category: AndroidNotificationCategory.progress,
|
||||||
|
icon: finished ? 'file_download_done' : 'downloading',
|
||||||
|
),
|
||||||
|
iOS: DarwinNotificationDetails(
|
||||||
|
attachments: [
|
||||||
|
DarwinNotificationAttachment(
|
||||||
|
filePath,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
payload: jsonEncode(
|
||||||
|
OpenDownloadedDocumentPayload(
|
||||||
|
filePath: filePath,
|
||||||
|
).toJson(),
|
||||||
|
),
|
||||||
|
); //TODO: INTL
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: INTL
|
//TODO: INTL
|
||||||
Future<void> notifyTaskChanged(Task task) {
|
Future<void> notifyTaskChanged(Task task) {
|
||||||
log("[LocalNotificationService] notifyTaskChanged: ${task.toString()}");
|
log("[LocalNotificationService] notifyTaskChanged: ${task.toString()}");
|
||||||
@@ -49,20 +101,17 @@ class LocalNotificationService {
|
|||||||
late int timestampMillis;
|
late int timestampMillis;
|
||||||
bool showProgress =
|
bool showProgress =
|
||||||
status == TaskStatus.started || status == TaskStatus.pending;
|
status == TaskStatus.started || status == TaskStatus.pending;
|
||||||
int progress = 0;
|
|
||||||
dynamic payload;
|
dynamic payload;
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case TaskStatus.started:
|
case TaskStatus.started:
|
||||||
title = "Document received";
|
title = "Document received";
|
||||||
body = task.taskFileName;
|
body = task.taskFileName;
|
||||||
timestampMillis = task.dateCreated.millisecondsSinceEpoch;
|
timestampMillis = task.dateCreated.millisecondsSinceEpoch;
|
||||||
progress = 10;
|
|
||||||
break;
|
break;
|
||||||
case TaskStatus.pending:
|
case TaskStatus.pending:
|
||||||
title = "Processing document...";
|
title = "Processing document...";
|
||||||
body = task.taskFileName;
|
body = task.taskFileName;
|
||||||
timestampMillis = task.dateCreated.millisecondsSinceEpoch;
|
timestampMillis = task.dateCreated.millisecondsSinceEpoch;
|
||||||
progress = 70;
|
|
||||||
break;
|
break;
|
||||||
case TaskStatus.failure:
|
case TaskStatus.failure:
|
||||||
title = "Failed to process document";
|
title = "Failed to process document";
|
||||||
@@ -73,7 +122,7 @@ class LocalNotificationService {
|
|||||||
title = "Document successfully created";
|
title = "Document successfully created";
|
||||||
body = task.taskFileName;
|
body = task.taskFileName;
|
||||||
timestampMillis = task.dateDone!.millisecondsSinceEpoch;
|
timestampMillis = task.dateDone!.millisecondsSinceEpoch;
|
||||||
payload = CreateDocumentSuccessNotificationResponsePayload(
|
payload = CreateDocumentSuccessPayload(
|
||||||
task.relatedDocument!,
|
task.relatedDocument!,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -93,7 +142,7 @@ class LocalNotificationService {
|
|||||||
showProgress: showProgress,
|
showProgress: showProgress,
|
||||||
maxProgress: 100,
|
maxProgress: 100,
|
||||||
when: timestampMillis,
|
when: timestampMillis,
|
||||||
progress: progress,
|
indeterminate: true,
|
||||||
actions: status == TaskStatus.success
|
actions: status == TaskStatus.success
|
||||||
? [
|
? [
|
||||||
//TODO: Implement once moved to new routing
|
//TODO: Implement once moved to new routing
|
||||||
@@ -109,6 +158,7 @@ class LocalNotificationService {
|
|||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
),
|
),
|
||||||
|
//TODO: Add darwin support
|
||||||
),
|
),
|
||||||
payload: jsonEncode(payload),
|
payload: jsonEncode(payload),
|
||||||
);
|
);
|
||||||
@@ -119,38 +169,68 @@ class LocalNotificationService {
|
|||||||
String? title,
|
String? title,
|
||||||
String? body,
|
String? body,
|
||||||
String? payload,
|
String? payload,
|
||||||
) {}
|
) {
|
||||||
|
debugPrint("onDidReceiveNotification!");
|
||||||
void onDidReceiveNotificationResponse(NotificationResponse response) {
|
|
||||||
debugPrint("Received Notification: ${response.payload}");
|
|
||||||
if (response.notificationResponseType ==
|
|
||||||
NotificationResponseType.selectedNotificationAction) {
|
|
||||||
final action =
|
|
||||||
NotificationResponseAction.values.byName(response.actionId!);
|
|
||||||
_handleResponseAction(action, response);
|
|
||||||
}
|
|
||||||
// Non-actionable notification pressed, ignoring...
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleResponseAction(
|
void onDidReceiveNotificationResponse(NotificationResponse response) {
|
||||||
NotificationResponseAction action,
|
debugPrint(
|
||||||
|
"Received Notification ${response.id}: Action is ${response.actionId}): ${response.payload}",
|
||||||
|
);
|
||||||
|
switch (response.notificationResponseType) {
|
||||||
|
case NotificationResponseType.selectedNotification:
|
||||||
|
if (response.payload != null) {
|
||||||
|
final payload =
|
||||||
|
const NotificationTapResponsePayloadConverter().fromJson(
|
||||||
|
jsonDecode(response.payload!),
|
||||||
|
);
|
||||||
|
_handleResponseTapAction(payload.type, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case NotificationResponseType.selectedNotificationAction:
|
||||||
|
final action =
|
||||||
|
NotificationResponseButtonAction.values.byName(response.actionId!);
|
||||||
|
_handleResponseButtonAction(action, response);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleResponseButtonAction(
|
||||||
|
NotificationResponseButtonAction action,
|
||||||
NotificationResponse response,
|
NotificationResponse response,
|
||||||
) {
|
) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case NotificationResponseAction.openCreatedDocument:
|
case NotificationResponseButtonAction.openCreatedDocument:
|
||||||
final payload =
|
final payload = CreateDocumentSuccessPayload.fromJson(
|
||||||
CreateDocumentSuccessNotificationResponsePayload.fromJson(
|
|
||||||
jsonDecode(response.payload!),
|
jsonDecode(response.payload!),
|
||||||
);
|
);
|
||||||
log("Navigate to document ${payload.documentId}");
|
log("Navigate to document ${payload.documentId}");
|
||||||
break;
|
break;
|
||||||
case NotificationResponseAction.acknowledgeCreatedDocument:
|
case NotificationResponseButtonAction.acknowledgeCreatedDocument:
|
||||||
final payload =
|
final payload = CreateDocumentSuccessPayload.fromJson(
|
||||||
CreateDocumentSuccessNotificationResponsePayload.fromJson(
|
|
||||||
jsonDecode(response.payload!),
|
jsonDecode(response.payload!),
|
||||||
);
|
);
|
||||||
log("Acknowledge document ${payload.documentId}");
|
log("Acknowledge document ${payload.documentId}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleResponseTapAction(
|
||||||
|
NotificationResponseOpenAction type,
|
||||||
|
NotificationResponse response,
|
||||||
|
) {
|
||||||
|
switch (type) {
|
||||||
|
case NotificationResponseOpenAction.openDownloadedDocumentPath:
|
||||||
|
final payload = OpenDownloadedDocumentPayload.fromJson(
|
||||||
|
jsonDecode(response.payload!));
|
||||||
|
OpenFilex.open(payload.filePath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDidReceiveBackgroundNotificationResponse(NotificationResponse response) {
|
||||||
|
//TODO: When periodic background inbox check is implemented, notification tap is handled here
|
||||||
|
debugPrint(response.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'open_created_document_notification_payload.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class CreateDocumentSuccessNotificationResponsePayload {
|
|
||||||
final int documentId;
|
|
||||||
|
|
||||||
CreateDocumentSuccessNotificationResponsePayload(this.documentId);
|
|
||||||
|
|
||||||
factory CreateDocumentSuccessNotificationResponsePayload.fromJson(
|
|
||||||
Map<String, dynamic> json) =>
|
|
||||||
_$CreateDocumentSuccessNotificationResponsePayloadFromJson(json);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() =>
|
|
||||||
_$CreateDocumentSuccessNotificationResponsePayloadToJson(this);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
enum NotificationResponseAction {
|
|
||||||
openCreatedDocument,
|
|
||||||
acknowledgeCreatedDocument;
|
|
||||||
}
|
|
||||||
@@ -677,5 +677,21 @@
|
|||||||
"dynamicColorScheme": "Dynamicky",
|
"dynamicColorScheme": "Dynamicky",
|
||||||
"@dynamicColorScheme": {},
|
"@dynamicColorScheme": {},
|
||||||
"classicColorScheme": "Klasicky",
|
"classicColorScheme": "Klasicky",
|
||||||
"@classicColorScheme": {}
|
"@classicColorScheme": {},
|
||||||
|
"notificationDownloadComplete": "Download complete",
|
||||||
|
"@notificationDownloadComplete": {
|
||||||
|
"description": "Notification title when a download has been completed."
|
||||||
|
},
|
||||||
|
"notificationDownloadingDocument": "Downloading document",
|
||||||
|
"@notificationDownloadingDocument": {
|
||||||
|
"description": "Notification title shown when a document download is pending"
|
||||||
|
},
|
||||||
|
"archiveSerialNumberUpdated": "Archive Serial Number updated.",
|
||||||
|
"@archiveSerialNumberUpdated": {
|
||||||
|
"description": "Message shown when the ASN has been updated."
|
||||||
|
},
|
||||||
|
"donateCoffee": "Buy me a coffee",
|
||||||
|
"@donateCoffee": {
|
||||||
|
"description": "Label displayed in the app drawer"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -677,5 +677,21 @@
|
|||||||
"dynamicColorScheme": "Dynamisch",
|
"dynamicColorScheme": "Dynamisch",
|
||||||
"@dynamicColorScheme": {},
|
"@dynamicColorScheme": {},
|
||||||
"classicColorScheme": "Klassisch",
|
"classicColorScheme": "Klassisch",
|
||||||
"@classicColorScheme": {}
|
"@classicColorScheme": {},
|
||||||
|
"notificationDownloadComplete": "Download abgeschlossen",
|
||||||
|
"@notificationDownloadComplete": {
|
||||||
|
"description": "Notification title when a download has been completed."
|
||||||
|
},
|
||||||
|
"notificationDownloadingDocument": "Dokument wird heruntergeladen",
|
||||||
|
"@notificationDownloadingDocument": {
|
||||||
|
"description": "Notification title shown when a document download is pending"
|
||||||
|
},
|
||||||
|
"archiveSerialNumberUpdated": "Archiv-Seriennummer aktualisiert.",
|
||||||
|
"@archiveSerialNumberUpdated": {
|
||||||
|
"description": "Message shown when the ASN has been updated."
|
||||||
|
},
|
||||||
|
"donateCoffee": "Spendiere mir einen Kaffee",
|
||||||
|
"@donateCoffee": {
|
||||||
|
"description": "Label displayed in the app drawer"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -677,5 +677,21 @@
|
|||||||
"dynamicColorScheme": "Dynamic",
|
"dynamicColorScheme": "Dynamic",
|
||||||
"@dynamicColorScheme": {},
|
"@dynamicColorScheme": {},
|
||||||
"classicColorScheme": "Classic",
|
"classicColorScheme": "Classic",
|
||||||
"@classicColorScheme": {}
|
"@classicColorScheme": {},
|
||||||
|
"notificationDownloadComplete": "Download complete",
|
||||||
|
"@notificationDownloadComplete": {
|
||||||
|
"description": "Notification title when a download has been completed."
|
||||||
|
},
|
||||||
|
"notificationDownloadingDocument": "Downloading document",
|
||||||
|
"@notificationDownloadingDocument": {
|
||||||
|
"description": "Notification title shown when a document download is pending"
|
||||||
|
},
|
||||||
|
"archiveSerialNumberUpdated": "Archive Serial Number updated.",
|
||||||
|
"@archiveSerialNumberUpdated": {
|
||||||
|
"description": "Message shown when the ASN has been updated."
|
||||||
|
},
|
||||||
|
"donateCoffee": "Buy me a coffee",
|
||||||
|
"@donateCoffee": {
|
||||||
|
"description": "Label displayed in the app drawer"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -516,15 +516,15 @@
|
|||||||
"@password": {},
|
"@password": {},
|
||||||
"passwordMustNotBeEmpty": "Le mot de passe ne doit pas être vide.",
|
"passwordMustNotBeEmpty": "Le mot de passe ne doit pas être vide.",
|
||||||
"@passwordMustNotBeEmpty": {},
|
"@passwordMustNotBeEmpty": {},
|
||||||
"connectionTimedOut": "La connection a expiré.",
|
"connectionTimedOut": "La connexion a expiré.",
|
||||||
"@connectionTimedOut": {},
|
"@connectionTimedOut": {},
|
||||||
"loginPageReachabilityMissingClientCertificateText": "Un certificat client était attendu mais n'a pas été envoyé. Veuillez fournir un certificat.",
|
"loginPageReachabilityMissingClientCertificateText": "Un certificat client était attendu, mais n'a pas été envoyé. Veuillez fournir un certificat.",
|
||||||
"@loginPageReachabilityMissingClientCertificateText": {},
|
"@loginPageReachabilityMissingClientCertificateText": {},
|
||||||
"couldNotEstablishConnectionToTheServer": "Impossible d'établir la connection jusqu'au serveur.",
|
"couldNotEstablishConnectionToTheServer": "Impossible d'établir la connexion jusqu'au serveur.",
|
||||||
"@couldNotEstablishConnectionToTheServer": {},
|
"@couldNotEstablishConnectionToTheServer": {},
|
||||||
"connectionSuccessfulylEstablished": "Connection établie avec succès.",
|
"connectionSuccessfulylEstablished": "Connexion établie avec succès.",
|
||||||
"@connectionSuccessfulylEstablished": {},
|
"@connectionSuccessfulylEstablished": {},
|
||||||
"hostCouldNotBeResolved": "L'hôte ne peut pas être résolu. Veuillez vérifier l'addresse du serveur et votre connection internet. ",
|
"hostCouldNotBeResolved": "L'hôte ne peut pas être résolu. Veuillez vérifier l'adresse du serveur et votre connexion internet. ",
|
||||||
"@hostCouldNotBeResolved": {},
|
"@hostCouldNotBeResolved": {},
|
||||||
"serverAddress": "Adresse du Serveur",
|
"serverAddress": "Adresse du Serveur",
|
||||||
"@serverAddress": {},
|
"@serverAddress": {},
|
||||||
@@ -532,7 +532,7 @@
|
|||||||
"@invalidAddress": {},
|
"@invalidAddress": {},
|
||||||
"serverAddressMustIncludeAScheme": "L'adresse du serveur doit respecter le schéma.",
|
"serverAddressMustIncludeAScheme": "L'adresse du serveur doit respecter le schéma.",
|
||||||
"@serverAddressMustIncludeAScheme": {},
|
"@serverAddressMustIncludeAScheme": {},
|
||||||
"serverAddressMustNotBeEmpty": "L'addresse du serveur ne doit pas être vide.",
|
"serverAddressMustNotBeEmpty": "L'adresse du serveur ne doit pas être vide.",
|
||||||
"@serverAddressMustNotBeEmpty": {},
|
"@serverAddressMustNotBeEmpty": {},
|
||||||
"signIn": "Se connecter",
|
"signIn": "Se connecter",
|
||||||
"@signIn": {},
|
"@signIn": {},
|
||||||
@@ -574,7 +574,7 @@
|
|||||||
"@documentMatchesThisRegularExpression": {},
|
"@documentMatchesThisRegularExpression": {},
|
||||||
"regularExpression": "Expression Régulière",
|
"regularExpression": "Expression Régulière",
|
||||||
"@regularExpression": {},
|
"@regularExpression": {},
|
||||||
"anInternetConnectionCouldNotBeEstablished": "Impossible d'établir une connection internet.",
|
"anInternetConnectionCouldNotBeEstablished": "Impossible d'établir une connexion internet.",
|
||||||
"@anInternetConnectionCouldNotBeEstablished": {},
|
"@anInternetConnectionCouldNotBeEstablished": {},
|
||||||
"done": "Fait",
|
"done": "Fait",
|
||||||
"@done": {},
|
"@done": {},
|
||||||
@@ -622,7 +622,7 @@
|
|||||||
"@languageAndVisualAppearance": {},
|
"@languageAndVisualAppearance": {},
|
||||||
"applicationSettings": "Application",
|
"applicationSettings": "Application",
|
||||||
"@applicationSettings": {},
|
"@applicationSettings": {},
|
||||||
"colorSchemeHint": "Choisissez entre une palette de couleurs inspirée par le vert Paperless traditionne, ou utilisez la palette de couleur dynamique basée sur le thème système.",
|
"colorSchemeHint": "Choisissez entre une palette de couleurs inspirée par le vert Paperless traditionnel, ou utilisez la palette de couleur dynamique basée sur le thème système.",
|
||||||
"@colorSchemeHint": {},
|
"@colorSchemeHint": {},
|
||||||
"colorSchemeNotSupportedWarning": "Le thème dynamique n'est supporté que sur les appareils sous Android 12 ou plus. Sélectionner l'option 'Dynamique' pourrait ne pas avoir d'effet en fonction de l'implémentation de votre système d'exploitation.",
|
"colorSchemeNotSupportedWarning": "Le thème dynamique n'est supporté que sur les appareils sous Android 12 ou plus. Sélectionner l'option 'Dynamique' pourrait ne pas avoir d'effet en fonction de l'implémentation de votre système d'exploitation.",
|
||||||
"@colorSchemeNotSupportedWarning": {},
|
"@colorSchemeNotSupportedWarning": {},
|
||||||
@@ -673,9 +673,25 @@
|
|||||||
"list": "Liste",
|
"list": "Liste",
|
||||||
"@list": {},
|
"@list": {},
|
||||||
"remove": "Retirer",
|
"remove": "Retirer",
|
||||||
"removeQueryFromSearchHistory": "Retirer la requête de l'historique de recherche ?",
|
"removeQueryFromSearchHistory": "Retirer la requête de l'historique de recherche ?",
|
||||||
"dynamicColorScheme": "Dynamique",
|
"dynamicColorScheme": "Dynamique",
|
||||||
"@dynamicColorScheme": {},
|
"@dynamicColorScheme": {},
|
||||||
"classicColorScheme": "Classique",
|
"classicColorScheme": "Classique",
|
||||||
"@classicColorScheme": {}
|
"@classicColorScheme": {},
|
||||||
|
"notificationDownloadComplete": "Download complete",
|
||||||
|
"@notificationDownloadComplete": {
|
||||||
|
"description": "Notification title when a download has been completed."
|
||||||
|
},
|
||||||
|
"notificationDownloadingDocument": "Downloading document",
|
||||||
|
"@notificationDownloadingDocument": {
|
||||||
|
"description": "Notification title shown when a document download is pending"
|
||||||
|
},
|
||||||
|
"archiveSerialNumberUpdated": "Archive Serial Number updated.",
|
||||||
|
"@archiveSerialNumberUpdated": {
|
||||||
|
"description": "Message shown when the ASN has been updated."
|
||||||
|
},
|
||||||
|
"donateCoffee": "Buy me a coffee",
|
||||||
|
"@donateCoffee": {
|
||||||
|
"description": "Label displayed in the app drawer"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -677,5 +677,21 @@
|
|||||||
"dynamicColorScheme": "Dynamic",
|
"dynamicColorScheme": "Dynamic",
|
||||||
"@dynamicColorScheme": {},
|
"@dynamicColorScheme": {},
|
||||||
"classicColorScheme": "Classic",
|
"classicColorScheme": "Classic",
|
||||||
"@classicColorScheme": {}
|
"@classicColorScheme": {},
|
||||||
|
"notificationDownloadComplete": "Download complete",
|
||||||
|
"@notificationDownloadComplete": {
|
||||||
|
"description": "Notification title when a download has been completed."
|
||||||
|
},
|
||||||
|
"notificationDownloadingDocument": "Downloading document",
|
||||||
|
"@notificationDownloadingDocument": {
|
||||||
|
"description": "Notification title shown when a document download is pending"
|
||||||
|
},
|
||||||
|
"archiveSerialNumberUpdated": "Archive Serial Number updated.",
|
||||||
|
"@archiveSerialNumberUpdated": {
|
||||||
|
"description": "Message shown when the ASN has been updated."
|
||||||
|
},
|
||||||
|
"donateCoffee": "Buy me a coffee",
|
||||||
|
"@donateCoffee": {
|
||||||
|
"description": "Label displayed in the app drawer"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -677,5 +677,21 @@
|
|||||||
"dynamicColorScheme": "Dynamic",
|
"dynamicColorScheme": "Dynamic",
|
||||||
"@dynamicColorScheme": {},
|
"@dynamicColorScheme": {},
|
||||||
"classicColorScheme": "Classic",
|
"classicColorScheme": "Classic",
|
||||||
"@classicColorScheme": {}
|
"@classicColorScheme": {},
|
||||||
|
"notificationDownloadComplete": "Download complete",
|
||||||
|
"@notificationDownloadComplete": {
|
||||||
|
"description": "Notification title when a download has been completed."
|
||||||
|
},
|
||||||
|
"notificationDownloadingDocument": "Downloading document",
|
||||||
|
"@notificationDownloadingDocument": {
|
||||||
|
"description": "Notification title shown when a document download is pending"
|
||||||
|
},
|
||||||
|
"archiveSerialNumberUpdated": "Archive Serial Number updated.",
|
||||||
|
"@archiveSerialNumberUpdated": {
|
||||||
|
"description": "Message shown when the ASN has been updated."
|
||||||
|
},
|
||||||
|
"donateCoffee": "Buy me a coffee",
|
||||||
|
"@donateCoffee": {
|
||||||
|
"description": "Label displayed in the app drawer"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ class DocumentDetailsRoute extends StatelessWidget {
|
|||||||
|
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => DocumentDetailsCubit(
|
create: (context) => DocumentDetailsCubit(
|
||||||
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
context.read(),
|
context.read(),
|
||||||
initialDocument: args.document,
|
initialDocument: args.document,
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class DocumentModel extends Equatable {
|
|||||||
DateTime? created,
|
DateTime? created,
|
||||||
DateTime? modified,
|
DateTime? modified,
|
||||||
DateTime? added,
|
DateTime? added,
|
||||||
int? archiveSerialNumber,
|
int? Function()? archiveSerialNumber,
|
||||||
String? originalFileName,
|
String? originalFileName,
|
||||||
String? archivedFileName,
|
String? archivedFileName,
|
||||||
}) {
|
}) {
|
||||||
@@ -84,17 +84,18 @@ class DocumentModel extends Equatable {
|
|||||||
id: id,
|
id: id,
|
||||||
title: title ?? this.title,
|
title: title ?? this.title,
|
||||||
content: content ?? this.content,
|
content: content ?? this.content,
|
||||||
documentType:
|
documentType: documentType != null ? documentType() : this.documentType,
|
||||||
documentType != null ? documentType.call() : this.documentType,
|
|
||||||
correspondent:
|
correspondent:
|
||||||
correspondent != null ? correspondent.call() : this.correspondent,
|
correspondent != null ? correspondent() : this.correspondent,
|
||||||
storagePath: storagePath != null ? storagePath.call() : this.storagePath,
|
storagePath: storagePath != null ? storagePath() : this.storagePath,
|
||||||
tags: tags ?? this.tags,
|
tags: tags ?? this.tags,
|
||||||
created: created ?? this.created,
|
created: created ?? this.created,
|
||||||
modified: modified ?? this.modified,
|
modified: modified ?? this.modified,
|
||||||
added: added ?? this.added,
|
added: added ?? this.added,
|
||||||
originalFileName: originalFileName ?? this.originalFileName,
|
originalFileName: originalFileName ?? this.originalFileName,
|
||||||
archiveSerialNumber: archiveSerialNumber ?? this.archiveSerialNumber,
|
archiveSerialNumber: archiveSerialNumber != null
|
||||||
|
? archiveSerialNumber()
|
||||||
|
: this.archiveSerialNumber,
|
||||||
archivedFileName: archivedFileName ?? this.archivedFileName,
|
archivedFileName: archivedFileName ?? this.archivedFileName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ abstract class PaperlessDocumentsApi {
|
|||||||
Future<Uint8List> getPreview(int docId);
|
Future<Uint8List> getPreview(int docId);
|
||||||
String getThumbnailUrl(int docId);
|
String getThumbnailUrl(int docId);
|
||||||
Future<Uint8List> download(DocumentModel document, {bool original});
|
Future<Uint8List> download(DocumentModel document, {bool original});
|
||||||
|
Future<void> downloadToFile(
|
||||||
|
DocumentModel document,
|
||||||
|
String localFilePath, {
|
||||||
|
bool original = false,
|
||||||
|
void Function(double)? onProgressChanged,
|
||||||
|
});
|
||||||
Future<FieldSuggestions> findSuggestions(DocumentModel document);
|
Future<FieldSuggestions> findSuggestions(DocumentModel document);
|
||||||
|
|
||||||
Future<List<String>> autocomplete(String query, [int limit = 10]);
|
Future<List<String>> autocomplete(String query, [int limit = 10]);
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
|||||||
try {
|
try {
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
"/api/documents/${document.id}/download/",
|
"/api/documents/${document.id}/download/",
|
||||||
queryParameters: original ? {'original': true} : {},
|
queryParameters: {'original': original},
|
||||||
options: Options(responseType: ResponseType.bytes),
|
options: Options(responseType: ResponseType.bytes),
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -208,6 +208,27 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> downloadToFile(
|
||||||
|
DocumentModel document,
|
||||||
|
String localFilePath, {
|
||||||
|
bool original = false,
|
||||||
|
void Function(double)? onProgressChanged,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await client.download(
|
||||||
|
"/api/documents/${document.id}/download/",
|
||||||
|
localFilePath,
|
||||||
|
onReceiveProgress: (count, total) =>
|
||||||
|
onProgressChanged?.call(count / total),
|
||||||
|
queryParameters: {'original': original},
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} on DioError catch (err) {
|
||||||
|
throw err.error!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<DocumentMetaData> getMetaData(DocumentModel document) async {
|
Future<DocumentMetaData> getMetaData(DocumentModel document) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@@ -824,6 +824,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.3.0"
|
version: "3.3.0"
|
||||||
|
in_app_review:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: in_app_review
|
||||||
|
sha256: "16328b8202d36522322b95804ae5d975577aa9f584d634985849ba1099645850"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.6"
|
||||||
|
in_app_review_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: in_app_review_platform_interface
|
||||||
|
sha256: b12ec9aaf6b34d3a72aa95895eb252b381896246bdad4ef378d444affe8410ef
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.4"
|
||||||
integration_test:
|
integration_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ dependencies:
|
|||||||
flutter_displaymode: ^0.5.0
|
flutter_displaymode: ^0.5.0
|
||||||
dynamic_color: ^1.5.4
|
dynamic_color: ^1.5.4
|
||||||
flutter_html: ^3.0.0-alpha.6
|
flutter_html: ^3.0.0-alpha.6
|
||||||
|
in_app_review: ^2.0.6
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
integration_test:
|
integration_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user