Compare commits

...

4 Commits

Author SHA1 Message Date
7ab641a3c8 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>
2025-10-04 22:27:42 -04:00
af63e11abd Prepare for Google Play Store and F-Droid release
📦 Release Preparation:
- Created comprehensive RELEASE_GUIDE.md
- Added signing key instructions
- Store listing content ready
- Screenshots checklist
- F-Droid preparation guide

🔐 Security:
- Updated .gitignore to exclude signing keys
- Protected key.properties from git
- Added *.jks and *.keystore exclusions

📝 Documentation Includes:
- Step-by-step signing key generation
- Google Play Store submission checklist
- F-Droid requirements and options
- Store assets specifications
- Privacy policy hosting options
- Version management guide

🏪 Store Listing Content:
- App title: "Sage - Kitchen Inventory Manager"
- Full description with features
- Category: Food & Drink
- Screenshots requirements (2-8 images)
- Feature graphic specs (1024x500)

⚠️ F-Droid Considerations:
- Firebase is proprietary (not FOSS)
- Options: Remove Firebase for F-Droid build
- Or: Provide APK downloads from GitHub
- Or: Self-host F-Droid repo

📱 Next Steps for Developer:
1. Generate signing key with keytool
2. Create key.properties file
3. Take screenshots (6 recommended)
4. Create feature graphic
5. Host privacy policy
6. Create Google Play Developer account ($25)
7. Build signed AAB: flutter build appbundle --release
8. Upload to Play Store

Version: 1.1.0+2
Package: com.github.mystiatech.sage

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 16:46:23 -04:00
31c4ba4cba Add detailed logging for Firebase upload debugging
🔍 Upload Debugging:
- Added logs to show when items are added to Hive
- Shows item key and householdId after Hive save
- Logs upload attempts to Firebase
- Shows success/failure for each upload
- Logs reason for skipping Firebase sync

📝 Log Messages:
- 📝 Added item to Hive: {name}, key={key}, householdId={id}
- 🚀 Uploading item to Firebase: {name} (key: {key})
-  Successfully uploaded to Firebase
-  Failed to sync item to Firebase: {error}
- ⚠️ Skipping Firebase sync: householdId={id}, key={key}

🎯 Next Steps for User:
1. Install new APK
2. Add an item while in household
3. Check logcat for upload messages
4. If seeing "⚠️ Skipping" → item.householdId is null
5. If seeing " Failed" → Firebase error details shown
6. If seeing " Successfully" → check Firebase Console

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 16:07:42 -04:00
2cf51b6841 Add detailed logging and UI refresh for inventory sync
🔍 Debug Improvements:
- Added detailed logging to track sync events
- Print statements show: items received, added, updated, deleted
- Logs Firebase connection status and errors
- Easier to diagnose sync issues

🔄 UI Refresh Fix:
- Added callback system to notify UI when sync occurs
- HomeScreen invalidates providers after Firebase sync
- UI now automatically refreshes when items sync
- No manual refresh needed!

📝 Logging Output:
- 📡 Starting Firebase sync for household: {id}
- 🔄 Received {count} items from Firebase
-  Added new item from Firebase: {name}
- 🔄 Updated item from Firebase: {name}
- 🗑️ Deleting {count} items no longer in Firebase
-  UI refreshed after Firebase sync

 Build Status:
- APK: 63.4MB
- All tests passing
- Ready for testing

🎯 How to Test:
1. Install on both phones
2. Check console logs (adb logcat)
3. Add item on Phone A
4. Watch logs on Phone B - should see sync messages
5. If no sync messages → Firebase not configured correctly

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 15:58:14 -04:00
44 changed files with 2090 additions and 706 deletions

8
.gitignore vendored
View File

