This commit is contained in:
2025-09-16 23:14:35 +07:00
parent be2ad0a8fd
commit 9ebe7c2919
55 changed files with 5953 additions and 893 deletions

View File

@@ -1,2 +1,9 @@
# This is a generated file; do not edit or check into version control. # 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/ 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/

4
.gitignore vendored
View File

@@ -5,11 +5,9 @@
*.swp *.swp
.DS_Store .DS_Store
.atom/ .atom/
.build/
.buildlog/ .buildlog/
.history .history
.svn/ .svn/
.swiftpm/
migrate_working_dir/ migrate_working_dir/
# IntelliJ related # IntelliJ related
@@ -27,11 +25,11 @@ migrate_working_dir/
**/doc/api/ **/doc/api/
**/ios/Flutter/.last_build_id **/ios/Flutter/.last_build_id
.dart_tool/ .dart_tool/
.flutter-plugins
.flutter-plugins-dependencies .flutter-plugins-dependencies
.pub-cache/ .pub-cache/
.pub/ .pub/
/build/ /build/
/coverage/
# Symbolication related # Symbolication related
app.*.symbols app.*.symbols

View File

@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited. # This file should be version controlled and should not be manually edited.
version: version:
revision: "a402d9a4376add5bc2d6b1e33e53edaae58c07f8" revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d"
channel: "stable" channel: "stable"
project_type: app project_type: app
@@ -13,14 +13,14 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: android - platform: android
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
- platform: ios - platform: ios
create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
# User provided section # User provided section

630
CLAUDE.md
View File

