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>
This commit is contained in:
2025-10-04 13:54:21 -04:00
commit 7be7b270e6
155 changed files with 13133 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
import 'package:hive/hive.dart';
part 'app_settings.g.dart';
@HiveType(typeId: 3)
class AppSettings extends HiveObject {
@HiveField(0)
String? discordWebhookUrl;
@HiveField(1)
bool expirationAlertsEnabled;
@HiveField(2)
bool discordNotificationsEnabled;
@HiveField(3)
String defaultView; // 'grid' or 'list'
@HiveField(4)
String sortBy; // 'expiration', 'name', 'location'
AppSettings({
this.discordWebhookUrl,
this.expirationAlertsEnabled = true,
this.discordNotificationsEnabled = false,
this.defaultView = 'grid',
this.sortBy = 'expiration',
});
}

View File

@@ -0,0 +1,53 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_settings.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class AppSettingsAdapter extends TypeAdapter<AppSettings> {
@override
final int typeId = 3;
@override
AppSettings read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return AppSettings(
discordWebhookUrl: fields[0] as String?,
expirationAlertsEnabled: fields[1] as bool,
discordNotificationsEnabled: fields[2] as bool,
defaultView: fields[3] as String,
sortBy: fields[4] as String,
);
}
@override
void write(BinaryWriter writer, AppSettings obj) {
writer
..writeByte(5)
..writeByte(0)
..write(obj.discordWebhookUrl)
..writeByte(1)
..write(obj.expirationAlertsEnabled)
..writeByte(2)
..write(obj.discordNotificationsEnabled)
..writeByte(3)
..write(obj.defaultView)
..writeByte(4)
..write(obj.sortBy);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppSettingsAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import '../../../core/constants/colors.dart';
class PrivacyPolicyScreen extends StatelessWidget {
const PrivacyPolicyScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Privacy Policy'),
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text(
'Sage - Kitchen Management',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Last Updated: October 4, 2025',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 24),
_buildSection(
'Data Collection',
'Sage is designed with your privacy in mind. All your data is stored locally on your device. We do not collect, transmit, or sell any personal information.',
),
_buildSection(
'Local Storage',
'Your inventory data, settings, and preferences are stored locally using Hive database. This data never leaves your device unless you explicitly choose to export it.',
),
_buildSection(
'Camera Permissions',
'The app requests camera permission only for barcode scanning functionality. No photos or videos are stored or transmitted.',
),
_buildSection(
'Internet Access',
'The app uses internet connection to:\n• Look up product information from public databases (Open Food Facts, UPCItemDB)\n• Send Discord notifications (only if you configure webhook)\n\nNo personal data is sent to these services except the barcode number for product lookup.',
),
_buildSection(
'Discord Integration',
'If you enable Discord notifications, you provide your own webhook URL. Notifications are sent directly from your device to your Discord server. We do not have access to or store your webhook URL on any server.',
),
_buildSection(
'Third-Party Services',
'The app may use the following third-party services:\n• Open Food Facts API - for product information\n• UPCItemDB API - for product information\n\nPlease review their respective privacy policies.',
),
_buildSection(
'Data Security',
'Your data is stored locally on your device and protected by your device\'s security measures. We recommend keeping your device secured with a password or biometric lock.',
),
_buildSection(
'Children\'s Privacy',
'Sage does not knowingly collect any information from children under 13. The app is designed for general household use.',
),
_buildSection(
'Changes to Privacy Policy',
'We may update this privacy policy from time to time. Any changes will be reflected in the app with an updated "Last Updated" date.',
),
_buildSection(
'Contact Us',
'If you have questions about this privacy policy, please open an issue on our GitHub repository or contact us through the app store.',
),
const SizedBox(height: 32),
],
),
);
}
Widget _buildSection(String title, String content) {
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
const SizedBox(height: 8),
Text(
content,
style: const TextStyle(
fontSize: 14,
height: 1.5,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,321 @@
import 'package:flutter/material.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 'privacy_policy_screen.dart';
import 'terms_of_service_screen.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
final _discordService = DiscordService();
AppSettings? _settings;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _loadSettings() async {
final settings = await HiveDatabase.getSettings();
setState(() {
_settings = settings;
_isLoading = false;
// Load Discord webhook into service
if (settings.discordWebhookUrl != null) {
_discordService.webhookUrl = settings.discordWebhookUrl;
}
});
}
Future<void> _saveSettings() async {
if (_settings != null) {
await _settings!.save();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: ListView(
children: [
const SizedBox(height: 16),
// Notifications Section
_buildSectionHeader('Notifications'),
SwitchListTile(
title: const Text('Expiration Alerts'),
subtitle: const Text('Get notified when items are expiring soon'),
value: _settings!.expirationAlertsEnabled,
onChanged: (value) {
setState(() => _settings!.expirationAlertsEnabled = value);
_saveSettings();
},
activeColor: AppColors.primary,
),
SwitchListTile(
title: const Text('Discord Notifications'),
subtitle: Text(_settings!.discordNotificationsEnabled
? 'Enabled - Tap to configure'
: 'Send alerts to Discord'),
value: _settings!.discordNotificationsEnabled,
onChanged: (value) {
if (value) {
_showDiscordSetup();
} else {
setState(() {
_settings!.discordNotificationsEnabled = false;
_settings!.discordWebhookUrl = null;
});
_saveSettings();
}
},
activeColor: AppColors.primary,
),
const Divider(),
// Display Section
_buildSectionHeader('Display'),
ListTile(
title: const Text('Default View'),
subtitle: const Text('Grid'),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
ListTile(
title: const Text('Sort By'),
subtitle: const Text('Expiration Date'),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
),
const Divider(),
// Data Section
_buildSectionHeader('Data'),
ListTile(
title: const Text('Export Data'),
subtitle: const Text('Export your inventory to CSV'),
leading: const Icon(Icons.file_download, color: AppColors.primary),
onTap: () {},
),
ListTile(
title: const Text('Clear All Data'),
subtitle: const Text('Delete all inventory items'),
leading: const Icon(Icons.delete_forever, color: AppColors.error),
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Clear All Data?'),
content: const Text(
'This will permanently delete all your inventory items. This action cannot be undone.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
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,
),
);
},
child: const Text(
'Clear',
style: TextStyle(color: AppColors.error),
),
),
],
),
);
},
),
const Divider(),
// About Section
_buildSectionHeader('About'),
const ListTile(
title: Text('App Name'),
subtitle: Text('Sage - Kitchen Management'),
),
const ListTile(
title: Text('Version'),
subtitle: Text('1.0.0'),
),
const ListTile(
title: Text('Developer'),
subtitle: Text('Built with ❤️ using Flutter'),
),
ListTile(
title: const Text('Privacy Policy'),
leading: const Icon(Icons.privacy_tip, color: AppColors.primary),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const PrivacyPolicyScreen(),
),
);
},
),
ListTile(
title: const Text('Terms of Service'),
leading: const Icon(Icons.description, color: AppColors.primary),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const TermsOfServiceScreen(),
),
);
},
),
ListTile(
title: const Text('Open Source Licenses'),
leading: const Icon(Icons.code, color: AppColors.primary),
trailing: const Icon(Icons.chevron_right),
onTap: () {
showLicensePage(
context: context,
applicationName: 'Sage',
applicationVersion: '1.0.0',
applicationIcon: const SageLeafIcon(
size: 64,
color: AppColors.primary,
),
);
},
),
const SizedBox(height: 32),
],
),
);
}
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
);
}
void _showDiscordSetup() {
final webhookController = TextEditingController(
text: _discordService.webhookUrl ?? '',
);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Discord Webhook Setup'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'To receive Discord notifications:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text('1. Go to your Discord server settings'),
const Text('2. Go to Integrations → Webhooks'),
const Text('3. Create a new webhook'),
const Text('4. Copy the webhook URL'),
const Text('5. Paste it below:'),
const SizedBox(height: 16),
TextField(
controller: webhookController,
decoration: const InputDecoration(
labelText: 'Webhook URL',
hintText: 'https://discord.com/api/webhooks/...',
border: OutlineInputBorder(),
),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
final webhookUrl = webhookController.text.trim();
_discordService.webhookUrl = webhookUrl;
// Test the webhook
final success = await _discordService.sendNotification(
title: '✅ Discord Connected!',
message: 'Sage kitchen management app is now connected to Discord.',
);
if (mounted) {
Navigator.pop(context);
if (success) {
// Save to Hive
setState(() {
_settings!.discordNotificationsEnabled = true;
_settings!.discordWebhookUrl = webhookUrl;
});
await _saveSettings();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Discord connected! Check your server.'),
backgroundColor: AppColors.success,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to connect. Check your webhook URL.'),
backgroundColor: AppColors.error,
),
);
}
}
},
child: const Text('Save & Test'),
),
],
),
);
}
}

