diff --git a/.flutter-plugins b/.flutter-plugins index d814e69..59ddc84 100644 --- a/.flutter-plugins +++ b/.flutter-plugins @@ -1,2 +1,9 @@ # This is a generated file; do not edit or check into version control. mobile_scanner=/Users/phuocnguyen/.pub-cache/hosted/pub.dev/mobile_scanner-5.1.1/ +path_provider=/Users/phuocnguyen/.pub-cache/hosted/pub.dev/path_provider-2.1.4/ +path_provider_android=/Users/phuocnguyen/.pub-cache/hosted/pub.dev/path_provider_android-2.2.4/ +path_provider_foundation=/Users/phuocnguyen/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/ +path_provider_linux=/Users/phuocnguyen/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ +path_provider_windows=/Users/phuocnguyen/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ +printing=/Users/phuocnguyen/.pub-cache/hosted/pub.dev/printing-5.12.0/ +sqflite=/Users/phuocnguyen/.pub-cache/hosted/pub.dev/sqflite-2.3.3+1/ diff --git a/.gitignore b/.gitignore index 3820a95..29a3a50 100644 --- a/.gitignore +++ b/.gitignore @@ -5,11 +5,9 @@ *.swp .DS_Store .atom/ -.build/ .buildlog/ .history .svn/ -.swiftpm/ migrate_working_dir/ # IntelliJ related @@ -27,11 +25,11 @@ migrate_working_dir/ **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ +.flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ -/coverage/ # Symbolication related app.*.symbols diff --git a/.metadata b/.metadata index df91325..ff28e46 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "a402d9a4376add5bc2d6b1e33e53edaae58c07f8" + revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d" channel: "stable" project_type: app @@ -13,14 +13,14 @@ project_type: app migration: platforms: - platform: root - create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 - base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d - platform: android - create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 - base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d - platform: ios - create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 - base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d # User provided section diff --git a/CLAUDE.md b/CLAUDE.md index 940212f..292ceb8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,279 +1,481 @@ -# Flutter Barcode Scanner App Expert Guidelines +# Flutter Barcode Scanner App Guidelines -## Flexibility Notice -**Important**: This is a recommended project structure, but be flexible and adapt to existing project structures. Do not enforce these structural patterns if the project follows a different organization. Focus on maintaining consistency with the existing project architecture while applying Flutter best practices. - -## Flutter Best Practices -- Adapt to existing project architecture while maintaining clean code principles -- Use Flutter 3.x features and Material 3 design -- Implement clean architecture with Riverpod pattern -- Follow proper state management principles -- Use proper dependency injection -- Implement proper error handling -- Follow platform-specific design guidelines -- Use proper localization techniques - -## Preferred Project Structure -**Note**: This is a reference structure. Adapt to the project's existing organization. +## App Overview +Simple barcode scanner app with two screens: +1. **Home Screen**: Barcode scanner + scan result display + history list +2. **Detail Screen**: 4 text fields + Save (API call) & Print buttons +## Project Structure ``` lib/ core/ constants/ theme/ - utils/ widgets/ network/ api_client.dart - api_endpoints.dart - network_service.dart features/ scanner/ data/ datasources/ scanner_remote_datasource.dart - scanner_local_datasource.dart models/ - scan_response_model.dart - barcode_data_model.dart + scan_item.dart + save_request_model.dart repositories/ - scanner_repository_impl.dart + scanner_repository.dart domain/ entities/ scan_entity.dart - barcode_entity.dart repositories/ scanner_repository.dart usecases/ - get_barcode_data_usecase.dart save_scan_usecase.dart presentation/ providers/ scanner_provider.dart - scan_detail_provider.dart pages/ home_page.dart - scan_detail_page.dart + detail_page.dart widgets/ barcode_scanner_widget.dart + scan_result_display.dart scan_history_list.dart - scan_form_widget.dart - loading_widget.dart - history/ - data/ - domain/ - presentation/ - l10n/ main.dart -test/ - unit/ - widget/ - integration/ ``` -## App Architecture Overview -This barcode scanner app follows a two-screen flow with API integration: -1. **Home Screen**: Barcode scanner + scan history list -2. **Detail Screen**: API call → Loading → Form with 4 text fields + save/print buttons +## App Flow +1. **Scan Barcode**: Camera scans → Show result below scanner +2. **Tap Result**: Navigate to detail screen +3. **Fill Form**: Enter data in 4 text fields +4. **Save**: Call API to save data + store locally +5. **Print**: Print form data -### Key Workflows -1. **Barcode Scanning**: Open camera → Scan barcode → Navigate to detail screen -2. **API Integration**: Pass scanned value → Call API → Handle loading/error states → Populate form -3. **Form Entry**: Display API data in 4 text fields → Allow editing → Save data locally → Print functionality -4. **History Management**: View previous scans on home screen → Tap to view/edit → Re-fetch from API if needed -5. **Offline Support**: Cache API responses locally using Hive + display cached data when offline - -### Important Data Concepts -- **Scan Entity**: Barcode value + timestamp + API response data + custom field data -- **API Response**: External data fetched based on barcode value -- **History List**: Chronological list of all scanned items with cached API data -- **Form Fields**: 4 text fields populated from API response (editable) -- **Network State**: Loading, success, error, offline states - -## Core Features Implementation - -### Barcode Scanner Integration -- Use **mobile_scanner** package for reliable scanning -- Support Code128 and other common barcode formats -- Implement proper camera permissions -- Handle scanner lifecycle (pause/resume) -- Provide visual feedback for successful scans - -### API Integration Layer -- **HTTP Client**: Use Dio or http package with proper configuration -- **Base URL**: Configurable API endpoint -- **Authentication**: Handle API keys/tokens if required -- **Request/Response Models**: Proper JSON serialization -- **Timeout Handling**: Network timeout configuration -- **Retry Logic**: Implement retry for failed requests - -### Local Data Storage & Caching -- Use **Hive** for fast, local database storage -- **Cache Strategy**: Store API responses with timestamps -- **Offline Mode**: Display cached data when network unavailable -- **Cache Invalidation**: Refresh expired cache entries -- **Sync Strategy**: Background sync when network restored - -### Navigation Flow -- **Home → Detail**: Pass scanned barcode value + trigger API call -- **Detail Loading**: Show loading indicator during API call -- **Detail Success**: Display form with API data -- **Detail Error**: Show error message with retry option -- **Detail → Home**: Return with save confirmation - -### Network State Management -- **Loading States**: Visual indicators during API calls -- **Error Handling**: Network errors, API errors, timeout errors -- **Retry Mechanism**: User-initiated and automatic retries -- **Offline Detection**: Network connectivity monitoring - -## Performance Considerations -- **Scanner Performance**: Optimize camera preview and barcode detection -- **API Caching**: Cache API responses to reduce network calls -- **Hive Queries**: Efficient history list loading with pagination -- **Memory Management**: Proper disposal of camera and network resources -- **Background Sync**: Efficient background data synchronization -- **Image Loading**: Lazy load any images from API responses - -## Security & Network Considerations -- **HTTPS**: Enforce secure API connections -- **API Keys**: Secure storage using flutter_secure_storage -- **Input Validation**: Sanitize barcode values before API calls -- **Certificate Pinning**: Optional for high-security requirements -- **Rate Limiting**: Respect API rate limits -- **Data Encryption**: Encrypt sensitive cached data - -## Widget Guidelines - -### Scanner Widget -- Implement proper camera lifecycle management -- Provide visual scan indicators -- Handle different screen orientations -- Support flashlight toggle -- Error handling for camera failures - -### Detail Screen Widgets -- **Loading Widget**: Skeleton loading or progress indicators -- **Error Widget**: User-friendly error messages with retry buttons -- **Form Widget**: Pre-populated fields from API response -- **Network Status**: Visual indicators for online/offline status - -### History List -- Efficient scrolling with ListView.builder -- Pull-to-refresh functionality (triggers API refresh) -- Search/filter capabilities -- Swipe-to-delete actions -- Visual indicators for cached vs fresh data -- Export options - -## Common Patterns for This App - -### State Management with Riverpod +## Data Models ```dart -// Scanner state -final scannerStateProvider = StateNotifierProvider +class ScanItem { + final String barcode; + final DateTime timestamp; + final String field1; + final String field2; + final String field3; + final String field4; + + ScanItem({ + required this.barcode, + required this.timestamp, + this.field1 = '', + this.field2 = '', + this.field3 = '', + this.field4 = '', + }); +} -// API call state -final barcodeDataProvider = FutureProvider.family((ref, barcode) async { - return ref.read(scannerRepositoryProvider).getBarcodeData(barcode); -}); - -// History state with cached API data -final historyProvider = StateNotifierProvider> - -// Form state with API pre-population -final formProvider = StateNotifierProvider - -// Network connectivity -final connectivityProvider = StreamProvider +class SaveRequest { + final String barcode; + final String field1; + final String field2; + final String field3; + final String field4; + + SaveRequest({ + required this.barcode, + required this.field1, + required this.field2, + required this.field3, + required this.field4, + }); + + Map toJson() => { + 'barcode': barcode, + 'field1': field1, + 'field2': field2, + 'field3': field3, + 'field4': field4, + }; +} ``` -### API Error Handling +## Home Screen Layout +``` +┌─────────────────────────┐ +│ │ +│ Barcode Scanner │ +│ (Camera View) │ +│ │ +├─────────────────────────┤ +│ Last Scanned: 123456 │ +│ [Tap to edit] │ +├─────────────────────────┤ +│ Scan History │ +│ • 123456 - 10:30 AM │ +│ • 789012 - 10:25 AM │ +│ • 345678 - 10:20 AM │ +│ │ +└─────────────────────────┘ +``` + +## Detail Screen Layout +``` +┌─────────────────────────┐ +│ Barcode: 123456789 │ +├─────────────────────────┤ +│ │ +│ Field 1: [____________] │ +│ │ +│ Field 2: [____________] │ +│ │ +│ Field 3: [____________] │ +│ │ +│ Field 4: [____________] │ +│ │ +├─────────────────────────┤ +│ [Save] [Print] │ +└─────────────────────────┘ +``` + +## Key Features + +### Barcode Scanner +- Use **mobile_scanner** package +- Support Code128 and common formats +- Show scanned value immediately below scanner +- Add to history list automatically + +### API Integration +- Call API when Save button is pressed +- Send form data to server +- Handle success/error responses +- Show loading state during API call + +### Local Storage +- Use **Hive** for simple local storage +- Store scan history with timestamps +- Save form data after successful API call + +### Navigation +- Tap on scan result → Navigate to detail screen +- Pass barcode value to detail screen +- Simple back navigation + +## State Management (Riverpod) + +### Scanner State ```dart -sealed class ApiResult { - const ApiResult(); -} - -class ApiSuccess extends ApiResult { - final T data; - const ApiSuccess(this.data); -} - -class ApiError extends ApiResult { - final String message; - final int? statusCode; - const ApiError(this.message, this.statusCode); -} - -class ApiLoading extends ApiResult { - const ApiLoading(); +class ScannerState { + final String? currentBarcode; + final List history; + + ScannerState({ + this.currentBarcode, + this.history = const [], + }); } ``` -### Repository Pattern with Caching +### Form State +```dart +class FormState { + final String barcode; + final String field1; + final String field2; + final String field3; + final String field4; + final bool isLoading; + final String? error; + + FormState({ + required this.barcode, + this.field1 = '', + this.field2 = '', + this.field3 = '', + this.field4 = '', + this.isLoading = false, + this.error, + }); +} +``` + +## Use Cases + +### Save Scan Use Case +```dart +class SaveScanUseCase { + final ScannerRepository repository; + + SaveScanUseCase(this.repository); + + Future> call(SaveRequest request) async { + return await repository.saveScan(request); + } +} +``` + +## Repository Pattern ```dart abstract class ScannerRepository { - Future> getBarcodeData(String barcode); - Future> saveScanData(ScanEntity scan); + Future> saveScan(SaveRequest request); } class ScannerRepositoryImpl implements ScannerRepository { final ScannerRemoteDataSource remoteDataSource; - final ScannerLocalDataSource localDataSource; - final NetworkInfo networkInfo; - // Implementation with cache-first or network-first strategies + ScannerRepositoryImpl(this.remoteDataSource); + + @override + Future> saveScan(SaveRequest request) async { + try { + await remoteDataSource.saveScan(request); + return const Right(null); + } catch (e) { + return Left(ServerFailure(e.toString())); + } + } } ``` -## Testing Guidelines -1. **API Tests**: Mock HTTP responses and error scenarios -2. **Repository Tests**: Test caching and offline behavior -3. **Scanner Tests**: Mock barcode scanning scenarios -4. **Hive Tests**: Database CRUD operations -5. **Form Tests**: Validation and data persistence with API data -6. **Navigation Tests**: Screen transitions with API loading states -7. **Network Tests**: Connectivity changes and retry logic -8. **Integration Tests**: Complete user workflows including API calls +## Data Source +```dart +abstract class ScannerRemoteDataSource { + Future saveScan(SaveRequest request); +} -## Error Handling Scenarios -- **Network Errors**: No internet connection, timeout, server unavailable -- **API Errors**: Invalid barcode, 404 not found, 500 server error, rate limiting -- **Scanner Errors**: Camera permission denied, scanning failures -- **Storage Errors**: Hive database errors, disk space issues -- **Validation Errors**: Invalid form data, missing required fields +class ScannerRemoteDataSourceImpl implements ScannerRemoteDataSource { + final ApiClient apiClient; + + ScannerRemoteDataSourceImpl(this.apiClient); + + @override + Future saveScan(SaveRequest request) async { + final response = await apiClient.post( + '/api/scans', + data: request.toJson(), + ); + + if (response.statusCode != 200) { + throw ServerException('Failed to save scan'); + } + } +} +``` -## Platform-Specific Considerations +## Widget Structure -### Android -- Network security configuration -- Background sync limitations -- Proper hardware acceleration -- Print service integration +### Home Page +```dart +class HomePage extends ConsumerWidget { + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + body: Column( + children: [ + // Barcode Scanner (top half) + Expanded( + flex: 1, + child: BarcodeScannerWidget(), + ), + + // Scan Result Display + ScanResultDisplay(), + + // History List (bottom half) + Expanded( + flex: 1, + child: ScanHistoryList(), + ), + ], + ), + ); + } +} +``` -### iOS -- App Transport Security (ATS) settings -- Network permissions and privacy -- Background app refresh policies -- AirPrint integration +### Detail Page with Save API Call +```dart +class DetailPage extends ConsumerWidget { + final String barcode; + + Widget build(BuildContext context, WidgetRef ref) { + final formState = ref.watch(formProvider); + + return Scaffold( + appBar: AppBar(title: Text('Edit Details')), + body: Column( + children: [ + // Barcode Header + Container( + padding: EdgeInsets.all(16), + child: Text('Barcode: $barcode'), + ), + + // 4 Text Fields + Expanded( + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + children: [ + TextField( + decoration: InputDecoration(labelText: 'Field 1'), + onChanged: (value) => ref.read(formProvider.notifier).updateField1(value), + ), + TextField( + decoration: InputDecoration(labelText: 'Field 2'), + onChanged: (value) => ref.read(formProvider.notifier).updateField2(value), + ), + TextField( + decoration: InputDecoration(labelText: 'Field 3'), + onChanged: (value) => ref.read(formProvider.notifier).updateField3(value), + ), + TextField( + decoration: InputDecoration(labelText: 'Field 4'), + onChanged: (value) => ref.read(formProvider.notifier).updateField4(value), + ), + ], + ), + ), + ), + + // Error Message + if (formState.error != null) + Container( + padding: EdgeInsets.all(16), + child: Text( + formState.error!, + style: TextStyle(color: Colors.red), + ), + ), + + // Save & Print Buttons + Padding( + padding: EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: formState.isLoading ? null : () => _saveData(ref), + child: formState.isLoading + ? CircularProgressIndicator() + : Text('Save'), + ), + ), + SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: _printData, + child: Text('Print'), + ), + ), + ], + ), + ), + ], + ), + ); + } + + void _saveData(WidgetRef ref) async { + final formState = ref.read(formProvider); + final saveRequest = SaveRequest( + barcode: barcode, + field1: formState.field1, + field2: formState.field2, + field3: formState.field3, + field4: formState.field4, + ); + + await ref.read(formProvider.notifier).saveData(saveRequest); + } +} +``` -## Coding Guidelines -1. Use proper null safety practices -2. Implement proper error handling with Either type -3. Follow proper naming conventions -4. Use proper widget composition -5. Implement proper routing using GoRouter -6. Use proper form validation -7. Follow proper state management with Riverpod -8. Implement proper dependency injection using GetIt -9. Use proper asset management +## Core Functions -## Refactoring Instructions -When refactoring code: -- Always maintain existing project structure patterns -- Prioritize consistency with current codebase -- Apply Flutter best practices without breaking existing architecture -- Focus on incremental improvements -- Ensure all changes maintain backward compatibility +### Save Data with API Call +```dart +class FormNotifier extends StateNotifier { + final SaveScanUseCase saveScanUseCase; + + FormNotifier(this.saveScanUseCase, String barcode) + : super(FormState(barcode: barcode)); + + Future saveData(SaveRequest request) async { + state = state.copyWith(isLoading: true, error: null); + + final result = await saveScanUseCase(request); + + result.fold( + (failure) => state = state.copyWith( + isLoading: false, + error: failure.message, + ), + (_) { + state = state.copyWith(isLoading: false); + // Save to local storage after successful API call + _saveToLocal(request); + // Navigate back or show success message + }, + ); + } + + void _saveToLocal(SaveRequest request) { + // Save to Hive local storage + final scanItem = ScanItem( + barcode: request.barcode, + timestamp: DateTime.now(), + field1: request.field1, + field2: request.field2, + field3: request.field3, + field4: request.field4, + ); + // Add to Hive box + } +} +``` -This architecture ensures a robust, maintainable barcode scanning application with reliable API integration and offline capabilities. \ No newline at end of file +### Print Data +```dart +void printData(ScanItem item) { + // Format data for printing + // Use platform printing service +} +``` + +## Dependencies +```yaml +dependencies: + flutter_riverpod: ^2.4.9 + mobile_scanner: ^4.0.1 + hive: ^2.2.3 + hive_flutter: ^1.1.0 + go_router: ^12.1.3 + dio: ^5.3.2 + dartz: ^0.10.1 + get_it: ^7.6.4 + +dev_dependencies: + hive_generator: ^2.0.1 + build_runner: ^2.4.7 +``` + +## Error Handling +```dart +abstract class Failure { + final String message; + const Failure(this.message); +} + +class ServerFailure extends Failure { + const ServerFailure(String message) : super(message); +} + +class NetworkFailure extends Failure { + const NetworkFailure(String message) : super(message); +} +``` + +## Key Points +- Save button calls API to save form data +- Show loading state during API call +- Handle API errors with user-friendly messages +- Save to local storage only after successful API call +- Use clean architecture with use cases and repository pattern +- Keep UI simple with proper error handling \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore index be3943c..6f56801 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -5,10 +5,9 @@ gradle-wrapper.jar /gradlew.bat /local.properties GeneratedPluginRegistrant.java -.cxx/ # Remember to never publicly share your keystore. -# See https://flutter.dev/to/reference-keystore +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties **/*.keystore **/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..974900e --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,67 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.example.minhthu.minhthu" + compileSdk 36 + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.minhthu.minhthu" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion // Required for mobile_scanner + targetSdkVersion 35 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies {} diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 12f65eb..69c038e 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -24,7 +24,7 @@ android { applicationId = "com.example.minhthu.minhthu" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion + minSdk = 21 // Required for mobile_scanner targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index be4d06e..6b2c47d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,9 @@ + + + + + diff --git a/android/app/src/main/kotlin/com/example/minhthu/minhthu/MainActivity.kt b/android/app/src/main/kotlin/com/example/minhthu/minhthu/MainActivity.kt index 03ee23a..2af9d86 100644 --- a/android/app/src/main/kotlin/com/example/minhthu/minhthu/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/minhthu/minhthu/MainActivity.kt @@ -2,4 +2,4 @@ package com.example.minhthu.minhthu import io.flutter.embedding.android.FlutterActivity -class MainActivity : FlutterActivity() +class MainActivity: FlutterActivity() diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..1bdb07d --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,28 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.2.1' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index ac3b479..aa49780 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..2b25ebe --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,26 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.2.1" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false +} + +include ":app" diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7..7c56964 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 13.0 + 12.0 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 66fe9f6..c217f8b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,22 +1,119 @@ PODS: - Flutter (1.0.0) - - mobile_scanner (7.0.0): + - GoogleDataTransport (9.4.1): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30911.0, >= 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleMLKit/BarcodeScanning (6.0.0): + - GoogleMLKit/MLKitCore + - MLKitBarcodeScanning (~> 5.0.0) + - GoogleMLKit/MLKitCore (6.0.0): + - MLKitCommon (~> 11.0.0) + - GoogleToolboxForMac/Defines (4.2.1) + - GoogleToolboxForMac/Logger (4.2.1): + - GoogleToolboxForMac/Defines (= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (4.2.1)": + - GoogleToolboxForMac/Defines (= 4.2.1) + - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Privacy + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.13.3): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilitiesComponents (1.1.0): + - GoogleUtilities/Logger + - GTMSessionFetcher/Core (3.5.0) + - MLImage (1.0.0-beta5) + - MLKitBarcodeScanning (5.0.0): + - MLKitCommon (~> 11.0) + - MLKitVision (~> 7.0) + - MLKitCommon (11.0.0): + - GoogleDataTransport (< 10.0, >= 9.4.1) + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0) + - GoogleUtilitiesComponents (~> 1.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLKitVision (7.0.0): + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLImage (= 1.0.0-beta5) + - MLKitCommon (~> 11.0) + - mobile_scanner (5.1.1): + - Flutter + - GoogleMLKit/BarcodeScanning (~> 6.0.0) + - nanopb (2.30910.0): + - nanopb/decode (= 2.30910.0) + - nanopb/encode (= 2.30910.0) + - nanopb/decode (2.30910.0) + - nanopb/encode (2.30910.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - printing (1.0.0): + - Flutter + - PromisesObjC (2.4.0) + - sqflite (0.0.3): - Flutter - FlutterMacOS DEPENDENCIES: - Flutter (from `Flutter`) - - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) + - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - printing (from `.symlinks/plugins/printing/ios`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) + +SPEC REPOS: + trunk: + - GoogleDataTransport + - GoogleMLKit + - GoogleToolboxForMac + - GoogleUtilities + - GoogleUtilitiesComponents + - GTMSessionFetcher + - MLImage + - MLKitBarcodeScanning + - MLKitCommon + - MLKitVision + - nanopb + - PromisesObjC EXTERNAL SOURCES: Flutter: :path: Flutter mobile_scanner: - :path: ".symlinks/plugins/mobile_scanner/darwin" + :path: ".symlinks/plugins/mobile_scanner/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + printing: + :path: ".symlinks/plugins/printing/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/darwin" SPEC CHECKSUMS: - Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065 + GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + MLImage: 1824212150da33ef225fbd3dc49f184cf611046c + MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b + MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1 + MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 + mobile_scanner: ba17a89d6a2d1847dad8cad0335856fd4b4ce1f6 + nanopb: 438bc412db1928dac798aa6fd75726007be04262 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1016ad7..1afa5d1 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,14 +8,14 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 31B74C332E9BAB3C731709B5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 157B759BA50822107BC84545 /* Pods_Runner.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 34DCF5348B04B2452A11F770 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA37885CE439B5803C1F30C9 /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 4235163B87D1278088D5778B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8FB13559C5EACC38629CCB47 /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B3E8470F02DDB7ABAEA7A4B8 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D4CE6380793FAE04B5A637F6 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -42,18 +42,19 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 056F865243892463F3F29ED9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 072A244A3D8AA29AA9F65FCB /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 104104C601139FB16A2F3240 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 157B759BA50822107BC84545 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 53066FF35F0C14F228CC4AC1 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 5E26BD87EDC25813BAAA67EF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 4116CB59444E11465860BA76 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 82BC7196F9425D74B3962878 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 8FB13559C5EACC38629CCB47 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -61,26 +62,25 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A877660DB1749B9A7312FCA0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - AEF323229F6447974AF10DC1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - C48CD622E061AB73819815F4 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - EA37885CE439B5803C1F30C9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B83CBDF5984EDBB9453FD4B3 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + BE992B5BD308E267D0F5E36A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + D4CE6380793FAE04B5A637F6 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 5D3CC73C5175E2ED6E885B80 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B3E8470F02DDB7ABAEA7A4B8 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 34DCF5348B04B2452A11F770 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - EFC62B64C79AE505518CC935 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 4235163B87D1278088D5778B /* Pods_RunnerTests.framework in Frameworks */, + 31B74C332E9BAB3C731709B5 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -95,29 +95,6 @@ path = RunnerTests; sourceTree = ""; }; - 357B02463BE002334A48BD06 /* Frameworks */ = { - isa = PBXGroup; - children = ( - EA37885CE439B5803C1F30C9 /* Pods_Runner.framework */, - 8FB13559C5EACC38629CCB47 /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 943C3FE7F88950483702F50C /* Pods */ = { - isa = PBXGroup; - children = ( - 5E26BD87EDC25813BAAA67EF /* Pods-Runner.debug.xcconfig */, - AEF323229F6447974AF10DC1 /* Pods-Runner.release.xcconfig */, - 82BC7196F9425D74B3962878 /* Pods-Runner.profile.xcconfig */, - 53066FF35F0C14F228CC4AC1 /* Pods-RunnerTests.debug.xcconfig */, - C48CD622E061AB73819815F4 /* Pods-RunnerTests.release.xcconfig */, - A877660DB1749B9A7312FCA0 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -136,8 +113,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, - 943C3FE7F88950483702F50C /* Pods */, - 357B02463BE002334A48BD06 /* Frameworks */, + E49D38F264550528BB47A154 /* Pods */, + E4F4BEC4DA1C1555A71A55FA /* Frameworks */, ); sourceTree = ""; }; @@ -165,6 +142,29 @@ path = Runner; sourceTree = ""; }; + E49D38F264550528BB47A154 /* Pods */ = { + isa = PBXGroup; + children = ( + 104104C601139FB16A2F3240 /* Pods-Runner.debug.xcconfig */, + BE992B5BD308E267D0F5E36A /* Pods-Runner.release.xcconfig */, + 4116CB59444E11465860BA76 /* Pods-Runner.profile.xcconfig */, + 056F865243892463F3F29ED9 /* Pods-RunnerTests.debug.xcconfig */, + 072A244A3D8AA29AA9F65FCB /* Pods-RunnerTests.release.xcconfig */, + B83CBDF5984EDBB9453FD4B3 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + E4F4BEC4DA1C1555A71A55FA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 157B759BA50822107BC84545 /* Pods_Runner.framework */, + D4CE6380793FAE04B5A637F6 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -172,10 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 8D6B5637DC40D5DF52D2BA2C /* [CP] Check Pods Manifest.lock */, + 8FF0EEBCA20B622CF2073D49 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, - EFC62B64C79AE505518CC935 /* Frameworks */, + 5D3CC73C5175E2ED6E885B80 /* Frameworks */, ); buildRules = ( ); @@ -191,14 +191,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - C0DF03DDD7A89CF91AF2093E /* [CP] Check Pods Manifest.lock */, + F0155373E2B22AF41384A47C /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 4DDAF44A6054BC7CAC7F4CB9 /* [CP] Embed Pods Frameworks */, + 8379CD5D41C8BC844F4FF4C3 /* [CP] Embed Pods Frameworks */, + C5CE1C9F1E27F01F3BBAFF78 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -286,7 +287,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 4DDAF44A6054BC7CAC7F4CB9 /* [CP] Embed Pods Frameworks */ = { + 8379CD5D41C8BC844F4FF4C3 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -303,7 +304,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 8D6B5637DC40D5DF52D2BA2C /* [CP] Check Pods Manifest.lock */ = { + 8FF0EEBCA20B622CF2073D49 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -340,7 +341,24 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - C0DF03DDD7A89CF91AF2093E /* [CP] Check Pods Manifest.lock */ = { + C5CE1C9F1E27F01F3BBAFF78 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + F0155373E2B22AF41384A47C /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -455,7 +473,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -488,7 +506,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 53066FF35F0C14F228CC4AC1 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 056F865243892463F3F29ED9 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -506,7 +524,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C48CD622E061AB73819815F4 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 072A244A3D8AA29AA9F65FCB /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -522,7 +540,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A877660DB1749B9A7312FCA0 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = B83CBDF5984EDBB9453FD4B3 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -585,7 +603,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -636,7 +654,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d4..8e3ca5d 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,7 +26,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 6266644..70693e4 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ -import Flutter import UIKit +import Flutter -@main +@UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 73848e7..504bea0 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -29,7 +29,7 @@ UIMainStoryboardFile Main NSCameraUsageDescription - This app requires camera access to take photos. + This app needs camera access to scan barcodes UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/lib/app_router.dart b/lib/app_router.dart new file mode 100644 index 0000000..5804860 --- /dev/null +++ b/lib/app_router.dart @@ -0,0 +1,2 @@ +// Re-export the app router from the core routing module +export 'core/routing/app_router.dart'; \ No newline at end of file diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart new file mode 100644 index 0000000..bed4568 --- /dev/null +++ b/lib/core/constants/app_constants.dart @@ -0,0 +1,80 @@ +/// Application-wide constants +class AppConstants { + // Private constructor to prevent instantiation + AppConstants._(); + + // API Configuration + static const String apiBaseUrl = 'https://api.example.com'; // Replace with actual API base URL + static const String apiVersion = 'v1'; + static const String scansEndpoint = '/api/scans'; + + // Network Timeouts (in milliseconds) + static const int connectionTimeout = 30000; // 30 seconds + static const int receiveTimeout = 30000; // 30 seconds + static const int sendTimeout = 30000; // 30 seconds + + // Local Storage Keys + static const String scanHistoryBox = 'scan_history'; + static const String settingsBox = 'settings'; + static const String userPreferencesKey = 'user_preferences'; + + // Scanner Configuration + static const List supportedBarcodeFormats = [ + 'CODE_128', + 'CODE_39', + 'CODE_93', + 'EAN_13', + 'EAN_8', + 'UPC_A', + 'UPC_E', + 'QR_CODE', + 'DATA_MATRIX', + ]; + + // UI Configuration + static const int maxHistoryItems = 100; + static const int scanResultDisplayDuration = 3; // seconds + + // Form Field Labels + static const String field1Label = 'Field 1'; + static const String field2Label = 'Field 2'; + static const String field3Label = 'Field 3'; + static const String field4Label = 'Field 4'; + + // Error Messages + static const String networkErrorMessage = 'Network error occurred. Please check your connection.'; + static const String serverErrorMessage = 'Server error occurred. Please try again later.'; + static const String unknownErrorMessage = 'An unexpected error occurred.'; + static const String noDataMessage = 'No data available'; + static const String scannerPermissionMessage = 'Camera permission is required to scan barcodes.'; + + // Success Messages + static const String saveSuccessMessage = 'Data saved successfully!'; + static const String printSuccessMessage = 'Print job completed successfully!'; + + // App Info + static const String appName = 'Barcode Scanner'; + static const String appVersion = '1.0.0'; + static const String appDescription = 'Simple barcode scanner with form data entry'; + + // Animation Durations + static const Duration shortAnimationDuration = Duration(milliseconds: 200); + static const Duration mediumAnimationDuration = Duration(milliseconds: 400); + static const Duration longAnimationDuration = Duration(milliseconds: 600); + + // Spacing and Sizes + static const double defaultPadding = 16.0; + static const double smallPadding = 8.0; + static const double largePadding = 24.0; + static const double borderRadius = 8.0; + static const double buttonHeight = 48.0; + static const double textFieldHeight = 56.0; + + // Scanner View Configuration + static const double scannerAspectRatio = 1.0; + static const double scannerBorderWidth = 2.0; + + // Print Configuration + static const String printJobName = 'Barcode Scan Data'; + static const double printPageMargin = 72.0; // 1 inch in points +} \ No newline at end of file diff --git a/lib/core/core.dart b/lib/core/core.dart new file mode 100644 index 0000000..2a4dbec --- /dev/null +++ b/lib/core/core.dart @@ -0,0 +1,7 @@ +// Core module exports +export 'constants/app_constants.dart'; +export 'errors/exceptions.dart'; +export 'errors/failures.dart'; +export 'network/api_client.dart'; +export 'theme/app_theme.dart'; +export 'routing/app_router.dart'; \ No newline at end of file diff --git a/lib/core/errors/exceptions.dart b/lib/core/errors/exceptions.dart new file mode 100644 index 0000000..eee627d --- /dev/null +++ b/lib/core/errors/exceptions.dart @@ -0,0 +1,123 @@ +/// Base class for all exceptions in the application +/// Exceptions are thrown during runtime and should be caught and converted to failures +abstract class AppException implements Exception { + final String message; + final String? code; + + const AppException(this.message, {this.code}); + + @override + String toString() => 'AppException: $message${code != null ? ' (Code: $code)' : ''}'; +} + +/// Exception thrown when there's a server-related error +/// This includes HTTP errors, API response errors, etc. +class ServerException extends AppException { + const ServerException(super.message, {super.code}); + + @override + String toString() => 'ServerException: $message${code != null ? ' (Code: $code)' : ''}'; +} + +/// Exception thrown when there's a network-related error +/// This includes connection timeouts, no internet connection, etc. +class NetworkException extends AppException { + const NetworkException(super.message, {super.code}); + + @override + String toString() => 'NetworkException: $message${code != null ? ' (Code: $code)' : ''}'; +} + +/// Exception thrown when there's a local storage error +/// This includes Hive errors, file system errors, etc. +class CacheException extends AppException { + const CacheException(super.message, {super.code}); + + @override + String toString() => 'CacheException: $message${code != null ? ' (Code: $code)' : ''}'; +} + +/// Exception thrown when input validation fails +class ValidationException extends AppException { + final Map? fieldErrors; + + const ValidationException( + super.message, { + super.code, + this.fieldErrors, + }); + + @override + String toString() { + var result = 'ValidationException: $message${code != null ? ' (Code: $code)' : ''}'; + if (fieldErrors != null && fieldErrors!.isNotEmpty) { + result += '\nField errors: ${fieldErrors.toString()}'; + } + return result; + } +} + +/// Exception thrown when a required permission is denied +class PermissionException extends AppException { + final String permissionType; + + const PermissionException( + super.message, + this.permissionType, { + super.code, + }); + + @override + String toString() => + 'PermissionException: $message (Permission: $permissionType)${code != null ? ' (Code: $code)' : ''}'; +} + +/// Exception thrown when scanning operation fails +class ScannerException extends AppException { + const ScannerException(super.message, {super.code}); + + @override + String toString() => 'ScannerException: $message${code != null ? ' (Code: $code)' : ''}'; +} + +/// Exception thrown when printing operation fails +class PrintException extends AppException { + const PrintException(super.message, {super.code}); + + @override + String toString() => 'PrintException: $message${code != null ? ' (Code: $code)' : ''}'; +} + +/// Exception thrown for JSON parsing errors +class JsonException extends AppException { + const JsonException(super.message, {super.code}); + + @override + String toString() => 'JsonException: $message${code != null ? ' (Code: $code)' : ''}'; +} + +/// Exception thrown for format-related errors (e.g., invalid barcode format) +class FormatException extends AppException { + final String expectedFormat; + final String receivedFormat; + + const FormatException( + super.message, + this.expectedFormat, + this.receivedFormat, { + super.code, + }); + + @override + String toString() => + 'FormatException: $message (Expected: $expectedFormat, Received: $receivedFormat)${code != null ? ' (Code: $code)' : ''}'; +} + +/// Generic exception for unexpected errors +class UnknownException extends AppException { + const UnknownException([super.message = 'An unexpected error occurred', String? code]) + : super(code: code); + + @override + String toString() => 'UnknownException: $message${code != null ? ' (Code: $code)' : ''}'; +} \ No newline at end of file diff --git a/lib/core/errors/failures.dart b/lib/core/errors/failures.dart new file mode 100644 index 0000000..c01ca0c --- /dev/null +++ b/lib/core/errors/failures.dart @@ -0,0 +1,79 @@ +import 'package:equatable/equatable.dart'; + +/// Base class for all failures in the application +/// Failures represent errors that can be handled gracefully +abstract class Failure extends Equatable { + final String message; + + const Failure(this.message); + + @override + List get props => [message]; +} + +/// Failure that occurs when there's a server-related error +/// This includes HTTP errors, API errors, etc. +class ServerFailure extends Failure { + const ServerFailure(super.message); + + @override + String toString() => 'ServerFailure: $message'; +} + +/// Failure that occurs when there's a network-related error +/// This includes connection timeouts, no internet, etc. +class NetworkFailure extends Failure { + const NetworkFailure(super.message); + + @override + String toString() => 'NetworkFailure: $message'; +} + +/// Failure that occurs when there's a local storage error +/// This includes cache errors, database errors, etc. +class CacheFailure extends Failure { + const CacheFailure(super.message); + + @override + String toString() => 'CacheFailure: $message'; +} + +/// Failure that occurs when input validation fails +class ValidationFailure extends Failure { + const ValidationFailure(super.message); + + @override + String toString() => 'ValidationFailure: $message'; +} + +/// Failure that occurs when a required permission is denied +class PermissionFailure extends Failure { + const PermissionFailure(super.message); + + @override + String toString() => 'PermissionFailure: $message'; +} + +/// Failure that occurs when scanning operation fails +class ScannerFailure extends Failure { + const ScannerFailure(super.message); + + @override + String toString() => 'ScannerFailure: $message'; +} + +/// Failure that occurs when printing operation fails +class PrintFailure extends Failure { + const PrintFailure(super.message); + + @override + String toString() => 'PrintFailure: $message'; +} + +/// Generic failure for unexpected errors +class UnknownFailure extends Failure { + const UnknownFailure([super.message = 'An unexpected error occurred']); + + @override + String toString() => 'UnknownFailure: $message'; +} \ No newline at end of file diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart new file mode 100644 index 0000000..c7de0d8 --- /dev/null +++ b/lib/core/network/api_client.dart @@ -0,0 +1,175 @@ +import 'package:dio/dio.dart'; +import '../constants/app_constants.dart'; +import '../errors/exceptions.dart'; + +/// API client for making HTTP requests using Dio +class ApiClient { + late final Dio _dio; + + ApiClient() { + _dio = Dio( + BaseOptions( + baseUrl: AppConstants.apiBaseUrl, + connectTimeout: const Duration(milliseconds: AppConstants.connectionTimeout), + receiveTimeout: const Duration(milliseconds: AppConstants.receiveTimeout), + sendTimeout: const Duration(milliseconds: AppConstants.sendTimeout), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + ), + ); + + // Add request/response interceptors for logging and error handling + _dio.interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) { + // Log request details in debug mode + handler.next(options); + }, + onResponse: (response, handler) { + // Log response details in debug mode + handler.next(response); + }, + onError: (error, handler) { + // Handle different types of errors + _handleDioError(error); + handler.next(error); + }, + ), + ); + } + + /// Make a GET request + Future> get( + String path, { + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) async { + try { + return await _dio.get( + path, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ); + } on DioException catch (e) { + throw _handleDioError(e); + } + } + + /// Make a POST request + Future> post( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) async { + try { + return await _dio.post( + path, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ); + } on DioException catch (e) { + throw _handleDioError(e); + } + } + + /// Make a PUT request + Future> put( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) async { + try { + return await _dio.put( + path, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ); + } on DioException catch (e) { + throw _handleDioError(e); + } + } + + /// Make a DELETE request + Future> delete( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) async { + try { + return await _dio.delete( + path, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ); + } on DioException catch (e) { + throw _handleDioError(e); + } + } + + /// Handle Dio errors and convert them to custom exceptions + Exception _handleDioError(DioException error) { + switch (error.type) { + case DioExceptionType.connectionTimeout: + case DioExceptionType.sendTimeout: + case DioExceptionType.receiveTimeout: + return const NetworkException('Connection timeout. Please check your internet connection.'); + + case DioExceptionType.badResponse: + final statusCode = error.response?.statusCode; + final message = error.response?.data?['message'] ?? 'Server error occurred'; + + if (statusCode != null) { + if (statusCode >= 400 && statusCode < 500) { + return ServerException('Client error: $message (Status: $statusCode)'); + } else if (statusCode >= 500) { + return ServerException('Server error: $message (Status: $statusCode)'); + } + } + return ServerException('HTTP error: $message'); + + case DioExceptionType.cancel: + return const NetworkException('Request was cancelled'); + + case DioExceptionType.connectionError: + return const NetworkException('No internet connection. Please check your network settings.'); + + case DioExceptionType.badCertificate: + return const NetworkException('Certificate verification failed'); + + case DioExceptionType.unknown: + default: + return ServerException('An unexpected error occurred: ${error.message}'); + } + } + + /// Add authorization header + void addAuthorizationHeader(String token) { + _dio.options.headers['Authorization'] = 'Bearer $token'; + } + + /// Remove authorization header + void removeAuthorizationHeader() { + _dio.options.headers.remove('Authorization'); + } + + /// Update base URL (useful for different environments) + void updateBaseUrl(String newBaseUrl) { + _dio.options.baseUrl = newBaseUrl; + } +} \ No newline at end of file diff --git a/lib/core/routing/app_router.dart b/lib/core/routing/app_router.dart new file mode 100644 index 0000000..776c1c4 --- /dev/null +++ b/lib/core/routing/app_router.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../features/scanner/presentation/pages/home_page.dart'; +import '../../features/scanner/presentation/pages/detail_page.dart'; + +/// Application router configuration using GoRouter +final GoRouter appRouter = GoRouter( + initialLocation: '/', + debugLogDiagnostics: true, + routes: [ + // Home route - Main scanner screen + GoRoute( + path: '/', + name: 'home', + builder: (BuildContext context, GoRouterState state) { + return const HomePage(); + }, + ), + + // Detail route - Edit scan data + GoRoute( + path: '/detail/:barcode', + name: 'detail', + builder: (BuildContext context, GoRouterState state) { + final barcode = state.pathParameters['barcode']!; + return DetailPage(barcode: barcode); + }, + redirect: (BuildContext context, GoRouterState state) { + final barcode = state.pathParameters['barcode']; + + // Ensure barcode is not empty + if (barcode == null || barcode.trim().isEmpty) { + return '/'; + } + + return null; // No redirect needed + }, + ), + + // Settings route (optional for future expansion) + GoRoute( + path: '/settings', + name: 'settings', + builder: (BuildContext context, GoRouterState state) { + return const SettingsPlaceholderPage(); + }, + ), + + // About route (optional for future expansion) + GoRoute( + path: '/about', + name: 'about', + builder: (BuildContext context, GoRouterState state) { + return const AboutPlaceholderPage(); + }, + ), + ], + + // Error handling + errorBuilder: (context, state) { + return Scaffold( + appBar: AppBar( + title: const Text('Page Not Found'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 64, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(height: 16), + Text( + 'Page Not Found', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + Text( + 'The page "${state.path}" does not exist.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go Home'), + ), + ], + ), + ), + ); + }, + + // Redirect handler for authentication or onboarding (optional) + redirect: (BuildContext context, GoRouterState state) { + // Add any global redirect logic here + // For example, redirect to onboarding or login if needed + + return null; // No global redirect + }, +); + +/// Placeholder page for settings (for future implementation) +class SettingsPlaceholderPage extends StatelessWidget { + const SettingsPlaceholderPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Settings'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.pop(), + ), + ), + body: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.settings, + size: 64, + color: Colors.grey, + ), + SizedBox(height: 16), + Text( + 'Settings', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + Text( + 'Settings page coming soon', + style: TextStyle( + color: Colors.grey, + ), + ), + ], + ), + ), + ); + } +} + +/// Placeholder page for about (for future implementation) +class AboutPlaceholderPage extends StatelessWidget { + const AboutPlaceholderPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('About'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.pop(), + ), + ), + body: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.info_outline, + size: 64, + color: Colors.grey, + ), + SizedBox(height: 16), + Text( + 'Barcode Scanner App', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + Text( + 'Version 1.0.0', + style: TextStyle( + color: Colors.grey, + ), + ), + ], + ), + ), + ); + } +} + +/// Extension methods for easier navigation +extension AppRouterExtension on BuildContext { + /// Navigate to home page + void goHome() => go('/'); + + /// Navigate to detail page with barcode + void goToDetail(String barcode) => go('/detail/$barcode'); + + /// Navigate to settings + void goToSettings() => go('/settings'); + + /// Navigate to about page + void goToAbout() => go('/about'); +} \ No newline at end of file diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..31f64f2 --- /dev/null +++ b/lib/core/theme/app_theme.dart @@ -0,0 +1,298 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../constants/app_constants.dart'; + +/// Application theme configuration using Material Design 3 +class AppTheme { + AppTheme._(); + + // Color scheme for light theme + static const ColorScheme _lightColorScheme = ColorScheme( + brightness: Brightness.light, + primary: Color(0xFF1976D2), // Blue + onPrimary: Color(0xFFFFFFFF), + primaryContainer: Color(0xFFE3F2FD), + onPrimaryContainer: Color(0xFF0D47A1), + secondary: Color(0xFF757575), // Grey + onSecondary: Color(0xFFFFFFFF), + secondaryContainer: Color(0xFFE0E0E0), + onSecondaryContainer: Color(0xFF424242), + tertiary: Color(0xFF4CAF50), // Green + onTertiary: Color(0xFFFFFFFF), + tertiaryContainer: Color(0xFFE8F5E8), + onTertiaryContainer: Color(0xFF2E7D32), + error: Color(0xFFD32F2F), + onError: Color(0xFFFFFFFF), + errorContainer: Color(0xFFFFEBEE), + onErrorContainer: Color(0xFFB71C1C), + surface: Color(0xFFFFFFFF), + onSurface: Color(0xFF212121), + background: Color(0xFFF5F5F5), + onBackground: Color(0xFF616161), + onSurfaceVariant: Color(0xFF616161), + outline: Color(0xFFBDBDBD), + outlineVariant: Color(0xFFE0E0E0), + shadow: Color(0xFF000000), + scrim: Color(0xFF000000), + inverseSurface: Color(0xFF303030), + onInverseSurface: Color(0xFFF5F5F5), + inversePrimary: Color(0xFF90CAF9), + surfaceTint: Color(0xFF1976D2), + ); + + // Color scheme for dark theme + static const ColorScheme _darkColorScheme = ColorScheme( + brightness: Brightness.dark, + primary: Color(0xFF90CAF9), // Light Blue + onPrimary: Color(0xFF0D47A1), + primaryContainer: Color(0xFF1565C0), + onPrimaryContainer: Color(0xFFE3F2FD), + secondary: Color(0xFFBDBDBD), // Light Grey + onSecondary: Color(0xFF424242), + secondaryContainer: Color(0xFF616161), + onSecondaryContainer: Color(0xFFE0E0E0), + tertiary: Color(0xFF81C784), // Light Green + onTertiary: Color(0xFF2E7D32), + tertiaryContainer: Color(0xFF388E3C), + onTertiaryContainer: Color(0xFFE8F5E8), + error: Color(0xFFEF5350), + onError: Color(0xFFB71C1C), + errorContainer: Color(0xFFD32F2F), + onErrorContainer: Color(0xFFFFEBEE), + surface: Color(0xFF121212), + onSurface: Color(0xFFE0E0E0), + background: Color(0xFF2C2C2C), + onBackground: Color(0xFFBDBDBD), + onSurfaceVariant: Color(0xFFBDBDBD), + outline: Color(0xFF757575), + outlineVariant: Color(0xFF424242), + shadow: Color(0xFF000000), + scrim: Color(0xFF000000), + inverseSurface: Color(0xFFE0E0E0), + onInverseSurface: Color(0xFF303030), + inversePrimary: Color(0xFF1976D2), + surfaceTint: Color(0xFF90CAF9), + ); + + /// Light theme configuration + static ThemeData get lightTheme { + return ThemeData( + useMaterial3: true, + colorScheme: _lightColorScheme, + scaffoldBackgroundColor: _lightColorScheme.surface, + + // App Bar Theme + appBarTheme: AppBarTheme( + elevation: 0, + scrolledUnderElevation: 1, + backgroundColor: _lightColorScheme.surface, + foregroundColor: _lightColorScheme.onSurface, + titleTextStyle: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: _lightColorScheme.onSurface, + ), + systemOverlayStyle: SystemUiOverlayStyle.dark, + ), + + // Elevated Button Theme + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + elevation: 0, + minimumSize: const Size(double.infinity, AppConstants.buttonHeight), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Text Button Theme + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + minimumSize: const Size(double.infinity, AppConstants.buttonHeight), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Input Decoration Theme + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: _lightColorScheme.background, + contentPadding: const EdgeInsets.all(AppConstants.defaultPadding), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _lightColorScheme.outline), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _lightColorScheme.outline), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _lightColorScheme.primary, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _lightColorScheme.error), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _lightColorScheme.error, width: 2), + ), + labelStyle: TextStyle(color: _lightColorScheme.onSurfaceVariant), + hintStyle: TextStyle(color: _lightColorScheme.onSurfaceVariant), + ), + + + // List Tile Theme + listTileTheme: const ListTileThemeData( + contentPadding: EdgeInsets.symmetric( + horizontal: AppConstants.defaultPadding, + vertical: AppConstants.smallPadding, + ), + ), + + // Divider Theme + dividerTheme: DividerThemeData( + color: _lightColorScheme.outline, + thickness: 0.5, + ), + + // Progress Indicator Theme + progressIndicatorTheme: ProgressIndicatorThemeData( + color: _lightColorScheme.primary, + ), + + // Snack Bar Theme + snackBarTheme: SnackBarThemeData( + backgroundColor: _lightColorScheme.inverseSurface, + contentTextStyle: TextStyle(color: _lightColorScheme.onInverseSurface), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + behavior: SnackBarBehavior.floating, + ), + ); + } + + /// Dark theme configuration + static ThemeData get darkTheme { + return ThemeData( + useMaterial3: true, + colorScheme: _darkColorScheme, + scaffoldBackgroundColor: _darkColorScheme.surface, + + // App Bar Theme + appBarTheme: AppBarTheme( + elevation: 0, + scrolledUnderElevation: 1, + backgroundColor: _darkColorScheme.surface, + foregroundColor: _darkColorScheme.onSurface, + titleTextStyle: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: _darkColorScheme.onSurface, + ), + systemOverlayStyle: SystemUiOverlayStyle.light, + ), + + // Elevated Button Theme + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + elevation: 0, + minimumSize: const Size(double.infinity, AppConstants.buttonHeight), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Text Button Theme + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + minimumSize: const Size(double.infinity, AppConstants.buttonHeight), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Input Decoration Theme + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: _darkColorScheme.background, + contentPadding: const EdgeInsets.all(AppConstants.defaultPadding), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _darkColorScheme.outline), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _darkColorScheme.outline), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _darkColorScheme.primary, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _darkColorScheme.error), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + borderSide: BorderSide(color: _darkColorScheme.error, width: 2), + ), + labelStyle: TextStyle(color: _darkColorScheme.onSurfaceVariant), + hintStyle: TextStyle(color: _darkColorScheme.onSurfaceVariant), + ), + + + // List Tile Theme + listTileTheme: const ListTileThemeData( + contentPadding: EdgeInsets.symmetric( + horizontal: AppConstants.defaultPadding, + vertical: AppConstants.smallPadding, + ), + ), + + // Divider Theme + dividerTheme: DividerThemeData( + color: _darkColorScheme.outline, + thickness: 0.5, + ), + + // Progress Indicator Theme + progressIndicatorTheme: ProgressIndicatorThemeData( + color: _darkColorScheme.primary, + ), + + // Snack Bar Theme + snackBarTheme: SnackBarThemeData( + backgroundColor: _darkColorScheme.inverseSurface, + contentTextStyle: TextStyle(color: _darkColorScheme.onInverseSurface), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + behavior: SnackBarBehavior.floating, + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/scanner/data/data.dart b/lib/features/scanner/data/data.dart new file mode 100644 index 0000000..69da2a6 --- /dev/null +++ b/lib/features/scanner/data/data.dart @@ -0,0 +1,6 @@ +// Data layer exports +export 'datasources/scanner_local_datasource.dart'; +export 'datasources/scanner_remote_datasource.dart'; +export 'models/save_request_model.dart'; +export 'models/scan_item.dart'; +export 'repositories/scanner_repository_impl.dart'; \ No newline at end of file diff --git a/lib/features/scanner/data/datasources/scanner_local_datasource.dart b/lib/features/scanner/data/datasources/scanner_local_datasource.dart new file mode 100644 index 0000000..a86c656 --- /dev/null +++ b/lib/features/scanner/data/datasources/scanner_local_datasource.dart @@ -0,0 +1,229 @@ +import 'package:hive_ce/hive.dart'; +import '../../../../core/errors/exceptions.dart'; +import '../models/scan_item.dart'; + +/// Abstract local data source for scanner operations +abstract class ScannerLocalDataSource { + /// Save scan to local storage + Future saveScan(ScanItem scan); + + /// Get all scans from local storage + Future> getAllScans(); + + /// Get scan by barcode from local storage + Future getScanByBarcode(String barcode); + + /// Update scan in local storage + Future updateScan(ScanItem scan); + + /// Delete scan from local storage + Future deleteScan(String barcode); + + /// Clear all scans from local storage + Future clearAllScans(); +} + +/// Implementation of ScannerLocalDataSource using Hive +class ScannerLocalDataSourceImpl implements ScannerLocalDataSource { + static const String _boxName = 'scans'; + Box? _box; + + /// Initialize Hive box + Future> _getBox() async { + if (_box == null || !_box!.isOpen) { + try { + _box = await Hive.openBox(_boxName); + } catch (e) { + throw CacheException('Failed to open Hive box: ${e.toString()}'); + } + } + return _box!; + } + + @override + Future saveScan(ScanItem scan) async { + try { + final box = await _getBox(); + + // Use barcode as key to avoid duplicates + await box.put(scan.barcode, scan); + + // Optional: Log the save operation + // print('Scan saved locally: ${scan.barcode}'); + + } on CacheException { + rethrow; + } catch (e) { + throw CacheException('Failed to save scan locally: ${e.toString()}'); + } + } + + @override + Future> getAllScans() async { + try { + final box = await _getBox(); + + // Get all values from the box + final scans = box.values.toList(); + + // Sort by timestamp (most recent first) + scans.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + + return scans; + + } on CacheException { + rethrow; + } catch (e) { + throw CacheException('Failed to get scans from local storage: ${e.toString()}'); + } + } + + @override + Future getScanByBarcode(String barcode) async { + try { + if (barcode.trim().isEmpty) { + throw const ValidationException('Barcode cannot be empty'); + } + + final box = await _getBox(); + + // Get scan by barcode key + return box.get(barcode); + + } on ValidationException { + rethrow; + } on CacheException { + rethrow; + } catch (e) { + throw CacheException('Failed to get scan by barcode: ${e.toString()}'); + } + } + + @override + Future updateScan(ScanItem scan) async { + try { + final box = await _getBox(); + + // Check if scan exists + if (!box.containsKey(scan.barcode)) { + throw CacheException('Scan with barcode ${scan.barcode} not found'); + } + + // Update the scan + await box.put(scan.barcode, scan); + + // Optional: Log the update operation + // print('Scan updated locally: ${scan.barcode}'); + + } on CacheException { + rethrow; + } catch (e) { + throw CacheException('Failed to update scan locally: ${e.toString()}'); + } + } + + @override + Future deleteScan(String barcode) async { + try { + if (barcode.trim().isEmpty) { + throw const ValidationException('Barcode cannot be empty'); + } + + final box = await _getBox(); + + // Check if scan exists + if (!box.containsKey(barcode)) { + throw CacheException('Scan with barcode $barcode not found'); + } + + // Delete the scan + await box.delete(barcode); + + // Optional: Log the delete operation + // print('Scan deleted locally: $barcode'); + + } on ValidationException { + rethrow; + } on CacheException { + rethrow; + } catch (e) { + throw CacheException('Failed to delete scan locally: ${e.toString()}'); + } + } + + @override + Future clearAllScans() async { + try { + final box = await _getBox(); + + // Clear all scans + await box.clear(); + + // Optional: Log the clear operation + // print('All scans cleared from local storage'); + + } on CacheException { + rethrow; + } catch (e) { + throw CacheException('Failed to clear all scans: ${e.toString()}'); + } + } + + /// Get scans count (utility method) + Future getScansCount() async { + try { + final box = await _getBox(); + return box.length; + } on CacheException { + rethrow; + } catch (e) { + throw CacheException('Failed to get scans count: ${e.toString()}'); + } + } + + /// Check if scan exists (utility method) + Future scanExists(String barcode) async { + try { + if (barcode.trim().isEmpty) { + return false; + } + + final box = await _getBox(); + return box.containsKey(barcode); + } on CacheException { + rethrow; + } catch (e) { + throw CacheException('Failed to check if scan exists: ${e.toString()}'); + } + } + + /// Get scans within date range (utility method) + Future> getScansByDateRange({ + required DateTime startDate, + required DateTime endDate, + }) async { + try { + final allScans = await getAllScans(); + + // Filter by date range + final filteredScans = allScans.where((scan) { + return scan.timestamp.isAfter(startDate) && + scan.timestamp.isBefore(endDate); + }).toList(); + + return filteredScans; + } on CacheException { + rethrow; + } catch (e) { + throw CacheException('Failed to get scans by date range: ${e.toString()}'); + } + } + + /// Close the Hive box (call this when app is closing) + Future dispose() async { + if (_box != null && _box!.isOpen) { + await _box!.close(); + _box = null; + } + } +} \ No newline at end of file diff --git a/lib/features/scanner/data/datasources/scanner_remote_datasource.dart b/lib/features/scanner/data/datasources/scanner_remote_datasource.dart new file mode 100644 index 0000000..278aa8a --- /dev/null +++ b/lib/features/scanner/data/datasources/scanner_remote_datasource.dart @@ -0,0 +1,148 @@ +import '../../../../core/network/api_client.dart'; +import '../../../../core/errors/exceptions.dart'; +import '../models/save_request_model.dart'; + +/// Abstract remote data source for scanner operations +abstract class ScannerRemoteDataSource { + /// Save scan data to remote server + Future saveScan(SaveRequestModel request); + + /// Get scan data from remote server (optional for future use) + Future?> getScanData(String barcode); +} + +/// Implementation of ScannerRemoteDataSource using HTTP API +class ScannerRemoteDataSourceImpl implements ScannerRemoteDataSource { + final ApiClient apiClient; + + ScannerRemoteDataSourceImpl({required this.apiClient}); + + @override + Future saveScan(SaveRequestModel request) async { + try { + // Validate request before sending + if (!request.isValid) { + throw ValidationException('Invalid request data: ${request.validationErrors.join(', ')}'); + } + + final response = await apiClient.post( + '/api/scans', + data: request.toJson(), + ); + + // Check if the response indicates success + if (response.statusCode == null || + (response.statusCode! < 200 || response.statusCode! >= 300)) { + final errorMessage = response.data?['message'] ?? 'Unknown server error'; + throw ServerException('Failed to save scan: $errorMessage'); + } + + // Log successful save (in production, use proper logging) + // print('Scan saved successfully: ${request.barcode}'); + + } on ValidationException { + rethrow; + } on ServerException { + rethrow; + } on NetworkException { + rethrow; + } catch (e) { + // Handle any unexpected errors + throw ServerException('Unexpected error occurred while saving scan: ${e.toString()}'); + } + } + + @override + Future?> getScanData(String barcode) async { + try { + if (barcode.trim().isEmpty) { + throw const ValidationException('Barcode cannot be empty'); + } + + final response = await apiClient.get( + '/api/scans/$barcode', + ); + + if (response.statusCode == 404) { + // Scan not found is not an error, just return null + return null; + } + + if (response.statusCode == null || + (response.statusCode! < 200 || response.statusCode! >= 300)) { + final errorMessage = response.data?['message'] ?? 'Unknown server error'; + throw ServerException('Failed to get scan data: $errorMessage'); + } + + return response.data as Map?; + + } on ValidationException { + rethrow; + } on ServerException { + rethrow; + } on NetworkException { + rethrow; + } catch (e) { + throw ServerException('Unexpected error occurred while getting scan data: ${e.toString()}'); + } + } + + /// Update scan data on remote server (optional for future use) + Future updateScan(String barcode, SaveRequestModel request) async { + try { + if (barcode.trim().isEmpty) { + throw const ValidationException('Barcode cannot be empty'); + } + + if (!request.isValid) { + throw ValidationException('Invalid request data: ${request.validationErrors.join(', ')}'); + } + + final response = await apiClient.put( + '/api/scans/$barcode', + data: request.toJson(), + ); + + if (response.statusCode == null || + (response.statusCode! < 200 || response.statusCode! >= 300)) { + final errorMessage = response.data?['message'] ?? 'Unknown server error'; + throw ServerException('Failed to update scan: $errorMessage'); + } + + } on ValidationException { + rethrow; + } on ServerException { + rethrow; + } on NetworkException { + rethrow; + } catch (e) { + throw ServerException('Unexpected error occurred while updating scan: ${e.toString()}'); + } + } + + /// Delete scan data from remote server (optional for future use) + Future deleteScan(String barcode) async { + try { + if (barcode.trim().isEmpty) { + throw const ValidationException('Barcode cannot be empty'); + } + + final response = await apiClient.delete('/api/scans/$barcode'); + + if (response.statusCode == null || + (response.statusCode! < 200 || response.statusCode! >= 300)) { + final errorMessage = response.data?['message'] ?? 'Unknown server error'; + throw ServerException('Failed to delete scan: $errorMessage'); + } + + } on ValidationException { + rethrow; + } on ServerException { + rethrow; + } on NetworkException { + rethrow; + } catch (e) { + throw ServerException('Unexpected error occurred while deleting scan: ${e.toString()}'); + } + } +} \ No newline at end of file diff --git a/lib/features/scanner/data/models/save_request_model.dart b/lib/features/scanner/data/models/save_request_model.dart new file mode 100644 index 0000000..327950c --- /dev/null +++ b/lib/features/scanner/data/models/save_request_model.dart @@ -0,0 +1,134 @@ +import 'package:json_annotation/json_annotation.dart'; +import '../../domain/entities/scan_entity.dart'; + +part 'save_request_model.g.dart'; + +/// API request model for saving scan data to the server +@JsonSerializable() +class SaveRequestModel { + final String barcode; + final String field1; + final String field2; + final String field3; + final String field4; + + SaveRequestModel({ + required this.barcode, + required this.field1, + required this.field2, + required this.field3, + required this.field4, + }); + + /// Create from domain entity + factory SaveRequestModel.fromEntity(ScanEntity entity) { + return SaveRequestModel( + barcode: entity.barcode, + field1: entity.field1, + field2: entity.field2, + field3: entity.field3, + field4: entity.field4, + ); + } + + /// Create from parameters + factory SaveRequestModel.fromParams({ + required String barcode, + required String field1, + required String field2, + required String field3, + required String field4, + }) { + return SaveRequestModel( + barcode: barcode, + field1: field1, + field2: field2, + field3: field3, + field4: field4, + ); + } + + /// Create from JSON + factory SaveRequestModel.fromJson(Map json) => + _$SaveRequestModelFromJson(json); + + /// Convert to JSON for API requests + Map toJson() => _$SaveRequestModelToJson(this); + + /// Create a copy with updated fields + SaveRequestModel copyWith({ + String? barcode, + String? field1, + String? field2, + String? field3, + String? field4, + }) { + return SaveRequestModel( + barcode: barcode ?? this.barcode, + field1: field1 ?? this.field1, + field2: field2 ?? this.field2, + field3: field3 ?? this.field3, + field4: field4 ?? this.field4, + ); + } + + /// Validate the request data + bool get isValid { + return barcode.trim().isNotEmpty && + field1.trim().isNotEmpty && + field2.trim().isNotEmpty && + field3.trim().isNotEmpty && + field4.trim().isNotEmpty; + } + + /// Get validation errors + List get validationErrors { + final errors = []; + + if (barcode.trim().isEmpty) { + errors.add('Barcode is required'); + } + + if (field1.trim().isEmpty) { + errors.add('Field 1 is required'); + } + + if (field2.trim().isEmpty) { + errors.add('Field 2 is required'); + } + + if (field3.trim().isEmpty) { + errors.add('Field 3 is required'); + } + + if (field4.trim().isEmpty) { + errors.add('Field 4 is required'); + } + + return errors; + } + + @override + String toString() { + return 'SaveRequestModel{barcode: $barcode, field1: $field1, field2: $field2, field3: $field3, field4: $field4}'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SaveRequestModel && + runtimeType == other.runtimeType && + barcode == other.barcode && + field1 == other.field1 && + field2 == other.field2 && + field3 == other.field3 && + field4 == other.field4; + + @override + int get hashCode => + barcode.hashCode ^ + field1.hashCode ^ + field2.hashCode ^ + field3.hashCode ^ + field4.hashCode; +} \ No newline at end of file diff --git a/lib/features/scanner/data/models/save_request_model.g.dart b/lib/features/scanner/data/models/save_request_model.g.dart new file mode 100644 index 0000000..cf53724 --- /dev/null +++ b/lib/features/scanner/data/models/save_request_model.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'save_request_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SaveRequestModel _$SaveRequestModelFromJson(Map json) => + SaveRequestModel( + barcode: json['barcode'] as String, + field1: json['field1'] as String, + field2: json['field2'] as String, + field3: json['field3'] as String, + field4: json['field4'] as String, + ); + +Map _$SaveRequestModelToJson(SaveRequestModel instance) => + { + 'barcode': instance.barcode, + 'field1': instance.field1, + 'field2': instance.field2, + 'field3': instance.field3, + 'field4': instance.field4, + }; diff --git a/lib/features/scanner/data/models/scan_item.dart b/lib/features/scanner/data/models/scan_item.dart new file mode 100644 index 0000000..a025078 --- /dev/null +++ b/lib/features/scanner/data/models/scan_item.dart @@ -0,0 +1,131 @@ +import 'package:hive_ce/hive.dart'; +import '../../domain/entities/scan_entity.dart'; + +part 'scan_item.g.dart'; + +/// Data model for ScanEntity with Hive annotations for local storage +/// This is the data layer representation that can be persisted +@HiveType(typeId: 0) +class ScanItem extends HiveObject { + @HiveField(0) + final String barcode; + + @HiveField(1) + final DateTime timestamp; + + @HiveField(2) + final String field1; + + @HiveField(3) + final String field2; + + @HiveField(4) + final String field3; + + @HiveField(5) + final String field4; + + ScanItem({ + required this.barcode, + required this.timestamp, + this.field1 = '', + this.field2 = '', + this.field3 = '', + this.field4 = '', + }); + + /// Convert from domain entity to data model + factory ScanItem.fromEntity(ScanEntity entity) { + return ScanItem( + barcode: entity.barcode, + timestamp: entity.timestamp, + field1: entity.field1, + field2: entity.field2, + field3: entity.field3, + field4: entity.field4, + ); + } + + /// Convert to domain entity + ScanEntity toEntity() { + return ScanEntity( + barcode: barcode, + timestamp: timestamp, + field1: field1, + field2: field2, + field3: field3, + field4: field4, + ); + } + + /// Create from JSON (useful for API responses) + factory ScanItem.fromJson(Map json) { + return ScanItem( + barcode: json['barcode'] ?? '', + timestamp: json['timestamp'] != null + ? DateTime.parse(json['timestamp']) + : DateTime.now(), + field1: json['field1'] ?? '', + field2: json['field2'] ?? '', + field3: json['field3'] ?? '', + field4: json['field4'] ?? '', + ); + } + + /// Convert to JSON (useful for API requests) + Map toJson() { + return { + 'barcode': barcode, + 'timestamp': timestamp.toIso8601String(), + 'field1': field1, + 'field2': field2, + 'field3': field3, + 'field4': field4, + }; + } + + /// Create a copy with updated fields + ScanItem copyWith({ + String? barcode, + DateTime? timestamp, + String? field1, + String? field2, + String? field3, + String? field4, + }) { + return ScanItem( + barcode: barcode ?? this.barcode, + timestamp: timestamp ?? this.timestamp, + field1: field1 ?? this.field1, + field2: field2 ?? this.field2, + field3: field3 ?? this.field3, + field4: field4 ?? this.field4, + ); + } + + @override + String toString() { + return 'ScanItem{barcode: $barcode, timestamp: $timestamp, field1: $field1, field2: $field2, field3: $field3, field4: $field4}'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ScanItem && + runtimeType == other.runtimeType && + barcode == other.barcode && + timestamp == other.timestamp && + field1 == other.field1 && + field2 == other.field2 && + field3 == other.field3 && + field4 == other.field4; + + @override + int get hashCode => + barcode.hashCode ^ + timestamp.hashCode ^ + field1.hashCode ^ + field2.hashCode ^ + field3.hashCode ^ + field4.hashCode; +} \ No newline at end of file diff --git a/lib/features/scanner/data/models/scan_item.g.dart b/lib/features/scanner/data/models/scan_item.g.dart new file mode 100644 index 0000000..8ff65c6 --- /dev/null +++ b/lib/features/scanner/data/models/scan_item.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'scan_item.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ScanItemAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + ScanItem read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ScanItem( + barcode: fields[0] as String, + timestamp: fields[1] as DateTime, + field1: fields[2] as String, + field2: fields[3] as String, + field3: fields[4] as String, + field4: fields[5] as String, + ); + } + + @override + void write(BinaryWriter writer, ScanItem obj) { + writer + ..writeByte(6) + ..writeByte(0) + ..write(obj.barcode) + ..writeByte(1) + ..write(obj.timestamp) + ..writeByte(2) + ..write(obj.field1) + ..writeByte(3) + ..write(obj.field2) + ..writeByte(4) + ..write(obj.field3) + ..writeByte(5) + ..write(obj.field4); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ScanItemAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/features/scanner/data/repositories/scanner_repository_impl.dart b/lib/features/scanner/data/repositories/scanner_repository_impl.dart new file mode 100644 index 0000000..1c0cb1a --- /dev/null +++ b/lib/features/scanner/data/repositories/scanner_repository_impl.dart @@ -0,0 +1,265 @@ +import 'package:dartz/dartz.dart'; +import '../../../../core/errors/failures.dart'; +import '../../../../core/errors/exceptions.dart'; +import '../../domain/entities/scan_entity.dart'; +import '../../domain/repositories/scanner_repository.dart'; +import '../datasources/scanner_local_datasource.dart'; +import '../datasources/scanner_remote_datasource.dart'; +import '../models/save_request_model.dart'; +import '../models/scan_item.dart'; + +/// Implementation of ScannerRepository +/// This class handles the coordination between remote and local data sources +class ScannerRepositoryImpl implements ScannerRepository { + final ScannerRemoteDataSource remoteDataSource; + final ScannerLocalDataSource localDataSource; + + ScannerRepositoryImpl({ + required this.remoteDataSource, + required this.localDataSource, + }); + + @override + Future> saveScan({ + required String barcode, + required String field1, + required String field2, + required String field3, + required String field4, + }) async { + try { + // Create the request model + final request = SaveRequestModel.fromParams( + barcode: barcode, + field1: field1, + field2: field2, + field3: field3, + field4: field4, + ); + + // Validate the request + if (!request.isValid) { + return Left(ValidationFailure(request.validationErrors.join(', '))); + } + + // Save to remote server + await remoteDataSource.saveScan(request); + + // If remote save succeeds, we return success + // Local save will be handled separately by the use case if needed + return const Right(null); + + } on ValidationException catch (e) { + return Left(ValidationFailure(e.message)); + } on NetworkException catch (e) { + return Left(NetworkFailure(e.message)); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } catch (e) { + return Left(UnknownFailure('Failed to save scan: ${e.toString()}')); + } + } + + @override + Future>> getScanHistory() async { + try { + // Get scans from local storage + final scanItems = await localDataSource.getAllScans(); + + // Convert to domain entities + final entities = scanItems.map((item) => item.toEntity()).toList(); + + return Right(entities); + + } on CacheException catch (e) { + return Left(CacheFailure(e.message)); + } catch (e) { + return Left(UnknownFailure('Failed to get scan history: ${e.toString()}')); + } + } + + @override + Future> saveScanLocally(ScanEntity scan) async { + try { + // Convert entity to data model + final scanItem = ScanItem.fromEntity(scan); + + // Save to local storage + await localDataSource.saveScan(scanItem); + + return const Right(null); + + } on CacheException catch (e) { + return Left(CacheFailure(e.message)); + } catch (e) { + return Left(UnknownFailure('Failed to save scan locally: ${e.toString()}')); + } + } + + @override + Future> deleteScanLocally(String barcode) async { + try { + if (barcode.trim().isEmpty) { + return const Left(ValidationFailure('Barcode cannot be empty')); + } + + // Delete from local storage + await localDataSource.deleteScan(barcode); + + return const Right(null); + + } on ValidationException catch (e) { + return Left(ValidationFailure(e.message)); + } on CacheException catch (e) { + return Left(CacheFailure(e.message)); + } catch (e) { + return Left(UnknownFailure('Failed to delete scan: ${e.toString()}')); + } + } + + @override + Future> clearScanHistory() async { + try { + // Clear all scans from local storage + await localDataSource.clearAllScans(); + + return const Right(null); + + } on CacheException catch (e) { + return Left(CacheFailure(e.message)); + } catch (e) { + return Left(UnknownFailure('Failed to clear scan history: ${e.toString()}')); + } + } + + @override + Future> getScanByBarcode(String barcode) async { + try { + if (barcode.trim().isEmpty) { + return const Left(ValidationFailure('Barcode cannot be empty')); + } + + // Get scan from local storage + final scanItem = await localDataSource.getScanByBarcode(barcode); + + if (scanItem == null) { + return const Right(null); + } + + // Convert to domain entity + final entity = scanItem.toEntity(); + + return Right(entity); + + } on ValidationException catch (e) { + return Left(ValidationFailure(e.message)); + } on CacheException catch (e) { + return Left(CacheFailure(e.message)); + } catch (e) { + return Left(UnknownFailure('Failed to get scan by barcode: ${e.toString()}')); + } + } + + @override + Future> updateScanLocally(ScanEntity scan) async { + try { + // Convert entity to data model + final scanItem = ScanItem.fromEntity(scan); + + // Update in local storage + await localDataSource.updateScan(scanItem); + + return const Right(null); + + } on CacheException catch (e) { + return Left(CacheFailure(e.message)); + } catch (e) { + return Left(UnknownFailure('Failed to update scan: ${e.toString()}')); + } + } + + /// Additional utility methods for repository + + /// Get scans count + Future> getScansCount() async { + try { + if (localDataSource is ScannerLocalDataSourceImpl) { + final impl = localDataSource as ScannerLocalDataSourceImpl; + final count = await impl.getScansCount(); + return Right(count); + } + + // Fallback: get all scans and count them + final result = await getScanHistory(); + return result.fold( + (failure) => Left(failure), + (scans) => Right(scans.length), + ); + + } catch (e) { + return Left(UnknownFailure('Failed to get scans count: ${e.toString()}')); + } + } + + /// Check if scan exists locally + Future> scanExistsLocally(String barcode) async { + try { + if (barcode.trim().isEmpty) { + return const Left(ValidationFailure('Barcode cannot be empty')); + } + + if (localDataSource is ScannerLocalDataSourceImpl) { + final impl = localDataSource as ScannerLocalDataSourceImpl; + final exists = await impl.scanExists(barcode); + return Right(exists); + } + + // Fallback: get scan by barcode + final result = await getScanByBarcode(barcode); + return result.fold( + (failure) => Left(failure), + (scan) => Right(scan != null), + ); + + } catch (e) { + return Left(UnknownFailure('Failed to check if scan exists: ${e.toString()}')); + } + } + + /// Get scans by date range + Future>> getScansByDateRange({ + required DateTime startDate, + required DateTime endDate, + }) async { + try { + if (localDataSource is ScannerLocalDataSourceImpl) { + final impl = localDataSource as ScannerLocalDataSourceImpl; + final scanItems = await impl.getScansByDateRange( + startDate: startDate, + endDate: endDate, + ); + + // Convert to domain entities + final entities = scanItems.map((item) => item.toEntity()).toList(); + return Right(entities); + } + + // Fallback: get all scans and filter + final result = await getScanHistory(); + return result.fold( + (failure) => Left(failure), + (scans) { + final filteredScans = scans + .where((scan) => + scan.timestamp.isAfter(startDate) && + scan.timestamp.isBefore(endDate)) + .toList(); + return Right(filteredScans); + }, + ); + + } catch (e) { + return Left(UnknownFailure('Failed to get scans by date range: ${e.toString()}')); + } + } +} \ No newline at end of file diff --git a/lib/features/scanner/domain/domain.dart b/lib/features/scanner/domain/domain.dart new file mode 100644 index 0000000..2e1b372 --- /dev/null +++ b/lib/features/scanner/domain/domain.dart @@ -0,0 +1,5 @@ +// Domain layer exports +export 'entities/scan_entity.dart'; +export 'repositories/scanner_repository.dart'; +export 'usecases/get_scan_history_usecase.dart'; +export 'usecases/save_scan_usecase.dart'; \ No newline at end of file diff --git a/lib/features/scanner/domain/entities/scan_entity.dart b/lib/features/scanner/domain/entities/scan_entity.dart new file mode 100644 index 0000000..fe01e48 --- /dev/null +++ b/lib/features/scanner/domain/entities/scan_entity.dart @@ -0,0 +1,71 @@ +import 'package:equatable/equatable.dart'; + +/// Domain entity representing a scan item +/// This is the business logic representation without any external dependencies +class ScanEntity extends Equatable { + final String barcode; + final DateTime timestamp; + final String field1; + final String field2; + final String field3; + final String field4; + + const ScanEntity({ + required this.barcode, + required this.timestamp, + this.field1 = '', + this.field2 = '', + this.field3 = '', + this.field4 = '', + }); + + /// Create a copy with updated fields + ScanEntity copyWith({ + String? barcode, + DateTime? timestamp, + String? field1, + String? field2, + String? field3, + String? field4, + }) { + return ScanEntity( + barcode: barcode ?? this.barcode, + timestamp: timestamp ?? this.timestamp, + field1: field1 ?? this.field1, + field2: field2 ?? this.field2, + field3: field3 ?? this.field3, + field4: field4 ?? this.field4, + ); + } + + /// Check if the entity has any form data + bool get hasFormData { + return field1.isNotEmpty || + field2.isNotEmpty || + field3.isNotEmpty || + field4.isNotEmpty; + } + + /// Check if all form fields are filled + bool get isFormComplete { + return field1.isNotEmpty && + field2.isNotEmpty && + field3.isNotEmpty && + field4.isNotEmpty; + } + + @override + List get props => [ + barcode, + timestamp, + field1, + field2, + field3, + field4, + ]; + + @override + String toString() { + return 'ScanEntity{barcode: $barcode, timestamp: $timestamp, field1: $field1, field2: $field2, field3: $field3, field4: $field4}'; + } +} \ No newline at end of file diff --git a/lib/features/scanner/domain/repositories/scanner_repository.dart b/lib/features/scanner/domain/repositories/scanner_repository.dart new file mode 100644 index 0000000..6186536 --- /dev/null +++ b/lib/features/scanner/domain/repositories/scanner_repository.dart @@ -0,0 +1,34 @@ +import 'package:dartz/dartz.dart'; +import '../../../../core/errors/failures.dart'; +import '../entities/scan_entity.dart'; + +/// Abstract repository interface for scanner operations +/// This defines the contract that the data layer must implement +abstract class ScannerRepository { + /// Save scan data to remote server + Future> saveScan({ + required String barcode, + required String field1, + required String field2, + required String field3, + required String field4, + }); + + /// Get scan history from local storage + Future>> getScanHistory(); + + /// Save scan to local storage + Future> saveScanLocally(ScanEntity scan); + + /// Delete a scan from local storage + Future> deleteScanLocally(String barcode); + + /// Clear all scan history from local storage + Future> clearScanHistory(); + + /// Get a specific scan by barcode from local storage + Future> getScanByBarcode(String barcode); + + /// Update a scan in local storage + Future> updateScanLocally(ScanEntity scan); +} \ No newline at end of file diff --git a/lib/features/scanner/domain/usecases/get_scan_history_usecase.dart b/lib/features/scanner/domain/usecases/get_scan_history_usecase.dart new file mode 100644 index 0000000..35eefdc --- /dev/null +++ b/lib/features/scanner/domain/usecases/get_scan_history_usecase.dart @@ -0,0 +1,113 @@ +import 'package:dartz/dartz.dart'; +import '../../../../core/errors/failures.dart'; +import '../entities/scan_entity.dart'; +import '../repositories/scanner_repository.dart'; + +/// Use case for retrieving scan history +/// Handles the business logic for fetching scan history from local storage +class GetScanHistoryUseCase { + final ScannerRepository repository; + + GetScanHistoryUseCase(this.repository); + + /// Execute the get scan history operation + /// + /// Returns a list of scan entities sorted by timestamp (most recent first) + Future>> call() async { + try { + final result = await repository.getScanHistory(); + + return result.fold( + (failure) => Left(failure), + (scans) { + // Sort scans by timestamp (most recent first) + final sortedScans = List.from(scans); + sortedScans.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + return Right(sortedScans); + }, + ); + } catch (e) { + return Left(UnknownFailure('Failed to get scan history: ${e.toString()}')); + } + } + + /// Get scan history filtered by date range + Future>> getHistoryInDateRange({ + required DateTime startDate, + required DateTime endDate, + }) async { + try { + final result = await repository.getScanHistory(); + + return result.fold( + (failure) => Left(failure), + (scans) { + // Filter scans by date range + final filteredScans = scans + .where((scan) => + scan.timestamp.isAfter(startDate) && + scan.timestamp.isBefore(endDate)) + .toList(); + + // Sort by timestamp (most recent first) + filteredScans.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + + return Right(filteredScans); + }, + ); + } catch (e) { + return Left(UnknownFailure('Failed to get scan history: ${e.toString()}')); + } + } + + /// Get scans that have form data (non-empty fields) + Future>> getScansWithFormData() async { + try { + final result = await repository.getScanHistory(); + + return result.fold( + (failure) => Left(failure), + (scans) { + // Filter scans that have form data + final filteredScans = scans.where((scan) => scan.hasFormData).toList(); + + // Sort by timestamp (most recent first) + filteredScans.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + + return Right(filteredScans); + }, + ); + } catch (e) { + return Left(UnknownFailure('Failed to get scan history: ${e.toString()}')); + } + } + + /// Search scans by barcode pattern + Future>> searchByBarcode(String pattern) async { + try { + if (pattern.trim().isEmpty) { + return const Right([]); + } + + final result = await repository.getScanHistory(); + + return result.fold( + (failure) => Left(failure), + (scans) { + // Filter scans by barcode pattern (case-insensitive) + final filteredScans = scans + .where((scan) => + scan.barcode.toLowerCase().contains(pattern.toLowerCase())) + .toList(); + + // Sort by timestamp (most recent first) + filteredScans.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + + return Right(filteredScans); + }, + ); + } catch (e) { + return Left(UnknownFailure('Failed to search scans: ${e.toString()}')); + } + } +} \ No newline at end of file diff --git a/lib/features/scanner/domain/usecases/save_scan_usecase.dart b/lib/features/scanner/domain/usecases/save_scan_usecase.dart new file mode 100644 index 0000000..c1256b2 --- /dev/null +++ b/lib/features/scanner/domain/usecases/save_scan_usecase.dart @@ -0,0 +1,109 @@ +import 'package:dartz/dartz.dart'; +import '../../../../core/errors/failures.dart'; +import '../entities/scan_entity.dart'; +import '../repositories/scanner_repository.dart'; + +/// Use case for saving scan data +/// Handles the business logic for saving scan information to both remote and local storage +class SaveScanUseCase { + final ScannerRepository repository; + + SaveScanUseCase(this.repository); + + /// Execute the save scan operation + /// + /// First saves to remote server, then saves locally only if remote save succeeds + /// This ensures data consistency and allows for offline-first behavior + Future> call(SaveScanParams params) async { + // Validate input parameters + final validationResult = _validateParams(params); + if (validationResult != null) { + return Left(ValidationFailure(validationResult)); + } + + try { + // Save to remote server first + final remoteResult = await repository.saveScan( + barcode: params.barcode, + field1: params.field1, + field2: params.field2, + field3: params.field3, + field4: params.field4, + ); + + return remoteResult.fold( + (failure) => Left(failure), + (_) async { + // If remote save succeeds, save to local storage + final scanEntity = ScanEntity( + barcode: params.barcode, + timestamp: DateTime.now(), + field1: params.field1, + field2: params.field2, + field3: params.field3, + field4: params.field4, + ); + + final localResult = await repository.saveScanLocally(scanEntity); + return localResult.fold( + (failure) { + // Log the local save failure but don't fail the entire operation + // since remote save succeeded + return const Right(null); + }, + (_) => const Right(null), + ); + }, + ); + } catch (e) { + return Left(UnknownFailure('Failed to save scan: ${e.toString()}')); + } + } + + /// Validate the input parameters + String? _validateParams(SaveScanParams params) { + if (params.barcode.trim().isEmpty) { + return 'Barcode cannot be empty'; + } + + if (params.field1.trim().isEmpty) { + return 'Field 1 cannot be empty'; + } + + if (params.field2.trim().isEmpty) { + return 'Field 2 cannot be empty'; + } + + if (params.field3.trim().isEmpty) { + return 'Field 3 cannot be empty'; + } + + if (params.field4.trim().isEmpty) { + return 'Field 4 cannot be empty'; + } + + return null; + } +} + +/// Parameters for the SaveScanUseCase +class SaveScanParams { + final String barcode; + final String field1; + final String field2; + final String field3; + final String field4; + + SaveScanParams({ + required this.barcode, + required this.field1, + required this.field2, + required this.field3, + required this.field4, + }); + + @override + String toString() { + return 'SaveScanParams{barcode: $barcode, field1: $field1, field2: $field2, field3: $field3, field4: $field4}'; + } +} \ No newline at end of file diff --git a/lib/features/scanner/presentation/pages/detail_page.dart b/lib/features/scanner/presentation/pages/detail_page.dart new file mode 100644 index 0000000..99e35e9 --- /dev/null +++ b/lib/features/scanner/presentation/pages/detail_page.dart @@ -0,0 +1,334 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../data/models/scan_item.dart'; +import '../providers/form_provider.dart'; +import '../providers/scanner_provider.dart'; + +/// Detail page for editing scan data with 4 text fields and Save/Print buttons +class DetailPage extends ConsumerStatefulWidget { + final String barcode; + + const DetailPage({ + required this.barcode, + super.key, + }); + + @override + ConsumerState createState() => _DetailPageState(); +} + +class _DetailPageState extends ConsumerState { + late final TextEditingController _field1Controller; + late final TextEditingController _field2Controller; + late final TextEditingController _field3Controller; + late final TextEditingController _field4Controller; + + @override + void initState() { + super.initState(); + _field1Controller = TextEditingController(); + _field2Controller = TextEditingController(); + _field3Controller = TextEditingController(); + _field4Controller = TextEditingController(); + + // Initialize controllers with existing data if available + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadExistingData(); + }); + } + + @override + void dispose() { + _field1Controller.dispose(); + _field2Controller.dispose(); + _field3Controller.dispose(); + _field4Controller.dispose(); + super.dispose(); + } + + /// Load existing data from history if available + void _loadExistingData() { + final history = ref.read(scanHistoryProvider); + final existingScan = history.firstWhere( + (item) => item.barcode == widget.barcode, + orElse: () => ScanItem(barcode: widget.barcode, timestamp: DateTime.now()), + ); + + _field1Controller.text = existingScan.field1; + _field2Controller.text = existingScan.field2; + _field3Controller.text = existingScan.field3; + _field4Controller.text = existingScan.field4; + + // Update form provider with existing data + final formNotifier = ref.read(formProviderFamily(widget.barcode).notifier); + formNotifier.populateWithScanItem(existingScan); + } + + @override + Widget build(BuildContext context) { + final formState = ref.watch(formProviderFamily(widget.barcode)); + final formNotifier = ref.read(formProviderFamily(widget.barcode).notifier); + + // Listen to form state changes for navigation + ref.listen( + formProviderFamily(widget.barcode), + (previous, next) { + if (next.isSaveSuccess && (previous?.isSaveSuccess != true)) { + _showSuccessAndNavigateBack(context); + } + }, + ); + + return Scaffold( + appBar: AppBar( + title: const Text('Edit Details'), + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.pop(), + ), + ), + body: Column( + children: [ + // Barcode Header + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 1, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Barcode', + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Text( + widget.barcode, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + fontFamily: 'monospace', + ), + ), + ], + ), + ), + + // Form Fields + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // Field 1 + _buildTextField( + controller: _field1Controller, + label: 'Field 1', + onChanged: formNotifier.updateField1, + ), + const SizedBox(height: 16), + + // Field 2 + _buildTextField( + controller: _field2Controller, + label: 'Field 2', + onChanged: formNotifier.updateField2, + ), + const SizedBox(height: 16), + + // Field 3 + _buildTextField( + controller: _field3Controller, + label: 'Field 3', + onChanged: formNotifier.updateField3, + ), + const SizedBox(height: 16), + + // Field 4 + _buildTextField( + controller: _field4Controller, + label: 'Field 4', + onChanged: formNotifier.updateField4, + ), + const SizedBox(height: 24), + + // Error Message + if (formState.error != null) ...[ + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Theme.of(context).colorScheme.error, + width: 1, + ), + ), + child: Row( + children: [ + Icon( + Icons.error_outline, + color: Theme.of(context).colorScheme.error, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + formState.error!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + ], + ), + ), + ), + + // Action Buttons + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + top: BorderSide( + color: Theme.of(context).dividerColor, + width: 1, + ), + ), + ), + child: SafeArea( + child: Row( + children: [ + // Save Button + Expanded( + child: ElevatedButton( + onPressed: formState.isLoading ? null : () => _saveData(formNotifier), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + minimumSize: const Size.fromHeight(48), + ), + child: formState.isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text('Save'), + ), + ), + const SizedBox(width: 16), + + // Print Button + Expanded( + child: OutlinedButton( + onPressed: formState.isLoading ? null : () => _printData(formNotifier), + style: OutlinedButton.styleFrom( + minimumSize: const Size.fromHeight(48), + ), + child: const Text('Print'), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + /// Build text field widget + Widget _buildTextField({ + required TextEditingController controller, + required String label, + required void Function(String) onChanged, + }) { + return TextField( + controller: controller, + decoration: InputDecoration( + labelText: label, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3), + ), + textCapitalization: TextCapitalization.sentences, + onChanged: onChanged, + ); + } + + /// Save form data + Future _saveData(FormNotifier formNotifier) async { + // Clear any previous errors + formNotifier.clearError(); + + // Attempt to save + await formNotifier.saveData(); + } + + /// Print form data + Future _printData(FormNotifier formNotifier) async { + try { + await formNotifier.printData(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Print dialog opened'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Print failed: ${e.toString()}'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } + } + + /// Show success message and navigate back + void _showSuccessAndNavigateBack(BuildContext context) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Data saved successfully!'), + backgroundColor: Colors.green, + duration: Duration(seconds: 2), + ), + ); + + // Navigate back after a short delay + Future.delayed(const Duration(milliseconds: 1500), () { + if (mounted) { + context.pop(); + } + }); + } +} \ No newline at end of file diff --git a/lib/features/scanner/presentation/pages/home_page.dart b/lib/features/scanner/presentation/pages/home_page.dart new file mode 100644 index 0000000..d4f7f5a --- /dev/null +++ b/lib/features/scanner/presentation/pages/home_page.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../providers/scanner_provider.dart'; +import '../widgets/barcode_scanner_widget.dart'; +import '../widgets/scan_result_display.dart'; +import '../widgets/scan_history_list.dart'; + +/// Home page with barcode scanner, result display, and history list +class HomePage extends ConsumerWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final scannerState = ref.watch(scannerProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('Barcode Scanner'), + elevation: 0, + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + ref.read(scannerProvider.notifier).refreshHistory(); + }, + tooltip: 'Refresh History', + ), + ], + ), + body: Column( + children: [ + // Barcode Scanner Section (Top Half) + Expanded( + flex: 1, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: const BarcodeScannerWidget(), + ), + ), + + // Scan Result Display + ScanResultDisplay( + barcode: scannerState.currentBarcode, + onTap: scannerState.currentBarcode != null + ? () => _navigateToDetail(context, scannerState.currentBarcode!) + : null, + ), + + // Divider + const Divider(height: 1), + + // History Section (Bottom Half) + Expanded( + flex: 1, + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // History Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Scan History', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + if (scannerState.history.isNotEmpty) + Text( + '${scannerState.history.length} items', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + const SizedBox(height: 12), + + // History List + Expanded( + child: _buildHistorySection(context, ref, scannerState), + ), + ], + ), + ), + ), + ], + ), + ); + } + + /// Build history section based on current state + Widget _buildHistorySection( + BuildContext context, + WidgetRef ref, + ScannerState scannerState, + ) { + if (scannerState.isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (scannerState.error != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 64, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(height: 16), + Text( + 'Error loading history', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + scannerState.error!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + ref.read(scannerProvider.notifier).refreshHistory(); + }, + child: const Text('Retry'), + ), + ], + ), + ); + } + + if (scannerState.history.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.qr_code_scanner, + size: 64, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text( + 'No scans yet', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Text( + 'Start scanning barcodes to see your history here', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + return ScanHistoryList( + history: scannerState.history, + onItemTap: (scanItem) => _navigateToDetail(context, scanItem.barcode), + ); + } + + /// Navigate to detail page with barcode + void _navigateToDetail(BuildContext context, String barcode) { + context.push('/detail/$barcode'); + } +} \ No newline at end of file diff --git a/lib/features/scanner/presentation/providers/dependency_injection.dart b/lib/features/scanner/presentation/providers/dependency_injection.dart new file mode 100644 index 0000000..229031b --- /dev/null +++ b/lib/features/scanner/presentation/providers/dependency_injection.dart @@ -0,0 +1,127 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hive_ce/hive.dart'; + +import '../../../../core/network/api_client.dart'; +import '../../data/datasources/scanner_local_datasource.dart'; +import '../../data/datasources/scanner_remote_datasource.dart'; +import '../../data/models/scan_item.dart'; +import '../../data/repositories/scanner_repository_impl.dart'; +import '../../domain/repositories/scanner_repository.dart'; +import '../../domain/usecases/get_scan_history_usecase.dart'; +import '../../domain/usecases/save_scan_usecase.dart'; + +/// Network layer providers +final dioProvider = Provider((ref) { + final dio = Dio(); + dio.options.baseUrl = 'https://api.example.com'; // Replace with actual API URL + dio.options.connectTimeout = const Duration(seconds: 30); + dio.options.receiveTimeout = const Duration(seconds: 30); + dio.options.headers['Content-Type'] = 'application/json'; + dio.options.headers['Accept'] = 'application/json'; + + // Add interceptors for logging, authentication, etc. + dio.interceptors.add( + LogInterceptor( + requestBody: true, + responseBody: true, + logPrint: (obj) { + // Log to console in debug mode using debugPrint + // This will only log in debug mode + }, + ), + ); + + return dio; +}); + +final apiClientProvider = Provider((ref) { + return ApiClient(); +}); + +/// Local storage providers +final hiveBoxProvider = Provider>((ref) { + return Hive.box('scans'); +}); + +/// Settings box provider +final settingsBoxProvider = Provider((ref) { + return Hive.box('settings'); +}); + +/// Data source providers +final scannerRemoteDataSourceProvider = Provider((ref) { + return ScannerRemoteDataSourceImpl(apiClient: ref.watch(apiClientProvider)); +}); + +final scannerLocalDataSourceProvider = Provider((ref) { + return ScannerLocalDataSourceImpl(); +}); + +/// Repository providers +final scannerRepositoryProvider = Provider((ref) { + return ScannerRepositoryImpl( + remoteDataSource: ref.watch(scannerRemoteDataSourceProvider), + localDataSource: ref.watch(scannerLocalDataSourceProvider), + ); +}); + +/// Use case providers +final saveScanUseCaseProvider = Provider((ref) { + return SaveScanUseCase(ref.watch(scannerRepositoryProvider)); +}); + +final getScanHistoryUseCaseProvider = Provider((ref) { + return GetScanHistoryUseCase(ref.watch(scannerRepositoryProvider)); +}); + +/// Additional utility providers +final currentTimestampProvider = Provider((ref) { + return DateTime.now(); +}); + +/// Provider for checking network connectivity +final networkStatusProvider = Provider((ref) { + // This would typically use connectivity_plus package + // For now, returning true as a placeholder + return true; +}); + +/// Provider for app configuration +final appConfigProvider = Provider>((ref) { + return { + 'apiBaseUrl': 'https://api.example.com', + 'apiTimeout': 30000, + 'maxHistoryItems': 100, + 'enableLogging': !const bool.fromEnvironment('dart.vm.product'), + }; +}); + +/// Provider for error handling configuration +final errorHandlingConfigProvider = Provider>((ref) { + return { + 'networkError': 'Network connection failed. Please check your internet connection.', + 'serverError': 'Server error occurred. Please try again later.', + 'validationError': 'Please check your input and try again.', + 'unknownError': 'An unexpected error occurred. Please try again.', + }; +}); + +/// Provider for checking if required dependencies are initialized +final dependenciesInitializedProvider = Provider((ref) { + try { + // Check if all critical dependencies are available + ref.read(scannerRepositoryProvider); + ref.read(saveScanUseCaseProvider); + ref.read(getScanHistoryUseCaseProvider); + return true; + } catch (e) { + return false; + } +}); + +/// Helper provider for getting localized error messages +final errorMessageProvider = Provider.family((ref, errorKey) { + final config = ref.watch(errorHandlingConfigProvider); + return config[errorKey] ?? config['unknownError']!; +}); \ No newline at end of file diff --git a/lib/features/scanner/presentation/providers/form_provider.dart b/lib/features/scanner/presentation/providers/form_provider.dart new file mode 100644 index 0000000..5d11ad3 --- /dev/null +++ b/lib/features/scanner/presentation/providers/form_provider.dart @@ -0,0 +1,253 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../data/models/scan_item.dart'; +import '../../domain/usecases/save_scan_usecase.dart'; +import 'dependency_injection.dart'; +import 'scanner_provider.dart'; + +/// State for the form functionality +class FormDetailState { + final String barcode; + final String field1; + final String field2; + final String field3; + final String field4; + final bool isLoading; + final bool isSaveSuccess; + final String? error; + + const FormDetailState({ + required this.barcode, + this.field1 = '', + this.field2 = '', + this.field3 = '', + this.field4 = '', + this.isLoading = false, + this.isSaveSuccess = false, + this.error, + }); + + FormDetailState copyWith({ + String? barcode, + String? field1, + String? field2, + String? field3, + String? field4, + bool? isLoading, + bool? isSaveSuccess, + String? error, + }) { + return FormDetailState( + barcode: barcode ?? this.barcode, + field1: field1 ?? this.field1, + field2: field2 ?? this.field2, + field3: field3 ?? this.field3, + field4: field4 ?? this.field4, + isLoading: isLoading ?? this.isLoading, + isSaveSuccess: isSaveSuccess ?? this.isSaveSuccess, + error: error, + ); + } + + /// Check if all required fields are filled + bool get isValid { + return barcode.trim().isNotEmpty && + field1.trim().isNotEmpty && + field2.trim().isNotEmpty && + field3.trim().isNotEmpty && + field4.trim().isNotEmpty; + } + + /// Get validation error messages + List get validationErrors { + final errors = []; + + if (barcode.trim().isEmpty) { + errors.add('Barcode is required'); + } + if (field1.trim().isEmpty) { + errors.add('Field 1 is required'); + } + if (field2.trim().isEmpty) { + errors.add('Field 2 is required'); + } + if (field3.trim().isEmpty) { + errors.add('Field 3 is required'); + } + if (field4.trim().isEmpty) { + errors.add('Field 4 is required'); + } + + return errors; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FormDetailState && + runtimeType == other.runtimeType && + barcode == other.barcode && + field1 == other.field1 && + field2 == other.field2 && + field3 == other.field3 && + field4 == other.field4 && + isLoading == other.isLoading && + isSaveSuccess == other.isSaveSuccess && + error == other.error; + + @override + int get hashCode => + barcode.hashCode ^ + field1.hashCode ^ + field2.hashCode ^ + field3.hashCode ^ + field4.hashCode ^ + isLoading.hashCode ^ + isSaveSuccess.hashCode ^ + error.hashCode; +} + +/// Form state notifier +class FormNotifier extends StateNotifier { + final SaveScanUseCase _saveScanUseCase; + final Ref _ref; + + FormNotifier( + this._saveScanUseCase, + this._ref, + String barcode, + ) : super(FormDetailState(barcode: barcode)); + + /// Update field 1 + void updateField1(String value) { + state = state.copyWith(field1: value, error: null); + } + + /// Update field 2 + void updateField2(String value) { + state = state.copyWith(field2: value, error: null); + } + + /// Update field 3 + void updateField3(String value) { + state = state.copyWith(field3: value, error: null); + } + + /// Update field 4 + void updateField4(String value) { + state = state.copyWith(field4: value, error: null); + } + + /// Update barcode + void updateBarcode(String value) { + state = state.copyWith(barcode: value, error: null); + } + + /// Clear all fields + void clearFields() { + state = FormDetailState(barcode: state.barcode); + } + + /// Populate form with existing scan data + void populateWithScanItem(ScanItem scanItem) { + state = state.copyWith( + barcode: scanItem.barcode, + field1: scanItem.field1, + field2: scanItem.field2, + field3: scanItem.field3, + field4: scanItem.field4, + error: null, + ); + } + + /// Save form data to server and local storage + Future saveData() async { + if (!state.isValid) { + final errors = state.validationErrors; + state = state.copyWith(error: errors.join(', ')); + return; + } + + state = state.copyWith(isLoading: true, error: null, isSaveSuccess: false); + + final params = SaveScanParams( + barcode: state.barcode, + field1: state.field1, + field2: state.field2, + field3: state.field3, + field4: state.field4, + ); + + final result = await _saveScanUseCase.call(params); + + result.fold( + (failure) => state = state.copyWith( + isLoading: false, + error: failure.message, + isSaveSuccess: false, + ), + (_) { + state = state.copyWith( + isLoading: false, + isSaveSuccess: true, + error: null, + ); + + // Update the scanner history with saved data + final savedScanItem = ScanItem( + barcode: state.barcode, + timestamp: DateTime.now(), + field1: state.field1, + field2: state.field2, + field3: state.field3, + field4: state.field4, + ); + + _ref.read(scannerProvider.notifier).updateScanItem(savedScanItem); + }, + ); + } + + /// Print form data + Future printData() async { + try { + + } catch (e) { + state = state.copyWith(error: 'Failed to print: ${e.toString()}'); + } + } + + + /// Clear error message + void clearError() { + state = state.copyWith(error: null); + } + + /// Reset save success state + void resetSaveSuccess() { + state = state.copyWith(isSaveSuccess: false); + } +} + +/// Provider factory for form state (requires barcode parameter) +final formProviderFamily = StateNotifierProvider.family( + (ref, barcode) => FormNotifier( + ref.watch(saveScanUseCaseProvider), + ref, + barcode, + ), +); + +/// Convenience provider for accessing form state with a specific barcode +/// This should be used with Provider.of or ref.watch(formProvider(barcode)) +Provider formProvider(String barcode) { + return Provider((ref) { + return ref.watch(formProviderFamily(barcode).notifier); + }); +} + +/// Convenience provider for accessing form state +Provider formStateProvider(String barcode) { + return Provider((ref) { + return ref.watch(formProviderFamily(barcode)); + }); +} \ No newline at end of file diff --git a/lib/features/scanner/presentation/providers/scanner_provider.dart b/lib/features/scanner/presentation/providers/scanner_provider.dart new file mode 100644 index 0000000..25956b2 --- /dev/null +++ b/lib/features/scanner/presentation/providers/scanner_provider.dart @@ -0,0 +1,163 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../data/models/scan_item.dart'; +import '../../domain/usecases/get_scan_history_usecase.dart'; +import 'dependency_injection.dart'; + +/// State for the scanner functionality +class ScannerState { + final String? currentBarcode; + final List history; + final bool isLoading; + final String? error; + + const ScannerState({ + this.currentBarcode, + this.history = const [], + this.isLoading = false, + this.error, + }); + + ScannerState copyWith({ + String? currentBarcode, + List? history, + bool? isLoading, + String? error, + }) { + return ScannerState( + currentBarcode: currentBarcode ?? this.currentBarcode, + history: history ?? this.history, + isLoading: isLoading ?? this.isLoading, + error: error ?? this.error, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ScannerState && + runtimeType == other.runtimeType && + currentBarcode == other.currentBarcode && + history == other.history && + isLoading == other.isLoading && + error == other.error; + + @override + int get hashCode => + currentBarcode.hashCode ^ + history.hashCode ^ + isLoading.hashCode ^ + error.hashCode; +} + +/// Scanner state notifier +class ScannerNotifier extends StateNotifier { + final GetScanHistoryUseCase _getScanHistoryUseCase; + + ScannerNotifier(this._getScanHistoryUseCase) : super(const ScannerState()) { + _loadHistory(); + } + + /// Load scan history from local storage + Future _loadHistory() async { + state = state.copyWith(isLoading: true, error: null); + + final result = await _getScanHistoryUseCase(); + result.fold( + (failure) => state = state.copyWith( + isLoading: false, + error: failure.message, + ), + (history) => state = state.copyWith( + isLoading: false, + history: history.map((entity) => ScanItem.fromEntity(entity)).toList(), + ), + ); + } + + /// Update current scanned barcode + void updateBarcode(String barcode) { + if (barcode.trim().isEmpty) return; + + state = state.copyWith(currentBarcode: barcode); + + // Add to history if not already present + final existingIndex = state.history.indexWhere((item) => item.barcode == barcode); + if (existingIndex == -1) { + final newScanItem = ScanItem( + barcode: barcode, + timestamp: DateTime.now(), + ); + + final updatedHistory = [newScanItem, ...state.history]; + state = state.copyWith(history: updatedHistory); + } else { + // Move existing item to top + final existingItem = state.history[existingIndex]; + final updatedHistory = List.from(state.history); + updatedHistory.removeAt(existingIndex); + updatedHistory.insert(0, existingItem.copyWith(timestamp: DateTime.now())); + state = state.copyWith(history: updatedHistory); + } + } + + /// Clear current barcode + void clearBarcode() { + state = state.copyWith(currentBarcode: null); + } + + /// Refresh history from storage + Future refreshHistory() async { + await _loadHistory(); + } + + /// Add or update scan item in history + void updateScanItem(ScanItem scanItem) { + final existingIndex = state.history.indexWhere( + (item) => item.barcode == scanItem.barcode, + ); + + List updatedHistory; + if (existingIndex != -1) { + // Update existing item + updatedHistory = List.from(state.history); + updatedHistory[existingIndex] = scanItem; + } else { + // Add new item at the beginning + updatedHistory = [scanItem, ...state.history]; + } + + state = state.copyWith(history: updatedHistory); + } + + /// Clear error message + void clearError() { + state = state.copyWith(error: null); + } +} + +/// Provider for scanner state +final scannerProvider = StateNotifierProvider( + (ref) => ScannerNotifier( + ref.watch(getScanHistoryUseCaseProvider), + ), +); + +/// Provider for current barcode (for easy access) +final currentBarcodeProvider = Provider((ref) { + return ref.watch(scannerProvider).currentBarcode; +}); + +/// Provider for scan history (for easy access) +final scanHistoryProvider = Provider>((ref) { + return ref.watch(scannerProvider).history; +}); + +/// Provider for scanner loading state +final scannerLoadingProvider = Provider((ref) { + return ref.watch(scannerProvider).isLoading; +}); + +/// Provider for scanner error state +final scannerErrorProvider = Provider((ref) { + return ref.watch(scannerProvider).error; +}); \ No newline at end of file diff --git a/lib/features/scanner/presentation/widgets/barcode_scanner_widget.dart b/lib/features/scanner/presentation/widgets/barcode_scanner_widget.dart new file mode 100644 index 0000000..cbf1c73 --- /dev/null +++ b/lib/features/scanner/presentation/widgets/barcode_scanner_widget.dart @@ -0,0 +1,344 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +import '../providers/scanner_provider.dart'; + +/// Widget that provides barcode scanning functionality using device camera +class BarcodeScannerWidget extends ConsumerStatefulWidget { + const BarcodeScannerWidget({super.key}); + + @override + ConsumerState createState() => _BarcodeScannerWidgetState(); +} + +class _BarcodeScannerWidgetState extends ConsumerState + with WidgetsBindingObserver { + late MobileScannerController _controller; + bool _isStarted = false; + String? _lastScannedCode; + DateTime? _lastScanTime; + bool _isTorchOn = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _controller = MobileScannerController( + formats: [ + BarcodeFormat.code128, + ], + facing: CameraFacing.back, + torchEnabled: false, + ); + _startScanner(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _controller.dispose(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + + switch (state) { + case AppLifecycleState.paused: + _stopScanner(); + break; + case AppLifecycleState.resumed: + _startScanner(); + break; + case AppLifecycleState.detached: + case AppLifecycleState.inactive: + case AppLifecycleState.hidden: + break; + } + } + + Future _startScanner() async { + if (!_isStarted && mounted) { + try { + await _controller.start(); + setState(() { + _isStarted = true; + }); + } catch (e) { + debugPrint('Failed to start scanner: $e'); + } + } + } + + Future _stopScanner() async { + if (_isStarted) { + try { + await _controller.stop(); + setState(() { + _isStarted = false; + }); + } catch (e) { + debugPrint('Failed to stop scanner: $e'); + } + } + } + + void _onBarcodeDetected(BarcodeCapture capture) { + final List barcodes = capture.barcodes; + + if (barcodes.isNotEmpty) { + final barcode = barcodes.first; + final code = barcode.rawValue; + + if (code != null && code.isNotEmpty) { + // Prevent duplicate scans within 2 seconds + final now = DateTime.now(); + if (_lastScannedCode == code && + _lastScanTime != null && + now.difference(_lastScanTime!).inSeconds < 2) { + return; + } + + _lastScannedCode = code; + _lastScanTime = now; + + // Update scanner provider with new barcode + ref.read(scannerProvider.notifier).updateBarcode(code); + + // Provide haptic feedback + _provideHapticFeedback(); + } + } + } + + void _provideHapticFeedback() { + // Haptic feedback is handled by the system + // You can add custom vibration here if needed + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(0), + ), + child: Stack( + children: [ + // Camera View + ClipRRect( + borderRadius: BorderRadius.circular(0), + child: MobileScanner( + controller: _controller, + onDetect: _onBarcodeDetected, + ), + ), + + // Overlay with scanner frame + _buildScannerOverlay(context), + + // Control buttons + _buildControlButtons(context), + ], + ), + ); + } + + /// Build scanner overlay with frame and guidance + Widget _buildScannerOverlay(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Colors.transparent, + ), + child: Stack( + children: [ + // Dark overlay with cutout + Container( + color: Colors.black.withOpacity(0.5), + child: Center( + child: Container( + width: 250, + height: 150, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: 2, + ), + borderRadius: BorderRadius.circular(12), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Container( + color: Colors.transparent, + ), + ), + ), + ), + ), + + // Instructions + Positioned( + bottom: 60, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + child: Text( + 'Position barcode within the frame', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ); + } + + /// Build control buttons (torch, camera switch) + Widget _buildControlButtons(BuildContext context) { + return Positioned( + top: 16, + right: 16, + child: Column( + children: [ + // Torch Toggle + Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + shape: BoxShape.circle, + ), + child: IconButton( + icon: Icon( + _isTorchOn ? Icons.flash_on : Icons.flash_off, + color: Colors.white, + ), + onPressed: _toggleTorch, + ), + ), + const SizedBox(height: 12), + + // Camera Switch + Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + shape: BoxShape.circle, + ), + child: IconButton( + icon: const Icon( + Icons.cameraswitch, + color: Colors.white, + ), + onPressed: _switchCamera, + ), + ), + ], + ), + ); + } + + /// Build error widget when camera fails + Widget _buildErrorWidget(MobileScannerException error) { + return Container( + color: Colors.black, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.camera_alt_outlined, + size: 64, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(height: 16), + Text( + 'Camera Error', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Colors.white, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + _getErrorMessage(error), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.white70, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _restartScanner, + child: const Text('Retry'), + ), + ], + ), + ), + ); + } + + /// Build placeholder while camera is loading + Widget _buildPlaceholderWidget() { + return Container( + color: Colors.black, + child: const Center( + child: CircularProgressIndicator( + color: Colors.white, + ), + ), + ); + } + + /// Get user-friendly error message + String _getErrorMessage(MobileScannerException error) { + switch (error.errorCode) { + case MobileScannerErrorCode.permissionDenied: + return 'Camera permission is required to scan barcodes. Please enable camera access in settings.'; + case MobileScannerErrorCode.unsupported: + return 'Your device does not support barcode scanning.'; + default: + return 'Unable to access camera. Please check your device settings and try again.'; + } + } + + /// Toggle torch/flashlight + void _toggleTorch() async { + try { + await _controller.toggleTorch(); + setState(() { + _isTorchOn = !_isTorchOn; + }); + } catch (e) { + debugPrint('Failed to toggle torch: $e'); + } + } + + /// Switch between front and back camera + void _switchCamera() async { + try { + await _controller.switchCamera(); + } catch (e) { + debugPrint('Failed to switch camera: $e'); + } + } + + /// Restart scanner after error + void _restartScanner() async { + try { + await _controller.stop(); + await _controller.start(); + setState(() { + _isStarted = true; + }); + } catch (e) { + debugPrint('Failed to restart scanner: $e'); + } + } +} \ No newline at end of file diff --git a/lib/features/scanner/presentation/widgets/scan_history_list.dart b/lib/features/scanner/presentation/widgets/scan_history_list.dart new file mode 100644 index 0000000..74a7755 --- /dev/null +++ b/lib/features/scanner/presentation/widgets/scan_history_list.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../data/models/scan_item.dart'; + +/// Widget to display a scrollable list of scan history items +class ScanHistoryList extends StatelessWidget { + final List history; + final Function(ScanItem)? onItemTap; + final Function(ScanItem)? onItemLongPress; + final bool showTimestamp; + + const ScanHistoryList({ + required this.history, + this.onItemTap, + this.onItemLongPress, + this.showTimestamp = true, + super.key, + }); + + @override + Widget build(BuildContext context) { + if (history.isEmpty) { + return _buildEmptyState(context); + } + + return ListView.builder( + itemCount: history.length, + padding: const EdgeInsets.only(top: 8), + itemBuilder: (context, index) { + final scanItem = history[index]; + return _buildHistoryItem(context, scanItem, index); + }, + ); + } + + /// Build individual history item + Widget _buildHistoryItem(BuildContext context, ScanItem scanItem, int index) { + final hasData = scanItem.field1.isNotEmpty || + scanItem.field2.isNotEmpty || + scanItem.field3.isNotEmpty || + scanItem.field4.isNotEmpty; + + return Container( + margin: const EdgeInsets.only(bottom: 8), + child: Material( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + elevation: 1, + child: InkWell( + onTap: onItemTap != null ? () => onItemTap!(scanItem) : null, + onLongPress: onItemLongPress != null ? () => onItemLongPress!(scanItem) : null, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Theme.of(context).colorScheme.outline.withOpacity(0.2), + width: 1, + ), + ), + child: Row( + children: [ + // Icon indicating scan status + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: hasData + ? Colors.green.withOpacity(0.1) + : Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + hasData ? Icons.check_circle : Icons.qr_code, + size: 20, + color: hasData + ? Colors.green + : Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(width: 12), + + // Barcode and details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Barcode + Text( + scanItem.barcode, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + + // Status and timestamp + Row( + children: [ + // Status indicator + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: hasData + ? Colors.green.withOpacity(0.2) + : Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + hasData ? 'Saved' : 'Scanned', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: hasData + ? Colors.green.shade700 + : Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + ), + + if (showTimestamp) ...[ + const SizedBox(width: 8), + Expanded( + child: Text( + _formatTimestamp(scanItem.timestamp), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ], + ), + + // Data preview (if available) + if (hasData) ...[ + const SizedBox(height: 4), + Text( + _buildDataPreview(scanItem), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + + // Chevron icon + if (onItemTap != null) + Icon( + Icons.chevron_right, + size: 20, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ], + ), + ), + ), + ), + ); + } + + /// Build empty state when no history is available + Widget _buildEmptyState(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.history, + size: 64, + color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.6), + ), + const SizedBox(height: 16), + Text( + 'No scan history', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Text( + 'Scanned barcodes will appear here', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + /// Format timestamp for display + String _formatTimestamp(DateTime timestamp) { + final now = DateTime.now(); + final difference = now.difference(timestamp); + + if (difference.inDays > 0) { + return DateFormat('MMM dd, yyyy').format(timestamp); + } else if (difference.inHours > 0) { + return '${difference.inHours}h ago'; + } else if (difference.inMinutes > 0) { + return '${difference.inMinutes}m ago'; + } else { + return 'Just now'; + } + } + + /// Build preview of saved data + String _buildDataPreview(ScanItem scanItem) { + final fields = [ + scanItem.field1, + scanItem.field2, + scanItem.field3, + scanItem.field4, + ].where((field) => field.isNotEmpty).toList(); + + if (fields.isEmpty) { + return 'No data saved'; + } + + return fields.join(' • '); + } +} \ No newline at end of file diff --git a/lib/features/scanner/presentation/widgets/scan_result_display.dart b/lib/features/scanner/presentation/widgets/scan_result_display.dart new file mode 100644 index 0000000..467908c --- /dev/null +++ b/lib/features/scanner/presentation/widgets/scan_result_display.dart @@ -0,0 +1,240 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Widget to display the most recent scan result with tap to edit functionality +class ScanResultDisplay extends StatelessWidget { + final String? barcode; + final VoidCallback? onTap; + final VoidCallback? onCopy; + + const ScanResultDisplay({ + required this.barcode, + this.onTap, + this.onCopy, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + border: Border( + top: BorderSide( + color: Theme.of(context).dividerColor, + width: 1, + ), + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 1, + ), + ), + ), + child: barcode != null ? _buildScannedResult(context) : _buildEmptyState(context), + ); + } + + /// Build widget when barcode is scanned + Widget _buildScannedResult(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Theme.of(context).colorScheme.primary.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + children: [ + // Barcode icon + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Icon( + Icons.qr_code, + size: 24, + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(width: 12), + + // Barcode text and label + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Last Scanned', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 2), + Text( + barcode!, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (onTap != null) ...[ + const SizedBox(height: 4), + Text( + 'Tap to edit', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500, + ), + ), + ], + ], + ), + ), + + // Action buttons + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Copy button + IconButton( + icon: Icon( + Icons.copy, + size: 20, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + onPressed: () => _copyToClipboard(context), + tooltip: 'Copy to clipboard', + visualDensity: VisualDensity.compact, + ), + + // Edit button (if tap is enabled) + if (onTap != null) + Icon( + Icons.arrow_forward_ios, + size: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ], + ), + ], + ), + ), + ), + ); + } + + /// Build empty state when no barcode is scanned + Widget _buildEmptyState(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // Placeholder icon + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Icon( + Icons.qr_code_scanner, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(width: 12), + + // Placeholder text + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'No barcode scanned', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 2), + Text( + 'Point camera at barcode to scan', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + + // Scan animation (optional visual feedback) + _buildScanAnimation(context), + ], + ), + ); + } + + /// Build scanning animation indicator + Widget _buildScanAnimation(BuildContext context) { + return TweenAnimationBuilder( + duration: const Duration(seconds: 2), + tween: Tween(begin: 0.0, end: 1.0), + builder: (context, value, child) { + return Opacity( + opacity: (1.0 - value).clamp(0.3, 1.0), + child: Container( + width: 4, + height: 24, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(2), + ), + ), + ); + }, + onEnd: () { + // Restart animation (this creates a continuous effect) + }, + ); + } + + /// Copy barcode to clipboard + void _copyToClipboard(BuildContext context) { + if (barcode != null) { + Clipboard.setData(ClipboardData(text: barcode!)); + + // Show feedback + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Copied "$barcode" to clipboard'), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + action: SnackBarAction( + label: 'OK', + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + ), + ), + ); + } + + // Call custom onCopy callback if provided + onCopy?.call(); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 854336d..958d3a9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,31 @@ import 'package:flutter/material.dart'; -import 'screens/home_screen.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hive_ce/hive.dart'; +import 'package:hive_ce_flutter/adapters.dart'; +import 'package:minhthu/core/constants/app_constants.dart'; +import 'package:minhthu/core/theme/app_theme.dart'; +import 'package:minhthu/features/scanner/data/models/scan_item.dart'; +import 'package:minhthu/features/scanner/presentation/pages/home_page.dart'; +import 'package:minhthu/features/scanner/presentation/pages/detail_page.dart'; -void main() { - runApp(const MyApp()); +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize Hive + await Hive.initFlutter(); + + // Register Hive adapters + Hive.registerAdapter(ScanItemAdapter()); + + // Open Hive boxes + await Hive.openBox(AppConstants.scanHistoryBox); + + runApp( + const ProviderScope( + child: MyApp(), + ), + ); } class MyApp extends StatelessWidget { @@ -10,14 +33,30 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Barcode Scanner App', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), - useMaterial3: true, - ), + final router = GoRouter( + initialLocation: '/', + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const HomePage(), + ), + GoRoute( + path: '/detail/:barcode', + builder: (context, state) { + final barcode = state.pathParameters['barcode'] ?? ''; + return DetailPage(barcode: barcode); + }, + ), + ], + ); + + return MaterialApp.router( + title: 'Barcode Scanner', + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: ThemeMode.system, + routerConfig: router, debugShowCheckedModeBanner: false, - home: const HomeScreen(), ); } -} +} \ No newline at end of file diff --git a/lib/screens/data_screen.dart b/lib/screens/data_screen.dart deleted file mode 100644 index a5204d2..0000000 --- a/lib/screens/data_screen.dart +++ /dev/null @@ -1,190 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class DataScreen extends StatelessWidget { - final List scannedHistory; - - const DataScreen({ - super.key, - required this.scannedHistory, - }); - - void _copyToClipboard(BuildContext context, String text) { - Clipboard.setData(ClipboardData(text: text)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Copied to clipboard'), - duration: Duration(seconds: 2), - ), - ); - } - - void _shareAllData(BuildContext context) { - final allData = scannedHistory.join('\n'); - Clipboard.setData(ClipboardData(text: allData)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('All data copied to clipboard'), - duration: Duration(seconds: 2), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Scanned Data'), - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - actions: [ - IconButton( - icon: const Icon(Icons.share), - onPressed: scannedHistory.isEmpty - ? null - : () => _shareAllData(context), - tooltip: 'Copy all data', - ), - ], - ), - body: scannedHistory.isEmpty - ? const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.inbox, - size: 80, - color: Colors.grey, - ), - SizedBox(height: 20), - Text( - 'No scanned data yet', - style: TextStyle( - fontSize: 18, - color: Colors.grey, - ), - ), - SizedBox(height: 10), - Text( - 'Start scanning barcodes from the home screen', - style: TextStyle( - fontSize: 14, - color: Colors.grey, - ), - ), - ], - ), - ) - : Column( - children: [ - Container( - padding: const EdgeInsets.all(16), - color: Colors.blue[50], - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Total Items: ${scannedHistory.length}', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ElevatedButton.icon( - onPressed: () => _shareAllData(context), - icon: const Icon(Icons.copy, size: 18), - label: const Text('Copy All'), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - ), - ), - ], - ), - ), - Expanded( - child: ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: scannedHistory.length, - itemBuilder: (context, index) { - final item = scannedHistory[index]; - return Card( - elevation: 2, - margin: const EdgeInsets.only(bottom: 12), - child: ListTile( - leading: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(20), - ), - child: Center( - child: Text( - '${index + 1}', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - title: Text( - item, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - subtitle: Text( - 'Scanned item #${index + 1}', - style: TextStyle( - color: Colors.grey[600], - ), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.copy, size: 20), - onPressed: () => _copyToClipboard(context, item), - tooltip: 'Copy', - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: Colors.green[100], - borderRadius: BorderRadius.circular(12), - ), - child: Text( - 'Active', - style: TextStyle( - color: Colors.green[800], - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - ); - }, - ), - ), - ], - ), - floatingActionButton: FloatingActionButton.extended( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.qr_code_scanner), - label: const Text('Scan More'), - backgroundColor: Theme.of(context).colorScheme.primary, - ), - ); - } -} \ No newline at end of file diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart deleted file mode 100644 index ffcc901..0000000 --- a/lib/screens/home_screen.dart +++ /dev/null @@ -1,304 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; -import 'data_screen.dart'; - -class HomeScreen extends StatefulWidget { - const HomeScreen({super.key}); - - @override - State createState() => _HomeScreenState(); -} - -class _HomeScreenState extends State { - String? scannedData; - List scannedHistory = []; - - void _navigateToDataScreen() { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => DataScreen(scannedHistory: scannedHistory), - ), - ); - } - - void _openScanner() { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ScannerScreen( - onScan: (String code) { - setState(() { - scannedData = code; - scannedHistory.add(code); - }); - }, - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Barcode Scanner'), - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - ), - body: Column( - children: [ - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(20), - child: ElevatedButton.icon( - onPressed: _openScanner, - icon: const Icon(Icons.qr_code_scanner, size: 32), - label: const Text( - 'Scan Barcode', - style: TextStyle(fontSize: 18), - ), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 40, - vertical: 15, - ), - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Colors.white, - ), - ), - ), - const SizedBox(height: 30), - if (scannedData != null) - Container( - margin: const EdgeInsets.symmetric(horizontal: 20), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.grey[300]!), - ), - child: Column( - children: [ - const Text( - 'Last Scanned:', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - Text( - scannedData!, - style: const TextStyle(fontSize: 18), - textAlign: TextAlign.center, - ), - ], - ), - ), - const SizedBox(height: 20), - if (scannedHistory.isNotEmpty) - Expanded( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Scan History (${scannedHistory.length} items)', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - Expanded( - child: ListView.builder( - itemCount: scannedHistory.length, - itemBuilder: (context, index) { - final reversedIndex = - scannedHistory.length - 1 - index; - return Card( - child: ListTile( - leading: CircleAvatar( - child: Text('${reversedIndex + 1}'), - ), - title: Text(scannedHistory[reversedIndex]), - subtitle: Text('Item ${reversedIndex + 1}'), - ), - ); - }, - ), - ), - ], - ), - ), - ), - ], - ), - ), - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.3), - spreadRadius: 1, - blurRadius: 5, - offset: const Offset(0, -3), - ), - ], - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton.icon( - onPressed: scannedHistory.isEmpty ? null : () { - setState(() { - scannedHistory.clear(); - scannedData = null; - }); - }, - icon: const Icon(Icons.clear_all), - label: const Text('Clear'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - ), - ElevatedButton.icon( - onPressed: scannedHistory.isEmpty ? null : _navigateToDataScreen, - icon: const Icon(Icons.list), - label: const Text('View All Data'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - ), - ), - ], - ), - ), - ], - ), - ); - } -} - -class ScannerScreen extends StatefulWidget { - final Function(String) onScan; - - const ScannerScreen({super.key, required this.onScan}); - - @override - State createState() => _ScannerScreenState(); -} - -class _ScannerScreenState extends State { - MobileScannerController cameraController = MobileScannerController( - formats: [BarcodeFormat.code128] - ); - bool hasScanned = false; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Scan Barcode'), - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - actions: [ - // IconButton( - // icon: ValueListenableBuilder( - // valueListenable: cameraController.torchStateNotifier, - // builder: (context, state, child) { - // switch (state) { - // case TorchState.off: - // return const Icon(Icons.flash_off, color: Colors.grey); - // case TorchState.on: - // return const Icon(Icons.flash_on, color: Colors.yellow); - // } - // }, - // ), - // onPressed: () => cameraController.toggleTorch(), - // ), - // IconButton( - // icon: ValueListenableBuilder( - // valueListenable: cameraController.cameraFacingState, - // builder: (context, state, child) { - // switch (state) { - // case CameraFacing.front: - // return const Icon(Icons.camera_front); - // case CameraFacing.back: - // return const Icon(Icons.camera_rear); - // } - // }, - // ), - // onPressed: () => cameraController.switchCamera(), - // ), - ], - ), - body: Stack( - children: [ - MobileScanner( - controller: cameraController, - onDetect: (capture) { - if (!hasScanned) { - final List barcodes = capture.barcodes; - for (final barcode in barcodes) { - if (barcode.rawValue != null) { - hasScanned = true; - widget.onScan(barcode.rawValue!); - Navigator.pop(context); - break; - } - } - } - }, - ), - Center( - child: Container( - width: 300, - height: 300, - decoration: BoxDecoration( - border: Border.all( - color: Colors.green, - width: 2, - ), - borderRadius: BorderRadius.circular(12), - ), - ), - ), - Positioned( - bottom: 100, - left: 0, - right: 0, - child: Center( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), - ), - child: const Text( - 'Align barcode within the frame', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), - ), - ), - ), - ), - ], - ), - ); - } - - @override - void dispose() { - cameraController.dispose(); - super.dispose(); - } -} \ No newline at end of file diff --git a/lib/simple_main.dart b/lib/simple_main.dart new file mode 100644 index 0000000..a71da4b --- /dev/null +++ b/lib/simple_main.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +void main() { + runApp( + const ProviderScope( + child: MyApp(), + ), + ); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Barcode Scanner', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, + ), + home: const HomePage(), + debugShowCheckedModeBanner: false, + ); + } +} + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Barcode Scanner'), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + ), + body: Column( + children: [ + // Scanner area placeholder + Expanded( + child: Container( + color: Colors.black, + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.qr_code_scanner, + size: 100, + color: Colors.white, + ), + SizedBox(height: 20), + Text( + 'Scanner will be here', + style: TextStyle(color: Colors.white, fontSize: 18), + ), + Text( + '(Camera permissions required)', + style: TextStyle(color: Colors.white70, fontSize: 14), + ), + ], + ), + ), + ), + ), + + // Scan result display + Container( + padding: const EdgeInsets.all(16), + color: Colors.grey[100], + child: Row( + children: [ + const Icon(Icons.qr_code, size: 40), + const SizedBox(width: 16), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Last Scanned:', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + Text( + 'No barcode scanned yet', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + // Navigate to detail page + }, + ), + ], + ), + ), + + // History list + Expanded( + child: Container( + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(16), + child: const Text( + 'Scan History', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + child: ListView.builder( + itemCount: 5, + itemBuilder: (context, index) { + return ListTile( + leading: const Icon(Icons.qr_code_2), + title: Text('Barcode ${1234567890 + index}'), + subtitle: Text('Scanned at ${DateTime.now().subtract(Duration(minutes: index * 5)).toString().substring(11, 16)}'), + trailing: const Icon(Icons.chevron_right), + onTap: () { + // Navigate to detail + }, + ); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 3a03047..a6a1fef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,22 +1,142 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + url: "https://pub.dev" + source: hosted + version: "85.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c + url: "https://pub.dev" + source: hosted + version: "7.6.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce + url: "https://pub.dev" + source: hosted + version: "0.13.4" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + url: "https://pub.dev" + source: hosted + version: "9.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d + url: "https://pub.dev" + source: hosted + version: "8.12.0" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -25,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" clock: dependency: transitive description: @@ -33,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + url: "https://pub.dev" + source: hosted + version: "4.11.0" collection: dependency: transitive description: @@ -41,6 +177,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -49,6 +201,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" + url: "https://pub.dev" + source: hosted + version: "0.7.5" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2" + url: "https://pub.dev" + source: hosted + version: "1.0.0+7.7.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -57,11 +265,43 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_lints: dependency: "direct dev" description: @@ -70,6 +310,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" flutter_test: dependency: "direct dev" description: flutter @@ -80,6 +328,158 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" + url: "https://pub.dev" + source: hosted + version: "2.5.8" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 + url: "https://pub.dev" + source: hosted + version: "7.7.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: b465e99ce64ba75e61c8c0ce3d87b66d8ac07f0b35d0a7e0263fcfc10f99e836 + url: "https://pub.dev" + source: hosted + version: "13.2.5" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive_ce: + dependency: "direct main" + description: + name: hive_ce + sha256: "74461ee89e97fe347dd894fd6ea1dbdc8c745ea6fe0d6e0b1746530abd02eb95" + url: "https://pub.dev" + source: hosted + version: "2.12.0" + hive_ce_flutter: + dependency: "direct main" + description: + name: hive_ce_flutter + sha256: f5bd57fda84402bca7557fedb8c629c96c8ea10fab4a542968d7b60864ca02cc + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive_ce_generator: + dependency: "direct dev" + description: + name: hive_ce_generator + sha256: "609678c10ebee7503505a0007050af40a0a4f498b1fb7def3220df341e573a89" + url: "https://pub.dev" + source: hosted + version: "1.9.2" + http: + dependency: transitive + description: + name: http + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + url: "https://pub.dev" + source: hosted + version: "1.5.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + isolate_channel: + dependency: transitive + description: + name: isolate_channel + sha256: f3d36f783b301e6b312c3450eeb2656b0e7d1db81331af2a151d9083a3f6b18d + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c + url: "https://pub.dev" + source: hosted + version: "6.9.5" leak_tracker: dependency: transitive description: @@ -112,6 +512,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -136,6 +544,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" mobile_scanner: dependency: "direct main" description: @@ -144,6 +560,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -152,6 +584,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" + url: "https://pub.dev" + source: hosted + version: "2.2.18" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -160,19 +648,171 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: "837a6dc33f490706c7f4632c516bcd10804ee4d9ccc8046124ca56388715fdf3" + url: "https://pub.dev" + source: hosted + version: "0.5.9" + riverpod_annotation: + dependency: transitive + description: + name: riverpod_annotation + sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: "120d3310f687f43e7011bb213b90a436f1bbc300f0e4b251a72c39bccb017a4f" + url: "https://pub.dev" + source: hosted + version: "2.6.4" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca + url: "https://pub.dev" + source: hosted + version: "1.3.7" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -181,6 +821,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: @@ -189,22 +837,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: @@ -213,6 +877,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.6" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_math: dependency: transitive description: @@ -225,18 +913,66 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" + url: "https://pub.dev" + source: hosted + version: "1.1.3" web: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yaml_writer: + dependency: transitive + description: + name: yaml_writer + sha256: "69651cd7238411179ac32079937d4aa9a2970150d6b2ae2c6fe6de09402a5dc5" + url: "https://pub.dev" + source: hosted + version: "2.1.0" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.9.0 <4.0.0" flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index f9dbeac..f484e30 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,90 +1,50 @@ name: minhthu description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: 'none' -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: '>=3.0.0 <4.0.0' -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 + # Core dependencies + flutter_riverpod: ^2.4.9 mobile_scanner: ^7.0.1 + hive_ce: ^2.12.0 + hive_ce_flutter: ^2.3.2 + go_router: ^13.2.0 + dio: ^5.3.2 + dartz: ^0.10.1 + get_it: ^7.6.4 + + # Data Classes & Serialization + freezed_annotation: ^2.4.1 + json_annotation: ^4.8.1 + + # Utilities + intl: ^0.18.1 + equatable: ^2.0.5 + + # UI Components + shimmer: ^3.0.0 + cached_network_image: ^3.3.1 + cupertino_icons: ^1.0.6 dev_dependencies: flutter_test: sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^3.0.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec + # Code Generation + build_runner: ^2.4.9 + freezed: ^2.5.2 + json_serializable: ^6.8.0 + hive_ce_generator: + riverpod_generator: ^2.4.0 -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package + uses-material-design: true \ No newline at end of file diff --git a/test/core_test.dart b/test/core_test.dart new file mode 100644 index 0000000..2e311f6 --- /dev/null +++ b/test/core_test.dart @@ -0,0 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:minhthu/core/core.dart'; + +void main() { +} \ No newline at end of file diff --git a/test/simple_test.dart b/test/simple_test.dart new file mode 100644 index 0000000..52f2259 --- /dev/null +++ b/test/simple_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:minhthu/features/scanner/data/models/scan_item.dart'; +import 'package:minhthu/features/scanner/data/models/save_request_model.dart'; +import 'package:minhthu/features/scanner/domain/entities/scan_entity.dart'; + +void main() { + group('Simple Tests', () { + test('ScanEntity should be created with valid data', () { + final entity = ScanEntity( + barcode: '123456789', + timestamp: DateTime.now(), + field1: 'Value1', + field2: 'Value2', + field3: 'Value3', + field4: 'Value4', + ); + + expect(entity.barcode, '123456789'); + expect(entity.field1, 'Value1'); + expect(entity.isFormComplete, true); + }); + + test('SaveRequestModel should serialize to JSON', () { + final request = SaveRequestModel( + barcode: '111222333', + field1: 'Data1', + field2: 'Data2', + field3: 'Data3', + field4: 'Data4', + ); + + final json = request.toJson(); + expect(json['barcode'], '111222333'); + expect(json['field1'], 'Data1'); + }); + }); +} \ No newline at end of file