added categories, import export, settings
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
/// Grocery categories a product can be assigned to.
|
||||
library;
|
||||
|
||||
const List<String> kDefaultCategories = [
|
||||
'Fruit & vegetables',
|
||||
'Dairy & eggs',
|
||||
'Meat & fish',
|
||||
'Bread & bakery',
|
||||
'Pantry',
|
||||
'Drinks',
|
||||
'Frozen',
|
||||
'Snacks',
|
||||
'Household',
|
||||
'Personal care',
|
||||
];
|
||||
|
||||
List<String> mergeCategories(Iterable<String> used) {
|
||||
final seen = <String>{};
|
||||
final result = <String>[];
|
||||
for (final name in [...kDefaultCategories, ...used]) {
|
||||
final trimmed = name.trim();
|
||||
if (trimmed.isEmpty) continue;
|
||||
final key = trimmed.toLowerCase();
|
||||
if (seen.contains(key)) continue;
|
||||
seen.add(key);
|
||||
result.add(trimmed);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/// Date formatting for shop log and product history.
|
||||
library;
|
||||
|
||||
const _months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
|
||||
String formatDate(DateTime date) =>
|
||||
'${date.day} ${_months[date.month - 1]} ${date.year}';
|
||||
@@ -0,0 +1,40 @@
|
||||
/// Lightweight fuzzy matching for product search.
|
||||
library;
|
||||
|
||||
String _normalize(String value) =>
|
||||
value.trim().toLowerCase().replaceAll(RegExp(r'\s+'), ' ');
|
||||
|
||||
/// True when [query] fuzzily matches [text].
|
||||
bool fuzzyMatch(String query, String text) {
|
||||
final q = _normalize(query);
|
||||
if (q.isEmpty) return true;
|
||||
final t = _normalize(text);
|
||||
if (t.contains(q)) return true;
|
||||
|
||||
for (final word in q.split(' ')) {
|
||||
if (word.isEmpty) continue;
|
||||
if (!t.contains(word) && !_subsequence(word, t)) return false;
|
||||
}
|
||||
if (q.contains(' ')) return true;
|
||||
return _subsequence(q, t);
|
||||
}
|
||||
|
||||
/// Higher is better. Null when [query] does not match.
|
||||
int? fuzzyScore(String query, String text) {
|
||||
final q = _normalize(query);
|
||||
if (q.isEmpty) return 0;
|
||||
final t = _normalize(text);
|
||||
if (t == q) return 1000;
|
||||
if (t.startsWith(q)) return 800;
|
||||
if (t.contains(q)) return 600 - t.indexOf(q);
|
||||
if (!fuzzyMatch(q, t)) return null;
|
||||
return 200 - (t.length - q.length).abs().clamp(0, 150);
|
||||
}
|
||||
|
||||
bool _subsequence(String query, String text) {
|
||||
var i = 0;
|
||||
for (var c = 0; c < text.length && i < query.length; c++) {
|
||||
if (text.codeUnitAt(c) == query.codeUnitAt(i)) i++;
|
||||
}
|
||||
return i == query.length;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/// Known Dutch supermarket names, default colors, and OCR detection.
|
||||
library;
|
||||
|
||||
import '../models/supermarket.dart';
|
||||
|
||||
const String kOtherSupermarket = 'Other';
|
||||
|
||||
final List<Supermarket> kDefaultSupermarkets = [
|
||||
const Supermarket(name: 'Albert Heijn', colorValue: 0xFF00A0E2, sortOrder: 0),
|
||||
const Supermarket(name: 'Jumbo', colorValue: 0xFFEEC21B, sortOrder: 1),
|
||||
const Supermarket(name: 'Lidl', colorValue: 0xFF0050AA, sortOrder: 2),
|
||||
const Supermarket(name: 'Aldi', colorValue: 0xFF00205B, sortOrder: 3),
|
||||
const Supermarket(name: 'Plus', colorValue: 0xFF6EC31E, sortOrder: 4),
|
||||
const Supermarket(name: 'Dirk', colorValue: 0xFFE30613, sortOrder: 5),
|
||||
const Supermarket(name: 'Coop', colorValue: 0xFFE30613, sortOrder: 6),
|
||||
const Supermarket(name: 'SPAR', colorValue: 0xFF009640, sortOrder: 7),
|
||||
const Supermarket(name: 'Nettorama', colorValue: 0xFFE87722, sortOrder: 8),
|
||||
const Supermarket(name: 'Hoogvliet', colorValue: 0xFFD32F2F, sortOrder: 9),
|
||||
const Supermarket(name: 'Picnic', colorValue: 0xFFE31C5F, sortOrder: 10),
|
||||
const Supermarket(name: kOtherSupermarket, colorValue: 0xFF78909C, sortOrder: 11),
|
||||
];
|
||||
|
||||
const Map<String, List<String>> kSupermarketAliases = {
|
||||
'Albert Heijn': ['ALBERT HEIJN', 'AH TO GO', 'AH XL'],
|
||||
'Jumbo': ['JUMBO'],
|
||||
'Lidl': ['LIDL'],
|
||||
'Aldi': ['ALDI'],
|
||||
'Plus': ['PLUS'],
|
||||
'Dirk': ['DIRK'],
|
||||
'Coop': ['COOP'],
|
||||
'SPAR': ['SPAR'],
|
||||
'Nettorama': ['NETTORAMA'],
|
||||
'Hoogvliet': ['HOOGVLIET'],
|
||||
'Picnic': ['PICNIC'],
|
||||
};
|
||||
|
||||
/// Names used before settings were user-editable.
|
||||
const List<String> kSupermarkets = [
|
||||
'Albert Heijn',
|
||||
'Jumbo',
|
||||
'Lidl',
|
||||
'Aldi',
|
||||
'Plus',
|
||||
'Dirk',
|
||||
'Coop',
|
||||
'SPAR',
|
||||
'Nettorama',
|
||||
'Hoogvliet',
|
||||
'Picnic',
|
||||
'Other',
|
||||
];
|
||||
|
||||
/// Returns a supermarket name if it appears in [rawText].
|
||||
String? detectSupermarket(
|
||||
String rawText, {
|
||||
Iterable<String> names = const [],
|
||||
}) {
|
||||
final upper = rawText.toUpperCase();
|
||||
final extra = names
|
||||
.map((name) => name.trim())
|
||||
.where((name) => name.isNotEmpty && name != kOtherSupermarket)
|
||||
.toList()
|
||||
..sort((a, b) => b.length.compareTo(a.length));
|
||||
|
||||
for (final name in extra) {
|
||||
if (RegExp('\\b${RegExp.escape(name)}\\b', caseSensitive: false)
|
||||
.hasMatch(rawText)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
for (final entry in kSupermarketAliases.entries) {
|
||||
for (final alias in entry.value) {
|
||||
if (RegExp('\\b${RegExp.escape(alias)}\\b').hasMatch(upper)) {
|
||||
return _canonicalName(entry.key, extra);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (RegExp(r'\bAH\b').hasMatch(upper)) {
|
||||
return _canonicalName('Albert Heijn', extra);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _canonicalName(String fallback, List<String> names) {
|
||||
for (final name in names) {
|
||||
if (name.toUpperCase() == fallback.toUpperCase()) return name;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
Supermarket? supermarketByName(Iterable<Supermarket> stores, String? name) {
|
||||
if (name == null || name.isEmpty) return null;
|
||||
for (final store in stores) {
|
||||
if (store.name.toLowerCase() == name.toLowerCase()) return store;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user