Display the list of expenses
This commit is contained in:
		
							
								
								
									
										57
									
								
								moneymgr_mobile/lib/routes/scans_list/scans_list_screen.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								moneymgr_mobile/lib/routes/scans_list/scans_list_screen.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/services/storage/expenses.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/utils/time_utils.dart';
 | 
				
			||||||
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part 'scans_list_screen.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@riverpod
 | 
				
			||||||
 | 
					Future<ExpensesList> _expensesList(Ref ref) {
 | 
				
			||||||
 | 
					  final expenses = ref.watch(expensesProvider).requireValue;
 | 
				
			||||||
 | 
					  return expenses.getList();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ScansListScreen extends HookConsumerWidget {
 | 
				
			||||||
 | 
					  const ScansListScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final expensesList = ref.watch(_expensesListProvider);
 | 
				
			||||||
 | 
					    return Scaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(title: Text("Expenses")),
 | 
				
			||||||
 | 
					      body: switch (expensesList) {
 | 
				
			||||||
 | 
					        AsyncData(:final value) => _ExpensesList(list: value),
 | 
				
			||||||
 | 
					        AsyncError(:final error) => Center(child: Text('Load error: $error')),
 | 
				
			||||||
 | 
					        _ => const Center(child: CircularProgressIndicator()),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _ExpensesList extends StatelessWidget {
 | 
				
			||||||
 | 
					  final ExpensesList list;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _ExpensesList({required this.list});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return ListView.builder(
 | 
				
			||||||
 | 
					      itemBuilder: (context, id) {
 | 
				
			||||||
 | 
					        final expense = list[id];
 | 
				
			||||||
 | 
					        return ListTile(
 | 
				
			||||||
 | 
					          leading: Icon(Icons.receipt_long),
 | 
				
			||||||
 | 
					          title: Text(
 | 
				
			||||||
 | 
					            expense.labelOrNull ?? "No label",
 | 
				
			||||||
 | 
					            style: TextStyle(
 | 
				
			||||||
 | 
					              fontStyle: expense.labelOrNull == null ? FontStyle.italic : null,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          subtitle: Text(expense.dateTime.simpleDate),
 | 
				
			||||||
 | 
					          trailing: Text("${expense.cost} €", style: TextStyle(fontSize: 20)),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      itemCount: list.length,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -7,6 +7,7 @@ import 'package:moneymgr_mobile/routes/login/manual_auth_screen.dart';
 | 
				
			|||||||
import 'package:moneymgr_mobile/routes/login/qr_auth_screen.dart';
 | 
					import 'package:moneymgr_mobile/routes/login/qr_auth_screen.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/routes/profile/profile_screen.dart';
 | 
					import 'package:moneymgr_mobile/routes/profile/profile_screen.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/routes/scan/scan_screen.dart';
 | 
					import 'package:moneymgr_mobile/routes/scan/scan_screen.dart';
 | 
				
			||||||
 | 
					import 'package:moneymgr_mobile/routes/scans_list/scans_list_screen.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/routes/settings/settings_screen.dart';
 | 
					import 'package:moneymgr_mobile/routes/settings/settings_screen.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/services/router/routes_list.dart';
 | 
					import 'package:moneymgr_mobile/services/router/routes_list.dart';
 | 
				
			||||||
import 'package:moneymgr_mobile/widgets/load_startup_data.dart';
 | 
					import 'package:moneymgr_mobile/widgets/load_startup_data.dart';
 | 
				
			||||||
@@ -41,6 +42,13 @@ GoRouter router(Ref ref) {
 | 
				
			|||||||
      selectedIcon: Icons.camera_alt,
 | 
					      selectedIcon: Icons.camera_alt,
 | 
				
			||||||
      label: "Scan",
 | 
					      label: "Scan",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    NavigationItem(
 | 
				
			||||||
 | 
					      path: scansPage,
 | 
				
			||||||
 | 
					      body: (_) => ScansListScreen(),
 | 
				
			||||||
 | 
					      icon: Icons.list,
 | 
				
			||||||
 | 
					      selectedIcon: Icons.list_alt,
 | 
				
			||||||
 | 
					      label: "List",
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
    NavigationItem(
 | 
					    NavigationItem(
 | 
				
			||||||
      path: profilePage,
 | 
					      path: profilePage,
 | 
				
			||||||
      body: (_) => ProfileScreen(),
 | 
					      body: (_) => ProfileScreen(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,8 +13,11 @@ const manualAuthPage = "/login/manual";
 | 
				
			|||||||
/// Settings path
 | 
					/// Settings path
 | 
				
			||||||
const settingsPage = "/settings";
 | 
					const settingsPage = "/settings";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Scan URL path
 | 
					/// Scan path
 | 
				
			||||||
const scanPage = "/scan";
 | 
					const scanPage = "/scan";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Scans page
 | 
				
			||||||
 | 
					const scansPage = "/scans";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Profile path
 | 
					/// Profile path
 | 
				
			||||||
const profilePage = "/profile";
 | 
					const profilePage = "/profile";
 | 
				
			||||||
@@ -16,7 +16,7 @@ typedef ExpensesList = List<Expense>;
 | 
				
			|||||||
@freezed
 | 
					@freezed
 | 
				
			||||||
abstract class BaseExpenseInfo with _$BaseExpenseInfo {
 | 
					abstract class BaseExpenseInfo with _$BaseExpenseInfo {
 | 
				
			||||||
  const factory BaseExpenseInfo({
 | 
					  const factory BaseExpenseInfo({
 | 
				
			||||||
    required String label,
 | 
					    required String? label,
 | 
				
			||||||
    required double cost,
 | 
					    required double cost,
 | 
				
			||||||
    required DateTime time,
 | 
					    required DateTime time,
 | 
				
			||||||
  }) = _BaseExpenseInfo;
 | 
					  }) = _BaseExpenseInfo;
 | 
				
			||||||
@@ -53,6 +53,12 @@ abstract class Expense with _$Expense {
 | 
				
			|||||||
    if (mimeType == "image/png") return "$id.png";
 | 
					    if (mimeType == "image/png") return "$id.png";
 | 
				
			||||||
    return id.toString();
 | 
					    return id.toString();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Get expense label or null if empty
 | 
				
			||||||
 | 
					  String? get labelOrNull => label == "" ? null : label;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Get expense date
 | 
				
			||||||
 | 
					  DateTime get dateTime => DateTime.fromMillisecondsSinceEpoch(time * 1000);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@riverpod
 | 
					@riverpod
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										65
									
								
								moneymgr_mobile/lib/utils/pdf_utils.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								moneymgr_mobile/lib/utils/pdf_utils.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					import 'dart:math';
 | 
				
			||||||
 | 
					import 'dart:typed_data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:path/path.dart' as p;
 | 
				
			||||||
 | 
					import 'package:path_provider/path_provider.dart';
 | 
				
			||||||
 | 
					import 'package:pdf_image_renderer/pdf_image_renderer.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Render PDF to image bits
 | 
				
			||||||
 | 
					Future<Uint8List> renderPdf(
 | 
				
			||||||
 | 
					     {
 | 
				
			||||||
 | 
					      String? path,
 | 
				
			||||||
 | 
					      Uint8List? pdfBytes,
 | 
				
			||||||
 | 
					    }) async {
 | 
				
			||||||
 | 
					  assert(path != null || pdfBytes != null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Create temporary file if required
 | 
				
			||||||
 | 
					  var isTemp = false;
 | 
				
			||||||
 | 
					  if (path == null) {
 | 
				
			||||||
 | 
					    path = p.join(
 | 
				
			||||||
 | 
					      (await getTemporaryDirectory()).absolute.path,
 | 
				
			||||||
 | 
					      "render-${Random().nextInt(10000).toString()}+.pdf",
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await File(path).writeAsBytes(pdfBytes!);
 | 
				
			||||||
 | 
					    isTemp = true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    final pdf = PdfImageRenderer(path: path);
 | 
				
			||||||
 | 
					    await pdf.open();
 | 
				
			||||||
 | 
					    await pdf.openPage(pageIndex: 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // get the render size after the page is loaded
 | 
				
			||||||
 | 
					    final size = await pdf.getPageSize(pageIndex: 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // get the actual image of the page
 | 
				
			||||||
 | 
					    final img = await pdf.renderPage(
 | 
				
			||||||
 | 
					      pageIndex: 0,
 | 
				
			||||||
 | 
					      x: 0,
 | 
				
			||||||
 | 
					      y: 0,
 | 
				
			||||||
 | 
					      width: size.width,
 | 
				
			||||||
 | 
					      // you can pass a custom size here to crop the image
 | 
				
			||||||
 | 
					      height: size.height,
 | 
				
			||||||
 | 
					      // you can pass a custom size here to crop the image
 | 
				
			||||||
 | 
					      scale: 1,
 | 
				
			||||||
 | 
					      // increase the scale for better quality (e.g. for zooming)
 | 
				
			||||||
 | 
					      background: Colors.white,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // close the page again
 | 
				
			||||||
 | 
					    await pdf.closePage(pageIndex: 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // close the PDF after rendering the page
 | 
				
			||||||
 | 
					    pdf.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return img!;
 | 
				
			||||||
 | 
					  } finally {
 | 
				
			||||||
 | 
					    if (isTemp) {
 | 
				
			||||||
 | 
					      await File(path).delete();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -54,7 +54,7 @@ class ExpenseEditor extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      pending.value = onFinished(
 | 
					      pending.value = onFinished(
 | 
				
			||||||
        BaseExpenseInfo(
 | 
					        BaseExpenseInfo(
 | 
				
			||||||
          label: labelController.text,
 | 
					          label: labelController.text.isEmpty ? null : labelController.text,
 | 
				
			||||||
          cost: double.tryParse(costController.text) ?? 0,
 | 
					          cost: double.tryParse(costController.text) ?? 0,
 | 
				
			||||||
          time: timeController.value,
 | 
					          time: timeController.value,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,8 @@
 | 
				
			|||||||
import 'dart:io';
 | 
					 | 
				
			||||||
import 'dart:math';
 | 
					 | 
				
			||||||
import 'dart:typed_data';
 | 
					import 'dart:typed_data';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:path/path.dart' as p;
 | 
					import 'package:moneymgr_mobile/utils/pdf_utils.dart';
 | 
				
			||||||
import 'package:path_provider/path_provider.dart';
 | 
					 | 
				
			||||||
import 'package:pdf_image_renderer/pdf_image_renderer.dart';
 | 
					 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
part 'pdf_viewer.g.dart';
 | 
					part 'pdf_viewer.g.dart';
 | 
				
			||||||
@@ -17,54 +13,7 @@ Future<Uint8List> _renderPdf(
 | 
				
			|||||||
  String? path,
 | 
					  String? path,
 | 
				
			||||||
  Uint8List? pdfBytes,
 | 
					  Uint8List? pdfBytes,
 | 
				
			||||||
}) async {
 | 
					}) async {
 | 
				
			||||||
  assert(path != null || pdfBytes != null);
 | 
					  return renderPdf(path: path, pdfBytes: pdfBytes);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Create temporary file if required
 | 
					 | 
				
			||||||
  var isTemp = false;
 | 
					 | 
				
			||||||
  if (path == null) {
 | 
					 | 
				
			||||||
    path = p.join(
 | 
					 | 
				
			||||||
      (await getTemporaryDirectory()).absolute.path,
 | 
					 | 
				
			||||||
      "render-${Random().nextInt(10000).toString()}+.pdf",
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await File(path).writeAsBytes(pdfBytes!);
 | 
					 | 
				
			||||||
    isTemp = true;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    final pdf = PdfImageRenderer(path: path);
 | 
					 | 
				
			||||||
    await pdf.open();
 | 
					 | 
				
			||||||
    await pdf.openPage(pageIndex: 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // get the render size after the page is loaded
 | 
					 | 
				
			||||||
    final size = await pdf.getPageSize(pageIndex: 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // get the actual image of the page
 | 
					 | 
				
			||||||
    final img = await pdf.renderPage(
 | 
					 | 
				
			||||||
      pageIndex: 0,
 | 
					 | 
				
			||||||
      x: 0,
 | 
					 | 
				
			||||||
      y: 0,
 | 
					 | 
				
			||||||
      width: size.width,
 | 
					 | 
				
			||||||
      // you can pass a custom size here to crop the image
 | 
					 | 
				
			||||||
      height: size.height,
 | 
					 | 
				
			||||||
      // you can pass a custom size here to crop the image
 | 
					 | 
				
			||||||
      scale: 1,
 | 
					 | 
				
			||||||
      // increase the scale for better quality (e.g. for zooming)
 | 
					 | 
				
			||||||
      background: Colors.white,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // close the page again
 | 
					 | 
				
			||||||
    await pdf.closePage(pageIndex: 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // close the PDF after rendering the page
 | 
					 | 
				
			||||||
    pdf.close();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return img!;
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    if (isTemp) {
 | 
					 | 
				
			||||||
      await File(path).delete();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PDFViewer extends ConsumerWidget {
 | 
					class PDFViewer extends ConsumerWidget {
 | 
				
			||||||
@@ -98,7 +47,7 @@ class PDFViewer extends ConsumerWidget {
 | 
				
			|||||||
        fit: fit,
 | 
					        fit: fit,
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      AsyncError(:final error) => Text('PDF error: $error'),
 | 
					      AsyncError(:final error) => Text('PDF error: $error'),
 | 
				
			||||||
      _ => const CircularProgressIndicator(),
 | 
					      _ => Center(child: const CircularProgressIndicator()),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user