mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 10:07:51 -06:00
WIP - Add system notifications for document upload progress/status
This commit is contained in:
@@ -35,6 +35,8 @@ android {
|
||||
compileSdkVersion 33
|
||||
|
||||
compileOptions {
|
||||
// Required for flutter_local_notifications
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
@@ -54,6 +56,8 @@ android {
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
// Required for flutter_local_notifications
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -82,4 +86,6 @@ dependencies {
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
// Required for flutter_local_notifications
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 837 B |
Binary file not shown.
|
After Width: | Height: | Size: 566 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
@@ -8,6 +8,8 @@ buildscript {
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
// implementation 'androidx.window:window:1.0.0'
|
||||
// implementation 'androidx.window:window-java:1.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
@@ -50,10 +51,8 @@ class WebSocketStatusService implements StatusService {
|
||||
}
|
||||
|
||||
class LongPollingStatusService implements StatusService {
|
||||
static const maxRetries = 60;
|
||||
|
||||
final BaseClient httpClient;
|
||||
LongPollingStatusService(this.httpClient);
|
||||
final Dio client;
|
||||
const LongPollingStatusService(this.client);
|
||||
|
||||
@override
|
||||
Future<void> startListeningBeforeDocumentUpload(
|
||||
@@ -1,11 +1,9 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/repository/impl/correspondent_repository_impl.dart';
|
||||
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
|
||||
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
|
||||
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
|
||||
@@ -21,12 +19,10 @@ import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
|
||||
import 'package:paperless_mobile/features/labels/bloc/providers/labels_bloc_provider.dart';
|
||||
import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart';
|
||||
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
import 'package:paperless_mobile/util.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class DocumentFilterIntent {
|
||||
final DocumentFilter? filter;
|
||||
|
||||
@@ -22,10 +22,12 @@ import 'package:paperless_mobile/features/documents/view/pages/documents_page.da
|
||||
import 'package:paperless_mobile/features/home/view/widget/bottom_navigation_bar.dart';
|
||||
import 'package:paperless_mobile/features/home/view/widget/info_drawer.dart';
|
||||
import 'package:paperless_mobile/features/labels/view/pages/labels_page.dart';
|
||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/features/scan/bloc/document_scanner_cubit.dart';
|
||||
import 'package:paperless_mobile/features/scan/view/scanner_page.dart';
|
||||
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:paperless_mobile/util.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
@@ -45,6 +47,46 @@ class _HomePageState extends State<HomePage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
LocalNotificationService.instance.notifyTaskChanged(
|
||||
Task(
|
||||
id: 100,
|
||||
dateCreated: DateTime.now(),
|
||||
dateDone: DateTime.now(),
|
||||
taskFileName: "test_file.pdf",
|
||||
status: TaskStatus.started,
|
||||
taskId: "abc-def-123-456",
|
||||
type: "file",
|
||||
),
|
||||
);
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
LocalNotificationService.instance.notifyTaskChanged(
|
||||
Task(
|
||||
id: 100,
|
||||
dateCreated: DateTime.now(),
|
||||
dateDone: DateTime.now(),
|
||||
taskFileName: "test_file.pdf",
|
||||
status: TaskStatus.pending,
|
||||
taskId: "abc-def-123-456",
|
||||
type: "file",
|
||||
),
|
||||
);
|
||||
});
|
||||
Future.delayed(const Duration(seconds: 10), () {
|
||||
LocalNotificationService.instance.notifyTaskChanged(
|
||||
Task(
|
||||
id: 100,
|
||||
acknowledged: false,
|
||||
dateCreated: DateTime.now(),
|
||||
dateDone: DateTime.now(),
|
||||
relatedDocumentId: 180,
|
||||
result: "New document successfully created.",
|
||||
status: TaskStatus.success,
|
||||
taskFileName: "test_file.pdf",
|
||||
taskId: "abc-def-123-456",
|
||||
type: "file",
|
||||
),
|
||||
);
|
||||
});
|
||||
_initializeData(context);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_listenForReceivedFiles();
|
||||
@@ -71,29 +113,6 @@ class _HomePageState extends State<HomePage> {
|
||||
}
|
||||
|
||||
Future<void> _handleReceivedFile(SharedMediaFile file) async {
|
||||
// final isGranted = await askForPermission(Permission.storage);
|
||||
|
||||
// if (!isGranted) {
|
||||
// return;
|
||||
// }
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (context) => AlertDialog(
|
||||
// title: Text("Received File."),
|
||||
// content: Column(
|
||||
// children: [
|
||||
// Text("Path: ${file.path}"),
|
||||
// Text("Type: ${file.type.name}"),
|
||||
// Text("Exists: ${File(file.path).existsSync()}"),
|
||||
// FutureBuilder<bool>(
|
||||
// future: Permission.storage.isGranted,
|
||||
// builder: (context, snapshot) =>
|
||||
// Text("Has storage permission: ${snapshot.data}"),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
SharedMediaFile mediaFile;
|
||||
if (Platform.isIOS) {
|
||||
// Workaround for file not found on iOS: https://stackoverflow.com/a/72813212
|
||||
@@ -165,12 +184,20 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<ConnectivityCubit, ConnectivityState>(
|
||||
return MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<ConnectivityCubit, ConnectivityState>(
|
||||
//Only re-initialize data if the connectivity changed from not connected to connected
|
||||
listenWhen: (previous, current) => current == ConnectivityState.connected,
|
||||
listenWhen: (previous, current) =>
|
||||
current == ConnectivityState.connected,
|
||||
listener: (context, state) {
|
||||
_initializeData(context);
|
||||
},
|
||||
),
|
||||
BlocListener<TaskStatusCubit, TaskStatusState>(
|
||||
listener: (context, state) {},
|
||||
),
|
||||
],
|
||||
child: Scaffold(
|
||||
key: rootScaffoldKey,
|
||||
bottomNavigationBar: BottomNavBar(
|
||||
|
||||
11
lib/features/notifications/cubit/notification_cubit.dart
Normal file
11
lib/features/notifications/cubit/notification_cubit.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
part 'notification_state.dart';
|
||||
|
||||
class NotificationCubit extends Cubit<NotificationState> {
|
||||
NotificationCubit() : super(NotificationInitialState());
|
||||
|
||||
void navigateTo(String route, dynamic args) {}
|
||||
}
|
||||
16
lib/features/notifications/cubit/notification_state.dart
Normal file
16
lib/features/notifications/cubit/notification_state.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
part of 'notification_cubit.dart';
|
||||
|
||||
abstract class NotificationState extends Equatable {
|
||||
const NotificationState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class NotificationInitialState extends NotificationState {}
|
||||
|
||||
class NotificationOpenDocumentDetailsPageState extends NotificationState {
|
||||
final int documentId;
|
||||
|
||||
const NotificationOpenDocumentDetailsPageState(this.documentId);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.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/services/notification_actions.dart';
|
||||
import 'package:paperless_mobile/features/notifications/services/notification_channels.dart';
|
||||
|
||||
class LocalNotificationService {
|
||||
final FlutterLocalNotificationsPlugin _plugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
LocalNotificationService._();
|
||||
|
||||
static final LocalNotificationService instance = LocalNotificationService._();
|
||||
|
||||
Future<void> initialize() async {
|
||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('ic_stat_paperless_logo_green');
|
||||
final DarwinInitializationSettings initializationSettingsDarwin =
|
||||
DarwinInitializationSettings(
|
||||
requestSoundPermission: false,
|
||||
requestBadgePermission: false,
|
||||
requestAlertPermission: false,
|
||||
onDidReceiveLocalNotification: onDidReceiveLocalNotification,
|
||||
);
|
||||
final InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
iOS: initializationSettingsDarwin,
|
||||
);
|
||||
await _plugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
|
||||
);
|
||||
}
|
||||
|
||||
//TODO: INTL
|
||||
Future<void> notifyTaskChanged(Task task) {
|
||||
log("[LocalNotificationService] notifyTaskChanged: ${task.toString()}");
|
||||
int id = task.id;
|
||||
final status = task.status;
|
||||
late String title;
|
||||
late String? body;
|
||||
late int timestampMillis;
|
||||
bool showProgress =
|
||||
status == TaskStatus.started || status == TaskStatus.pending;
|
||||
int progress = 0;
|
||||
dynamic payload;
|
||||
switch (status) {
|
||||
case TaskStatus.started:
|
||||
title = "Document received";
|
||||
body = task.taskFileName;
|
||||
timestampMillis = task.dateCreated.millisecondsSinceEpoch;
|
||||
progress = 10;
|
||||
break;
|
||||
case TaskStatus.pending:
|
||||
title = "Processing document...";
|
||||
body = task.taskFileName;
|
||||
timestampMillis = task.dateCreated.millisecondsSinceEpoch;
|
||||
progress = 70;
|
||||
break;
|
||||
case TaskStatus.failure:
|
||||
title = "Failed to process document";
|
||||
body = "Document ${task.taskFileName} was rejected by the server.";
|
||||
timestampMillis = task.dateCreated.millisecondsSinceEpoch;
|
||||
break;
|
||||
case TaskStatus.success:
|
||||
title = "Document successfully created";
|
||||
body = task.taskFileName;
|
||||
timestampMillis = task.dateDone!.millisecondsSinceEpoch;
|
||||
payload = CreateDocumentSuccessNotificationResponsePayload(
|
||||
task.relatedDocumentId!,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return _plugin.show(
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
'${NotificationChannel.task.id}_${task.id}',
|
||||
NotificationChannel.task.name,
|
||||
category: AndroidNotificationCategory.status,
|
||||
ongoing: showProgress,
|
||||
showProgress: showProgress,
|
||||
maxProgress: 100,
|
||||
when: timestampMillis,
|
||||
progress: progress,
|
||||
actions: status == TaskStatus.success
|
||||
? [
|
||||
AndroidNotificationAction(
|
||||
NotificationResponseAction.openCreatedDocument.name,
|
||||
"Open",
|
||||
showsUserInterface: true,
|
||||
),
|
||||
AndroidNotificationAction(
|
||||
NotificationResponseAction.acknowledgeCreatedDocument.name,
|
||||
"Acknowledge",
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
payload: jsonEncode(payload),
|
||||
);
|
||||
}
|
||||
|
||||
void onDidReceiveLocalNotification(
|
||||
int id,
|
||||
String? title,
|
||||
String? body,
|
||||
String? payload,
|
||||
) {}
|
||||
|
||||
void onDidReceiveNotificationResponse(NotificationResponse response) {
|
||||
log("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(
|
||||
NotificationResponseAction action,
|
||||
NotificationResponse response,
|
||||
) {
|
||||
switch (action) {
|
||||
case NotificationResponseAction.openCreatedDocument:
|
||||
final payload =
|
||||
CreateDocumentSuccessNotificationResponsePayload.fromJson(
|
||||
jsonDecode(response.payload!),
|
||||
);
|
||||
log("Navigate to document ${payload.documentId}");
|
||||
break;
|
||||
case NotificationResponseAction.acknowledgeCreatedDocument:
|
||||
final payload =
|
||||
CreateDocumentSuccessNotificationResponsePayload.fromJson(
|
||||
jsonDecode(response.payload!),
|
||||
);
|
||||
log("Acknowledge document ${payload.documentId}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'open_created_document_notification_payload.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
CreateDocumentSuccessNotificationResponsePayload
|
||||
_$CreateDocumentSuccessNotificationResponsePayloadFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
CreateDocumentSuccessNotificationResponsePayload(
|
||||
json['documentId'] as int,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CreateDocumentSuccessNotificationResponsePayloadToJson(
|
||||
CreateDocumentSuccessNotificationResponsePayload instance) =>
|
||||
<String, dynamic>{
|
||||
'documentId': instance.documentId,
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
enum NotificationResponseAction {
|
||||
openCreatedDocument,
|
||||
acknowledgeCreatedDocument;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
enum NotificationChannel {
|
||||
task("task_channel", "Paperless Tasks");
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
const NotificationChannel(this.id, this.name);
|
||||
}
|
||||
26
lib/features/tasks/cubit/task_status_cubit.dart
Normal file
26
lib/features/tasks/cubit/task_status_cubit.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
part 'task_status_state.dart';
|
||||
|
||||
class TaskStatusCubit extends Cubit<TaskStatusState> {
|
||||
final PaperlessTasksApi _api;
|
||||
TaskStatusCubit(this._api) : super(const TaskStatusState());
|
||||
|
||||
void startListeningToTask(String taskId) {
|
||||
_api
|
||||
.listenForTaskChanges(taskId)
|
||||
.forEach(
|
||||
(element) => TaskStatusState(
|
||||
isListening: true,
|
||||
isAcknowledged: false,
|
||||
task: element,
|
||||
),
|
||||
)
|
||||
.whenComplete(() => emit(state.copyWith(isListening: false)));
|
||||
}
|
||||
|
||||
void acknowledgeCurrentTask() {
|
||||
emit(state.copyWith(isListening: false, isAcknowledged: true));
|
||||
}
|
||||
}
|
||||
34
lib/features/tasks/cubit/task_status_state.dart
Normal file
34
lib/features/tasks/cubit/task_status_state.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
part of 'task_status_cubit.dart';
|
||||
|
||||
class TaskStatusState extends Equatable {
|
||||
final Task? task;
|
||||
final bool isListening;
|
||||
final bool isAcknowledged;
|
||||
|
||||
const TaskStatusState({
|
||||
this.task,
|
||||
this.isListening = false,
|
||||
this.isAcknowledged = false,
|
||||
});
|
||||
|
||||
bool get isActive => isListening && !isAcknowledged;
|
||||
|
||||
bool get isSuccess => task?.status == TaskStatus.success;
|
||||
|
||||
String? get taskId => task?.taskId;
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
|
||||
TaskStatusState copyWith({
|
||||
Task? task,
|
||||
bool? isListening,
|
||||
bool? isAcknowledged,
|
||||
}) {
|
||||
return TaskStatusState(
|
||||
task: task ?? this.task,
|
||||
isListening: isListening ?? this.isListening,
|
||||
isAcknowledged: isAcknowledged ?? this.isAcknowledged,
|
||||
);
|
||||
}
|
||||
}
|
||||
8
lib/features/tasks/cubit/tasks_cubit.dart
Normal file
8
lib/features/tasks/cubit/tasks_cubit.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
part 'tasks_state.dart';
|
||||
|
||||
class TasksCubit extends Cubit<TasksState> {
|
||||
TasksCubit() : super(TasksInitial());
|
||||
}
|
||||
10
lib/features/tasks/cubit/tasks_state.dart
Normal file
10
lib/features/tasks/cubit/tasks_state.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
part of 'tasks_cubit.dart';
|
||||
|
||||
abstract class TasksState extends Equatable {
|
||||
const TasksState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class TasksInitial extends TasksState {}
|
||||
@@ -39,9 +39,11 @@ import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
|
||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||
import 'package:paperless_mobile/features/login/view/login_page.dart';
|
||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
|
||||
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
|
||||
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
|
||||
import 'package:paperless_mobile/generated/l10n.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
@@ -53,6 +55,7 @@ void main() async {
|
||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await findSystemLocale();
|
||||
await LocalNotificationService.instance.initialize();
|
||||
|
||||
// Initialize External dependencies
|
||||
final connectivity = Connectivity();
|
||||
@@ -95,6 +98,7 @@ void main() async {
|
||||
final labelsApi = PaperlessLabelApiImpl(dioWrapper.client);
|
||||
final statsApi = PaperlessServerStatsApiImpl(dioWrapper.client);
|
||||
final savedViewsApi = PaperlessSavedViewsApiImpl(dioWrapper.client);
|
||||
final tasksApi = PaperlessTasksApiImpl(dioWrapper.client);
|
||||
|
||||
// Initialize Blocs/Cubits
|
||||
final connectivityCubit = ConnectivityCubit(connectivityStatusService);
|
||||
@@ -140,6 +144,7 @@ void main() async {
|
||||
Provider<PaperlessLabelsApi>.value(value: labelsApi),
|
||||
Provider<PaperlessServerStatsApi>.value(value: statsApi),
|
||||
Provider<PaperlessSavedViewsApi>.value(value: savedViewsApi),
|
||||
Provider<PaperlessTasksApi>.value(value: tasksApi),
|
||||
Provider<cm.CacheManager>(
|
||||
create: (context) => cm.CacheManager(
|
||||
cm.Config(
|
||||
@@ -328,7 +333,11 @@ class _AuthenticationWrapperState extends State<AuthenticationWrapper> {
|
||||
builder: (context, authentication) {
|
||||
if (authentication.isAuthenticated &&
|
||||
(authentication.wasLocalAuthenticationSuccessful ?? true)) {
|
||||
return const HomePage();
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
TaskStatusCubit(context.read<PaperlessTasksApi>()),
|
||||
child: const HomePage(),
|
||||
);
|
||||
} else {
|
||||
if (authentication.wasLoginStored &&
|
||||
!(authentication.wasLocalAuthenticationSuccessful ?? false)) {
|
||||
|
||||
@@ -22,3 +22,5 @@ export 'paperless_server_information_model.dart';
|
||||
export 'paperless_server_statistics_model.dart';
|
||||
export 'saved_view_model.dart';
|
||||
export 'similar_document_model.dart';
|
||||
export 'task/task.dart';
|
||||
export 'task/task_status.dart';
|
||||
|
||||
52
packages/paperless_api/lib/src/models/task/task.dart
Normal file
52
packages/paperless_api/lib/src/models/task/task.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:paperless_api/src/request_utils.dart';
|
||||
import 'task_status.dart';
|
||||
|
||||
part 'task.g.dart';
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||
class Task extends Equatable {
|
||||
final int id;
|
||||
final String? taskId;
|
||||
final String? taskFileName;
|
||||
final DateTime dateCreated;
|
||||
final DateTime? dateDone;
|
||||
final String? type;
|
||||
final TaskStatus? status;
|
||||
final String? result;
|
||||
final bool acknowledged;
|
||||
@JsonKey(fromJson: tryParseNullable)
|
||||
final int? relatedDocumentId;
|
||||
|
||||
const Task({
|
||||
required this.id,
|
||||
this.taskId,
|
||||
this.taskFileName,
|
||||
required this.dateCreated,
|
||||
this.dateDone,
|
||||
this.type,
|
||||
this.status,
|
||||
this.acknowledged = false,
|
||||
this.relatedDocumentId,
|
||||
this.result,
|
||||
});
|
||||
|
||||
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$TaskToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
taskId,
|
||||
taskFileName,
|
||||
dateCreated,
|
||||
dateDone,
|
||||
type,
|
||||
status,
|
||||
result,
|
||||
acknowledged,
|
||||
relatedDocumentId,
|
||||
];
|
||||
}
|
||||
43
packages/paperless_api/lib/src/models/task/task.g.dart
Normal file
43
packages/paperless_api/lib/src/models/task/task.g.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'task.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
||||
id: json['id'] as int,
|
||||
taskId: json['task_id'] as String?,
|
||||
taskFileName: json['task_file_name'] as String?,
|
||||
dateCreated: DateTime.parse(json['date_created'] as String),
|
||||
dateDone: json['date_done'] == null
|
||||
? null
|
||||
: DateTime.parse(json['date_done'] as String),
|
||||
type: json['type'] as String?,
|
||||
status: $enumDecodeNullable(_$TaskStatusEnumMap, json['status']),
|
||||
acknowledged: json['acknowledged'] as bool? ?? false,
|
||||
relatedDocumentId:
|
||||
tryParseNullable(json['related_document_id'] as String?),
|
||||
result: json['result'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'task_id': instance.taskId,
|
||||
'task_file_name': instance.taskFileName,
|
||||
'date_created': instance.dateCreated.toIso8601String(),
|
||||
'date_done': instance.dateDone?.toIso8601String(),
|
||||
'type': instance.type,
|
||||
'status': _$TaskStatusEnumMap[instance.status],
|
||||
'result': instance.result,
|
||||
'acknowledged': instance.acknowledged,
|
||||
'related_document_id': instance.relatedDocumentId,
|
||||
};
|
||||
|
||||
const _$TaskStatusEnumMap = {
|
||||
TaskStatus.started: 'STARTED',
|
||||
TaskStatus.pending: 'PENDING',
|
||||
TaskStatus.failure: 'FAILURE',
|
||||
TaskStatus.success: 'SUCCESS',
|
||||
};
|
||||
13
packages/paperless_api/lib/src/models/task/task_status.dart
Normal file
13
packages/paperless_api/lib/src/models/task/task_status.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
@JsonEnum(valueField: 'value')
|
||||
enum TaskStatus {
|
||||
started("STARTED"),
|
||||
pending("PENDING"),
|
||||
failure("FAILURE"),
|
||||
success("SUCCESS");
|
||||
|
||||
final String value;
|
||||
|
||||
const TaskStatus(this.value);
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import 'package:paperless_api/src/models/paged_search_result.dart';
|
||||
import 'package:paperless_api/src/models/similar_document_model.dart';
|
||||
|
||||
abstract class PaperlessDocumentsApi {
|
||||
Future<void> create(
|
||||
/// Uploads a document using a form data request and from server version 1.11.3
|
||||
/// returns the celery task id which can be used to track the status of the document.
|
||||
Future<String?> create(
|
||||
Uint8List documentBytes, {
|
||||
required String filename,
|
||||
required String title,
|
||||
@@ -27,7 +29,9 @@ abstract class PaperlessDocumentsApi {
|
||||
Future<Uint8List> getPreview(int docId);
|
||||
String getThumbnailUrl(int docId);
|
||||
Future<DocumentModel> waitForConsumptionFinished(
|
||||
String filename, String title);
|
||||
String filename,
|
||||
String title,
|
||||
);
|
||||
Future<Uint8List> download(DocumentModel document);
|
||||
|
||||
Future<List<String>> autocomplete(String query, [int limit = 10]);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -6,6 +7,7 @@ import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_api/src/constants.dart';
|
||||
import 'package:paperless_api/src/converters/document_model_json_converter.dart';
|
||||
import 'package:paperless_api/src/converters/similar_document_model_json_converter.dart';
|
||||
import 'package:paperless_api/src/request_utils.dart';
|
||||
|
||||
class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
final Dio client;
|
||||
@@ -13,7 +15,7 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
PaperlessDocumentsApiImpl(this.client);
|
||||
|
||||
@override
|
||||
Future<void> create(
|
||||
Future<String?> create(
|
||||
Uint8List documentBytes, {
|
||||
required String filename,
|
||||
required String title,
|
||||
@@ -46,8 +48,12 @@ class PaperlessDocumentsApiImpl implements PaperlessDocumentsApi {
|
||||
try {
|
||||
final response =
|
||||
await client.post('/api/documents/post_document/', data: formData);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
if (response.statusCode == 200) {
|
||||
if (response.data is String && response.data != "OK") {
|
||||
return response.data;
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
throw PaperlessServerException(
|
||||
ErrorCode.documentUploadFailed,
|
||||
httpStatusCode: response.statusCode,
|
||||
|
||||
@@ -9,3 +9,5 @@ export 'saved_views_api/paperless_saved_views_api.dart';
|
||||
export 'saved_views_api/paperless_saved_views_api_impl.dart';
|
||||
export 'server_stats_api/paperless_server_stats_api.dart';
|
||||
export 'server_stats_api/paperless_server_stats_api_impl.dart';
|
||||
export 'tasks_api/paperless_tasks_api.dart';
|
||||
export 'tasks_api/paperless_tasks_api_impl.dart';
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import 'package:paperless_api/src/models/task/task.dart';
|
||||
|
||||
abstract class PaperlessTasksApi {
|
||||
Future<Task?> find({int? id, String? taskId});
|
||||
Future<Iterable<Task>> findAll([Iterable<int>? ids]);
|
||||
Stream<Task> listenForTaskChanges(String taskId);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:paperless_api/src/models/task/task.dart';
|
||||
import 'package:paperless_api/src/models/task/task_status.dart';
|
||||
|
||||
import 'paperless_tasks_api.dart';
|
||||
|
||||
class PaperlessTasksApiImpl implements PaperlessTasksApi {
|
||||
final Dio client;
|
||||
|
||||
const PaperlessTasksApiImpl(this.client);
|
||||
|
||||
@override
|
||||
Future<Task?> find({int? id, String? taskId}) async {
|
||||
assert(id != null || taskId != null);
|
||||
String url = "/api/tasks/";
|
||||
if (taskId != null) {
|
||||
url += "?task_id=$taskId";
|
||||
} else {
|
||||
url += "$id/";
|
||||
}
|
||||
|
||||
final response = await client.get(url);
|
||||
if (response.statusCode == 200) {
|
||||
return Task.fromJson(response.data);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Iterable<Task>> findAll([Iterable<int>? ids]) async {
|
||||
final response = await client.get("/api/tasks/");
|
||||
if (response.statusCode == 200) {
|
||||
return (response.data as List).map((e) => Task.fromJson(e));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Task> listenForTaskChanges(String taskId) async* {
|
||||
bool isSuccess = false;
|
||||
while (!isSuccess) {
|
||||
final task = await find(taskId: taskId);
|
||||
if (task == null) {
|
||||
throw Exception("Task with taskId $taskId does not exist.");
|
||||
}
|
||||
yield task;
|
||||
if (task.status == TaskStatus.success) {
|
||||
isSuccess = true;
|
||||
}
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,3 +81,14 @@ class _CollectionFromJsonSerializationParams<T> {
|
||||
|
||||
_CollectionFromJsonSerializationParams(this.fromJson, this.list);
|
||||
}
|
||||
|
||||
int getExtendedVersionNumber(String version) {
|
||||
List versionCells = version.split('.');
|
||||
versionCells = versionCells.map((i) => int.parse(i)).toList();
|
||||
return versionCells[0] * 100000 + versionCells[1] * 1000 + versionCells[2];
|
||||
}
|
||||
|
||||
int? tryParseNullable(String? source, {int? radix}) {
|
||||
if (source == null) return null;
|
||||
return int.tryParse(source, radix: radix);
|
||||
}
|
||||
|
||||
32
pubspec.lock
32
pubspec.lock
@@ -588,6 +588,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: "293995f94e120c8afce768981bd1fa9c5d6de67c547568e3b42ae2defdcbb4a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.0"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
sha256: "8f6c1611e0c4a88a382691a97bb3c3feb24cc0c0b54152b8b5fb7ffb837f7fbf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -1577,6 +1601,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.21"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
sha256: "24c8fcdd49a805d95777a39064862133ff816ebfffe0ceff110fb5960e557964"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.1"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -84,6 +84,7 @@ dependencies:
|
||||
pretty_dio_logger: ^1.2.0-beta-1
|
||||
collection: ^1.17.0
|
||||
device_info_plus: ^4.1.3
|
||||
flutter_local_notifications: ^13.0.0
|
||||
|
||||
dev_dependencies:
|
||||
integration_test:
|
||||
|
||||
Reference in New Issue
Block a user