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