@@ -1,279 +1,481 @@
# Flutter Barcode Scanner App Expert Guidelines # Flutter Barcode Scanner App Guidelines
## Flexibility Notice ## App Overview
**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. Simple barcode scanner app with two screens:
1. **Home Screen**: Barcode scanner + scan result display + history list
## Flutter Best Practices 2. **Detail Screen**: 4 text fields + Save (API call) & Print buttons
- 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.
## Project Structure
``` ```
lib/ lib/
core/ core/
constants/ constants/
theme/ theme/
utils/
widgets/ widgets/
network/ network/
api_client.dart api_client.dart
api_endpoints.dart
network_service.dart
features/ features/
scanner/ scanner/
data/ data/
datasources/ datasources/
scanner_remote_datasource.dart scanner_remote_datasource.dart
scanner_local_datasource.dart
models/ models/
scan_response_model.dart scan_item.dart
barcode_data_model.dart save_request_model.dart
repositories/ repositories/
scanner_repository_impl.dart scanner_repository.dart
domain/ domain/
entities/ entities/
scan_entity.dart scan_entity.dart
barcode_entity.dart
repositories/ repositories/
scanner_repository.dart scanner_repository.dart
usecases/ usecases/
get_barcode_data_usecase.dart
save_scan_usecase.dart save_scan_usecase.dart
presentation/ presentation/
providers/ providers/
scanner_provider.dart scanner_provider.dart
scan_detail_provider.dart
pages/ pages/
home_page.dart home_page.dart
scan_detail_page.dart detail_page.dart
widgets/ widgets/
barcode_scanner_widget.dart barcode_scanner_widget.dart
scan_result_display.dart
scan_history_list.dart scan_history_list.dart
scan_form_widget.dart
loading_widget.dart
history/
data/
domain/
presentation/
l10n/
main.dart main.dart
test/
unit/
widget/
integration/
``` ```
## App Architecture Overview ## App Flow
This barcode scanner app follows a two-screen flow with API integration: 1. **Scan Barcode**: Camera scans → Show result below scanner
1. **Home Screen**: Barcode scanner + scan history list 2. **Tap Result**: Navigate to detail screen
2. **Detail Screen**: API call → Loading → Form with 4 text fields + save/print buttons 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 ## Data Models
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
```dart ```dart
// Scanner state class ScanItem {
final scannerStateProvider = StateNotifierProvider<ScannerNotifier, ScannerState> final String barcode;
final DateTime timestamp;
final String field1;
final String field2;
final String field3;
final String field4;
// API call state ScanItem({
final barcodeDataProvider = FutureProvider.family<BarcodeData, String>((ref, barcode) async { required this.barcode,
return ref.read(scannerRepositoryProvider).getBarcodeData(barcode); required this.timestamp,
}); this.field1 = '',
this.field2 = '',
this.field3 = '',
this.field4 = '',
});
}
// History state with cached API data class SaveRequest {
final historyProvider = StateNotifierProvider<HistoryNotifier, List<ScanEntity>> final String barcode;
final String field1;
final String field2;
final String field3;
final String field4;
// Form state with API pre-population SaveRequest({
final formProvider = StateNotifierProvider<FormNotifier, FormState> required this.barcode,
required this.field1,
required this.field2,
required this.field3,
required this.field4,
});
// Network connectivity Map<String, dynamic> toJson() => {
final connectivityProvider = StreamProvider<ConnectivityResult> '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 ```dart
sealed class ApiResult<T> { class ScannerState {
const ApiResult(); final String? currentBarcode;
} final List<ScanItem> history;
class ApiSuccess<T> extends ApiResult<T> { ScannerState({
final T data; this.currentBarcode,
const ApiSuccess(this.data); this.history = const [],
} });
class ApiError<T> extends ApiResult<T> {
final String message;
final int? statusCode;
const ApiError(this.message, this.statusCode);
}
class ApiLoading<T> extends ApiResult<T> {
const ApiLoading();
} }
``` ```
### 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<Either<Failure, void>> call(SaveRequest request) async {
return await repository.saveScan(request);
}
}
```
## Repository Pattern
```dart ```dart
abstract class ScannerRepository { abstract class ScannerRepository {
Future<Either<Failure, BarcodeData>> getBarcodeData(String barcode); Future<Either<Failure, void>> saveScan(SaveRequest request);
Future<Either<Failure, void>> saveScanData(ScanEntity scan);
} }
class ScannerRepositoryImpl implements ScannerRepository { class ScannerRepositoryImpl implements ScannerRepository {
final ScannerRemoteDataSource remoteDataSource; final ScannerRemoteDataSource remoteDataSource;
final ScannerLocalDataSource localDataSource;
final NetworkInfo networkInfo;
// Implementation with cache-first or network-first strategies ScannerRepositoryImpl(this.remoteDataSource);
@override
Future<Either<Failure, void>> saveScan(SaveRequest request) async {
try {
await remoteDataSource.saveScan(request);
return const Right(null);
} catch (e) {
return Left(ServerFailure(e.toString()));
}
}
} }
``` ```
## Testing Guidelines ## Data Source
1. **API Tests**: Mock HTTP responses and error scenarios ```dart
2. **Repository Tests**: Test caching and offline behavior abstract class ScannerRemoteDataSource {
3. **Scanner Tests**: Mock barcode scanning scenarios Future<void> saveScan(SaveRequest request);
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
## Error Handling Scenarios class ScannerRemoteDataSourceImpl implements ScannerRemoteDataSource {
- **Network Errors**: No internet connection, timeout, server unavailable final ApiClient apiClient;
- **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
## Platform-Specific Considerations ScannerRemoteDataSourceImpl(this.apiClient);
### Android @override
- Network security configuration Future<void> saveScan(SaveRequest request) async {
- Background sync limitations final response = await apiClient.post(
- Proper hardware acceleration '/api/scans',
- Print service integration data: request.toJson(),
);
### iOS if (response.statusCode != 200) {
- App Transport Security (ATS) settings throw ServerException('Failed to save scan');
- Network permissions and privacy }
- Background app refresh policies }
- AirPrint integration }
```
## Coding Guidelines ## Widget Structure
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
## Refactoring Instructions ### Home Page
When refactoring code: ```dart
- Always maintain existing project structure patterns class HomePage extends ConsumerWidget {
- Prioritize consistency with current codebase Widget build(BuildContext context, WidgetRef ref) {
- Apply Flutter best practices without breaking existing architecture return Scaffold(
- Focus on incremental improvements body: Column(
- Ensure all changes maintain backward compatibility children: [
// Barcode Scanner (top half)
Expanded(
flex: 1,
child: BarcodeScannerWidget(),
),
This architecture ensures a robust, maintainable barcode scanning application with reliable API integration and offline capabilities. // Scan Result Display
ScanResultDisplay(),
// History List (bottom half)
Expanded(
flex: 1,
child: ScanHistoryList(),
),
],
),
);
}
}
```
### 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);
}
}
```
## Core Functions
### Save Data with API Call
```dart
class FormNotifier extends StateNotifier<FormState> {
final SaveScanUseCase saveScanUseCase;
FormNotifier(this.saveScanUseCase, String barcode)
: super(FormState(barcode: barcode));
Future<void> 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
}
}
```
### 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