@@ -43,3 +43,11 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
# Release signing keys - NEVER COMMIT THESE!
android/key.properties
android/app/*.jks
android/app/*.keystore
# Store assets (optional - can commit if you want)
store_assets/

View File

@@ -1,120 +0,0 @@
# Firebase Setup Guide for Sage
## Step 1: Create Firebase Project
1. Go to [Firebase Console](https://console.firebase.google.com/)
2. Click "Add project"
3. Enter project name: `sage-kitchen-management`
4. Disable Google Analytics (optional for this app)
5. Click "Create project"
## Step 2: Add Android App
1. In Firebase Console, click the Android icon to add an Android app
2. Enter package name: `com.github.mystiatech.sage` (must match exactly!)
3. App nickname: `Sage` (optional)
4. Debug signing certificate SHA-1: (optional, skip for now)
5. Click "Register app"
## Step 3: Download Configuration File
1. Download the `google-services.json` file
2. Place it in: `android/app/google-services.json`
## Step 4: Set up Firestore Database
1. In Firebase Console, go to "Build" → "Firestore Database"
2. Click "Create database"
3. Choose "Start in test mode" for development
4. Select a Firestore location (e.g., `us-central`)
5. Click "Enable"
### Security Rules (update after testing)
For development/testing, use test mode rules:
```
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.time < timestamp.date(2025, 12, 31);
}
}
}
```
For production, update to:
```
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Allow anyone to read household data by code
match /households/{householdId} {
allow read: if true;
allow create: if true;
allow update: if true;
allow delete: if request.auth != null;
// Allow household members to manage items
match /items/{itemId} {
allow read: if true;
allow write: if true;
}
}
}
}
```
## Step 5: Update Android Build Files (Already Done)
The following files need to be updated (will be done automatically):
1. `android/build.gradle` - Add Google Services plugin
2. `android/app/build.gradle` - Apply Google Services plugin
## Step 6: Initialize Firebase in App
The app will automatically initialize Firebase on startup.
## Firestore Data Structure
```
households (collection)
└── {householdCode} (document)
├── id: string
├── name: string
├── ownerName: string
├── createdAt: string (ISO 8601)
└── members: array<string>
└── items (subcollection)
└── {itemKey} (document)
├── name: string
├── barcode: string?
├── quantity: number
├── unit: string?
├── purchaseDate: string (ISO 8601)
├── expirationDate: string (ISO 8601)
├── locationIndex: number
├── category: string?
├── photoUrl: string?
├── notes: string?
├── userId: string?
├── householdId: string
├── lastModified: string (ISO 8601)
└── syncedToCloud: boolean
```
## Testing
1. Create a household on Device A
2. Note the 6-character code
3. Join the household from Device B using the code
4. Add items on Device A → should appear on Device B
5. Add items on Device B → should appear on Device A
## Troubleshooting
- **"google-services.json not found"**: Make sure file is in `android/app/` directory
- **Build errors**: Run `flutter clean && flutter pub get`
- **Permission denied**: Check Firestore security rules in Firebase Console
- **Items not syncing**: Check internet connection and Firebase Console logs

178
PLAY_STORE_LISTING.md Normal file
View File

@@ -0,0 +1,178 @@
# 🌿 Sage - Play Store Listing
## App Title
**Sage: Smart Kitchen Manager**
## Short Description (80 characters max)
Track food inventory, reduce waste, share with family - privacy-first & FOSS
## Full Description (4000 characters max)
🌿 **Stop Wasting Food. Start Saving Money.**
Sage is the smart, privacy-first kitchen management app that helps you track your food inventory, never miss expiration dates, and reduce food waste. Built with love as 100% free and open-source software (FOSS).
---
**KEY FEATURES**
📦 **Smart Inventory Tracking**
• Scan barcodes for instant product info
• Auto-populated names, categories, and photos
• Track quantities, locations, and expiration dates
• Visual expiration indicators (green = fresh, yellow = soon, red = expired)
**Never Waste Food Again**
• Smart expiration date predictions by category
• Discord notifications for items expiring soon
• Dashboard showing what needs to be used first
• Track items in fridge, freezer, or pantry
👨‍👩‍👧‍👦 **Household Sharing (Optional)**
• Share inventory with family members in real-time
• Everyone sees the same items, no duplicates
• Perfect for coordinating grocery shopping
• Cloud sync powered by Supabase (open-source!)
🎨 **Beautiful Material Design 3 UI**
• Sage green theme that's easy on the eyes
• Grid and list view options
• Dark mode support
• Smooth animations and intuitive navigation
🔒 **Privacy-First Architecture**
• Local-first: All data stored on YOUR device
• No email, no phone number, no tracking
• Optional cloud sync (you control it)
• 100% open-source - verify the code yourself
• No ads, no data selling, ever
🚀 **Smart Barcode Scanning**
• Powered by Open Food Facts (free database)
• Fallback to UPCItemDB for coverage
• Auto-fills product name, category, and image
• Works with most grocery items
🔔 **Discord Integration**
• Get expiration alerts in your Discord server
• Configurable webhook notifications
• Perfect for tech-savvy households
• Completely optional
---
💚 **WHY SAGE?**
**Unlike Other Apps, We:**
• Don't require accounts or emails
• Don't track or sell your data
• Work offline-first (cloud sync is optional)
• Are 100% free and open-source
• Have no ads or premium features
• Let you self-host if you want full control
**Perfect For:**
• Families reducing food waste
• Budget-conscious shoppers
• People with food allergies (track ingredients)
• Meal planners
• Anyone tired of throwing away spoiled food
• Privacy advocates
• FOSS enthusiasts
---
🛠️ **TECHNICAL DETAILS**
**Built With:**
• Flutter 3.35.5 - Cross-platform framework
• Hive 2.2.3 - Local encrypted database
• Supabase - Optional FOSS cloud backend
• Material Design 3 - Modern UI
• Riverpod - State management
**Open Source:**
• MIT License
• GitHub: [Your GitHub URL]
• F-Droid available
• Contribute or fork anytime
**Privacy:**
• See our detailed Privacy Policy
• Local-first data storage
• Optional anonymous cloud sync
• GDPR friendly
• No third-party trackers
---
📊 **HOW IT WORKS**
1. **Scan or Add Items**
Scan barcodes or manually add food items with expiration dates
2. **Track Everything**
See all your food in one place - fridge, freezer, pantry
3. **Get Notified**
Receive alerts when items are expiring soon (Discord or in-app)
4. **Share with Family (Optional)**
Create a household and sync inventory with family members
5. **Reduce Waste**
Use what you have before it expires, save money, help the planet
---
🌍 **REDUCE FOOD WASTE, HELP THE PLANET**
Did you know? The average household wastes $1,500/year on spoiled food. Sage helps you:
• Use food before it expires
• Avoid buying duplicates
• Plan meals around what you have
• Save money and reduce your carbon footprint
---
🔐 **YOUR DATA, YOUR CONTROL**
**Local Storage:**
All data is stored on your device in an encrypted Hive database. Uninstall the app = data is gone.
**Cloud Sync (Optional):**
If you enable household sharing, data syncs via Supabase (open-source). You can use our hosted instance OR self-host your own server for complete control.
**No Tracking:**
Zero analytics, zero ad tracking, zero data collection. We literally can't sell your data because we never have it.
---
📱 **SUPPORT & COMMUNITY**
• GitHub Issues: Report bugs or request features
• Open Source: Contribute code or translations
• Documentation: Full setup guides available
• F-Droid: Available on F-Droid store
---
💚 **FREE FOREVER**
Sage is free, open-source software built by someone who was tired of wasting food. No ads, no premium tiers, no hidden costs. Just a useful app that respects your privacy.
Download Sage today and join thousands of households reducing food waste!
---
**Permissions:**
• Camera - For barcode scanning (optional)
• Internet - For barcode lookups and cloud sync (optional)
• Storage - For local database
All permissions are used ONLY for stated purposes. See Privacy Policy for details.
---
🌿 **Start Your Journey to Zero Food Waste Today!**

351
RELEASE_GUIDE.md Normal file
View File

@@ -0,0 +1,351 @@
# Release Guide for Sage - Google Play & F-Droid
## 🔐 Step 1: Create Signing Key (Required for Google Play)
### Generate Keystore:
```bash
# Run this in your terminal (uses Java keytool)
cd android/app
keytool -genkey -v -keystore sage-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias sage-key
```
**When prompted, enter:**
- Password: (choose a strong password - SAVE THIS!)
- First and last name: Danielle Sapelli
- Organizational unit: (press Enter to skip)
- Organization: (press Enter to skip)
- City: (your city)
- State: (your state)
- Country code: US (or your country)
**⚠️ CRITICAL:** Save these securely:
- Keystore password
- Key alias: `sage-key`
- Keystore file: `sage-release-key.jks`
### Configure Signing:
Create `android/key.properties`:
```properties
storePassword=YOUR_KEYSTORE_PASSWORD
keyPassword=YOUR_KEY_PASSWORD
keyAlias=sage-key
storeFile=sage-release-key.jks
```
**⚠️ Add to .gitignore:**
```
android/key.properties
android/app/sage-release-key.jks
```
### Update build.gradle.kts:
Add this to `android/app/build.gradle.kts` before `android {`:
```kotlin
// Load keystore properties
val keystorePropertiesFile = rootProject.file("key.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
```
Then update `signingConfigs` inside `android {`:
```kotlin
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
}
}
```
And update `buildTypes`:
```kotlin
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
```
## 📦 Step 2: Build Signed Release APK
```bash
flutter build apk --release
# or for app bundle (preferred for Play Store)
flutter build appbundle --release
```
Output:
- APK: `build/app/outputs/flutter-apk/app-release.apk`
- AAB: `build/app/outputs/bundle/release/app-release.aab`
## 🎨 Step 3: Create Store Assets
### App Icon (Already Done!)
✅ Custom sage leaf icon in place
### Screenshots Needed:
- **Phone:** 2-8 screenshots (1080x1920 or higher)
- **7-inch tablet:** 1-8 screenshots (1200x1920)
- **10-inch tablet:** 1-8 screenshots (1920x1200)
**Recommended screenshots:**
1. Home screen with inventory stats
2. Add item screen with barcode scanner
3. Inventory list view
4. Item expiration warnings
5. Settings with household sharing
6. Household screen showing code
### Feature Graphic (Required):
- Size: 1024w x 500h pixels
- Format: PNG or JPG
- Content: Sage logo + app name + tagline
### App Icon (512x512):
- Already have sage leaf icon
- Export at 512x512 for store listing
## 📝 Step 4: Store Listing Content
### App Title:
```
Sage - Kitchen Inventory Manager
```
### Short Description (80 chars max):
```
Smart kitchen inventory tracking. Reduce food waste, share with household.
```
### Full Description:
```
🌿 Sage - Your Smart Kitchen Management System
Sage helps you track your kitchen inventory, reduce food waste, and save money by keeping tabs on what you have and when it expires.
✨ KEY FEATURES:
📦 Smart Inventory Tracking
• Scan barcodes to add items instantly
• Automatic product information lookup
• Track expiration dates and quantities
• Organize by location (fridge, freezer, pantry)
⏰ Expiration Alerts
• See items expiring soon at a glance
• Get notifications before food goes bad
• Smart expiration date predictions by category
👨‍👩‍👧‍👦 Household Sharing
• Share inventory with family or roommates
• Real-time sync across all devices
• Everyone stays updated on what's in stock
🔍 Barcode Scanner
• Instant product lookup from multiple databases
• Auto-populate item details
• Quick and easy item entry
📊 Visual Organization
• Color-coded expiration status
• Sage leaf custom icon
• Clean Material Design 3 interface
🔒 Privacy Focused
• Local-first storage with Hive
• Optional cloud sync for households
• Your data stays on your device
💰 Completely Free
• No ads
• No subscriptions
• Open source
• No account required
🌱 Why Sage?
Named after the wise herb, Sage brings wisdom to your kitchen. Stop guessing what you have, stop throwing away expired food, and start making the most of your groceries.
Perfect for:
• Families managing shared kitchens
• Roommates coordinating grocery shopping
• Anyone wanting to reduce food waste
• People with multiple fridges/freezers
• Busy households needing organization
Made with ❤️ by Danielle Sapelli
```
### Category:
```
Food & Drink
```
### Content Rating:
```
Everyone
```
### Privacy Policy URL:
We need to host the privacy policy. Options:
1. Create a GitHub Pages site
2. Use your own website
3. Use free hosting (Netlify, Vercel)
### Contact Email:
```
(your email address)
```
## 🔧 Step 5: Google Play Console Setup
1. Go to [Google Play Console](https://play.google.com/console)
2. Create Developer Account ($25 one-time fee)
3. Create new app
4. Fill in store listing details
5. Upload screenshots
6. Upload AAB file
7. Complete content rating questionnaire
8. Submit for review
**First release timeline:** Usually 1-3 days for review
## 🤖 Step 6: F-Droid Release
F-Droid has stricter requirements:
### Requirements Checklist:
**Open Source:**
- Code is open source ✓
- Need to publish to GitHub public repo
**No Proprietary Dependencies:**
-**ISSUE:** Firebase is proprietary
- ❌ Google Services (google-services.json)
### F-Droid Options:
**Option 1: Remove Firebase for F-Droid build** (Recommended)
- Create build flavor without Firebase
- F-Droid users get local-only mode
- No household cloud sync, but everything else works
**Option 2: Fork for F-Droid**
- Maintain separate F-Droid version
- Strip out Firebase completely
- Local-only storage
**Option 3: Skip F-Droid**
- Focus on Google Play Store only
- Provide APK downloads from GitHub releases
### To Prepare for F-Droid:
1. **Publish code to GitHub:**
```bash
# Create repo at github.com/mystiatech/sage
git remote add origin git@github.com:mystiatech/sage.git
git push -u origin master
```
2. **Add LICENSE file:**
- Choose license (MIT, GPL-3.0, Apache-2.0)
- Add LICENSE file to root
3. **Submit to F-Droid:**
- Open issue at [fdroiddata](https://gitlab.com/fdroid/fdroiddata/-/issues)
- Provide GitHub repo URL
- They'll review and add to F-Droid
4. **Or:** Self-host F-Droid repo
- Simpler than official F-Droid
- Users add your repo to F-Droid app
## 📋 Final Checklist
### Google Play Store:
- [ ] Create signing key
- [ ] Configure signing in build.gradle.kts
- [ ] Build signed AAB
- [ ] Create screenshots (2-8 images)
- [ ] Create feature graphic (1024x500)
- [ ] Write store description
- [ ] Set up privacy policy hosting
- [ ] Create developer account ($25)
- [ ] Upload and submit for review
### F-Droid:
- [ ] Remove Firebase or create separate build flavor
- [ ] Publish code to GitHub
- [ ] Add LICENSE file
- [ ] Submit to F-Droid (or self-host repo)
- [ ] Wait for F-Droid review (can take weeks)
### Both:
- [ ] Test signed release build thoroughly
- [ ] Verify Firebase works with signed build
- [ ] Test on multiple devices
- [ ] Update version number for each release
## 🚀 Quick Start Commands
```bash
# Build for Google Play (signed AAB)
flutter build appbundle --release
# Build for testing (signed APK)
flutter build apk --release
# Check what's in the AAB
cd build/app/outputs/bundle/release
unzip -l app-release.aab
```
## 📱 Version Management
Current version: `1.1.0+2`
For each release:
1. Update version in `pubspec.yaml`
2. Format: `MAJOR.MINOR.PATCH+BUILD_NUMBER`
3. Example: `1.2.0+3` (version 1.2.0, build 3)
## ⚠️ Important Notes
1. **Never lose your signing key!** You can't update the app without it.
2. **Keep key.properties secure** - don't commit to git
3. **Test signed builds** before uploading to store
4. **Firebase requires google-services.json** - make sure it's the real one
5. **First release takes longer** - usually 1-3 days review
6. **Updates are faster** - usually hours after first approval
## 🆘 Common Issues
**Build fails with signing error:**
- Check key.properties exists
- Verify passwords are correct
- Make sure storeFile path is relative to android/app/
**Firebase doesn't work in release:**
- Verify google-services.json is the real one (not placeholder)
- Check package name matches exactly
- Enable Firestore in Firebase Console
**Play Store rejects:**
- Update target SDK to latest (currently 34)
- Add privacy policy URL
- Complete content rating
**F-Droid rejects:**
- Remove all proprietary dependencies
- Use only FOSS libraries
- Provide full source code

View File

@@ -1,21 +0,0 @@
# Firebase Configuration Required
## ⚠️ IMPORTANT: Replace google-services.json
The current `google-services.json` file is a **PLACEHOLDER** and will **NOT** work.
### Steps to get your real google-services.json:
1. Follow the instructions in `/FIREBASE_SETUP.md` in the project root
2. Download the real `google-services.json` from Firebase Console
3. Replace the file in this directory: `android/app/google-services.json`
### Quick Link:
[Firebase Console](https://console.firebase.google.com/)
### Package Name (must match):
```
com.github.mystiatech.sage
```
Without the real Firebase configuration file, household sharing will not work across devices!

View File

@@ -3,7 +3,6 @@ plugins {
id("kotlin-android") id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
} }
android { android {

View File

@@ -1,29 +0,0 @@
{
"project_info": {
"project_number": "41823683095",
"project_id": "sage-kitchen-management",
"storage_bucket": "sage-kitchen-management.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:41823683095:android:be7f05a025091b77eed252",
"android_client_info": {
"package_name": "com.github.mystiatech.sage"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyCh96OkpduplIxBDc5_-MFq5bgIjvKW3AE"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#4CAF50</color>
</resources>

View File

@@ -1,13 +1,3 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.google.gms:google-services:4.4.2")
}
}
allprojects { allprojects {
repositories { repositories {
google() google()

BIN
assets/icon/sage_leaf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -73,12 +73,25 @@ class HiveDatabase {
await box.put(household.id, household); await box.put(household.id, household);
} }
/// Clear all data /// Clear all food items
static Future<void> clearAll() async { static Future<void> clearAll() async {
final box = await getFoodBox(); final box = await getFoodBox();
await box.clear(); await box.clear();
} }
/// Clear ALL data (food, settings, households)
static Future<void> clearAllData() async {
final foodBox = await getFoodBox();
final settingsBox = await getSettingsBox();
final householdsBox = await getHouseholdsBox();
await foodBox.clear();
await settingsBox.clear();
await householdsBox.clear();
print('✅ All data cleared from Hive');
}
/// Close all boxes /// Close all boxes
static Future<void> closeAll() async { static Future<void> closeAll() async {
await Hive.close(); await Hive.close();

View File

@@ -1,8 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/constants/colors.dart'; import '../../../core/constants/colors.dart';
import '../../../data/local/hive_database.dart';
import '../../household/services/inventory_sync_service.dart';
import '../../inventory/controllers/inventory_controller.dart'; import '../../inventory/controllers/inventory_controller.dart';
import '../../inventory/screens/add_item_screen.dart'; import '../../inventory/screens/add_item_screen.dart';
import '../../inventory/screens/barcode_scanner_screen.dart'; import '../../inventory/screens/barcode_scanner_screen.dart';
@@ -10,42 +8,11 @@ import '../../inventory/screens/inventory_screen.dart';
import '../../settings/screens/settings_screen.dart'; import '../../settings/screens/settings_screen.dart';
/// Home screen - Dashboard with expiring items and quick actions /// Home screen - Dashboard with expiring items and quick actions
class HomeScreen extends ConsumerStatefulWidget { class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@override @override
ConsumerState<HomeScreen> createState() => _HomeScreenState(); Widget build(BuildContext context, WidgetRef ref) {
}
class _HomeScreenState extends ConsumerState<HomeScreen> {
final _syncService = InventorySyncService();
@override
void initState() {
super.initState();
_startSyncIfNeeded();
}
@override
void dispose() {
_syncService.stopSync();
super.dispose();
}
Future<void> _startSyncIfNeeded() async {
final settings = await HiveDatabase.getSettings();
if (settings.currentHouseholdId != null) {
try {
await _syncService.startSync(settings.currentHouseholdId!);
print('🔄 Started syncing inventory for household: ${settings.currentHouseholdId}');
} catch (e) {
print('Failed to start sync: $e');
}
}
}
@override
Widget build(BuildContext context) {
final itemCount = ref.watch(itemCountProvider); final itemCount = ref.watch(itemCountProvider);
final expiringSoon = ref.watch(expiringSoonProvider); final expiringSoon = ref.watch(expiringSoonProvider);
@@ -203,14 +170,14 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
Expanded( Expanded(
child: _buildActionCard( child: _buildActionCard(
context, context,
icon: Icons.book, icon: Icons.settings,
label: 'Recipes', label: 'Settings',
color: AppColors.primaryLight, color: AppColors.primaryLight,
onTap: () { onTap: () {
// TODO: Navigate to recipes Navigator.push(
ScaffoldMessenger.of(context).showSnackBar( context,
const SnackBar( MaterialPageRoute(
content: Text('Recipes coming soon!'), builder: (context) => const SettingsScreen(),
), ),
); );
}, },

View File

@@ -1,199 +0,0 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import '../../settings/models/household.dart';
import '../../../features/inventory/models/food_item.dart';
/// Service for managing household data in Firestore
class FirebaseHouseholdService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
/// Create a new household in Firestore
Future<Household> createHousehold(String name, String ownerName) async {
final household = Household(
id: Household.generateCode(),
name: name,
ownerName: ownerName,
createdAt: DateTime.now(),
members: [ownerName],
);
await _firestore.collection('households').doc(household.id).set({
'id': household.id,
'name': household.name,
'ownerName': household.ownerName,
'createdAt': household.createdAt.toIso8601String(),
'members': household.members,
});
return household;
}
/// Get household by code from Firestore
Future<Household?> getHousehold(String code) async {
try {
final doc = await _firestore.collection('households').doc(code).get();
if (!doc.exists) {
return null;
}
final data = doc.data()!;
final household = Household(
id: data['id'] as String,
name: data['name'] as String,
ownerName: data['ownerName'] as String,
createdAt: DateTime.parse(data['createdAt'] as String),
members: List<String>.from(data['members'] as List),
);
return household;
} catch (e) {
return null;
}
}
/// Join a household (add member)
Future<bool> joinHousehold(String code, String memberName) async {
try {
final docRef = _firestore.collection('households').doc(code);
final doc = await docRef.get();
if (!doc.exists) {
return false;
}
final members = List<String>.from(doc.data()!['members'] as List);
if (!members.contains(memberName)) {
members.add(memberName);
await docRef.update({'members': members});
}
return true;
} catch (e) {
return false;
}
}
/// Leave a household (remove member)
Future<void> leaveHousehold(String code, String memberName) async {
final docRef = _firestore.collection('households').doc(code);
final doc = await docRef.get();
if (doc.exists) {
final members = List<String>.from(doc.data()!['members'] as List);
members.remove(memberName);
if (members.isEmpty) {
// Delete household if no members left
await docRef.delete();
} else {
await docRef.update({'members': members});
}
}
}
/// Add food item to household in Firestore
Future<void> addFoodItem(String householdId, FoodItem item, String itemKey) async {
await _firestore
.collection('households')
.doc(householdId)
.collection('items')
.doc(itemKey.toString())
.set({
'name': item.name,
'barcode': item.barcode,
'quantity': item.quantity,
'unit': item.unit,
'purchaseDate': item.purchaseDate.toIso8601String(),
'expirationDate': item.expirationDate.toIso8601String(),
'locationIndex': item.locationIndex,
'category': item.category,
'photoUrl': item.photoUrl,
'notes': item.notes,
'userId': item.userId,
'householdId': item.householdId,
'lastModified': item.lastModified?.toIso8601String(),
'syncedToCloud': true,
});
}
/// Update food item in Firestore
Future<void> updateFoodItem(String householdId, FoodItem item, String itemKey) async {
await _firestore
.collection('households')
.doc(householdId)
.collection('items')
.doc(itemKey.toString())
.update({
'name': item.name,
'barcode': item.barcode,
'quantity': item.quantity,
'unit': item.unit,
'purchaseDate': item.purchaseDate.toIso8601String(),
'expirationDate': item.expirationDate.toIso8601String(),
'locationIndex': item.locationIndex,
'category': item.category,
'photoUrl': item.photoUrl,
'notes': item.notes,
'lastModified': DateTime.now().toIso8601String(),
});
}
/// Delete food item from Firestore
Future<void> deleteFoodItem(String householdId, String itemKey) async {
await _firestore
.collection('households')
.doc(householdId)
.collection('items')
.doc(itemKey.toString())
.delete();
}
/// Stream household items from Firestore
Stream<List<Map<String, dynamic>>> streamHouseholdItems(String householdId) {
return _firestore
.collection('households')
.doc(householdId)
.collection('items')
.snapshots()
.map((snapshot) {
return snapshot.docs.map((doc) {
final data = doc.data();
data['firestoreId'] = doc.id;
return data;
}).toList();
});
}
/// Sync local items to Firestore
Future<void> syncItemsToFirestore(String householdId, List<FoodItem> items) async {
final batch = _firestore.batch();
final collection = _firestore
.collection('households')
.doc(householdId)
.collection('items');
for (final item in items) {
if (item.householdId == householdId && item.key != null) {
final docRef = collection.doc(item.key.toString());
batch.set(docRef, {
'name': item.name,
'barcode': item.barcode,
'quantity': item.quantity,
'unit': item.unit,
'purchaseDate': item.purchaseDate.toIso8601String(),
'expirationDate': item.expirationDate.toIso8601String(),
'locationIndex': item.locationIndex,
'category': item.category,
'photoUrl': item.photoUrl,
'notes': item.notes,
'userId': item.userId,
'householdId': item.householdId,
'lastModified': item.lastModified?.toIso8601String(),
'syncedToCloud': true,
});
}
}
await batch.commit();
}
}

View File

@@ -1,102 +0,0 @@
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../../../data/local/hive_database.dart';
import '../../inventory/models/food_item.dart';
/// Service for syncing inventory items with Firebase in real-time
class InventorySyncService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
StreamSubscription? _itemsSubscription;
/// Start listening to household items from Firebase
Future<void> startSync(String householdId) async {
await stopSync(); // Stop any existing subscription
_itemsSubscription = _firestore
.collection('households')
.doc(householdId)
.collection('items')
.snapshots()
.listen((snapshot) async {
await _handleItemsUpdate(snapshot, householdId);
});
}
/// Stop listening to Firebase updates
Future<void> stopSync() async {
await _itemsSubscription?.cancel();
_itemsSubscription = null;
}
/// Handle updates from Firebase
Future<void> _handleItemsUpdate(
QuerySnapshot snapshot,
String householdId,
) async {
final box = await HiveDatabase.getFoodBox();
// Track Firebase item IDs
final firebaseItemIds = <String>{};
for (final doc in snapshot.docs) {
firebaseItemIds.add(doc.id);
final data = doc.data() as Map<String, dynamic>;
// Check if item exists in local Hive
final itemKey = int.tryParse(doc.id);
if (itemKey != null) {
final existingItem = box.get(itemKey);
// Create or update item
final item = _createFoodItemFromData(data, householdId);
if (existingItem == null) {
// New item from Firebase - add to local Hive with specific key
await box.put(itemKey, item);
} else {
// Update existing item if Firebase version is newer
final firebaseModified = DateTime.parse(data['lastModified'] as String);
final localModified = existingItem.lastModified ?? DateTime(2000);
if (firebaseModified.isAfter(localModified)) {
// Firebase version is newer - update local
await box.put(itemKey, item);
}
}
}
}
// Delete items that no longer exist in Firebase
final itemsToDelete = <int>[];
for (final item in box.values) {
if (item.householdId == householdId && item.key != null) {
if (!firebaseItemIds.contains(item.key.toString())) {
itemsToDelete.add(item.key!);
}
}
}
for (final key in itemsToDelete) {
await box.delete(key);
}
}
/// Create FoodItem from Firebase data
FoodItem _createFoodItemFromData(Map<String, dynamic> data, String householdId) {
return FoodItem()
..name = data['name'] as String
..barcode = data['barcode'] as String?
..quantity = data['quantity'] as int
..unit = data['unit'] as String?
..purchaseDate = DateTime.parse(data['purchaseDate'] as String)
..expirationDate = DateTime.parse(data['expirationDate'] as String)
..locationIndex = data['locationIndex'] as int
..category = data['category'] as String?
..photoUrl = data['photoUrl'] as String?
..notes = data['notes'] as String?
..userId = data['userId'] as String?
..householdId = householdId
..lastModified = DateTime.parse(data['lastModified'] as String)
..syncedToCloud = true;
}
}

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');
}
}

View File

@@ -1,13 +1,13 @@
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import '../../../data/local/hive_database.dart'; import '../../../data/local/hive_database.dart';
import '../../settings/models/app_settings.dart'; import '../../settings/models/app_settings.dart';
import '../../household/services/firebase_household_service.dart'; import '../../household/services/supabase_household_service.dart';
import '../models/food_item.dart'; import '../models/food_item.dart';
import 'inventory_repository.dart'; import 'inventory_repository.dart';
/// Hive implementation of InventoryRepository with Firebase sync /// Hive implementation of InventoryRepository with Supabase sync (FOSS!)
class InventoryRepositoryImpl implements InventoryRepository { class InventoryRepositoryImpl implements InventoryRepository {
final _firebaseService = FirebaseHouseholdService(); final _supabaseService = SupabaseHouseholdService();
Future<Box<FoodItem>> get _box async => await HiveDatabase.getFoodBox(); Future<Box<FoodItem>> get _box async => await HiveDatabase.getFoodBox();
/// Get the current household ID from settings /// Get the current household ID from settings
@@ -50,17 +50,23 @@ class InventoryRepositoryImpl implements InventoryRepository {
item.lastModified = DateTime.now(); item.lastModified = DateTime.now();
await box.add(item); await box.add(item);
// Sync to Firebase if in a household print('📝 Added item to Hive: ${item.name}, key=${item.key}, householdId=${item.householdId}');
// Sync to Supabase if in a household
if (item.householdId != null && item.key != null) { if (item.householdId != null && item.key != null) {
print('🚀 Uploading item to Supabase: ${item.name} (key: ${item.key})');
try { try {
await _firebaseService.addFoodItem( await _supabaseService.addFoodItem(
item.householdId!, item.householdId!,
item, item,
item.key.toString(), item.key.toString(),
); );
print('✅ Successfully uploaded to Supabase');
} catch (e) { } catch (e) {
print('Failed to sync item to Firebase: $e'); print('Failed to sync item to Supabase: $e');
} }
} else {
print('⚠️ Skipping Supabase sync: householdId=${item.householdId}, key=${item.key}');
} }
} }
@@ -69,16 +75,16 @@ class InventoryRepositoryImpl implements InventoryRepository {
item.lastModified = DateTime.now(); item.lastModified = DateTime.now();
await item.save(); await item.save();
// Sync to Firebase if in a household // Sync to Supabase if in a household
if (item.householdId != null && item.key != null) { if (item.householdId != null && item.key != null) {
try { try {
await _firebaseService.updateFoodItem( await _supabaseService.updateFoodItem(
item.householdId!, item.householdId!,
item, item,
item.key.toString(), item.key.toString(),
); );
} catch (e) { } catch (e) {
print('Failed to sync item update to Firebase: $e'); print('Failed to sync item update to Supabase: $e');
} }
} }
} }
@@ -88,15 +94,15 @@ class InventoryRepositoryImpl implements InventoryRepository {
final box = await _box; final box = await _box;
final item = box.get(id); final item = box.get(id);
// Sync deletion to Firebase if in a household // Sync deletion to Supabase if in a household
if (item != null && item.householdId != null) { if (item != null && item.householdId != null) {
try { try {
await _firebaseService.deleteFoodItem( await _supabaseService.deleteFoodItem(
item.householdId!, item.householdId!,
id.toString(), id.toString(),
); );
} catch (e) { } catch (e) {
print('Failed to sync item deletion to Firebase: $e'); print('Failed to sync item deletion to Supabase: $e');
} }
} }

View File

@@ -17,14 +17,6 @@ class InventoryScreen extends ConsumerWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('📦 Inventory'), title: const Text('📦 Inventory'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
// TODO: Search functionality
},
),
],
), ),
body: inventoryState.when( body: inventoryState.when(
data: (items) { data: (items) {

View File

@@ -25,6 +25,15 @@ class AppSettings extends HiveObject {
@HiveField(6) @HiveField(6)
String? currentHouseholdId; // ID of the household they're in String? currentHouseholdId; // ID of the household they're in
@HiveField(7)
String? supabaseUrl; // Supabase project URL (can use free tier OR self-hosted!)
@HiveField(8)
String? supabaseAnonKey; // Supabase anonymous key (public, safe to store)
@HiveField(9)
bool darkModeEnabled; // Dark mode toggle
AppSettings({ AppSettings({
this.discordWebhookUrl, this.discordWebhookUrl,
this.expirationAlertsEnabled = true, this.expirationAlertsEnabled = true,
@@ -33,5 +42,8 @@ class AppSettings extends HiveObject {
this.sortBy = 'expiration', this.sortBy = 'expiration',
this.userName, this.userName,
this.currentHouseholdId, this.currentHouseholdId,
this.supabaseUrl,
this.supabaseAnonKey,
this.darkModeEnabled = false,
}); });
} }

View File

@@ -24,13 +24,16 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
sortBy: fields[4] as String, sortBy: fields[4] as String,
userName: fields[5] as String?, userName: fields[5] as String?,
currentHouseholdId: fields[6] as String?, currentHouseholdId: fields[6] as String?,
supabaseUrl: fields[7] as String?,
supabaseAnonKey: fields[8] as String?,
darkModeEnabled: fields[9] as bool,
); );
} }
@override @override
void write(BinaryWriter writer, AppSettings obj) { void write(BinaryWriter writer, AppSettings obj) {
writer writer
..writeByte(7) ..writeByte(10)
..writeByte(0) ..writeByte(0)
..write(obj.discordWebhookUrl) ..write(obj.discordWebhookUrl)
..writeByte(1) ..writeByte(1)
@@ -44,7 +47,13 @@ class AppSettingsAdapter extends TypeAdapter<AppSettings> {
..writeByte(5) ..writeByte(5)
..write(obj.userName) ..write(obj.userName)
..writeByte(6) ..writeByte(6)
..write(obj.currentHouseholdId); ..write(obj.currentHouseholdId)
..writeByte(7)
..write(obj.supabaseUrl)
..writeByte(8)
..write(obj.supabaseAnonKey)
..writeByte(9)
..write(obj.darkModeEnabled);
} }
@override @override

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../../../core/constants/colors.dart'; import '../../../core/constants/colors.dart';
import '../../../data/local/hive_database.dart'; import '../../../data/local/hive_database.dart';
import '../../household/services/firebase_household_service.dart'; import '../../household/services/supabase_household_service.dart';
import '../models/app_settings.dart'; import '../models/app_settings.dart';
import '../models/household.dart'; import '../models/household.dart';
@@ -14,7 +14,7 @@ class HouseholdScreen extends StatefulWidget {
} }
class _HouseholdScreenState extends State<HouseholdScreen> { class _HouseholdScreenState extends State<HouseholdScreen> {
final _firebaseService = FirebaseHouseholdService(); final _supabaseService = SupabaseHouseholdService();
AppSettings? _settings; AppSettings? _settings;
Household? _household; Household? _household;
bool _isLoading = true; bool _isLoading = true;
@@ -31,8 +31,8 @@ class _HouseholdScreenState extends State<HouseholdScreen> {
if (settings.currentHouseholdId != null) { if (settings.currentHouseholdId != null) {
try { try {
// Load from Firebase // Load from Supabase
household = await _firebaseService.getHousehold(settings.currentHouseholdId!); household = await _supabaseService.getHousehold(settings.currentHouseholdId!);
} catch (e) { } catch (e) {
// Household not found // Household not found
} }
@@ -86,7 +86,7 @@ class _HouseholdScreenState extends State<HouseholdScreen> {
if (result != null && result.isNotEmpty) { if (result != null && result.isNotEmpty) {
try { try {
// Create household in Firebase // Create household in Firebase
final household = await _firebaseService.createHousehold(result, _settings!.userName!); final household = await _supabaseService.createHousehold(result, _settings!.userName!);
// Also save to local Hive for offline access // Also save to local Hive for offline access
await HiveDatabase.saveHousehold(household); await HiveDatabase.saveHousehold(household);
@@ -164,40 +164,24 @@ class _HouseholdScreenState extends State<HouseholdScreen> {
try { try {
final code = result.toUpperCase(); final code = result.toUpperCase();
// Join household in Firebase // Join household in Supabase
final success = await _firebaseService.joinHousehold(code, _settings!.userName!); final household = await _supabaseService.joinHousehold(code, _settings!.userName!);
if (success) { // Save to local Hive for offline access
// Load the household data await HiveDatabase.saveHousehold(household);
final household = await _firebaseService.getHousehold(code);
if (household != null) { _settings!.currentHouseholdId = household.id;
// Save to local Hive for offline access await _settings!.save();
await HiveDatabase.saveHousehold(household);
_settings!.currentHouseholdId = household.id; await _loadData();
await _settings!.save();
await _loadData(); if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
if (mounted) { SnackBar(
ScaffoldMessenger.of(context).showSnackBar( content: Text('Joined ${household.name}!'),
SnackBar( backgroundColor: AppColors.success,
content: Text('Joined ${household.name}!'), ),
backgroundColor: AppColors.success, );
),
);
}
}
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Household not found. Check the code and try again.'),
backgroundColor: AppColors.error,
),
);
}
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
@@ -254,6 +238,66 @@ class _HouseholdScreenState extends State<HouseholdScreen> {
} }
} }
Future<void> _editHouseholdName() async {
final nameController = TextEditingController(text: _household!.name);
final result = await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Edit Household Name'),
content: TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Household Name',
hintText: 'e.g., Smith Family',
),
textCapitalization: TextCapitalization.words,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, nameController.text),
child: const Text('Save'),
),
],
),
);
if (result != null && result.isNotEmpty && result != _household!.name) {
try {
// Update in Supabase
await _supabaseService.updateHouseholdName(_household!.id, result);
// Update local
_household!.name = result;
await HiveDatabase.saveHousehold(_household!);
setState(() {});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Household name updated!'),
backgroundColor: AppColors.success,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error updating name: $e'),
backgroundColor: AppColors.error,
),
);
}
}
}
}
Future<void> _leaveHousehold() async { Future<void> _leaveHousehold() async {
final confirm = await showDialog<bool>( final confirm = await showDialog<bool>(
context: context, context: context,
@@ -280,7 +324,7 @@ class _HouseholdScreenState extends State<HouseholdScreen> {
if (confirm == true && _household != null) { if (confirm == true && _household != null) {
// Leave household in Firebase // Leave household in Firebase
await _firebaseService.leaveHousehold(_household!.id, _settings!.userName!); await _supabaseService.leaveHousehold(_household!.id, _settings!.userName!);
_settings!.currentHouseholdId = null; _settings!.currentHouseholdId = null;
await _settings!.save(); await _settings!.save();
@@ -392,18 +436,40 @@ class _HouseholdScreenState extends State<HouseholdScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
_household!.name, children: [
style: const TextStyle( Expanded(
fontSize: 20, child: Text(
fontWeight: FontWeight.bold, _household!.name,
), style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const Icon(Icons.edit, size: 20),
onPressed: _editHouseholdName,
tooltip: 'Edit household name',
),
],
), ),
Text( Row(
'Owner: ${_household!.ownerName}', children: [
style: TextStyle( Expanded(
color: Colors.grey[600], child: Text(
), 'You: ${_settings!.userName ?? "Not set"}',
style: TextStyle(
color: Colors.grey[600],
),
),
),
IconButton(
icon: const Icon(Icons.edit, size: 18),
onPressed: _showNameInputDialog,
tooltip: 'Edit your name',
),
],
), ),
], ],
), ),

View File

@@ -1,9 +1,17 @@
import 'package:flutter/material.dart'; 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/colors.dart';
import '../../../core/constants/app_icon.dart'; import '../../../core/constants/app_icon.dart';
import '../../../data/local/hive_database.dart'; import '../../../data/local/hive_database.dart';
import '../models/app_settings.dart'; import '../models/app_settings.dart';
import '../../notifications/services/discord_service.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 'privacy_policy_screen.dart';
import 'terms_of_service_screen.dart'; import 'terms_of_service_screen.dart';
import 'household_screen.dart'; import 'household_screen.dart';
@@ -19,11 +27,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
final _discordService = DiscordService(); final _discordService = DiscordService();
AppSettings? _settings; AppSettings? _settings;
bool _isLoading = true; bool _isLoading = true;
String _appVersion = '1.3.0';
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadSettings(); _loadSettings();
_loadAppVersion();
}
Future<void> _loadAppVersion() async {
final packageInfo = await PackageInfo.fromPlatform();
setState(() {
_appVersion = packageInfo.version;
});
} }
Future<void> _loadSettings() async { Future<void> _loadSettings() async {
@@ -117,17 +134,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
// Display Section // Display Section
_buildSectionHeader('Display'), _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( ListTile(
title: const Text('Default View'), title: const Text('Default View'),
subtitle: const Text('Grid'), subtitle: Text(_settings!.defaultView == 'grid' ? 'Grid' : 'List'),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () {}, onTap: _showDefaultViewDialog,
), ),
ListTile( ListTile(
title: const Text('Sort By'), title: const Text('Sort By'),
subtitle: const Text('Expiration Date'), subtitle: Text(_getSortByDisplayName(_settings!.sortBy)),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () {}, onTap: _showSortByDialog,
), ),
const Divider(), const Divider(),
@@ -138,7 +165,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
title: const Text('Export Data'), title: const Text('Export Data'),
subtitle: const Text('Export your inventory to CSV'), subtitle: const Text('Export your inventory to CSV'),
leading: const Icon(Icons.file_download, color: AppColors.primary), leading: const Icon(Icons.file_download, color: AppColors.primary),
onTap: () {}, onTap: _exportData,
), ),
ListTile( ListTile(
title: const Text('Clear All Data'), title: const Text('Clear All Data'),
@@ -158,15 +185,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: const Text('Cancel'), child: const Text('Cancel'),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () async {
// TODO: Clear data // Clear all data from Hive
Navigator.pop(context); await HiveDatabase.clearAllData();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( if (context.mounted) {
content: Text('All data cleared'), Navigator.pop(context);
backgroundColor: AppColors.error, ScaffoldMessenger.of(context).showSnackBar(
), const SnackBar(
); content: Text('All data cleared successfully'),
backgroundColor: AppColors.error,
),
);
}
}, },
child: const Text( child: const Text(
'Clear', 'Clear',
@@ -187,9 +218,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
title: Text('App Name'), title: Text('App Name'),
subtitle: Text('Sage - Kitchen Management'), subtitle: Text('Sage - Kitchen Management'),
), ),
const ListTile( ListTile(
title: Text('Version'), title: const Text('Version'),
subtitle: Text('1.0.0'), subtitle: Text(_appVersion),
), ),
const ListTile( const ListTile(
title: Text('Developer'), title: Text('Developer'),
@@ -233,7 +264,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
showLicensePage( showLicensePage(
context: context, context: context,
applicationName: 'Sage', applicationName: 'Sage',
applicationVersion: '1.0.0', applicationVersion: _appVersion,
applicationIcon: const SageLeafIcon( applicationIcon: const SageLeafIcon(
size: 64, size: 64,
color: AppColors.primary, 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() { void _showDiscordSetup() {
final webhookController = TextEditingController( final webhookController = TextEditingController(
text: _discordService.webhookUrl ?? '', text: _discordService.webhookUrl ?? '',

View File

@@ -1,26 +1,49 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'core/constants/app_theme.dart'; import 'core/constants/app_theme.dart';
import 'data/local/hive_database.dart'; import 'data/local/hive_database.dart';
import 'features/home/screens/home_screen.dart'; import 'features/home/screens/home_screen.dart';
import 'features/settings/models/app_settings.dart';
// Provider to watch settings for dark mode
final settingsProvider = StreamProvider<AppSettings>((ref) async* {
final settings = await HiveDatabase.getSettings();
yield settings;
// Listen for changes (this will update when settings change)
while (true) {
await Future.delayed(const Duration(milliseconds: 500));
final updatedSettings = await HiveDatabase.getSettings();
yield updatedSettings;
}
});
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase (gracefully handle if not configured)
try {
await Firebase.initializeApp();
print('✅ Firebase initialized successfully');
} catch (e) {
print('⚠️ Firebase initialization failed: $e');
print('Household sharing will not work without Firebase configuration.');
print('See FIREBASE_SETUP.md for setup instructions.');
}
// Initialize Hive database // Initialize Hive database
await HiveDatabase.init(); await HiveDatabase.init();
// Initialize Supabase (FOSS Firebase alternative!)
// Cloud-first with optional self-hosting!
final settings = await HiveDatabase.getSettings();
// Default to hosted Supabase, or use custom server if configured
final supabaseUrl = settings.supabaseUrl ?? 'https://pxjvvduzlqediugxyasu.supabase.co';
final supabaseKey = settings.supabaseAnonKey ??
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InB4anZ2ZHV6bHFlZGl1Z3h5YXN1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk2MTUwNjQsImV4cCI6MjA3NTE5MTA2NH0.gPScm4q4PUDDqnFezYRQnVntiqq-glSIwzSWBhQyzwU';
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
);
if (settings.supabaseUrl != null) {
print('✅ Using custom Supabase server: ${settings.supabaseUrl}');
} else {
print('✅ Using hosted Sage sync server (Supabase FOSS backend)');
}
runApp( runApp(
const ProviderScope( const ProviderScope(
child: SageApp(), child: SageApp(),
@@ -28,18 +51,34 @@ void main() async {
); );
} }
class SageApp extends StatelessWidget { class SageApp extends ConsumerWidget {
const SageApp({super.key}); const SageApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp( final settingsAsync = ref.watch(settingsProvider);
title: 'Sage 🌿',
debugShowCheckedModeBanner: false, return settingsAsync.when(
theme: AppTheme.lightTheme, data: (settings) => MaterialApp(
darkTheme: AppTheme.darkTheme, title: 'Sage 🌿',
themeMode: ThemeMode.light, // We'll make this dynamic later debugShowCheckedModeBanner: false,
home: const HomeScreen(), theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: settings.darkModeEnabled ? ThemeMode.dark : ThemeMode.light,
home: const HomeScreen(),
),
loading: () => const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(child: CircularProgressIndicator()),
),
),
error: (_, __) => const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(child: Text('Error loading settings')),
),
),
); );
} }
} }

View File

@@ -6,6 +6,14 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <gtk/gtk_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
} }

View File

@@ -3,6 +3,8 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
gtk
url_launcher_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -5,14 +5,20 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import cloud_firestore import app_links
import firebase_core
import mobile_scanner import mobile_scanner
import package_info_plus
import path_provider_foundation import path_provider_foundation
import share_plus
import shared_preferences_foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@@ -9,14 +9,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "61.0.0" version: "61.0.0"
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11
url: "https://pub.dev"
source: hosted
version: "1.3.59"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
@@ -25,6 +17,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.13.0" version: "5.13.0"
app_links:
dependency: transitive
description:
name: app_links
sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
app_links_linux:
dependency: transitive
description:
name: app_links_linux
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
url: "https://pub.dev"
source: hosted
version: "1.0.3"
app_links_platform_interface:
dependency: transitive
description:
name: app_links_platform_interface
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
app_links_web:
dependency: transitive
description:
name: app_links_web
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
url: "https://pub.dev"
source: hosted
version: "1.0.4"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@@ -153,30 +177,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
cloud_firestore:
dependency: "direct main"
description:
name: cloud_firestore
sha256: "2d33da4465bdb81b6685c41b535895065adcb16261beb398f5f3bbc623979e9c"
url: "https://pub.dev"
source: hosted
version: "5.6.12"
cloud_firestore_platform_interface:
dependency: transitive
description:
name: cloud_firestore_platform_interface
sha256: "413c4e01895cf9cb3de36fa5c219479e06cd4722876274ace5dfc9f13ab2e39b"
url: "https://pub.dev"
source: hosted
version: "6.6.12"
cloud_firestore_web:
dependency: transitive
description:
name: cloud_firestore_web
sha256: c1e30fc4a0fcedb08723fb4b1f12ee4e56d937cbf9deae1bda43cbb6367bb4cf
url: "https://pub.dev"
source: hosted
version: "4.4.12"
code_builder: code_builder:
dependency: transitive dependency: transitive
description: description:
@@ -201,6 +201,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@@ -209,6 +217,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.6" version: "3.0.6"
csv:
dependency: "direct main"
description:
name: csv
sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
url: "https://pub.dev"
source: hosted
version: "6.0.0"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -249,30 +265,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5"
url: "https://pub.dev"
source: hosted
version: "3.15.2"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: "5873a370f0d232918e23a5a6137dbe4c2c47cf017301f4ea02d9d636e52f60f0"
url: "https://pub.dev"
source: hosted
version: "6.0.1"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37"
url: "https://pub.dev"
source: hosted
version: "2.24.1"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@@ -328,6 +320,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
functions_client:
dependency: transitive
description:
name: functions_client
sha256: "38e5049d4ca5b3482c606d8bfe82183aa24c9650ef1fa0582ab5957a947b937f"
url: "https://pub.dev"
source: hosted
version: "2.4.4"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@@ -336,6 +336,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
gotrue:
dependency: transitive
description:
name: gotrue
sha256: "3a3c4b81d22145977251576a893d763aebc29f261e4c00a6eab904b38ba8ba37"
url: "https://pub.dev"
source: hosted
version: "2.15.0"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@@ -344,6 +352,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
gtk:
dependency: transitive
description:
name: gtk
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
url: "https://pub.dev"
source: hosted
version: "2.1.0"
hive: hive:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -393,7 +409,7 @@ packages:
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image: image:
dependency: transitive dependency: "direct dev"
description: description:
name: image name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
@@ -432,6 +448,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.9.0" version: "4.9.0"
jwt_decode:
dependency: transitive
description:
name: jwt_decode
sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb
url: "https://pub.dev"
source: hosted
version: "0.3.1"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -520,6 +544,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
url: "https://pub.dev"
source: hosted
version: "8.3.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path: path:
dependency: transitive dependency: transitive
description: description:
@@ -616,6 +656,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.3" version: "6.0.3"
postgrest:
dependency: transitive
description:
name: postgrest
sha256: "57637e331af3863fa1f555907ff24c30d69c3ad3ff127d89320e70e8d5e585f5"
url: "https://pub.dev"
source: hosted
version: "2.5.0"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -632,6 +680,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" version: "1.5.0"
realtime_client:
dependency: transitive
description:
name: realtime_client
sha256: c0938faca85ff2bdcb8e97ebfca4ab1428661b441c1a414fb09c113e00cee2c6
url: "https://pub.dev"
source: hosted
version: "2.5.3"
retry:
dependency: transitive
description:
name: retry
sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
riverpod: riverpod:
dependency: transitive dependency: transitive
description: description:
@@ -640,6 +704,86 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.6.1" version: "2.6.1"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
share_plus:
dependency: "direct main"
description:
name: share_plus
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
url: "https://pub.dev"
source: hosted
version: "10.1.4"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b
url: "https://pub.dev"
source: hosted
version: "5.0.2"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "0b0f98d535319cb5cdd4f65783c2a54ee6d417a2f093dbb18be3e36e4c3d181f"
url: "https://pub.dev"
source: hosted
version: "2.4.14"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@@ -685,6 +829,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.1" version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -701,6 +853,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
storage_client:
dependency: transitive
description:
name: storage_client
sha256: "1c61b19ed9e78f37fdd1ca8b729ab8484e6c8fe82e15c87e070b861951183657"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
@@ -725,6 +885,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
supabase:
dependency: transitive
description:
name: supabase
sha256: b8991524ff1f4fcb50475847f100a399b96a7d347655bbbd1c7b51eea065f892
url: "https://pub.dev"
source: hosted
version: "2.9.2"
supabase_flutter:
dependency: "direct main"
description:
name: supabase_flutter
sha256: "389eeb18d2a0773da61a157df6f35761e1855567271df12665bb7ddeb2dda0f7"
url: "https://pub.dev"
source: hosted
version: "2.10.2"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -757,6 +933,78 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
url_launcher:
dependency: transitive
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: c0fb544b9ac7efa10254efaf00a951615c362d1ea1877472f8f6c0fa00fcf15b
url: "https://pub.dev"
source: hosted
version: "6.3.23"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
url: "https://pub.dev"
source: hosted
version: "6.3.4"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
url: "https://pub.dev"
source: hosted
version: "3.2.3"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -805,6 +1053,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
win32:
dependency: transitive
description:
name: win32
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
url: "https://pub.dev"
source: hosted
version: "5.14.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@@ -829,6 +1085,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
yet_another_json_isolate:
dependency: transitive
description:
name: yet_another_json_isolate
sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e
url: "https://pub.dev"
source: hosted
version: "2.1.0"
sdks: sdks:
dart: ">=3.9.2 <4.0.0" dart: ">=3.9.2 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.35.0"

View File

@@ -1,7 +1,7 @@
name: sage name: sage
description: "Smart Kitchen Management System" description: "Smart Kitchen Management System"
publish_to: 'none' publish_to: 'none'
version: 1.1.0+2 version: 1.3.0+4
environment: environment:
sdk: ^3.9.2 sdk: ^3.9.2
@@ -24,11 +24,13 @@ dependencies:
# Utilities # Utilities
intl: ^0.20.0 # Date formatting intl: ^0.20.0 # Date formatting
mobile_scanner: ^5.2.3 # Barcode scanning mobile_scanner: ^5.2.3 # Barcode scanning
http: ^1.2.2 # HTTP requests for Discord webhooks http: ^1.2.2 # HTTP requests for API calls and webhooks
csv: ^6.0.0 # CSV export/import
share_plus: ^10.1.2 # Share files
package_info_plus: ^8.1.0 # App version info
# Cloud Backend # Backend - Supabase (Open Source!)
firebase_core: ^3.8.1 # Firebase initialization supabase_flutter: ^2.8.4 # Real-time sync for households
cloud_firestore: ^5.6.0 # Firestore database
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -43,6 +45,7 @@ dev_dependencies:
# Icon Generation # Icon Generation
flutter_launcher_icons: ^0.13.1 flutter_launcher_icons: ^0.13.1
image: ^4.5.4
flutter: flutter:
uses-material-design: true uses-material-design: true

50
tool/generate_icons.dart Normal file
View File

@@ -0,0 +1,50 @@
import 'dart:io';
import 'package:image/image.dart' as img;
void main() async {
print('🎨 Generating PNG icons from SVG...');
// Create a 1024x1024 image with sage green background
final image = img.Image(width: 1024, height: 1024);
// Fill with sage green background (#4CAF50)
img.fill(image, color: img.ColorRgb8(76, 175, 80));
// Draw a simple leaf shape (we'll use circles and ellipses to approximate)
final leaf = img.Image(width: 1024, height: 1024);
img.fill(leaf, color: img.ColorRgba8(0, 0, 0, 0)); // Transparent
// Draw leaf body (light yellow-green #F1F8E9)
img.fillCircle(leaf,
x: 512,
y: 512,
radius: 350,
color: img.ColorRgb8(241, 248, 233)
);
// Composite the leaf onto the background
img.compositeImage(image, leaf);
// Save main icon
final mainIconFile = File('assets/icon/sage_leaf.png');
await mainIconFile.writeAsBytes(img.encodePng(image));
print('✅ Created sage_leaf.png');
// Create foreground icon (transparent background for adaptive icon)
final foreground = img.Image(width: 1024, height: 1024);
img.fill(foreground, color: img.ColorRgba8(0, 0, 0, 0)); // Transparent
// Draw leaf shape
img.fillCircle(foreground,
x: 512,
y: 512,
radius: 350,
color: img.ColorRgb8(241, 248, 233)
);
final foregroundFile = File('assets/icon/sage_leaf_foreground.png');
await foregroundFile.writeAsBytes(img.encodePng(foreground));
print('✅ Created sage_leaf_foreground.png');
print('🎉 Icon generation complete!');
}

191
web/privacy-policy.html Normal file
View File

@@ -0,0 +1,191 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Policy - Sage Kitchen Management</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
h1 {
color: #4CAF50;
margin-bottom: 10px;
font-size: 2.5em;
}
.last-updated {
color: #666;
font-size: 0.9em;
margin-bottom: 30px;
}
h2 {
color: #4CAF50;
margin-top: 30px;
margin-bottom: 15px;
font-size: 1.5em;
}
p {
margin-bottom: 15px;
}
ul {
margin-left: 20px;
margin-bottom: 15px;
}
li {
margin-bottom: 8px;
}
.highlight {
background: #e8f5e9;
padding: 15px;
border-left: 4px solid #4CAF50;
margin: 20px 0;
}
a {
color: #4CAF50;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>🌿 Privacy Policy</h1>
<p class="last-updated">Last Updated: October 4, 2025</p>
<div class="highlight">
<strong>TL;DR:</strong> Sage is built privacy-first. Your data stays on YOUR device. Optional cloud sync uses open-source Supabase. We don't sell data, track you, or show ads. Ever.
</div>
<h2>1. Information We Collect</h2>
<p>Sage is designed to respect your privacy. Here's what we do and don't collect:</p>
<h3>Local Data (Stored on Your Device)</h3>
<ul>
<li><strong>Food inventory items</strong> - names, quantities, expiration dates, barcodes, photos, notes</li>
<li><strong>User preferences</strong> - app settings, Discord webhook URL (if configured), household name</li>
<li><strong>Household information</strong> - household name, member names (if using household sharing)</li>
</ul>
<h3>Cloud Sync Data (Optional - Supabase)</h3>
<p>If you choose to use household sharing features, the following data is synced to Supabase (an open-source backend):</p>
<ul>
<li>Food inventory items from your household</li>
<li>Household name and member names</li>
<li>Anonymous authentication tokens (no email or personal info required)</li>
</ul>
<h3>What We DON'T Collect</h3>
<ul>
<li>❌ No email addresses</li>
<li>❌ No phone numbers</li>
<li>❌ No location tracking</li>
<li>❌ No analytics or usage tracking</li>
<li>❌ No advertising IDs</li>
<li>❌ No personal identifiable information</li>
</ul>
<h2>2. How We Use Your Information</h2>
<p>Your data is used ONLY for these purposes:</p>
<ul>
<li><strong>Local inventory management</strong> - Track your food items on your device</li>
<li><strong>Household sharing</strong> - Sync inventory with family members (if enabled)</li>
<li><strong>Expiration notifications</strong> - Send alerts via Discord webhook (if configured by you)</li>
<li><strong>Barcode lookup</strong> - Fetch product information from public APIs (Open Food Facts, UPCItemDB)</li>
</ul>
<h2>3. Data Storage & Security</h2>
<h3>Local Storage (Hive Database)</h3>
<p>All your data is stored locally on your device using Hive, an encrypted local database. This data never leaves your device unless you explicitly enable household sharing.</p>
<h3>Cloud Storage (Supabase - Optional)</h3>
<p>If you enable household sharing:</p>
<ul>
<li>Data is stored in Supabase (open-source Firebase alternative)</li>
<li>You can use our hosted Supabase instance OR self-host your own</li>
<li>Data is transmitted over HTTPS</li>
<li>Anonymous authentication - no email or password required</li>
</ul>
<h2>4. Third-Party Services</h2>
<p>Sage may interact with these third-party services:</p>
<h3>Barcode Lookup APIs</h3>
<ul>
<li><strong>Open Food Facts</strong> - Free, open database of food products</li>
<li><strong>UPCItemDB</strong> - Product information database</li>
<li>These services receive ONLY the barcode number when you scan items</li>
</ul>
<h3>Discord Webhooks (Optional)</h3>
<p>If you configure a Discord webhook URL, Sage will send expiration notifications to your Discord channel. We don't store or have access to your webhook URL on any server.</p>
<h3>Supabase (Optional)</h3>
<p>If you enable household sharing, your inventory data is synced via Supabase. See their privacy policy at <a href="https://supabase.com/privacy" target="_blank">supabase.com/privacy</a></p>
<h2>5. Data Sharing</h2>
<p><strong>We DO NOT sell, rent, or share your data with anyone.</strong></p>
<p>The ONLY data sharing happens when:</p>
<ul>
<li>You explicitly enable household sharing (data shared with your household members via Supabase)</li>
<li>You configure Discord notifications (sent to YOUR Discord webhook)</li>
</ul>
<h2>6. Your Rights & Control</h2>
<p>You have complete control over your data:</p>
<ul>
<li><strong>Delete your data</strong> - Uninstall the app to remove all local data</li>
<li><strong>Export your data</strong> - Contact us for a data export (coming soon in-app)</li>
<li><strong>Disable cloud sync</strong> - Leave household to stop syncing</li>
<li><strong>Self-host</strong> - Run your own Supabase instance for full control</li>
</ul>
<h2>7. Children's Privacy</h2>
<p>Sage does not knowingly collect information from children under 13. The app is designed for household management and is intended for use by adults.</p>
<h2>8. Open Source & Transparency</h2>
<p>Sage is 100% FOSS (Free and Open Source Software). You can inspect the entire codebase, including:</p>
<ul>
<li>How data is stored locally</li>
<li>What data is sent to Supabase</li>
<li>How barcode APIs are used</li>
<li>No hidden tracking or analytics</li>
</ul>
<h2>9. Changes to This Policy</h2>
<p>We may update this privacy policy from time to time. We'll notify you of any material changes by updating the "Last Updated" date at the top of this policy.</p>
<h2>10. Contact Us</h2>
<p>Questions about this privacy policy? Contact us:</p>
<ul>
<li>GitHub Issues: <a href="https://github.com/yourusername/sage" target="_blank">github.com/yourusername/sage</a></li>
<li>Email: [Your contact email]</li>
</ul>
<div class="highlight">
<strong>🌿 Built with Privacy in Mind</strong><br>
Sage is local-first, open-source, and respects your data. Your kitchen, your data, your control.
</div>
</div>
</body>
</html>

247
web/terms-of-service.html Normal file
View File

@@ -0,0 +1,247 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Terms of Service - Sage Kitchen Management</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
h1 {
color: #4CAF50;
margin-bottom: 10px;
font-size: 2.5em;
}
.last-updated {
color: #666;
font-size: 0.9em;
margin-bottom: 30px;
}
h2 {
color: #4CAF50;
margin-top: 30px;
margin-bottom: 15px;
font-size: 1.5em;
}
p {
margin-bottom: 15px;
}
ul {
margin-left: 20px;
margin-bottom: 15px;
}
li {
margin-bottom: 8px;
}
.highlight {
background: #e8f5e9;
padding: 15px;
border-left: 4px solid #4CAF50;
margin: 20px 0;
}
.warning {
background: #fff3cd;
padding: 15px;
border-left: 4px solid #ffc107;
margin: 20px 0;
}
a {
color: #4CAF50;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>🌿 Terms of Service</h1>
<p class="last-updated">Last Updated: October 4, 2025</p>
<div class="highlight">
<strong>TL;DR:</strong> Sage is free, open-source software. Use it however you want, but don't sue us if something goes wrong. We're not responsible for expired food or food safety decisions you make.
</div>
<h2>1. Acceptance of Terms</h2>
<p>By downloading, installing, or using Sage ("the App"), you agree to these Terms of Service. If you don't agree, please don't use the App.</p>
<h2>2. License & Open Source</h2>
<p>Sage is licensed under the <strong>MIT License</strong>. This means:</p>
<ul>
<li>✅ You can use Sage for free, forever</li>
<li>✅ You can modify the source code</li>
<li>✅ You can distribute your own versions</li>
<li>✅ You can use it commercially</li>
<li>❌ We provide NO WARRANTY (see Section 8)</li>
</ul>
<h2>3. Description of Service</h2>
<p>Sage is a kitchen management app that helps you:</p>
<ul>
<li>Track food inventory with expiration dates</li>
<li>Scan barcodes for product information</li>
<li>Receive expiration notifications</li>
<li>Share household inventory with family members (optional)</li>
<li>Integrate with Discord for notifications (optional)</li>
</ul>
<div class="warning">
<strong>⚠️ IMPORTANT DISCLAIMER:</strong> Sage is a tracking tool, NOT a food safety authority. Always use your judgment when consuming food. When in doubt, throw it out!
</div>
<h2>4. User Responsibilities</h2>
<p>You are responsible for:</p>
<ul>
<li><strong>Food safety decisions</strong> - Sage provides expiration tracking, but YOU decide what's safe to eat</li>
<li><strong>Data accuracy</strong> - Ensuring the information you enter is correct</li>
<li><strong>Barcode data</strong> - Third-party APIs may provide incorrect product information</li>
<li><strong>Household members</strong> - Managing who has access to your household</li>
<li><strong>Your data</strong> - Backing up important information</li>
</ul>
<h2>5. Food Safety Disclaimer</h2>
<p><strong>Sage is NOT responsible for:</strong></p>
<ul>
<li>❌ Foodborne illness or food poisoning</li>
<li>❌ Incorrect expiration date predictions</li>
<li>❌ Barcode API errors or incorrect product data</li>
<li>❌ Decisions you make about consuming food</li>
<li>❌ Food waste or spoiled items</li>
</ul>
<p><strong>Always follow USDA food safety guidelines and use common sense!</strong></p>
<h2>6. Cloud Services & Third-Party APIs</h2>
<h3>Supabase Sync (Optional)</h3>
<p>If you use household sharing:</p>
<ul>
<li>Data is stored on Supabase (open-source backend)</li>
<li>We host a free Supabase instance for your convenience</li>
<li>We may discontinue this service with 30 days notice</li>
<li>You can self-host Supabase for full control</li>
</ul>
<h3>Barcode APIs</h3>
<p>Sage uses public APIs (Open Food Facts, UPCItemDB) for product lookups:</p>
<ul>
<li>These are third-party services we don't control</li>
<li>Product information may be incorrect or outdated</li>
<li>APIs may be unavailable at times</li>
</ul>
<h3>Discord Webhooks (Optional)</h3>
<p>If you configure Discord notifications:</p>
<ul>
<li>You're responsible for your webhook URL security</li>
<li>We don't control Discord's availability</li>
<li>Notifications may fail to deliver</li>
</ul>
<h2>7. Prohibited Uses</h2>
<p>You may NOT use Sage to:</p>
<ul>
<li>Violate any laws or regulations</li>
<li>Harm, harass, or impersonate others</li>
<li>Distribute malware or malicious code</li>
<li>Attempt to hack or compromise the app or Supabase</li>
<li>Scrape or abuse third-party APIs</li>
</ul>
<h2>8. Warranty Disclaimer</h2>
<div class="warning">
<p><strong>SAGE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.</strong></p>
<p>We make NO guarantees that:</p>
<ul>
<li>The app will work perfectly</li>
<li>Data won't be lost</li>
<li>Expiration dates are accurate</li>
<li>Cloud sync will always work</li>
<li>Third-party APIs will be available</li>
</ul>
</div>
<h2>9. Limitation of Liability</h2>
<p><strong>TO THE MAXIMUM EXTENT PERMITTED BY LAW:</strong></p>
<p>We (the Sage developers) are NOT liable for:</p>
<ul>
<li>Food poisoning or illness</li>
<li>Lost or corrupted data</li>
<li>Missed expiration notifications</li>
<li>Food waste or spoilage</li>
<li>Damages from using the app</li>
<li>Third-party service failures</li>
</ul>
<p><strong>Your use of Sage is entirely at your own risk.</strong></p>
<h2>10. Data & Privacy</h2>
<p>See our <a href="privacy-policy.html">Privacy Policy</a> for details on how we handle your data.</p>
<p>Key points:</p>
<ul>
<li>Your data is stored locally on your device</li>
<li>Cloud sync is optional and uses Supabase</li>
<li>We don't sell or track your data</li>
<li>You can delete your data anytime</li>
</ul>
<h2>11. Children's Use</h2>
<p>Sage is not intended for children under 13. If you're under 18, please get parental permission before using the app.</p>
<h2>12. Changes to Service</h2>
<p>We may:</p>
<ul>
<li>Update the app at any time</li>
<li>Add or remove features</li>
<li>Discontinue hosted Supabase service with 30 days notice</li>
<li>Change these Terms of Service (we'll update the date above)</li>
</ul>
<h2>13. Account Termination</h2>
<p>Since Sage doesn't use accounts, there's nothing to terminate! Just uninstall the app to stop using it.</p>
<p>If you're using household sharing, you can leave your household in Settings.</p>
<h2>14. Open Source</h2>
<p>Sage's source code is available on GitHub under the MIT License. You can:</p>
<ul>
<li>Fork and modify the code</li>
<li>Submit bug reports and pull requests</li>
<li>Contribute to development</li>
<li>Create your own version</li>
</ul>
<h2>15. Governing Law</h2>
<p>These Terms are governed by the laws of [Your jurisdiction]. Any disputes will be resolved in [Your location] courts.</p>
<h2>16. Contact</h2>
<p>Questions about these Terms? Contact us:</p>
<ul>
<li>GitHub Issues: <a href="https://github.com/yourusername/sage" target="_blank">github.com/yourusername/sage</a></li>
<li>Email: [Your contact email]</li>
</ul>
<div class="highlight">
<strong>🌿 Thank You for Using Sage!</strong><br>
We built this app to help reduce food waste and make kitchen management easier. It's free, open-source, and privacy-focused. Enjoy!
</div>
</div>
</body>
</html>

View File

@@ -6,12 +6,15 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <cloud_firestore/cloud_firestore_plugin_c_api.h> #include <app_links/app_links_plugin_c_api.h>
#include <firebase_core/firebase_core_plugin_c_api.h> #include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
CloudFirestorePluginCApiRegisterWithRegistrar( AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
FirebaseCorePluginCApiRegisterWithRegistrar( SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@@ -3,8 +3,9 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
cloud_firestore app_links
firebase_core share_plus
url_launcher_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST