Приложение на флаторе, настройки
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
class AppConstants {
|
||||
AppConstants._();
|
||||
|
||||
// API
|
||||
static const String apiBaseUrl = 'https://api.messenger.app';
|
||||
static const int apiTimeout = 30;
|
||||
|
||||
// Storage
|
||||
static const String sharedPrefsName = 'messenger_prefs';
|
||||
static const String tokenKey = 'auth_token';
|
||||
static const String refreshTokenKey = 'refresh_token';
|
||||
static const String userIdKey = 'user_id';
|
||||
|
||||
// Pagination
|
||||
static const int pageSize = 20;
|
||||
static const int defaultPageSize = 50;
|
||||
|
||||
// Chat
|
||||
static const int maxMessageLength = 4096;
|
||||
static const int typingIndicatorTimeout = 3000;
|
||||
|
||||
// Media
|
||||
static const int maxImageSize = 10 * 1024 * 1024; // 10MB
|
||||
static const int maxVideoSize = 100 * 1024 * 1024; // 100MB
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
class StorageKeys {
|
||||
StorageKeys._();
|
||||
|
||||
static const String apiUrl = 'api_url';
|
||||
static const String languageCode = 'language_code';
|
||||
static const String serverConfig = 'server_config';
|
||||
static const String authToken = 'auth_token';
|
||||
static const String refreshToken = 'refresh_token';
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'errors.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class AppError with _$AppError {
|
||||
const factory AppError.unknown({String? message}) = UnknownError;
|
||||
const factory AppError.network({String? message}) = NetworkError;
|
||||
const factory AppError.unauthorized({String? message}) = UnauthorizedError;
|
||||
const factory AppError.forbidden({String? message}) = ForbiddenError;
|
||||
const factory AppError.notFound({String? message}) = NotFoundError;
|
||||
const factory AppError.server({int? statusCode, String? message}) = ServerError;
|
||||
const factory AppError.database({String? message}) = DatabaseError;
|
||||
const factory AppError.validation({Map<String, String>? fieldErrors}) = ValidationError;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
import 'errors.dart';
|
||||
|
||||
/// A generic class for handling success/failure results
|
||||
class Result<T> {
|
||||
final T? data;
|
||||
final AppError? error;
|
||||
|
||||
const Result.success(this.data) : error = null;
|
||||
const Result.failure(this.error) : data = null;
|
||||
|
||||
bool get isSuccess => error == null;
|
||||
bool get isFailure => error != null;
|
||||
|
||||
R when<R>({
|
||||
required R Function(T data) onSuccess,
|
||||
required R Function(AppError error) onFailure,
|
||||
}) {
|
||||
if (isSuccess) {
|
||||
return onSuccess(data as T);
|
||||
}
|
||||
return onFailure(error!);
|
||||
}
|
||||
|
||||
T getOrThrow() {
|
||||
if (isFailure) {
|
||||
throw error!;
|
||||
}
|
||||
return data as T;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../errors/errors.dart';
|
||||
import '../errors/result.dart';
|
||||
|
||||
class ApiClient {
|
||||
final Dio _dio;
|
||||
|
||||
ApiClient(this._dio);
|
||||
|
||||
Future<Result<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get<T>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
);
|
||||
return Result.success(response.data as T);
|
||||
} on DioException catch (e) {
|
||||
return Result.failure(_handleDioError(e));
|
||||
} catch (e) {
|
||||
return Result.failure(const AppError.unknown(message: 'Unknown error occurred'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<Result<T>> post<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
);
|
||||
return Result.success(response.data as T);
|
||||
} on DioException catch (e) {
|
||||
return Result.failure(_handleDioError(e));
|
||||
} catch (e) {
|
||||
return Result.failure(const AppError.unknown(message: 'Unknown error occurred'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<Result<T>> put<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.put<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
);
|
||||
return Result.success(response.data as T);
|
||||
} on DioException catch (e) {
|
||||
return Result.failure(_handleDioError(e));
|
||||
} catch (e) {
|
||||
return Result.failure(const AppError.unknown(message: 'Unknown error occurred'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<Result<T>> delete<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.delete<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
);
|
||||
return Result.success(response.data as T);
|
||||
} on DioException catch (e) {
|
||||
return Result.failure(_handleDioError(e));
|
||||
} catch (e) {
|
||||
return Result.failure(const AppError.unknown(message: 'Unknown error occurred'));
|
||||
}
|
||||
}
|
||||
|
||||
AppError _handleDioError(DioException error) {
|
||||
switch (error.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return const AppError.network(message: 'Connection timeout');
|
||||
case DioExceptionType.connectionError:
|
||||
return const AppError.network(message: 'No internet connection');
|
||||
case DioExceptionType.badResponse:
|
||||
final statusCode = error.response?.statusCode;
|
||||
switch (statusCode) {
|
||||
case 401:
|
||||
return const AppError.unauthorized();
|
||||
case 403:
|
||||
return const AppError.forbidden();
|
||||
case 404:
|
||||
return const AppError.notFound();
|
||||
case 422:
|
||||
return AppError.validation(fieldErrors: _extractValidationErrors(error.response?.data));
|
||||
case int s when s >= 500:
|
||||
return AppError.server(statusCode: statusCode, message: 'Server error');
|
||||
default:
|
||||
return AppError.server(statusCode: statusCode, message: 'Request failed');
|
||||
}
|
||||
case DioExceptionType.cancel:
|
||||
return const AppError.unknown(message: 'Request cancelled');
|
||||
default:
|
||||
return const AppError.unknown();
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String>? _extractValidationErrors(dynamic data) {
|
||||
if (data is Map<String, dynamic> && data.containsKey('errors')) {
|
||||
final errors = data['errors'] as Map<String, dynamic>;
|
||||
return errors.map((key, value) {
|
||||
if (value is List && value.isNotEmpty) {
|
||||
return MapEntry(key, value.first.toString());
|
||||
}
|
||||
return MapEntry(key, 'Invalid');
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class AppTheme {
|
||||
AppTheme._();
|
||||
|
||||
static const _primaryColor = Color(0xFF2481CC);
|
||||
static const _primaryContainerColor = Color(0xFFD3E4FD);
|
||||
static const _secondaryColor = Color(0xFF5F6AC4);
|
||||
static const _tertiaryColor = Color(0xFFFFD700);
|
||||
|
||||
static const _lightSurfaceColor = Color(0xFFFFFBFE);
|
||||
static const _darkSurfaceColor = Color(0xFF1C1B1F);
|
||||
|
||||
static ThemeData get lightTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: _primaryColor,
|
||||
primaryContainer: _primaryContainerColor,
|
||||
secondary: _secondaryColor,
|
||||
tertiary: _tertiaryColor,
|
||||
surface: _lightSurfaceColor,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.white,
|
||||
onSurface: Colors.black87,
|
||||
error: Color(0xFFBA1A1A),
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
backgroundColor: _primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
type: BottomNavigationBarType.fixed,
|
||||
backgroundColor: Colors.white,
|
||||
selectedItemColor: _primaryColor,
|
||||
unselectedItemColor: Colors.grey,
|
||||
elevation: 8,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100],
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static ThemeData get darkTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: _primaryColor,
|
||||
primaryContainer: _primaryContainerColor,
|
||||
secondary: _secondaryColor,
|
||||
tertiary: _tertiaryColor,
|
||||
surface: _darkSurfaceColor,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.white,
|
||||
onSurface: const Color(0xFFE6E1E5),
|
||||
error: Color(0xFFFFB4AB),
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
backgroundColor: Color(0xFF1F1F1F),
|
||||
foregroundColor: Colors.white,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
type: BottomNavigationBarType.fixed,
|
||||
backgroundColor: Color(0xFF1F1F1F),
|
||||
selectedItemColor: _primaryColor,
|
||||
unselectedItemColor: Colors.grey,
|
||||
elevation: 8,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF2D2D2D),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user