6 Commits

Author SHA1 Message Date
Artem Barkalov
9b5f80e7d9 feat(debug): Добавить экраны отладки для плагинов 2025-06-23 01:03:38 +03:00
PetrovY
fb92795b67 delete(app): Удалить интерфейс конфигурации приложения i_app_config.dart. 2025-06-20 16:54:36 +03:00
PetrovY
24bf652319 delete(tools): Добавить скрипт для переключения сервисов 2025-06-20 16:54:05 +03:00
Yuri Petrov
98630f744f Update README.md 2025-06-20 16:51:18 +03:00
Yuri Petrov
ba5fdba9be refactor(app): Обновить описание и структуру файлов конфигурации, улучшить документацию (#14)
Co-authored-by: PetrovY <y.petrov@friflex.com>
2025-06-20 16:50:48 +03:00
zl0y4951
427a821e5d feat(app): Реализовать тему через theme_tailor (#13)
* chore(.gitignore): добавил игнорирование кодогенерации
* chore(pubspec): добавил theme_tailor в зависимости
* feat(app): добавил theme_tailor кодогенерацию темы
* refactor(app, debug): заменил использование расширение контекста
* chore(.gitignore): убрал игнорирование кодогена
* feat(app): добавил сгенерированный файл
2025-06-19 12:51:57 +03:00
52 changed files with 1247 additions and 304 deletions

View File

@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
# 🚀 Friflex Starter - Корпоративный шаблон # 🚀 Friflex Flutter Starter - Корпоративный шаблон
</div> </div>
<div align="center"> <div align="center">

View File

@@ -2,3 +2,4 @@ library;
export 'src/app_path_provider.dart'; export 'src/app_path_provider.dart';
export 'src/app_secure_storage.dart'; export 'src/app_secure_storage.dart';
export 'src/app_url_launcher.dart';

View File

@@ -11,6 +11,8 @@ class AppPathProvider implements IPathProvider {
/// Наименование сервиса /// Наименование сервиса
static const name = 'AuroraAppPathProvider'; static const name = 'AuroraAppPathProvider';
String get nameImpl => AppPathProvider.name;
@override @override
Future<String> getAppDocumentsDirectoryPath() async { Future<String> getAppDocumentsDirectoryPath() async {
return (await getApplicationDocumentsDirectory()).path; return (await getApplicationDocumentsDirectory()).path;

View File

@@ -15,11 +15,15 @@ final class AppSecureStorage implements ISecureStorage {
FlutterSecureStorageAurora.setSecret(secretKey); FlutterSecureStorageAurora.setSecret(secretKey);
} }
/// Наименование сервиса
static const name = 'AuroraAppSecureStorage';
@override
String get nameImpl => AppSecureStorage.name;
@override @override
final String secretKey; final String secretKey;
static const name = 'AuroraAppSecureStorage';
/// Экземпляр хранилища данных /// Экземпляр хранилища данных
final _box = const FlutterSecureStorage(); final _box = const FlutterSecureStorage();
@@ -47,7 +51,4 @@ final class AppSecureStorage implements ISecureStorage {
Future<void> write(String key, String value) async { Future<void> write(String key, String value) async {
await _box.write(key: key, value: value); await _box.write(key: key, value: value);
} }
@override
String get nameImpl => AppSecureStorage.name;
} }

View File

@@ -0,0 +1,26 @@
import 'package:i_app_services/i_app_services.dart';
import 'package:url_launcher/url_launcher.dart' as url_launcher;
/// {@template app_url_launcher}
/// Класс для Аврора реализации сервиса работы с URL
/// {@endtemplate}
class AppUrlLauncher implements IUrlLauncher {
/// {@macro app_url_launcher}
AppUrlLauncher();
/// Наименование сервиса
static const String name = 'AuroraAppUrlLauncher';
@override
String get nameImpl => AppUrlLauncher.name;
@override
Future<bool> canLaunchUrl(Uri url) async {
return url_launcher.canLaunchUrl(url);
}
@override
Future<bool> launchUrl(Uri url) async {
return url_launcher.launchUrl(url);
}
}

View File

@@ -1,11 +1,10 @@
name: app_services name: app_services
description: "Google сервисы для приложения" description: "Аврора ОС сервисы для приложения"
version: 0.0.1 version: 0.0.1
publish_to: none publish_to: none
environment: environment:
sdk: ^3.8.0 sdk: '>=3.16.2 <4.0.0'
dependencies: dependencies:
flutter: flutter:
@@ -18,13 +17,11 @@ dependencies:
url: https://gitlab.com/omprussia/flutter/flutter-community-plugins/flutter_secure_storage_aurora.git url: https://gitlab.com/omprussia/flutter/flutter-community-plugins/flutter_secure_storage_aurora.git
ref: aurora-0.5.3 ref: aurora-0.5.3
# для работы с путями в хранилища # Зависимости для работы с путями (плагин встроен в sdk flutter 3.27.3)
path_provider: 2.1.4 path_provider: 2.1.5
path_provider_aurora:
git: # Зависимости для работы с открытием ссылок (плагин встроен в sdk flutter 3.27.3)
url: https://gitlab.com/omprussia/flutter/packages.git url_launcher: 6.3.1
ref: aurora-path_provider_aurora-0.6.0
path: packages/path_provider_aurora
# Обязательные интерфейсы # Обязательные интерфейсы
i_app_services: i_app_services:

View File

@@ -2,3 +2,4 @@ library;
export 'src/app_path_provider.dart'; export 'src/app_path_provider.dart';
export 'src/app_secure_storage.dart'; export 'src/app_secure_storage.dart';
export 'src/app_url_launcher.dart';

View File

@@ -11,6 +11,9 @@ class AppPathProvider implements IPathProvider {
/// Наименование сервиса /// Наименование сервиса
static const name = 'BaseAppPathProvider'; static const name = 'BaseAppPathProvider';
@override
String get nameImpl => AppPathProvider.name;
@override @override
Future<String> getAppDocumentsDirectoryPath() async { Future<String> getAppDocumentsDirectoryPath() async {
return (await getApplicationDocumentsDirectory()).path; return (await getApplicationDocumentsDirectory()).path;

View File

@@ -2,17 +2,24 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:i_app_services/i_app_services.dart'; import 'package:i_app_services/i_app_services.dart';
/// {@template app_secure_storage} /// {@template app_secure_storage}
/// Класс для Aurora реализации сервис по работе с защищенным хранилищем /// Класс для базовой реализации сервиса работы с защищенным хранилищем.
/// [secretKey] - ключ для шифрования данных, если нужен ///
/// Использует flutter_secure_storage для безопасного хранения данных.
/// Поддерживает все основные операции с защищенным хранилищем.
/// {@endtemplate} /// {@endtemplate}
final class AppSecureStorage implements ISecureStorage { final class AppSecureStorage implements ISecureStorage {
/// {@macro app_secure_storage}
AppSecureStorage({this.secretKey}); AppSecureStorage({this.secretKey});
/// Наименование сервиса
static const name = 'BaseAppSecureStorage';
@override
String get nameImpl => AppSecureStorage.name;
@override @override
final String? secretKey; final String? secretKey;
static const name = 'BaseAppSecureStorage';
/// Экземпляр хранилища данных /// Экземпляр хранилища данных
final _box = const FlutterSecureStorage(); final _box = const FlutterSecureStorage();
@@ -40,7 +47,4 @@ final class AppSecureStorage implements ISecureStorage {
Future<void> write(String key, String value) async { Future<void> write(String key, String value) async {
await _box.write(key: key, value: value); await _box.write(key: key, value: value);
} }
@override
String get nameImpl => AppSecureStorage.name;
} }

View File

@@ -0,0 +1,26 @@
import 'package:i_app_services/i_app_services.dart';
import 'package:url_launcher/url_launcher.dart' as url_launcher;
/// {@template app_url_launcher}
/// Класс для базовой реализации сервиса работы с URL
/// {@endtemplate}
class AppUrlLauncher implements IUrlLauncher {
/// {@macro app_url_launcher}
AppUrlLauncher();
/// Наименование сервиса
static const String name = 'BaseAppUrlLauncher';
@override
String get nameImpl => AppUrlLauncher.name;
@override
Future<bool> canLaunchUrl(Uri url) async {
return url_launcher.canLaunchUrl(url);
}
@override
Future<bool> launchUrl(Uri url) async {
return url_launcher.launchUrl(url);
}
}

View File

@@ -19,6 +19,9 @@ dependencies:
# для работы с путями в хранилища # для работы с путями в хранилища
path_provider: 2.1.5 path_provider: 2.1.5
# Зависимости для сервиса внешних ссылок
url_launcher: 6.3.1
# Обязательные интерфейсы # Обязательные интерфейсы
i_app_services: i_app_services:
path: ../../i_app_services path: ../../i_app_services

View File

@@ -2,3 +2,4 @@ library;
export 'src/i_path_provider.dart'; export 'src/i_path_provider.dart';
export 'src/i_secure_storage.dart'; export 'src/i_secure_storage.dart';
export 'src/i_url_launcher.dart';

View File

@@ -1,9 +1,11 @@
/// Класс для описания интерфейса сервиса /// Класс для описания интерфейса сервиса для получения пути хранения файлов
/// для получения пути хранения файлов
abstract interface class IPathProvider { abstract interface class IPathProvider {
/// Наименования интерфейса /// Наименования интерфейса
static const name = 'IPathProvider'; static const name = 'IPathProvider';
/// Получение имени имплементации
String get nameImpl;
/// Получение path на внутренне хранилище приложения /// Получение path на внутренне хранилище приложения
Future<String?> getAppDocumentsDirectoryPath(); Future<String?> getAppDocumentsDirectoryPath();
} }

View File

@@ -1,4 +1,4 @@
/// Класс интерфейса для работы с защищенным хранилищем /// Класс для описания интерфейса для работы с защищенным хранилищем
abstract interface class ISecureStorage { abstract interface class ISecureStorage {
/// Описывает обязательные параметры имплементаций /// Описывает обязательные параметры имплементаций
/// ///
@@ -6,14 +6,17 @@ abstract interface class ISecureStorage {
/// - [secretKey] - секретный ключ для шифрования данных /// - [secretKey] - секретный ключ для шифрования данных
const ISecureStorage._({required this.secretKey}); const ISecureStorage._({required this.secretKey});
/// Наименования интерфейса
static const name = 'ISecureStorage';
/// Получение имени имплементации
String get nameImpl;
/// Секретный ключ для шифрования данных /// Секретный ключ для шифрования данных
/// Нужен, если надо передать ключ в реализацию /// Нужен, если надо передать ключ в реализацию
/// например, в Aurora /// например, в Aurora
final String? secretKey; final String? secretKey;
/// Наименования интерфейса
static const name = 'ISecureStorage';
/// Метод для получения значения из защищенного хранилища /// Метод для получения значения из защищенного хранилища
/// ///
/// Принимает: /// Принимает:
@@ -41,6 +44,4 @@ abstract interface class ISecureStorage {
/// Принимает: /// Принимает:
/// - [key] - ключ /// - [key] - ключ
Future<bool> containsKey(String key); Future<bool> containsKey(String key);
String get nameImpl;
} }

View File

@@ -0,0 +1,18 @@
/// Класс для описания интерфейса сервиса для запуска URL
abstract interface class IUrlLauncher {
/// Наименования интерфейса
static const name = 'IUrlLauncher';
/// Получение имени имплементации
String get nameImpl;
/// Метод для проверки возможности запуска ссылки
///
/// - [url] - ссылка для проверки
Future<bool> canLaunchUrl(Uri url);
/// Метод для запуска ссылки
///
/// - [url] - ссылка для запуска
Future<bool> launchUrl(Uri url);
}

View File

@@ -12,22 +12,43 @@ import 'package:friflex_starter/l10n/gen/app_localizations.dart';
import 'package:friflex_starter/l10n/localization_notifier.dart'; import 'package:friflex_starter/l10n/localization_notifier.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
/// Класс приложения /// {@template app}
/// Главный виджет приложения, управляющий инициализацией зависимостей
/// и отображением основного интерфейса приложения.
///
/// Отвечает за:
/// - Инициализацию зависимостей приложения
/// - Отображение экрана загрузки во время инициализации
/// - Обработку ошибок инициализации
/// - Настройку провайдеров для темы и локализации
/// {@endtemplate}
class App extends StatefulWidget { class App extends StatefulWidget {
/// {@macro app}
const App({required this.router, required this.initDependencies, super.key}); const App({required this.router, required this.initDependencies, super.key});
/// Роутер приложения /// Роутер приложения для навигации между экранами
final GoRouter router; final GoRouter router;
/// Функция для инициализации зависимостей /// Функция для инициализации зависимостей приложения
/// Возвращает Future с контейнером зависимостей
final Future<DiContainer> Function() initDependencies; final Future<DiContainer> Function() initDependencies;
@override @override
State<App> createState() => _AppState(); State<App> createState() => _AppState();
} }
/// {@template app_state}
/// Состояние главного виджета приложения.
///
/// Управляет процессом инициализации зависимостей и отображением
/// соответствующих экранов в зависимости от состояния инициализации.
/// {@endtemplate}
class _AppState extends State<App> { class _AppState extends State<App> {
/// {@macro app_state}
_AppState();
/// Мутабельная Future для инициализации зависимостей /// Мутабельная Future для инициализации зависимостей
/// Позволяет перезапускать инициализацию при ошибках
late Future<DiContainer> _initFuture; late Future<DiContainer> _initFuture;
@override @override
@@ -83,6 +104,8 @@ class _AppState extends State<App> {
); );
} }
/// Метод для перезапуска инициализации зависимостей
/// Вызывается при ошибках инициализации для повторной попытки
void _retryInit() { void _retryInit() {
setState(() { setState(() {
_initFuture = widget.initDependencies(); _initFuture = widget.initDependencies();
@@ -90,9 +113,17 @@ class _AppState extends State<App> {
} }
} }
/// {@template app_internal}
/// Внутренний виджет приложения, отображающий основной интерфейс
/// после успешной инициализации зависимостей.
///
/// Настраивает MaterialApp с роутером, темами и локализацией.
/// {@endtemplate}
class _App extends StatelessWidget { class _App extends StatelessWidget {
/// {@macro app_internal}
const _App({required this.router}); const _App({required this.router});
/// Роутер приложения для навигации
final GoRouter router; final GoRouter router;
@override @override

View File

@@ -1,12 +1,45 @@
import 'package:envied/envied.dart'; import 'package:envied/envied.dart';
import 'package:friflex_starter/app/app_config/i_app_config.dart';
import 'package:friflex_starter/app/app_env.dart'; import 'package:friflex_starter/app/app_env.dart';
part 'app_config.g.dart'; part 'app_config.g.dart';
/// Класс для реализации конфигурации с моковыми данными /// {@template i_app_config}
/// Интерфейс для конфигурации приложения.
///
/// Определяет обязательные параметры для всех реализаций конфигурации:
/// - Наименование конфигурации
/// - Базовый URL для API
/// - Тип окружения (dev, prod, stage)
/// - Секретный ключ для шифрования данных
/// {@endtemplate}
abstract interface class IAppConfig {
/// {@macro i_app_config}
IAppConfig();
/// Наименование сервиса конфигурации
String get name => 'IAppConfig';
/// Основной адрес для запросов к API
String get baseUrl;
/// Тип окружения (dev, prod, stage)
AppEnv get env;
/// Секретный ключ для шифрования данных
String get secretKey;
}
/// {@template app_config_dev}
/// Класс для реализации конфигурации приложения в режиме разработки.
///
/// Использует переменные окружения из файла env/dev.env.
/// Предназначен для локальной разработки и тестирования.
/// {@endtemplate}
@Envied(name: 'Dev', path: 'env/dev.env') @Envied(name: 'Dev', path: 'env/dev.env')
class AppConfigDev implements IAppConfig { class AppConfigDev implements IAppConfig {
/// {@macro app_config_dev}
AppConfigDev();
@override @override
AppEnv get env => AppEnv.dev; AppEnv get env => AppEnv.dev;
@@ -22,9 +55,17 @@ class AppConfigDev implements IAppConfig {
final String secretKey = _Dev.secretKey; final String secretKey = _Dev.secretKey;
} }
/// Класс для реализации конфигурации с продакшн данными /// {@template app_config_prod}
/// Класс для реализации конфигурации приложения в продакшн режиме.
///
/// Использует переменные окружения из файла env/prod.env.
/// Предназначен для финальной сборки приложения.
/// {@endtemplate}
@Envied(name: 'Prod', path: 'env/prod.env') @Envied(name: 'Prod', path: 'env/prod.env')
class AppConfigProd implements IAppConfig { class AppConfigProd implements IAppConfig {
/// {@macro app_config_prod}
AppConfigProd();
@override @override
AppEnv get env => AppEnv.prod; AppEnv get env => AppEnv.prod;
@@ -40,9 +81,17 @@ class AppConfigProd implements IAppConfig {
final String secretKey = _Prod.secretKey; final String secretKey = _Prod.secretKey;
} }
/// Класс для реализации конфигурации с стейдж данными /// {@template app_config_stage}
/// Класс для реализации конфигурации приложения в стейдж режиме.
///
/// Использует переменные окружения из файла env/stage.env.
/// Предназначен для тестирования в среде, близкой к продакшн.
/// {@endtemplate}
@Envied(name: 'Stage', path: 'env/stage.env') @Envied(name: 'Stage', path: 'env/stage.env')
class AppConfigStage implements IAppConfig { class AppConfigStage implements IAppConfig {
/// {@macro app_config_stage}
AppConfigStage();
@override @override
AppEnv get env => AppEnv.stage; AppEnv get env => AppEnv.stage;

View File

@@ -24,11 +24,13 @@ final class _Dev {
4081271699, 4081271699,
]; ];
static final String secretKey = String.fromCharCodes(List<int>.generate( static final String secretKey = String.fromCharCodes(
_envieddatasecretKey.length, List<int>.generate(
(int i) => i, _envieddatasecretKey.length,
growable: false, (int i) => i,
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i])); growable: false,
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i]),
);
} }
// coverage:ignore-file // coverage:ignore-file
@@ -65,11 +67,13 @@ final class _Prod {
655048645, 655048645,
]; ];
static final String baseUrl = String.fromCharCodes(List<int>.generate( static final String baseUrl = String.fromCharCodes(
_envieddatabaseUrl.length, List<int>.generate(
(int i) => i, _envieddatabaseUrl.length,
growable: false, (int i) => i,
).map((int i) => _envieddatabaseUrl[i] ^ _enviedkeybaseUrl[i])); growable: false,
).map((int i) => _envieddatabaseUrl[i] ^ _enviedkeybaseUrl[i]),
);
static const List<int> _enviedkeysecretKey = <int>[ static const List<int> _enviedkeysecretKey = <int>[
359753139, 359753139,
@@ -85,11 +89,13 @@ final class _Prod {
3044498279, 3044498279,
]; ];
static final String secretKey = String.fromCharCodes(List<int>.generate( static final String secretKey = String.fromCharCodes(
_envieddatasecretKey.length, List<int>.generate(
(int i) => i, _envieddatasecretKey.length,
growable: false, (int i) => i,
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i])); growable: false,
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i]),
);
} }
// coverage:ignore-file // coverage:ignore-file
@@ -128,11 +134,13 @@ final class _Stage {
568662398, 568662398,
]; ];
static final String baseUrl = String.fromCharCodes(List<int>.generate( static final String baseUrl = String.fromCharCodes(
_envieddatabaseUrl.length, List<int>.generate(
(int i) => i, _envieddatabaseUrl.length,
growable: false, (int i) => i,
).map((int i) => _envieddatabaseUrl[i] ^ _enviedkeybaseUrl[i])); growable: false,
).map((int i) => _envieddatabaseUrl[i] ^ _enviedkeybaseUrl[i]),
);
static const List<int> _enviedkeysecretKey = <int>[ static const List<int> _enviedkeysecretKey = <int>[
2132342089, 2132342089,
@@ -150,9 +158,11 @@ final class _Stage {
1192880631, 1192880631,
]; ];
static final String secretKey = String.fromCharCodes(List<int>.generate( static final String secretKey = String.fromCharCodes(
_envieddatasecretKey.length, List<int>.generate(
(int i) => i, _envieddatasecretKey.length,
growable: false, (int i) => i,
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i])); growable: false,
).map((int i) => _envieddatasecretKey[i] ^ _enviedkeysecretKey[i]),
);
} }

View File

@@ -1,16 +0,0 @@
import 'package:friflex_starter/app/app_env.dart';
/// Класс для описания интерфейса конфигурации
abstract interface class IAppConfig {
/// Наименование сервиса
String get name => 'IAppConfig';
/// Основной адрес для запросов к API
String get baseUrl;
/// Тип окружения
AppEnv get env;
/// Секретный ключ для шифрования данных
String get secretKey;
}

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:friflex_starter/app/theme/app_colors_scheme.dart';
import 'package:friflex_starter/app/theme/theme_notifier.dart'; import 'package:friflex_starter/app/theme/theme_notifier.dart';
import 'package:friflex_starter/di/di_container.dart'; import 'package:friflex_starter/di/di_container.dart';
import 'package:friflex_starter/l10n/gen/app_localizations.dart'; import 'package:friflex_starter/l10n/gen/app_localizations.dart';
@@ -11,9 +10,6 @@ extension AppContextExt on BuildContext {
/// Метод для получения экземпляра DIContainer /// Метод для получения экземпляра DIContainer
DiContainer get di => read<DiContainer>(); DiContainer get di => read<DiContainer>();
/// Геттер для получения цветовой схемы
AppColors get colors => Theme.of(this).extension<AppColors>()!;
/// Геттер для получения темы /// Геттер для получения темы
ThemeNotifier get theme => read<ThemeNotifier>(); ThemeNotifier get theme => read<ThemeNotifier>();

View File

@@ -1,5 +1,5 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:friflex_starter/app/app_config/i_app_config.dart'; import 'package:friflex_starter/app/app_config/app_config.dart';
import 'package:friflex_starter/app/http/i_http_client.dart'; import 'package:friflex_starter/app/http/i_http_client.dart';
import 'package:friflex_starter/features/debug/i_debug_service.dart'; import 'package:friflex_starter/features/debug/i_debug_service.dart';

View File

@@ -1,10 +1,13 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:theme_tailor_annotation/theme_tailor_annotation.dart';
part 'app_colors_scheme.tailor.dart';
/// {@template app_colors} /// {@template app_colors}
/// Класс, реализующий расширение для добавления токенов в цветовую схему /// Класс, реализующий расширение для добавления токенов в цветовую схему
/// {@endtemplate} /// {@endtemplate}
class AppColors extends ThemeExtension<AppColors> with DiagnosticableTreeMixin { @TailorMixin(themeGetter: ThemeGetter.onBuildContext)
class AppColors extends ThemeExtension<AppColors> with _$AppColorsTailorMixin {
/// {@macro app_colors} /// {@macro app_colors}
/// ///
/// Принимает: /// Принимает:
@@ -23,18 +26,23 @@ class AppColors extends ThemeExtension<AppColors> with DiagnosticableTreeMixin {
}); });
/// Цвет тестовый /// Цвет тестовый
@override
final Color testColor; final Color testColor;
/// Цвет элемента текста /// Цвет элемента текста
@override
final Color itemTextColor; final Color itemTextColor;
/// Цвет фона снекбара ошибки /// Цвет фона снекбара ошибки
@override
final Color errorSnackbarBackground; final Color errorSnackbarBackground;
/// Цвет фона снекбара успеха /// Цвет фона снекбара успеха
@override
final Color successSnackbarBackground; final Color successSnackbarBackground;
/// Цвет фона снекбара информации /// Цвет фона снекбара информации
@override
final Color infoSnackbarBackground; final Color infoSnackbarBackground;
/// Цвета светлой темы /// Цвета светлой темы
@@ -54,50 +62,4 @@ class AppColors extends ThemeExtension<AppColors> with DiagnosticableTreeMixin {
infoSnackbarBackground: const Color.fromARGB(255, 35, 147, 178), infoSnackbarBackground: const Color.fromARGB(255, 35, 147, 178),
itemTextColor: Colors.white, itemTextColor: Colors.white,
); );
@override
ThemeExtension<AppColors> copyWith({
Color? testColor,
Color? errorSnackbarBackground,
Color? successSnackbarBackground,
Color? infoSnackbarBackground,
Color? itemTextColor,
}) => AppColors(
testColor: testColor ?? this.testColor,
errorSnackbarBackground:
errorSnackbarBackground ?? this.errorSnackbarBackground,
successSnackbarBackground:
successSnackbarBackground ?? this.successSnackbarBackground,
infoSnackbarBackground:
infoSnackbarBackground ?? this.infoSnackbarBackground,
itemTextColor: itemTextColor ?? this.itemTextColor,
);
@override
ThemeExtension<AppColors> lerp(
covariant ThemeExtension<AppColors>? other,
double t,
) {
if (other is! AppColors) return this;
return AppColors(
testColor: Color.lerp(testColor, other.testColor, t)!,
errorSnackbarBackground: Color.lerp(
errorSnackbarBackground,
other.errorSnackbarBackground,
t,
)!,
successSnackbarBackground: Color.lerp(
successSnackbarBackground,
other.successSnackbarBackground,
t,
)!,
infoSnackbarBackground: Color.lerp(
infoSnackbarBackground,
other.infoSnackbarBackground,
t,
)!,
itemTextColor: Color.lerp(itemTextColor, other.itemTextColor, t)!,
);
}
} }

View File

@@ -0,0 +1,102 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'app_colors_scheme.dart';
// **************************************************************************
// TailorAnnotationsGenerator
// **************************************************************************
mixin _$AppColorsTailorMixin on ThemeExtension<AppColors> {
Color get testColor;
Color get itemTextColor;
Color get errorSnackbarBackground;
Color get successSnackbarBackground;
Color get infoSnackbarBackground;
@override
AppColors copyWith({
Color? testColor,
Color? itemTextColor,
Color? errorSnackbarBackground,
Color? successSnackbarBackground,
Color? infoSnackbarBackground,
}) {
return AppColors(
testColor: testColor ?? this.testColor,
itemTextColor: itemTextColor ?? this.itemTextColor,
errorSnackbarBackground:
errorSnackbarBackground ?? this.errorSnackbarBackground,
successSnackbarBackground:
successSnackbarBackground ?? this.successSnackbarBackground,
infoSnackbarBackground:
infoSnackbarBackground ?? this.infoSnackbarBackground,
);
}
@override
AppColors lerp(covariant ThemeExtension<AppColors>? other, double t) {
if (other is! AppColors) return this as AppColors;
return AppColors(
testColor: Color.lerp(testColor, other.testColor, t)!,
itemTextColor: Color.lerp(itemTextColor, other.itemTextColor, t)!,
errorSnackbarBackground: Color.lerp(
errorSnackbarBackground,
other.errorSnackbarBackground,
t,
)!,
successSnackbarBackground: Color.lerp(
successSnackbarBackground,
other.successSnackbarBackground,
t,
)!,
infoSnackbarBackground: Color.lerp(
infoSnackbarBackground,
other.infoSnackbarBackground,
t,
)!,
);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is AppColors &&
const DeepCollectionEquality().equals(testColor, other.testColor) &&
const DeepCollectionEquality().equals(
itemTextColor,
other.itemTextColor,
) &&
const DeepCollectionEquality().equals(
errorSnackbarBackground,
other.errorSnackbarBackground,
) &&
const DeepCollectionEquality().equals(
successSnackbarBackground,
other.successSnackbarBackground,
) &&
const DeepCollectionEquality().equals(
infoSnackbarBackground,
other.infoSnackbarBackground,
));
}
@override
int get hashCode {
return Object.hash(
runtimeType.hashCode,
const DeepCollectionEquality().hash(testColor),
const DeepCollectionEquality().hash(itemTextColor),
const DeepCollectionEquality().hash(errorSnackbarBackground),
const DeepCollectionEquality().hash(successSnackbarBackground),
const DeepCollectionEquality().hash(infoSnackbarBackground),
);
}
}
extension AppColorsBuildContext on BuildContext {
AppColors get appColors => Theme.of(this).extension<AppColors>()!;
}

View File

@@ -1,12 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
/// Тип функции для построения виджета с учетом темы
typedef ThemeBuilder = Widget Function(); typedef ThemeBuilder = Widget Function();
/// Виджет для подписки на изменение темы приложения /// {@template theme_consumer}
/// Виджет для подписки на изменения темы приложения.
///
/// Автоматически перестраивает дочерние виджеты при изменении темы,
/// обеспечивая реактивность интерфейса к изменениям настроек темы.
/// {@endtemplate}
class ThemeConsumer extends StatelessWidget { class ThemeConsumer extends StatelessWidget {
/// {@macro theme_consumer}
const ThemeConsumer({required this.builder, super.key}); const ThemeConsumer({required this.builder, super.key});
/// Функция для построения виджета с учетом текущей темы
final ThemeBuilder builder; final ThemeBuilder builder;
@override @override
@@ -19,12 +27,29 @@ class ThemeConsumer extends StatelessWidget {
} }
} }
/// Класс для управления темой приложения /// {@template theme_notifier}
/// Класс для управления темой приложения.
///
/// Отвечает за:
/// - Хранение текущего режима темы (светлая/темная/системная)
/// - Уведомление подписчиков об изменениях темы
/// - Переключение между режимами темы
/// {@endtemplate}
final class ThemeNotifier extends ChangeNotifier { final class ThemeNotifier extends ChangeNotifier {
/// {@macro theme_notifier}
ThemeNotifier();
/// Текущий режим темы приложения
/// По умолчанию используется системная тема
ThemeMode _themeMode = ThemeMode.system; ThemeMode _themeMode = ThemeMode.system;
/// Получение текущего режима темы
ThemeMode get themeMode => _themeMode; ThemeMode get themeMode => _themeMode;
/// Метод для переключения темы приложения.
///
/// Переключает между светлой и темной темой.
/// Если текущая тема светлая, переключает на темную и наоборот.
void changeTheme() { void changeTheme() {
_themeMode = _themeMode == ThemeMode.light _themeMode = _themeMode == ThemeMode.light
? ThemeMode.dark ? ThemeMode.dark

View File

@@ -1,7 +1,10 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
/// {@template h_box} /// {@template h_box}
/// HBox виджет для вертикального отступа (Надстройка над SizedBox) /// Виджет для создания вертикального отступа.
///
/// Надстройка над SizedBox, предназначенная для создания
/// отступов по вертикали с более понятным названием.
/// {@endtemplate} /// {@endtemplate}
class HBox extends SizedBox { class HBox extends SizedBox {
/// {@macro h_box} /// {@macro h_box}
@@ -9,7 +12,10 @@ class HBox extends SizedBox {
} }
/// {@template w_box} /// {@template w_box}
/// WBox виджет для вертикального отступа (Надстройка над SizedBox) /// Виджет для создания горизонтального отступа.
///
/// Надстройка над SizedBox, предназначенная для создания
/// отступов по горизонтали с более понятным названием.
/// {@endtemplate} /// {@endtemplate}
class WBox extends SizedBox { class WBox extends SizedBox {
/// {@macro w_box} /// {@macro w_box}

View File

@@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart'; import 'package:friflex_starter/app/theme/app_colors_scheme.dart';
import 'package:friflex_starter/app/ui_kit/app_box.dart'; import 'package:friflex_starter/app/ui_kit/app_box.dart';
/// {@template app_snackbar} /// {@template app_snackbar}
@@ -264,9 +264,9 @@ class _AppSnackBarState extends State<AppSnackBar>
/// [TypeSnackBar.error] - цвет ошибки /// [TypeSnackBar.error] - цвет ошибки
Color _getBackgroundColor(TypeSnackBar type) { Color _getBackgroundColor(TypeSnackBar type) {
return switch (type) { return switch (type) {
TypeSnackBar.success => context.colors.successSnackbarBackground, TypeSnackBar.success => context.appColors.successSnackbarBackground,
TypeSnackBar.error => context.colors.errorSnackbarBackground, TypeSnackBar.error => context.appColors.errorSnackbarBackground,
TypeSnackBar.info => context.colors.infoSnackbarBackground, TypeSnackBar.info => context.appColors.infoSnackbarBackground,
}; };
} }
} }

View File

@@ -1,6 +1,17 @@
/// Миксин репозитория в приложении. /// {@template di_base_repo}
/// Каждый интерфейс репозитория в приложении должен подмешивать текущий класс /// Базовый миксин для всех репозиториев в приложении.
///
/// Предоставляет общую функциональность для всех репозиториев:
/// - Уникальное наименование репозитория
/// - Базовую структуру для DI контейнера
///
/// Каждый репозиторий в приложении должен использовать этот миксин
/// для обеспечения совместимости с системой зависимостей.
/// {@endtemplate}
mixin class DiBaseRepo { mixin class DiBaseRepo {
/// Наименование репозитория /// {@macro di_base_repo}
DiBaseRepo();
/// Наименование репозитория для идентификации в DI контейнере
String get name => 'DiBaseRepo'; String get name => 'DiBaseRepo';
} }

View File

@@ -1,5 +1,4 @@
import 'package:friflex_starter/app/app_config/app_config.dart'; import 'package:friflex_starter/app/app_config/app_config.dart';
import 'package:friflex_starter/app/app_config/i_app_config.dart';
import 'package:friflex_starter/app/app_env.dart'; import 'package:friflex_starter/app/app_env.dart';
import 'package:friflex_starter/app/http/app_http_client.dart'; import 'package:friflex_starter/app/http/app_http_client.dart';
import 'package:friflex_starter/app/http/i_http_client.dart'; import 'package:friflex_starter/app/http/i_http_client.dart';

View File

@@ -13,7 +13,7 @@ import 'package:friflex_starter/features/profile/data/repository/profile_reposit
import 'package:friflex_starter/features/profile/domain/repository/i_profile_repository.dart'; import 'package:friflex_starter/features/profile/domain/repository/i_profile_repository.dart';
/// Список названий моковых репозиториев, которые должны быть подменены /// Список названий моковых репозиториев, которые должны быть подменены
/// для использования в сборке stage окружения /// для использования в сборке stage окружения.
/// ///
/// Для того, чтобы репозиторий был автоматически подменен на моковый в stage /// Для того, чтобы репозиторий был автоматически подменен на моковый в stage
/// сборке, необходимо в этом списке указать название мокового репозитория, /// сборке, необходимо в этом списке указать название мокового репозитория,
@@ -25,12 +25,24 @@ import 'package:friflex_starter/features/profile/domain/repository/i_profile_rep
/// ``` /// ```
final List<String> _mockReposToSwitch = []; final List<String> _mockReposToSwitch = [];
/// Класс для инициализации репозиториев в приложении /// {@template di_repositories}
/// Класс для инициализации и управления репозиториями приложения.
/// ///
/// По умолчанию репозиторию присваивается моковая реализация. /// Отвечает за:
/// В зависимости от окружения либо выполняется подмена репозиторий, /// - Инициализацию репозиториев для работы с данными
/// либо используется моковый. /// - Автоматическое переключение между моковыми и реальными репозиториями
/// - Уведомление о прогрессе инициализации
/// - Обработку ошибок инициализации репозиториев
///
/// Стратегия инициализации по окружениям:
/// - dev: всегда используются моковые репозитории
/// - prod: всегда используются реальные репозитории
/// - stage: используются моковые репозитории из списка _mockReposToSwitch
/// {@endtemplate}
final class DiRepositories { final class DiRepositories {
/// {@macro di_repositories}
DiRepositories();
/// Интерфейс для работы с репозиторием авторизации /// Интерфейс для работы с репозиторием авторизации
late final IAuthRepository authRepository; late final IAuthRepository authRepository;
@@ -40,18 +52,24 @@ final class DiRepositories {
/// Интерфейс для работы с репозиторием профиля /// Интерфейс для работы с репозиторием профиля
late final IProfileRepository profileRepository; late final IProfileRepository profileRepository;
/// Метод для инициализации репозиториев в приложении /// Метод для инициализации репозиториев в приложении.
/// ///
/// Принимает: /// Принимает:
/// - [onProgress] - обратный вызов при прогрессе /// - [onProgress] - обратный вызов для уведомления о прогрессе инициализации
/// - [diContainer] - контейнер зависимостей /// - [diContainer] - контейнер зависимостей с конфигурацией приложения
/// - [onError] - обратный вызов для обработки ошибок инициализации
///
/// Последовательность инициализации:
/// 1. Инициализация репозитория авторизации
/// 2. Инициализация репозитория главного сервиса
/// 3. Инициализация репозитория профиля
void init({ void init({
required OnProgress onProgress, required OnProgress onProgress,
required OnError onError, required OnError onError,
required DiContainer diContainer, required DiContainer diContainer,
}) { }) {
try { try {
//Инициализация репозитория авторизации // Инициализация репозитория авторизации
authRepository = _lazyInitRepo<IAuthRepository>( authRepository = _lazyInitRepo<IAuthRepository>(
mockFactory: AuthMockRepository.new, mockFactory: AuthMockRepository.new,
mainFactory: () => AuthRepository( mainFactory: () => AuthRepository(
@@ -125,9 +143,13 @@ final class DiRepositories {
/// В зависимости от окружения инициализируется моковый или сетевой репозиторий. /// В зависимости от окружения инициализируется моковый или сетевой репозиторий.
/// ///
/// Принимает: /// Принимает:
/// - [mockFactory] - функция - фабрика для инициализации репозитория для управления моковыми запросами /// - [mockFactory] - функция-фабрика для инициализации мокового репозитория
/// - [mainFactory] - функция - фабрика для инициализации основного репозиторий /// - [mainFactory] - функция-фабрика для инициализации основного репозитория
/// - [onProgress] - обратный вызов при прогрессе /// - [onProgress] - обратный вызов для уведомления о прогрессе
/// - [environment] - окружение приложения для определения стратегии инициализации
///
/// Возвращает:
/// - Экземпляр репозитория в зависимости от окружения
T _lazyInitRepo<T extends DiBaseRepo>({ T _lazyInitRepo<T extends DiBaseRepo>({
required AppEnv environment, required AppEnv environment,
required T Function() mainFactory, required T Function() mainFactory,

View File

@@ -3,20 +3,39 @@ import 'package:friflex_starter/di/di_container.dart';
import 'package:friflex_starter/di/di_typedefs.dart'; import 'package:friflex_starter/di/di_typedefs.dart';
import 'package:i_app_services/i_app_services.dart'; import 'package:i_app_services/i_app_services.dart';
/// Класс для инициализации сервисов /// {@template di_services}
/// Класс для инициализации и управления сервисами приложения.
///
/// Отвечает за:
/// - Инициализацию сервисов для работы с путями
/// - Инициализацию сервисов для работы с защищенным хранилищем
/// - Уведомление о прогрессе инициализации
/// - Обработку ошибок инициализации сервисов
/// {@endtemplate}
final class DiServices { final class DiServices {
/// Сервис для работы с путями /// {@macro di_services}
DiServices();
/// Сервис для работы с путями файловой системы
late final IPathProvider pathProvider; late final IPathProvider pathProvider;
/// Сервис для работы с локальным хранилищем /// Сервис для работы с защищенным локальным хранилищем
late final ISecureStorage secureStorage; late final ISecureStorage secureStorage;
/// Метод для инициализации репозиториев в приложении /// Сервис для работы с URL
late final IUrlLauncher urlLauncher;
/// Метод для инициализации сервисов в приложении.
/// ///
/// Принимает: /// Принимает:
/// - [onProgress] - обратный вызов при прогрессе /// - [onProgress] - обратный вызов для уведомления о прогрессе инициализации
/// - [diContainer] - контейнер зависимостей /// - [diContainer] - контейнер зависимостей с конфигурацией приложения
/// - [onError] - обратный вызов при ошибке /// - [onError] - обратный вызов для обработки ошибок инициализации
///
/// Последовательность инициализации:
/// 1. Инициализация сервиса путей (AppPathProvider)
/// 2. Инициализация защищенного хранилища (AppSecureStorage)
/// 3. Инициализация сервиса URL (AppUrlLauncherService)
void init({ void init({
required OnProgress onProgress, required OnProgress onProgress,
required OnError onError, required OnError onError,
@@ -29,14 +48,19 @@ final class DiServices {
onError('Ошибка инициализации ${IPathProvider.name}', error, stackTrace); onError('Ошибка инициализации ${IPathProvider.name}', error, stackTrace);
} }
try { try {
secureStorage = AppSecureStorage( secureStorage = AppSecureStorage(secretKey: diContainer.appConfig.secretKey);
secretKey: diContainer.appConfig.secretKey,
);
onProgress(AppSecureStorage.name); onProgress(AppSecureStorage.name);
} on Object catch (error, stackTrace) { } on Object catch (error, stackTrace) {
onError('Ошибка инициализации ${ISecureStorage.name}', error, stackTrace); onError('Ошибка инициализации ${ISecureStorage.name}', error, stackTrace);
} }
try {
urlLauncher = AppUrlLauncher();
onProgress(AppUrlLauncher.name);
} on Object catch (error, stackTrace) {
onError('Ошибка инициализации ${IUrlLauncher.name}', error, stackTrace);
}
onProgress('Инициализация сервисов завершена!'); onProgress('Инициализация сервисов завершена!');
} }
} }

View File

@@ -1,10 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// {@template AuthScreen} /// {@template auth_screen}
/// Экран авторизации пользователя.
/// ///
/// Отвечает за:
/// - Отображение формы входа в приложение
/// - Обработку процесса аутентификации
/// - Навигацию после успешной авторизации
///
/// В текущей реализации является заглушкой для будущей функциональности.
/// {@endtemplate} /// {@endtemplate}
class AuthScreen extends StatelessWidget { class AuthScreen extends StatelessWidget {
/// {@macro AuthScreen} /// {@macro auth_screen}
const AuthScreen({super.key}); const AuthScreen({super.key});
@override @override

View File

@@ -2,9 +2,12 @@ import 'package:friflex_starter/features/debug/screens/components_screen.dart';
import 'package:friflex_starter/features/debug/screens/debug_screen.dart'; import 'package:friflex_starter/features/debug/screens/debug_screen.dart';
import 'package:friflex_starter/features/debug/screens/icons_screen.dart'; import 'package:friflex_starter/features/debug/screens/icons_screen.dart';
import 'package:friflex_starter/features/debug/screens/lang_screen.dart'; import 'package:friflex_starter/features/debug/screens/lang_screen.dart';
import 'package:friflex_starter/features/debug/screens/path_provider_screen.dart';
import 'package:friflex_starter/features/debug/screens/secure_storage_screen.dart';
import 'package:friflex_starter/features/debug/screens/theme_screen.dart'; import 'package:friflex_starter/features/debug/screens/theme_screen.dart';
import 'package:friflex_starter/features/debug/screens/tokens_screen.dart'; import 'package:friflex_starter/features/debug/screens/tokens_screen.dart';
import 'package:friflex_starter/features/debug/screens/ui_kit_screen.dart'; import 'package:friflex_starter/features/debug/screens/ui_kit_screen.dart';
import 'package:friflex_starter/features/debug/screens/url_launcher_screen.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
/// {@template debug_routes} /// {@template debug_routes}
@@ -20,6 +23,9 @@ abstract final class DebugRoutes {
static const String themeScreenName = 'theme_screen'; static const String themeScreenName = 'theme_screen';
static const String langScreenName = 'lang_screen'; static const String langScreenName = 'lang_screen';
static const String componentsScreenName = 'components_screen'; static const String componentsScreenName = 'components_screen';
static const String pathProviderScreenName = 'path_provider_screen';
static const String secureStorageScreenName = 'secure_storage_screen';
static const String urlLauncherScreenName = 'url_launcher_screen';
/// Пути к экранам /// Пути к экранам
static const String debugScreenPath = '/debug'; static const String debugScreenPath = '/debug';
@@ -29,6 +35,9 @@ abstract final class DebugRoutes {
static const String themeScreenPath = 'debug/theme'; static const String themeScreenPath = 'debug/theme';
static const String langScreenPath = 'debug/lang'; static const String langScreenPath = 'debug/lang';
static const String componentsScreenPath = 'debug/components'; static const String componentsScreenPath = 'debug/components';
static const String pathProviderScreenPath = 'debug/path_provider';
static const String secureStorageScreenPath = 'debug/secure_storage';
static const String urlLauncherScreenPath = 'debug/url_launcher';
/// Метод для создания роутов для отладки /// Метод для создания роутов для отладки
/// ///
@@ -70,6 +79,21 @@ abstract final class DebugRoutes {
name: componentsScreenName, name: componentsScreenName,
builder: (context, state) => const ComponentsScreen(), builder: (context, state) => const ComponentsScreen(),
), ),
GoRoute(
path: pathProviderScreenPath,
name: pathProviderScreenName,
builder: (context, state) => const PathProviderScreen(),
),
GoRoute(
path: secureStorageScreenPath,
name: secureStorageScreenName,
builder: (context, state) => const SecureStorageScreen(),
),
GoRoute(
path: urlLauncherScreenPath,
name: urlLauncherScreenName,
builder: (context, state) => const UrlLauncherScreen(),
),
], ],
); );
} }

View File

@@ -2,18 +2,33 @@ import 'package:flutter/material.dart';
import 'package:friflex_starter/app/ui_kit/app_box.dart'; import 'package:friflex_starter/app/ui_kit/app_box.dart';
import 'package:friflex_starter/app/ui_kit/app_snackbar.dart'; import 'package:friflex_starter/app/ui_kit/app_snackbar.dart';
/// {@template ComponentsScreen} /// {@template components_screen}
/// Экран для демонстрации компонентов приложения. /// Экран для демонстрации и тестирования компонентов приложения.
///
/// Отвечает за:
/// - Демонстрацию различных типов снекбаров (ошибка, успех, информация)
/// - Тестирование кастомных UI компонентов
/// - Предоставление примеров использования компонентов
/// - Валидацию корректности работы компонентов
/// {@endtemplate} /// {@endtemplate}
class ComponentsScreen extends StatefulWidget { class ComponentsScreen extends StatefulWidget {
/// {@macro ComponentsScreen} /// {@macro components_screen}
const ComponentsScreen({super.key}); const ComponentsScreen({super.key});
@override @override
State<ComponentsScreen> createState() => _ComponentsScreenState(); State<ComponentsScreen> createState() => _ComponentsScreenState();
} }
/// {@template components_screen_state}
/// Состояние экрана компонентов.
///
/// Управляет отображением различных типов снекбаров
/// и демонстрирует их функциональность.
/// {@endtemplate}
class _ComponentsScreenState extends State<ComponentsScreen> { class _ComponentsScreenState extends State<ComponentsScreen> {
/// {@macro components_screen_state}
_ComponentsScreenState();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:friflex_starter/app/ui_kit/app_box.dart';
import 'package:friflex_starter/app/app_context_ext.dart'; import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/app/ui_kit/app_box.dart';
import 'package:friflex_starter/features/debug/debug_routes.dart'; import 'package:friflex_starter/features/debug/debug_routes.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -20,9 +20,7 @@ class DebugScreen extends StatelessWidget {
children: [ children: [
Text('Окружение: ${context.di.appConfig.env.name}'), Text('Окружение: ${context.di.appConfig.env.name}'),
const HBox(22), const HBox(22),
Text( Text('Реализация AppServices: ${context.di.services.secureStorage.nameImpl}'),
'Реализация AppServices: ${context.di.services.secureStorage.nameImpl}',
),
const HBox(22), const HBox(22),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
@@ -74,14 +72,33 @@ class DebugScreen extends StatelessWidget {
}, },
child: const Text('Экран компонентов'), child: const Text('Экран компонентов'),
), ),
const HBox(16),
ElevatedButton(
onPressed: () {
context.pushNamed(DebugRoutes.pathProviderScreenName);
},
child: const Text('Экран Path Provider'),
),
const HBox(16),
ElevatedButton(
onPressed: () {
context.pushNamed(DebugRoutes.secureStorageScreenName);
},
child: const Text('Экран Secure Storage'),
),
const HBox(16),
ElevatedButton(
onPressed: () {
context.pushNamed(DebugRoutes.urlLauncherScreenName);
},
child: const Text('Экран Url Launcher'),
),
const HBox(22), const HBox(22),
const Text('Имитирование ошибок:'), const Text('Имитирование ошибок:'),
const HBox(16), const HBox(16),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
throw Exception( throw Exception('Тестовая ошибка Exception для отладки FlutterError');
'Тестовая ошибка Exception для отладки FlutterError',
);
}, },
child: const Text('Вызывать ошибку FlutterError'), child: const Text('Вызывать ошибку FlutterError'),
), ),

View File

@@ -2,11 +2,16 @@ import 'package:flutter/material.dart';
import 'package:friflex_starter/app/ui_kit/app_box.dart'; import 'package:friflex_starter/app/ui_kit/app_box.dart';
import 'package:friflex_starter/gen/assets.gen.dart'; import 'package:friflex_starter/gen/assets.gen.dart';
/// {@template IconsScreen} /// {@template icons_screen}
/// Экран для отрисовки иконок /// Экран для отображения всех доступных иконок приложения.
///
/// Отвечает за:
/// - Отображение списка всех SVG иконок из assets/icons/
/// - Предоставление возможности просмотра иконок для разработчиков
/// - Демонстрацию использования системы генерации ресурсов
/// {@endtemplate} /// {@endtemplate}
class IconsScreen extends StatelessWidget { class IconsScreen extends StatelessWidget {
/// {@macro IconsScreen} /// {@macro icons_screen}
const IconsScreen({super.key}); const IconsScreen({super.key});
@override @override
@@ -30,19 +35,20 @@ class IconsScreen extends StatelessWidget {
} }
} }
// Приватный класс для реализации элемента списка иконок /// {@template item_icon}
/// Виджет для отображения отдельной иконки в списке.
///
/// Отображает SVG иконку вместе с её названием файла
/// для удобства идентификации в процессе разработки.
/// {@endtemplate}
class _ItemIcon extends StatelessWidget { class _ItemIcon extends StatelessWidget {
/// Создает экземпляр элемента списка иконок /// {@macro item_icon}
///
/// Принимает:
/// - [icon] - иконка
/// - [name] - название иконки
const _ItemIcon({required this.icon, required this.name}); const _ItemIcon({required this.icon, required this.name});
/// Иконка /// SVG иконка для отображения
final Widget icon; final Widget icon;
/// Название иконки /// Название файла иконки для идентификации
final String name; final String name;
@override @override

View File

@@ -1,13 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart'; import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/app/theme/app_colors_scheme.dart';
import 'package:friflex_starter/gen/assets.gen.dart'; import 'package:friflex_starter/gen/assets.gen.dart';
import 'package:friflex_starter/gen/fonts.gen.dart'; import 'package:friflex_starter/gen/fonts.gen.dart';
/// {@template LangScreen} /// {@template lang_screen}
/// Экран для отладки языков приложения /// Экран для отладки и тестирования локализации приложения.
///
/// Отвечает за:
/// - Демонстрацию переключения между поддерживаемыми языками
/// - Отображение локализованных строк с разными шрифтами
/// - Тестирование системы локализации и шрифтов
/// - Показ текущего языка приложения
/// {@endtemplate} /// {@endtemplate}
class LangScreen extends StatelessWidget { class LangScreen extends StatelessWidget {
/// {@macro LangScreen} /// {@macro lang_screen}
const LangScreen({super.key}); const LangScreen({super.key});
@override @override
@@ -36,7 +43,7 @@ class LangScreen extends StatelessWidget {
Text( Text(
'Тестовое слово bold: ${context.l10n.helloWorld}', 'Тестовое слово bold: ${context.l10n.helloWorld}',
style: TextStyle( style: TextStyle(
color: context.colors.testColor, color: context.appColors.testColor,
fontFamily: Assets.fonts.montserratBold, fontFamily: Assets.fonts.montserratBold,
), ),
), ),
@@ -44,7 +51,7 @@ class LangScreen extends StatelessWidget {
Text( Text(
'Тестовое слово medium: ${context.l10n.helloWorld}', 'Тестовое слово medium: ${context.l10n.helloWorld}',
style: TextStyle( style: TextStyle(
color: context.colors.testColor, color: context.appColors.testColor,
fontFamily: FontFamily.montserrat, fontFamily: FontFamily.montserrat,
), ),
), ),

View File

@@ -0,0 +1,147 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/app/ui_kit/app_box.dart';
import 'package:i_app_services/i_app_services.dart';
/// {@template path_provider_screen}
/// Экран для отладки и тестирования плагина path_provider.
///
/// Отвечает за:
/// - Тестирование работы реализаций плагина для получения путей к директориям приложения
/// - Демонстрацию содержимого директории файлов приложения
/// {@endtemplate}
class PathProviderScreen extends StatefulWidget {
/// {@macro path_provider_screen}
const PathProviderScreen({super.key});
@override
State<PathProviderScreen> createState() => _PathProviderScreenState();
}
class _PathProviderScreenState extends State<PathProviderScreen> {
/// Плагин для работы с путями в приложении
late final IPathProvider _pathProvider;
/// Корневой путь к директории файлов приложения
String? _rootPath;
/// Текущий путь к директории, отображаемой в списке
String? _currentPath;
/// Загрузка файлов
Future<List<FileSystemEntity>?>? _loadFilesFuture;
@override
void initState() {
super.initState();
_pathProvider = context.di.services.pathProvider;
_loadFilesFuture = _initRoot();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Path Provider')),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Реализация Path Provider: ${context.di.services.pathProvider.nameImpl}'),
const HBox(8),
Text('Содержимое папки документов приложения:'),
const HBox(8),
Text('Текущий путь:'),
const HBox(8),
Text(_currentPath ?? ''),
const HBox(8),
ElevatedButton(
onPressed: _currentPath != null && _rootPath != null && _currentPath != _rootPath
? _goBack
: null,
child: const Text('Назад'),
),
Expanded(
child: FutureBuilder<List<FileSystemEntity>?>(
future: _loadFilesFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Ошибка: \\${snapshot.error}'));
}
final files = snapshot.data;
if (files == null) {
return const Center(child: Text('Недоступно'));
}
if (files.isEmpty) {
return const Center(child: Text('Папка пуста'));
}
return ListView(
children: files
.map(
(item) => ListTile(
leading: Icon(
item is Directory ? Icons.folder : Icons.insert_drive_file,
),
title: Text(item.path.split(Platform.pathSeparator).last),
onTap: item is Directory ? () => _openDir(item.path) : null,
),
)
.toList(),
);
},
),
),
],
),
),
);
}
/// Метод для инициализации корневой директории и загрузки её содержимого
Future<List<FileSystemEntity>?> _initRoot() async {
final dirPath = await _pathProvider.getAppDocumentsDirectoryPath();
if (dirPath == null) {
setState(() {
_rootPath = null;
_currentPath = null;
});
return null;
}
final files = Directory(dirPath).listSync();
setState(() {
_rootPath = dirPath;
_currentPath = dirPath;
});
return files;
}
/// Метод для загрузки файлов в указанной директории
List<FileSystemEntity>? _loadFiles(String path) {
return Directory(path).listSync();
}
/// Метод для открытия директории и загрузки её содержимого
void _openDir(String path) async {
setState(() {
_currentPath = path;
_loadFilesFuture = Future.value(_loadFiles(path));
});
}
/// Метод для перехода к родительской директории
void _goBack() async {
if (_currentPath == null || _rootPath == null || _currentPath == _rootPath) return;
final parent = Directory(_currentPath!).parent.path;
if (parent.length < _rootPath!.length) return;
final files = _loadFiles(parent);
setState(() {
_currentPath = parent;
_loadFilesFuture = Future.value(files);
});
}
}

View File

@@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/app/ui_kit/app_box.dart';
import 'package:friflex_starter/app/ui_kit/app_snackbar.dart';
import 'package:i_app_services/i_app_services.dart';
/// {@template secure_storage_screen}
/// Экран для отладки и тестирования плагина flutter_secure_storage.
///
/// Отвечает за:
/// - Тестирование работы реализаций плагина для провеки записи и чтения защищенных данных
/// {@endtemplate}
class SecureStorageScreen extends StatefulWidget {
/// {@macro secure_storage_screen}
const SecureStorageScreen({super.key});
@override
State<SecureStorageScreen> createState() => _SecureStorageScreenState();
}
class _SecureStorageScreenState extends State<SecureStorageScreen> {
/// Плагин для работы с защищенным хранилищем
late final ISecureStorage _secureStorage;
/// Контроллер для ввода ключа
final TextEditingController _keyController = TextEditingController();
/// Контроллер для ввода значения
final TextEditingController _valueController = TextEditingController();
@override
void initState() {
super.initState();
_secureStorage = context.di.services.secureStorage;
}
@override
void dispose() {
_keyController.dispose();
_valueController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Secure Storage')),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Реализация Secure Storage: ${context.di.services.secureStorage.nameImpl}'),
const HBox(8),
TextField(
controller: _keyController,
onChanged: (value) {
_valueController.clear();
},
decoration: const InputDecoration(labelText: 'Ключ'),
),
const HBox(8),
TextField(
controller: _valueController,
decoration: const InputDecoration(labelText: 'Значение'),
),
const HBox(8),
ElevatedButton(
onPressed: () => _handleWrite(context),
child: const Text('Записать в Secure Storage'),
),
const HBox(8),
ElevatedButton(
onPressed: () => _handleRead(context),
child: const Text('Прочитать из Secure Storage'),
),
const HBox(8),
ElevatedButton(
onPressed: () => _handleDelete(context),
child: const Text('Удалить из Secure Storage'),
),
],
),
),
);
}
/// Обработчик для записи значения в Secure Storage
Future<void> _handleWrite(BuildContext context) async {
final key = _keyController.text;
final value = _valueController.text;
try {
await _secureStorage.write(key, value);
if (!context.mounted) return;
AppSnackBar.showSuccess(context: context, message: 'Значение записано в Secure Storage');
} on Object catch (e) {
AppSnackBar.showError(context, message: 'Ошибка записи: $e');
}
}
/// Обработчик для чтения значения из Secure Storage
Future<void> _handleRead(BuildContext context) async {
final key = _keyController.text;
try {
final value = await _secureStorage.read(key) ?? 'Значение не найдено';
_valueController.value = TextEditingValue(text: value);
} on Object catch (e) {
if (!context.mounted) return;
AppSnackBar.showError(context, message: 'Ошибка чтения: $e');
}
}
/// Обработчик для удаления значения из Secure Storage
Future<void> _handleDelete(BuildContext context) async {
final key = _keyController.text;
try {
await _secureStorage.delete(key);
if (!context.mounted) return;
_valueController.clear();
AppSnackBar.showSuccess(context: context, message: 'Значение удалено из Secure Storage');
} on Object catch (e) {
AppSnackBar.showError(context, message: 'Ошибка удаления: $e');
}
}
}

View File

@@ -1,16 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart'; import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/app/theme/app_colors_scheme.dart';
/// {@template ThemeScreen} /// {@template theme_screen}
/// Экран для отладки темы приложения /// Экран для отладки и тестирования темы приложения.
///
/// Отвечает за:
/// - Демонстрацию переключения между светлой и темной темами
/// - Отображение тестовых цветов из цветовой схемы
/// - Показ текущего режима темы
/// - Тестирование системы управления темами
/// {@endtemplate} /// {@endtemplate}
class ThemeScreen extends StatelessWidget { class ThemeScreen extends StatelessWidget {
/// {@macro ThemeScreen} /// {@macro theme_screen}
const ThemeScreen({super.key}); const ThemeScreen({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = context.colors; final colors = context.appColors;
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Theme')), appBar: AppBar(title: const Text('Theme')),
body: Center( body: Center(
@@ -25,7 +32,7 @@ class ThemeScreen extends StatelessWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ColoredBox( ColoredBox(
color: context.colors.testColor, color: context.appColors.testColor,
child: const SizedBox(height: 100, width: 100), child: const SizedBox(height: 100, width: 100),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),

View File

@@ -1,10 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// {@template TokensScreen} /// {@template tokens_screen}
/// Экран для отображения токенов /// Экран для отображения и управления токенами аутентификации.
///
/// Отвечает за:
/// - Отображение текущих токенов доступа и обновления
/// - Демонстрацию работы с токенами в приложении
/// - Тестирование функциональности аутентификации
///
/// В текущей реализации является заглушкой для будущей функциональности.
/// {@endtemplate} /// {@endtemplate}
class TokensScreen extends StatelessWidget { class TokensScreen extends StatelessWidget {
/// {@macro TokensScreen} /// {@macro tokens_screen}
const TokensScreen({super.key}); const TokensScreen({super.key});
@override @override

View File

@@ -1,11 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// {@template UiKitScreen} /// {@template ui_kit_screen}
/// Экран для отрисовки UI Kit /// Экран для демонстрации и тестирования компонентов UI Kit.
/// и тестирования его компонентов. ///
/// Отвечает за:
/// - Отображение всех доступных компонентов UI Kit
/// - Демонстрацию использования кастомных виджетов
/// - Тестирование стилей и тем оформления
/// - Предоставление примера использования UI компонентов
///
/// В текущей реализации является заглушкой для будущих компонентов.
/// {@endtemplate} /// {@endtemplate}
class UiKitScreen extends StatelessWidget { class UiKitScreen extends StatelessWidget {
/// {@macro UiKitScreen} /// {@macro ui_kit_screen}
const UiKitScreen({super.key}); const UiKitScreen({super.key});
@override @override

View File

@@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/app/ui_kit/app_box.dart';
import 'package:friflex_starter/app/ui_kit/app_snackbar.dart';
import 'package:i_app_services/i_app_services.dart';
/// {@template url_launcher_screen}
/// Экран для отладки и тестирования плагина url_launcher.
///
/// Отвечает за:
/// - Тестирование работы реализаций плагина для проверки открытия URL
/// {@endtemplate}
class UrlLauncherScreen extends StatefulWidget {
/// {@macro url_launcher_screen}
const UrlLauncherScreen({super.key});
@override
State<UrlLauncherScreen> createState() => _UrlLauncherScreenState();
}
class _UrlLauncherScreenState extends State<UrlLauncherScreen> {
/// Плагин для работы с URL
late final IUrlLauncher _urlLauncher;
/// Контроллер для ввода URL для открытия
final TextEditingController _urlController = TextEditingController();
/// Контроллер для ввода URL для проверки возможности открытия
final TextEditingController _canOpenUrlController = TextEditingController();
@override
void initState() {
super.initState();
_urlLauncher = context.di.services.urlLauncher;
}
@override
void dispose() {
_urlController.dispose();
_canOpenUrlController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('URL Launcher')),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Реализация Url Launcher: ${context.di.services.urlLauncher.nameImpl}'),
const HBox(8),
TextField(
controller: _urlController,
decoration: const InputDecoration(labelText: 'Введите ссылку'),
),
const HBox(16),
ElevatedButton(
onPressed: () => _launchUrl(context),
child: const Text('Открыть ссылку'),
),
const HBox(16),
TextField(
controller: _canOpenUrlController,
decoration: const InputDecoration(labelText: 'Введите ссылку'),
),
const HBox(16),
ElevatedButton(
onPressed: () => _checkCanOpenUrl(context),
child: const Text('Проверить возможность открытия'),
),
],
),
),
);
}
/// Метод для открытия URL
Future<void> _launchUrl(BuildContext context) async {
final url = _urlController.text.trim();
if (url.isEmpty) {
AppSnackBar.showInfo(context, message: 'Введите ссылку для открытия');
return;
}
final uri = Uri.tryParse(url);
if (uri == null) {
AppSnackBar.showError(context, message: 'Некорректная ссылка: $url');
return;
}
try {
final success = await _urlLauncher.launchUrl(uri);
if (!context.mounted) return;
if (!success) {
AppSnackBar.showError(context, message: 'Не удалось открыть ссылку: $url');
}
} on Object catch (e) {
if (!context.mounted) return;
AppSnackBar.showError(context, message: 'Ошибка при открытии ссылки: $e');
}
}
/// Метод для проверки возможности открытия URL
Future<void> _checkCanOpenUrl(BuildContext context) async {
final url = _canOpenUrlController.text.trim();
if (url.isEmpty) {
AppSnackBar.showInfo(context, message: 'Введите ссылку для проверки');
return;
}
final uri = Uri.tryParse(url);
if (uri == null) {
AppSnackBar.showError(context, message: 'Некорректная ссылка: $url');
return;
}
try {
final canOpen = await _urlLauncher.canLaunchUrl(uri);
if (!context.mounted) return;
if (canOpen) {
AppSnackBar.showSuccess(context: context, message: 'Возможно открыть ссылку: $url');
} else {
AppSnackBar.showError(context, message: 'Не удалось открыть ссылку: $url');
}
} on Object catch (e) {
if (!context.mounted) return;
AppSnackBar.showError(context, message: 'Ошибка при проверке ссылки: $e');
}
}
}

View File

@@ -5,10 +5,16 @@ import 'package:friflex_starter/features/profile/domain/repository/i_profile_rep
part 'profile_event.dart'; part 'profile_event.dart';
part 'profile_state.dart'; part 'profile_state.dart';
/// {@template profile_bloc}
/// Bloc для управления состоянием профиля пользователя.
///
/// Обрабатывает события загрузки данных профиля и управляет
/// соответствующими состояниями (загрузка, успех, ошибка).
/// {@endtemplate}
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> { class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
/// {@macro profile_bloc}
ProfileBloc(this._profileRepository) : super(ProfileInitialState()) { ProfileBloc(this._profileRepository) : super(ProfileInitialState()) {
// Вам необходимо добавлять только // Регистрируем обработчики событий в конструкторе
// один обработчик событий в конструкторе
on<ProfileEvent>((event, emit) async { on<ProfileEvent>((event, emit) async {
if (event is ProfileFetchProfileEvent) { if (event is ProfileFetchProfileEvent) {
await _fetchProfile(event, emit); await _fetchProfile(event, emit);
@@ -16,8 +22,19 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
}); });
} }
/// Репозиторий для работы с данными профиля
final IProfileRepository _profileRepository; final IProfileRepository _profileRepository;
/// Метод для загрузки данных профиля пользователя.
///
/// Принимает:
/// - [event] - событие с ID пользователя для загрузки
/// - [emit] - функция для эмиссии состояний
///
/// Последовательность состояний:
/// 1. ProfileWaitingState - начало загрузки
/// 2. ProfileSuccessState - успешная загрузка с данными
/// 3. ProfileErrorState - ошибка загрузки с сообщением
Future<void> _fetchProfile( Future<void> _fetchProfile(
ProfileFetchProfileEvent event, ProfileFetchProfileEvent event,
Emitter<ProfileState> emit, Emitter<ProfileState> emit,

View File

@@ -3,17 +3,22 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:friflex_starter/app/app_context_ext.dart'; import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/features/profile/domain/bloc/profile_bloc.dart'; import 'package:friflex_starter/features/profile/domain/bloc/profile_bloc.dart';
// Класс экрана, где мы инициализируем ProfileBloc /// {@template profile_screen}
// и вызываем событие ProfileFetchProfileEvent /// Экран профиля пользователя.
///
/// Отвечает за:
/// - Инициализацию ProfileBloc с репозиторием профиля
/// - Отображение данных профиля пользователя
/// - Обработку состояний загрузки, успеха и ошибок
/// {@endtemplate}
class ProfileScreen extends StatelessWidget { class ProfileScreen extends StatelessWidget {
/// {@macro profile_screen}
const ProfileScreen({super.key}); const ProfileScreen({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final profileRepository = context.di.repositories.profileRepository; final profileRepository = context.di.repositories.profileRepository;
// Здесь мы инициализируем ProfileBloc // Инициализируем ProfileBloc с репозиторием и загружаем данные профиля
// и вызываем событие ProfileFetchProfileEvent
// Или любые другие события, которые вам нужны
return BlocProvider( return BlocProvider(
create: (context) => create: (context) =>
ProfileBloc(profileRepository) ProfileBloc(profileRepository)
@@ -23,8 +28,16 @@ class ProfileScreen extends StatelessWidget {
} }
} }
/// Виджет, который отображает UI экрана /// {@template profile_screen_view}
/// Виджет для отображения UI экрана профиля.
///
/// Отображает данные профиля в зависимости от состояния ProfileBloc:
/// - Индикатор загрузки во время получения данных
/// - Данные профиля при успешной загрузке
/// - Сообщение об ошибке при неудачной загрузке
/// {@endtemplate}
class _ProfileScreenView extends StatelessWidget { class _ProfileScreenView extends StatelessWidget {
/// {@macro profile_screen_view}
const _ProfileScreenView(); const _ProfileScreenView();
@override @override

View File

@@ -4,15 +4,21 @@ import 'package:friflex_starter/app/app_env.dart';
import 'package:friflex_starter/features/debug/debug_routes.dart'; import 'package:friflex_starter/features/debug/debug_routes.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
/// Класс для реализации корневой страницы приложения /// {@template root_screen}
/// Корневой экран приложения с навигационной структурой.
///
/// Отвечает за:
/// - Отображение основного навигационного интерфейса
/// - Управление переключением между основными разделами приложения
/// - Отображение кнопки отладки в не-продакшн окружениях
/// - Интеграцию с GoRouter для навигации
/// {@endtemplate}
class RootScreen extends StatelessWidget { class RootScreen extends StatelessWidget {
/// Создает корневую страницу приложения /// {@macro root_screen}
///
/// Принимает:
/// - [navigationShell] - текущая ветка навигации
const RootScreen({required this.navigationShell, super.key}); const RootScreen({required this.navigationShell, super.key});
/// Текущая ветка навигации /// Текущая ветка навигации от GoRouter
/// Содержит информацию о текущем состоянии навигации
final StatefulNavigationShell navigationShell; final StatefulNavigationShell navigationShell;
@override @override
@@ -29,7 +35,7 @@ class RootScreen extends StatelessWidget {
body: navigationShell, body: navigationShell,
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[ items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Главная'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Профиль'), BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Профиль'),
], ],
currentIndex: navigationShell.currentIndex, currentIndex: navigationShell.currentIndex,

View File

@@ -33,12 +33,12 @@ class $AssetsFontsGen {
/// List of all assets /// List of all assets
List<String> get values => [ List<String> get values => [
montserratBold, montserratBold,
montserratExtraBold, montserratExtraBold,
montserratMedium, montserratMedium,
montserratRegular, montserratRegular,
montserratSemiBold montserratSemiBold,
]; ];
} }
class $AssetsIconsGen { class $AssetsIconsGen {
@@ -71,17 +71,11 @@ class Assets {
} }
class SvgGenImage { class SvgGenImage {
const SvgGenImage( const SvgGenImage(this._assetName, {this.size, this.flavors = const {}})
this._assetName, { : _isVecFormat = false;
this.size,
this.flavors = const {},
}) : _isVecFormat = false;
const SvgGenImage.vec( const SvgGenImage.vec(this._assetName, {this.size, this.flavors = const {}})
this._assetName, { : _isVecFormat = true;
this.size,
this.flavors = const {},
}) : _isVecFormat = true;
final String _assetName; final String _assetName;
final Size? size; final Size? size;
@@ -135,7 +129,8 @@ class SvgGenImage {
placeholderBuilder: placeholderBuilder, placeholderBuilder: placeholderBuilder,
semanticsLabel: semanticsLabel, semanticsLabel: semanticsLabel,
excludeFromSemantics: excludeFromSemantics, excludeFromSemantics: excludeFromSemantics,
colorFilter: colorFilter ?? colorFilter:
colorFilter ??
(color == null ? null : ColorFilter.mode(color, colorBlendMode)), (color == null ? null : ColorFilter.mode(color, colorBlendMode)),
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
cacheColorFilter: cacheColorFilter, cacheColorFilter: cacheColorFilter,
@@ -148,10 +143,7 @@ class SvgGenImage {
} }
class LottieGenImage { class LottieGenImage {
const LottieGenImage( const LottieGenImage(this._assetName, {this.flavors = const {}});
this._assetName, {
this.flavors = const {},
});
final String _assetName; final String _assetName;
final Set<String> flavors; final Set<String> flavors;
@@ -168,11 +160,8 @@ class LottieGenImage {
_lottie.LottieImageProviderFactory? imageProviderFactory, _lottie.LottieImageProviderFactory? imageProviderFactory,
Key? key, Key? key,
AssetBundle? bundle, AssetBundle? bundle,
Widget Function( Widget Function(BuildContext, Widget, _lottie.LottieComposition?)?
BuildContext, frameBuilder,
Widget,
_lottie.LottieComposition?,
)? frameBuilder,
ImageErrorWidgetBuilder? errorBuilder, ImageErrorWidgetBuilder? errorBuilder,
double? width, double? width,
double? height, double? height,

View File

@@ -1,12 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
/// Тип функции для построения виджета с учетом локализации
typedef LocalizationBuilder = Widget Function(); typedef LocalizationBuilder = Widget Function();
/// Виджет для перестройки виджета в зависимости от локализации /// {@template localization_consumer}
/// Виджет для подписки на изменения локализации приложения.
///
/// Автоматически перестраивает дочерние виджеты при изменении языка,
/// обеспечивая реактивность интерфейса к изменениям настроек локализации.
/// {@endtemplate}
class LocalizationConsumer extends StatelessWidget { class LocalizationConsumer extends StatelessWidget {
/// {@macro localization_consumer}
const LocalizationConsumer({required this.builder, super.key}); const LocalizationConsumer({required this.builder, super.key});
/// Функция для построения виджета с учетом текущей локализации
final LocalizationBuilder builder; final LocalizationBuilder builder;
@override @override
@@ -19,12 +27,34 @@ class LocalizationConsumer extends StatelessWidget {
} }
} }
/// Класс для управления локализацией /// {@template localization_notifier}
/// Класс для управления локализацией приложения.
///
/// Отвечает за:
/// - Хранение текущей локали приложения
/// - Уведомление подписчиков об изменениях языка
/// - Переключение между поддерживаемыми языками
/// {@endtemplate}
final class LocalizationNotifier extends ChangeNotifier { final class LocalizationNotifier extends ChangeNotifier {
Locale _locale = const Locale('en', 'US'); /// {@macro localization_notifier}
LocalizationNotifier();
/// Текущая локаль приложения
/// По умолчанию используется русский язык
Locale _locale = const Locale('ru', 'RU');
/// Получение текущей локали
Locale get locale => _locale; Locale get locale => _locale;
/// Получение текущего языка в виде кода языка
String get language => _locale.languageCode;
/// Метод для изменения локали приложения.
///
/// Принимает:
/// - [locale] - новая локаль для установки
///
/// Уведомляет всех подписчиков об изменении локали.
void changeLocal(Locale locale) { void changeLocal(Locale locale) {
_locale = locale; _locale = locale;
notifyListeners(); notifyListeners();

View File

@@ -5,23 +5,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "76.0.0" version: "82.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.3"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.11.0" version: "7.4.5"
ansicolor: ansicolor:
dependency: transitive dependency: transitive
description: description:
@@ -225,10 +220,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.8" version: "3.1.0"
dartx: dartx:
dependency: transitive dependency: transitive
description: description:
@@ -592,14 +587,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" version: "3.3.1"
macros:
dependency: transitive
description:
name: macros
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.3-main.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -881,10 +868,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_gen name: source_gen
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" version: "2.0.0"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
url: "https://pub.dev"
source: hosted
version: "1.3.5"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@@ -989,6 +984,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.4" version: "0.7.4"
theme_tailor:
dependency: "direct dev"
description:
name: theme_tailor
sha256: ba98be1d04856deef932757a3ca8fa7a5e2a6f96c30466a59c48924eeb608b97
url: "https://pub.dev"
source: hosted
version: "3.0.3"
theme_tailor_annotation:
dependency: "direct main"
description:
name: theme_tailor_annotation
sha256: "0d5ecd13a6a52add2082aa60497179f6093acf482eb69e7fa3a9f37eb990ac34"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
time: time:
dependency: transitive dependency: transitive
description: description:
@@ -1013,6 +1024,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
url_launcher:
dependency: transitive
description:
name: url_launcher
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.dev"
source: hosted
version: "6.3.1"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
url: "https://pub.dev"
source: hosted
version: "6.3.16"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
url: "https://pub.dev"
source: hosted
version: "6.3.3"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@@ -1021,6 +1056,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.1" version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:

View File

@@ -32,6 +32,8 @@ dependencies:
equatable: 2.0.7 equatable: 2.0.7
theme_tailor_annotation: 3.0.2
### основной сервис с интерфейсами ### основной сервис с интерфейсами
i_app_services: i_app_services:
path: ./app_services/i_app_services path: ./app_services/i_app_services
@@ -50,6 +52,7 @@ dev_dependencies:
flutter_gen_runner: 5.10.0 flutter_gen_runner: 5.10.0
flutter_gen: 5.10.0 flutter_gen: 5.10.0
flutter_lints: 6.0.0 flutter_lints: 6.0.0
theme_tailor: 3.0.3
flutter: flutter:
uses-material-design: true uses-material-design: true

View File

@@ -18,9 +18,7 @@ void main() {
/// Создание мок-темы с правильными стилями текста /// Создание мок-темы с правильными стилями текста
TextTheme createMockTextTheme() { TextTheme createMockTextTheme() {
return const TextTheme( return const TextTheme(bodyMedium: TextStyle(fontSize: 14));
bodyMedium: TextStyle(fontSize: 14),
);
} }
setUp(() { setUp(() {
@@ -41,7 +39,9 @@ void main() {
} }
group('AppSnackBar.showInfo', () { group('AppSnackBar.showInfo', () {
testTester('показывает снекбар с информацией и правильными стилями', (tester) async { testTester('показывает снекбар с информацией и правильными стилями', (
tester,
) async {
const infoMessage = 'Это просто сообщение'; const infoMessage = 'Это просто сообщение';
AppSnackBar.showInfo( AppSnackBar.showInfo(
@@ -54,7 +54,7 @@ void main() {
expect(find.byType(AppSnackBar), findsOneWidget); expect(find.byType(AppSnackBar), findsOneWidget);
expect(find.text(infoMessage), findsOneWidget); expect(find.text(infoMessage), findsOneWidget);
// Проверяем иконку и её цвет // Проверяем иконку и её цвет
final iconFinder = find.byType(Icon); final iconFinder = find.byType(Icon);
expect(iconFinder, findsOneWidget); expect(iconFinder, findsOneWidget);
@@ -74,7 +74,9 @@ void main() {
}); });
group('AppSnackBar.showError', () { group('AppSnackBar.showError', () {
testTester('показывает снекбар с ошибкой и правильными стилями', (tester) async { testTester('показывает снекбар с ошибкой и правильными стилями', (
tester,
) async {
const errorMessage = 'Произошла ошибка'; const errorMessage = 'Произошла ошибка';
AppSnackBar.showError( AppSnackBar.showError(
@@ -158,7 +160,9 @@ void main() {
}); });
group('AppSnackBar.showSuccess', () { group('AppSnackBar.showSuccess', () {
testTester('показывает снекбар с успехом и правильными стилями', (tester) async { testTester('показывает снекбар с успехом и правильными стилями', (
tester,
) async {
const successMessage = 'Операция выполнена успешно'; const successMessage = 'Операция выполнена успешно';
AppSnackBar.showSuccess( AppSnackBar.showSuccess(
@@ -186,7 +190,10 @@ void main() {
), ),
); );
final decoration = container.decoration as BoxDecoration; final decoration = container.decoration as BoxDecoration;
expect(decoration.color, equals(const Color(0xFF6FB62C))); // Success фон expect(
decoration.color,
equals(const Color(0xFF6FB62C)),
); // Success фон
}); });
testTester('показывает снекбар с кастомной продолжительностью', ( testTester('показывает снекбар с кастомной продолжительностью', (
@@ -210,29 +217,41 @@ void main() {
}); });
group('AppSnackBar виджет поведение', () { group('AppSnackBar виджет поведение', () {
testTester('показывает анимацию появления с правильной последовательностью', (tester) async { testTester(
const message = 'Тестовое сообщение'; 'показывает анимацию появления с правильной последовательностью',
(tester) async {
const message = 'Тестовое сообщение';
AppSnackBar.showError( AppSnackBar.showError(
tester.element(find.byType(Scaffold)), tester.element(find.byType(Scaffold)),
message: message, message: message,
); );
// Проверяем начальное состояние // Проверяем начальное состояние
await tester.pump(); await tester.pump();
final initialPosition = tester.widget<Positioned>(find.byType(Positioned)); final initialPosition = tester.widget<Positioned>(
expect(initialPosition.top ?? 0, lessThan(0)); find.byType(Positioned),
);
expect(initialPosition.top ?? 0, lessThan(0));
// Проверяем промежуточное состояние // Проверяем промежуточное состояние
await tester.pump(const Duration(milliseconds: 150)); await tester.pump(const Duration(milliseconds: 150));
final middlePosition = tester.widget<Positioned>(find.byType(Positioned)); final middlePosition = tester.widget<Positioned>(
expect(middlePosition.top ?? 0, greaterThan(initialPosition.top ?? 0)); find.byType(Positioned),
);
expect(
middlePosition.top ?? 0,
greaterThan(initialPosition.top ?? 0),
);
// Проверяем конечное состояние // Проверяем конечное состояние
await tester.pump(const Duration(milliseconds: 150)); await tester.pump(const Duration(milliseconds: 150));
final finalPosition = tester.widget<Positioned>(find.byType(Positioned)); final finalPosition = tester.widget<Positioned>(
expect(finalPosition.top ?? 0, greaterThan(0)); find.byType(Positioned),
}); );
expect(finalPosition.top ?? 0, greaterThan(0));
},
);
testTester('закрывается при тапе', (tester) async { testTester('закрывается при тапе', (tester) async {
const message = 'Тап для закрытия'; const message = 'Тап для закрытия';
@@ -323,7 +342,9 @@ void main() {
expect(find.byType(Text), findsAtLeastNWidgets(1)); expect(find.byType(Text), findsAtLeastNWidgets(1));
}); });
testTester('имеет правильные отступы и размеры на разных экранах', (tester) async { testTester('имеет правильные отступы и размеры на разных экранах', (
tester,
) async {
const message = 'Размеры'; const message = 'Размеры';
// Тестируем на маленьком экране // Тестируем на маленьком экране
@@ -360,7 +381,10 @@ void main() {
matching: find.byType(Container), matching: find.byType(Container),
), ),
); );
expect(container.constraints?.maxWidth, equals(350)); // Максимальная ширина expect(
container.constraints?.maxWidth,
equals(350),
); // Максимальная ширина
}); });
testTester('имеет правильное скругление углов', (tester) async { testTester('имеет правильное скругление углов', (tester) async {
@@ -465,7 +489,9 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
testTester('правильно обрабатывает быстрые последовательные вызовы', (tester) async { testTester('правильно обрабатывает быстрые последовательные вызовы', (
tester,
) async {
const messages = ['Сообщение 1', 'Сообщение 2', 'Сообщение 3']; const messages = ['Сообщение 1', 'Сообщение 2', 'Сообщение 3'];
for (final message in messages) { for (final message in messages) {

View File

@@ -1,3 +0,0 @@
format code
dart format --output=none --set-exit-if-changed .

10
tools/switch_services.sh Normal file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
### Скрипт для переключения сервисов для CI/CD
TYPE=$1
if [ -z "$TYPE" ]; then
echo "Error: TYPE is not set. Please provide a value."
exit 1
fi
yq -i '.dependencies.app_services.path = "app_services/'"$TYPE"'/app_services"' pubspec.yaml