fixes before build 3
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
/// Editable list of parsed receipt lines before they are saved as a shop trip.
|
/// Editable list of parsed receipt lines before they are saved as a shop trip.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'models/line_item.dart';
|
import 'models/line_item.dart';
|
||||||
@@ -113,6 +115,24 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
|||||||
setState(() => _shoppedAt = picked);
|
setState(() => _shoppedAt = picked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _openReceipt() async {
|
||||||
|
final path = widget.imagePath;
|
||||||
|
if (path == null || path.isEmpty) return;
|
||||||
|
final file = File(path);
|
||||||
|
if (!file.existsSync()) return;
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return Dialog(
|
||||||
|
insetPadding: const EdgeInsets.all(16),
|
||||||
|
child: InteractiveViewer(
|
||||||
|
child: Image.file(file, fit: BoxFit.contain),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _confirm() async {
|
Future<void> _confirm() async {
|
||||||
final supermarket = _resolvedSupermarket;
|
final supermarket = _resolvedSupermarket;
|
||||||
if (supermarket == null) {
|
if (supermarket == null) {
|
||||||
@@ -160,6 +180,10 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
|||||||
if (_supermarket != null && !names.contains(_supermarket)) {
|
if (_supermarket != null && !names.contains(_supermarket)) {
|
||||||
names.add(_supermarket!);
|
names.add(_supermarket!);
|
||||||
}
|
}
|
||||||
|
final receiptPath = widget.imagePath;
|
||||||
|
final hasReceipt = receiptPath != null &&
|
||||||
|
receiptPath.isNotEmpty &&
|
||||||
|
File(receiptPath).existsSync();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -313,6 +337,27 @@ class _ReviewScreenState extends State<ReviewScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (hasReceipt)
|
||||||
|
ExpansionTile(
|
||||||
|
title: const Text('See receipt'),
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _openReceipt,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image.file(
|
||||||
|
File(receiptPath),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
height: 220,
|
||||||
|
width: double.infinity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
if (widget.rawText.isNotEmpty)
|
if (widget.rawText.isNotEmpty)
|
||||||
ExpansionTile(
|
ExpansionTile(
|
||||||
title: const Text('Raw OCR text'),
|
title: const Text('Raw OCR text'),
|
||||||
|
|||||||
@@ -68,6 +68,15 @@ class AppSettings extends ChangeNotifier {
|
|||||||
catalogGeneration++;
|
catalogGeneration++;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> wipeAllData() async {
|
||||||
|
await repository.wipeAllData();
|
||||||
|
darkMode = false;
|
||||||
|
supermarkets = List<Supermarket>.from(kDefaultSupermarkets);
|
||||||
|
catalogGeneration++;
|
||||||
|
notifyListeners();
|
||||||
|
await load();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsScope extends InheritedNotifier<AppSettings> {
|
class SettingsScope extends InheritedNotifier<AppSettings> {
|
||||||
|
|||||||
@@ -1148,6 +1148,7 @@ class ProductRepository extends ChangeNotifier {
|
|||||||
double? price,
|
double? price,
|
||||||
int? quantity,
|
int? quantity,
|
||||||
double? unitPrice,
|
double? unitPrice,
|
||||||
|
bool clearQuantity = false,
|
||||||
}) async {
|
}) async {
|
||||||
final db = await _database;
|
final db = await _database;
|
||||||
final rows = await db.query(
|
final rows = await db.query(
|
||||||
@@ -1161,8 +1162,13 @@ class ProductRepository extends ChangeNotifier {
|
|||||||
final values = <String, Object?>{};
|
final values = <String, Object?>{};
|
||||||
if (name != null) values['name'] = name.trim();
|
if (name != null) values['name'] = name.trim();
|
||||||
if (price != null) values['price'] = price;
|
if (price != null) values['price'] = price;
|
||||||
if (quantity != null) values['quantity'] = quantity;
|
if (clearQuantity) {
|
||||||
if (unitPrice != null) values['unit_price'] = unitPrice;
|
values['quantity'] = null;
|
||||||
|
values['unit_price'] = null;
|
||||||
|
} else {
|
||||||
|
if (quantity != null) values['quantity'] = quantity;
|
||||||
|
if (unitPrice != null) values['unit_price'] = unitPrice;
|
||||||
|
}
|
||||||
if (values.isEmpty) return;
|
if (values.isEmpty) return;
|
||||||
await db.update('shop_items', values, where: 'id = ?', whereArgs: [id]);
|
await db.update('shop_items', values, where: 'id = ?', whereArgs: [id]);
|
||||||
await _recalculateShopTotal(shopId);
|
await _recalculateShopTotal(shopId);
|
||||||
@@ -1540,15 +1546,45 @@ class ProductRepository extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearImageFolders() async {
|
Future<void> clearImageFolders() async {
|
||||||
final dir = await getApplicationDocumentsDirectory();
|
try {
|
||||||
for (final name in ['receipts', 'products']) {
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
final folder = Directory(p.join(dir.path, name));
|
for (final name in ['receipts', 'products', 'bonus_cards']) {
|
||||||
if (await folder.exists()) {
|
final folder = Directory(p.join(dir.path, name));
|
||||||
await folder.delete(recursive: true);
|
if (await folder.exists()) {
|
||||||
|
await folder.delete(recursive: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Platform folders are unavailable in some unit tests.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deletes all app data and restores default supermarkets.
|
||||||
|
Future<void> wipeAllData() async {
|
||||||
|
final db = await _database;
|
||||||
|
await db.transaction((txn) async {
|
||||||
|
await txn.delete('shopping_list_items');
|
||||||
|
await txn.delete('shopping_lists');
|
||||||
|
await txn.delete('shop_tags');
|
||||||
|
await txn.delete('shop_items');
|
||||||
|
await txn.delete('shops');
|
||||||
|
await txn.delete('products');
|
||||||
|
await txn.delete('supermarkets');
|
||||||
|
await txn.delete('app_settings');
|
||||||
|
|
||||||
|
await _resetSequence(txn, 'supermarkets');
|
||||||
|
await _resetSequence(txn, 'products');
|
||||||
|
await _resetSequence(txn, 'shops');
|
||||||
|
await _resetSequence(txn, 'shop_items');
|
||||||
|
await _resetSequence(txn, 'shop_tags');
|
||||||
|
await _resetSequence(txn, 'shopping_lists');
|
||||||
|
await _resetSequence(txn, 'shopping_list_items');
|
||||||
|
await _seedSupermarkets(txn);
|
||||||
|
});
|
||||||
|
await clearImageFolders();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> replaceAllData({
|
Future<void> replaceAllData({
|
||||||
required List<Map<String, Object?>> supermarkets,
|
required List<Map<String, Object?>> supermarkets,
|
||||||
required List<Map<String, Object?>> products,
|
required List<Map<String, Object?>> products,
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ class _ShopDetailScreenState extends State<ShopDetailScreen> {
|
|||||||
price: result.price,
|
price: result.price,
|
||||||
quantity: result.quantity,
|
quantity: result.quantity,
|
||||||
unitPrice: result.unitPrice,
|
unitPrice: result.unitPrice,
|
||||||
|
clearQuantity: result.quantity == null,
|
||||||
);
|
);
|
||||||
if (mounted) _load();
|
if (mounted) _load();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import '../models/line_item.dart';
|
import '../models/line_item.dart';
|
||||||
import '../utils/money.dart';
|
import '../utils/money.dart';
|
||||||
@@ -26,47 +27,147 @@ class ItemEditSheet extends StatefulWidget {
|
|||||||
|
|
||||||
class _ItemEditSheetState extends State<ItemEditSheet> {
|
class _ItemEditSheetState extends State<ItemEditSheet> {
|
||||||
late final TextEditingController _nameController;
|
late final TextEditingController _nameController;
|
||||||
late final TextEditingController _priceController;
|
late final TextEditingController _quantityController;
|
||||||
|
late final TextEditingController _unitPriceController;
|
||||||
|
late final TextEditingController _totalController;
|
||||||
String? _error;
|
String? _error;
|
||||||
|
bool _syncing = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final item = widget.item;
|
final item = widget.item;
|
||||||
|
final quantity = item?.quantity ?? 1;
|
||||||
|
final total = item?.price;
|
||||||
|
final unit = item?.unitPrice ??
|
||||||
|
(total != null && quantity > 0 ? roundMoney(total / quantity) : null);
|
||||||
|
|
||||||
_nameController = TextEditingController(text: item?.name ?? '');
|
_nameController = TextEditingController(text: item?.name ?? '');
|
||||||
_priceController = TextEditingController(
|
_quantityController = TextEditingController(text: '$quantity');
|
||||||
text: item == null ? '' : item.price.toStringAsFixed(2),
|
_unitPriceController = TextEditingController(
|
||||||
|
text: unit == null ? '' : unit.toStringAsFixed(2),
|
||||||
);
|
);
|
||||||
|
_totalController = TextEditingController(
|
||||||
|
text: total == null ? '' : total.toStringAsFixed(2),
|
||||||
|
);
|
||||||
|
|
||||||
|
_quantityController.addListener(_onQuantityChanged);
|
||||||
|
_unitPriceController.addListener(_onUnitPriceChanged);
|
||||||
|
_totalController.addListener(_onTotalChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_quantityController.removeListener(_onQuantityChanged);
|
||||||
|
_unitPriceController.removeListener(_onUnitPriceChanged);
|
||||||
|
_totalController.removeListener(_onTotalChanged);
|
||||||
_nameController.dispose();
|
_nameController.dispose();
|
||||||
_priceController.dispose();
|
_quantityController.dispose();
|
||||||
|
_unitPriceController.dispose();
|
||||||
|
_totalController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int? get _quantity {
|
||||||
|
final raw = _quantityController.text.trim();
|
||||||
|
if (raw.isEmpty) return null;
|
||||||
|
return int.tryParse(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setText(TextEditingController controller, String value) {
|
||||||
|
if (controller.text == value) return;
|
||||||
|
controller.value = TextEditingValue(
|
||||||
|
text: value,
|
||||||
|
selection: TextSelection.collapsed(offset: value.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onQuantityChanged() {
|
||||||
|
if (_syncing) return;
|
||||||
|
final quantity = _quantity;
|
||||||
|
if (quantity == null || quantity <= 0) {
|
||||||
|
setState(() {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final unit = tryParsePrice(_unitPriceController.text);
|
||||||
|
final total = tryParsePrice(_totalController.text);
|
||||||
|
_syncing = true;
|
||||||
|
if (unit != null) {
|
||||||
|
_setText(_totalController, roundMoney(quantity * unit).toStringAsFixed(2));
|
||||||
|
} else if (total != null) {
|
||||||
|
_setText(
|
||||||
|
_unitPriceController,
|
||||||
|
roundMoney(total / quantity).toStringAsFixed(2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_syncing = false;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUnitPriceChanged() {
|
||||||
|
if (_syncing) return;
|
||||||
|
final quantity = _quantity;
|
||||||
|
final unit = tryParsePrice(_unitPriceController.text);
|
||||||
|
if (quantity == null || quantity <= 0 || unit == null) {
|
||||||
|
setState(() {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_syncing = true;
|
||||||
|
_setText(_totalController, roundMoney(quantity * unit).toStringAsFixed(2));
|
||||||
|
_syncing = false;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTotalChanged() {
|
||||||
|
if (_syncing) return;
|
||||||
|
final quantity = _quantity;
|
||||||
|
final total = tryParsePrice(_totalController.text);
|
||||||
|
if (quantity == null || quantity <= 0 || total == null) {
|
||||||
|
setState(() {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_syncing = true;
|
||||||
|
_setText(
|
||||||
|
_unitPriceController,
|
||||||
|
roundMoney(total / quantity).toStringAsFixed(2),
|
||||||
|
);
|
||||||
|
_syncing = false;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
void _save() {
|
void _save() {
|
||||||
final name = _nameController.text.trim();
|
final name = _nameController.text.trim();
|
||||||
final price = tryParsePrice(_priceController.text);
|
final quantity = _quantity;
|
||||||
if (name.isEmpty || price == null) {
|
final unit = tryParsePrice(_unitPriceController.text);
|
||||||
setState(() => _error = 'Enter a product name and a price.');
|
final total = tryParsePrice(_totalController.text);
|
||||||
|
|
||||||
|
if (name.isEmpty) {
|
||||||
|
setState(() => _error = 'Enter a product name.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final rounded = roundMoney(price);
|
if (quantity == null || quantity <= 0) {
|
||||||
final quantity = widget.item?.quantity;
|
setState(() => _error = 'Enter a quantity of at least 1.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (total == null && unit == null) {
|
||||||
|
setState(() => _error = 'Enter a unit price or a total.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final resolvedTotal = total ?? roundMoney(quantity * unit!);
|
||||||
|
final resolvedUnit = unit ?? roundMoney(resolvedTotal / quantity);
|
||||||
|
final hasMulti = quantity > 1;
|
||||||
|
|
||||||
Navigator.of(context).pop(
|
Navigator.of(context).pop(
|
||||||
LineItem(
|
LineItem(
|
||||||
id:
|
id:
|
||||||
widget.item?.id ??
|
widget.item?.id ??
|
||||||
'manual-${DateTime.now().microsecondsSinceEpoch}',
|
'manual-${DateTime.now().microsecondsSinceEpoch}',
|
||||||
name: name,
|
name: name,
|
||||||
price: rounded,
|
price: roundMoney(resolvedTotal),
|
||||||
quantity: quantity,
|
quantity: hasMulti ? quantity : null,
|
||||||
unitPrice: quantity != null && quantity > 0
|
unitPrice: hasMulti ? roundMoney(resolvedUnit) : null,
|
||||||
? roundMoney(rounded / quantity)
|
discount: widget.item?.price == roundMoney(resolvedTotal)
|
||||||
: widget.item?.unitPrice,
|
|
||||||
discount: widget.item?.price == rounded
|
|
||||||
? widget.item?.discount ?? 0
|
? widget.item?.discount ?? 0
|
||||||
: 0,
|
: 0,
|
||||||
),
|
),
|
||||||
@@ -76,6 +177,11 @@ class _ItemEditSheetState extends State<ItemEditSheet> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final bottom = MediaQuery.viewInsetsOf(context).bottom;
|
final bottom = MediaQuery.viewInsetsOf(context).bottom;
|
||||||
|
final quantity = _quantity;
|
||||||
|
final unit = tryParsePrice(_unitPriceController.text);
|
||||||
|
final preview = quantity != null && quantity > 1 && unit != null
|
||||||
|
? '$quantity × ${formatEuro(unit)}'
|
||||||
|
: null;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.fromLTRB(16, 0, 16, 16 + bottom),
|
padding: EdgeInsets.fromLTRB(16, 0, 16, 16 + bottom),
|
||||||
@@ -95,10 +201,37 @@ class _ItemEditSheetState extends State<ItemEditSheet> {
|
|||||||
autofocus: widget.item == null,
|
autofocus: widget.item == null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 88,
|
||||||
|
child: TextField(
|
||||||
|
controller: _quantityController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
decoration: const InputDecoration(labelText: 'Qty'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _unitPriceController,
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(
|
||||||
|
decimal: true,
|
||||||
|
),
|
||||||
|
decoration: const InputDecoration(labelText: 'Unit price'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _priceController,
|
controller: _totalController,
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
decoration: const InputDecoration(labelText: 'Price'),
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Total price',
|
||||||
|
helperText: preview,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (_error != null) ...[
|
if (_error != null) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||||||
import '../services/app_settings.dart';
|
import '../services/app_settings.dart';
|
||||||
import '../services/backup_service.dart';
|
import '../services/backup_service.dart';
|
||||||
import '../supermarkets_screen.dart';
|
import '../supermarkets_screen.dart';
|
||||||
|
import 'confirm_dialog.dart';
|
||||||
|
|
||||||
class SettingsButton extends StatelessWidget {
|
class SettingsButton extends StatelessWidget {
|
||||||
const SettingsButton({super.key});
|
const SettingsButton({super.key});
|
||||||
@@ -104,6 +105,36 @@ class _SettingsSheetState extends State<_SettingsSheet> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _wipeAllData(AppSettings settings) async {
|
||||||
|
final confirmed = await confirmAction(
|
||||||
|
context,
|
||||||
|
title: 'Delete all data?',
|
||||||
|
message:
|
||||||
|
'This permanently removes all products, shopping trips, lists, '
|
||||||
|
'photos, bonus cards, and settings on this device. '
|
||||||
|
'Export a backup first if you want to keep anything.',
|
||||||
|
confirmLabel: 'Delete everything',
|
||||||
|
);
|
||||||
|
if (!confirmed || !mounted) return;
|
||||||
|
|
||||||
|
setState(() => _busy = true);
|
||||||
|
try {
|
||||||
|
await settings.wipeAllData();
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('All data deleted.')),
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Could not delete all data.')),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _busy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _openSupermarkets(BuildContext context, AppSettings settings) {
|
void _openSupermarkets(BuildContext context, AppSettings settings) {
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
..pop()
|
..pop()
|
||||||
@@ -117,18 +148,20 @@ class _SettingsSheetState extends State<_SettingsSheet> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final settings = SettingsScope.of(context);
|
final settings = SettingsScope.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final scheme = theme.colorScheme;
|
||||||
final bottom = MediaQuery.viewInsetsOf(context).bottom;
|
final bottom = MediaQuery.viewInsetsOf(context).bottom;
|
||||||
|
final maxHeight = MediaQuery.sizeOf(context).height * 0.85;
|
||||||
|
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: settings,
|
listenable: settings,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return Padding(
|
return SizedBox(
|
||||||
padding: EdgeInsets.fromLTRB(16, 0, 16, 16 + bottom),
|
height: maxHeight,
|
||||||
child: Column(
|
child: ListView(
|
||||||
mainAxisSize: MainAxisSize.min,
|
padding: EdgeInsets.fromLTRB(16, 0, 16, 16 + bottom),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
children: [
|
||||||
Text('Settings', style: Theme.of(context).textTheme.titleLarge),
|
Text('Settings', style: theme.textTheme.titleLarge),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
@@ -151,10 +184,10 @@ class _SettingsSheetState extends State<_SettingsSheet> {
|
|||||||
: () => _openSupermarkets(context, settings),
|
: () => _openSupermarkets(context, settings),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text('Backup', style: Theme.of(context).textTheme.titleMedium),
|
Text('Backup', style: theme.textTheme.titleMedium),
|
||||||
Text(
|
Text(
|
||||||
'Save everything to a JSON file, or replace this device from a backup.',
|
'Save everything to a JSON file, or replace this device from a backup.',
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: theme.textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
@@ -180,6 +213,28 @@ class _SettingsSheetState extends State<_SettingsSheet> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const LinearProgressIndicator(),
|
const LinearProgressIndicator(),
|
||||||
],
|
],
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Text(
|
||||||
|
'Danger zone',
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: scheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Permanently delete everything on this device.',
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: scheme.error,
|
||||||
|
side: BorderSide(color: scheme.error),
|
||||||
|
),
|
||||||
|
onPressed: _busy ? null : () => _wipeAllData(settings),
|
||||||
|
icon: const Icon(Icons.delete_forever_outlined),
|
||||||
|
label: const Text('Delete all data'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,6 +71,25 @@ void main() {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('wipeAllData clears products, trips, lists, and settings', () async {
|
||||||
|
final repository = memoryRepository();
|
||||||
|
await repository.saveShop(
|
||||||
|
supermarket: 'Jumbo',
|
||||||
|
shoppedAt: DateTime(2026, 8, 12),
|
||||||
|
items: [LineItem(id: '1', name: 'Milk', price: 1.5)],
|
||||||
|
);
|
||||||
|
await repository.createShoppingList('Weekly');
|
||||||
|
await repository.setDarkMode(true);
|
||||||
|
|
||||||
|
await repository.wipeAllData();
|
||||||
|
|
||||||
|
expect(await repository.getAll(), isEmpty);
|
||||||
|
expect(await repository.getShops(), isEmpty);
|
||||||
|
expect(await repository.getShoppingLists(), isEmpty);
|
||||||
|
expect(await repository.getDarkMode(), isFalse);
|
||||||
|
expect((await repository.getSupermarkets()).length, greaterThan(0));
|
||||||
|
});
|
||||||
|
|
||||||
test('deleting all products leaves shopping trips in place', () async {
|
test('deleting all products leaves shopping trips in place', () async {
|
||||||
final repository = memoryRepository();
|
final repository = memoryRepository();
|
||||||
await repository.saveShop(
|
await repository.saveShop(
|
||||||
|
|||||||
Reference in New Issue
Block a user