mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-06 05:15:51 -06:00
feat: Implement updated receive share logic
This commit is contained in:
@@ -15,7 +15,6 @@ import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
class HiveBoxes {
|
||||
HiveBoxes._();
|
||||
static const globalSettings = 'globalSettings';
|
||||
static const authentication = 'authentication';
|
||||
static const localUserCredentials = 'localUserCredentials';
|
||||
static const localUserAccount = 'localUserAccount';
|
||||
static const localUserAppState = 'localUserAppState';
|
||||
|
||||
@@ -4,6 +4,11 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
|
||||
|
||||
///
|
||||
/// Opens an encrypted box, calls [callback] with the now opened box, awaits
|
||||
@@ -40,3 +45,16 @@ Future<Uint8List> _getEncryptedBoxKey() async {
|
||||
final key = (await secureStorage.read(key: 'key'))!;
|
||||
return base64Decode(key);
|
||||
}
|
||||
|
||||
extension HiveBoxAccessors on HiveInterface {
|
||||
Box<GlobalSettings> get settingsBox =>
|
||||
box<GlobalSettings>(HiveBoxes.globalSettings);
|
||||
Box<LocalUserAccount> get localUserAccountBox =>
|
||||
box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
Box<LocalUserAppState> get localUserAppStateBox =>
|
||||
box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||
Box<LocalUserSettings> get localUserSettingsBox =>
|
||||
box<LocalUserSettings>(HiveBoxes.localUserSettings);
|
||||
Box<GlobalSettings> get globalSettingsBox =>
|
||||
box<GlobalSettings>(HiveBoxes.globalSettings);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ class GlobalSettings with HiveObjectMixin {
|
||||
@HiveField(7, defaultValue: false)
|
||||
bool enforceSinglePagePdfUpload;
|
||||
|
||||
@HiveField(8, defaultValue: false)
|
||||
bool skipDocumentPreprarationOnUpload;
|
||||
|
||||
GlobalSettings({
|
||||
required this.preferredLocaleSubtag,
|
||||
this.preferredThemeMode = ThemeMode.system,
|
||||
@@ -41,5 +44,6 @@ class GlobalSettings with HiveObjectMixin {
|
||||
this.defaultDownloadType = FileDownloadType.alwaysAsk,
|
||||
this.defaultShareType = FileDownloadType.alwaysAsk,
|
||||
this.enforceSinglePagePdfUpload = false,
|
||||
this.skipDocumentPreprarationOnUpload = false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
const supportedFileExtensions = ['pdf', 'png', 'tiff', 'gif', 'jpg', 'jpeg'];
|
||||
const supportedFileExtensions = [
|
||||
'.pdf',
|
||||
'.png',
|
||||
'.tiff',
|
||||
'.gif',
|
||||
'.jpg',
|
||||
'.jpeg'
|
||||
];
|
||||
|
||||
@@ -3,9 +3,12 @@ import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class FileService {
|
||||
const FileService._();
|
||||
|
||||
static Future<File> saveToFile(
|
||||
Uint8List bytes,
|
||||
String filename,
|
||||
@@ -19,16 +22,13 @@ class FileService {
|
||||
}
|
||||
|
||||
static Future<Directory?> getDirectory(PaperlessDirectoryType type) {
|
||||
switch (type) {
|
||||
case PaperlessDirectoryType.documents:
|
||||
return documentsDirectory;
|
||||
case PaperlessDirectoryType.temporary:
|
||||
return temporaryDirectory;
|
||||
case PaperlessDirectoryType.scans:
|
||||
return temporaryScansDirectory;
|
||||
case PaperlessDirectoryType.download:
|
||||
return downloadsDirectory;
|
||||
}
|
||||
return switch (type) {
|
||||
PaperlessDirectoryType.documents => documentsDirectory,
|
||||
PaperlessDirectoryType.temporary => temporaryDirectory,
|
||||
PaperlessDirectoryType.scans => temporaryScansDirectory,
|
||||
PaperlessDirectoryType.download => downloadsDirectory,
|
||||
PaperlessDirectoryType.upload => uploadDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
static Future<File> allocateTemporaryFile(
|
||||
@@ -50,8 +50,8 @@ class FileService {
|
||||
))!
|
||||
.first;
|
||||
} else if (Platform.isIOS) {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final dir = Directory('${appDir.path}/documents');
|
||||
final dir = await getApplicationDocumentsDirectory()
|
||||
.then((dir) => Directory('${dir.path}/documents'));
|
||||
return dir.create(recursive: true);
|
||||
} else {
|
||||
throw UnsupportedError("Platform not supported.");
|
||||
@@ -77,17 +77,32 @@ class FileService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Directory> get uploadDirectory async {
|
||||
final dir = await getApplicationDocumentsDirectory()
|
||||
.then((dir) => Directory('${dir.path}/upload'));
|
||||
return dir.create(recursive: true);
|
||||
}
|
||||
|
||||
static Future<Directory> getConsumptionDirectory(
|
||||
{required String userId}) async {
|
||||
final uploadDir =
|
||||
await uploadDirectory.then((dir) => Directory('${dir.path}/$userId'));
|
||||
return uploadDir.create(recursive: true);
|
||||
}
|
||||
|
||||
static Future<Directory> get temporaryScansDirectory async {
|
||||
final tempDir = await temporaryDirectory;
|
||||
final scansDir = Directory('${tempDir.path}/scans');
|
||||
return scansDir.create(recursive: true);
|
||||
}
|
||||
|
||||
static Future<void> clearUserData() async {
|
||||
static Future<void> clearUserData({required String userId}) async {
|
||||
final scanDir = await temporaryScansDirectory;
|
||||
final tempDir = await temporaryDirectory;
|
||||
final consumptionDir = await getConsumptionDirectory(userId: userId);
|
||||
await scanDir.delete(recursive: true);
|
||||
await tempDir.delete(recursive: true);
|
||||
await consumptionDir.delete(recursive: true);
|
||||
}
|
||||
|
||||
static Future<void> clearDirectoryContent(PaperlessDirectoryType type) async {
|
||||
@@ -101,11 +116,20 @@ class FileService {
|
||||
dir.listSync().map((item) => item.delete(recursive: true)),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<File>> getAllFiles(Directory directory) {
|
||||
return directory.list().whereType<File>().toList();
|
||||
}
|
||||
|
||||
static Future<List<Directory>> getAllSubdirectories(Directory directory) {
|
||||
return directory.list().whereType<Directory>().toList();
|
||||
}
|
||||
}
|
||||
|
||||
enum PaperlessDirectoryType {
|
||||
documents,
|
||||
temporary,
|
||||
scans,
|
||||
download;
|
||||
download,
|
||||
upload;
|
||||
}
|
||||
|
||||
35
lib/core/widgets/future_or_builder.dart
Normal file
35
lib/core/widgets/future_or_builder.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FutureOrBuilder<T> extends StatelessWidget {
|
||||
final FutureOr<T>? futureOrValue;
|
||||
|
||||
final T? initialData;
|
||||
|
||||
final AsyncWidgetBuilder<T> builder;
|
||||
|
||||
const FutureOrBuilder({
|
||||
super.key,
|
||||
FutureOr<T>? future,
|
||||
this.initialData,
|
||||
required this.builder,
|
||||
}) : futureOrValue = future;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final futureOrValue = this.futureOrValue;
|
||||
if (futureOrValue is T) {
|
||||
return builder(
|
||||
context,
|
||||
AsyncSnapshot.withData(ConnectionState.done, futureOrValue),
|
||||
);
|
||||
} else {
|
||||
return FutureBuilder(
|
||||
future: futureOrValue,
|
||||
initialData: initialData,
|
||||
builder: builder,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:paperless_mobile/constants.dart';
|
||||
import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/upload_queue_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class AppDrawer extends StatelessWidget {
|
||||
@@ -16,6 +24,7 @@ class AppDrawer extends StatelessWidget {
|
||||
return SafeArea(
|
||||
child: Drawer(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
@@ -93,6 +102,29 @@ class AppDrawer extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
Consumer<ConsumptionChangeNotifier>(
|
||||
builder: (context, value, child) {
|
||||
final files = value.pendingFiles;
|
||||
final child = ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.drive_folder_upload_outlined),
|
||||
title: const Text("Upload Queue"),
|
||||
onTap: () {
|
||||
UploadQueueRoute().push(context);
|
||||
},
|
||||
trailing: Text(
|
||||
'${files.length}',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
if (files.isEmpty) {
|
||||
return child;
|
||||
}
|
||||
return child
|
||||
.animate(onPlay: (c) => c.repeat(reverse: true))
|
||||
.fade(duration: 1.seconds, begin: 1, end: 0.3);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
@@ -101,12 +133,57 @@ class AppDrawer extends StatelessWidget {
|
||||
),
|
||||
onTap: () => SettingsRoute().push(context),
|
||||
),
|
||||
const Divider(),
|
||||
Text(
|
||||
S.of(context)!.views,
|
||||
textAlign: TextAlign.left,
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
).padded(16),
|
||||
_buildSavedViews(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSavedViews() {
|
||||
return BlocBuilder<SavedViewCubit, SavedViewState>(
|
||||
builder: (context, state) {
|
||||
return state.when(
|
||||
initial: () => const SizedBox.shrink(),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
loaded: (savedViews) {
|
||||
final sidebarViews = savedViews.values
|
||||
.where((element) => element.showInSidebar)
|
||||
.toList();
|
||||
if (sidebarViews.isEmpty) {
|
||||
return Text("Nothing to show here.").paddedOnly(left: 16);
|
||||
}
|
||||
return Expanded(
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final view = sidebarViews[index];
|
||||
return ListTile(
|
||||
title: Text(view.name),
|
||||
trailing: Icon(Icons.arrow_forward),
|
||||
onTap: () {
|
||||
Scaffold.of(context).closeDrawer();
|
||||
context
|
||||
.read<DocumentsCubit>()
|
||||
.updateFilter(filter: view.toDocumentFilter());
|
||||
DocumentsRoute().go(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
itemCount: sidebarViews.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
error: () => Text(S.of(context)!.couldNotLoadSavedViews),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _showAboutDialog(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
@@ -265,7 +265,7 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
// For paperless version older than 1.11.3, task id will always be null!
|
||||
context.read<DocumentScannerCubit>().reset();
|
||||
context
|
||||
.read<TaskStatusCubit>()
|
||||
.read<PendingTasksNotifier>()
|
||||
.listenToTaskChanges(uploadResult!.taskId!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -13,6 +14,7 @@ import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/widgets/future_or_builder.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
|
||||
import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart';
|
||||
@@ -20,6 +22,7 @@ import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type
|
||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
|
||||
import 'package:paperless_mobile/features/sharing/view/widgets/file_thumbnail.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
@@ -33,7 +36,7 @@ class DocumentUploadResult {
|
||||
}
|
||||
|
||||
class DocumentUploadPreparationPage extends StatefulWidget {
|
||||
final Uint8List fileBytes;
|
||||
final FutureOr<Uint8List> fileBytes;
|
||||
final String? title;
|
||||
final String? filename;
|
||||
final String? fileExtension;
|
||||
@@ -68,9 +71,10 @@ class _DocumentUploadPreparationPageState
|
||||
void initState() {
|
||||
super.initState();
|
||||
_syncTitleAndFilename = widget.filename == null && widget.title == null;
|
||||
_titleColor = _computeAverageColor().computeLuminance() > 0.5
|
||||
? Colors.black
|
||||
: Colors.white;
|
||||
_computeAverageColor().then((value) {
|
||||
_titleColor =
|
||||
value.computeLuminance() > 0.5 ? Colors.black : Colors.white;
|
||||
});
|
||||
initializeDateFormatting();
|
||||
}
|
||||
|
||||
@@ -104,9 +108,17 @@ class _DocumentUploadPreparationPageState
|
||||
pinned: true,
|
||||
expandedHeight: 150,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Image.memory(
|
||||
widget.fileBytes,
|
||||
fit: BoxFit.cover,
|
||||
background: FutureOrBuilder<Uint8List>(
|
||||
future: widget.fileBytes,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return FileThumbnail(
|
||||
bytes: snapshot.data!,
|
||||
fit: BoxFit.fitWidth,
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
S.of(context)!.prepareDocument,
|
||||
@@ -116,7 +128,7 @@ class _DocumentUploadPreparationPageState
|
||||
),
|
||||
),
|
||||
bottom: _isUploadLoading
|
||||
? const PreferredSize(
|
||||
? PreferredSize(
|
||||
child: LinearProgressIndicator(),
|
||||
preferredSize: Size.fromHeight(4.0),
|
||||
)
|
||||
@@ -359,7 +371,7 @@ class _DocumentUploadPreparationPageState
|
||||
?.whenOrNull(fromId: (id) => id);
|
||||
final asn = fv[DocumentModel.asnKey] as int?;
|
||||
final taskId = await cubit.upload(
|
||||
widget.fileBytes,
|
||||
await widget.fileBytes,
|
||||
filename: _padWithExtension(
|
||||
_formKey.currentState?.value[fkFileName],
|
||||
widget.fileExtension,
|
||||
@@ -404,8 +416,8 @@ class _DocumentUploadPreparationPageState
|
||||
return source.replaceAll(RegExp(r"[\W_]"), "_").toLowerCase();
|
||||
}
|
||||
|
||||
Color _computeAverageColor() {
|
||||
final bitmap = img.decodeImage(widget.fileBytes);
|
||||
Future<Color> _computeAverageColor() async {
|
||||
final bitmap = img.decodeImage(await widget.fileBytes);
|
||||
if (bitmap == null) {
|
||||
return Colors.black;
|
||||
}
|
||||
|
||||
@@ -59,18 +59,48 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<PendingTasksNotifier>().addListener(_onTasksChanged);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_nestedScrollViewKey.currentState!.innerController
|
||||
.addListener(_scrollExtentChangedListener);
|
||||
});
|
||||
}
|
||||
|
||||
void _onTasksChanged() {
|
||||
final notifier = context.read<PendingTasksNotifier>();
|
||||
final tasks = notifier.value;
|
||||
final finishedTasks = tasks.values.where((element) => element.isSuccess);
|
||||
if (finishedTasks.isNotEmpty) {
|
||||
showSnackBar(
|
||||
context,
|
||||
S.of(context)!.newDocumentAvailable,
|
||||
action: SnackBarActionConfig(
|
||||
label: S.of(context)!.reload,
|
||||
onPressed: () {
|
||||
// finishedTasks.forEach((task) {
|
||||
// notifier.acknowledgeTasks([finishedTasks]);
|
||||
// });
|
||||
context.read<DocumentsCubit>().reload();
|
||||
},
|
||||
),
|
||||
duration: const Duration(seconds: 10),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reloadData() async {
|
||||
final user = context.read<LocalUserAccount>().paperlessUser;
|
||||
try {
|
||||
await Future.wait([
|
||||
context.read<DocumentsCubit>().reload(),
|
||||
context.read<SavedViewCubit>().reload(),
|
||||
context.read<LabelCubit>().reload(),
|
||||
if (user.canViewSavedViews) context.read<SavedViewCubit>().reload(),
|
||||
if (user.canViewTags) context.read<LabelCubit>().reloadTags(),
|
||||
if (user.canViewCorrespondents)
|
||||
context.read<LabelCubit>().reloadCorrespondents(),
|
||||
if (user.canViewDocumentTypes)
|
||||
context.read<LabelCubit>().reloadDocumentTypes(),
|
||||
if (user.canViewStoragePaths)
|
||||
context.read<LabelCubit>().reloadStoragePaths(),
|
||||
]);
|
||||
} catch (error, stackTrace) {
|
||||
showGenericError(context, error, stackTrace);
|
||||
@@ -96,196 +126,174 @@ class _DocumentsPageState extends State<DocumentsPage> {
|
||||
void dispose() {
|
||||
_nestedScrollViewKey.currentState?.innerController
|
||||
.removeListener(_scrollExtentChangedListener);
|
||||
context.read<PendingTasksNotifier>().removeListener(_onTasksChanged);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<TaskStatusCubit, TaskStatusState>(
|
||||
return BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||
listenWhen: (previous, current) =>
|
||||
!previous.isSuccess && current.isSuccess,
|
||||
previous != ConnectivityState.connected &&
|
||||
current == ConnectivityState.connected,
|
||||
listener: (context, state) {
|
||||
showSnackBar(
|
||||
context,
|
||||
S.of(context)!.newDocumentAvailable,
|
||||
action: SnackBarActionConfig(
|
||||
label: S.of(context)!.reload,
|
||||
onPressed: () {
|
||||
context.read<TaskStatusCubit>().acknowledgeCurrentTask();
|
||||
context.read<DocumentsCubit>().reload();
|
||||
},
|
||||
),
|
||||
duration: const Duration(seconds: 10),
|
||||
);
|
||||
_reloadData();
|
||||
},
|
||||
child: BlocConsumer<ConnectivityCubit, ConnectivityState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous != ConnectivityState.connected &&
|
||||
current == ConnectivityState.connected,
|
||||
listener: (context, state) {
|
||||
_reloadData();
|
||||
},
|
||||
builder: (context, connectivityState) {
|
||||
return SafeArea(
|
||||
top: true,
|
||||
child: Scaffold(
|
||||
drawer: const AppDrawer(),
|
||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
final show = state.selection.isEmpty;
|
||||
final canReset = state.filter.appliedFiltersCount > 0;
|
||||
if (show) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
DeferredPointerHandler(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
FloatingActionButton.extended(
|
||||
extendedPadding: _showExtendedFab
|
||||
? null
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 16),
|
||||
heroTag: "fab_documents_page_filter",
|
||||
label: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axis: Axis.horizontal,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: _showExtendedFab
|
||||
? Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.filter_alt_outlined,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
S.of(context)!.filterDocuments,
|
||||
),
|
||||
],
|
||||
)
|
||||
: const Icon(Icons.filter_alt_outlined),
|
||||
),
|
||||
onPressed: _openDocumentFilter,
|
||||
builder: (context, connectivityState) {
|
||||
return SafeArea(
|
||||
top: true,
|
||||
child: Scaffold(
|
||||
drawer: const AppDrawer(),
|
||||
floatingActionButton: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
final show = state.selection.isEmpty;
|
||||
final canReset = state.filter.appliedFiltersCount > 0;
|
||||
if (show) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
DeferredPointerHandler(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
FloatingActionButton.extended(
|
||||
extendedPadding: _showExtendedFab
|
||||
? null
|
||||
: const EdgeInsets.symmetric(horizontal: 16),
|
||||
heroTag: "fab_documents_page_filter",
|
||||
label: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axis: Axis.horizontal,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: _showExtendedFab
|
||||
? Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.filter_alt_outlined,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
S.of(context)!.filterDocuments,
|
||||
),
|
||||
],
|
||||
)
|
||||
: const Icon(Icons.filter_alt_outlined),
|
||||
),
|
||||
if (canReset)
|
||||
Positioned(
|
||||
top: -20,
|
||||
right: -8,
|
||||
child: DeferPointer(
|
||||
paintOnTop: true,
|
||||
child: Material(
|
||||
color:
|
||||
Theme.of(context).colorScheme.error,
|
||||
onPressed: _openDocumentFilter,
|
||||
),
|
||||
if (canReset)
|
||||
Positioned(
|
||||
top: -20,
|
||||
right: -8,
|
||||
child: DeferPointer(
|
||||
paintOnTop: true,
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
_onResetFilter();
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (_showExtendedFab)
|
||||
Text(
|
||||
"Reset (${state.filter.appliedFiltersCount})",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onError,
|
||||
),
|
||||
).padded()
|
||||
else
|
||||
Icon(
|
||||
Icons.replay,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onError,
|
||||
).padded(4),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
HapticFeedback.mediumImpact();
|
||||
_onResetFilter();
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (_showExtendedFab)
|
||||
Text(
|
||||
"Reset (${state.filter.appliedFiltersCount})",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onError,
|
||||
),
|
||||
).padded()
|
||||
else
|
||||
Icon(
|
||||
Icons.replay,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onError,
|
||||
).padded(4),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (context
|
||||
.read<DocumentsCubit>()
|
||||
.state
|
||||
.selection
|
||||
.isNotEmpty) {
|
||||
context.read<DocumentsCubit>().resetSelection();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: NestedScrollView(
|
||||
key: _nestedScrollViewKey,
|
||||
floatHeaderSlivers: true,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: searchBarHandle,
|
||||
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.selection.isEmpty) {
|
||||
return SliverSearchBar(
|
||||
floating: true,
|
||||
titleText: S.of(context)!.documents,
|
||||
);
|
||||
} else {
|
||||
return DocumentSelectionSliverAppBar(
|
||||
state: state,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
SliverOverlapAbsorber(
|
||||
handle: savedViewsHandle,
|
||||
sliver: SliverPinnedHeader(
|
||||
child: Material(
|
||||
child: _buildViewActions(),
|
||||
elevation: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: WillPopScope(
|
||||
onWillPop: () async {
|
||||
if (context.read<DocumentsCubit>().state.selection.isNotEmpty) {
|
||||
context.read<DocumentsCubit>().resetSelection();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: NestedScrollView(
|
||||
key: _nestedScrollViewKey,
|
||||
floatHeaderSlivers: true,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverOverlapAbsorber(
|
||||
handle: searchBarHandle,
|
||||
sliver: BlocBuilder<DocumentsCubit, DocumentsState>(
|
||||
builder: (context, state) {
|
||||
if (state.selection.isEmpty) {
|
||||
return SliverSearchBar(
|
||||
floating: true,
|
||||
titleText: S.of(context)!.documents,
|
||||
);
|
||||
} else {
|
||||
return DocumentSelectionSliverAppBar(
|
||||
state: state,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
body: _buildDocumentsTab(
|
||||
connectivityState,
|
||||
context,
|
||||
),
|
||||
SliverOverlapAbsorber(
|
||||
handle: savedViewsHandle,
|
||||
sliver: SliverPinnedHeader(
|
||||
child: Material(
|
||||
child: _buildViewActions(),
|
||||
elevation: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: _buildDocumentsTab(
|
||||
connectivityState,
|
||||
context,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ class _SavedViewsWidgetState extends State<SavedViewsWidget>
|
||||
.maybeMap(
|
||||
loaded: (value) {
|
||||
if (value.savedViews.isEmpty) {
|
||||
return Text(S.of(context)!.noItemsFound)
|
||||
return Text(S.of(context)!.youDidNotSaveAnyViewsYet)
|
||||
.paddedOnly(left: 16);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ class HomeShellWidget extends StatelessWidget {
|
||||
builder: (context, settings) {
|
||||
final currentUserId = settings.loggedInUserId;
|
||||
if (currentUserId == null) {
|
||||
// This is the case when the current user logs out of the app.
|
||||
return SizedBox.shrink();
|
||||
// This is currently the case (only for a few ms) when the current user logs out of the app.
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final apiVersion = ApiVersion(paperlessApiVersion);
|
||||
return ValueListenableBuilder(
|
||||
@@ -57,8 +57,6 @@ class HomeShellWidget extends StatelessWidget {
|
||||
.listenable(keys: [currentUserId]),
|
||||
builder: (context, box, _) {
|
||||
final currentLocalUser = box.get(currentUserId)!;
|
||||
print(currentLocalUser.paperlessUser.canViewDocuments);
|
||||
print(currentLocalUser.paperlessUser.canViewTags);
|
||||
return MultiProvider(
|
||||
key: ValueKey(currentUserId),
|
||||
providers: [
|
||||
@@ -195,8 +193,8 @@ class HomeShellWidget extends StatelessWidget {
|
||||
context.read(),
|
||||
),
|
||||
),
|
||||
Provider(
|
||||
create: (context) => TaskStatusCubit(
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => PendingTasksNotifier(
|
||||
context.read(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -17,6 +17,7 @@ import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
|
||||
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
|
||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||
@@ -159,7 +160,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
final userAppStateBox =
|
||||
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||
|
||||
await FileService.clearUserData(userId: userId);
|
||||
await userAccountBox.delete(userId);
|
||||
await userAppStateBox.delete(userId);
|
||||
await withEncryptedBox<UserCredentials, void>(
|
||||
|
||||
@@ -133,7 +133,6 @@ class LocalNotificationService {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
//TODO: INTL
|
||||
Future<void> notifyTaskChanged(Task task) {
|
||||
log("[LocalNotificationService] notifyTaskChanged: ${task.toString()}");
|
||||
@@ -158,7 +157,7 @@ class LocalNotificationService {
|
||||
break;
|
||||
case TaskStatus.failure:
|
||||
title = "Failed to process document";
|
||||
body = "Document ${task.taskFileName} was rejected by the server.";
|
||||
body = task.result ?? 'Rejected by the server.';
|
||||
timestampMillis = task.dateCreated.millisecondsSinceEpoch;
|
||||
break;
|
||||
case TaskStatus.success:
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
|
||||
part 'saved_view_preview_state.dart';
|
||||
part 'saved_view_preview_cubit.freezed.dart';
|
||||
|
||||
class SavedViewPreviewCubit extends Cubit<SavedViewPreviewState> {
|
||||
final PaperlessDocumentsApi _api;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:paperless_mobile/features/settings/view/widgets/default_download
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/default_share_file_type_setting.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/enforce_pdf_upload_setting.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/language_selection_setting.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/skip_document_prepraration_on_share_setting.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/theme_mode_setting.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/user_settings_builder.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
@@ -33,6 +34,7 @@ class SettingsPage extends StatelessWidget {
|
||||
const DefaultDownloadFileTypeSetting(),
|
||||
const DefaultShareFileTypeSetting(),
|
||||
const EnforcePdfUploadSetting(),
|
||||
const SkipDocumentPreprationOnShareSetting(),
|
||||
_buildSectionHeader(context, S.of(context)!.storage),
|
||||
const ClearCacheSetting(),
|
||||
],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:paperless_mobile/constants.dart';
|
||||
import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.dart';
|
||||
import 'package:paperless_mobile/core/widgets/hint_card.dart';
|
||||
@@ -9,7 +8,6 @@ import 'package:paperless_mobile/features/settings/model/color_scheme_option.dar
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.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/theme.dart';
|
||||
|
||||
class ColorSchemeOptionSetting extends StatelessWidget {
|
||||
const ColorSchemeOptionSetting({super.key});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||
|
||||
class SkipDocumentPreprationOnShareSetting extends StatelessWidget {
|
||||
const SkipDocumentPreprationOnShareSetting({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GlobalSettingsBuilder(
|
||||
builder: (context, settings) {
|
||||
return SwitchListTile(
|
||||
title: Text("Direct share"),
|
||||
subtitle:
|
||||
Text("Always directly upload when sharing files with the app."),
|
||||
value: settings.skipDocumentPreprarationOnUpload,
|
||||
onChanged: (value) {
|
||||
settings.skipDocumentPreprarationOnUpload = value;
|
||||
settings.save();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
72
lib/features/sharing/cubit/receive_share_cubit.dart
Normal file
72
lib/features/sharing/cubit/receive_share_cubit.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
part 'receive_share_state.dart';
|
||||
|
||||
class ConsumptionChangeNotifier extends ChangeNotifier {
|
||||
List<File> pendingFiles = [];
|
||||
|
||||
ConsumptionChangeNotifier();
|
||||
|
||||
Future<void> loadFromConsumptionDirectory({required String userId}) async {
|
||||
pendingFiles = await _getCurrentFiles(userId);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Creates a local copy of all shared files and reloads all files
|
||||
/// from the user's consumption directory.
|
||||
Future<void> addFiles({
|
||||
required List<File> files,
|
||||
required String userId,
|
||||
}) async {
|
||||
if (files.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final consumptionDirectory =
|
||||
await FileService.getConsumptionDirectory(userId: userId);
|
||||
for (final file in files) {
|
||||
File localFile;
|
||||
if (file.path.startsWith(consumptionDirectory.path)) {
|
||||
localFile = file;
|
||||
} else {
|
||||
final fileName = p.basename(file.path);
|
||||
localFile = File(p.join(consumptionDirectory.path, fileName));
|
||||
await file.copy(localFile.path);
|
||||
}
|
||||
}
|
||||
return loadFromConsumptionDirectory(userId: userId);
|
||||
}
|
||||
|
||||
/// Marks a file as processed by removing it from the queue and deleting the local copy of the file.
|
||||
Future<void> discardFile(
|
||||
File file, {
|
||||
required String userId,
|
||||
}) async {
|
||||
final consumptionDirectory =
|
||||
await FileService.getConsumptionDirectory(userId: userId);
|
||||
if (file.path.startsWith(consumptionDirectory.path)) {
|
||||
await file.delete();
|
||||
}
|
||||
return loadFromConsumptionDirectory(userId: userId);
|
||||
}
|
||||
|
||||
/// Returns the next file to process of null if no file exists.
|
||||
Future<File?> getNextFile({required String userId}) async {
|
||||
final files = await _getCurrentFiles(userId);
|
||||
if (files.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return files.first;
|
||||
}
|
||||
|
||||
Future<List<File>> _getCurrentFiles(String userId) async {
|
||||
final directory = await FileService.getConsumptionDirectory(userId: userId);
|
||||
final files = await FileService.getAllFiles(directory);
|
||||
return files;
|
||||
}
|
||||
}
|
||||
32
lib/features/sharing/cubit/receive_share_state.dart
Normal file
32
lib/features/sharing/cubit/receive_share_state.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
part of 'receive_share_cubit.dart';
|
||||
|
||||
sealed class ReceiveShareState {
|
||||
final List<File> files;
|
||||
|
||||
const ReceiveShareState({this.files = const []});
|
||||
}
|
||||
|
||||
class ReceiveShareStateInitial extends ReceiveShareState {
|
||||
const ReceiveShareStateInitial();
|
||||
}
|
||||
|
||||
class ReceiveShareStateLoading extends ReceiveShareState {
|
||||
const ReceiveShareStateLoading();
|
||||
}
|
||||
|
||||
class ReceiveShareStateLoaded extends ReceiveShareState {
|
||||
const ReceiveShareStateLoaded({super.files});
|
||||
|
||||
ReceiveShareStateLoaded copyWith({
|
||||
List<File>? files,
|
||||
}) {
|
||||
return ReceiveShareStateLoaded(
|
||||
files: files ?? this.files,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReceiveShareStateError extends ReceiveShareState {
|
||||
final String message;
|
||||
const ReceiveShareStateError(this.message);
|
||||
}
|
||||
73
lib/features/sharing/logic/upload_queue_processor.dart
Normal file
73
lib/features/sharing/logic/upload_queue_processor.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/global/constants.dart';
|
||||
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
|
||||
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
|
||||
import 'package:paperless_mobile/features/sharing/model/share_intent_queue.dart';
|
||||
import 'package:paperless_mobile/features/sharing/view/dialog/discard_shared_file_dialog.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class UploadQueueProcessor {
|
||||
final ShareIntentQueue queue;
|
||||
|
||||
UploadQueueProcessor({required this.queue});
|
||||
|
||||
bool _isFileTypeSupported(File file) {
|
||||
final isSupported =
|
||||
supportedFileExtensions.contains(p.extension(file.path));
|
||||
return isSupported;
|
||||
}
|
||||
|
||||
void processIncomingFiles(
|
||||
BuildContext context, {
|
||||
required List<SharedMediaFile> sharedFiles,
|
||||
}) async {
|
||||
if (sharedFiles.isEmpty) {
|
||||
return;
|
||||
}
|
||||
Iterable<File> files = sharedFiles.map((file) => File(file.path));
|
||||
if (Platform.isIOS) {
|
||||
files = files
|
||||
.map((file) => File(file.path.replaceAll('file://', '')))
|
||||
.toList();
|
||||
}
|
||||
final supportedFiles = files.where(_isFileTypeSupported);
|
||||
final unsupportedFiles = files.whereNot(_isFileTypeSupported);
|
||||
debugPrint(
|
||||
"Received ${files.length} files, out of which ${supportedFiles.length} are supported.}");
|
||||
if (supportedFiles.isEmpty) {
|
||||
Fluttertoast.showToast(
|
||||
msg: translateError(
|
||||
context,
|
||||
ErrorCode.unsupportedFileFormat,
|
||||
),
|
||||
);
|
||||
if (Platform.isAndroid) {
|
||||
// As stated in the docs, SystemNavigator.pop() is ignored on IOS to comply with HCI guidelines.
|
||||
await SystemNavigator.pop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (unsupportedFiles.isNotEmpty) {
|
||||
//TODO: INTL
|
||||
Fluttertoast.showToast(
|
||||
msg:
|
||||
"${unsupportedFiles.length}/${files.length} files could not be processed.");
|
||||
}
|
||||
await ShareIntentQueue.instance.addAll(
|
||||
supportedFiles,
|
||||
userId: context.read<LocalUserAccount>().id,
|
||||
);
|
||||
}
|
||||
}
|
||||
105
lib/features/sharing/model/share_intent_queue.dart
Normal file
105
lib/features/sharing/model/share_intent_queue.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class ShareIntentQueue extends ChangeNotifier {
|
||||
final Map<String, Queue<File>> _queues = {};
|
||||
|
||||
ShareIntentQueue._();
|
||||
|
||||
static final instance = ShareIntentQueue._();
|
||||
|
||||
Future<void> initialize() async {
|
||||
final users = Hive.localUserAccountBox.values;
|
||||
for (final user in users) {
|
||||
final userId = user.id;
|
||||
debugPrint("Locating remaining files to be uploaded for $userId...");
|
||||
final consumptionDir =
|
||||
await FileService.getConsumptionDirectory(userId: userId);
|
||||
final files = await FileService.getAllFiles(consumptionDir);
|
||||
debugPrint(
|
||||
"Found ${files.length} files to be uploaded for $userId. Adding to queue...");
|
||||
getQueue(userId).addAll(files);
|
||||
}
|
||||
}
|
||||
|
||||
void add(
|
||||
File file, {
|
||||
required String userId,
|
||||
}) =>
|
||||
addAll([file], userId: userId);
|
||||
|
||||
Future<void> addAll(
|
||||
Iterable<File> files, {
|
||||
required String userId,
|
||||
}) async {
|
||||
if (files.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final consumptionDirectory =
|
||||
await FileService.getConsumptionDirectory(userId: userId);
|
||||
final copiedFiles = await Future.wait([
|
||||
for (var file in files)
|
||||
file.copy('${consumptionDirectory.path}/${p.basename(file.path)}')
|
||||
]);
|
||||
|
||||
debugPrint(
|
||||
"Adding received files to queue: ${files.map((e) => e.path).join(",")}",
|
||||
);
|
||||
getQueue(userId).addAll(copiedFiles);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Removes and returns the first item in the requested user's queue if it exists.
|
||||
File? pop(String userId) {
|
||||
if (hasUnhandledFiles(userId: userId)) {
|
||||
final file = getQueue(userId).removeFirst();
|
||||
notifyListeners();
|
||||
return file;
|
||||
// Don't notify listeners, only when new item is added.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> onConsumed(File file) {
|
||||
debugPrint(
|
||||
"File ${file.path} successfully consumed. Delelting local copy.");
|
||||
return file.delete();
|
||||
}
|
||||
|
||||
Future<void> discard(File file) {
|
||||
debugPrint("Discarding file ${file.path}.");
|
||||
return file.delete();
|
||||
}
|
||||
|
||||
/// Returns whether the queue of the requested user contains files waiting for processing.
|
||||
bool hasUnhandledFiles({
|
||||
required String userId,
|
||||
}) =>
|
||||
getQueue(userId).isNotEmpty;
|
||||
|
||||
int unhandledFileCount({
|
||||
required String userId,
|
||||
}) =>
|
||||
getQueue(userId).length;
|
||||
|
||||
Queue<File> getQueue(String userId) {
|
||||
if (!_queues.containsKey(userId)) {
|
||||
_queues[userId] = Queue<File>();
|
||||
}
|
||||
return _queues[userId]!;
|
||||
}
|
||||
}
|
||||
|
||||
class UserAwareShareMediaFile {
|
||||
final String userId;
|
||||
final SharedMediaFile sharedFile;
|
||||
|
||||
UserAwareShareMediaFile(this.userId, this.sharedFile);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
|
||||
class ShareIntentQueue extends ChangeNotifier {
|
||||
final Map<String, Queue<SharedMediaFile>> _queues = {};
|
||||
|
||||
ShareIntentQueue._();
|
||||
|
||||
static final instance = ShareIntentQueue._();
|
||||
|
||||
void add(
|
||||
SharedMediaFile file, {
|
||||
required String userId,
|
||||
}) {
|
||||
debugPrint("Adding received file to queue: ${file.path}");
|
||||
_getQueue(userId).add(file);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void addAll(
|
||||
Iterable<SharedMediaFile> files, {
|
||||
required String userId,
|
||||
}) {
|
||||
debugPrint(
|
||||
"Adding received files to queue: ${files.map((e) => e.path).join(",")}");
|
||||
_getQueue(userId).addAll(files);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
SharedMediaFile? pop(String userId) {
|
||||
if (userHasUnhandlesFiles(userId)) {
|
||||
return _getQueue(userId).removeFirst();
|
||||
// Don't notify listeners, only when new item is added.
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Queue<SharedMediaFile> _getQueue(String userId) {
|
||||
if (!_queues.containsKey(userId)) {
|
||||
_queues[userId] = Queue<SharedMediaFile>();
|
||||
}
|
||||
return _queues[userId]!;
|
||||
}
|
||||
|
||||
bool userHasUnhandlesFiles(String userId) => _getQueue(userId).isNotEmpty;
|
||||
}
|
||||
|
||||
class UserAwareShareMediaFile {
|
||||
final String userId;
|
||||
final SharedMediaFile sharedFile;
|
||||
|
||||
UserAwareShareMediaFile(this.userId, this.sharedFile);
|
||||
}
|
||||
110
lib/features/sharing/view/consumption_queue_view.dart
Normal file
110
lib/features/sharing/view/consumption_queue_view.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
||||
import 'package:paperless_mobile/features/sharing/view/widgets/file_thumbnail.dart';
|
||||
import 'package:paperless_mobile/features/sharing/view/widgets/upload_queue_shell.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ConsumptionQueueView extends StatelessWidget {
|
||||
const ConsumptionQueueView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentUser = context.watch<LocalUserAccount>();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Upload Queue"), //TODO: INTL
|
||||
),
|
||||
body: Consumer<ConsumptionChangeNotifier>(
|
||||
builder: (context, value, child) {
|
||||
if (value.pendingFiles.isEmpty) {
|
||||
return Center(
|
||||
child: Text("No pending files."),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final file = value.pendingFiles.elementAt(index);
|
||||
final filename = p.basename(file.path);
|
||||
return ListTile(
|
||||
title: Text(filename),
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: FileThumbnail(
|
||||
file: file,
|
||||
fit: BoxFit.cover,
|
||||
width: 75,
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
context
|
||||
.read<ConsumptionChangeNotifier>()
|
||||
.discardFile(file, userId: currentUser.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(filename, maxLines: 1),
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
ActionChip(
|
||||
label: Text(S.of(context)!.upload),
|
||||
avatar: Icon(Icons.file_upload_outlined),
|
||||
onPressed: () {
|
||||
consumeLocalFile(
|
||||
context,
|
||||
file: file,
|
||||
userId: currentUser.id,
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
ActionChip(
|
||||
label: Text(S.of(context)!.discard),
|
||||
avatar: Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
context
|
||||
.read<ConsumptionChangeNotifier>()
|
||||
.discardFile(
|
||||
file,
|
||||
userId: currentUser.id,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).padded(),
|
||||
),
|
||||
],
|
||||
).padded();
|
||||
},
|
||||
itemCount: value.pendingFiles.length,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart';
|
||||
import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_confirm_button.dart';
|
||||
import 'package:paperless_mobile/core/widgets/future_or_builder.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
|
||||
class DiscardSharedFileDialog extends StatelessWidget {
|
||||
final FutureOr<Uint8List> bytes;
|
||||
const DiscardSharedFileDialog({
|
||||
super.key,
|
||||
required this.bytes,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
icon: FutureOrBuilder<Uint8List>(
|
||||
future: bytes,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
return LimitedBox(
|
||||
maxHeight: 200,
|
||||
maxWidth: 200,
|
||||
child: FadeInImage(
|
||||
fit: BoxFit.contain,
|
||||
placeholder: MemoryImage(kTransparentImage),
|
||||
image: MemoryImage(snapshot.data!),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(S.of(context)!.discardFile),
|
||||
content: Text(
|
||||
"The shared file was not yet processed. Do you want to discrad the file?", //TODO: INTL
|
||||
),
|
||||
actions: [
|
||||
DialogCancelButton(),
|
||||
DialogConfirmButton(
|
||||
label: S.of(context)!.discard,
|
||||
style: DialogConfirmButtonStyle.danger,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
102
lib/features/sharing/view/widgets/file_thumbnail.dart
Normal file
102
lib/features/sharing/view/widgets/file_thumbnail.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:mime/mime.dart' as mime;
|
||||
import 'package:printing/printing.dart';
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
|
||||
class FileThumbnail extends StatefulWidget {
|
||||
final File? file;
|
||||
final Uint8List? bytes;
|
||||
|
||||
final BoxFit? fit;
|
||||
final double? width;
|
||||
final double? height;
|
||||
const FileThumbnail({
|
||||
super.key,
|
||||
this.file,
|
||||
this.bytes,
|
||||
this.fit,
|
||||
this.width,
|
||||
this.height,
|
||||
}) : assert((bytes != null) != (file != null));
|
||||
|
||||
@override
|
||||
State<FileThumbnail> createState() => _FileThumbnailState();
|
||||
}
|
||||
|
||||
class _FileThumbnailState extends State<FileThumbnail> {
|
||||
late String? mimeType;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
mimeType = widget.file != null
|
||||
? mime.lookupMimeType(widget.file!.path)
|
||||
: mime.lookupMimeType('', headerBytes: widget.bytes);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return switch (mimeType) {
|
||||
"application/pdf" => SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: Center(
|
||||
child: FutureBuilder<Uint8List?>(
|
||||
future: widget.file?.readAsBytes().then(_convertPdfToPng) ??
|
||||
_convertPdfToPng(widget.bytes!),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return ColoredBox(
|
||||
color: Colors.white,
|
||||
child: Image.memory(
|
||||
snapshot.data!,
|
||||
alignment: Alignment.topCenter,
|
||||
fit: widget.fit,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
"image/png" ||
|
||||
"image/jpeg" ||
|
||||
"image/tiff" ||
|
||||
"image/gif" ||
|
||||
"image/webp" =>
|
||||
widget.file != null
|
||||
? Image.file(
|
||||
widget.file!,
|
||||
fit: widget.fit,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
)
|
||||
: Image.memory(
|
||||
widget.bytes!,
|
||||
fit: widget.fit,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
),
|
||||
"text/plain" => const Center(
|
||||
child: Text(".txt"),
|
||||
),
|
||||
_ => const Icon(Icons.file_present_outlined),
|
||||
};
|
||||
}
|
||||
|
||||
// send pdfFile as params
|
||||
Future<Uint8List?> _convertPdfToPng(Uint8List bytes) async {
|
||||
final info = await Printing.info();
|
||||
if (!info.canRaster) {
|
||||
return kTransparentImage;
|
||||
}
|
||||
final raster = await Printing.raster(bytes, pages: [0], dpi: 72).first;
|
||||
return raster.toPng();
|
||||
}
|
||||
}
|
||||
195
lib/features/sharing/view/widgets/upload_queue_shell.dart
Normal file
195
lib/features/sharing/view/widgets/upload_queue_shell.dart
Normal file
@@ -0,0 +1,195 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
|
||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
||||
import 'package:paperless_mobile/features/sharing/view/dialog/discard_shared_file_dialog.dart';
|
||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/helpers/message_helpers.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/upload_queue_route.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
|
||||
class UploadQueueShell extends StatefulWidget {
|
||||
final Widget child;
|
||||
const UploadQueueShell({super.key, required this.child});
|
||||
|
||||
@override
|
||||
State<UploadQueueShell> createState() => _UploadQueueShellState();
|
||||
}
|
||||
|
||||
class _UploadQueueShellState extends State<UploadQueueShell> {
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ReceiveSharingIntent.getInitialMedia().then(_onReceiveSharedFiles);
|
||||
_subscription =
|
||||
ReceiveSharingIntent.getMediaStream().listen(_onReceiveSharedFiles);
|
||||
|
||||
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
// context.read<ReceiveShareCubit>().loadFromConsumptionDirectory(
|
||||
// userId: context.read<LocalUserAccount>().id,
|
||||
// );
|
||||
// final state = context.read<ReceiveShareCubit>().state;
|
||||
// print("Current state is " + state.toString());
|
||||
// final files = state.files;
|
||||
// if (files.isNotEmpty) {
|
||||
// showSnackBar(
|
||||
// context,
|
||||
// "You have ${files.length} shared files waiting to be uploaded.",
|
||||
// action: SnackBarActionConfig(
|
||||
// label: "Show me",
|
||||
// onPressed: () {
|
||||
// UploadQueueRoute().push(context);
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// // showDialog(
|
||||
// // context: context,
|
||||
// // builder: (context) => AlertDialog(
|
||||
// // title: Text("Pending files"),
|
||||
// // content: Text(
|
||||
// // "You have ${files.length} files waiting to be uploaded.",
|
||||
// // ),
|
||||
// // actions: [
|
||||
// // TextButton(
|
||||
// // child: Text(S.of(context)!.gotIt),
|
||||
// // onPressed: () {
|
||||
// // Navigator.pop(context);
|
||||
// // UploadQueueRoute().push(context);
|
||||
// // },
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// // );
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
context.read<PendingTasksNotifier>().addListener(_onTasksChanged);
|
||||
}
|
||||
|
||||
void _onTasksChanged() {
|
||||
final taskNotifier = context.read<PendingTasksNotifier>();
|
||||
for (var task in taskNotifier.value.values) {
|
||||
context.read<LocalNotificationService>().notifyTaskChanged(task);
|
||||
}
|
||||
}
|
||||
|
||||
void _onReceiveSharedFiles(List<SharedMediaFile> sharedFiles) async {
|
||||
final files = sharedFiles.map((file) => File(file.path)).toList();
|
||||
|
||||
if (files.isNotEmpty) {
|
||||
final userId = context.read<LocalUserAccount>().id;
|
||||
final notifier = context.read<ConsumptionChangeNotifier>();
|
||||
await notifier.addFiles(
|
||||
files: files,
|
||||
userId: userId,
|
||||
);
|
||||
final localFiles = notifier.pendingFiles;
|
||||
for (int i = 0; i < localFiles.length; i++) {
|
||||
final file = localFiles[i];
|
||||
await consumeLocalFile(
|
||||
context,
|
||||
file: file,
|
||||
userId: userId,
|
||||
exitAppAfterConsumed: i == localFiles.length - 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
context.read<PendingTasksNotifier>().removeListener(_onTasksChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> consumeLocalFile(
|
||||
BuildContext context, {
|
||||
required File file,
|
||||
required String userId,
|
||||
bool exitAppAfterConsumed = false,
|
||||
}) async {
|
||||
final consumptionNotifier = context.read<ConsumptionChangeNotifier>();
|
||||
final taskNotifier = context.read<PendingTasksNotifier>();
|
||||
final ioFile = File(file.path);
|
||||
// if (!await ioFile.exists()) {
|
||||
// Fluttertoast.showToast(
|
||||
// msg: S.of(context)!.couldNotAccessReceivedFile,
|
||||
// toastLength: Toast.LENGTH_LONG,
|
||||
// );
|
||||
// }
|
||||
|
||||
final bytes = ioFile.readAsBytes();
|
||||
final shouldDirectlyUpload =
|
||||
Hive.globalSettingsBox.getValue()!.skipDocumentPreprarationOnUpload;
|
||||
if (shouldDirectlyUpload) {
|
||||
final taskId = await context.read<PaperlessDocumentsApi>().create(
|
||||
await bytes,
|
||||
filename: p.basename(file.path),
|
||||
title: p.basenameWithoutExtension(file.path),
|
||||
);
|
||||
consumptionNotifier.discardFile(file, userId: userId);
|
||||
if (taskId != null) {
|
||||
taskNotifier.listenToTaskChanges(taskId);
|
||||
}
|
||||
} else {
|
||||
final result = await DocumentUploadRoute(
|
||||
$extra: bytes,
|
||||
filename: p.basenameWithoutExtension(file.path),
|
||||
title: p.basenameWithoutExtension(file.path),
|
||||
fileExtension: p.extension(file.path),
|
||||
).push<DocumentUploadResult>(context) ??
|
||||
DocumentUploadResult(false, null);
|
||||
|
||||
if (result.success) {
|
||||
await Fluttertoast.showToast(
|
||||
msg: S.of(context)!.documentSuccessfullyUploadedProcessing,
|
||||
);
|
||||
await consumptionNotifier.discardFile(file, userId: userId);
|
||||
|
||||
if (result.taskId != null) {
|
||||
taskNotifier.listenToTaskChanges(result.taskId!);
|
||||
}
|
||||
if (exitAppAfterConsumed) {
|
||||
SystemNavigator.pop();
|
||||
}
|
||||
} else {
|
||||
final shouldDiscard = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => DiscardSharedFileDialog(bytes: bytes),
|
||||
) ??
|
||||
false;
|
||||
if (shouldDiscard) {
|
||||
await context
|
||||
.read<ConsumptionChangeNotifier>()
|
||||
.discardFile(file, userId: userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,26 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
part 'task_status_state.dart';
|
||||
|
||||
class TaskStatusCubit extends Cubit<TaskStatusState> {
|
||||
class PendingTasksNotifier extends ValueNotifier<Map<String, Task>> {
|
||||
final PaperlessTasksApi _api;
|
||||
TaskStatusCubit(this._api) : super(const TaskStatusState());
|
||||
PendingTasksNotifier(this._api) : super({});
|
||||
|
||||
void listenToTaskChanges(String taskId) {
|
||||
_api
|
||||
.listenForTaskChanges(taskId)
|
||||
.forEach(
|
||||
(element) => emit(
|
||||
TaskStatusState(
|
||||
isListening: true,
|
||||
task: element,
|
||||
),
|
||||
),
|
||||
)
|
||||
.whenComplete(() => emit(state.copyWith(isListening: false)));
|
||||
}
|
||||
|
||||
Future<void> acknowledgeCurrentTask() async {
|
||||
if (state.task == null) {
|
||||
return;
|
||||
}
|
||||
final task = await _api.acknowledgeTask(state.task!);
|
||||
emit(
|
||||
state.copyWith(
|
||||
task: task,
|
||||
isListening: false,
|
||||
),
|
||||
_api.listenForTaskChanges(taskId).forEach((task) {
|
||||
value = {...value, taskId: task};
|
||||
notifyListeners();
|
||||
}).whenComplete(
|
||||
() {
|
||||
value = value..remove(taskId);
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> acknowledgeTasks(Iterable<String> taskIds) async {
|
||||
final tasks = value.values.where((task) => taskIds.contains(task.taskId));
|
||||
await Future.wait([for (var task in tasks) _api.acknowledgeTask(task)]);
|
||||
value = value..removeWhere((key, value) => taskIds.contains(key));
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
part of 'task_status_cubit.dart';
|
||||
|
||||
class TaskStatusState extends Equatable {
|
||||
final Task? task;
|
||||
final bool isListening;
|
||||
|
||||
const TaskStatusState({
|
||||
this.task,
|
||||
this.isListening = false,
|
||||
});
|
||||
|
||||
bool get isSuccess => task?.status == TaskStatus.success;
|
||||
|
||||
bool get isAcknowledged => task?.acknowledged ?? false;
|
||||
|
||||
String? get taskId => task?.taskId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [task, isListening];
|
||||
|
||||
TaskStatusState copyWith({
|
||||
Task? task,
|
||||
bool? isListening,
|
||||
bool? isAcknowledged,
|
||||
}) {
|
||||
return TaskStatusState(
|
||||
task: task ?? this.task,
|
||||
isListening: isListening ?? this.isListening,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -865,11 +865,11 @@
|
||||
"@missingPermissions": {
|
||||
"description": "Message shown in a snackbar when a user without the reequired permissions performs an action."
|
||||
},
|
||||
"editView": "Edit View",
|
||||
"editView": "Editar Vista",
|
||||
"@editView": {
|
||||
"description": "Title of the edit saved view page"
|
||||
},
|
||||
"donate": "Donate",
|
||||
"donate": "Donar",
|
||||
"@donate": {
|
||||
"description": "Label of the in-app donate button"
|
||||
},
|
||||
@@ -877,23 +877,23 @@
|
||||
"@donationDialogContent": {
|
||||
"description": "Text displayed in the donation dialog"
|
||||
},
|
||||
"noDocumentsFound": "No documents found.",
|
||||
"noDocumentsFound": "Sense Documents trobats.",
|
||||
"@noDocumentsFound": {
|
||||
"description": "Message shown when no documents were found."
|
||||
},
|
||||
"couldNotDeleteCorrespondent": "Could not delete correspondent, please try again.",
|
||||
"couldNotDeleteCorrespondent": "No es pot esborrar corresponsal, torna a provar.",
|
||||
"@couldNotDeleteCorrespondent": {
|
||||
"description": "Message shown in snackbar when a correspondent could not be deleted."
|
||||
},
|
||||
"couldNotDeleteDocumentType": "Could not delete document type, please try again.",
|
||||
"couldNotDeleteDocumentType": "No es pot esborrar document, prova de nou.",
|
||||
"@couldNotDeleteDocumentType": {
|
||||
"description": "Message shown when a document type could not be deleted"
|
||||
},
|
||||
"couldNotDeleteTag": "Could not delete tag, please try again.",
|
||||
"couldNotDeleteTag": "No es pot esborrar etiqueta, prova de nou.",
|
||||
"@couldNotDeleteTag": {
|
||||
"description": "Message shown when a tag could not be deleted"
|
||||
},
|
||||
"couldNotDeleteStoragePath": "Could not delete storage path, please try again.",
|
||||
"couldNotDeleteStoragePath": "No es pot esborrar ruta emmagatzematge, prova de nou.",
|
||||
"@couldNotDeleteStoragePath": {
|
||||
"description": "Message shown when a storage path could not be deleted"
|
||||
},
|
||||
@@ -938,7 +938,7 @@
|
||||
"@savedViewSuccessfullyUpdated": {
|
||||
"description": "Message shown when a saved view was successfully updated."
|
||||
},
|
||||
"discardChanges": "Discard changes?",
|
||||
"discardChanges": "Descartar canvis?",
|
||||
"@discardChanges": {
|
||||
"description": "Title of the alert dialog shown when a user tries to close a view with unsaved changes."
|
||||
},
|
||||
@@ -950,11 +950,11 @@
|
||||
"@createFromCurrentFilter": {
|
||||
"description": "Tooltip of the \"New saved view\" button"
|
||||
},
|
||||
"home": "Home",
|
||||
"home": "Inici",
|
||||
"@home": {
|
||||
"description": "Label of the \"Home\" route"
|
||||
},
|
||||
"welcomeUser": "Welcome, {name}!",
|
||||
"welcomeUser": "Benvingut {name}!",
|
||||
"@welcomeUser": {
|
||||
"description": "Top message shown on the home page"
|
||||
},
|
||||
@@ -962,16 +962,23 @@
|
||||
"@noSavedViewOnHomepageHint": {
|
||||
"description": "Message shown when there is no saved view to display on the home page."
|
||||
},
|
||||
"statistics": "Statistics",
|
||||
"documentsInInbox": "Documents in inbox",
|
||||
"statistics": "Estadístiques",
|
||||
"documentsInInbox": "Document safata",
|
||||
"totalDocuments": "Total documents",
|
||||
"totalCharacters": "Total characters",
|
||||
"showAll": "Show all",
|
||||
"totalCharacters": "Caràcters Totals",
|
||||
"showAll": "Mostra tot",
|
||||
"@showAll": {
|
||||
"description": "Button label shown on a saved view preview to open this view in the documents page"
|
||||
},
|
||||
"userAlreadyExists": "This user already exists.",
|
||||
"userAlreadyExists": "Usuari ja esisteix.",
|
||||
"@userAlreadyExists": {
|
||||
"description": "Error message shown when the user tries to add an already existing account."
|
||||
}
|
||||
},
|
||||
"youDidNotSaveAnyViewsYet": "You did not save any views yet, create one and it will be shown here.",
|
||||
"@youDidNotSaveAnyViewsYet": {
|
||||
"description": "Message shown when there are no saved views yet."
|
||||
},
|
||||
"tryAgain": "Try again",
|
||||
"discardFile": "Discard file?",
|
||||
"discard": "Discard"
|
||||
}
|
||||
@@ -973,5 +973,12 @@
|
||||
"userAlreadyExists": "This user already exists.",
|
||||
"@userAlreadyExists": {
|
||||
"description": "Error message shown when the user tries to add an already existing account."
|
||||
}
|
||||
},
|
||||
"youDidNotSaveAnyViewsYet": "You did not save any views yet, create one and it will be shown here.",
|
||||
"@youDidNotSaveAnyViewsYet": {
|
||||
"description": "Message shown when there are no saved views yet."
|
||||
},
|
||||
"tryAgain": "Try again",
|
||||
"discardFile": "Discard file?",
|
||||
"discard": "Discard"
|
||||
}
|
||||
@@ -973,5 +973,12 @@
|
||||
"userAlreadyExists": "Dieser Nutzer existiert bereits.",
|
||||
"@userAlreadyExists": {
|
||||
"description": "Error message shown when the user tries to add an already existing account."
|
||||
}
|
||||
},
|
||||
"youDidNotSaveAnyViewsYet": "Du hast noch keine Ansichten gespeichert. Erstelle eine neue Ansicht, und sie wird hier angezeigt.",
|
||||
"@youDidNotSaveAnyViewsYet": {
|
||||
"description": "Message shown when there are no saved views yet."
|
||||
},
|
||||
"tryAgain": "Erneut versuchen",
|
||||
"discardFile": "Datei verwerfen?",
|
||||
"discard": "Verwerfen"
|
||||
}
|
||||
@@ -973,5 +973,12 @@
|
||||
"userAlreadyExists": "This user already exists.",
|
||||
"@userAlreadyExists": {
|
||||
"description": "Error message shown when the user tries to add an already existing account."
|
||||
}
|
||||
},
|
||||
"youDidNotSaveAnyViewsYet": "You did not save any views yet, create one and it will be shown here.",
|
||||
"@youDidNotSaveAnyViewsYet": {
|
||||
"description": "Message shown when there are no saved views yet."
|
||||
},
|
||||
"tryAgain": "Try again",
|
||||
"discardFile": "Discard file?",
|
||||
"discard": "Discard"
|
||||
}
|
||||
@@ -973,5 +973,12 @@
|
||||
"userAlreadyExists": "This user already exists.",
|
||||
"@userAlreadyExists": {
|
||||
"description": "Error message shown when the user tries to add an already existing account."
|
||||
}
|
||||
},
|
||||
"youDidNotSaveAnyViewsYet": "You did not save any views yet, create one and it will be shown here.",
|
||||
"@youDidNotSaveAnyViewsYet": {
|
||||
"description": "Message shown when there are no saved views yet."
|
||||
},
|
||||
"tryAgain": "Try again",
|
||||
"discardFile": "Discard file?",
|
||||
"discard": "Discard"
|
||||
}
|
||||
@@ -973,5 +973,12 @@
|
||||
"userAlreadyExists": "This user already exists.",
|
||||
"@userAlreadyExists": {
|
||||
"description": "Error message shown when the user tries to add an already existing account."
|
||||
}
|
||||
},
|
||||
"youDidNotSaveAnyViewsYet": "You did not save any views yet, create one and it will be shown here.",
|
||||
"@youDidNotSaveAnyViewsYet": {
|
||||
"description": "Message shown when there are no saved views yet."
|
||||
},
|
||||
"tryAgain": "Try again",
|
||||
"discardFile": "Discard file?",
|
||||
"discard": "Discard"
|
||||
}
|
||||
@@ -973,5 +973,12 @@
|
||||
"userAlreadyExists": "This user already exists.",
|
||||
"@userAlreadyExists": {
|
||||
"description": "Error message shown when the user tries to add an already existing account."
|
||||
}
|
||||
},
|
||||
"youDidNotSaveAnyViewsYet": "You did not save any views yet, create one and it will be shown here.",
|
||||
"@youDidNotSaveAnyViewsYet": {
|
||||
"description": "Message shown when there are no saved views yet."
|
||||
},
|
||||
"tryAgain": "Try again",
|
||||
"discardFile": "Discard file?",
|
||||
"discard": "Discard"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -973,5 +973,12 @@
|
||||
"userAlreadyExists": "This user already exists.",
|
||||
"@userAlreadyExists": {
|
||||
"description": "Error message shown when the user tries to add an already existing account."
|
||||
}
|
||||
},
|
||||
"youDidNotSaveAnyViewsYet": "You did not save any views yet, create one and it will be shown here.",
|
||||
"@youDidNotSaveAnyViewsYet": {
|
||||
"description": "Message shown when there are no saved views yet."
|
||||
},
|
||||
"tryAgain": "Try again",
|
||||
"discardFile": "Discard file?",
|
||||
"discard": "Discard"
|
||||
}
|
||||
@@ -28,15 +28,14 @@ import 'package:paperless_mobile/core/exception/server_message_exception.dart';
|
||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||
import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart';
|
||||
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
|
||||
import 'package:paperless_mobile/core/model/info_message_exception.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
import 'package:paperless_mobile/core/security/session_manager.dart';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
import 'package:paperless_mobile/core/translation/error_code_localization_mapper.dart';
|
||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||
import 'package:paperless_mobile/features/settings/view/widgets/global_settings_builder.dart';
|
||||
import 'package:paperless_mobile/features/sharing/model/share_intent_queue.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:paperless_mobile/routes/navigation_keys.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||
@@ -45,6 +44,7 @@ import 'package:paperless_mobile/routes/typed/branches/labels_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/landing_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/saved_views_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/scanner_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/upload_queue_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/shells/provider_shell_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/shells/scaffold_shell_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/login_route.dart';
|
||||
@@ -84,17 +84,17 @@ Future<void> _initHive() async {
|
||||
void main() async {
|
||||
runZonedGuarded(() async {
|
||||
Paint.enableDithering = true;
|
||||
if (kDebugMode) {
|
||||
// URL: http://localhost:3131
|
||||
// Login: admin:test
|
||||
await LocalMockApiServer(
|
||||
// RandomDelayGenerator(
|
||||
// const Duration(milliseconds: 100),
|
||||
// const Duration(milliseconds: 800),
|
||||
// ),
|
||||
)
|
||||
.start();
|
||||
}
|
||||
// if (kDebugMode) {
|
||||
// // URL: http://localhost:3131
|
||||
// // Login: admin:test
|
||||
// await LocalMockApiServer(
|
||||
// // RandomDelayGenerator(
|
||||
// // const Duration(milliseconds: 100),
|
||||
// // const Duration(milliseconds: 800),
|
||||
// // ),
|
||||
// )
|
||||
// .start();
|
||||
// }
|
||||
await _initHive();
|
||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||
final globalSettingsBox =
|
||||
@@ -154,7 +154,7 @@ void main() async {
|
||||
connectivityStatusService,
|
||||
);
|
||||
await authenticationCubit.restoreSessionState();
|
||||
|
||||
await ShareIntentQueue.instance.initialize();
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
@@ -240,6 +240,7 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
||||
routes: [
|
||||
$settingsRoute,
|
||||
$savedViewsRoute,
|
||||
$uploadQueueRoute,
|
||||
StatefulShellRoute(
|
||||
navigatorContainerBuilder: (context, navigationShell, children) {
|
||||
return children[navigationShell.currentIndex];
|
||||
@@ -281,7 +282,6 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
||||
case UnauthenticatedState():
|
||||
const LoginRoute().go(context);
|
||||
break;
|
||||
|
||||
case RequiresLocalAuthenticationState():
|
||||
const VerifyIdentityRoute().go(context);
|
||||
break;
|
||||
@@ -292,6 +292,8 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
||||
const LandingRoute().go(context);
|
||||
break;
|
||||
case AuthenticationErrorState():
|
||||
const LoginRoute().go(context);
|
||||
break;
|
||||
}
|
||||
},
|
||||
child: GlobalSettingsBuilder(
|
||||
|
||||
@@ -20,4 +20,5 @@ class R {
|
||||
static const settings = "settings";
|
||||
static const linkedDocuments = "linkedDocuments";
|
||||
static const bulkEditDocuments = "bulkEditDocuments";
|
||||
static const uploadQueue = "uploadQueue";
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -51,7 +53,7 @@ class ScannerRoute extends GoRouteData {
|
||||
|
||||
class DocumentUploadRoute extends GoRouteData {
|
||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
||||
final Uint8List $extra;
|
||||
final FutureOr<Uint8List> $extra;
|
||||
final String? title;
|
||||
final String? filename;
|
||||
final String? fileExtension;
|
||||
|
||||
20
lib/routes/typed/branches/upload_queue_route.dart
Normal file
20
lib/routes/typed/branches/upload_queue_route.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:paperless_mobile/features/sharing/view/consumption_queue_view.dart';
|
||||
import 'package:paperless_mobile/routes/navigation_keys.dart';
|
||||
import 'package:paperless_mobile/routes/routes.dart';
|
||||
|
||||
part 'upload_queue_route.g.dart';
|
||||
|
||||
@TypedGoRoute<UploadQueueRoute>(
|
||||
path: "/upload-queue",
|
||||
name: R.uploadQueue,
|
||||
)
|
||||
class UploadQueueRoute extends GoRouteData {
|
||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ConsumptionQueueView();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
|
||||
@@ -7,7 +6,12 @@ import 'package:paperless_mobile/core/database/tables/global_settings.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
|
||||
import 'package:paperless_mobile/features/home/view/home_shell_widget.dart';
|
||||
import 'package:paperless_mobile/features/sharing/cubit/receive_share_cubit.dart';
|
||||
import 'package:paperless_mobile/features/sharing/view/widgets/upload_queue_shell.dart';
|
||||
import 'package:paperless_mobile/routes/navigation_keys.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
/// Key used to access
|
||||
|
||||
//part 'provider_shell_route.g.dart';
|
||||
//TODO: Wait for https://github.com/flutter/flutter/issues/127371 to be merged
|
||||
@@ -66,7 +70,11 @@ class ProviderShellRoute extends ShellRouteData {
|
||||
localUserId: authenticatedUser.id,
|
||||
paperlessApiVersion: authenticatedUser.apiVersion,
|
||||
paperlessProviderFactory: apiFactory,
|
||||
child: navigator,
|
||||
child: ChangeNotifierProvider(
|
||||
create: (context) => ConsumptionChangeNotifier()
|
||||
..loadFromConsumptionDirectory(userId: currentUserId),
|
||||
child: UploadQueueShell(child: navigator),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user