runable
This commit is contained in:
@@ -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
4
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
14
.metadata
14
.metadata
@@ -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
630
CLAUDE.md
@@ -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
3
android/.gitignore
vendored
@@ -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
67
android/app/build.gradle
Normal 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 {}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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. -->
|
||||||
|
|||||||
@@ -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
28
android/build.gradle
Normal 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
|
||||||
|
}
|
||||||
@@ -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
26
android/settings.gradle
Normal 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"
|
||||||
@@ -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>
|
||||||
|
|||||||
107
ios/Podfile.lock
107
ios/Podfile.lock
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
2
lib/app_router.dart
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Re-export the app router from the core routing module
|
||||||
|
export 'core/routing/app_router.dart';
|
||||||
80
lib/core/constants/app_constants.dart
Normal file
80
lib/core/constants/app_constants.dart
Normal 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
7
lib/core/core.dart
Normal 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';
|
||||||
123
lib/core/errors/exceptions.dart
Normal file
123
lib/core/errors/exceptions.dart
Normal 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)' : ''}';
|
||||||
|
}
|
||||||
79
lib/core/errors/failures.dart
Normal file
79
lib/core/errors/failures.dart
Normal 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';
|
||||||
|
}
|
||||||
175
lib/core/network/api_client.dart
Normal file
175
lib/core/network/api_client.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
211
lib/core/routing/app_router.dart
Normal file
211
lib/core/routing/app_router.dart
Normal 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');
|
||||||
|
}
|
||||||
298
lib/core/theme/app_theme.dart
Normal file
298
lib/core/theme/app_theme.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
lib/features/scanner/data/data.dart
Normal file
6
lib/features/scanner/data/data.dart
Normal 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';
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
lib/features/scanner/data/models/save_request_model.dart
Normal file
134
lib/features/scanner/data/models/save_request_model.dart
Normal 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;
|
||||||
|
}
|
||||||
25
lib/features/scanner/data/models/save_request_model.g.dart
Normal file
25
lib/features/scanner/data/models/save_request_model.g.dart
Normal 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,
|
||||||
|
};
|
||||||
131
lib/features/scanner/data/models/scan_item.dart
Normal file
131
lib/features/scanner/data/models/scan_item.dart
Normal 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;
|
||||||
|
}
|
||||||
56
lib/features/scanner/data/models/scan_item.g.dart
Normal file
56
lib/features/scanner/data/models/scan_item.g.dart
Normal 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;
|
||||||
|
}
|
||||||
@@ -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()}'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
lib/features/scanner/domain/domain.dart
Normal file
5
lib/features/scanner/domain/domain.dart
Normal 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';
|
||||||
71
lib/features/scanner/domain/entities/scan_entity.dart
Normal file
71
lib/features/scanner/domain/entities/scan_entity.dart
Normal 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}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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()}'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
lib/features/scanner/domain/usecases/save_scan_usecase.dart
Normal file
109
lib/features/scanner/domain/usecases/save_scan_usecase.dart
Normal 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}';
|
||||||
|
}
|
||||||
|
}
|
||||||
334
lib/features/scanner/presentation/pages/detail_page.dart
Normal file
334
lib/features/scanner/presentation/pages/detail_page.dart
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
193
lib/features/scanner/presentation/pages/home_page.dart
Normal file
193
lib/features/scanner/presentation/pages/home_page.dart
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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']!;
|
||||||
|
});
|
||||||
253
lib/features/scanner/presentation/providers/form_provider.dart
Normal file
253
lib/features/scanner/presentation/providers/form_provider.dart
Normal 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));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
236
lib/features/scanner/presentation/widgets/scan_history_list.dart
Normal file
236
lib/features/scanner/presentation/widgets/scan_history_list.dart
Normal 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(' • ');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
143
lib/simple_main.dart
Normal 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
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
766
pubspec.lock
766
pubspec.lock
@@ -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"
|
||||||
|
|||||||
96
pubspec.yaml
96
pubspec.yaml
@@ -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
5
test/core_test.dart
Normal 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
37
test/simple_test.dart
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user