3
android/.gitignore vendored
View File

@@ -5,10 +5,9 @@ gradle-wrapper.jar
/gradlew.bat /gradlew.bat
/local.properties /local.properties
GeneratedPluginRegistrant.java GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore. # 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 key.properties
**/*.keystore **/*.keystore
**/*.jks **/*.jks

67
android/app/build.gradle Normal file
View File

@@ -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 {}

View File

@@ -24,7 +24,7 @@ android {
applicationId = "com.example.minhthu.minhthu" applicationId = "com.example.minhthu.minhthu"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = 21 // Required for mobile_scanner
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName

View File

@@ -1,4 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:label="minhthu" android:label="minhthu"
android:name="${applicationName}" android:name="${applicationName}"
@@ -7,7 +12,6 @@
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
@@ -32,7 +36,7 @@
android:value="2" /> android:value="2" />
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility?hl=en and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT. https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. --> In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->

View File

@@ -2,4 +2,4 @@ package com.example.minhthu.minhthu
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity() class MainActivity: FlutterActivity()

28
android/build.gradle Normal file
View File

@@ -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
}

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

26
android/settings.gradle Normal file
View File

@@ -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"

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>13.0</string> <string>12.0</string>
</dict> </dict>
</plist> </plist>

View File

@@ -1,22 +1,119 @@
PODS: PODS:
- Flutter (1.0.0) - 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 - Flutter
- FlutterMacOS - FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- Flutter (from `Flutter`) - 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: EXTERNAL SOURCES:
Flutter: Flutter:
:path: Flutter :path: Flutter
mobile_scanner: 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: SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 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 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e

View File

