v1.3.0+4: FOSS Compliance + Dark Mode + Enhanced Settings

 Major Features:
- Dark mode toggle with app-wide theme switching
- Sort inventory by Expiration Date, Name, or Location
- Toggle between Grid and List view for inventory
- Export inventory data to CSV with share functionality
- Custom sage leaf app icon with adaptive icon support

🔄 FOSS Compliance (F-Droid Ready):
- Replaced Firebase with Supabase (open-source backend)
- Anonymous authentication (no user accounts required)
- Cloud-first with hosted Supabase as default
- Optional self-hosting support
- 100% FOSS-compliant dependencies

🎨 UI/UX Improvements:
- Dynamic version display from package.json (was hardcoded)
- Added edit buttons for household and user names
- Removed non-functional search button
- Replaced Recipes placeholder with Settings button
- Improved settings organization with clear sections

📦 Dependencies:
Added:
- supabase_flutter: ^2.8.4 (FOSS backend sync)
- package_info_plus: ^8.1.0 (dynamic version)
- csv: ^6.0.0 (data export)
- share_plus: ^10.1.2 (file sharing)
- image: ^4.5.4 (dev, icon generation)

Removed:
- firebase_core (replaced with Supabase)
- cloud_firestore (replaced with Supabase)

🗑️ Cleanup:
- Removed Firebase setup files and google-services.json
- Removed unimplemented features (Recipes, Search)
- Removed firebase_household_service.dart
- Removed inventory_sync_service.dart (replaced with Supabase)

📄 New Files:
- lib/features/household/services/supabase_household_service.dart
- web/privacy-policy.html (Play Store requirement)
- web/terms-of-service.html (Play Store requirement)
- PLAY_STORE_LISTING.md (marketing copy)
- tool/generate_icons.dart (icon generation script)
- assets/icon/sage_leaf.png (1024x1024)
- assets/icon/sage_leaf_foreground.png (adaptive icon)

🐛 Bug Fixes:
- Fixed version display showing hardcoded "1.0.0"
- Fixed Sort By and Default View showing static text
- Fixed ConsumerWidget build signatures
- Fixed Location.displayName import issues
- Added clearAllData method to Hive database

📊 Stats: +1,728 additions, -756 deletions across 42 files

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-04 22:27:42 -04:00
parent af63e11abd
commit 7ab641a3c8
42 changed files with 1728 additions and 756 deletions

View File

