Files
Sage/lib/features/inventory/services/barcode_service.dart
Dani 7be7b270e6 Initial commit: Sage Kitchen Management App v1.0.0
 Features implemented:
- Smart inventory tracking with Hive database
- Barcode scanning with auto-populated product info
- Multiple API fallbacks (Open Food Facts, UPCItemDB)
- Smart expiration date predictions by category
- Discord webhook notifications (persisted)
- Custom sage leaf vector icon
- Material Design 3 UI with sage green theme
- Privacy Policy & Terms of Service
- Local-first, privacy-focused architecture

🎨 UI/UX:
- Home dashboard with inventory stats
- Add Item screen with barcode integration
- Inventory list with expiration indicators
- Settings with persistent preferences
- About section with legal docs

🔧 Technical:
- Flutter 3.35.5 with Riverpod state management
- Hive 2.2.3 for local database
- Mobile scanner for barcode detection
- Feature-first architecture

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 13:54:21 -04:00

186 lines
5.4 KiB
Dart

import 'dart:convert';
import 'package:http/http.dart' as http;
class BarcodeService {
/// Lookup product info from barcode using multiple APIs
static Future<ProductInfo?> lookupBarcode(String barcode) async {
// Try Open Food Facts first (best for food)
final openFoodResult = await _tryOpenFoodFacts(barcode);
if (openFoodResult != null) return openFoodResult;
// Try UPCItemDB (good for vitamins, supplements, general products)
final upcItemDbResult = await _tryUPCItemDB(barcode);
if (upcItemDbResult != null) return upcItemDbResult;
return null;
}
/// Try Open Food Facts API
static Future<ProductInfo?> _tryOpenFoodFacts(String barcode) async {
try {
final response = await http.get(
Uri.parse('https://world.openfoodfacts.org/api/v0/product/$barcode.json'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['status'] == 1) {
final product = data['product'];
return ProductInfo(
name: product['product_name'] ?? 'Unknown Product',
category: _extractCategory(product),
imageUrl: product['image_url'],
);
}
}
} catch (e) {
print('Open Food Facts error: $e');
}
return null;
}
/// Try UPCItemDB API (no key needed for limited requests)
static Future<ProductInfo?> _tryUPCItemDB(String barcode) async {
try {
final response = await http.get(
Uri.parse('https://api.upcitemdb.com/prod/trial/lookup?upc=$barcode'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['items'] != null && data['items'].isNotEmpty) {
final item = data['items'][0];
return ProductInfo(
name: item['title'] ?? 'Unknown Product',
category: item['category'] ?? _guessCategoryFromTitle(item['title']),
imageUrl: item['images']?[0],
);
}
}
} catch (e) {
print('UPCItemDB error: $e');
}
return null;
}
/// Guess category from product title
static String? _guessCategoryFromTitle(String? title) {
if (title == null) return null;
final titleLower = title.toLowerCase();
if (titleLower.contains('vitamin') || titleLower.contains('supplement')) return 'Supplements';
if (titleLower.contains('protein') || titleLower.contains('powder')) return 'Supplements';
if (titleLower.contains('milk') || titleLower.contains('cheese')) return 'Dairy';
if (titleLower.contains('sauce') || titleLower.contains('dressing')) return 'Condiments';
if (titleLower.contains('drink') || titleLower.contains('beverage')) return 'Beverages';
return null;
}
static String? _extractCategory(Map<String, dynamic> product) {
// Try to get category from various fields
if (product['categories'] != null && product['categories'].toString().isNotEmpty) {
final categories = product['categories'].toString().split(',');
if (categories.isNotEmpty) {
return categories.first.trim();
}
}
if (product['food_groups'] != null) {
return product['food_groups'].toString();
}
return null;
}
/// Get smart expiration days based on category
static int getSmartExpirationDays(String? category) {
if (category == null) return 7; // Default 1 week
final categoryLower = category.toLowerCase();
// Dairy products
if (categoryLower.contains('milk') ||
categoryLower.contains('dairy') ||
categoryLower.contains('yogurt') ||
categoryLower.contains('cheese')) {
return 7; // 1 week
}
// Meat and seafood
if (categoryLower.contains('meat') ||
categoryLower.contains('chicken') ||
categoryLower.contains('beef') ||
categoryLower.contains('pork') ||
categoryLower.contains('fish') ||
categoryLower.contains('seafood')) {
return 3; // 3 days
}
// Produce
if (categoryLower.contains('fruit') ||
categoryLower.contains('vegetable') ||
categoryLower.contains('produce')) {
return 5; // 5 days
}
// Beverages
if (categoryLower.contains('beverage') ||
categoryLower.contains('drink') ||
categoryLower.contains('juice') ||
categoryLower.contains('soda')) {
return 30; // 30 days
}
// Condiments and sauces
if (categoryLower.contains('sauce') ||
categoryLower.contains('condiment') ||
categoryLower.contains('dressing') ||
categoryLower.contains('ketchup') ||
categoryLower.contains('mustard')) {
return 90; // 3 months
}
// Canned/packaged goods
if (categoryLower.contains('canned') ||
categoryLower.contains('packaged') ||
categoryLower.contains('snack')) {
return 180; // 6 months
}
// Bread and bakery
if (categoryLower.contains('bread') ||
categoryLower.contains('bakery') ||
categoryLower.contains('pastry')) {
return 5; // 5 days
}
// Supplements and vitamins
if (categoryLower.contains('supplement') ||
categoryLower.contains('vitamin') ||
categoryLower.contains('protein') ||
categoryLower.contains('pill')) {
return 365; // 1 year
}
return 7; // Default 1 week
}
}
class ProductInfo {
final String name;
final String? category;
final String? imageUrl;
ProductInfo({
required this.name,
this.category,
this.imageUrl,
});
}