@@ -8,14 +8,14 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 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 */; }; 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 */; }; 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 */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -42,18 +42,19 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference 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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
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 = "<group>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
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 = "<group>"; }; 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 = "<group>"; };
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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; }; 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 = "<group>"; };
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 = "<group>"; }; 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 = "<group>"; };
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 = "<group>"; }; D4CE6380793FAE04B5A637F6 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
EA37885CE439B5803C1F30C9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
5D3CC73C5175E2ED6E885B80 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B3E8470F02DDB7ABAEA7A4B8 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = { 97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
34DCF5348B04B2452A11F770 /* Pods_Runner.framework in Frameworks */, 31B74C332E9BAB3C731709B5 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
EFC62B64C79AE505518CC935 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4235163B87D1278088D5778B /* Pods_RunnerTests.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -95,29 +95,6 @@
path = RunnerTests; path = RunnerTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
357B02463BE002334A48BD06 /* Frameworks */ = {
isa = PBXGroup;
children = (
EA37885CE439B5803C1F30C9 /* Pods_Runner.framework */,
8FB13559C5EACC38629CCB47 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
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 = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -136,8 +113,8 @@
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */, 331C8082294A63A400263BE5 /* RunnerTests */,
943C3FE7F88950483702F50C /* Pods */, E49D38F264550528BB47A154 /* Pods */,
357B02463BE002334A48BD06 /* Frameworks */, E4F4BEC4DA1C1555A71A55FA /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -165,6 +142,29 @@
path = Runner; path = Runner;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
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 = "<group>";
};
E4F4BEC4DA1C1555A71A55FA /* Frameworks */ = {
isa = PBXGroup;
children = (
157B759BA50822107BC84545 /* Pods_Runner.framework */,
D4CE6380793FAE04B5A637F6 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -172,10 +172,10 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = ( buildPhases = (
8D6B5637DC40D5DF52D2BA2C /* [CP] Check Pods Manifest.lock */, 8FF0EEBCA20B622CF2073D49 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */, 331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */, 331C807F294A63A400263BE5 /* Resources */,
EFC62B64C79AE505518CC935 /* Frameworks */, 5D3CC73C5175E2ED6E885B80 /* Frameworks */,
); );
buildRules = ( buildRules = (
); );
@@ -191,14 +191,15 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
C0DF03DDD7A89CF91AF2093E /* [CP] Check Pods Manifest.lock */, F0155373E2B22AF41384A47C /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
4DDAF44A6054BC7CAC7F4CB9 /* [CP] Embed Pods Frameworks */, 8379CD5D41C8BC844F4FF4C3 /* [CP] Embed Pods Frameworks */,
C5CE1C9F1E27F01F3BBAFF78 /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@@ -286,7 +287,7 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 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; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@@ -303,7 +304,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
8D6B5637DC40D5DF52D2BA2C /* [CP] Check Pods Manifest.lock */ = { 8FF0EEBCA20B622CF2073D49 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@@ -340,7 +341,24 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 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; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@@ -455,7 +473,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@@ -488,7 +506,7 @@
}; };
331C8088294A63A400263BE5 /* Debug */ = { 331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 53066FF35F0C14F228CC4AC1 /* Pods-RunnerTests.debug.xcconfig */; baseConfigurationReference = 056F865243892463F3F29ED9 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -506,7 +524,7 @@
}; };
331C8089294A63A400263BE5 /* Release */ = { 331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = C48CD622E061AB73819815F4 /* Pods-RunnerTests.release.xcconfig */; baseConfigurationReference = 072A244A3D8AA29AA9F65FCB /* Pods-RunnerTests.release.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -522,7 +540,7 @@
}; };
331C808A294A63A400263BE5 /* Profile */ = { 331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = A877660DB1749B9A7312FCA0 /* Pods-RunnerTests.profile.xcconfig */; baseConfigurationReference = B83CBDF5984EDBB9453FD4B3 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -585,7 +603,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -636,7 +654,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;

View File

@@ -26,7 +26,6 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
@@ -55,13 +54,11 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES"> allowLocationSimulation = "YES">
<BuildableProductRunnable <BuildableProductRunnable
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">

View File

@@ -1,7 +1,7 @@
import Flutter
import UIKit import UIKit
import Flutter
@main @UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,

View File

@@ -29,7 +29,7 @@
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>This app requires camera access to take photos.</string> <string>This app needs camera access to scan barcodes</string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>

2
lib/app_router.dart Normal file
View File

@@ -0,0 +1,2 @@
// Re-export the app router from the core routing module
export 'core/routing/app_router.dart';

View File

@@ -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<String> 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
}

7
lib/core/core.dart Normal file
View File

@@ -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';

View File

@@ -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<String, String>? 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)' : ''}';
}

View File

@@ -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<Object> 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';
}

View File

@@ -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<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.get<T>(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
/// Make a POST request
Future<Response<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.post<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
/// Make a PUT request
Future<Response<T>> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.put<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
/// Make a DELETE request
Future<Response<T>> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.delete<T>(
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;
}
}

View File

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

View File

@@ -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,
),
);
}
}

View File

@@ -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';

View File

@@ -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<void> saveScan(ScanItem scan);
/// Get all scans from local storage
Future<List<ScanItem>> getAllScans();
/// Get scan by barcode from local storage
Future<ScanItem?> getScanByBarcode(String barcode);
/// Update scan in local storage
Future<void> updateScan(ScanItem scan);
/// Delete scan from local storage
Future<void> deleteScan(String barcode);
/// Clear all scans from local storage
Future<void> clearAllScans();
}
/// Implementation of ScannerLocalDataSource using Hive
class ScannerLocalDataSourceImpl implements ScannerLocalDataSource {
static const String _boxName = 'scans';
Box<ScanItem>? _box;
/// Initialize Hive box
Future<Box<ScanItem>> _getBox() async {
if (_box == null || !_box!.isOpen) {
try {
_box = await Hive.openBox<ScanItem>(_boxName);
} catch (e) {
throw CacheException('Failed to open Hive box: ${e.toString()}');
}
}
return _box!;
}
@override
Future<void> 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<List<ScanItem>> 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<ScanItem?> 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<void> 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<void> 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<void> 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<int> 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<bool> 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<List<ScanItem>> 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<void> dispose() async {
if (_box != null && _box!.isOpen) {
await _box!.close();
_box = null;
}
}
}