@@ -0,0 +1,227 @@
import 'package:supabase_flutter/supabase_flutter.dart';
import '../../settings/models/household.dart';
import '../../../features/inventory/models/food_item.dart';
/// FOSS-compliant household sync using Supabase (open source Firebase alternative)
/// Users can use free Supabase cloud tier OR self-host their own instance!
class SupabaseHouseholdService {
final SupabaseClient _client = Supabase.instance.client;
/// Check if user is authenticated with Supabase
bool get isAuthenticated => _client.auth.currentUser != null;
/// Create a new household in Supabase
Future<Household> createHousehold(String name, String ownerName) async {
// Ensure we're signed in anonymously
await signInAnonymously();
final household = Household(
id: Household.generateCode(),
name: name,
ownerName: ownerName,
createdAt: DateTime.now(),
members: [ownerName],
);
await _client.from('households').insert({
'id': household.id,
'name': household.name,
'owner_name': household.ownerName,
'created_at': household.createdAt.toIso8601String(),
'members': household.members,
});
print('✅ Household created: ${household.id}');
return household;
}
/// Get household by ID
Future<Household?> getHousehold(String householdId) async {
// Ensure we're signed in anonymously
await signInAnonymously();
final response = await _client
.from('households')
.select()
.eq('id', householdId)
.single();
if (response == null) return null;
return Household(
id: response['id'],
name: response['name'],
ownerName: response['owner_name'],
createdAt: DateTime.parse(response['created_at']),
members: List<String>.from(response['members']),
);
}
/// Join an existing household
Future<Household> joinHousehold(String householdId, String userName) async {
// Ensure we're signed in anonymously
await signInAnonymously();
// Get current household
final household = await getHousehold(householdId);
if (household == null) {
throw Exception('Household not found');
}
// Add user to members if not already there
if (!household.members.contains(userName)) {
final updatedMembers = [...household.members, userName];
await _client.from('households').update({
'members': updatedMembers,
}).eq('id', householdId);
print('✅ Joined household: $householdId');
// Return updated household
household.members = updatedMembers;
return household;
}
return household;
}
/// Leave a household
Future<void> leaveHousehold(String householdId, String userName) async {
final household = await getHousehold(householdId);
if (household == null) return;
final updatedMembers = household.members.where((m) => m != userName).toList();
await _client.from('households').update({
'members': updatedMembers,
}).eq('id', householdId);
print('✅ Left household: $householdId');
}
/// Update household name
Future<void> updateHouseholdName(String householdId, String newName) async {
// Ensure we're signed in anonymously
await signInAnonymously();
await _client.from('households').update({
'name': newName,
}).eq('id', householdId);
print('✅ Updated household name: $newName');
}
/// Add food item to household inventory
Future<void> addFoodItem(String householdId, FoodItem item, String localKey) async {
// Ensure we're signed in anonymously
await signInAnonymously();
await _client.from('food_items').insert({
'household_id': householdId,
'local_key': localKey,
'name': item.name,
'category': item.category,
'barcode': item.barcode,
'quantity': item.quantity,
'unit': item.unit,
'purchase_date': item.purchaseDate.toIso8601String(),
'expiration_date': item.expirationDate.toIso8601String(),
'notes': item.notes,
'last_modified': item.lastModified?.toIso8601String() ?? DateTime.now().toIso8601String(),
});
print('✅ Synced item to Supabase: ${item.name}');
}
/// Update food item in household inventory
Future<void> updateFoodItem(String householdId, FoodItem item, String localKey) async {
await _client.from('food_items').update({
'name': item.name,
'category': item.category,
'barcode': item.barcode,
'quantity': item.quantity,
'unit': item.unit,
'purchase_date': item.purchaseDate.toIso8601String(),
'expiration_date': item.expirationDate.toIso8601String(),
'notes': item.notes,
'last_modified': item.lastModified?.toIso8601String() ?? DateTime.now().toIso8601String(),
}).eq('household_id', householdId).eq('local_key', localKey);
print('✅ Updated item in Supabase: ${item.name}');
}
/// Delete food item from household inventory
Future<void> deleteFoodItem(String householdId, String localKey) async {
await _client
.from('food_items')
.delete()
.eq('household_id', householdId)
.eq('local_key', localKey);
print('✅ Deleted item from Supabase');
}
/// Get all food items for a household
Future<List<FoodItem>> getHouseholdItems(String householdId) async {
final response = await _client
.from('food_items')
.select()
.eq('household_id', householdId);
return (response as List).map<FoodItem>((item) {
final foodItem = FoodItem();
foodItem.name = item['name'];
foodItem.category = item['category'];
foodItem.barcode = item['barcode'];
foodItem.quantity = item['quantity'];
foodItem.unit = item['unit'];
foodItem.purchaseDate = DateTime.parse(item['purchase_date']);
foodItem.expirationDate = DateTime.parse(item['expiration_date']);
foodItem.notes = item['notes'];
foodItem.lastModified = DateTime.parse(item['last_modified']);
foodItem.householdId = item['household_id'];
return foodItem;
}).toList();
}
/// Subscribe to real-time updates for household items
/// Returns a stream that emits whenever items change
Stream<List<FoodItem>> subscribeToHouseholdItems(String householdId) {
return _client
.from('food_items')
.stream(primaryKey: ['household_id', 'local_key'])
.eq('household_id', householdId)
.map((data) {
return data.map<FoodItem>((item) {
final foodItem = FoodItem();
foodItem.name = item['name'];
foodItem.category = item['category'];
foodItem.barcode = item['barcode'];
foodItem.quantity = item['quantity'];
foodItem.unit = item['unit'];
foodItem.purchaseDate = DateTime.parse(item['purchase_date']);
foodItem.expirationDate = DateTime.parse(item['expiration_date']);
foodItem.notes = item['notes'];
foodItem.lastModified = DateTime.parse(item['last_modified']);
foodItem.householdId = item['household_id'];
return foodItem;
}).toList();
});
}
/// Sign in anonymously (no account needed!)
/// This lets users sync without creating accounts
Future<void> signInAnonymously() async {
if (!isAuthenticated) {
await _client.auth.signInAnonymously();
print('✅ Signed in anonymously to Supabase');
}
}
/// Sign out
Future<void> signOut() async {
await _client.auth.signOut();
print('✅ Signed out from Supabase');
}
}