Compare commits
4 Commits
3388f24eb4
...
7ab641a3c8
Author | SHA1 | Date | |
---|---|---|---|
7ab641a3c8 | |||
af63e11abd | |||
31c4ba4cba | |||
2cf51b6841 |
8
.gitignore
vendored
@@ -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/
|
||||||
|
@@ -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
@@ -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
@@ -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
|
@@ -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!
|
|
@@ -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 {
|
||||||
|
@@ -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"
|
|
||||||
}
|
|
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 5.2 KiB |
@@ -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>
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 901 B |
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.2 KiB |
4
android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#4CAF50</color>
|
||||||
|
</resources>
|
@@ -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
After Width: | Height: | Size: 7.7 KiB |
BIN
assets/icon/sage_leaf_foreground.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
@@ -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();
|
||||||
|
@@ -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(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
227
lib/features/household/services/supabase_household_service.dart
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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,14 +164,9 @@ 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) {
|
|
||||||
// Load the household data
|
|
||||||
final household = await _firebaseService.getHousehold(code);
|
|
||||||
|
|
||||||
if (household != null) {
|
|
||||||
// Save to local Hive for offline access
|
// Save to local Hive for offline access
|
||||||
await HiveDatabase.saveHousehold(household);
|
await HiveDatabase.saveHousehold(household);
|
||||||
|
|
||||||
@@ -188,17 +183,6 @@ class _HouseholdScreenState extends State<HouseholdScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} 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) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -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,19 +436,41 @@ class _HouseholdScreenState extends State<HouseholdScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
_household!.name,
|
_household!.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
'Owner: ${_household!.ownerName}',
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit, size: 20),
|
||||||
|
onPressed: _editHouseholdName,
|
||||||
|
tooltip: 'Edit household name',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'You: ${_settings!.userName ?? "Not set"}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit, size: 18),
|
||||||
|
onPressed: _showNameInputDialog,
|
||||||
|
tooltip: 'Edit your name',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -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
|
||||||
|
await HiveDatabase.clearAllData();
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('All data cleared'),
|
content: Text('All data cleared successfully'),
|
||||||
backgroundColor: AppColors.error,
|
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 ?? '',
|
||||||
|
@@ -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);
|
||||||
|
|
||||||
|
return settingsAsync.when(
|
||||||
|
data: (settings) => MaterialApp(
|
||||||
title: 'Sage 🌿',
|
title: 'Sage 🌿',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: AppTheme.lightTheme,
|
theme: AppTheme.lightTheme,
|
||||||
darkTheme: AppTheme.darkTheme,
|
darkTheme: AppTheme.darkTheme,
|
||||||
themeMode: ThemeMode.light, // We'll make this dynamic later
|
themeMode: settings.darkModeEnabled ? ThemeMode.dark : ThemeMode.light,
|
||||||
home: const HomeScreen(),
|
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')),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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"))
|
||||||
}
|
}
|
||||||
|
380
pubspec.lock
@@ -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"
|
||||||
|
13
pubspec.yaml
@@ -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
@@ -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
@@ -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
@@ -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>
|
@@ -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"));
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|