View File

@@ -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<void> saveScan(SaveRequestModel request);
/// Get scan data from remote server (optional for future use)
Future<Map<String, dynamic>?> getScanData(String barcode);
}
/// Implementation of ScannerRemoteDataSource using HTTP API
class ScannerRemoteDataSourceImpl implements ScannerRemoteDataSource {
final ApiClient apiClient;
ScannerRemoteDataSourceImpl({required this.apiClient});
@override
Future<void> 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<Map<String, dynamic>?> 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<String, dynamic>?;
} 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<void> 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<void> 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()}');
}
}
}

View File

@@ -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<String, dynamic> json) =>
_$SaveRequestModelFromJson(json);
/// Convert to JSON for API requests
Map<String, dynamic> 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<String> get validationErrors {
final errors = <String>[];
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;
}

View File

@@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'save_request_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SaveRequestModel _$SaveRequestModelFromJson(Map<String, dynamic> 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<String, dynamic> _$SaveRequestModelToJson(SaveRequestModel instance) =>
<String, dynamic>{
'barcode': instance.barcode,
'field1': instance.field1,
'field2': instance.field2,
'field3': instance.field3,
'field4': instance.field4,
};

View File

@@ -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<String, dynamic> 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<String, dynamic> 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;
}

View File

@@ -0,0 +1,56 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'scan_item.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class ScanItemAdapter extends TypeAdapter<ScanItem> {
@override
final int typeId = 0;
@override
ScanItem read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
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;
}

View File

@@ -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<Either<Failure, void>> 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<Either<Failure, List<ScanEntity>>> 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<Either<Failure, void>> 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<Either<Failure, void>> 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<Either<Failure, void>> 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<Either<Failure, ScanEntity?>> 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<Either<Failure, void>> 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<Either<Failure, int>> 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<Either<Failure, bool>> 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<Either<Failure, List<ScanEntity>>> 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()}'));
}
}
}

View File

@@ -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';

View File

@@ -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<Object> 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}';
}
}

View File

@@ -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<Either<Failure, void>> saveScan({
required String barcode,
required String field1,
required String field2,
required String field3,
required String field4,
});
/// Get scan history from local storage
Future<Either<Failure, List<ScanEntity>>> getScanHistory();
/// Save scan to local storage
Future<Either<Failure, void>> saveScanLocally(ScanEntity scan);
/// Delete a scan from local storage
Future<Either<Failure, void>> deleteScanLocally(String barcode);
/// Clear all scan history from local storage
Future<Either<Failure, void>> clearScanHistory();
/// Get a specific scan by barcode from local storage
Future<Either<Failure, ScanEntity?>> getScanByBarcode(String barcode);
/// Update a scan in local storage
Future<Either<Failure, void>> updateScanLocally(ScanEntity scan);
}

View File

@@ -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<Either<Failure, List<ScanEntity>>> 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<ScanEntity>.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<Either<Failure, List<ScanEntity>>> 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<Either<Failure, List<ScanEntity>>> 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<Either<Failure, List<ScanEntity>>> 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()}'));
}
}
}

View File

@@ -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<Either<Failure, void>> 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}';
}
}

View File

@@ -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<DetailPage> createState() => _DetailPageState();
}
class _DetailPageState extends ConsumerState<DetailPage> {
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<FormDetailState>(
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<Color>(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<void> _saveData(FormNotifier formNotifier) async {
// Clear any previous errors
formNotifier.clearError();
// Attempt to save
await formNotifier.saveData();
}
/// Print form data
Future<void> _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();
}
});
}
}

