WIP - Add system notifications for document upload progress/status

This commit is contained in:
Anton Stubenbord
2023-01-09 01:35:47 +01:00
parent 3c6c4e63d7
commit 8cf3020335
34 changed files with 615 additions and 44 deletions

View File

@@ -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'
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -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'
}
}

View File

@@ -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(

View File

@@ -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;

View File

@@ -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(

View 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) {}
}

View 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);
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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,
};

View File

@@ -0,0 +1,4 @@
enum NotificationResponseAction {
openCreatedDocument,
acknowledgeCreatedDocument;
}

View File

@@ -0,0 +1,8 @@
enum NotificationChannel {
task("task_channel", "Paperless Tasks");
final String id;
final String name;
const NotificationChannel(this.id, this.name);
}

View 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));
}
}

View 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,
);
}
}

View 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());
}

View 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 {}

View File

@@ -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)) {

View File

@@ -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';

View 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,
];
}

View 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',
};

View 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);
}

View File

@@ -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]);

View File

@@ -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,

View File

@@ -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';

View File

@@ -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);
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}

View File

@@ -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:

View File

@@ -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: