Start to build synchronization logic
This commit is contained in:
@ -4,6 +4,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:moneymgr_mobile/services/router/routes_list.dart';
|
import 'package:moneymgr_mobile/services/router/routes_list.dart';
|
||||||
import 'package:moneymgr_mobile/services/storage/expenses.dart';
|
import 'package:moneymgr_mobile/services/storage/expenses.dart';
|
||||||
import 'package:moneymgr_mobile/utils/time_utils.dart';
|
import 'package:moneymgr_mobile/utils/time_utils.dart';
|
||||||
|
import 'package:moneymgr_mobile/widgets/full_screen_error.dart';
|
||||||
|
import 'package:moneymgr_mobile/widgets/loading_scaffold.dart';
|
||||||
|
import 'package:moneymgr_mobile/widgets/synchronize_button.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'scans_list_screen.g.dart';
|
part 'scans_list_screen.g.dart';
|
||||||
@ -20,14 +23,21 @@ class ScansListScreen extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final expensesList = ref.watch(_expensesListProvider);
|
final expensesList = ref.watch(_expensesListProvider);
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: Text("Expenses")),
|
return switch (expensesList) {
|
||||||
body: switch (expensesList) {
|
AsyncData(:final value) => Scaffold(
|
||||||
AsyncData(:final value) => _ExpensesList(list: value),
|
appBar: AppBar(
|
||||||
AsyncError(:final error) => Center(child: Text('Load error: $error')),
|
title: Text("Expenses"),
|
||||||
_ => const Center(child: CircularProgressIndicator()),
|
actions: [value.isEmpty ? Container() : SynchronizeButton()],
|
||||||
},
|
),
|
||||||
);
|
body: _ExpensesList(list: value),
|
||||||
|
),
|
||||||
|
AsyncError(:final error) => FullScreenError(
|
||||||
|
message: "Failed to load the list of expenses",
|
||||||
|
error: error.toString(),
|
||||||
|
),
|
||||||
|
_ => const LoadingScaffold(title: "Expenses"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,11 @@ class ApiClient {
|
|||||||
: client = Dio(BaseOptions(baseUrl: token.apiUrl));
|
: client = Dio(BaseOptions(baseUrl: token.apiUrl));
|
||||||
|
|
||||||
/// Get Dio instance
|
/// Get Dio instance
|
||||||
Future<Response<T>> execute<T>(String uri, {String method = "GET"}) async {
|
Future<Response<T>> execute<T>(
|
||||||
|
String uri, {
|
||||||
|
String method = "GET",
|
||||||
|
Object? data,
|
||||||
|
}) async {
|
||||||
Logger.root.fine("Request on ${token.apiUrl} - URI $uri");
|
Logger.root.fine("Request on ${token.apiUrl} - URI $uri");
|
||||||
return client.request(
|
return client.request(
|
||||||
uri,
|
uri,
|
||||||
@ -32,6 +36,7 @@ class ApiClient {
|
|||||||
method: method,
|
method: method,
|
||||||
headers: {apiTokenHeader: _genJWT(method, uri)},
|
headers: {apiTokenHeader: _genJWT(method, uri)},
|
||||||
),
|
),
|
||||||
|
data: data,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
43
moneymgr_mobile/lib/services/api/files_api.dart
Normal file
43
moneymgr_mobile/lib/services/api/files_api.dart
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:moneymgr_mobile/services/api/api_client.dart';
|
||||||
|
|
||||||
|
part 'files_api.freezed.dart';
|
||||||
|
part 'files_api.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class UploadResult with _$UploadResult {
|
||||||
|
const factory UploadResult({required int id}) = _UploadResult;
|
||||||
|
|
||||||
|
factory UploadResult.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UploadResultFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FilesApi on ApiClient {
|
||||||
|
/// Upload a file
|
||||||
|
Future<UploadResult> uploadFile({
|
||||||
|
required String filename,
|
||||||
|
required String mimeType,
|
||||||
|
required Uint8List bytes,
|
||||||
|
}) async {
|
||||||
|
final res = await execute(
|
||||||
|
"/file",
|
||||||
|
method: "POST",
|
||||||
|
data: FormData.fromMap({
|
||||||
|
"file": MultipartFile.fromBytes(
|
||||||
|
bytes,
|
||||||
|
filename: filename,
|
||||||
|
contentType: MediaType.parse(mimeType),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Logger.root.fine("Successfully uploaded file with response=${res.data}");
|
||||||
|
|
||||||
|
return UploadResult.fromJson(res.data);
|
||||||
|
}
|
||||||
|
}
|
74
moneymgr_mobile/lib/widgets/synchronize_button.dart
Normal file
74
moneymgr_mobile/lib/widgets/synchronize_button.dart
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:moneymgr_mobile/services/api/api_client.dart';
|
||||||
|
import 'package:moneymgr_mobile/services/api/files_api.dart';
|
||||||
|
import 'package:moneymgr_mobile/services/storage/expenses.dart';
|
||||||
|
import 'package:moneymgr_mobile/utils/extensions.dart';
|
||||||
|
import 'package:moneymgr_mobile/utils/hooks.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'synchronize_button.g.dart';
|
||||||
|
|
||||||
|
/// Synchronize expenses list with backend
|
||||||
|
@riverpod
|
||||||
|
Future<void> _performSynchronization(Ref ref) async {
|
||||||
|
final expenses = ref.watch(expensesProvider).requireValue;
|
||||||
|
final apiService = ref.watch(apiServiceProvider)!;
|
||||||
|
|
||||||
|
final list = await expenses.getList();
|
||||||
|
|
||||||
|
for (final exp in list) {
|
||||||
|
// First, upload file
|
||||||
|
final bytes = await expenses.loadFile(exp);
|
||||||
|
final file = await apiService.uploadFile(
|
||||||
|
filename: exp.localFileName,
|
||||||
|
mimeType: exp.mimeType,
|
||||||
|
bytes: bytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO continue
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SynchronizeButton extends HookConsumerWidget {
|
||||||
|
const SynchronizeButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final (:pending, :snapshot, :hasError) = useAsyncTask();
|
||||||
|
|
||||||
|
handleSynchronize() async {
|
||||||
|
try {
|
||||||
|
await ref.watch(
|
||||||
|
_performSynchronizationProvider.selectAsync((it) => it),
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Logger.root.warning("Failed to synchronize expenses! $e $s");
|
||||||
|
if (context.mounted) {
|
||||||
|
context.showTextSnackBar("Failed to synchronize expenses! $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot.connectionState == ConnectionState.waiting
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: IconButton(
|
||||||
|
onPressed: () => pending.value = handleSynchronize(),
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: hasError
|
||||||
|
? WidgetStatePropertyAll(Colors.red)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
icon: Icon(Icons.sync_rounded),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -529,7 +529,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.2"
|
version: "3.2.2"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
|
@ -72,6 +72,7 @@ dependencies:
|
|||||||
|
|
||||||
# API requests
|
# API requests
|
||||||
dio: ^5.8.0+1
|
dio: ^5.8.0+1
|
||||||
|
http_parser: ^4.1.2
|
||||||
|
|
||||||
# Qr Code library
|
# Qr Code library
|
||||||
mobile_scanner: ^7.0.1
|
mobile_scanner: ^7.0.1
|
||||||
|
Reference in New Issue
Block a user