View File

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

View File

@@ -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<Dio>((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<ApiClient>((ref) {
return ApiClient();
});
/// Local storage providers
final hiveBoxProvider = Provider<Box<ScanItem>>((ref) {
return Hive.box<ScanItem>('scans');
});
/// Settings box provider
final settingsBoxProvider = Provider<Box>((ref) {
return Hive.box('settings');
});
/// Data source providers
final scannerRemoteDataSourceProvider = Provider<ScannerRemoteDataSource>((ref) {
return ScannerRemoteDataSourceImpl(apiClient: ref.watch(apiClientProvider));
});
final scannerLocalDataSourceProvider = Provider<ScannerLocalDataSource>((ref) {
return ScannerLocalDataSourceImpl();
});
/// Repository providers
final scannerRepositoryProvider = Provider<ScannerRepository>((ref) {
return ScannerRepositoryImpl(
remoteDataSource: ref.watch(scannerRemoteDataSourceProvider),
localDataSource: ref.watch(scannerLocalDataSourceProvider),
);
});
/// Use case providers
final saveScanUseCaseProvider = Provider<SaveScanUseCase>((ref) {
return SaveScanUseCase(ref.watch(scannerRepositoryProvider));
});
final getScanHistoryUseCaseProvider = Provider<GetScanHistoryUseCase>((ref) {
return GetScanHistoryUseCase(ref.watch(scannerRepositoryProvider));
});
/// Additional utility providers
final currentTimestampProvider = Provider<DateTime>((ref) {
return DateTime.now();
});
/// Provider for checking network connectivity
final networkStatusProvider = Provider<bool>((ref) {
// This would typically use connectivity_plus package
// For now, returning true as a placeholder
return true;
});
/// Provider for app configuration
final appConfigProvider = Provider<Map<String, dynamic>>((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<Map<String, String>>((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<bool>((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<String, String>((ref, errorKey) {
final config = ref.watch(errorHandlingConfigProvider);
return config[errorKey] ?? config['unknownError']!;
});

View File

@@ -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<String> get validationErrors {
final errors = <String>[];
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<FormDetailState> {
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<void> 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<void> 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<FormNotifier, FormDetailState, String>(
(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<FormNotifier> formProvider(String barcode) {
return Provider<FormNotifier>((ref) {
return ref.watch(formProviderFamily(barcode).notifier);
});
}
/// Convenience provider for accessing form state
Provider<FormDetailState> formStateProvider(String barcode) {
return Provider<FormDetailState>((ref) {
return ref.watch(formProviderFamily(barcode));
});
}

View File

@@ -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<ScanItem> history;
final bool isLoading;
final String? error;
const ScannerState({
this.currentBarcode,
this.history = const [],
this.isLoading = false,
this.error,
});
ScannerState copyWith({
String? currentBarcode,
List<ScanItem>? 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<ScannerState> {
final GetScanHistoryUseCase _getScanHistoryUseCase;
ScannerNotifier(this._getScanHistoryUseCase) : super(const ScannerState()) {
_loadHistory();
}
/// Load scan history from local storage
Future<void> _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<ScanItem>.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<void> 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<ScanItem> updatedHistory;
if (existingIndex != -1) {
// Update existing item
updatedHistory = List<ScanItem>.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<ScannerNotifier, ScannerState>(
(ref) => ScannerNotifier(
ref.watch(getScanHistoryUseCaseProvider),
),
);
/// Provider for current barcode (for easy access)
final currentBarcodeProvider = Provider<String?>((ref) {
return ref.watch(scannerProvider).currentBarcode;
});
/// Provider for scan history (for easy access)
final scanHistoryProvider = Provider<List<ScanItem>>((ref) {
return ref.watch(scannerProvider).history;
});
/// Provider for scanner loading state
final scannerLoadingProvider = Provider<bool>((ref) {
return ref.watch(scannerProvider).isLoading;
});
/// Provider for scanner error state
final scannerErrorProvider = Provider<String?>((ref) {
return ref.watch(scannerProvider).error;
});

View File

@@ -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<BarcodeScannerWidget> createState() => _BarcodeScannerWidgetState();
}
class _BarcodeScannerWidgetState extends ConsumerState<BarcodeScannerWidget>
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<void> _startScanner() async {
if (!_isStarted && mounted) {
try {
await _controller.start();
setState(() {
_isStarted = true;
});
} catch (e) {
debugPrint('Failed to start scanner: $e');
}
}
}
Future<void> _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<Barcode> 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');
}
}
}

View File

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

View File

@@ -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<double>(
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();
}
}

View File

@@ -1,8 +1,31 @@
import 'package:flutter/material.dart'; 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() { void main() async {
runApp(const MyApp()); WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive
await Hive.initFlutter();
// Register Hive adapters
Hive.registerAdapter(ScanItemAdapter());
// Open Hive boxes
await Hive.openBox<ScanItem>(AppConstants.scanHistoryBox);
runApp(
const ProviderScope(
child: MyApp(),
),
);
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@@ -10,14 +33,30 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( final router = GoRouter(
title: 'Barcode Scanner App', initialLocation: '/',
theme: ThemeData( routes: [
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), GoRoute(
useMaterial3: true, 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, debugShowCheckedModeBanner: false,
home: const HomeScreen(),
); );
} }
} }

View File

@@ -1,190 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class DataScreen extends StatelessWidget {
final List<String> 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,
),
);
}
}

View File

@@ -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<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
String? scannedData;
List<String> 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<ScannerScreen> createState() => _ScannerScreenState();
}
class _ScannerScreenState extends State<ScannerScreen> {
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<Barcode> 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();
}
}

143
lib/simple_main.dart Normal file
View File

@@ -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
},
);
},
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -1,22 +1,142 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: 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: async:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" version: "2.13.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
name: boolean_selector name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: characters:
dependency: transitive dependency: transitive
description: description:
@@ -25,6 +145,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" 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: clock:
dependency: transitive dependency: transitive
description: description:
@@ -33,6 +161,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" 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: collection:
dependency: transitive dependency: transitive
description: description:
@@ -41,6 +177,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" 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: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -49,6 +201,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" 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: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -57,11 +265,43 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.3" 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: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -70,6 +310,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" 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: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -80,6 +328,158 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -112,6 +512,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -136,6 +544,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.16.0" version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mobile_scanner: mobile_scanner:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -144,6 +560,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" 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: path:
dependency: transitive dependency: transitive
description: description:
@@ -152,6 +584,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" 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: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -160,19 +648,171 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: source_span:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -181,6 +821,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.12.1" 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: stream_channel:
dependency: transitive dependency: transitive
description: description:
@@ -189,22 +837,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" 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: string_scanner:
dependency: transitive dependency: transitive
description: description:
name: string_scanner name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: term_glyph:
dependency: transitive dependency: transitive
description: description:
name: term_glyph name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.2"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
@@ -213,6 +877,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.6" 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: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -225,18 +913,66 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: web:
dependency: transitive dependency: transitive
description: description:
name: web name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: sdks:
dart: ">=3.8.0-0 <4.0.0" dart: ">=3.9.0 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.29.0"

View File

@@ -1,90 +1,50 @@
name: minhthu name: minhthu
description: "A new Flutter project." description: "A new Flutter project."
# The following line prevents the package from being accidentally published to publish_to: 'none'
# 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
# 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 version: 1.0.0+1
environment: environment:
sdk: '>=3.0.0 <4.0.0' 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: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
# The following adds the Cupertino Icons font to your application. # Core dependencies
# Use with the CupertinoIcons class for iOS style icons. flutter_riverpod: ^2.4.9
cupertino_icons: ^1.0.8
mobile_scanner: ^7.0.1 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: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter 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 flutter_lints: ^3.0.0
# For information on the generic Dart part of this file, see the # Code Generation
# following page: https://dart.dev/tools/pub/pubspec 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: 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 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

5
test/core_test.dart Normal file
View File

@@ -0,0 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:minhthu/core/core.dart';
void main() {
}

37
test/simple_test.dart Normal file
View File

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