✨ Features implemented: - Smart inventory tracking with Hive database - Barcode scanning with auto-populated product info - Multiple API fallbacks (Open Food Facts, UPCItemDB) - Smart expiration date predictions by category - Discord webhook notifications (persisted) - Custom sage leaf vector icon - Material Design 3 UI with sage green theme - Privacy Policy & Terms of Service - Local-first, privacy-focused architecture 🎨 UI/UX: - Home dashboard with inventory stats - Add Item screen with barcode integration - Inventory list with expiration indicators - Settings with persistent preferences - About section with legal docs 🔧 Technical: - Flutter 3.35.5 with Riverpod state management - Hive 2.2.3 for local database - Mobile scanner for barcode detection - Feature-first architecture 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1068 lines
29 KiB
Markdown
1068 lines
29 KiB
Markdown
# SAGE - PROJECT STRUCTURE & SETUP GUIDE
|
|
## Flutter Project Organization 🌿
|
|
|
|
**Last Updated:** October 3, 2025
|
|
**Flutter Version:** 3.x
|
|
**Dart Version:** 3.x
|
|
|
|
---
|
|
|
|
## 📁 COMPLETE FILE STRUCTURE
|
|
|
|
```
|
|
sage/
|
|
│
|
|
├── android/ # Android-specific files
|
|
│ ├── app/
|
|
│ │ ├── src/
|
|
│ │ │ └── main/
|
|
│ │ │ ├── AndroidManifest.xml
|
|
│ │ │ └── res/
|
|
│ │ └── build.gradle
|
|
│ └── build.gradle
|
|
│
|
|
├── ios/ # iOS-specific files
|
|
│ ├── Runner/
|
|
│ │ ├── Info.plist
|
|
│ │ └── Assets.xcassets/
|
|
│ └── Runner.xcworkspace/
|
|
│
|
|
├── lib/ # Main application code
|
|
│ │
|
|
│ ├── main.dart # App entry point
|
|
│ ├── app.dart # Root app widget
|
|
│ │
|
|
│ ├── core/ # Core utilities & constants
|
|
│ │ │
|
|
│ │ ├── constants/
|
|
│ │ │ ├── app_constants.dart
|
|
│ │ │ ├── api_constants.dart
|
|
│ │ │ ├── colors.dart
|
|
│ │ │ ├── text_styles.dart
|
|
│ │ │ └── dimensions.dart
|
|
│ │ │
|
|
│ │ ├── utils/
|
|
│ │ │ ├── date_utils.dart
|
|
│ │ │ ├── validators.dart
|
|
│ │ │ ├── formatters.dart
|
|
│ │ │ └── helpers.dart
|
|
│ │ │
|
|
│ │ ├── extensions/
|
|
│ │ │ ├── date_extensions.dart
|
|
│ │ │ ├── string_extensions.dart
|
|
│ │ │ ├── color_extensions.dart
|
|
│ │ │ └── context_extensions.dart
|
|
│ │ │
|
|
│ │ └── errors/
|
|
│ │ ├── exceptions.dart
|
|
│ │ └── failures.dart
|
|
│ │
|
|
│ ├── features/ # Feature modules (feature-first architecture)
|
|
│ │ │
|
|
│ │ ├── home/
|
|
│ │ │ ├── screens/
|
|
│ │ │ │ └── home_screen.dart
|
|
│ │ │ ├── widgets/
|
|
│ │ │ │ ├── expiring_soon_carousel.dart
|
|
│ │ │ │ ├── quick_stats_card.dart
|
|
│ │ │ │ └── quick_actions.dart
|
|
│ │ │ └── controllers/
|
|
│ │ │ └── home_controller.dart
|
|
│ │ │
|
|
│ │ ├── inventory/
|
|
│ │ │ ├── models/
|
|
│ │ │ │ ├── food_item.dart
|
|
│ │ │ │ ├── food_item.g.dart # Generated by Isar
|
|
│ │ │ │ └── location.dart
|
|
│ │ │ │
|
|
│ │ │ ├── repositories/
|
|
│ │ │ │ ├── inventory_repository.dart
|
|
│ │ │ │ └── inventory_repository_impl.dart
|
|
│ │ │ │
|
|
│ │ │ ├── controllers/
|
|
│ │ │ │ ├── inventory_controller.dart
|
|
│ │ │ │ └── item_form_controller.dart
|
|
│ │ │ │
|
|
│ │ │ ├── screens/
|
|
│ │ │ │ ├── inventory_screen.dart
|
|
│ │ │ │ ├── add_item_screen.dart
|
|
│ │ │ │ ├── edit_item_screen.dart
|
|
│ │ │ │ └── item_detail_screen.dart
|
|
│ │ │ │
|
|
│ │ │ └── widgets/
|
|
│ │ │ ├── inventory_list.dart
|
|
│ │ │ ├── inventory_item_card.dart
|
|
│ │ │ ├── expiration_badge.dart
|
|
│ │ │ ├── location_filter.dart
|
|
│ │ │ └── category_chip.dart
|
|
│ │ │
|
|
│ │ ├── barcode/
|
|
│ │ │ ├── screens/
|
|
│ │ │ │ └── barcode_scanner_screen.dart
|
|
│ │ │ ├── widgets/
|
|
│ │ │ │ ├── scanner_overlay.dart
|
|
│ │ │ │ └── product_info_card.dart
|
|
│ │ │ └── controllers/
|
|
│ │ │ └── barcode_controller.dart
|
|
│ │ │
|
|
│ │ ├── recipes/
|
|
│ │ │ ├── models/
|
|
│ │ │ │ ├── recipe.dart
|
|
│ │ │ │ ├── recipe.g.dart
|
|
│ │ │ │ ├── ingredient.dart
|
|
│ │ │ │ └── difficulty_level.dart
|
|
│ │ │ │
|
|
│ │ │ ├── repositories/
|
|
│ │ │ │ ├── recipe_repository.dart
|
|
│ │ │ │ └── recipe_repository_impl.dart
|
|
│ │ │ │
|
|
│ │ │ ├── controllers/
|
|
│ │ │ │ ├── recipe_controller.dart
|
|
│ │ │ │ ├── recipe_form_controller.dart
|
|
│ │ │ │ └── recipe_matcher_controller.dart
|
|
│ │ │ │
|
|
│ │ │ ├── screens/
|
|
│ │ │ │ ├── recipes_screen.dart
|
|
│ │ │ │ ├── add_recipe_screen.dart
|
|
│ │ │ │ ├── edit_recipe_screen.dart
|
|
│ │ │ │ ├── recipe_detail_screen.dart
|
|
│ │ │ │ ├── what_can_i_make_screen.dart
|
|
│ │ │ │ └── use_it_up_screen.dart
|
|
│ │ │ │
|
|
│ │ │ └── widgets/
|
|
│ │ │ ├── recipe_card.dart
|
|
│ │ │ ├── recipe_list.dart
|
|
│ │ │ ├── ingredient_list.dart
|
|
│ │ │ ├── recipe_tag_chip.dart
|
|
│ │ │ └── match_percentage_indicator.dart
|
|
│ │ │
|
|
│ │ ├── shopping/
|
|
│ │ │ ├── models/
|
|
│ │ │ │ ├── shopping_list.dart
|
|
│ │ │ │ ├── shopping_list.g.dart
|
|
│ │ │ │ ├── shopping_item.dart
|
|
│ │ │ │ └── priority.dart
|
|
│ │ │ │
|
|
│ │ │ ├── repositories/
|
|
│ │ │ │ ├── shopping_repository.dart
|
|
│ │ │ │ └── shopping_repository_impl.dart
|
|
│ │ │ │
|
|
│ │ │ ├── controllers/
|
|
│ │ │ │ ├── shopping_lists_controller.dart
|
|
│ │ │ │ └── shopping_list_controller.dart
|
|
│ │ │ │
|
|
│ │ │ ├── screens/
|
|
│ │ │ │ ├── shopping_lists_screen.dart
|
|
│ │ │ │ ├── list_detail_screen.dart
|
|
│ │ │ │ └── create_list_screen.dart
|
|
│ │ │ │
|
|
│ │ │ └── widgets/
|
|
│ │ │ ├── shopping_list_card.dart
|
|
│ │ │ ├── shopping_item_tile.dart
|
|
│ │ │ ├── list_progress_indicator.dart
|
|
│ │ │ └── quick_add_dialog.dart
|
|
│ │ │
|
|
│ │ ├── settings/
|
|
│ │ │ ├── models/
|
|
│ │ │ │ ├── user_settings.dart
|
|
│ │ │ │ ├── user_settings.g.dart
|
|
│ │ │ │ └── shopping_frequency.dart
|
|
│ │ │ │
|
|
│ │ │ ├── repositories/
|
|
│ │ │ │ ├── settings_repository.dart
|
|
│ │ │ │ └── settings_repository_impl.dart
|
|
│ │ │ │
|
|
│ │ │ ├── controllers/
|
|
│ │ │ │ ├── settings_controller.dart
|
|
│ │ │ │ └── theme_controller.dart
|
|
│ │ │ │
|
|
│ │ │ ├── screens/
|
|
│ │ │ │ ├── settings_screen.dart
|
|
│ │ │ │ ├── notifications_settings_screen.dart
|
|
│ │ │ │ ├── sync_settings_screen.dart
|
|
│ │ │ │ ├── discord_settings_screen.dart
|
|
│ │ │ │ └── shopping_frequency_screen.dart
|
|
│ │ │ │
|
|
│ │ │ └── widgets/
|
|
│ │ │ ├── settings_tile.dart
|
|
│ │ │ ├── settings_section.dart
|
|
│ │ │ └── frequency_picker.dart
|
|
│ │ │
|
|
│ │ └── auth/
|
|
│ │ ├── models/
|
|
│ │ │ └── user.dart
|
|
│ │ │
|
|
│ │ ├── repositories/
|
|
│ │ │ ├── auth_repository.dart
|
|
│ │ │ └── auth_repository_impl.dart
|
|
│ │ │
|
|
│ │ ├── controllers/
|
|
│ │ │ └── auth_controller.dart
|
|
│ │ │
|
|
│ │ └── screens/
|
|
│ │ ├── login_screen.dart
|
|
│ │ ├── signup_screen.dart
|
|
│ │ └── onboarding_screen.dart
|
|
│ │
|
|
│ ├── services/ # Business logic services
|
|
│ │ ├── notification_service.dart
|
|
│ │ ├── sync_service.dart
|
|
│ │ ├── barcode_service.dart
|
|
│ │ ├── discord_service.dart
|
|
│ │ ├── openfoodfacts_service.dart
|
|
│ │ ├── expiration_tracker_service.dart
|
|
│ │ ├── shopping_day_calculator.dart
|
|
│ │ └── recipe_parser_service.dart
|
|
│ │
|
|
│ ├── data/ # Data layer
|
|
│ │ ├── local/
|
|
│ │ │ ├── isar_database.dart
|
|
│ │ │ └── database_provider.dart
|
|
│ │ │
|
|
│ │ └── remote/
|
|
│ │ ├── supabase_client.dart
|
|
│ │ └── api_client.dart
|
|
│ │
|
|
│ └── shared/ # Shared components
|
|
│ ├── widgets/
|
|
│ │ ├── custom_button.dart
|
|
│ │ ├── custom_text_field.dart
|
|
│ │ ├── custom_dropdown.dart
|
|
│ │ ├── loading_indicator.dart
|
|
│ │ ├── error_widget.dart
|
|
│ │ ├── empty_state.dart
|
|
│ │ └── app_bar.dart
|
|
│ │
|
|
│ ├── navigation/
|
|
│ │ ├── app_router.dart
|
|
│ │ └── route_names.dart
|
|
│ │
|
|
│ └── providers/
|
|
│ └── common_providers.dart
|
|
│
|
|
├── assets/ # Static assets
|
|
│ ├── images/
|
|
│ │ ├── logo.png
|
|
│ │ ├── onboarding/
|
|
│ │ └── placeholders/
|
|
│ │
|
|
│ ├── icons/
|
|
│ │ └── app_icon.png
|
|
│ │
|
|
│ └── fonts/
|
|
│ └── # Custom fonts if needed
|
|
│
|
|
├── test/ # Tests
|
|
│ ├── unit/
|
|
│ │ ├── models/
|
|
│ │ ├── services/
|
|
│ │ └── repositories/
|
|
│ │
|
|
│ ├── widget/
|
|
│ │ └── widgets/
|
|
│ │
|
|
│ └── integration/
|
|
│ └── flows/
|
|
│
|
|
├── pubspec.yaml # Dependencies & assets
|
|
├── analysis_options.yaml # Linter rules
|
|
├── README.md # Project documentation
|
|
├── .gitignore # Git ignore rules
|
|
└── .env.example # Environment variables template
|
|
```
|
|
|
|
---
|
|
|
|
## 📦 PUBSPEC.YAML
|
|
|
|
Complete dependency file:
|
|
|
|
```yaml
|
|
name: sage
|
|
description: Smart Kitchen Management System
|
|
publish_to: 'none'
|
|
version: 1.0.0+1
|
|
|
|
environment:
|
|
sdk: '>=3.0.0 <4.0.0'
|
|
|
|
dependencies:
|
|
flutter:
|
|
sdk: flutter
|
|
|
|
# State Management
|
|
flutter_riverpod: ^2.4.0
|
|
riverpod_annotation: ^2.3.0
|
|
|
|
# Database - Local
|
|
isar: ^3.1.0
|
|
isar_flutter_libs: ^3.1.0
|
|
path_provider: ^2.1.0
|
|
|
|
# Database - Cloud (Optional)
|
|
supabase_flutter: ^2.0.0
|
|
|
|
# Barcode Scanning
|
|
mobile_scanner: ^3.5.0
|
|
permission_handler: ^11.0.0
|
|
|
|
# HTTP & API
|
|
http: ^1.1.0
|
|
dio: ^5.4.0 # Alternative to http, better features
|
|
|
|
# Notifications
|
|
flutter_local_notifications: ^16.0.0
|
|
timezone: ^0.9.2
|
|
|
|
# Images
|
|
cached_network_image: ^3.3.0
|
|
image_picker: ^1.0.0
|
|
|
|
# UI Components
|
|
flutter_slidable: ^3.0.0 # Swipe actions
|
|
shimmer: ^3.0.0 # Loading placeholders
|
|
|
|
# Utilities
|
|
intl: ^0.18.0 # Date formatting, i18n
|
|
uuid: ^4.0.0 # Generate unique IDs
|
|
share_plus: ^7.2.0 # Share functionality
|
|
url_launcher: ^6.2.0 # Open URLs
|
|
|
|
# QR Codes (for sharing)
|
|
qr_flutter: ^4.1.0
|
|
qr_code_scanner: ^1.0.0
|
|
|
|
# Secure Storage
|
|
flutter_secure_storage: ^9.0.0
|
|
|
|
# Animations
|
|
animations: ^2.0.0
|
|
|
|
# Forms & Validation
|
|
flutter_form_builder: ^9.1.0
|
|
form_builder_validators: ^9.0.0
|
|
|
|
dev_dependencies:
|
|
flutter_test:
|
|
sdk: flutter
|
|
|
|
# Code Generation
|
|
isar_generator: ^3.1.0
|
|
build_runner: ^2.4.0
|
|
riverpod_generator: ^2.3.0
|
|
|
|
# Linting
|
|
flutter_lints: ^3.0.0
|
|
|
|
# Testing
|
|
mockito: ^5.4.0
|
|
integration_test:
|
|
sdk: flutter
|
|
|
|
flutter:
|
|
uses-material-design: true
|
|
|
|
assets:
|
|
- assets/images/
|
|
- assets/icons/
|
|
|
|
# fonts:
|
|
# - family: CustomFont
|
|
# fonts:
|
|
# - asset: assets/fonts/CustomFont-Regular.ttf
|
|
# - asset: assets/fonts/CustomFont-Bold.ttf
|
|
# weight: 700
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 SETUP INSTRUCTIONS
|
|
|
|
### Prerequisites
|
|
|
|
1. **Install Flutter:**
|
|
```bash
|
|
# macOS (using Homebrew)
|
|
brew install --cask flutter
|
|
|
|
# Windows - Download from:
|
|
# https://docs.flutter.dev/get-started/install/windows
|
|
|
|
# Linux
|
|
# Follow: https://docs.flutter.dev/get-started/install/linux
|
|
```
|
|
|
|
2. **Verify Installation:**
|
|
```bash
|
|
flutter doctor
|
|
```
|
|
|
|
Fix any issues reported (Android Studio, Xcode, etc.)
|
|
|
|
3. **Install Android Studio:**
|
|
- Download from: https://developer.android.com/studio
|
|
- Install Android SDK
|
|
- Set up Android emulator
|
|
|
|
---
|
|
|
|
### Project Creation
|
|
|
|
1. **Create Flutter Project:**
|
|
```bash
|
|
flutter create sage
|
|
cd sage
|
|
```
|
|
|
|
2. **Update pubspec.yaml:**
|
|
- Replace with the pubspec.yaml content above
|
|
- Run: `flutter pub get`
|
|
|
|
3. **Generate Isar Files:**
|
|
```bash
|
|
flutter pub run build_runner build
|
|
```
|
|
|
|
4. **Set Up Git:**
|
|
```bash
|
|
git init
|
|
git add .
|
|
git commit -m "Initial commit"
|
|
```
|
|
|
|
---
|
|
|
|
### Android Configuration
|
|
|
|
#### 1. Update AndroidManifest.xml
|
|
|
|
**File:** `android/app/src/main/AndroidManifest.xml`
|
|
|
|
```xml
|
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
|
|
<!-- Permissions -->
|
|
<uses-permission android:name="android.permission.INTERNET"/>
|
|
<uses-permission android:name="android.permission.CAMERA"/>
|
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
|
|
|
|
<!-- Camera feature (optional, won't block non-camera devices) -->
|
|
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
|
|
|
<application
|
|
android:label="Sage"
|
|
android:name="${applicationName}"
|
|
android:icon="@mipmap/ic_launcher">
|
|
|
|
<activity
|
|
android:name=".MainActivity"
|
|
android:exported="true"
|
|
android:launchMode="singleTop"
|
|
android:theme="@style/LaunchTheme"
|
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
android:hardwareAccelerated="true"
|
|
android:windowSoftInputMode="adjustResize">
|
|
|
|
<meta-data
|
|
android:name="io.flutter.embedding.android.NormalTheme"
|
|
android:resource="@style/NormalTheme"
|
|
/>
|
|
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.MAIN"/>
|
|
<category android:name="android.intent.category.LAUNCHER"/>
|
|
</intent-filter>
|
|
</activity>
|
|
|
|
<meta-data
|
|
android:name="flutterEmbedding"
|
|
android:value="2" />
|
|
</application>
|
|
</manifest>
|
|
```
|
|
|
|
#### 2. Update build.gradle (app level)
|
|
|
|
**File:** `android/app/build.gradle`
|
|
|
|
```gradle
|
|
android {
|
|
namespace "com.sage.app"
|
|
compileSdkVersion 34
|
|
|
|
defaultConfig {
|
|
applicationId "com.sage.app"
|
|
minSdkVersion 21 // Minimum for most packages
|
|
targetSdkVersion 34
|
|
versionCode 1
|
|
versionName "1.0.0"
|
|
multiDexEnabled true
|
|
}
|
|
|
|
buildTypes {
|
|
release {
|
|
signingConfig signingConfigs.debug // Change for production
|
|
minifyEnabled true
|
|
shrinkResources true
|
|
}
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
implementation 'androidx.multidex:multidex:2.0.1'
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### iOS Configuration
|
|
|
|
#### 1. Update Info.plist
|
|
|
|
**File:** `ios/Runner/Info.plist`
|
|
|
|
```xml
|
|
<dict>
|
|
<!-- ... existing entries ... -->
|
|
|
|
<!-- Camera Permission -->
|
|
<key>NSCameraUsageDescription</key>
|
|
<string>Sage needs camera access to scan barcodes for quick item entry.</string>
|
|
|
|
<!-- Photo Library (if using image_picker) -->
|
|
<key>NSPhotoLibraryUsageDescription</key>
|
|
<string>Sage needs access to your photos to attach images to items.</string>
|
|
|
|
<!-- Notifications -->
|
|
<key>UIBackgroundModes</key>
|
|
<array>
|
|
<string>fetch</string>
|
|
<string>remote-notification</string>
|
|
</array>
|
|
</dict>
|
|
```
|
|
|
|
#### 2. Update minimum iOS version
|
|
|
|
**File:** `ios/Podfile`
|
|
|
|
```ruby
|
|
platform :ios, '12.0' # Minimum for most packages
|
|
```
|
|
|
|
---
|
|
|
|
## 🏗️ CODE ORGANIZATION PATTERNS
|
|
|
|
### Feature-First Architecture
|
|
|
|
Each feature is self-contained with its own:
|
|
- Models (data structures)
|
|
- Repositories (data access)
|
|
- Controllers (business logic)
|
|
- Screens (UI)
|
|
- Widgets (reusable UI components)
|
|
|
|
**Benefits:**
|
|
- Easy to find related code
|
|
- Easy to test in isolation
|
|
- Easy to add/remove features
|
|
- Clear separation of concerns
|
|
|
|
---
|
|
|
|
### Riverpod Providers Structure
|
|
|
|
**File:** `lib/features/inventory/controllers/inventory_controller.dart`
|
|
|
|
```dart
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../models/food_item.dart';
|
|
import '../repositories/inventory_repository.dart';
|
|
|
|
// Provider for repository
|
|
final inventoryRepositoryProvider = Provider<InventoryRepository>((ref) {
|
|
return InventoryRepositoryImpl();
|
|
});
|
|
|
|
// Provider for controller
|
|
final inventoryControllerProvider =
|
|
StateNotifierProvider<InventoryController, AsyncValue<List<FoodItem>>>((ref) {
|
|
final repository = ref.watch(inventoryRepositoryProvider);
|
|
return InventoryController(repository);
|
|
});
|
|
|
|
// Controller class
|
|
class InventoryController extends StateNotifier<AsyncValue<List<FoodItem>>> {
|
|
final InventoryRepository _repository;
|
|
|
|
InventoryController(this._repository) : super(const AsyncValue.loading()) {
|
|
loadItems();
|
|
}
|
|
|
|
Future<void> loadItems() async {
|
|
state = const AsyncValue.loading();
|
|
try {
|
|
final items = await _repository.getAllItems();
|
|
state = AsyncValue.data(items);
|
|
} catch (e, stack) {
|
|
state = AsyncValue.error(e, stack);
|
|
}
|
|
}
|
|
|
|
Future<void> addItem(FoodItem item) async {
|
|
await _repository.addItem(item);
|
|
await loadItems(); // Refresh list
|
|
}
|
|
|
|
Future<void> deleteItem(int id) async {
|
|
await _repository.deleteItem(id);
|
|
await loadItems();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Repository Pattern
|
|
|
|
**File:** `lib/features/inventory/repositories/inventory_repository.dart`
|
|
|
|
```dart
|
|
import '../models/food_item.dart';
|
|
|
|
abstract class InventoryRepository {
|
|
Future<List<FoodItem>> getAllItems();
|
|
Future<FoodItem?> getItemById(int id);
|
|
Future<void> addItem(FoodItem item);
|
|
Future<void> updateItem(FoodItem item);
|
|
Future<void> deleteItem(int id);
|
|
Future<List<FoodItem>> getItemsExpiringWithinDays(int days);
|
|
Future<List<FoodItem>> getItemsByLocation(Location location);
|
|
}
|
|
```
|
|
|
|
**File:** `lib/features/inventory/repositories/inventory_repository_impl.dart`
|
|
|
|
```dart
|
|
import 'package:isar/isar.dart';
|
|
import '../models/food_item.dart';
|
|
import 'inventory_repository.dart';
|
|
import '../../../data/local/isar_database.dart';
|
|
|
|
class InventoryRepositoryImpl implements InventoryRepository {
|
|
final Isar _isar = IsarDatabase.instance;
|
|
|
|
@override
|
|
Future<List<FoodItem>> getAllItems() async {
|
|
return await _isar.foodItems.where().findAll();
|
|
}
|
|
|
|
@override
|
|
Future<FoodItem?> getItemById(int id) async {
|
|
return await _isar.foodItems.get(id);
|
|
}
|
|
|
|
@override
|
|
Future<void> addItem(FoodItem item) async {
|
|
await _isar.writeTxn(() async {
|
|
await _isar.foodItems.put(item);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> updateItem(FoodItem item) async {
|
|
await _isar.writeTxn(() async {
|
|
await _isar.foodItems.put(item);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<void> deleteItem(int id) async {
|
|
await _isar.writeTxn(() async {
|
|
await _isar.foodItems.delete(id);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Future<List<FoodItem>> getItemsExpiringWithinDays(int days) async {
|
|
final targetDate = DateTime.now().add(Duration(days: days));
|
|
return await _isar.foodItems
|
|
.filter()
|
|
.expirationDateLessThan(targetDate)
|
|
.findAll();
|
|
}
|
|
|
|
@override
|
|
Future<List<FoodItem>> getItemsByLocation(Location location) async {
|
|
return await _isar.foodItems
|
|
.filter()
|
|
.locationEqualTo(location)
|
|
.findAll();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Service Layer
|
|
|
|
**File:** `lib/services/expiration_tracker_service.dart`
|
|
|
|
```dart
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../features/inventory/repositories/inventory_repository.dart';
|
|
import '../features/settings/repositories/settings_repository.dart';
|
|
import 'notification_service.dart';
|
|
import 'discord_service.dart';
|
|
|
|
final expirationTrackerProvider = Provider<ExpirationTrackerService>((ref) {
|
|
final inventory = ref.watch(inventoryRepositoryProvider);
|
|
final settings = ref.watch(settingsRepositoryProvider);
|
|
final notifications = ref.watch(notificationServiceProvider);
|
|
final discord = ref.watch(discordServiceProvider);
|
|
|
|
return ExpirationTrackerService(
|
|
inventory: inventory,
|
|
settings: settings,
|
|
notifications: notifications,
|
|
discord: discord,
|
|
);
|
|
});
|
|
|
|
class ExpirationTrackerService {
|
|
final InventoryRepository inventory;
|
|
final SettingsRepository settings;
|
|
final NotificationService notifications;
|
|
final DiscordService discord;
|
|
|
|
ExpirationTrackerService({
|
|
required this.inventory,
|
|
required this.settings,
|
|
required this.notifications,
|
|
required this.discord,
|
|
});
|
|
|
|
Future<void> runDailyCheck() async {
|
|
final userSettings = await settings.getSettings();
|
|
final allItems = await inventory.getAllItems();
|
|
|
|
for (final item in allItems) {
|
|
await _checkAndNotify(item, userSettings);
|
|
}
|
|
}
|
|
|
|
Future<void> _checkAndNotify(FoodItem item, UserSettings settings) async {
|
|
final days = item.daysUntilExpiration;
|
|
|
|
// 1 month notification
|
|
if (days == 30 && settings.notify1Month) {
|
|
await notifications.scheduleNotification(
|
|
title: '🌿 Sage: FYI',
|
|
body: '${item.name} expires in 30 days',
|
|
payload: 'item:${item.id}',
|
|
);
|
|
}
|
|
|
|
// 2 weeks notification
|
|
if (days == 14 && settings.notify2Weeks) {
|
|
await notifications.scheduleNotification(
|
|
title: '🌿 Sage: Heads up!',
|
|
body: '${item.name} expires in 2 weeks',
|
|
payload: 'item:${item.id}',
|
|
);
|
|
}
|
|
|
|
// 1 week notification
|
|
if (days == 7 && settings.notify1Week) {
|
|
await notifications.scheduleNotification(
|
|
title: '🌿 Sage: Use soon!',
|
|
body: '${item.name} expires in 1 week',
|
|
payload: 'useItUp:${item.id}',
|
|
);
|
|
}
|
|
|
|
// Shopping day check
|
|
if (settings.nextShoppingDay != null) {
|
|
if (item.expirationDate.isBefore(settings.nextShoppingDay!)) {
|
|
await notifications.scheduleNotification(
|
|
title: '🌿 Sage: Add to list!',
|
|
body: '${item.name} expires before shopping day',
|
|
payload: 'addToList:${item.id}',
|
|
);
|
|
|
|
// Discord alert for shopping day items
|
|
if (settings.enableDiscord) {
|
|
// Collect all such items and send once
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 UI COMPONENTS
|
|
|
|
### Screen Template
|
|
|
|
**File:** `lib/features/inventory/screens/inventory_screen.dart`
|
|
|
|
```dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../controllers/inventory_controller.dart';
|
|
import '../widgets/inventory_list.dart';
|
|
import '../../../shared/widgets/loading_indicator.dart';
|
|
import '../../../shared/widgets/error_widget.dart';
|
|
|
|
class InventoryScreen extends ConsumerWidget {
|
|
const InventoryScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final inventoryState = ref.watch(inventoryControllerProvider);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Inventory'),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.search),
|
|
onPressed: () {
|
|
// Navigate to search
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: inventoryState.when(
|
|
data: (items) => items.isEmpty
|
|
? const EmptyState(
|
|
message: 'No items yet!\nTap + to add your first item.',
|
|
)
|
|
: InventoryList(items: items),
|
|
loading: () => const LoadingIndicator(),
|
|
error: (error, stack) => CustomErrorWidget(
|
|
error: error.toString(),
|
|
onRetry: () => ref.refresh(inventoryControllerProvider),
|
|
),
|
|
),
|
|
floatingActionButton: FloatingActionButton(
|
|
onPressed: () {
|
|
// Navigate to add item screen
|
|
},
|
|
child: const Icon(Icons.add),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🧪 TESTING SETUP
|
|
|
|
### Unit Test Example
|
|
|
|
**File:** `test/unit/services/expiration_tracker_service_test.dart`
|
|
|
|
```dart
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:mockito/mockito.dart';
|
|
import 'package:mockito/annotations.dart';
|
|
|
|
import 'package:sage/services/expiration_tracker_service.dart';
|
|
import 'package:sage/features/inventory/models/food_item.dart';
|
|
|
|
@GenerateMocks([InventoryRepository, SettingsRepository, NotificationService])
|
|
import 'expiration_tracker_service_test.mocks.dart';
|
|
|
|
void main() {
|
|
group('ExpirationTrackerService', () {
|
|
late ExpirationTrackerService service;
|
|
late MockInventoryRepository mockInventory;
|
|
late MockSettingsRepository mockSettings;
|
|
late MockNotificationService mockNotifications;
|
|
|
|
setUp(() {
|
|
mockInventory = MockInventoryRepository();
|
|
mockSettings = MockSettingsRepository();
|
|
mockNotifications = MockNotificationService();
|
|
|
|
service = ExpirationTrackerService(
|
|
inventory: mockInventory,
|
|
settings: mockSettings,
|
|
notifications: mockNotifications,
|
|
discord: MockDiscordService(),
|
|
);
|
|
});
|
|
|
|
test('sends notification for item expiring in 7 days', () async {
|
|
// Arrange
|
|
final item = FoodItem()
|
|
..name = 'Milk'
|
|
..expirationDate = DateTime.now().add(Duration(days: 7));
|
|
|
|
when(mockInventory.getAllItems()).thenAnswer((_) async => [item]);
|
|
when(mockSettings.getSettings()).thenAnswer((_) async => UserSettings());
|
|
|
|
// Act
|
|
await service.runDailyCheck();
|
|
|
|
// Assert
|
|
verify(mockNotifications.scheduleNotification(
|
|
title: '🌿 Sage: Use soon!',
|
|
body: 'Milk expires in 1 week',
|
|
payload: any,
|
|
)).called(1);
|
|
});
|
|
});
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 CODE STYLE GUIDE
|
|
|
|
### Naming Conventions
|
|
|
|
```dart
|
|
// Classes: PascalCase
|
|
class FoodItem {}
|
|
class InventoryController {}
|
|
|
|
// Files: snake_case
|
|
// food_item.dart
|
|
// inventory_controller.dart
|
|
|
|
// Variables & functions: camelCase
|
|
final expirationDate = DateTime.now();
|
|
void loadItems() {}
|
|
|
|
// Constants: lowerCamelCase or UPPER_SNAKE_CASE for compile-time constants
|
|
const apiBaseUrl = 'https://api.sage.app';
|
|
const int MAX_ITEMS = 1000;
|
|
|
|
// Private members: prefix with _
|
|
final _repository = InventoryRepository();
|
|
void _internalMethod() {}
|
|
```
|
|
|
|
### Code Formatting
|
|
|
|
Run before committing:
|
|
```bash
|
|
# Format all Dart files
|
|
dart format .
|
|
|
|
# Analyze code for issues
|
|
flutter analyze
|
|
|
|
# Run tests
|
|
flutter test
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 DEVELOPMENT WORKFLOW
|
|
|
|
### 1. Start Development
|
|
|
|
```bash
|
|
# Get dependencies
|
|
flutter pub get
|
|
|
|
# Generate code (Isar, Riverpod)
|
|
flutter pub run build_runner watch
|
|
|
|
# Run app
|
|
flutter run
|
|
```
|
|
|
|
### 2. Development Mode
|
|
|
|
```bash
|
|
# Hot reload: Press 'r' in terminal
|
|
# Hot restart: Press 'R'
|
|
# Open DevTools: Press 'w'
|
|
```
|
|
|
|
### 3. Testing
|
|
|
|
```bash
|
|
# Run all tests
|
|
flutter test
|
|
|
|
# Run specific test file
|
|
flutter test test/unit/services/expiration_tracker_service_test.dart
|
|
|
|
# Run with coverage
|
|
flutter test --coverage
|
|
```
|
|
|
|
### 4. Building
|
|
|
|
```bash
|
|
# Android APK (debug)
|
|
flutter build apk --debug
|
|
|
|
# Android APK (release)
|
|
flutter build apk --release
|
|
|
|
# Android App Bundle (for Play Store)
|
|
flutter build appbundle --release
|
|
|
|
# iOS (requires Mac)
|
|
flutter build ios --release
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 HELPFUL RESOURCES
|
|
|
|
### Official Documentation
|
|
- Flutter Docs: https://docs.flutter.dev/
|
|
- Dart Docs: https://dart.dev/guides
|
|
- Riverpod: https://riverpod.dev/
|
|
- Isar: https://isar.dev/
|
|
|
|
### Tutorials
|
|
- Flutter Codelabs: https://docs.flutter.dev/codelabs
|
|
- Riverpod Tutorial: https://codewithandrea.com/articles/flutter-state-management-riverpod/
|
|
- Isar Tutorial: https://isar.dev/tutorials/quickstart.html
|
|
|
|
### Community
|
|
- Flutter Discord: https://discord.gg/flutter
|
|
- r/FlutterDev: https://reddit.com/r/FlutterDev
|
|
- Stack Overflow: [flutter] tag
|
|
|
|
---
|
|
|
|
## ✅ PHASE 1 CHECKLIST
|
|
|
|
Use this to track Phase 1 progress:
|
|
|
|
- [ ] Flutter installed and verified (`flutter doctor`)
|
|
- [ ] Android Studio set up with emulator
|
|
- [ ] Project created (`flutter create sage`)
|
|
- [ ] Dependencies added to pubspec.yaml
|
|
- [ ] Run `flutter pub get`
|
|
- [ ] Isar code generation working (`build_runner`)
|
|
- [ ] Project structure created (folders)
|
|
- [ ] Basic models created (FoodItem, etc.)
|
|
- [ ] Repository pattern implemented
|
|
- [ ] Riverpod providers set up
|
|
- [ ] Home screen UI built
|
|
- [ ] Add Item screen (manual entry) built
|
|
- [ ] Inventory list screen built
|
|
- [ ] Local database working (save/load items)
|
|
- [ ] App runs on emulator/device
|
|
- [ ] Basic navigation working
|
|
|
|
---
|
|
|
|
**Ready to start coding? Let's build Sage! 🌿🔥**
|