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:
@@ -1,9 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:io';
|
||||
import 'package:csv/csv.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import '../../../core/constants/colors.dart';
|
||||
import '../../../core/constants/app_icon.dart';
|
||||
import '../../../data/local/hive_database.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../../notifications/services/discord_service.dart';
|
||||
import '../../inventory/repositories/inventory_repository_impl.dart';
|
||||
import '../../inventory/models/food_item.dart';
|
||||
import 'privacy_policy_screen.dart';
|
||||
import 'terms_of_service_screen.dart';
|
||||
import 'household_screen.dart';
|
||||
@@ -19,11 +27,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final _discordService = DiscordService();
|
||||
AppSettings? _settings;
|
||||
bool _isLoading = true;
|
||||
String _appVersion = '1.3.0';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSettings();
|
||||
_loadAppVersion();
|
||||
}
|
||||
|
||||
Future<void> _loadAppVersion() async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
setState(() {
|
||||
_appVersion = packageInfo.version;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadSettings() async {
|
||||
@@ -117,17 +134,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
// Display Section
|
||||
_buildSectionHeader('Display'),
|
||||
SwitchListTile(
|
||||
title: const Text('Dark Mode'),
|
||||
subtitle: const Text('Reduce eye strain with dark theme'),
|
||||
value: _settings!.darkModeEnabled,
|
||||
onChanged: (value) {
|
||||
setState(() => _settings!.darkModeEnabled = value);
|
||||
_saveSettings();
|
||||
},
|
||||
activeColor: AppColors.primary,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Default View'),
|
||||
subtitle: const Text('Grid'),
|
||||
subtitle: Text(_settings!.defaultView == 'grid' ? 'Grid' : 'List'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {},
|
||||
onTap: _showDefaultViewDialog,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Sort By'),
|
||||
subtitle: const Text('Expiration Date'),
|
||||
subtitle: Text(_getSortByDisplayName(_settings!.sortBy)),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {},
|
||||
onTap: _showSortByDialog,
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
@@ -138,7 +165,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
title: const Text('Export Data'),
|
||||
subtitle: const Text('Export your inventory to CSV'),
|
||||
leading: const Icon(Icons.file_download, color: AppColors.primary),
|
||||
onTap: () {},
|
||||
onTap: _exportData,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Clear All Data'),
|
||||
@@ -158,15 +185,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// TODO: Clear data
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('All data cleared'),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
onPressed: () async {
|
||||
// Clear all data from Hive
|
||||
await HiveDatabase.clearAllData();
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('All data cleared successfully'),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text(
|
||||
'Clear',
|
||||
@@ -187,9 +218,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
title: Text('App Name'),
|
||||
subtitle: Text('Sage - Kitchen Management'),
|
||||
),
|
||||
const ListTile(
|
||||
title: Text('Version'),
|
||||
subtitle: Text('1.0.0'),
|
||||
ListTile(
|
||||
title: const Text('Version'),
|
||||
subtitle: Text(_appVersion),
|
||||
),
|
||||
const ListTile(
|
||||
title: Text('Developer'),
|
||||
@@ -233,7 +264,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
showLicensePage(
|
||||
context: context,
|
||||
applicationName: 'Sage',
|
||||
applicationVersion: '1.0.0',
|
||||
applicationVersion: _appVersion,
|
||||
applicationIcon: const SageLeafIcon(
|
||||
size: 64,
|
||||
color: AppColors.primary,
|
||||
@@ -262,6 +293,189 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _exportData() async {
|
||||
try {
|
||||
final repository = InventoryRepositoryImpl();
|
||||
final items = await repository.getAllItems();
|
||||
|
||||
if (items.isEmpty) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('No items to export!')),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create CSV data
|
||||
List<List<dynamic>> csvData = [
|
||||
['Name', 'Category', 'Location', 'Quantity', 'Unit', 'Barcode', 'Purchase Date', 'Expiration Date', 'Notes'],
|
||||
];
|
||||
|
||||
for (var item in items) {
|
||||
csvData.add([
|
||||
item.name,
|
||||
item.category ?? '',
|
||||
item.location.displayName,
|
||||
item.quantity,
|
||||
item.unit ?? '',
|
||||
item.barcode ?? '',
|
||||
DateFormat('yyyy-MM-dd').format(item.purchaseDate),
|
||||
DateFormat('yyyy-MM-dd').format(item.expirationDate),
|
||||
item.notes ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
// Convert to CSV string
|
||||
String csv = const ListToCsvConverter().convert(csvData);
|
||||
|
||||
// Save to temporary file
|
||||
final directory = await getTemporaryDirectory();
|
||||
final timestamp = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
|
||||
final filePath = '${directory.path}/sage_inventory_$timestamp.csv';
|
||||
final file = File(filePath);
|
||||
await file.writeAsString(csv);
|
||||
|
||||
// Share the file
|
||||
await Share.shareXFiles(
|
||||
[XFile(filePath)],
|
||||
subject: 'Sage Inventory Export',
|
||||
text: 'Exported ${items.length} items from Sage Kitchen Manager',
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Exported ${items.length} items!'),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error exporting data: $e'),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _getSortByDisplayName(String sortBy) {
|
||||
switch (sortBy) {
|
||||
case 'expiration':
|
||||
return 'Expiration Date';
|
||||
case 'name':
|
||||
return 'Name';
|
||||
case 'location':
|
||||
return 'Location';
|
||||
default:
|
||||
return 'Expiration Date';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showDefaultViewDialog() async {
|
||||
final result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Default View'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('Grid'),
|
||||
leading: Radio<String>(
|
||||
value: 'grid',
|
||||
groupValue: _settings!.defaultView,
|
||||
onChanged: (value) => Navigator.pop(context, value),
|
||||
activeColor: AppColors.primary,
|
||||
),
|
||||
onTap: () => Navigator.pop(context, 'grid'),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('List'),
|
||||
leading: Radio<String>(
|
||||
value: 'list',
|
||||
groupValue: _settings!.defaultView,
|
||||
onChanged: (value) => Navigator.pop(context, value),
|
||||
activeColor: AppColors.primary,
|
||||
),
|
||||
onTap: () => Navigator.pop(context, 'list'),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
setState(() => _settings!.defaultView = result);
|
||||
await _saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showSortByDialog() async {
|
||||
final result = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Sort By'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('Expiration Date'),
|
||||
leading: Radio<String>(
|
||||
value: 'expiration',
|
||||
groupValue: _settings!.sortBy,
|
||||
onChanged: (value) => Navigator.pop(context, value),
|
||||
activeColor: AppColors.primary,
|
||||
),
|
||||
onTap: () => Navigator.pop(context, 'expiration'),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Name'),
|
||||
leading: Radio<String>(
|
||||
value: 'name',
|
||||
groupValue: _settings!.sortBy,
|
||||
onChanged: (value) => Navigator.pop(context, value),
|
||||
activeColor: AppColors.primary,
|
||||
),
|
||||
onTap: () => Navigator.pop(context, 'name'),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Location'),
|
||||
leading: Radio<String>(
|
||||
value: 'location',
|
||||
groupValue: _settings!.sortBy,
|
||||
onChanged: (value) => Navigator.pop(context, value),
|
||||
activeColor: AppColors.primary,
|
||||
),
|
||||
onTap: () => Navigator.pop(context, 'location'),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
setState(() => _settings!.sortBy = result);
|
||||
await _saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
void _showDiscordSetup() {
|
||||
final webhookController = TextEditingController(
|
||||
text: _discordService.webhookUrl ?? '',
|
||||
|
Reference in New Issue
Block a user