runable
This commit is contained in:
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user