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/profile/profile_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/services/router/routes_list.dart';
|
||||
import 'package:moneymgr_mobile/widgets/load_startup_data.dart';
|
||||
@ -41,6 +42,13 @@ GoRouter router(Ref ref) {
|
||||
selectedIcon: Icons.camera_alt,
|
||||
label: "Scan",
|
||||
),
|
||||
NavigationItem(
|
||||
path: scansPage,
|
||||
body: (_) => ScansListScreen(),
|
||||
icon: Icons.list,
|
||||
selectedIcon: Icons.list_alt,
|
||||
label: "List",
|
||||
),
|
||||
NavigationItem(
|
||||
path: profilePage,
|
||||
body: (_) => ProfileScreen(),
|
||||
|
@ -13,8 +13,11 @@ const manualAuthPage = "/login/manual";
|
||||
/// Settings path
|
||||
const settingsPage = "/settings";
|
||||
|
||||
/// Scan URL path
|
||||
/// Scan path
|
||||
const scanPage = "/scan";
|
||||
|
||||
/// Scans page
|
||||
const scansPage = "/scans";
|
||||
|
||||
/// Profile path
|
||||
const profilePage = "/profile";
|
@ -16,7 +16,7 @@ typedef ExpensesList = List<Expense>;
|
||||
@freezed
|
||||
abstract class BaseExpenseInfo with _$BaseExpenseInfo {
|
||||
const factory BaseExpenseInfo({
|
||||
required String label,
|
||||
required String? label,
|
||||
required double cost,
|
||||
required DateTime time,
|
||||
}) = _BaseExpenseInfo;
|
||||
@ -53,6 +53,12 @@ abstract class Expense with _$Expense {
|
||||
if (mimeType == "image/png") return "$id.png";
|
||||
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
|
||||
|
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(
|
||||
BaseExpenseInfo(
|
||||
label: labelController.text,
|
||||
label: labelController.text.isEmpty ? null : labelController.text,
|
||||
cost: double.tryParse(costController.text) ?? 0,
|
||||
time: timeController.value,
|
||||
),
|
||||
|
@ -1,12 +1,8 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:pdf_image_renderer/pdf_image_renderer.dart';
|
||||
import 'package:moneymgr_mobile/utils/pdf_utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'pdf_viewer.g.dart';
|
||||
@ -17,54 +13,7 @@ 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();
|
||||
}
|
||||
}
|
||||
return renderPdf(path: path, pdfBytes: pdfBytes);
|
||||
}
|
||||
|
||||
class PDFViewer extends ConsumerWidget {
|
||||
@ -98,7 +47,7 @@ class PDFViewer extends ConsumerWidget {
|
||||
fit: fit,
|
||||
),
|
||||
AsyncError(:final error) => Text('PDF error: $error'),
|
||||
_ => const CircularProgressIndicator(),
|
||||
_ => Center(child: const CircularProgressIndicator()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user