633 lines
22 KiB
Dart
633 lines
22 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import '../providers/app_state.dart';
|
|
import '../models/bean.dart';
|
|
import '../models/machine.dart';
|
|
import '../models/recipe.dart';
|
|
import '../models/drink.dart';
|
|
import '../models/journal_entry.dart';
|
|
import 'dart:convert';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
class SettingsScreen extends StatefulWidget {
|
|
const SettingsScreen({super.key});
|
|
|
|
@override
|
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
|
}
|
|
|
|
class _SettingsScreenState extends State<SettingsScreen> {
|
|
bool _notificationsEnabled = true;
|
|
bool _darkTheme = true;
|
|
String _selectedLanguage = 'English';
|
|
|
|
final List<String> _languages = ['English', 'Spanish', 'French', 'German', 'Italian'];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadSettings();
|
|
}
|
|
|
|
Future<void> _loadSettings() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
setState(() {
|
|
_notificationsEnabled = prefs.getBool('notifications_enabled') ?? true;
|
|
_darkTheme = prefs.getBool('dark_theme') ?? true;
|
|
_selectedLanguage = prefs.getString('selected_language') ?? 'English';
|
|
});
|
|
}
|
|
|
|
Future<void> _saveSettings() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setBool('notifications_enabled', _notificationsEnabled);
|
|
await prefs.setBool('dark_theme', _darkTheme);
|
|
await prefs.setString('selected_language', _selectedLanguage);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Settings', style: Theme.of(context).textTheme.headlineMedium),
|
|
const SizedBox(height: 24),
|
|
Expanded(
|
|
child: ListView(
|
|
children: [
|
|
_buildSettingsSection(context, 'Data', [
|
|
ListTile(
|
|
leading: const Icon(Icons.backup),
|
|
title: const Text('Export Data'),
|
|
subtitle: const Text('Export your coffee data to JSON'),
|
|
onTap: () => _exportData(context),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.restore),
|
|
title: const Text('Import Data'),
|
|
subtitle: const Text('Import coffee data from JSON'),
|
|
onTap: () => _importData(context),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.delete_forever),
|
|
title: const Text('Clear All Data'),
|
|
subtitle: const Text('Delete all your coffee data'),
|
|
onTap: () => _showClearDataDialog(context),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.refresh),
|
|
title: const Text('Reset with Sample Data'),
|
|
subtitle: const Text('Clear all data and add sample entries'),
|
|
onTap: () => _showReseedDataDialog(context),
|
|
),
|
|
]),
|
|
const SizedBox(height: 24),
|
|
_buildSettingsSection(context, 'Preferences', [
|
|
SwitchListTile(
|
|
secondary: const Icon(Icons.notifications),
|
|
title: const Text('Notifications'),
|
|
subtitle: const Text('Enable app notifications'),
|
|
value: _notificationsEnabled,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_notificationsEnabled = value;
|
|
});
|
|
_saveSettings();
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.language),
|
|
title: const Text('Language'),
|
|
subtitle: Text(_selectedLanguage),
|
|
onTap: () => _showLanguageDialog(context),
|
|
),
|
|
SwitchListTile(
|
|
secondary: const Icon(Icons.palette),
|
|
title: const Text('Dark Theme'),
|
|
subtitle: const Text('Use dark color scheme'),
|
|
value: _darkTheme,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_darkTheme = value;
|
|
});
|
|
_saveSettings();
|
|
_showThemeChangeMessage(context);
|
|
},
|
|
),
|
|
]),
|
|
const SizedBox(height: 24),
|
|
_buildSettingsSection(context, 'Statistics', [
|
|
ListTile(
|
|
leading: const Icon(Icons.analytics),
|
|
title: const Text('View Statistics'),
|
|
subtitle: const Text('See your coffee data insights'),
|
|
onTap: () => _showStatisticsDialog(context),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.refresh),
|
|
title: const Text('Refresh Data'),
|
|
subtitle: const Text('Reload all data from storage'),
|
|
onTap: () => _refreshData(context),
|
|
),
|
|
]),
|
|
const SizedBox(height: 24),
|
|
_buildSettingsSection(context, 'About', [
|
|
ListTile(
|
|
leading: const Icon(Icons.info),
|
|
title: const Text('About Coffee at Home'),
|
|
subtitle: const Text('Version 1.0.0'),
|
|
onTap: () => _showAboutDialog(context),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.help),
|
|
title: const Text('Help & Support'),
|
|
subtitle: const Text('Get help using the app'),
|
|
onTap: () => _showHelpDialog(context),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.star),
|
|
title: const Text('Rate App'),
|
|
subtitle: const Text('Rate us on the app store'),
|
|
onTap: () => _showRateDialog(context),
|
|
),
|
|
]),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSettingsSection(
|
|
BuildContext context,
|
|
String title,
|
|
List<Widget> children,
|
|
) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title, style: Theme.of(context).textTheme.titleLarge),
|
|
const SizedBox(height: 8),
|
|
Card(child: Column(children: children)),
|
|
],
|
|
);
|
|
}
|
|
|
|
void _exportData(BuildContext context) async {
|
|
try {
|
|
final appState = Provider.of<AppState>(context, listen: false);
|
|
|
|
final data = {
|
|
'beans': appState.beans.map((e) => e.toJson()).toList(),
|
|
'machines': appState.machines.map((e) => e.toJson()).toList(),
|
|
'recipes': appState.recipes.map((e) => e.toJson()).toList(),
|
|
'drinks': appState.drinks.map((e) => e.toJson()).toList(),
|
|
'journalEntries': appState.journalEntries.map((e) => e.toJson()).toList(),
|
|
'exportDate': DateTime.now().toIso8601String(),
|
|
'version': '1.0.0',
|
|
};
|
|
|
|
final jsonString = const JsonEncoder.withIndent(' ').convert(data);
|
|
|
|
// Show the JSON data to user
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Export Data'),
|
|
content: SingleChildScrollView(
|
|
child: SelectableText(
|
|
jsonString,
|
|
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Close'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Data exported successfully! Copy the JSON text to save.'),
|
|
),
|
|
);
|
|
} catch (e) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Export failed: $e')),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _importData(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
final controller = TextEditingController();
|
|
return AlertDialog(
|
|
title: const Text('Import Data'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text('Paste your exported JSON data below:'),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: controller,
|
|
decoration: const InputDecoration(
|
|
hintText: 'Paste JSON data here...',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
maxLines: 10,
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
final navigator = Navigator.of(context);
|
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
|
final appState = Provider.of<AppState>(context, listen: false);
|
|
|
|
try {
|
|
final jsonData = jsonDecode(controller.text) as Map<String, dynamic>;
|
|
|
|
// Validate the imported data structure
|
|
if (!jsonData.containsKey('beans') || !jsonData.containsKey('machines') ||
|
|
!jsonData.containsKey('recipes') || !jsonData.containsKey('journalEntries') ||
|
|
!jsonData.containsKey('drinks')) {
|
|
throw Exception('Invalid data format: Missing required fields');
|
|
}
|
|
|
|
// Clear existing data first
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.clear();
|
|
|
|
// Import beans
|
|
final beansData = jsonData['beans'] as List;
|
|
for (final beanJson in beansData) {
|
|
final bean = Bean.fromJson(beanJson);
|
|
await appState.addBean(bean);
|
|
}
|
|
|
|
// Import machines
|
|
final machinesData = jsonData['machines'] as List;
|
|
for (final machineJson in machinesData) {
|
|
final machine = Machine.fromJson(machineJson);
|
|
await appState.addMachine(machine);
|
|
}
|
|
|
|
// Import recipes
|
|
final recipesData = jsonData['recipes'] as List;
|
|
for (final recipeJson in recipesData) {
|
|
final recipe = Recipe.fromJson(recipeJson);
|
|
await appState.addRecipe(recipe);
|
|
}
|
|
|
|
// Import drinks
|
|
final drinksData = jsonData['drinks'] as List;
|
|
for (final drinkJson in drinksData) {
|
|
final drink = Drink.fromJson(drinkJson);
|
|
await appState.addDrink(drink);
|
|
}
|
|
|
|
// Import journal entries
|
|
final journalData = jsonData['journalEntries'] as List;
|
|
for (final journalJson in journalData) {
|
|
final journal = JournalEntry.fromJson(journalJson);
|
|
await appState.addJournalEntry(journal);
|
|
}
|
|
|
|
if (mounted) {
|
|
navigator.pop();
|
|
scaffoldMessenger.showSnackBar(
|
|
SnackBar(
|
|
content: Text('Successfully imported ${beansData.length} beans, ${machinesData.length} machines, ${recipesData.length} recipes, ${journalData.length} journal entries!'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
scaffoldMessenger.showSnackBar(
|
|
SnackBar(
|
|
content: Text('Import failed: $e'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
child: const Text('Import'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _showClearDataDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text('Clear All Data'),
|
|
content: const Text(
|
|
'Are you sure you want to delete all your coffee data? This action cannot be undone.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
onPressed: () async {
|
|
final navigator = Navigator.of(context);
|
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
|
final appState = Provider.of<AppState>(context, listen: false);
|
|
|
|
try {
|
|
// Clear all data using the app state
|
|
await appState.clearAllData();
|
|
|
|
if (mounted) {
|
|
navigator.pop();
|
|
scaffoldMessenger.showSnackBar(
|
|
const SnackBar(
|
|
content: Text('All data cleared successfully!'),
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
navigator.pop();
|
|
scaffoldMessenger.showSnackBar(
|
|
SnackBar(content: Text('Failed to clear data: $e')),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
child: const Text('Clear Data'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _showReseedDataDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text('Reset with Sample Data'),
|
|
content: const Text(
|
|
'This will clear all your current data and add sample coffee entries to get you started. This action cannot be undone.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Theme.of(context).primaryColor,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
onPressed: () async {
|
|
final navigator = Navigator.of(context);
|
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
|
final appState = Provider.of<AppState>(context, listen: false);
|
|
|
|
try {
|
|
// Show loading indicator
|
|
navigator.pop(); // Close dialog first
|
|
scaffoldMessenger.showSnackBar(
|
|
const SnackBar(
|
|
content: Row(
|
|
children: [
|
|
CircularProgressIndicator(strokeWidth: 2),
|
|
SizedBox(width: 16),
|
|
Text('Resetting data with samples...'),
|
|
],
|
|
),
|
|
duration: Duration(seconds: 3),
|
|
),
|
|
);
|
|
|
|
// Clear and reseed data
|
|
await appState.clearAndReseedData();
|
|
|
|
if (mounted) {
|
|
scaffoldMessenger.clearSnackBars();
|
|
scaffoldMessenger.showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Data reset with sample entries!'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
scaffoldMessenger.clearSnackBars();
|
|
scaffoldMessenger.showSnackBar(
|
|
SnackBar(
|
|
content: Text('Failed to reset data: $e'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
child: const Text('Reset Data'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _showLanguageDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Select Language'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: _languages.map((language) {
|
|
return RadioListTile<String>(
|
|
title: Text(language),
|
|
value: language,
|
|
groupValue: _selectedLanguage,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_selectedLanguage = value!;
|
|
});
|
|
_saveSettings();
|
|
Navigator.of(context).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Language changed to $value')),
|
|
);
|
|
},
|
|
);
|
|
}).toList(),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showThemeChangeMessage(BuildContext context) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Theme preference saved! Restart the app to see changes.'),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showStatisticsDialog(BuildContext context) {
|
|
final appState = Provider.of<AppState>(context, listen: false);
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Statistics'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Total Beans: ${appState.beans.length}'),
|
|
Text('Total Machines: ${appState.machines.length}'),
|
|
Text('Total Recipes: ${appState.recipes.length}'),
|
|
Text('Total Journal Entries: ${appState.journalEntries.length}'),
|
|
Text('Total Drinks: ${appState.drinks.length}'),
|
|
const SizedBox(height: 16),
|
|
Text('Preferred Beans: ${appState.preferredBeans.length}'),
|
|
Text('Preferred Drinks: ${appState.preferredDrinks.length}'),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Close'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _refreshData(BuildContext context) async {
|
|
try {
|
|
// In a real implementation, you would reload data from storage
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Data refreshed successfully!')),
|
|
);
|
|
} catch (e) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Failed to refresh data: $e')),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _showHelpDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Help & Support'),
|
|
content: const Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Coffee at Home - User Guide\n'),
|
|
Text('• Use the Beans section to track your coffee beans'),
|
|
Text('• Manage your equipment in the Machines section'),
|
|
Text('• Create and save brewing recipes'),
|
|
Text('• Record your daily coffee experiences in the Journal'),
|
|
Text('• Use the search feature to find anything quickly'),
|
|
Text('\nFor more help, visit our website or contact support.'),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Close'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showRateDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Rate Coffee at Home'),
|
|
content: const Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.favorite, size: 48, color: Colors.red),
|
|
SizedBox(height: 16),
|
|
Text('Enjoying Coffee at Home?'),
|
|
SizedBox(height: 8),
|
|
Text('Rate us on the app store and help other coffee lovers discover our app!'),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Maybe Later'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Thanks for your support!')),
|
|
);
|
|
},
|
|
child: const Text('Rate Now'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showAboutDialog(BuildContext context) {
|
|
showAboutDialog(
|
|
context: context,
|
|
applicationName: 'Coffee at Home',
|
|
applicationVersion: '1.0.0',
|
|
applicationIcon: const Icon(Icons.coffee, size: 48, color: Color(0xFFD4A574)),
|
|
children: [
|
|
const Text(
|
|
'A comprehensive coffee tracking app for managing your beans, equipment, recipes, and coffee journal.\n\n'
|
|
'Features:\n'
|
|
'• Track coffee beans and their origins\n'
|
|
'• Manage coffee equipment\n'
|
|
'• Create and save brewing recipes\n'
|
|
'• Keep a coffee journal with ratings\n'
|
|
'• Search across all your data\n'
|
|
'• Export and import your data\n\n'
|
|
'Built with Flutter and lots of ☕',
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|