View File

@@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import '../../../core/constants/colors.dart';
class TermsOfServiceScreen extends StatelessWidget {
const TermsOfServiceScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Terms of Service'),
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text(
'Sage - Terms of Service',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Last Updated: October 4, 2025',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 24),
_buildSection(
'Acceptance of Terms',
'By downloading, installing, or using the Sage app, you agree to be bound by these Terms of Service. If you do not agree to these terms, please do not use the app.',
),
_buildSection(
'Use of the App',
'Sage is a personal kitchen management tool designed to help you track food inventory and reduce waste. You may use the app for personal, non-commercial purposes.',
),
_buildSection(
'User Responsibilities',
'You are responsible for:\n• Maintaining the security of your device\n• Ensuring accuracy of data you enter\n• Complying with food safety guidelines\n• Backing up your data if needed\n\nThe app is a tool to assist you - always use your best judgment regarding food safety.',
),
_buildSection(
'Disclaimer of Warranties',
'THE APP IS PROVIDED "AS IS" WITHOUT WARRANTIES OF ANY KIND. We do not guarantee:\n• Accuracy of product information from third-party APIs\n• Accuracy of automatically suggested expiration dates\n• Prevention of food spoilage or food-borne illness\n\nAlways check food quality and safety yourself before consumption.',
),
_buildSection(
'Limitation of Liability',
'To the maximum extent permitted by law, we shall not be liable for any damages arising from:\n• Use or inability to use the app\n• Food spoilage or food-borne illness\n• Data loss\n• Reliance on expiration date estimates\n\nYou use the app at your own risk.',
),
_buildSection(
'Third-Party Services',
'The app uses third-party APIs (Open Food Facts, UPCItemDB) for product information. We are not responsible for the accuracy, availability, or content of these services.',
),
_buildSection(
'Discord Integration',
'If you choose to use Discord notifications:\n• You are responsible for your webhook URL security\n• We are not responsible for Discord service availability\n• You must comply with Discord\'s Terms of Service',
),
_buildSection(
'Intellectual Property',
'The Sage app and its original content are provided under an open-source license. Third-party services and APIs have their own terms and licenses.',
),
_buildSection(
'Changes to Terms',
'We reserve the right to modify these terms at any time. Continued use of the app after changes constitutes acceptance of the new terms.',
),
_buildSection(
'Termination',
'You may stop using the app at any time by uninstalling it. Your local data will be removed with the app.',
),
_buildSection(
'Governing Law',
'These terms shall be governed by and construed in accordance with applicable local laws.',
),
_buildSection(
'Contact',
'For questions about these Terms of Service, please contact us through the app store or GitHub repository.',
),
const SizedBox(height: 16),
const Text(
'⚠️ FOOD SAFETY REMINDER',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.error,
),
),
const SizedBox(height: 8),
const Text(
'This app is a tracking tool only. Always inspect food for signs of spoilage, follow proper food safety guidelines, and use your best judgment. When in doubt, throw it out!',
style: TextStyle(
fontSize: 14,
height: 1.5,
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 32),
],
),
);
}
Widget _buildSection(String title, String content) {
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
const SizedBox(height: 8),
Text(
content,
style: const TextStyle(
fontSize: 14,
height: 1.5,
),
),
],
),
);
}
}