diff --git a/.gitea/workflows/docker-run-tests.yml b/.gitea/workflows/docker-run-tests.yml index 85d14070..08fb5031 100644 --- a/.gitea/workflows/docker-run-tests.yml +++ b/.gitea/workflows/docker-run-tests.yml @@ -41,7 +41,7 @@ jobs: # - name: Build apk # run: flutter build apk - # some test + # some test 2 # - name: Rename and move apk # run: | diff --git a/.gitea/workflows/test-flight-cd.yaml b/.gitea/workflows/test-flight-cd.yaml index c9649f0e..caef60b3 100644 --- a/.gitea/workflows/test-flight-cd.yaml +++ b/.gitea/workflows/test-flight-cd.yaml @@ -1,4 +1,4 @@ -name: "Build Test Flight" +name: 'Build Test Flight' on: pull_request: branches: @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - + - name: Checkout env repository uses: actions/checkout@v2 with: @@ -39,8 +39,8 @@ jobs: run: | security unlock-keychain -p "$KEYCHAIN_PASSWORD" /Users/gitea_runner_ci/Library/Keychains/login.keychain-db env: - KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} - + KEYCHAIN_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} + - name: Setup API key run: | echo "${API_APPSTORE_KEY}" | tr -d '\n' | base64 --decode > ios/AuthKey.p8 @@ -66,9 +66,9 @@ jobs: env: LANG: en_US.UTF-8 LC_ALL: en_US.UTF-8 - MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} - FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }} + MATCH_PASSWORD: ${{ vars.IOS_RUNNER_CI_PASSWORD }} + FASTLANE_PASSWORD: ${{ vars.FASTLANE_PASSWORD }} MATCH_GIT_URL: http://gitea.office.local:3000/mso7nueh/fastlane FASTLANE_USER: eatvio2023@icloud.com - FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} + FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ vars.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} API_APPSTORE_KEY: ${{ vars.API_APPSTORE_KEY }} diff --git a/.kilocode/rules/memory-bank-instructions.md b/.kilocode/rules/memory-bank-instructions.md new file mode 100644 index 00000000..6482c0b3 --- /dev/null +++ b/.kilocode/rules/memory-bank-instructions.md @@ -0,0 +1,167 @@ +# Memory Bank + +I am an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder. + +When I start a task, I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization. + +## Memory Bank Structure + +The Memory Bank consists of core files and optional context files, all in Markdown format. + +### Core Files (Required) +1. `brief.md` + This file is created and maintained manually by the developer. Don't edit this file directly but suggest to user to update it if it can be improved. + - Foundation document that shapes all other files + - Created at project start if it doesn't exist + - Defines core requirements and goals + - Source of truth for project scope + +2. `product.md` + - Why this project exists + - Problems it solves + - How it should work + - User experience goals + +3. `context.md` + This file should be short and factual, not creative or speculative. + - Current work focus + - Recent changes + - Next steps + +4. `architecture.md` + - System architecture + - Source Code paths + - Key technical decisions + - Design patterns in use + - Component relationships + - Critical implementation paths + +5. `tech.md` + - Technologies used + - Development setup + - Technical constraints + - Dependencies + - Tool usage patterns + +### Additional Files +Create additional files/folders within memory-bank/ when they help organize: +- `tasks.md` - Documentation of repetitive tasks and their workflows +- Complex feature documentation +- Integration specifications +- API documentation +- Testing strategies +- Deployment procedures + +## Core workflows + +### Memory Bank Initialization + +The initialization step is CRITICALLY IMPORTANT and must be done with extreme thoroughness as it defines all future effectiveness of the Memory Bank. This is the foundation upon which all future interactions will be built. + +When user requests initialization of the memory bank (command `initialize memory bank`), I'll perform an exhaustive analysis of the project, including: +- All source code files and their relationships +- Configuration files and build system setup +- Project structure and organization patterns +- Documentation and comments +- Dependencies and external integrations +- Testing frameworks and patterns + +I must be extremely thorough during initialization, spending extra time and effort to build a comprehensive understanding of the project. A high-quality initialization will dramatically improve all future interactions, while a rushed or incomplete initialization will permanently limit my effectiveness. + +After initialization, I will ask the user to read through the memory bank files and verify product description, used technologies and other information. I should provide a summary of what I've understood about the project to help the user verify the accuracy of the memory bank files. I should encourage the user to correct any misunderstandings or add missing information, as this will significantly improve future interactions. + +### Memory Bank Update + +Memory Bank updates occur when: +1. Discovering new project patterns +2. After implementing significant changes +3. When user explicitly requests with the phrase **update memory bank** (MUST review ALL files) +4. When context needs clarification + +If I notice significant changes that should be preserved but the user hasn't explicitly requested an update, I should suggest: "Would you like me to update the memory bank to reflect these changes?" + +To execute Memory Bank update, I will: + +1. Review ALL project files +2. Document current state +3. Document Insights & Patterns +4. If requested with additional context (e.g., "update memory bank using information from @/Makefile"), focus special attention on that source + +Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on context.md as it tracks current state. + +### Add Task + +When user completes a repetitive task (like adding support for a new model version) and wants to document it for future reference, they can request: **add task** or **store this as a task**. + +This workflow is designed for repetitive tasks that follow similar patterns and require editing the same files. Examples include: +- Adding support for new AI model versions +- Implementing new API endpoints following established patterns +- Adding new features that follow existing architecture + +Tasks are stored in the file `tasks.md` in the memory bank folder. The file is optional and can be empty. The file can store many tasks. + +To execute Add Task workflow: + +1. Create or update `tasks.md` in the memory bank folder +2. Document the task with: + - Task name and description + - Files that need to be modified + - Step-by-step workflow followed + - Important considerations or gotchas + - Example of the completed implementation +3. Include any context that was discovered during task execution but wasn't previously documented + +Example task entry: +```markdown +## Add New Model Support +**Last performed:** [date] +**Files to modify:** +- `/providers/gemini.md` - Add model to documentation +- `/src/providers/gemini-config.ts` - Add model configuration +- `/src/constants/models.ts` - Add to model list +- `/tests/providers/gemini.test.ts` - Add test cases + +**Steps:** +1. Add model configuration with proper token limits +2. Update documentation with model capabilities +3. Add to constants file for UI display +4. Write tests for new model configuration + +**Important notes:** +- Check Google's documentation for exact token limits +- Ensure backward compatibility with existing configurations +- Test with actual API calls before committing +``` + +### Regular Task Execution + +In the beginning of EVERY task I MUST read ALL memory bank files - this is not optional. + +The memory bank files are located in `.kilocode/rules/memory-bank` folder. If the folder doesn't exist or is empty, I will warn user about potential issues with the memory bank. I will include `[Memory Bank: Active]` at the beginning of my response if I successfully read the memory bank files, or `[Memory Bank: Missing]` if the folder doesn't exist or is empty. If memory bank is missing, I will warn the user about potential issues and suggest initialization. I should briefly summarize my understanding of the project to confirm alignment with the user's expectations, like: + +"[Memory Bank: Active] I understand we're building a React inventory system with barcode scanning. Currently implementing the scanner component that needs to work with the backend API." + +When starting a task that matches a documented task in `tasks.md`, I should mention this and follow the documented workflow to ensure no steps are missed. + +If the task was repetitive and might be needed again, I should suggest: "Would you like me to add this task to the memory bank for future reference?" + +In the end of the task, when it seems to be completed, I will update `context.md` accordingly. If the change seems significant, I will suggest to the user: "Would you like me to update memory bank to reflect these changes?" I will not suggest updates for minor changes. + +## Context Window Management + +When the context window fills up during an extended session: +1. I should suggest updating the memory bank to preserve the current state +2. Recommend starting a fresh conversation/task +3. In the new conversation, I will automatically load the memory bank files to maintain continuity + +## Technical Implementation + +Memory Bank is built on Kilo Code's Custom Rules feature, with files stored as standard markdown documents that both the user and I can access. + +## Important Notes + +REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy. + +If I detect inconsistencies between memory bank files, I should prioritize brief.md and note any discrepancies to the user. + +IMPORTANT: I MUST read ALL memory bank files at the start of EVERY task - this is not optional. The memory bank files are located in `.kilocode/rules/memory-bank` folder. diff --git a/.kilocode/rules/memory-bank/architecture.md b/.kilocode/rules/memory-bank/architecture.md new file mode 100644 index 00000000..bfa07317 --- /dev/null +++ b/.kilocode/rules/memory-bank/architecture.md @@ -0,0 +1,374 @@ +# System Architecture + +## Project Structure + +### Core Directory (`lib/core/`) +Central infrastructure and shared components: +- **Dependency Injection**: GetIt-based service locator in `locator_service.dart` +- **Routing**: GoRouter implementation in `app_router.dart` with deep linking +- **Theme**: Material theme with Mulish font family in `theme/theme_helper.dart` +- **Networking**: Dio HTTP client with token interceptor +- **WebSockets**: Pusher Channels for real-time communication +- **State Management**: Flutter BLoC/Cubit pattern + +### Features Directory (`lib/features/`) +Feature-based modular architecture with 35+ feature modules: + +**Core Features:** +- `auth/` - Authentication (email/password, OAuth) +- `feed/` - Content discovery feed +- `diary/` - Food diary tracking +- `chat/` - Real-time messaging +- `consultations/` - Consultation management +- `profile/` - User profiles and settings + +**Content Creation:** +- `create_recipe/` - Recipe creation +- `create_diet/` - Diet plan creation +- `create_plan/` - Nutrition plan creation +- `blog_post_create/` - Blog post creation + +**User Management:** +- `settings/` - User preferences +- `user_details/` - User information management +- `menu/` - App navigation menu +- `bottom_navigation_bar/` - Main navigation + +**Consultant Features:** +- `consultant/` - Consultant verification +- `consultant_rating/` - Rating system +- `finances_and_consultations_archive/` - Financial tracking + +**Supporting Features:** +- `notifications/` - Push notifications +- `glossary/` - Nutrition glossary +- `calendar/` - Calendar integration +- `support/` - Customer support +- `welcome/` - Onboarding +- `update_screen/` - App update management + +## Architectural Patterns + +### Clean Architecture Layers + +Each feature follows a layered structure: + +``` +feature/ +├── models/ # Data models (freezed + json_serializable) +├── services/ # API services (Retrofit) +├── repositories/ # Data repositories +├── presentation/ # UI layer +│ ├── pages/ # Screen widgets +│ ├── widgets/ # Feature-specific widgets +│ └── bloc/ # State management (Cubit) +└── entities/ # Domain entities +``` + +### Dependency Injection + +**Registration Structure** (`lib/core/di/`): +- `app_config_registration.dart` - App configuration +- `external_registrations.dart` - External dependencies (Dio, etc.) +- `service_registrations.dart` - API services +- `repository_registrations.dart` - Data repositories +- `cubit_registrations.dart` - State management + +**Service Lifecycle:** +- Singleton: Services, Repositories (most common) +- Factory: Cubits that need fresh instances +- Lazy Singleton: WebSocket manager, core services + +### State Management Pattern + +**BLoC/Cubit Implementation:** +- Global state: Registered in `AppConfig.globalBlocProviders` +- Feature state: Created per screen/feature +- State classes: Freezed unions for type-safe states + +**Global Cubits:** +- `NewMessagesCubit` - Message notifications +- `NotificationsCubit` - Push notifications +- `FeedCubit` - Feed content +- `ProfileAvatarCubit` - User profile +- `ChatsCubit` - Chat rooms +- `ViewsCubit` - Content view tracking +- `InternetConnectionCubit` - Network status + +## Data Flow + +### API Integration + +**Base Setup:** +- Base URL: Configurable via environment (prod/dev) +- HTTP Client: Dio with interceptors +- API Services: Retrofit for type-safe endpoints +- Authentication: Bearer token in headers + +**Interceptors:** +1. `TokenInterceptor` - Adds auth token, handles 401 errors +2. `PhoneVerifyErrorInterceptor` - Handles phone verification errors + +### Real-time Communication + +**WebSocket Architecture:** +- Library: `dart_pusher_channels` +- Manager: `WsManager` (singleton) +- Connection: Authenticated via backend endpoint + +**Channels:** +1. Private Notifications: `App\Events\BroadcastNotificationCreated` +2. Private Messages: `App\Events\ChatMessageUpdated` +3. Direct Chat: Room-specific message updates +4. Client Typing: Real-time typing indicators + +**Socket ID Storage:** +- Persisted via SharedPreferences +- Used for authentication and channel subscription + +### Authentication Flow + +**Methods:** +1. Email/Password - Traditional credentials +2. OAuth Providers: + - VK ID (Russian social network) + - Yandex ID + - OK (Odnoklassniki) + - Mail.ru + +**Token Management:** +- Storage: SharedPreferences via `TokenStorage` +- Refresh: Automatic on 401 response +- Logout: Token removal + WebSocket cleanup + +**Post-Authentication:** +1. WebSocket initialization +2. Profile loading +3. Chat room loading +4. Notification loading +5. Firebase push token registration + +## Navigation Architecture + +### GoRouter Setup + +**Route Structure:** +- Shell routes for persistent bottom navigation +- Named routes for easy navigation +- Deep linking support for content (recipe/blog_post/plan/diet) + +**Route Groups:** +- `AuthRoutes` - Login, registration, password reset +- `MainNavigationRoutes` - Shell route with bottom nav +- `ContentRoutes` - Content viewing screens +- `SettingsRoutes` - User settings +- `OtherRoutes` - Miscellaneous screens + +**Deep Linking Pattern:** +``` +/content/{type}/{identifier} +- /content/recipe/{slug} +- /content/blog_post/{id} +- /content/plan/{slug} +- /content/diet/{slug} +``` + +### Bottom Navigation + +**Main Sections:** +1. My Profile - User profile and content +2. Feed - Content discovery +3. Diary - Food tracking +4. Consultations - Consultation management +5. Chat - Messaging +6. Notifications - Notification center +7. Menu - Settings and more + +**Implementation:** +- `BottomNavigationBarWidget` wraps content +- No transition animations for tab switches +- WebSocket initialization on feed screen + +## Data Persistence + +### Storage Solutions + +**SharedPreferences:** +- Authentication token +- Socket ID +- User preferences + +**Cached Network Images:** +- Library: `cached_network_image` +- Automatic caching of user avatars, content images + +**Firebase:** +- Push notification tokens +- Remote configuration +- Analytics events + +## Critical Implementation Details + +### Content View Tracking + +**Pattern:** +- `ViewsCubit` handles visibility detection +- `VisibilityDetector` widget tracks on-screen time +- Views sent to backend when 50%+ visible for 2+ seconds +- Debounced to prevent excessive API calls + +**Tracked Content:** +- Blog posts +- Recipes +- Diets +- Nutrition plans + +### Image Handling + +**Upload Flow:** +1. `ImagePicker` for selection +2. `FlutterImageCompress` for compression +3. MultiPart form data upload via Dio +4. Preview via `CachedNetworkImage` + +**Supported Formats:** +- User photos (profile, diary) +- Content images (recipes, diets, plans) +- Chat attachments + +### Search Implementation + +**Debounced Search:** +- Library: `easy_debounce` +- Delay: 500ms +- Min query length: 3 characters +- Applied to: Feed, Recipes, Diets, Plans, Consultants + +**Tag-based Filtering:** +- Recipe tags for filtering +- Modal bottom sheet UI +- Persistent selection across sessions + +### Pagination Pattern + +**Implementation:** +- Page-based pagination +- Scroll detection for next page load +- Loading state during fetch +- Meta data tracking (current page, last page) + +**Example (FeedCubit):** +```dart +currentPage = 1 +lastPage = meta.lastPage +loadNextPage() when currentPage < lastPage +``` + +## Error Handling + +### Network Errors + +**401 Unauthorized:** +- Auto logout +- Token removal +- Navigate to auth screen + +**Connection Errors:** +- `InternetConnectionCubit` monitors status +- Retry mechanisms in repositories +- User-friendly error messages + +### State Error Handling + +**Pattern:** +```dart +sealed class State { + const State.loading(); + const State.loaded({required data}); + const State.error({required String errorMessage}); +} +``` + +**UI Response:** +- Loading: Show loaders/skeletons +- Error: Show `DataErrorWidget` with retry +- Empty: Show empty state messages + +## Performance Optimizations + +### Lazy Loading +- Lazy singleton services +- On-demand cubit creation +- Deferred widget loading + +### Image Optimization +- Compression before upload +- Cached network images +- Thumbnail generation + +### WebSocket Management +- Connection reuse +- Automatic reconnection +- Proper cleanup on logout + +### List Rendering +- Pagination for large lists +- ListView.builder for efficiency +- Visibility detection for tracking + +## Platform-Specific Features + +### Android +- Huawei HMS support for Huawei devices +- Google FCM for push notifications +- ProGuard rules for release builds +- QR code scanning via camera + +### iOS +- APNs for push notifications +- Deep linking configuration +- App Store compliance + +### Firebase Integration +- Separate configs for Android/iOS +- Environment-based configuration +- Push notification handling +- Analytics tracking + +## Security Considerations + +### Authentication +- Bearer token authentication +- Secure token storage +- Automatic session management +- OAuth 2.0 for social login + +### Data Protection +- HTTPS only communication +- Token encryption in storage +- Secure WebSocket connections +- Privacy policy compliance + +## Key Files Reference + +**Entry Point:** +- `lib/main.dart` - App initialization + +**Core Infrastructure:** +- `lib/core/app_initializer.dart` - Startup sequence +- `lib/core/locator_service.dart` - DI container +- `lib/core/app_config.dart` - App configuration +- `lib/core/app_router.dart` - Routing setup + +**WebSocket:** +- `lib/core/websockets/ws_manager.dart` - Connection management +- `lib/core/websockets/ws_service.dart` - API service + +**Authentication:** +- `lib/features/auth/sdk/social_auth_facade.dart` - Social auth +- `lib/core/token_storage.dart` - Token persistence +- `lib/core/interceptors/token_interceptor.dart` - Auth interceptor + +**Environment:** +- `lib/core/env_config.dart` - Environment variables +- `.env` - Environment file (not in repo) \ No newline at end of file diff --git a/.kilocode/rules/memory-bank/brief.md b/.kilocode/rules/memory-bank/brief.md new file mode 100644 index 00000000..d8990cea --- /dev/null +++ b/.kilocode/rules/memory-bank/brief.md @@ -0,0 +1,35 @@ +# Project Brief + +## Project Name +Eatvio - Nutrition & Diet Management Application + +## Project Description +A comprehensive mobile application connecting users with nutrition consultants, providing diet planning tools, recipes, food diary tracking, and professional consultation services. + +## Core Features +- **Blog Posts**: Educational nutrition content and articles +- **Nutrition Consultation**: Professional consultation services with certified nutritionists +- **Recipes**: User-generated and professional recipes with nutritional information +- **Diets**: Structured diet plans with daily schedules +- **Nutrition Plans**: Long-term nutrition planning and tracking + +## Target Audience +- Primary: Russian-speaking users interested in nutrition and healthy eating +- Secondary: Certified nutrition consultants offering professional services +- Market: Russian Federation and Russian-speaking countries + +## Platform +- Mobile: iOS and Android (including Huawei devices) +- Language: Russian (ru) +- Current Version: 1.3.0+79 + +## Business Model +- Paid consultations with nutritionists +- Content marketplace for premium diets and plans +- Subscription-based features for consultants + +## Key Success Metrics +- User engagement with food diary +- Consultation bookings and completion rates +- Content creation by consultants +- User retention and progress tracking diff --git a/.kilocode/rules/memory-bank/context.md b/.kilocode/rules/memory-bank/context.md new file mode 100644 index 00000000..0bde3ccf --- /dev/null +++ b/.kilocode/rules/memory-bank/context.md @@ -0,0 +1,29 @@ +# Current Context + +## Project Status +Memory Bank initialized on 2025-10-16. This is a mature Flutter application (v1.3.0+79) currently in production. + +## Recent Activity +- Memory Bank initialization completed +- All core files documented +- Architecture and technology stack catalogued + +## Current Focus +Ready for development tasks. No active work in progress. + +## Known Issues +None documented at initialization. + +## Next Steps +Awaiting user direction for: +- Feature development +- Bug fixes +- Architecture improvements +- Performance optimizations + +## Important Notes +- Primary language: Russian (ru) +- Target market: Russian users +- Multi-platform: Android (including Huawei) and iOS +- Environment: Uses `.env` file for configuration (not in repository) +- Build system: Requires code generation for models/services \ No newline at end of file diff --git a/.kilocode/rules/memory-bank/product.md b/.kilocode/rules/memory-bank/product.md new file mode 100644 index 00000000..fc732d24 --- /dev/null +++ b/.kilocode/rules/memory-bank/product.md @@ -0,0 +1,73 @@ +# Product Overview + +## Purpose +Eatvio is a comprehensive nutrition and diet management mobile application that connects users with nutrition consultants, provides diet planning tools, recipes, and personal food diary tracking. + +## Core Problems It Solves + +### For Regular Users: +1. **Diet Management**: Track daily food intake with detailed nutritional information +2. **Meal Planning**: Access to recipes and structured diet plans +3. **Professional Guidance**: Connect with certified nutrition consultants +4. **Progress Tracking**: Monitor weight, body measurements, and dietary habits +5. **Knowledge Access**: Learn about nutrition through blog posts and glossary + +### For Nutrition Consultants: +1. **Client Management**: Communicate with clients via integrated chat +2. **Content Creation**: Create and publish diets, meal plans, recipes, and blog posts +3. **Consultation Services**: Offer paid nutrition consultation services +4. **Income Tracking**: Monitor earnings and client purchases +5. **Professional Presence**: Build profile and gain subscribers + +## How It Works + +### User Journey: +1. **Authentication**: Sign up via email/password or social networks (VK, Yandex, OK, Mail.ru) +2. **Feed Exploration**: Browse recipes, diets, nutrition plans, blog posts, and consultants +3. **Content Interaction**: Like, bookmark, comment on content +4. **Diary Tracking**: Log daily meals with photos and nutritional data +5. **Consultations**: Book and pay for nutrition consultant services +6. **Real-time Chat**: Communicate with consultants through integrated messaging +7. **Progress Monitoring**: Track weight and body measurements over time + +### Consultant Journey: +1. **Become Consultant**: Apply and get verified as nutrition consultant +2. **Content Creation**: Publish professional diets, meal plans, recipes +3. **Client Interaction**: Respond to consultations and provide guidance +4. **Diary Review**: Review client food diaries and provide feedback +5. **Earnings Management**: Track income from consultations and content + +## User Experience Goals + +### Seamless Navigation: +- Bottom navigation bar for main sections (Profile, Feed, Diary, Consultations, Chat) +- Tab-based feed filtering (Blog posts, Recipes, Diets, Plans, Consultants) +- Deep linking support for direct content access + +### Real-time Engagement: +- WebSocket-powered chat for instant messaging +- Push notifications for messages and updates +- Live content updates without app restart + +### Professional Interface: +- Clean, modern UI with Mulish font family +- Intuitive search and filtering across all content types +- Visual progress tracking with charts and gauges + +### Content Discovery: +- Tag-based recipe filtering +- Search functionality across all content types +- Personalized feed based on user interests + +### Offline Capability: +- Token persistence for seamless re-authentication +- Cached content for better performance +- Queue system for offline actions + +## Key Differentiators + +1. **Integrated Platform**: Combines diet tracking, content, and professional consultations +2. **Social Features**: Follow consultants, like/bookmark content, engage with community +3. **Professional Tools**: Full toolkit for consultants to manage practice +4. **Russian Market Focus**: Optimized for Russian users with local payment and auth integrations +5. **Multi-platform Support**: Works on Android (including Huawei) and iOS \ No newline at end of file diff --git a/.kilocode/rules/memory-bank/tech.md b/.kilocode/rules/memory-bank/tech.md new file mode 100644 index 00000000..9a2d0fa5 --- /dev/null +++ b/.kilocode/rules/memory-bank/tech.md @@ -0,0 +1,152 @@ +# Technology Stack + +## Core Framework +- **Flutter**: SDK 3.8.0+ +- **Dart**: Language version >=3.8.0 <4.0.0 +- **Version**: 1.3.0+79 (build 79) + +## State Management +- **flutter_bloc**: ^9.1.1 - BLoC pattern implementation +- **equatable**: ^2.0.7 - Value equality for state objects + +## Data Management +- **freezed**: ^3.2.0 - Immutable data classes +- **freezed_annotation**: ^3.1.0 - Code generation annotations +- **json_annotation**: ^4.9.0 - JSON serialization +- **json_serializable**: ^6.10.0 - JSON code generation + +## Networking +- **dio**: ^5.9.0 - HTTP client +- **retrofit**: ^4.7.1 - Type-safe REST client +- **retrofit_generator**: ^10.0.2 - Retrofit code generation +- **http**: ^1.5.0 - Additional HTTP support +- **http_parser**: ^4.1.2 - HTTP parsing utilities + +## Real-time Communication +- **dart_pusher_channels**: ^1.2.3 - WebSocket support via Pusher +- **connectivity_plus**: ^6.1.5 - Network connectivity monitoring + +## Navigation & Routing +- **go_router**: ^16.1.0 - Declarative routing +- Deep linking support for content + +## UI Components +- **cached_network_image**: ^3.4.1 - Image caching +- **flutter_svg**: ^2.2.0 - SVG rendering +- **google_fonts**: ^6.3.0 - Mulish font family +- **carousel_slider_plus**: ^7.1.1 - Carousel widgets +- **photo_view**: ^0.15.0 - Image viewer +- **dismissible_page**: ^1.0.2 - Swipe to dismiss +- **badges**: ^3.1.2 - Notification badges +- **lottie**: ^3.3.1 - Animation support +- **flutter_portal**: ^1.1.4 - Overlay widgets +- **dotted_border**: ^3.1.0 - Dotted borders +- **flutter_rating_bar**: ^4.0.1 - Rating widgets + +## Forms & Input +- **flutter_form_builder**: ^10.2.0 - Form management +- **validators**: ^3.0.0 - Input validation +- **flutter_typeahead**: ^5.2.0 - Autocomplete +- **keyboard_dismisser**: ^3.0.0 - Keyboard management +- **flutter_keyboard_visibility**: ^6.0.0 - Keyboard state +- **mask_text_input_formatter**: ^2.9.0 - Input masking + +## Media & Files +- **image_picker**: ^1.2.0 - Image selection +- **flutter_image_compress**: ^2.4.0 - Image compression +- **image**: ^4.5.4 - Image manipulation +- **file_picker**: ^10.3.1 - File selection +- **audioplayers**: ^6.5.0 - Audio playback +- **qr_code_scanner_plus**: ^2.0.10+1 - QR code scanning + +## Data Visualization +- **syncfusion_flutter_charts**: ^30.2.5 - Chart widgets +- **syncfusion_flutter_gauges**: ^30.2.5 - Gauge widgets +- **table_calendar**: ^3.2.0 - Calendar widget + +## Storage & Persistence +- **shared_preferences**: ^2.5.3 - Local key-value storage +- **path_provider**: ^2.1.5 - File system paths +- **path**: ^1.9.1 - Path manipulation + +## Firebase +- **firebase_core**: ^4.0.0 - Firebase initialization +- **firebase_messaging**: ^16.0.0 - Push notifications + +## Analytics & Monitoring +- **sentry_flutter**: ^9.6.0 - Error tracking +- **appmetrica_plugin**: ^3.3.0 - Analytics (Russian market) + +## Authentication +- **vk_id**: ^1.0.3 - VK social login +- **flutter_login_yandex**: ^0.0.5 - Yandex ID login +- OAuth support for OK and Mail.ru + +## Utilities +- **url_launcher**: ^6.3.2 - URL handling +- **easy_debounce**: ^2.0.3 - Debouncing +- **share_plus**: ^11.1.0 - Content sharing +- **flutter_html**: ^3.0.0 - HTML rendering +- **webview_flutter**: ^4.13.0 - WebView +- **intl**: ^0.20.2 - Internationalization +- **collection**: ^1.19.1 - Collection utilities +- **uuid**: ^4.5.1 - UUID generation +- **visibility_detector**: ^0.4.0+2 - Visibility tracking +- **fluttertoast**: ^8.2.12 - Toast messages +- **version**: ^3.0.2 - Version management +- **package_info_plus**: ^8.3.1 - Package info +- **device_info_plus**: ^11.5.0 - Device information +- **permission_handler**: ^12.0.1 - Permissions + +## Platform-Specific + +### Android +- Huawei HMS Push (6.12.0.300) +- Huawei Analytics (6.12.0.300) +- Google Play Services +- ProGuard for code obfuscation +- NDK version 27.0.12077973 +- Compile SDK: Flutter default +- Min SDK: Flutter default +- Target SDK: Flutter default + +### iOS +- APNs for push notifications +- Deep linking support + +## Development Tools +- **build_runner**: ^2.7.0 - Code generation +- **flutter_native_splash**: ^2.4.6 - Splash screen +- **flutter_lints**: ^6.0.0 - Linting +- **mockito**: ^5.5.0 - Mocking for tests +- **flutter_test**: SDK - Testing framework +- **integration_test**: SDK - Integration testing +- **flutter_dotenv**: ^5.2.1 - Environment variables + +## Localization +- **flutter_localizations**: SDK - Localization +- **flutter_intl**: Enabled - Internationalization +- Primary language: Russian (ru) + +## Build Configuration +- **Environment**: Production/Development/Test modes +- **Build types**: Debug, Release, Profile +- **Code generation**: Freezed, JSON, Retrofit +- **Obfuscation**: ProGuard (Android release) +- **Resource optimization**: Shrink resources enabled + +## API Integration Patterns +- RESTful API via Retrofit +- Bearer token authentication +- Interceptor-based auth injection +- Automatic 401 handling +- Environment-based base URLs +- Type-safe API clients + +## Development Workflow +1. Code generation: `flutter pub run build_runner build` +2. Clean generation: `flutter pub run build_runner build --delete-conflicting-outputs` +3. Watch mode: `flutter pub run build_runner watch` +4. Environment setup: `.env` file configuration +5. Firebase setup: Separate configs per platform +6. Testing: Unit tests + Integration tests \ No newline at end of file diff --git a/ENV_SETUP.md b/ENV_SETUP.md index 0873fb1c..092a3bd9 100644 --- a/ENV_SETUP.md +++ b/ENV_SETUP.md @@ -54,6 +54,15 @@ cp env.example .env - `GOOGLE_CHECK_URL` - URL для проверки соединения (Google) - `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex) +### VK ID Configuration +- `VK_CLIENT_ID` - ID приложения VK (числовой) +- **Примечание**: VK_CLIENT_ID - это числовой "ID приложения" из консоли VK ID, НЕ "Защищённый ключ" или "Сервисный ключ доступа" + +### Yandex ID Configuration +- `YANDEX_CLIENT_ID` - ID приложения Yandex OAuth +- `YANDEX_CLIENT_SECRET` - секретный ключ приложения Yandex OAuth +- `YANDEX_REDIRECT_URI` - URI для перенаправления (должен совпадать с настройками в Yandex OAuth приложении) + ## Безопасность ⚠️ **ВАЖНО**: Файл `.env` добавлен в `.gitignore` и не должен попадать в репозиторий. Все секретные данные должны храниться только в этом файле. diff --git a/README.md b/README.md index d7368d78..3dcc0829 100644 --- a/README.md +++ b/README.md @@ -1 +1,449 @@ -# eatvio_app \ No newline at end of file +# Eatvio - Nutrition & Diet Management Application + +## Project Overview + +Eatvio is a comprehensive mobile application connecting users with nutrition consultants, providing diet planning tools, recipes, food diary tracking, and professional consultation services. The application is specifically designed for Russian-speaking users interested in nutrition and healthy eating. + +### Core Features +- **Blog Posts**: Educational nutrition content and articles +- **Nutrition Consultation**: Professional consultation services with certified nutritionists +- **Recipes**: User-generated and professional recipes with nutritional information +- **Diets**: Structured diet plans with daily schedules +- **Nutrition Plans**: Long-term nutrition planning and tracking + +### Target Audience +- **Primary**: Russian-speaking users interested in nutrition and healthy eating +- **Secondary**: Certified nutrition consultants offering professional services +- **Market**: Russian Federation and Russian-speaking countries + +### Business Model +- Paid consultations with nutritionists +- Content marketplace for premium diets and plans +- Subscription-based features for consultants + +### Platform Information +- **Mobile**: iOS and Android (including Huawei devices) +- **Language**: Russian (ru) +- **Current Version**: 1.3.0+79 (build 79) + +## Prerequisites + +### Flutter & Dart Versions +- **Flutter**: SDK 3.8.0+ +- **Dart**: Language version >=3.8.0 <4.0.0 + +### Platform-Specific Requirements + +#### Android +- **NDK Version**: 27.0.12077973 +- **Huawei HMS Dependencies**: + - Huawei Push (6.12.0.300) + - Huawei Analytics (6.12.0.300) +- **Google Play Services** +- **ProGuard** for code obfuscation +- **Compile SDK**: Flutter default +- **Min SDK**: Flutter default +- **Target SDK**: Flutter default + +#### iOS +- **APNs** for push notifications +- **Deep linking** configuration +- **Xcode** version compatible with Flutter requirements + +#### General IDE Requirements +- **VS Code** with Flutter/Dart extensions +- **Android Studio** with Flutter plugin +- **Visual Studio Code** recommended for development + +## Environment Configuration + +### .env Setup Process + +1. Copy the environment template: + ```bash + cp env.example .env + ``` + +2. Edit the `.env` file with your configuration values + +### Required Environment Variables + +#### API URLs +- `BASE_URL` - основной URL API (по умолчанию: https://eatvio.ru) +- `DEV_BASE_URL` - URL API для разработки (по умолчанию: https://dev2.eatvio.com) + +#### Sentry Configuration +- `SENTRY_DSN` - DSN для Sentry мониторинга + +#### Firebase Configuration +- `FIREBASE_PROJECT_ID` - ID проекта Firebase +- `FIREBASE_DATABASE_URL` - URL базы данных Firebase +- `FIREBASE_STORAGE_BUCKET` - bucket для хранения файлов +- `FIREBASE_MESSAGING_SENDER_ID` - ID отправителя сообщений + +#### Firebase Android Configuration +- `FIREBASE_ANDROID_API_KEY` - API ключ для Android +- `FIREBASE_ANDROID_APP_ID` - ID приложения для Android + +#### Firebase iOS Configuration +- `FIREBASE_IOS_API_KEY` - API ключ для iOS +- `FIREBASE_IOS_APP_ID` - ID приложения для iOS +- `FIREBASE_IOS_BUNDLE_ID` - Bundle ID для iOS + +#### App Store URLs +- `APP_STORE_URL` - URL App Store +- `RUSTORE_URL` - URL RuStore + +#### Support URLs +- `TELEGRAM_SUPPORT_URL` - URL поддержки в Telegram +- `SUPPORT_EMAIL` - email для поддержки + +#### Test Credentials (только для разработки) +- `TEST_EMAIL` - тестовый email +- `TEST_PASSWORD` - тестовый пароль + +#### Email Validation API +- `EMAIL_VALIDATION_API_URL` - URL API для валидации email + +#### Connection Check URLs +- `GOOGLE_CHECK_URL` - URL для проверки соединения (Google) +- `YANDEX_CHECK_URL` - URL для проверки соединения (Yandex) + +#### VK ID Configuration +- `VK_CLIENT_ID` - ID приложения VK (числовой) +- **Примечание**: VK_CLIENT_ID - это числовой "ID приложения" из консоли VK ID, НЕ "Защищённый ключ" или "Сервисный ключ доступа" + +#### Yandex ID Configuration +- `YANDEX_CLIENT_ID` - ID приложения Yandex OAuth +- `YANDEX_CLIENT_SECRET` - секретный ключ приложения Yandex OAuth +- `YANDEX_REDIRECT_URI` - URI для перенаправления (должен совпадать с настройками в Yandex OAuth приложении) + +## Technology Stack + +### Core Framework +- **Flutter**: SDK 3.8.0+ +- **Dart**: Language version >=3.8.0 <4.0.0 +- **Version**: 1.3.0+79 (build 79) + +### State Management +- **flutter_bloc**: ^9.1.1 - BLoC pattern implementation +- **equatable**: ^2.0.7 - Value equality for state objects + +### Data Management +- **freezed**: ^3.2.0 - Immutable data classes +- **freezed_annotation**: ^3.1.0 - Code generation annotations +- **json_annotation**: ^4.9.0 - JSON serialization +- **json_serializable**: ^6.10.0 - JSON code generation + +### Networking +- **dio**: ^5.9.0 - HTTP client +- **retrofit**: ^4.7.1 - Type-safe REST client +- **retrofit_generator**: ^10.0.2 - Retrofit code generation +- **http**: ^1.5.0 - Additional HTTP support +- **http_parser**: ^4.1.2 - HTTP parsing utilities + +### Real-time Communication +- **dart_pusher_channels**: ^1.2.3 - WebSocket support via Pusher +- **connectivity_plus**: ^6.1.5 - Network connectivity monitoring + +### Navigation & Routing +- **go_router**: ^16.1.0 - Declarative routing +- Deep linking support for content + +### UI Components +- **cached_network_image**: ^3.4.1 - Image caching +- **flutter_svg**: ^2.2.0 - SVG rendering +- **google_fonts**: ^6.3.0 - Mulish font family +- **carousel_slider_plus**: ^7.1.1 - Carousel widgets +- **photo_view**: ^0.15.0 - Image viewer +- **dismissible_page**: ^1.0.2 - Swipe to dismiss +- **badges**: ^3.1.2 - Notification badges +- **lottie**: ^3.3.1 - Animation support +- **flutter_portal**: ^1.1.4 - Overlay widgets +- **dotted_border**: ^3.1.0 - Dotted borders +- **flutter_rating_bar**: ^4.0.1 - Rating widgets + +### Forms & Input +- **flutter_form_builder**: ^10.2.0 - Form management +- **validators**: ^3.0.0 - Input validation +- **flutter_typeahead**: ^5.2.0 - Autocomplete +- **keyboard_dismisser**: ^3.0.0 - Keyboard management +- **flutter_keyboard_visibility**: ^6.0.0 - Keyboard state +- **mask_text_input_formatter**: ^2.9.0 - Input masking + +### Media & Files +- **image_picker**: ^1.2.0 - Image selection +- **flutter_image_compress**: ^2.4.0 - Image compression +- **image**: ^4.5.4 - Image manipulation +- **file_picker**: ^10.3.1 - File selection +- **audioplayers**: ^6.5.0 - Audio playback +- **qr_code_scanner_plus**: ^2.0.10+1 - QR code scanning + +### Data Visualization +- **syncfusion_flutter_charts**: ^30.2.5 - Chart widgets +- **syncfusion_flutter_gauges**: ^30.2.5 - Gauge widgets +- **table_calendar**: ^3.2.0 - Calendar widget + +### Storage & Persistence +- **shared_preferences**: ^2.5.3 - Local key-value storage +- **path_provider**: ^2.1.5 - File system paths +- **path**: ^1.9.1 - Path manipulation + +### Firebase +- **firebase_core**: ^4.0.0 - Firebase initialization +- **firebase_messaging**: ^16.0.0 - Push notifications + +### Analytics & Monitoring +- **sentry_flutter**: ^9.6.0 - Error tracking +- **appmetrica_plugin**: ^3.3.0 - Analytics (Russian market) + +### Authentication +- **vk_id**: ^1.0.3 - VK social login +- **flutter_login_yandex**: ^0.0.5 - Yandex ID login +- OAuth support for OK and Mail.ru + +### Utilities +- **url_launcher**: ^6.3.2 - URL handling +- **easy_debounce**: ^2.0.3 - Debouncing +- **share_plus**: ^11.1.0 - Content sharing +- **flutter_html**: ^3.0.0 - HTML rendering +- **webview_flutter**: ^4.13.0 - WebView +- **intl**: ^0.20.2 - Internationalization +- **collection**: ^1.19.1 - Collection utilities +- **uuid**: ^4.5.1 - UUID generation +- **visibility_detector**: ^0.4.0+2 - Visibility tracking +- **fluttertoast**: ^8.2.12 - Toast messages +- **version**: ^3.0.2 - Version management +- **package_info_plus**: ^8.3.1 - Package info +- **device_info_plus**: ^11.5.0 - Device information +- **permission_handler**: ^12.0.1 - Permissions + +## Development Workflow + +### Code Generation +- Run `flutter pub run build_runner build` for code generation +- Run `flutter pub run build_runner build --delete-conflicting-outputs` for clean generation +- Use `flutter pub run build_runner watch` for watch mode + +### Environment Setup +- Configure `.env` file with all required variables +- Set up Firebase for both Android and iOS platforms +- Configure social authentication providers (VK, Yandex, etc.) + +### Testing +- Unit tests with `flutter_test` +- Integration tests with `integration_test` +- Mocking with `mockito` + +## Build Commands + +### Development Builds +```bash +# Run on connected device +flutter run + +# Run on specific Android device +flutter run -d emulator-5554 + +# Run on specific iOS simulator +flutter run -d iPhone\ 15 +``` + +### Production Builds + +#### Android +```bash +# Build APK (split by ABI) +flutter build apk --release --split-per-abi + +# Build App Bundle +flutter build appbundle --release + +# Huawei-specific build (with HMS) +flutter build apk --release --flavor production --target-platform android-arm64 +``` + +#### iOS +```bash +# Build IPA for TestFlight +flutter build ipa --export-options-plist=ExportOptions.plist + +# Build for specific device architecture +flutter build ipa --export-options-plist=ExportOptions.plist --no-codesign +``` + +## Optimization Techniques + +### Android Build Optimization +1. **ProGuard Rules**: + - Configure rules in [`android/app/proguard-rules.pro`](android/app/proguard-rules.pro:1) + - Essential rules for Flutter: + ``` + -keep class io.flutter.app.** { *; } + -keep class io.flutter.plugin.** { *; } + -keep class io.flutter.util.** { *; } + -keep class io.flutter.view.** { *; } + ``` + +2. **Resource Shrinking**: + - Enabled in `android/app/build.gradle`: + ```gradle + buildTypes { + release { + shrinkResources true + minifyEnabled true + } + } + ``` + +3. **Code Obfuscation**: + - Use `--obfuscate` flag with build commands + - Verify with `flutter build apk --release --obfuscate --split-debug-info=debug-info/` + +4. **Image Compression Workflow**: + - Automatic compression via [`flutter_image_compress`](lib/features/create_diet/models/create_diet_model.dart:1) + - Configuration in image upload flows: + ```dart + final compressedFile = await FlutterImageCompress.compressWithFile( + file.path, + quality: 85, + minWidth: 1024, + minHeight: 1024, + ); + ``` + +## Troubleshooting + +### Common Issues + +#### Missing .env Variables +- **Symptom**: Application crashes on startup with "Missing environment variable" error +- **Solution**: + 1. Verify `.env` file exists in project root + 2. Check [`ENV_SETUP.md`](ENV_SETUP.md) for required variables + 3. Regenerate configuration with `flutter clean && flutter pub get` + +#### Firebase Configuration Errors +- **Android**: + - Missing `google-services.json` in `android/app/` + - Missing `agconnect-services.json` for Huawei devices + - Verify SHA-1 fingerprints match Firebase console +- **iOS**: + - Missing `GoogleService-Info.plist` in `ios/Runner` + - Check bundle identifier matches Firebase project + +#### WebSocket Connection Failures +- **Symptoms**: Chat messages not delivering, real-time updates not working +- **Solutions**: + 1. Verify Pusher configuration in [`lib/core/websockets/ws_manager.dart`](lib/core/websockets/ws_manager.dart:1) + 2. Check network connectivity with `connectivity_plus` package + 3. Ensure proper authentication token is sent with WebSocket connection + +#### Platform-Specific Build Errors +- **Huawei vs Google Services**: + - For Huawei devices, ensure `agconnect-services.json` is present + - Use `--flavor` flag to specify build variant: + ```bash + flutter build apk --release --flavor hms + ``` + - Check for conflicting dependencies between HMS and GMS + +- **iOS Code Signing Issues**: + - Verify `ExportOptions.plist` contains correct provisioning profiles + - Update signing certificates in Xcode + - Clean build folder: `flutter clean && rm -rf ios/Pods ios/Podfile.lock` + +## Key Implementation Details + +### Content View Tracking +- `ViewsCubit` handles visibility detection +- `VisibilityDetector` widget tracks on-screen time +- Views sent to backend when 50%+ visible for 2+ seconds +- Debounced to prevent excessive API calls + +### Image Handling +- `ImagePicker` for selection +- `FlutterImageCompress` for compression +- MultiPart form data upload via Dio +- Preview via `CachedNetworkImage` + +### Search Implementation +- Debounced search with 500ms delay +- Min query length: 3 characters +- Applied to: Feed, Recipes, Diets, Plans, Consultants +- Tag-based filtering for recipes + +### Pagination Pattern +- Page-based pagination +- Scroll detection for next page load +- Loading state during fetch +- Meta data tracking (current page, last page) + +## Security Considerations + +### Authentication +- Bearer token authentication +- Secure token storage +- Automatic session management +- OAuth 2.0 for social login + +### Data Protection +- HTTPS only communication +- Token encryption in storage +- Secure WebSocket connections +- Privacy policy compliance + +## Key Files Reference + +### Entry Point +- `lib/main.dart` - App initialization + +### Core Infrastructure +- `lib/core/app_initializer.dart` - Startup sequence +- `lib/core/locator_service.dart` - DI container +- `lib/core/app_config.dart` - App configuration +- `lib/core/app_router.dart` - Routing setup + +### WebSocket +- `lib/core/websockets/ws_manager.dart` - Connection management +- `lib/core/websockets/ws_service.dart` - API service + +### Authentication +- `lib/features/auth/sdk/social_auth_facade.dart` - Social auth +- `lib/core/token_storage.dart` - Token persistence +- `lib/core/interceptors/token_interceptor.dart` - Auth interceptor + +### Environment +- `lib/core/env_config.dart` - Environment variables +- `.env` - Environment file (not in repo) + +## Key Success Metrics + +- User engagement with food diary +- Consultation bookings and completion rates +- Content creation by consultants +- User retention and progress tracking + +## Documentation Standards + +- **Last Updated**: 2025-10-16 +- All paths are relative to `/home/apoloz/dev/eatvio_app` +- Russian market focus maintained throughout documentation +- Version compatibility matrix: + | Component | Version | Notes | + |-----------|---------|-------| + | Flutter | 3.8.0+ | Required | + | Dart | 3.8.0-3.9.9 | Required | + | Android SDK | 34 | Target SDK | + | iOS Deployment | 14.0 | Minimum | + +## Ubuntu Development PC Optimization + ```bash +-# apt install linux-oem-24.04 +-# sudo apt-get install --install-recommends linux-lowlatency-hwe-24.04 +``` +---- +- ubuntu switch to hwe [kernel](https://canonical-kernel-docs.readthedocs-hosted.com/latest/reference/hwe-kernels/#installing-a-hwe-kernel) +---- +- ubuntu instal intel iRIS gpu [drivers](https://dgpu-docs.intel.com/driver/client/overview.html#ubuntu-latest) +``` diff --git a/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive b/android/.kotlin/sessions/kotlin-compiler-10039299475187077818.salive deleted file mode 100644 index e69de29b..00000000 diff --git a/assets/icons/consultant_rating_icon.svg b/assets/icons/consultant_rating_icon.svg new file mode 100644 index 00000000..493b4d95 --- /dev/null +++ b/assets/icons/consultant_rating_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/mail_logo.svg b/assets/icons/mail_logo.svg new file mode 100644 index 00000000..5891b116 --- /dev/null +++ b/assets/icons/mail_logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/ok_logo.svg b/assets/icons/ok_logo.svg new file mode 100644 index 00000000..07a62fbd --- /dev/null +++ b/assets/icons/ok_logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/vk_logo.svg b/assets/icons/vk_logo.svg new file mode 100644 index 00000000..274b3486 --- /dev/null +++ b/assets/icons/vk_logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/yandex_logo.svg b/assets/icons/yandex_logo.svg new file mode 100644 index 00000000..51b51eee --- /dev/null +++ b/assets/icons/yandex_logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/integration_test/auth_screen/auth_login_test.dart b/integration_test/auth_screen/auth_login_test.dart new file mode 100644 index 00000000..ce538770 --- /dev/null +++ b/integration_test/auth_screen/auth_login_test.dart @@ -0,0 +1,37 @@ +import 'package:eatvio_app/app_keys.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/main.dart' as app; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import '../common_cases/skip_welcome_screen.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + await AppInitializer.initialize(buildType: AppBuildType.tests); + }); + + testWidgets('Signs in successfully with valid credentials', (WidgetTester tester) async { + await tester.pumpWidget(const app.MyApp()); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await skipWelcomeScreen(tester); + final emailFieldFinder = find.byKey(AppKeys.emailTextFieldAuthScreen); + final passwordFieldFinder = find.byKey(AppKeys.passwordTextFieldAuthScreen); + final signInButtonFinder = find.byKey(AppKeys.signInButtonAuthScreen); + + expect(emailFieldFinder, findsOneWidget, reason: 'Email text field should be found.'); + expect(passwordFieldFinder, findsOneWidget, reason: 'Password text field should be found.'); + expect(signInButtonFinder, findsOneWidget, reason: 'Sign In button should be found.'); + + await tester.enterText(emailFieldFinder, 'reshetnick.mike+user4@gmail.com'); + await tester.enterText(passwordFieldFinder, '1234567890'); + await tester.pumpAndSettle(); + + await tester.tap(signInButtonFinder); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + expect(signInButtonFinder, findsNothing, reason: 'Sign-in button should not be visible after successful login and navigation.'); + }); +} diff --git a/integration_test/auth_screen/auth_logo_button_test.dart b/integration_test/auth_screen/auth_logo_button_test.dart new file mode 100644 index 00000000..c7ac598d --- /dev/null +++ b/integration_test/auth_screen/auth_logo_button_test.dart @@ -0,0 +1,31 @@ +import 'package:eatvio_app/app_keys.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:eatvio_app/main.dart' as app; +import '../common_cases/skip_welcome_screen.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + await AppInitializer.initialize(buildType: AppBuildType.tests); + }); + + testWidgets('tapping logo 7 times opens dev tools', (WidgetTester tester) async { + await tester.pumpWidget(const app.MyApp()); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await skipWelcomeScreen(tester); + final logoButtonFinder = find.byKey(AppKeys.hiddenLogoButtonAuthScreen); + expect(logoButtonFinder, findsOneWidget, reason: 'The logo GestureDetector should be found in auth screen'); + + for (int i = 0; i < 7; i++) { + await tester.tap(logoButtonFinder); + await tester.pump(const Duration(milliseconds: 50)); + } + await tester.pumpAndSettle(); + + expect(find.byType(DevToolsScreen), findsOneWidget, reason: 'DevToolsScreen should be visible after tapping the logo 7 times.'); + }); +} diff --git a/integration_test/common_cases/skip_welcome_screen.dart b/integration_test/common_cases/skip_welcome_screen.dart new file mode 100644 index 00000000..20f8899b --- /dev/null +++ b/integration_test/common_cases/skip_welcome_screen.dart @@ -0,0 +1,8 @@ +import 'package:eatvio_app/app_keys.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Future skipWelcomeScreen(WidgetTester tester) async { + final welcomeScreenButtonFinder = find.byKey(AppKeys.welcomeScreenButton); + await tester.tap(welcomeScreenButtonFinder); + await tester.pumpAndSettle(); +} \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 8e9edd22..aa7beccd 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 80; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -539,7 +539,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 80; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -578,7 +578,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 74; + CURRENT_PROJECT_VERSION = 80; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index b6363034..fc70b0b0 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ -import UIKit import Flutter +import UIKit +import YandexLoginSDK @main @objc class AppDelegate: FlutterAppDelegate { @@ -10,4 +11,20 @@ import Flutter GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + @available(iOS 8.0, *) + func application( + _ application: UIApplication, continue userActivity: NSUserActivity, + restorationHandler: @escaping ([Any]?) -> Void + ) -> Bool { + YXLSdk.shared.processUserActivity(userActivity) + return true + } + + override public func application( + _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + return YXLSdk.shared.handleOpen( + url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String) + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7e82c5b2..ad606b38 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 74 + 80 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS @@ -67,5 +67,24 @@ UIViewControllerBasedStatusBarAppearance + LSApplicationQueriesSchemes + + yandexauth + yandexauth2 + yandexauth4 + + YAClientId + 5399b03636104c6d920b87c1ad329254 + CFBundleURLTypes + + + CFBundleURLName + YandexLoginSDK + CFBundleURLSchemes + + yx5399b03636104c6d920b87c1ad329254 + + + diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 7b8ca2a8..4ed4ad00 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -8,6 +8,7 @@ applinks:eatvio.ru applinks:www.eatvio.ru + applinks:5399b03636104c6d920b87c1ad329254.oauth.yandex.ru diff --git a/lib/app_keys.dart b/lib/app_keys.dart new file mode 100644 index 00000000..3e732506 --- /dev/null +++ b/lib/app_keys.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class AppKeys { + + // Auth Screen + static const hiddenLogoButtonAuthScreen = ValueKey('hiddenLogoButtonAuthScreen'); + static const emailTextFieldAuthScreen = ValueKey('emailTextFieldAuthScreen'); + static const passwordTextFieldAuthScreen = ValueKey('passwordTextFieldAuthScreen'); + static const signInButtonAuthScreen = ValueKey('signInButtonAuthScreen'); + + // Welcome Screen + static const welcomeScreenButton = ValueKey('welcomeScreenButton'); +} diff --git a/lib/core/app_config.dart b/lib/core/app_config.dart new file mode 100644 index 00000000..7dc49aa0 --- /dev/null +++ b/lib/core/app_config.dart @@ -0,0 +1,55 @@ +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/core/env_config.dart'; +import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +/// Конфигурация приложения +class AppConfig { + AppConfig({required this.buildType}) { + _baseUrl = switch (buildType) { + AppBuildType.prod => EnvConfig.baseUrl, + AppBuildType.tests => EnvConfig.devBaseUrl, + AppBuildType.dev => EnvConfig.devBaseUrl, + }; + } + + final AppBuildType buildType; + late String _baseUrl; + String get baseUrl => _baseUrl; + + void setBaseUrl(String baseUrls) { + _baseUrl = baseUrls; + } + + /// Список глобальных BlocProvider'ов + List get globalBlocProviders => [ + BlocProvider(create: (context) => sl()), + BlocProvider(create: (context) => sl()..getNotifications()), + BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), + BlocProvider(create: (context) => sl()..getMyProfile()), + BlocProvider(create: (context) => sl()..getChatRoomsWithLoading()), + BlocProvider(create: (context) => sl()), + BlocProvider(lazy: false, create: (context) => sl()), + ]; + + /// Делегаты локализации + List get localizationDelegates => [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ]; + + /// Поддерживаемые локали + List get supportedLocales => [const Locale.fromSubtags(languageCode: 'ru')]; +} diff --git a/lib/core/app_initializer.dart b/lib/core/app_initializer.dart new file mode 100644 index 00000000..ea0b8f5d --- /dev/null +++ b/lib/core/app_initializer.dart @@ -0,0 +1,74 @@ +import 'dart:io' as io; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/env_config.dart'; +import 'package:eatvio_app/core/locator_service.dart' as di; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/firebase_options.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:appmetrica_plugin/appmetrica_plugin.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +/// Класс для инициализации приложения +class AppInitializer { + /// Инициализация всех сервисов приложения + static Future initialize({required AppBuildType buildType}) async { + await _initializeEnvironment(); + await _initializeDependencyInjection(buildType: buildType); + await _initializeSentry(); + await _initializeFirebase(); + await _initializeDebugMode(); + await _initializeAnalytics(); + } + + /// Инициализация Sentry + static Future _initializeSentry() async { + final widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + } + + /// Загрузка переменных окружения + static Future _initializeEnvironment() async { + await dotenv.load(fileName: ".env"); + } + + /// Инициализация Firebase + static Future _initializeFirebase() async { + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + if (sl().buildType == AppBuildType.prod) { + await FirebaseMessaging.instance.requestPermission(); + } + } + + /// Инициализация системы внедрения зависимостей + static Future _initializeDependencyInjection({required AppBuildType buildType}) async { + await di.init(buildType: buildType); + } + + /// Настройка режима отладки + static Future _initializeDebugMode() async { + if (kDebugMode) { + io.HttpClient.enableTimelineLogging = true; + } + } + + /// Инициализация аналитики + static Future _initializeAnalytics() async { + AppMetrica.activate(AppMetricaConfig(EnvConfig.appMetricaApiKey)); + AppMetrica.reportEvent('Application started'); + } + + /// Запуск приложения с Sentry + static Future runAppWithSentry(VoidCallback appRunner) async { + await SentryFlutter.init((options) { + options.dsn = EnvConfig.sentryDsn; + options.tracesSampleRate = 1.0; + options.debug = false; + }, appRunner: appRunner); + } +} + +enum AppBuildType { prod, tests, dev } diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index dda4395a..d17019d2 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,455 +1,58 @@ -import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/firebase_api.dart'; -import 'package:eatvio_app/core/token_storage.dart'; -import 'package:eatvio_app/core/websockets/ws_manager.dart'; -import 'package:eatvio_app/core/locator_service.dart'; -import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; -import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; -import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart'; -import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; -import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; -import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart'; -import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; -import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart'; -import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart'; -import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; -import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; -import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; -import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; -import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; -import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; -import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; -import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.dart'; -import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; -import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; -import 'package:eatvio_app/features/feed/models/recipes_model.dart'; -import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; -import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.dart'; -import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart'; -import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; -import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart'; -import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart'; -import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; -import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart'; -import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart'; -import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; -import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'; -import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart'; -import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; -import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart'; -import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart'; -import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart'; -import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart'; -import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/change_password_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/general_info_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/physical_activity_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart'; -import 'package:eatvio_app/features/support/presentation/pages/support_screen.dart'; -import 'package:eatvio_app/features/update_screen/presentation/update_screen.dart'; -import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; -import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart'; +import 'package:eatvio_app/core/routing/auth_routes.dart'; +import 'package:eatvio_app/core/routing/content_routes.dart'; +import 'package:eatvio_app/core/routing/main_navigation_routes.dart'; +import 'package:eatvio_app/core/routing/other_routes.dart'; +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/core/routing/settings_routes.dart'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:go_router/go_router.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:version/version.dart'; -import 'dart:developer'; +/// Главный роутер приложения class AppRouter { static GlobalKey navigatorKey = GlobalKey(); static GoRouter appRouter = GoRouter( - initialLocation: '/update-screen', + initialLocation: RouteConstants.updateScreen, navigatorKey: navigatorKey, - redirect: (context, state) { - final segments = state.uri.pathSegments; - if (segments.length >= 3) { - final type = segments[1]; - final contentIdentifier = segments[2]; - - switch (type) { - case 'recipe': - FlutterNativeSplash.remove(); - return '/about-recipes-screen?slug=$contentIdentifier'; - case 'blog_post': - FlutterNativeSplash.remove(); - return '/blog-post-screen?id=$contentIdentifier'; - case 'plan': - FlutterNativeSplash.remove(); - return '/about-plan-screen?slug=$contentIdentifier'; - case 'diet': - FlutterNativeSplash.remove(); - return '/about-diet-screen?slug=$contentIdentifier'; - } - } - - return null; - }, + redirect: _handleRedirect, routes: [ - GoRoute( - path: '/auth-screen', - name: 'authScreen', - builder: (context, state) => const AuthScreen(), - routes: [ - GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), - GoRoute( - path: 'forget-password-screen', - name: 'forgetPasswordScreen', - builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool), - ), - GoRoute(path: 'scan-qr-code-screen', name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()), - ], - ), - ShellRoute( - builder: (context, state, child) { - return BlocBuilder( - builder: (BuildContext context, ProfileAvatarState state) { - if (state is ProfileAvatarStateLoaded) { - return BottomNavigationBarWidget(profileModel: state.profile, child: child); - } - return const SizedBox.shrink(); - }, - ); - }, - routes: [ - GoRoute( - path: '/my-profile-screen', - name: 'myProfileScreen', - pageBuilder: (context, state) { - return const NoTransitionPage(child: ProfileScreen()); - }, - ), - GoRoute( - path: '/feed-screen', - name: 'feedScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()), - redirect: (context, state) async { - try { - await sl().initialize(); - } catch (e, st) { - log(e.toString(), stackTrace: st); - } - return null; - }, - ), - GoRoute( - path: '/diary-screen', - name: 'diaryScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), - ), - GoRoute( - path: ConsultationsScreen.path, - name: ConsultationsScreen.name, - pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()), - ), - GoRoute( - path: '/chat-select-screen', - name: 'chatSelectScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), - ), - GoRoute( - path: '/notifications-screen', - name: 'notificationsScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()), - ), - GoRoute( - path: '/menu-screen', - name: 'menuScreen', - pageBuilder: (context, state) => NoTransitionPage( - child: BlocBuilder( - builder: (BuildContext context, ProfileAvatarState state) { - if (state is ProfileAvatarStateLoaded) { - return MenuScreen(profileModel: state.profile); - } - return const SizedBox.shrink(); - }, - ), - ), - ), - ], - ), - GoRoute(name: 'blockedUsersScreen', path: '/blocked-users-screen', builder: (context, state) => const BlockedUsersScreen()), - GoRoute( - path: '/welcome-screen', - name: 'welcomeScreen', - builder: (context, state) => const WelcomeScreen(), - redirect: (context, state) async { - try { - final sharedPreferences = await SharedPreferences.getInstance(); - final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false; - await sl().registerPushToken(); - if (welcomeScreenWasShowed == false) { - return null; - } else if (await TokenStorage.haveTokenInStorage()) { - return '/feed-screen'; - } else { - return '/auth-screen'; - } - } catch (e) { - return '/feed-screen'; - } - }, - ), - GoRoute( - path: '/update-screen', - name: 'updateScreen', - builder: (context, state) => const UpdateScreen(), - redirect: (context, state) async { - try { - final versions = (await sl().get('$baseUrl/api/app_versions')).data; - PackageInfo packageInfo = await PackageInfo.fromPlatform(); - String version = packageInfo.version; - Version currentVersion = Version.parse(version); - Version latestVersion = Version.parse(versions['min_version']); - if (latestVersion > currentVersion) { - return null; - } else { - return '/welcome-screen'; - } - } catch (e) { - return '/welcome-screen'; - } - }, - ), - GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()), - GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()), - GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()), - GoRoute(path: '/user-finance-screen', name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()), - GoRoute( - path: CardSettingsScreen.path, - name: CardSettingsScreen.name, - builder: (context, state) { - final chosenPlanId = state.extra as int?; - return CardSettingsScreen(chosenPlanId: chosenPlanId); - }, - ), - GoRoute( - path: '/about-plan-screen', - name: 'aboutPlanScreen', - builder: (context, state) { - final slug = state.uri.queryParameters['slug']; - if (slug != null) { - return AboutPlanScreen(slug: slug, navigateToComments: false, index: null); - } - - final data = state.extra as Map; - return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); - }, - ), - GoRoute(path: '/glossary-screen', name: 'glossaryScreen', builder: (context, state) => GlossaryScreen(passKey: state.extra as String,)), - GoRoute( - path: '/about-recipes-screen', - name: 'aboutRecipesScreen', - builder: (context, state) { - final slug = state.uri.queryParameters['slug']; - if (slug != null) { - return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null); - } - - final data = state.extra as Map; - return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); - }, - ), - GoRoute( - path: RecipeCreateScreen.path, - name: RecipeCreateScreen.name, - builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null), - ), - GoRoute( - path: '/profile-screen', - name: 'profileScreen', - builder: (context, state) => ProfileScreen(author: state.extra as Author), - ), - GoRoute( - path: '/subs-screen/:slug', - name: 'subsScreen', - builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType), - ), - GoRoute( - path: '/about-diet-screen', - name: 'aboutDietScreen', - builder: (context, state) { - final slug = state.uri.queryParameters['slug']; - if (slug != null) { - return AboutDietScreen(slug: slug, index: null, navigateToComments: false); - } - final data = state.extra as Map; - return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); - }, - ), - GoRoute( - path: '/blog-post-create-screen', - name: 'blogPostCreateScreen', - builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null), - ), - GoRoute( - path: '/blog-post-screen', - name: 'blogPostScreen', - builder: (context, state) { - final id = state.uri.queryParameters['id']; - final parsedId = int.tryParse(id ?? ''); - if (parsedId != null) { - return BlogPostScreen(id: parsedId); - } - - return BlogPostScreen(id: state.extra as int); - }, - ), - GoRoute( - path: '/chat-screen', - name: 'chatScreen', - builder: (context, state) { - final data = state.extra as Map; - final uuid = data['uuid'] as String; - final sendInitialMessage = data['sendInitialMessage'] as bool?; - return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage); - }, - ), - GoRoute( - path: '/empty-chat-screen', - name: 'emptyChatScreen', - builder: (context, state) => EmptyChatScreen(author: state.extra as Author), - ), - GoRoute( - path: '/settings-screen', - name: 'settingsScreen', - builder: (context, state) => const SettingsScreen(), - routes: [ - GoRoute( - path: 'general-info-screen', - name: 'generalInfoScreen', - builder: (context, state) => GeneralInfoScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'other-info-screen', - name: 'otherInfoScreen', - builder: (context, state) => OtherInfoScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'physical-activity-screen', - name: 'physicalActivityScreen', - builder: (context, state) => PhysicalActivityScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'physical-form-screen', - name: 'physicalFormScreen', - builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'pressure-screen', - name: 'pressureScreen', - builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'change-password-screen', - name: 'changePasswordScreen', - builder: (context, state) => ChangePasswordScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'change-email-screen', - name: 'changeEmailScreen', - builder: (context, state) => ChangeEmailScreen(settingsEntity: state.extra as SettingsEntity), - ), - ], - ), - GoRoute(path: '/become-consultant-screen', name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()), - GoRoute( - path: '/stepper-become-consultant-screen', - name: 'stepperBecomeConsultantScreen', - builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String), - ), - GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), - GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()), - GoRoute( - path: '/photo-view-screen/:index', - name: 'photoViewScreen', - builder: (context, state) => PhotoViewScreen(imageUrls: state.extra as List, index: int.parse(state.pathParameters['index']!)), - ), - GoRoute( - path: CreateDietScreen.path, - name: CreateDietScreen.name, - builder: (context, state) { - final diet = state.extra as Diet?; - return CreateDietScreen(diet: diet); - }, - ), - GoRoute( - path: DietPreviewScreen.path, - name: DietPreviewScreen.name, - builder: (context, state) { - final data = state.extra as Diet; - return DietPreviewScreen(diet: data); - }, - ), - GoRoute(path: CreatePlanScreen.path, name: CreatePlanScreen.name, builder: (context, state) => const CreatePlanScreen()), - GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()), - GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()), - GoRoute( - path: UserDetailsScreen.path, - name: UserDetailsScreen.name, - builder: (context, state) => UserDetailsScreen(slug: state.extra as String), - ), - GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()), - GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()), - GoRoute( - path: InfoByOrderScreen.path, - name: InfoByOrderScreen.name, - builder: (context, state) { - final data = state.extra as Map; - final planId = data['planId'] as int; - final successfulPayment = data['successfulPayment'] as bool?; - return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment); - }, - ), - GoRoute( - path: PaymentScreen.path, - name: PaymentScreen.name, - builder: (context, state) { - final data = state.extra as Map; - final planId = data['planId'] as int; - final url = data['url'] as String; - return PaymentScreen(url: url, planId: planId); - }, - ), - GoRoute( - path: UserDiaryScreen.path, - name: UserDiaryScreen.name, - builder: (context, state) { - final data = state.extra as Map; - final diaryId = data['diaryId'] as int; - final currentPlanDay = data['currentDiaryDay'] as int; - return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay); - }, - ), - GoRoute( - path: '/verify-screen', - name: 'verifyScreen', - builder: (context, state) => VerifyScreen(passKey: state.extra as String), - ), - GoRoute(path: '/verify-email-screen', name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), - GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()), - GoRoute(path: '/notifications-settings-screen', name: 'notificationsSettingsScreen', builder: (context, state) => const NotificationsSettingsScreen()), + ...AuthRoutes.createRoutes(), + ...MainNavigationRoutes.createRoutes(), + ...ContentRoutes.createRoutes(), + ...SettingsRoutes.createRoutes(), + ...OtherRoutes.createRoutes(), ], ); + + /// Обработка редиректов для deep links + static String? _handleRedirect(BuildContext context, GoRouterState state) { + final segments = state.uri.pathSegments; + if (segments.length >= 3) { + final type = segments[1]; + final contentIdentifier = segments[2]; + + switch (type) { + case 'recipe': + FlutterNativeSplash.remove(); + return '${RouteConstants.aboutRecipesScreen}?slug=$contentIdentifier'; + case 'blog_post': + FlutterNativeSplash.remove(); + return '${RouteConstants.blogPostScreen}?id=$contentIdentifier'; + case 'plan': + FlutterNativeSplash.remove(); + return '${RouteConstants.aboutPlanScreen}?slug=$contentIdentifier'; + case 'diet': + FlutterNativeSplash.remove(); + return '${RouteConstants.aboutDietScreen}?slug=$contentIdentifier'; + } + } + + return null; + } + + static void navigateToRegisterViaSocial({required AuthResultEntity authResult, required String provider}) { + appRouter.pushNamed('registerViaSocialScreen', extra: {'authResult': authResult, 'provider': provider}); + } } diff --git a/lib/core/const.dart b/lib/core/const.dart index 98f7bc4a..5aab4fd4 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,6 +1,3 @@ -import 'package:eatvio_app/core/env_config.dart'; - -String baseUrl = EnvConfig.baseUrl; bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false); const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: ''); diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart new file mode 100644 index 00000000..f9c945c9 --- /dev/null +++ b/lib/core/constants/app_constants.dart @@ -0,0 +1,72 @@ +/// Константы приложения +class AppConstants { + // Размеры + static const double defaultPadding = 16.0; + static const double smallPadding = 8.0; + static const double largePadding = 24.0; + static const double extraLargePadding = 32.0; + + // Радиусы + static const double defaultBorderRadius = 8.0; + static const double smallBorderRadius = 4.0; + static const double largeBorderRadius = 16.0; + + // Высоты + static const double defaultButtonHeight = 48.0; + static const double smallButtonHeight = 36.0; + static const double largeButtonHeight = 56.0; + + // Ширины + static const double maxContentWidth = 400.0; + static const double minContentWidth = 280.0; + + // Анимации + static const Duration defaultAnimationDuration = Duration(milliseconds: 300); + static const Duration fastAnimationDuration = Duration(milliseconds: 150); + static const Duration slowAnimationDuration = Duration(milliseconds: 500); + + // Таймауты + static const Duration defaultTimeout = Duration(seconds: 30); + static const Duration shortTimeout = Duration(seconds: 10); + static const Duration longTimeout = Duration(minutes: 2); + + // Лимиты + static const int maxImageSize = 5 * 1024 * 1024; // 5MB + static const int maxVideoSize = 100 * 1024 * 1024; // 100MB + static const int maxTextLength = 1000; + static const int maxTitleLength = 100; + static const int maxDescriptionLength = 500; + + // Пагинация + static const int defaultPageSize = 20; + static const int smallPageSize = 10; + static const int largePageSize = 50; + + // Форматы + static const List supportedImageFormats = ['jpg', 'jpeg', 'png', 'webp']; + static const List supportedVideoFormats = ['mp4', 'mov', 'avi']; + + // Регулярные выражения + static const String emailRegex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; + static const String phoneRegex = r'^\+?[1-9]\d{1,14}$'; + static const String urlRegex = r'^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$'; + + // Сообщения + static const String defaultErrorMessage = 'Произошла ошибка. Попробуйте еще раз.'; + static const String networkErrorMessage = 'Проблема с подключением к интернету.'; + static const String timeoutErrorMessage = 'Превышено время ожидания.'; + static const String unknownErrorMessage = 'Неизвестная ошибка.'; + + // Ключи для SharedPreferences + static const String welcomeScreenShownKey = 'welcomeScreenWasShowed'; + static const String userTokenKey = 'userToken'; + static const String userDataKey = 'userData'; + static const String settingsKey = 'settings'; + + // API endpoints + static const String apiVersion = '/api'; + static const String authEndpoint = '/auth'; + static const String usersEndpoint = '/users'; + static const String postsEndpoint = '/posts'; + static const String commentsEndpoint = '/comments'; +} diff --git a/lib/core/cubit/base_cubit.dart b/lib/core/cubit/base_cubit.dart new file mode 100644 index 00000000..03ff3cc2 --- /dev/null +++ b/lib/core/cubit/base_cubit.dart @@ -0,0 +1,42 @@ +import 'dart:developer'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// Базовый класс для всех Cubit'ов с унифицированной обработкой ошибок +abstract class BaseCubit extends Cubit { + BaseCubit(super.initialState); + + /// Унифицированная обработка ошибок + void handleError(Object error, [StackTrace? stackTrace]) { + log('Error in $runtimeType: $error', stackTrace: stackTrace); + // Здесь можно добавить отправку ошибок в Sentry или другую систему мониторинга + } + + /// Безопасное выполнение асинхронных операций + Future safeExecute(Future Function() operation) async { + try { + return await operation(); + } catch (error, stackTrace) { + handleError(error, stackTrace); + return null; + } + } + + /// Безопасное выполнение асинхронных операций с обработкой состояния + Future safeExecuteWithState( + Future Function() operation, { + required State loadingState, + required State Function(T result) successState, + required State Function(Object error) errorState, + }) async { + try { + emit(loadingState); + final result = await operation(); + emit(successState(result)); + return result; + } catch (error, stackTrace) { + handleError(error, stackTrace); + emit(errorState(error)); + return null; + } + } +} diff --git a/lib/core/cubit/state_utils.dart b/lib/core/cubit/state_utils.dart new file mode 100644 index 00000000..9c6dfb78 --- /dev/null +++ b/lib/core/cubit/state_utils.dart @@ -0,0 +1,28 @@ +/// Утилиты для работы с состояниями Cubit'ов +class StateUtils { + /// Проверяет, является ли состояние загрузочным + static bool isLoading(dynamic state) { + return state.toString().contains('loading') || state.toString().contains('Loading'); + } + + /// Проверяет, является ли состояние загруженным + static bool isLoaded(dynamic state) { + return state.toString().contains('loaded') || state.toString().contains('Loaded'); + } + + /// Проверяет, является ли состояние ошибочным + static bool isError(dynamic state) { + return state.toString().contains('error') || state.toString().contains('Error'); + } + + /// Получает сообщение об ошибке из состояния + static String? getErrorMessage(dynamic state) { + if (isError(state)) { + // Попытка извлечь сообщение об ошибке из состояния + final stateString = state.toString(); + final errorMatch = RegExp(r'error[:\s]+([^,}]+)').firstMatch(stateString); + return errorMatch?.group(1)?.trim(); + } + return null; + } +} diff --git a/lib/core/di/app_config_registration.dart b/lib/core/di/app_config_registration.dart new file mode 100644 index 00000000..de003d20 --- /dev/null +++ b/lib/core/di/app_config_registration.dart @@ -0,0 +1,12 @@ +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:get_it/get_it.dart'; + +class AppConfigRegistration { + static final sl = GetIt.instance; + + /// Регистрация AppConfig + static void registerAppConfig({required AppBuildType buildType}) { + sl.registerLazySingleton(() => AppConfig(buildType: buildType)); + } +} diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart new file mode 100644 index 00000000..ca3ece97 --- /dev/null +++ b/lib/core/di/cubit_registrations.dart @@ -0,0 +1,147 @@ +import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart'; +import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; +import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart'; +import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart'; +import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; +import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; +import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; +import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; +import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; +import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; +import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart'; +import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans; +import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart'; +import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart'; +import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; +import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart'; +import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart'; +import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; +import 'package:get_it/get_it.dart'; + +/// Регистрация всех Cubit'ов в системе внедрения зависимостей +class CubitRegistrations { + static final sl = GetIt.instance; + + /// Регистрация всех Cubit'ов + static void registerAll() { + _registerSingletonCubits(); + _registerFactoryCubits(); + } + + /// Регистрация Cubit'ов как синглтонов + static void _registerSingletonCubits() { + sl.registerLazySingleton(() => NewMessagesCubit()); + sl.registerLazySingleton(() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl())); + sl.registerLazySingleton(() => ProfileAvatarCubit(profileRepository: sl())); + sl.registerLazySingleton(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl())); + sl.registerLazySingleton(() => InternetConnectionCubit()); + } + + /// Регистрация Cubit'ов как фабрик + static void _registerFactoryCubits() { + // Auth related cubits + sl.registerFactory(() => AuthCubit(authRepository: sl())); + sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); + sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); + sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl())); + sl.registerFactory(() => OAuthCubit(authRepository: sl())); + // Feed related cubits + sl.registerFactory(() => FeedCubit(feedRepository: sl())); + sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl())); + sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); + sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); + sl.registerFactory(() => ConsultantsCubit(feedRepository: sl())); + sl.registerFactory(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl())); + sl.registerFactory(() => FeedProfileCubit(profileRepository: sl())); + + // Chat related cubits + sl.registerFactory(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl())); + + // Profile related cubits + sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl())); + sl.registerFactory(() => profile_plans.PlansCubit(plansRepository: sl())); + sl.registerFactory(() => SubCubit(profileRepository: sl())); + + // Content creation cubits + sl.registerFactory(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl())); + sl.registerFactory(() => CreatePlanCubit(createPlanRepository: sl())); + sl.registerFactory(() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl())); + sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); + + // Content viewing cubits + sl.registerFactory(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl())); + sl.registerFactory(() => AboutPlanCubit(planRepository: sl())); + sl.registerFactory(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl())); + sl.registerFactory(() => BlogPostCubit(sl())); + + // User content cubits + sl.registerFactory(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl())); + sl.registerFactory(() => MyPlansCubit(myPlansRepository: sl())); + sl.registerFactory(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl())); + + // Other feature cubits + sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); + sl.registerFactory( + () => DiaryCubit( + chatRepository: sl(), + diaryRepository: sl(), + glossaryRepository: sl(), + profileRepository: sl(), + dietRepository: sl(), + settingsRepository: sl(), + ), + ); + sl.registerFactory( + () => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl()), + ); + sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); + sl.registerFactory(() => ComplainingCubit(repository: sl())); + sl.registerFactory(() => UserCardsCubit(cardsRepository: sl())); + sl.registerFactory(() => OrderInfoCubit(chatRepository: sl())); + sl.registerFactory(() => ConsultationsCubit(consultationsRepository: sl())); + sl.registerFactory(() => ConsultantCommentsCubit(diaryRepository: sl())); + sl.registerFactory(() => CancelPlanCubit(cancelPlanRepository: sl())); + sl.registerFactory(() => ConsultationsArchiveCubit(consultationsRepository: sl())); + sl.registerFactory(() => ConsultantFinancesCubit(consultationsRepository: sl())); + sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl())); + sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); + sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); + sl.registerFactory(() => ConsultantRatingCubit(feedRepository: sl(), repository: sl(), profileRepository: sl())); + sl.registerFactory(() => ViewsCubit(viewsRepository: sl())); + sl.registerFactory(() => UserDetailsCubit(profileRepository: sl())); + sl.registerFactory(() => MenuCubit(sl())); + } +} diff --git a/lib/core/di/external_registrations.dart b/lib/core/di/external_registrations.dart new file mode 100644 index 00000000..231eeef3 --- /dev/null +++ b/lib/core/di/external_registrations.dart @@ -0,0 +1,33 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/interceptors/phone_verify_error_interceptor.dart'; +import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; +import 'package:get_it/get_it.dart'; + +/// Регистрация внешних зависимостей в системе внедрения зависимостей +class ExternalRegistrations { + static final sl = GetIt.instance; + + /// Регистрация всех внешних зависимостей + static void registerAll() { + _registerDio(); + } + + /// Регистрация Dio HTTP клиента + static void _registerDio() { + sl.registerLazySingleton( + () => Dio( + BaseOptions( + baseUrl: sl().baseUrl, + headers: { + 'accept': 'application/json', + 'content-type': 'application/json', + }, + ), + )..interceptors.addAll([ + TokenInterceptor(), + PhoneVerifyErrorInterceptor(), + ]), + ); + } +} diff --git a/lib/core/di/repository_registrations.dart b/lib/core/di/repository_registrations.dart new file mode 100644 index 00000000..6d467810 --- /dev/null +++ b/lib/core/di/repository_registrations.dart @@ -0,0 +1,89 @@ +import 'package:eatvio_app/core/comments/repositories/comments_repository.dart'; +import 'package:eatvio_app/core/views/repository/views_repository.dart'; +import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; +import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart'; +import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; +import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart'; +import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart'; +import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; +import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; +import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart'; +import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; +import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; +import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; +import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; +import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; +import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; +import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; +import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; +import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; +import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; +import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; +import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart'; +import 'package:eatvio_app/features/plans/repositories/plan_repository.dart'; +import 'package:eatvio_app/features/plans/repositories/plans_repository.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; +import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; +import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart'; +import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; +import 'package:eatvio_app/features/settings/repositories/service_repository.dart'; +import 'package:get_it/get_it.dart'; + +/// Регистрация всех Repository в системе внедрения зависимостей +class RepositoryRegistrations { + static final sl = GetIt.instance; + + /// Регистрация всех Repository + static void registerAll() { + _registerSingletonRepositories(); + _registerFactoryRepositories(); + } + + /// Регистрация Repository как синглтонов + static void _registerSingletonRepositories() { + // Core repositories + sl.registerLazySingleton(() => ChatRepository(chatService: sl())); + sl.registerLazySingleton(() => FeedRepository(feedService: sl())); + sl.registerLazySingleton(() => ProfileRepository(profileService: sl())); + sl.registerLazySingleton(() => CalendarRepository(calendarService: sl())); + sl.registerLazySingleton(() => SettingsRepository(settingsService: sl())); + sl.registerLazySingleton(() => ConsultantRepository(consultantService: sl())); + sl.registerLazySingleton(() => DiaryRepository(diaryService: sl())); + sl.registerLazySingleton(() => GlossaryRepository(glossaryService: sl())); + sl.registerLazySingleton(() => DietRepository(dietsService: sl())); + sl.registerLazySingleton(() => PlansRepository(plansService: sl())); + sl.registerLazySingleton(() => PlanRepository(planService: sl())); + sl.registerLazySingleton(() => AboutRecipeRepository(recipeService: sl())); + sl.registerLazySingleton(() => RecipeRepository(recipeService: sl())); + sl.registerLazySingleton(() => ComplainingRepository(service: sl())); + sl.registerLazySingleton(() => AuthRepository(authService: sl())); + sl.registerLazySingleton(() => CardsRepository(cardsService: sl())); + sl.registerLazySingleton(() => ConsultationsRepository(consultationService: sl())); + sl.registerLazySingleton(() => CancelPlanRepository(cancelService: sl())); + sl.registerLazySingleton(() => CreatePlanRepository(createPlanService: sl())); + sl.registerLazySingleton(() => NotificationsRepository(notificationsService: sl())); + sl.registerLazySingleton(() => BlockedUsersRepository(blockedUserService: sl())); + sl.registerLazySingleton(() => CommentsRepository(service: sl())); + sl.registerLazySingleton(() => NotificationSettingsRepository(sl())); + + // Content repositories + sl.registerLazySingleton(() => BlogPostRepository(blogPostService: sl())); + sl.registerLazySingleton(() => CreateDietRepository(createDietService: sl())); + sl.registerLazySingleton(() => MyDietsRepository(myDietsService: sl())); + sl.registerLazySingleton(() => MyPlansRepository(myPlansService: sl())); + + // Auth repositories + sl.registerLazySingleton(() => RegistrationRepository(registrationService: sl())); + } + + /// Регистрация Repository как фабрик + static void _registerFactoryRepositories() { + sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl())); + sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl())); + sl.registerFactory(() => ConsultantRatingRepository(sl())); + sl.registerFactory(() => ViewsRepository(viewsService: sl())); + } +} diff --git a/lib/core/di/service_registrations.dart b/lib/core/di/service_registrations.dart new file mode 100644 index 00000000..454d9064 --- /dev/null +++ b/lib/core/di/service_registrations.dart @@ -0,0 +1,88 @@ +import 'package:eatvio_app/core/comments/services/comments_service.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/views/service/views_service.dart'; +import 'package:eatvio_app/core/websockets/ws_service.dart'; +import 'package:eatvio_app/features/auth/services/auth_service.dart'; +import 'package:eatvio_app/features/auth/services/reset_password_service.dart'; +import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; +import 'package:eatvio_app/features/calendar/services/calendar_service.dart'; +import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart'; +import 'package:eatvio_app/features/chat/services/chat_service.dart'; +import 'package:eatvio_app/features/complaining/services/complaining_service.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart'; +import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart'; +import 'package:eatvio_app/features/consultations/services/consultation_service.dart'; +import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; +import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; +import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart'; +import 'package:eatvio_app/features/diary/services/diary_service.dart'; +import 'package:eatvio_app/features/diets/services/diet_service.dart'; +import 'package:eatvio_app/features/feed/services/feed_service.dart'; +import 'package:eatvio_app/features/glossary/services/glossary_service.dart'; +import 'package:eatvio_app/features/menu/services/blocked_users_service.dart'; +import 'package:eatvio_app/features/menu/services/notification_settings_service.dart'; +import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; +import 'package:eatvio_app/features/my_finance/services/cards_service.dart'; +import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; +import 'package:eatvio_app/features/notifications/service/notifications_service.dart'; +import 'package:eatvio_app/features/plans/services/plan_service.dart'; +import 'package:eatvio_app/features/plans/services/plans_service.dart'; +import 'package:eatvio_app/features/profile/services/profile_service.dart'; +import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart'; +import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart'; +import 'package:eatvio_app/features/registration/services/registration_service.dart'; +import 'package:eatvio_app/features/settings/services/settings_service.dart'; +import 'package:get_it/get_it.dart'; + +/// Регистрация всех Service в системе внедрения зависимостей +class ServiceRegistrations { + static final sl = GetIt.instance; + + /// Регистрация всех Service + static void registerAll() { + _registerSingletonServices(); + } + + /// Регистрация Service как синглтонов + static void _registerSingletonServices() { + // Core services + sl.registerLazySingleton(() => ChatService(sl())); + sl.registerLazySingleton(() => FeedService(sl())); + sl.registerLazySingleton(() => ProfileService(sl())); + sl.registerLazySingleton(() => CalendarService(sl())); + sl.registerLazySingleton(() => SettingsService(sl())); + sl.registerLazySingleton(() => ConsultantService(sl())); + sl.registerLazySingleton(() => DiaryService(sl())); + sl.registerLazySingleton(() => GlossaryService(sl())); + sl.registerLazySingleton(() => DietService(sl())); + sl.registerLazySingleton(() => PlansService(sl())); + sl.registerLazySingleton(() => PlanService(sl())); + sl.registerLazySingleton(() => MyDietsService(sl())); + sl.registerLazySingleton(() => MyPlansService(sl())); + sl.registerLazySingleton(() => WsService(sl())); + sl.registerLazySingleton(() => AboutRecipeService(sl())); + sl.registerLazySingleton(() => ComplainingService(sl())); + sl.registerLazySingleton(() => AuthService(sl())); + sl.registerLazySingleton(() => CardsService(sl())); + sl.registerLazySingleton(() => ConsultationService(sl())); + sl.registerLazySingleton(() => CancelPlanService(sl())); + sl.registerLazySingleton(() => CreatePlanService(sl())); + sl.registerLazySingleton(() => NotificationsService(sl())); + sl.registerLazySingleton(() => FirebaseApi()); + sl.registerLazySingleton(() => ResetPasswordService(sl())); + sl.registerLazySingleton(() => BlockedUserService(sl())); + sl.registerLazySingleton(() => CommentsService(sl())); + sl.registerLazySingleton(() => MyRecipesService(sl())); + sl.registerLazySingleton(() => ConsultantRatingService(sl())); + sl.registerLazySingleton(() => NotificationSettingsService(sl())); + sl.registerLazySingleton(() => ViewsService(sl())); + + // Content services + sl.registerLazySingleton(() => CreateRecipeService(sl())); + sl.registerLazySingleton(() => BlogPostService(sl())); + sl.registerLazySingleton(() => CreateDietService(sl())); + + // Auth services + sl.registerLazySingleton(() => RegistrationService(sl())); + } +} diff --git a/lib/core/env_config.dart b/lib/core/env_config.dart index a3404aeb..3d451c6f 100644 --- a/lib/core/env_config.dart +++ b/lib/core/env_config.dart @@ -43,4 +43,13 @@ class EnvConfig { // Connection Check URLs static String get googleCheckUrl => dotenv.env['GOOGLE_CHECK_URL'] ?? ''; static String get yandexCheckUrl => dotenv.env['YANDEX_CHECK_URL'] ?? ''; + + // VK ID Configuration + static String get vkClientIdIos => dotenv.env['VK_CLIENT_ID_IOS'] ?? ''; + static String get vkClientIdAndroid => dotenv.env['VK_CLIENT_ID_ANDROID'] ?? ''; + + // Yandex ID Configuration + static String get yandexClientId => dotenv.env['YANDEX_CLIENT_ID'] ?? ''; + static String get yandexClientSecret => dotenv.env['YANDEX_CLIENT_SECRET'] ?? ''; + static String get yandexRedirectUri => dotenv.env['YANDEX_REDIRECT_URI'] ?? ''; } diff --git a/lib/core/firebase_api.dart b/lib/core/firebase_api.dart index 2d89bd43..7f4baad7 100644 --- a/lib/core/firebase_api.dart +++ b/lib/core/firebase_api.dart @@ -1,5 +1,7 @@ import 'dart:developer'; import 'dart:io'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:flutter/services.dart'; @@ -19,7 +21,9 @@ class FirebaseApi { if (await _isHuaweiDevice()) { token = await _getHuaweiPushTokenNative(); } else { - await _firebaseMessaging.requestPermission(); + if ((sl().buildType == AppBuildType.prod)) { + await _firebaseMessaging.requestPermission(); + } token = await _firebaseMessaging.getToken(); } return token; diff --git a/lib/core/dio/phone_verify_error_interceptor.dart b/lib/core/interceptors/phone_verify_error_interceptor.dart similarity index 100% rename from lib/core/dio/phone_verify_error_interceptor.dart rename to lib/core/interceptors/phone_verify_error_interceptor.dart diff --git a/lib/core/internet_connection_cubit/internet_connection_cubit.dart b/lib/core/internet_connection_cubit/internet_connection_cubit.dart new file mode 100644 index 00000000..67427c6c --- /dev/null +++ b/lib/core/internet_connection_cubit/internet_connection_cubit.dart @@ -0,0 +1,73 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:eatvio_app/core/env_config.dart'; +import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_state.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:http/http.dart' as http; + +class InternetConnectionCubit extends Cubit { + InternetConnectionCubit() : super(InternetConnectionState()) { + checkConnection(); + _connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateAccessStatus); + _listener = AppLifecycleListener( + onResume: () async { + _connectivitySubscription.resume(); + await checkConnection(); + }, + onHide: _connectivitySubscription.pause, + onPause: _connectivitySubscription.pause, + ); + } + + final Connectivity _connectivity = Connectivity(); + late StreamSubscription> _connectivitySubscription; + late final AppLifecycleListener _listener; + + Future checkConnection() async { + late List result; + + try { + result = await _connectivity.checkConnectivity(); + } on PlatformException catch (e) { + log('Couldn\'t check connectivity status', error: e); + return; + } + + return _updateAccessStatus(result); + } + + Future _updateAccessStatus(List result) async { + final hasInternet = await _checkInternetAccess(result); + if (!hasInternet) showInternetConnectionToast(); + emit(state.copyWith(isInternetAvailable: hasInternet)); + } + + Future _checkInternetAccess(List result) async { + if (result.contains(ConnectivityResult.none)) { + return false; + } + + final uris = [Uri.parse(EnvConfig.googleCheckUrl), Uri.parse(EnvConfig.yandexCheckUrl), Uri.parse(EnvConfig.devBaseUrl), Uri.parse(EnvConfig.baseUrl)]; + for (final uri in uris) { + try { + final response = await http.get(uri).timeout(const Duration(seconds: 3)); + return response.statusCode >= 100 && response.statusCode < 600; + } catch (e) { + continue; + } + } + return false; + } + + @override + Future close() { + _connectivitySubscription.cancel(); + _listener.dispose(); + return super.close(); + } +} diff --git a/lib/core/internet_connection_cubit/internet_connection_state.dart b/lib/core/internet_connection_cubit/internet_connection_state.dart new file mode 100644 index 00000000..95c831e1 --- /dev/null +++ b/lib/core/internet_connection_cubit/internet_connection_state.dart @@ -0,0 +1,13 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'internet_connection_state.freezed.dart'; + + +@freezed +abstract class InternetConnectionState with _$InternetConnectionState { + + factory InternetConnectionState({ + @Default(true) bool isInternetAvailable, + }) = _InternetConnectionState; + +} \ No newline at end of file diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 917c9dd1..d3ed9b95 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -1,265 +1,32 @@ -import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/comments/repositories/comments_repository.dart'; -import 'package:eatvio_app/core/comments/services/comments_service.dart'; -import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart'; -import 'package:eatvio_app/core/firebase_api.dart'; -import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; -import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; -import 'package:eatvio_app/core/views/repository/views_repository.dart'; -import 'package:eatvio_app/core/views/service/views_service.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/core/di/app_config_registration.dart'; +import 'package:eatvio_app/core/di/cubit_registrations.dart'; +import 'package:eatvio_app/core/di/external_registrations.dart'; +import 'package:eatvio_app/core/di/repository_registrations.dart'; +import 'package:eatvio_app/core/di/service_registrations.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; -import 'package:eatvio_app/core/websockets/ws_service.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; -import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; -import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; -import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart'; -import 'package:eatvio_app/features/auth/services/auth_service.dart'; -import 'package:eatvio_app/features/auth/services/reset_password_service.dart'; -import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart'; -import 'package:eatvio_app/features/cancel_plan_bottom_sheet/cubit/cancel_plan_cubit.dart'; -import 'package:eatvio_app/features/cancel_plan_bottom_sheet/repository/cancel_plan_repository.dart'; -import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan_service.dart'; -import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart'; -import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart'; -import 'package:eatvio_app/features/complaining/services/complaining_service.dart'; -import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; -import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart'; -import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart'; -import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart'; -import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; -import 'package:eatvio_app/features/consultations/services/consultation_service.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart'; -import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart'; -import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; -import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart'; -import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart'; -import 'package:eatvio_app/features/create_plan/repositories/create_plan_repository.dart'; -import 'package:eatvio_app/features/create_plan/services/create_plan_service.dart'; -import 'package:eatvio_app/features/diary/presentation/bloc/consultant_comments_cubit/consultant_comments_cubit.dart'; -import 'package:eatvio_app/features/diets/repositories/diet_repository.dart'; -import 'package:eatvio_app/features/diets/services/diet_service.dart'; -import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; -import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; -import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart'; -import 'package:eatvio_app/features/menu/presentation/bloc/menu_cubit/menu_cubit.dart'; -import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; -import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart'; -import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart'; -import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart'; -import 'package:eatvio_app/features/menu/services/blocked_users_service.dart'; -import 'package:eatvio_app/features/menu/services/notification_settings_service.dart'; -import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; -import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart'; -import 'package:eatvio_app/features/calendar/repositories/calendar_repository.dart'; -import 'package:eatvio_app/features/calendar/services/calendar_service.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; -import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; -import 'package:eatvio_app/features/chat/services/chat_service.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart'; -import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; -import 'package:eatvio_app/features/diary/repositories/diary_repository.dart'; -import 'package:eatvio_app/features/diary/services/diary_service.dart'; -import 'package:eatvio_app/features/diets/presentation/cubit/about_diet_cubit.dart'; -import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/profile_cubit/profile_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart'; -import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; -import 'package:eatvio_app/features/feed/services/feed_service.dart'; -import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart'; -import 'package:eatvio_app/features/glossary/services/glossary_service.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart'; -import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; -import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; -import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; -import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart'; -import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; -import 'package:eatvio_app/features/my_finance/services/cards_service.dart'; -import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; -import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; -import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; -import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart'; -import 'package:eatvio_app/features/notifications/service/notifications_service.dart'; -import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart'; -import 'package:eatvio_app/features/plans/repositories/plans_repository.dart'; -import 'package:eatvio_app/features/plans/services/plans_service.dart'; -import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart'; -import 'package:eatvio_app/features/plans/repositories/plan_repository.dart'; -import 'package:eatvio_app/features/plans/services/plan_service.dart'; -import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart'; -import 'package:eatvio_app/features/profile/presentation/bloc/sub_cubit/sub_cubit.dart'; -import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; -import 'package:eatvio_app/features/profile/services/profile_service.dart'; -import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart'; -import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart'; -import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart'; -import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart'; -import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart'; -import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; -import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart'; -import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart'; -import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart'; -import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart'; -import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart'; -import 'package:eatvio_app/features/registration/repositories/registration_repository.dart'; -import 'package:eatvio_app/features/registration/services/registration_service.dart'; -import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart'; -import 'package:eatvio_app/features/settings/repositories/service_repository.dart'; -import 'package:eatvio_app/features/settings/services/settings_service.dart'; -import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart'; import 'package:get_it/get_it.dart'; -import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans; +/// Глобальный экземпляр GetIt для внедрения зависимостей final sl = GetIt.instance; -Future init() async { - // Cubit singletons +/// Инициализация системы внедрения зависимостей +Future init({required AppBuildType buildType}) async { + // Регистрация AppConfig + AppConfigRegistration.registerAppConfig(buildType: buildType); + + // Регистрация внешних зависимостей (Dio, etc.) + ExternalRegistrations.registerAll(); + + // Регистрация сервисов + ServiceRegistrations.registerAll(); + + // Регистрация репозиториев + RepositoryRegistrations.registerAll(); + + // Регистрация Cubit'ов + CubitRegistrations.registerAll(); + + // Регистрация WebSocket менеджера sl.registerLazySingleton(() => WsManager(wsService: sl())); - sl.registerLazySingleton(() => NewMessagesCubit()); - sl.registerLazySingleton(() => NotificationsCubit(wsManager: sl(), notificationsRepository: sl())); - sl.registerLazySingleton(() => ProfileAvatarCubit(profileRepository: sl())); - sl.registerLazySingleton(() => ChatsCubit(chatRepository: sl(), wsManager: sl(), profileRepository: sl())); - - // Cubit factories - sl.registerFactory(() => FeedCubit(feedRepository: sl())); - sl.registerFactory(() => RegistrationCubit(registrationRepository: sl())); - sl.registerFactory(() => ChatRoomCubit(cardsRepository: sl(), chatRepository: sl(), wsManager: sl())); - sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl())); - sl.registerFactory(() => CalendarCubit(calendarRepository: sl())); - sl.registerFactory(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl())); - sl.registerFactory(() => profile_plans.PlansCubit(plansRepository: sl())); - sl.registerFactory(() => DietsCubit(feedRepository: sl(), dietsRepository: sl())); - sl.registerFactory(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl())); - sl.registerFactory(() => PlansCubit(feedRepository: sl(), planRepository: sl())); - sl.registerFactory(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl())); - sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl())); - sl.registerFactory( - () => DiaryCubit( - chatRepository: sl(), - diaryRepository: sl(), - glossaryRepository: sl(), - profileRepository: sl(), - dietRepository: sl(), - settingsRepository: sl(), - ), - ); - sl.registerFactory(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl())); - sl.registerFactory(() => BlogPostCreateCubit(blogPostRepository: sl())); - sl.registerFactory(() => SubCubit(profileRepository: sl())); - sl.registerFactory(() => CreateDietCubit(profileRepository: sl(), createDietRepository: sl(), glossaryRepository: sl())); - sl.registerFactory(() => CreatePlanCubit(createPlanRepository: sl())); - sl.registerFactory(() => FeedProfileCubit(profileRepository: sl())); - sl.registerFactory(() => AboutPlanCubit(planRepository: sl())); - sl.registerFactory(() => CreateRecipeCubit(recipeRepository: sl(), glossaryRepository: sl())); - sl.registerFactory(() => MyDietsCubit(myDietsRepository: sl(), createDietRepository: sl())); - sl.registerFactory(() => MyPlansCubit(myPlansRepository: sl())); - sl.registerFactory(() => UserDetailsCubit(profileRepository: sl())); - sl.registerFactory(() => ConsultantsCubit(feedRepository: sl())); - sl.registerFactory(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl())); - sl.registerFactory(() => AuthCubit(authRepository: sl())); - sl.registerFactory(() => ComplainingCubit(repository: sl())); - sl.registerFactory(() => UserCardsCubit(cardsRepository: sl())); - sl.registerFactory(() => OrderInfoCubit(chatRepository: sl())); - sl.registerFactory(() => ConsultationsCubit(consultationsRepository: sl())); - sl.registerFactory(() => ConsultantCommentsCubit(diaryRepository: sl())); - sl.registerFactory(() => CancelPlanCubit(cancelPlanRepository: sl())); - sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl())); - sl.registerFactory(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl())); - sl.registerFactory(() => ConsultationsArchiveCubit(consultationsRepository: sl())); - sl.registerFactory(() => ConsultantFinancesCubit(consultationsRepository: sl())); - sl.registerFactory(() => UserPurchasesCubit(consultationsRepository: sl())); - sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); - sl.registerFactory(() => BlogPostCubit(sl())); - sl.registerFactory(() => BlockedUsersCubit(blockedUsersRepository: sl())); - sl.registerFactory(() => NotificationSettingsCubit(notificationSettingsRepository: sl())); - sl.registerFactory(() => ConsultantRatingCubit(sl())); - sl.registerFactory(() => ViewsCubit(viewsRepository: sl())); - sl.registerFactory(() => MenuCubit(sl())); - - // Repository - sl.registerLazySingleton(() => ChatRepository(chatService: sl())); - sl.registerLazySingleton(() => FeedRepository(feedService: sl())); - sl.registerLazySingleton(() => RegistrationRepository(registrationService: sl())); - sl.registerLazySingleton(() => CalendarRepository(calendarService: sl())); - sl.registerLazySingleton(() => ProfileRepository(profileService: sl())); - sl.registerLazySingleton(() => RecipeRepository(recipeService: sl())); - sl.registerLazySingleton(() => SettingsRepository(settingsService: sl())); - sl.registerLazySingleton(() => ConsultantRepository(consultantService: sl())); - sl.registerLazySingleton(() => DiaryRepository(diaryService: sl())); - sl.registerLazySingleton(() => GlossaryRepository(glossaryService: sl())); - sl.registerLazySingleton(() => DietRepository(dietsService: sl())); - sl.registerLazySingleton(() => BlogPostRepository(blogPostService: sl())); - sl.registerLazySingleton(() => CreateDietRepository(createDietService: sl())); - sl.registerLazySingleton(() => PlansRepository(plansService: sl())); - sl.registerLazySingleton(() => PlanRepository(planService: sl())); - sl.registerLazySingleton(() => MyDietsRepository(myDietsService: sl())); - sl.registerLazySingleton(() => MyPlansRepository(myPlansService: sl())); - sl.registerLazySingleton(() => AboutRecipeRepository(recipeService: sl())); - sl.registerLazySingleton(() => ComplainingRepository(service: sl())); - sl.registerLazySingleton(() => AuthRepository(authService: sl())); - sl.registerLazySingleton(() => CardsRepository(cardsService: sl())); - sl.registerLazySingleton(() => ConsultationsRepository(consultationService: sl())); - sl.registerLazySingleton(() => CancelPlanRepository(cancelService: sl())); - sl.registerLazySingleton(() => CreatePlanRepository(createPlanService: sl())); - sl.registerLazySingleton(() => NotificationsRepository(notificationsService: sl())); - sl.registerFactory(() => MyRecipesRepository(myRecipesService: sl())); - sl.registerFactory(() => ResetPasswordRepository(resetPasswordService: sl())); - sl.registerLazySingleton(() => BlockedUsersRepository(blockedUserService: sl())); - sl.registerLazySingleton(() => CommentsRepository(service: sl())); - sl.registerLazySingleton(() => NotificationSettingsRepository(sl())); - sl.registerFactory(() => ConsultantRatingRepository(sl())); - sl.registerFactory(() => ViewsRepository(viewsService: sl())); - - // Services - sl.registerLazySingleton(() => ChatService(sl())); - sl.registerLazySingleton(() => FeedService(sl())); - sl.registerLazySingleton(() => RegistrationService(sl())); - sl.registerLazySingleton(() => CalendarService(sl())); - sl.registerLazySingleton(() => ProfileService(sl())); - sl.registerLazySingleton(() => CreateRecipeService(sl())); - sl.registerLazySingleton(() => SettingsService(sl())); - sl.registerLazySingleton(() => ConsultantService(sl())); - sl.registerLazySingleton(() => DiaryService(sl())); - sl.registerLazySingleton(() => GlossaryService(sl())); - sl.registerLazySingleton(() => DietService(sl())); - sl.registerLazySingleton(() => BlogPostService(sl())); - sl.registerLazySingleton(() => CreateDietService(sl())); - sl.registerLazySingleton(() => PlansService(sl())); - sl.registerLazySingleton(() => PlanService(sl())); - sl.registerLazySingleton(() => MyDietsService(sl())); - sl.registerLazySingleton(() => MyPlansService(sl())); - sl.registerLazySingleton(() => WsService(sl())); - sl.registerLazySingleton(() => AboutRecipeService(sl())); - sl.registerLazySingleton(() => ComplainingService(sl())); - sl.registerLazySingleton(() => AuthService(sl())); - sl.registerLazySingleton(() => CardsService(sl())); - sl.registerLazySingleton(() => ConsultationService(sl())); - sl.registerLazySingleton(() => CancelPlanService(sl())); - sl.registerLazySingleton(() => CreatePlanService(sl())); - sl.registerLazySingleton(() => NotificationsService(sl())); - sl.registerLazySingleton(() => FirebaseApi()); - sl.registerLazySingleton(() => ResetPasswordService(sl())); - sl.registerLazySingleton(() => BlockedUserService(sl())); - sl.registerLazySingleton(() => CommentsService(sl())); - sl.registerLazySingleton(() => MyRecipesService(sl())); - sl.registerLazySingleton(() => ConsultantRatingService(sl())); - sl.registerLazySingleton(() => NotificationSettingsService(sl())); - sl.registerLazySingleton(() => ViewsService(sl())); - - // External - sl.registerLazySingleton( - () => - Dio(BaseOptions(baseUrl: baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'})) - ..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]), - ); } diff --git a/lib/core/routing/auth_routes.dart b/lib/core/routing/auth_routes.dart new file mode 100644 index 00000000..b94cc2c8 --- /dev/null +++ b/lib/core/routing/auth_routes.dart @@ -0,0 +1,47 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/register_via_social_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart'; +import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart'; +import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart'; +import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart'; +import 'package:go_router/go_router.dart'; + +/// Маршруты для аутентификации +class AuthRoutes { + /// Создание маршрутов аутентификации + static List createRoutes() { + return [ + GoRoute( + path: RouteConstants.authScreen, + name: 'authScreen', + builder: (context, state) => const AuthScreen(), + routes: [ + GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), + GoRoute( + path: RouteConstants.forgetPasswordScreen, + name: 'forgetPasswordScreen', + builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool), + ), + GoRoute(path: RouteConstants.scanQRCodeScreen, name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()), + ], + ), + GoRoute( + path: RouteConstants.verifyScreen, + name: 'verifyScreen', + builder: (context, state) => VerifyScreen(passKey: state.extra as String), + ), + GoRoute(path: RouteConstants.verifyEmailScreen, name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), + GoRoute( + path: RouteConstants.registerViaSocialScreen, + name: 'registerViaSocialScreen', + builder: (context, state) { + final extra = state.extra as Map?; + return RegisterViaSocialScreen(authResult: extra?['authResult'] as AuthResultEntity?, provider: extra?['provider'] as String?); + }, + ), + ]; + } +} diff --git a/lib/core/routing/content_routes.dart b/lib/core/routing/content_routes.dart new file mode 100644 index 00000000..0556fab6 --- /dev/null +++ b/lib/core/routing/content_routes.dart @@ -0,0 +1,78 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; +import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart'; +import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.dart'; +import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; +import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart'; +import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart'; +import 'package:go_router/go_router.dart'; + +/// Маршруты для контента +class ContentRoutes { + /// Создание маршрутов контента + static List createRoutes() { + return [ + GoRoute( + path: RouteConstants.aboutPlanScreen, + name: 'aboutPlanScreen', + builder: (context, state) { + final slug = state.uri.queryParameters['slug']; + if (slug != null) { + return AboutPlanScreen(slug: slug, navigateToComments: false, index: null); + } + + final data = state.extra as Map; + return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + }, + ), + GoRoute( + path: RouteConstants.aboutRecipesScreen, + name: 'aboutRecipesScreen', + builder: (context, state) { + final slug = state.uri.queryParameters['slug']; + if (slug != null) { + return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null); + } + + final data = state.extra as Map; + return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + }, + ), + GoRoute( + path: RouteConstants.aboutDietScreen, + name: 'aboutDietScreen', + builder: (context, state) { + final slug = state.uri.queryParameters['slug']; + if (slug != null) { + return AboutDietScreen(id: null, slug: slug, index: null, navigateToComments: false); + } + final data = state.extra as Map; + return AboutDietScreen( + id: data['id'] as int?, + slug: data['slug'] as String?, + index: data['index'] as int?, + navigateToComments: data['navigateToComments'] as bool, + ); + }, + ), + GoRoute( + path: RouteConstants.blogPostCreateScreen, + name: 'blogPostCreateScreen', + builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null), + ), + GoRoute( + path: RouteConstants.blogPostScreen, + name: 'blogPostScreen', + builder: (context, state) { + final id = state.uri.queryParameters['id']; + final parsedId = int.tryParse(id ?? ''); + if (parsedId != null) { + return BlogPostScreen(id: parsedId); + } + + return BlogPostScreen(id: state.extra as int); + }, + ), + ]; + } +} diff --git a/lib/core/routing/main_navigation_routes.dart b/lib/core/routing/main_navigation_routes.dart new file mode 100644 index 00000000..818f625c --- /dev/null +++ b/lib/core/routing/main_navigation_routes.dart @@ -0,0 +1,94 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; +import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; +import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart'; +import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'dart:developer'; + +/// Маршруты для основной навигации +class MainNavigationRoutes { + /// Создание маршрутов основной навигации + static List createRoutes() { + return [ + ShellRoute( + builder: (context, state, child) { + return BlocBuilder( + builder: (BuildContext context, ProfileAvatarState state) { + if (state is ProfileAvatarStateLoaded) { + return BottomNavigationBarWidget(profileModel: state.profile, child: child); + } + return const SizedBox.shrink(); + }, + ); + }, + routes: [ + GoRoute( + path: RouteConstants.myProfileScreen, + name: 'myProfileScreen', + pageBuilder: (context, state) { + return const NoTransitionPage(child: ProfileScreen()); + }, + ), + GoRoute( + path: RouteConstants.feedScreen, + name: 'feedScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()), + redirect: (context, state) async { + try { + await sl().initialize(); + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + return null; + }, + ), + GoRoute( + path: RouteConstants.diaryScreen, + name: 'diaryScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), + ), + GoRoute( + path: ConsultationsScreen.path, + name: ConsultationsScreen.name, + pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()), + ), + GoRoute( + path: RouteConstants.chatSelectScreen, + name: 'chatSelectScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), + ), + GoRoute( + path: RouteConstants.notificationsScreen, + name: 'notificationsScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()), + ), + GoRoute( + path: RouteConstants.menuScreen, + name: 'menuScreen', + pageBuilder: (context, state) => NoTransitionPage( + child: BlocBuilder( + builder: (BuildContext context, ProfileAvatarState state) { + if (state is ProfileAvatarStateLoaded) { + return MenuScreen(profileModel: state.profile); + } + return const SizedBox.shrink(); + }, + ), + ), + ), + ], + ), + ]; + } +} diff --git a/lib/core/routing/other_routes.dart b/lib/core/routing/other_routes.dart new file mode 100644 index 00000000..7b5017df --- /dev/null +++ b/lib/core/routing/other_routes.dart @@ -0,0 +1,229 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; +import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; +import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; +import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; +import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; +import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart'; +import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart'; +import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; +import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; +import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; +import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'; +import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart'; +import 'package:eatvio_app/features/support/presentation/pages/support_screen.dart'; +import 'package:eatvio_app/features/update_screen/presentation/update_screen.dart'; +import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; +import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:go_router/go_router.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:version/version.dart'; + +/// Остальные маршруты приложения +class OtherRoutes { + /// Создание остальных маршрутов + static List createRoutes() { + return [ + // Welcome and update screens + GoRoute( + path: RouteConstants.welcomeScreen, + name: 'welcomeScreen', + builder: (context, state) => const WelcomeScreen(), + redirect: (context, state) async { + try { + final sharedPreferences = await SharedPreferences.getInstance(); + final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false; + await sl().registerPushToken(); + if (welcomeScreenWasShowed == false) { + return null; + } else if (await TokenStorage.haveTokenInStorage()) { + return RouteConstants.feedScreen; + } else { + return RouteConstants.authScreen; + } + } catch (e) { + return RouteConstants.feedScreen; + } + }, + ), + GoRoute( + path: RouteConstants.updateScreen, + name: 'updateScreen', + builder: (context, state) => const UpdateScreen(), + redirect: (context, state) async { + try { + final versions = (await sl().get('${sl().baseUrl}/api/app_versions')).data; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + String version = packageInfo.version; + Version currentVersion = Version.parse(version); + Version latestVersion = Version.parse(versions['min_version']); + if (latestVersion > currentVersion) { + return null; + } else { + return RouteConstants.welcomeScreen; + } + } catch (e) { + return RouteConstants.welcomeScreen; + } + }, + ), + + // Archive routes + GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()), + GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()), + GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()), + + // Finance routes + GoRoute(path: RouteConstants.userFinanceScreen, name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()), + GoRoute( + path: CardSettingsScreen.path, + name: CardSettingsScreen.name, + builder: (context, state) { + final chosenPlanId = state.extra as int?; + return CardSettingsScreen(chosenPlanId: chosenPlanId); + }, + ), + + // Content creation routes + GoRoute( + path: RecipeCreateScreen.path, + name: RecipeCreateScreen.name, + builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null), + ), + GoRoute( + path: CreateDietScreen.path, + name: CreateDietScreen.name, + builder: (context, state) { + final diet = state.extra as Diet?; + return CreateDietScreen(diet: diet); + }, + ), + GoRoute( + path: DietPreviewScreen.path, + name: DietPreviewScreen.name, + builder: (context, state) { + final data = state.extra as Diet; + return DietPreviewScreen(diet: data); + }, + ), + GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', builder: (context, state) => const CreatePlanScreen()), + GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()), + GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()), + GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()), + + // Profile routes + GoRoute( + path: RouteConstants.profileScreen, + name: 'profileScreen', + builder: (context, state) => ProfileScreen(author: state.extra as Author), + ), + GoRoute( + path: RouteConstants.subsScreen, + name: 'subsScreen', + builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType), + ), + + // Chat routes + GoRoute( + path: RouteConstants.chatScreen, + name: 'chatScreen', + builder: (context, state) { + final data = state.extra as Map; + final uuid = data['uuid'] as String; + final sendInitialMessage = data['sendInitialMessage'] as bool?; + return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage); + }, + ), + GoRoute( + path: RouteConstants.emptyChatScreen, + name: 'emptyChatScreen', + builder: (context, state) => EmptyChatScreen(author: state.extra as Author), + ), + + // Consultant routes + GoRoute(path: RouteConstants.becomeConsultantScreen, name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()), + GoRoute( + path: RouteConstants.stepperBecomeConsultantScreen, + name: 'stepperBecomeConsultantScreen', + builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String), + ), + GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), + GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()), + GoRoute( + path: RouteConstants.photoViewScreen, + name: 'photoViewScreen', + builder: (context, state) => PhotoViewScreen(imageUrls: state.extra as List, index: int.parse(state.pathParameters['index']!)), + ), + + // Other routes + GoRoute(path: RouteConstants.glossaryScreen, name: 'glossaryScreen', builder: (context, state) => GlossaryScreen(passKey: state.extra as String)), + GoRoute(path: RouteConstants.blockedUsersScreen, name: 'blockedUsersScreen', builder: (context, state) => const BlockedUsersScreen()), + GoRoute( + path: RouteConstants.notificationsSettingsScreen, + name: 'notificationsSettingsScreen', + builder: (context, state) => const NotificationsSettingsScreen(), + ), + GoRoute( + path: UserDetailsScreen.path, + name: UserDetailsScreen.name, + builder: (context, state) => UserDetailsScreen(slug: state.extra as String), + ), + GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()), + GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()), + GoRoute( + path: InfoByOrderScreen.path, + name: InfoByOrderScreen.name, + builder: (context, state) { + final data = state.extra as Map; + final planId = data['planId'] as int; + final successfulPayment = data['successfulPayment'] as bool?; + return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment); + }, + ), + GoRoute( + path: PaymentScreen.path, + name: PaymentScreen.name, + builder: (context, state) { + final data = state.extra as Map; + final planId = data['planId'] as int; + final url = data['url'] as String; + return PaymentScreen(url: url, planId: planId); + }, + ), + GoRoute( + path: UserDiaryScreen.path, + name: UserDiaryScreen.name, + builder: (context, state) { + final data = state.extra as Map; + final diaryId = data['diaryId'] as int; + final currentPlanDay = data['currentDiaryDay'] as int; + return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay); + }, + ), + ]; + } +} diff --git a/lib/core/routing/route_constants.dart b/lib/core/routing/route_constants.dart new file mode 100644 index 00000000..1702b425 --- /dev/null +++ b/lib/core/routing/route_constants.dart @@ -0,0 +1,85 @@ +/// Константы маршрутов приложения +class RouteConstants { + // Auth routes + static const String authScreen = '/auth-screen'; + static const String registrationScreen = 'registration-screen'; + static const String forgetPasswordScreen = 'forget-password-screen'; + static const String scanQRCodeScreen = 'scan-qr-code-screen'; + static const String verifyScreen = '/verify-screen'; + static const String verifyEmailScreen = '/verify-email-screen'; + static const String registerViaSocialScreen = '/register-via-social-screen'; + + // Main navigation routes + static const String myProfileScreen = '/my-profile-screen'; + static const String feedScreen = '/feed-screen'; + static const String diaryScreen = '/diary-screen'; + static const String consultationsScreen = '/consultations-screen'; + static const String chatSelectScreen = '/chat-select-screen'; + static const String notificationsScreen = '/notifications-screen'; + static const String menuScreen = '/menu-screen'; + + // Content routes + static const String aboutPlanScreen = '/about-plan-screen'; + static const String aboutRecipesScreen = '/about-recipes-screen'; + static const String aboutDietScreen = '/about-diet-screen'; + static const String blogPostScreen = '/blog-post-screen'; + static const String blogPostCreateScreen = '/blog-post-create-screen'; + + // Profile routes + static const String profileScreen = '/profile-screen'; + static const String subsScreen = '/subs-screen'; + + // Settings routes + static const String settingsScreen = '/settings-screen'; + static const String generalInfoScreen = 'general-info-screen'; + static const String otherInfoScreen = 'other-info-screen'; + static const String physicalActivityScreen = 'physical-activity-screen'; + static const String physicalFormScreen = 'physical-form-screen'; + static const String pressureScreen = 'pressure-screen'; + static const String changePasswordScreen = 'change-password-screen'; + static const String changeEmailScreen = 'change-email-screen'; + + // Consultant routes + static const String becomeConsultantScreen = '/become-consultant-screen'; + static const String stepperBecomeConsultantScreen = '/stepper-become-consultant-screen'; + static const String consultantRatingScreen = '/consultant-rating-screen'; + + // Other routes + static const String welcomeScreen = '/welcome-screen'; + static const String updateScreen = '/update-screen'; + static const String calendarScreen = '/calendar-screen'; + static const String supportScreen = '/support-screen'; + static const String photoViewScreen = '/photo-view-screen'; + static const String chatScreen = '/chat-screen'; + static const String emptyChatScreen = '/empty-chat-screen'; + static const String glossaryScreen = '/glossary-screen'; + static const String blockedUsersScreen = '/blocked-users-screen'; + static const String notificationsSettingsScreen = '/notifications-settings-screen'; + + // Finance routes + static const String userFinanceScreen = '/user-finance-screen'; + static const String cardSettingsScreen = '/card-settings-screen'; + static const String orderInfoScreen = '/order-info-screen'; + static const String paymentScreen = '/payment-screen'; + + // Content creation routes + static const String recipeCreateScreen = '/recipe-create-screen'; + static const String createDietScreen = '/create-diet-screen'; + static const String dietPreviewScreen = '/diet-preview-screen'; + static const String createPlanScreen = '/create-plan-screen'; + static const String myDietsScreen = '/my-diets-screen'; + static const String myPlansScreen = '/my-plans-screen'; + static const String myRecipesScreen = '/my-recipes-screen'; + + // User details + static const String userDetailsScreen = '/user-details-screen'; + static const String userDiaryScreen = '/user-diary-screen'; + + // Dev tools + static const String devToolsScreen = '/dev-tools-screen'; + + // Archive routes + static const String consultationsArchiveScreen = '/consultations-archive-screen'; + static const String consultantFinancesScreen = '/consultant-finances-screen'; + static const String userPurchasesScreen = '/user-purchases-screen'; +} diff --git a/lib/core/routing/settings_routes.dart b/lib/core/routing/settings_routes.dart new file mode 100644 index 00000000..8316617c --- /dev/null +++ b/lib/core/routing/settings_routes.dart @@ -0,0 +1,62 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/change_password_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/general_info_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/physical_activity_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart'; +import 'package:go_router/go_router.dart'; + +/// Маршруты для настроек +class SettingsRoutes { + /// Создание маршрутов настроек + static List createRoutes() { + return [ + GoRoute( + path: RouteConstants.settingsScreen, + name: 'settingsScreen', + builder: (context, state) => const SettingsScreen(), + routes: [ + GoRoute( + path: RouteConstants.generalInfoScreen, + name: 'generalInfoScreen', + builder: (context, state) => GeneralInfoScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.otherInfoScreen, + name: 'otherInfoScreen', + builder: (context, state) => OtherInfoScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.physicalActivityScreen, + name: 'physicalActivityScreen', + builder: (context, state) => PhysicalActivityScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.physicalFormScreen, + name: 'physicalFormScreen', + builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.pressureScreen, + name: 'pressureScreen', + builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.changePasswordScreen, + name: 'changePasswordScreen', + builder: (context, state) => ChangePasswordScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.changeEmailScreen, + name: 'changeEmailScreen', + builder: (context, state) => ChangeEmailScreen(settingsEntity: state.extra as SettingsEntity), + ), + ], + ), + ]; + } +} diff --git a/lib/core/utils/toasts.dart b/lib/core/utils/toasts.dart index 3d4fda0d..4683cb86 100644 --- a/lib/core/utils/toasts.dart +++ b/lib/core/utils/toasts.dart @@ -38,4 +38,16 @@ void showSuccessToast(String message) { textColor: appTheme.whiteA700, fontSize: 16.0, ); -} \ No newline at end of file +} + +void showInternetConnectionToast() { + Fluttertoast.showToast( + msg: 'Проверьте подключение к интернету', + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.TOP, + timeInSecForIosWeb: 3, + backgroundColor: appTheme.red, + textColor: Colors.white, + fontSize: 16.0, + ); +} diff --git a/lib/core/utils/validation_utils.dart b/lib/core/utils/validation_utils.dart new file mode 100644 index 00000000..1add6cda --- /dev/null +++ b/lib/core/utils/validation_utils.dart @@ -0,0 +1,87 @@ +import 'package:eatvio_app/core/constants/app_constants.dart'; + +/// Утилиты для валидации +class ValidationUtils { + /// Валидация email + static bool isValidEmail(String email) { + if (email.isEmpty) return false; + final regex = RegExp(AppConstants.emailRegex); + return regex.hasMatch(email); + } + + /// Валидация телефона + static bool isValidPhone(String phone) { + if (phone.isEmpty) return false; + final regex = RegExp(AppConstants.phoneRegex); + return regex.hasMatch(phone); + } + + /// Валидация URL + static bool isValidUrl(String url) { + if (url.isEmpty) return false; + final regex = RegExp(AppConstants.urlRegex); + return regex.hasMatch(url); + } + + /// Валидация длины текста + static bool isValidTextLength(String text, {int? maxLength}) { + final length = maxLength ?? AppConstants.maxTextLength; + return text.length <= length; + } + + /// Валидация длины заголовка + static bool isValidTitleLength(String title) { + return title.isNotEmpty && title.length <= AppConstants.maxTitleLength; + } + + /// Валидация длины описания + static bool isValidDescriptionLength(String description) { + return description.length <= AppConstants.maxDescriptionLength; + } + + /// Валидация пароля + static bool isValidPassword(String password) { + // Минимум 8 символов, хотя бы одна буква и одна цифра + if (password.length < 8) return false; + final hasLetter = RegExp(r'[a-zA-Z]').hasMatch(password); + final hasDigit = RegExp(r'[0-9]').hasMatch(password); + return hasLetter && hasDigit; + } + + /// Валидация имени пользователя + static bool isValidUsername(String username) { + // От 3 до 20 символов, только буквы, цифры и подчеркивания + if (username.length < 3 || username.length > 20) return false; + final regex = RegExp(r'^[a-zA-Z0-9_]+$'); + return regex.hasMatch(username); + } + + /// Получение сообщения об ошибке валидации + static String? getValidationErrorMessage(String? value, ValidationType type) { + if (value == null || value.isEmpty) { + return 'Поле не может быть пустым'; + } + + switch (type) { + case ValidationType.email: + return isValidEmail(value) ? null : 'Введите корректный email'; + case ValidationType.phone: + return isValidPhone(value) ? null : 'Введите корректный номер телефона'; + case ValidationType.url: + return isValidUrl(value) ? null : 'Введите корректный URL'; + case ValidationType.password: + return isValidPassword(value) ? null : 'Пароль должен содержать минимум 8 символов, включая буквы и цифры'; + case ValidationType.username: + return isValidUsername(value) ? null : 'Имя пользователя должно содержать от 3 до 20 символов (буквы, цифры, подчеркивания)'; + case ValidationType.title: + return isValidTitleLength(value) ? null : 'Заголовок не может быть пустым и должен содержать не более ${AppConstants.maxTitleLength} символов'; + case ValidationType.description: + return isValidDescriptionLength(value) ? null : 'Описание не может содержать более ${AppConstants.maxDescriptionLength} символов'; + case ValidationType.text: + return isValidTextLength(value) ? null : 'Текст не может содержать более ${AppConstants.maxTextLength} символов'; + } + } +} + +/// Типы валидации +enum ValidationType { email, phone, url, password, username, title, description, text } diff --git a/lib/core/websockets/ws_manager.dart b/lib/core/websockets/ws_manager.dart index 528f2ae9..24430f6c 100644 --- a/lib/core/websockets/ws_manager.dart +++ b/lib/core/websockets/ws_manager.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/websockets/ws_service.dart'; @@ -111,7 +112,7 @@ class WsManager { _privateNotificationChannel = client?.privateChannel( '$newNotificationsChannelName.$userId', authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( - authorizationEndpoint: Uri.parse('$baseUrl/broadcasting/auth'), + authorizationEndpoint: Uri.parse('${sl().baseUrl}/broadcasting/auth'), headers: { 'Authorization': 'Bearer $token', }, @@ -124,7 +125,7 @@ class WsManager { _privateMessagesChannel = client?.privateChannel( '$newMessagesChannelName.$userId', authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( - authorizationEndpoint: Uri.parse('$baseUrl/broadcasting/auth'), + authorizationEndpoint: Uri.parse('${sl().baseUrl}/broadcasting/auth'), headers: { 'Authorization': 'Bearer $token', }, @@ -137,7 +138,7 @@ class WsManager { _privateDirectChatChannel = client?.privateChannel( '$directChatChannelName.$chatId', authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel( - authorizationEndpoint: Uri.parse('$baseUrl/broadcasting/auth'), + authorizationEndpoint: Uri.parse('${sl().baseUrl}/broadcasting/auth'), headers: { 'Authorization': 'Bearer $token', }, diff --git a/lib/core/widgets/custom_popup.dart b/lib/core/widgets/custom_popup.dart index b5934f4d..dbe0090b 100644 --- a/lib/core/widgets/custom_popup.dart +++ b/lib/core/widgets/custom_popup.dart @@ -1,4 +1,4 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -86,7 +86,7 @@ class CustomPopup extends StatelessWidget { if (isAuthor) { addItem('assets/icons/copy2.svg', 'Скопировать ссылку', () { - Clipboard.setData(ClipboardData(text: '$baseUrl/ru/$path')); + Clipboard.setData(ClipboardData(text: '${sl().baseUrl}/ru/$path')); showInfoToast(s.link_was_copied); }); addItem('assets/icons/share2.svg', 'Поделиться', () { @@ -94,7 +94,7 @@ class CustomPopup extends StatelessWidget { SharePlus.instance.share( ShareParams( uri: Uri.parse( - '$baseUrl/ru/$path', + '${sl().baseUrl}/ru/$path', ), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), @@ -140,7 +140,7 @@ class CustomPopup extends StatelessWidget { }); } else { addItem('assets/icons/copy2.svg', 'Скопировать ссылку', () { - Clipboard.setData(ClipboardData(text: '$baseUrl/ru/$path')); + Clipboard.setData(ClipboardData(text: '${sl().baseUrl}/ru/$path')); showInfoToast(s.link_was_copied); }); addItem('assets/icons/share2.svg', 'Поделиться', () { @@ -148,7 +148,7 @@ class CustomPopup extends StatelessWidget { SharePlus.instance.share( ShareParams( uri: Uri.parse( - '$baseUrl/ru/$path', + '${sl().baseUrl}/ru/$path', ), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), diff --git a/lib/features/auth/entities/auth_result_entity.dart b/lib/features/auth/entities/auth_result_entity.dart new file mode 100644 index 00000000..a8d08ff4 --- /dev/null +++ b/lib/features/auth/entities/auth_result_entity.dart @@ -0,0 +1,52 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:vk_id/vk_id.dart'; + +part 'auth_result_entity.freezed.dart'; + +@freezed +abstract class AuthResultEntity with _$AuthResultEntity { + const factory AuthResultEntity({ + required String accessToken, + required String userId, + required String clientId, + String? refreshToken, + String? firstName, + String? lastName, + String? email, + String? phone, + GenderEnum? gender, + String? birthDate, + }) = _AuthResultEntity; +} + +enum GenderEnum { + male, + female, + notSet; + + static GenderEnum? fromVkGender(VkSex? vkGender) { + switch (vkGender) { + case VkSex.male: + return GenderEnum.male; + case VkSex.female: + return GenderEnum.female; + case VkSex.notStated: + return GenderEnum.notSet; + case null: + return null; + } + } + + static GenderEnum? fromString(String? gender) { + switch (gender) { + case 'male': + return GenderEnum.male; + case 'female': + return GenderEnum.female; + case 'notSet': + return GenderEnum.notSet; + default: + return null; + } + } +} diff --git a/lib/features/auth/models/oauth_login_request.dart b/lib/features/auth/models/oauth_login_request.dart new file mode 100644 index 00000000..97145a4d --- /dev/null +++ b/lib/features/auth/models/oauth_login_request.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'oauth_login_request.freezed.dart'; +part 'oauth_login_request.g.dart'; + +@freezed +abstract class OAuthLoginRequest with _$OAuthLoginRequest { + const factory OAuthLoginRequest({ + @JsonKey(name: 'client_id') required String clientId, + @JsonKey(name: 'access_token') required String accessToken, + @JsonKey(name: 'refresh_token') required String refreshToken, + required String provider, + }) = _OAuthLoginRequest; + + factory OAuthLoginRequest.fromJson(Map json) => _$OAuthLoginRequestFromJson(json); +} diff --git a/lib/features/auth/models/oauth_model.dart b/lib/features/auth/models/oauth_model.dart new file mode 100644 index 00000000..0156e7aa --- /dev/null +++ b/lib/features/auth/models/oauth_model.dart @@ -0,0 +1,25 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'oauth_model.freezed.dart'; +part 'oauth_model.g.dart'; + +@freezed +abstract class OAuthModel with _$OAuthModel { + const factory OAuthModel({ + @JsonKey(name: 'access_token') required String accessToken, + @JsonKey(name: 'refresh_token') required String refreshToken, + @JsonKey(name: 'first_name') required String firstName, + @JsonKey(name: 'last_name') required String lastName, + @JsonKey(name: 'date_of_birth') required String dateOfBirth, + @JsonKey(name: 'client_id') required String clientId, + required String provider, + required String id, + required String email, + required String phone, + required String gender, + required int weight, + required int height, + }) = _OAuthModel; + + factory OAuthModel.fromJson(Map json) => _$OAuthModelFromJson(json); +} diff --git a/lib/features/auth/models/yandex_profile_model.dart b/lib/features/auth/models/yandex_profile_model.dart new file mode 100644 index 00000000..9bb84998 --- /dev/null +++ b/lib/features/auth/models/yandex_profile_model.dart @@ -0,0 +1,30 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'yandex_profile_model.freezed.dart'; +part 'yandex_profile_model.g.dart'; + +@freezed +abstract class YandexProfileModel with _$YandexProfileModel { + factory YandexProfileModel({ + required String id, + required String login, + required String birthday, + @JsonKey(name: 'client_id') required String clientId, + @JsonKey(name: 'display_name') required String displayName, + @JsonKey(name: 'real_name') required String realName, + @JsonKey(name: 'first_name') required String firstName, + @JsonKey(name: 'last_name') required String lastName, + @JsonKey(name: 'sex') required String sex, + @JsonKey(name: 'default_email') required String defaultEmail, + @JsonKey(name: 'default_phone') required YandexPhoneData defaultPhone, + }) = _YandexProfileModel; + + factory YandexProfileModel.fromJson(Map json) => _$YandexProfileModelFromJson(json); +} + +@freezed +abstract class YandexPhoneData with _$YandexPhoneData { + factory YandexPhoneData({required int id, required String number}) = _YandexPhoneData; + + factory YandexPhoneData.fromJson(Map json) => _$YandexPhoneDataFromJson(json); +} diff --git a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart index 7b58a4b0..6f1a59f5 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart @@ -25,19 +25,4 @@ class AuthCubit extends Cubit { return false; } } - - Future getTokenBySocial({required String provider, required String accessToken}) async { - emit(const AuthState.loading()); - try { - final token = (await authRepository.getTokenBySocial(provider: provider, accessToken: accessToken)).token; - emit(AuthState.loaded(token: token)); - await TokenStorage.saveTokenInStorage(token); - return true; - } catch (e) { - log(e.toString()); - showErrorToast('Не удалось войти через $provider'); - emit(AuthState.error(errorMessage: '$e')); - return false; - } - } } diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart new file mode 100644 index 00000000..52a0e73b --- /dev/null +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart @@ -0,0 +1,101 @@ +import 'dart:developer'; +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; +import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class OAuthCubit extends Cubit { + final AuthRepository authRepository; + + OAuthCubit({required this.authRepository}) : super(const OAuthState()); + + Future signInViaSocialMedia({required String provider, required String accessToken, required String refreshToken, required String clientId}) async { + try { + emit(const OAuthState.loading()); + + final tokenModel = await authRepository.signInViaSocialMedia( + provider: provider, + accessToken: accessToken, + refreshToken: refreshToken, + clientId: clientId, + ); + + log('OAuthCubit: Got token model: ${tokenModel.token}'); + TokenStorage.saveTokenInStorage(tokenModel.token); + emit(OAuthState.loaded(token: tokenModel.token)); + } catch (e) { + log('OAuthCubit: Error occurred: $e'); + emit(OAuthState.error(errorMessage: e.toString())); + } + } + + Future?> signUpViaSocialMedia({ + required String provider, + required String accessToken, + required String id, + required String refreshToken, + required String firstName, + required String lastName, + required String email, + required String phone, + required String dateOfBirth, + required String gender, + required int weight, + required int height, + required String clientId, + }) async { + try { + emit(const OAuthState.loading()); + final token = await authRepository.signUpViaSocialMedia( + provider: provider, + accessToken: accessToken, + id: id, + refreshToken: refreshToken, + firstName: firstName, + lastName: lastName, + email: email, + phone: phone, + dateOfBirth: dateOfBirth, + gender: gender, + weight: weight, + height: height, + clientId: clientId, + ); + TokenStorage.saveTokenInStorage(token); + emit(OAuthState.loaded(token: token)); + } on DioException catch (e) { + emit(OAuthState.error(errorMessage: e.toString())); + final errors = e.response?.data['errors'] as Map?; + final emailErrors = errors?['email'] as List?; + final phoneErrors = errors?['phone'] as List?; + final dateOfBirth = errors?['date_of_birth'] as List?; + final errorsList = []; + if (emailErrors != null && emailErrors.isNotEmpty) { + if (emailErrors.contains('Некорректный формат email')) { + errorsList.add(RegistrationValidationErrors.incorrectEmailFormat); + } + if (emailErrors.contains('Такой Эл. почта уже существует.')) { + errorsList.add(RegistrationValidationErrors.emailExist); + } + } + if (phoneErrors != null && phoneErrors.isNotEmpty) { + if (phoneErrors.contains('Такой phone уже существует.')) { + errorsList.add(RegistrationValidationErrors.phoneExist); + } + } + if (dateOfBirth != null && dateOfBirth.isNotEmpty) { + if (dateOfBirth.contains('Некорректный формат даты рождения')) { + errorsList.add(RegistrationValidationErrors.incorrectDateOfBirthFormat); + } + } + if (errorsList.isNotEmpty) { + return errorsList; + } + } + + return null; + } +} + +enum RegistrationValidationErrors { incorrectEmailFormat, emailExist, phoneExist, incorrectDateOfBirthFormat } diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart new file mode 100644 index 00000000..7da9ab7c --- /dev/null +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'oauth_state.freezed.dart'; + +@freezed +abstract class OAuthState with _$OAuthState { + const factory OAuthState() = _OAuthState; + + const factory OAuthState.loading() = OAuthStateLoading; + + const factory OAuthState.loaded({required String token}) = OAuthStateLoaded; + + const factory OAuthState.error({required String errorMessage}) = OAuthStateError; +} diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index f9eeddf5..f7484a87 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -1,6 +1,7 @@ -// ignore_for_file: unused_element, duplicate_ignore - import 'dart:async'; +import 'dart:developer'; +import 'package:eatvio_app/app_keys.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; @@ -9,9 +10,14 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; +import 'package:eatvio_app/core/di/cubit_registrations.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; @@ -44,7 +50,7 @@ class _AuthScreenState extends State { late StreamSubscription keyboardSubscription; bool isFocused = false; int tapsCount = 0; - + bool _isLoading = false; @override void initState() { super.initState(); @@ -87,9 +93,142 @@ class _AuthScreenState extends State { AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); } + void _onVkPressed() async { + try { + log('VK button pressed'); + final context = AppRouter.navigatorKey.currentContext; + if (context == null) { + log('Context is null'); + return; + } + + log('Getting VK auth result'); + final authResult = await SocialAuthFacade.signInWithVkAndFetchProfile(context: context); + if (authResult == null) { + log('VK auth result is null'); + return; + } + + log('VK auth result received, trying OAuth login'); + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'vk'); + } else { + log('Context is not mounted'); + } + } catch (e) { + log('VK button error: $e'); + } + } + + void _onOkPressed() async { + try { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithOkAndFetchProfile(); + if (authResult == null) { + return; + } + + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'vk'); + } + } catch (e) { + log(e.toString()); + } + } + + void _onMailRuPressed() async { + try { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithMailRuAndFetchProfile(); + if (authResult == null) { + return; + } + + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'vk'); + } + } catch (e) { + log(e.toString()); + } + } + + void _onYandexPressed() async { + try { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + + final authResult = await SocialAuthFacade.signInWithYandex(); + if (authResult == null) return; + + if (context.mounted) { + await _tryOAuthLogin(context, authResult, 'yandex'); + } + } catch (e) { + log(e.toString()); + } + } + + Future _tryOAuthLogin(BuildContext context, AuthResultEntity authResult, String provider) async { + log('Starting OAuth login for provider: $provider'); + + try { + final oauthCubit = CubitRegistrations.sl(); + StreamSubscription? subscription; + + subscription = oauthCubit.stream.listen((state) { + log('OAuth state changed: ${state.runtimeType}'); + + if (state is OAuthStateLoaded) { + log('OAuth login successful, navigating to main screen'); + subscription?.cancel(); + _reconnectAfterAuth(); + } else if (state is OAuthStateError) { + log('OAuth login failed: ${state.errorMessage}'); + subscription?.cancel(); + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); + } + }); + + log('Calling getOAuthLogin with provider: $provider'); + await oauthCubit.signInViaSocialMedia( + provider: provider, + accessToken: authResult.accessToken, + refreshToken: authResult.refreshToken ?? '', + clientId: authResult.clientId, + ); + } catch (e) { + log('OAuth login error in catch block: $e'); + AppRouter.navigateToRegisterViaSocial(authResult: authResult, provider: provider); + } + } + + Future _reconnectAfterAuth() async { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return; + try { + await sl().clear(); + await sl().initialize(); + await sl().getMyProfile(); + await sl().getChatRoomsWithLoading(); + await sl().connectToWebSocket(); + await sl().getNotifications(); + await sl().connectToWebSocket(); + await sl().registerPushToken(); + showSuccessToast('Успешный вход'); + } catch (e) { + log(e.toString()); + } + AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); + } + @override Widget build(BuildContext context) { var shortestSide = MediaQuery.sizeOf(context).shortestSide; + final screenSize = MediaQuery.of(context).size; final bool useMobileLayout = shortestSide < 600; if (!useMobileLayout) isFocused = true; final s = S.of(context); @@ -100,9 +239,10 @@ class _AuthScreenState extends State { builder: (BuildContext context, AuthState state) { return Scaffold( appBar: AppBar( + toolbarHeight: screenSize.height * 0.15, centerTitle: true, - toolbarHeight: isFocused ? 64 : MediaQuery.of(context).size.width - 64, title: GestureDetector( + key: AppKeys.hiddenLogoButtonAuthScreen, onTap: () { tapsCount++; if (tapsCount == 7) { @@ -110,7 +250,7 @@ class _AuthScreenState extends State { GoRouter.of(context).pushNamed(DevToolsScreen.name); } }, - child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: isFocused ? 64 : MediaQuery.of(context).size.width - 64, fit: BoxFit.contain), + child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: screenSize.height * 0.20), ), ), body: SafeArea( @@ -119,6 +259,41 @@ class _AuthScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const Center(child: Text('Авторизация через')), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _SocialButton( + onTap: () => _onVkPressed(), + backgroundColor: const Color(0xFF2787F5), + tooltip: 'VK ID', + child: SvgPicture.asset('assets/icons/vk_logo.svg', height: 24, width: 24), + ), + _SocialButton( + onTap: _onOkPressed, + backgroundColor: const Color(0xFFF58220), + tooltip: 'Одноклассники', + child: SvgPicture.asset('assets/icons/ok_logo.svg', height: 24, width: 24), + ), + _SocialButton( + onTap: _onMailRuPressed, + backgroundColor: const Color(0xFF005FF9), + tooltip: 'Mail.ru', + child: SvgPicture.asset('assets/icons/mail_logo.svg', height: 24, width: 24), + ), + _SocialButton( + onTap: _onYandexPressed, + backgroundColor: const Color(0xFFFFCC00), + tooltip: 'Яндекс ID', + child: SvgPicture.asset('assets/icons/yandex_logo.svg', height: 24, width: 24), + ), + ], + ), + const SizedBox(height: 16), + const CustomDivider(), + const SizedBox(height: 8), + const SizedBox(height: 16), Text(s.email), const SizedBox(height: 4), CustomTextFormField( @@ -126,6 +301,7 @@ class _AuthScreenState extends State { textInputAction: TextInputAction.next, keyboardType: TextInputType.emailAddress, textInputType: TextInputType.emailAddress, + key: AppKeys.emailTextFieldAuthScreen, hintText: 'Введите email', ), const SizedBox(height: 16), @@ -134,6 +310,7 @@ class _AuthScreenState extends State { CustomTextFormField( obscureText: hidePassword, controller: passwordController, + key: AppKeys.passwordTextFieldAuthScreen, hintText: 'Введите пароль', suffix: IconButton( onPressed: () { @@ -168,52 +345,6 @@ class _AuthScreenState extends State { ), ], ), - // const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _SocialButton( - onTap: () => _onVkPressed(context), - backgroundColor: const Color(0xFF2787F5), - tooltip: 'VK ID', - child: SvgPicture.asset( - 'assets/icons/vk.svg', - height: 24, - width: 24, - colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - ), - _SocialButton( - onTap: _onOkPressed, - backgroundColor: const Color(0xFFF58220), - tooltip: 'Одноклассники', - child: SvgPicture.asset( - 'assets/icons/ok.svg', - height: 24, - width: 24, - colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - ), - const _SocialButton( - onTap: _onMailRuPressed, - backgroundColor: Color(0xFF005FF9), - tooltip: 'Mail.ru', - child: Text( - 'Mail', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700), - ), - ), - const _SocialButton( - onTap: _onYandexPressed, - backgroundColor: Color(0xFFFFCC00), - tooltip: 'Яндекс ID', - child: Text( - 'Yandex', - style: TextStyle(color: Colors.black, fontWeight: FontWeight.w700), - ), - ), - ], - ), const Spacer(), Center( child: RichText( @@ -232,7 +363,7 @@ class _AuthScreenState extends State { decorationColor: appTheme.blueA400, decoration: TextDecoration.underline, ), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl().baseUrl}/oferta_bs.pdf')), ), const TextSpan(text: ' и '), ], @@ -244,7 +375,7 @@ class _AuthScreenState extends State { decorationColor: appTheme.blueA400, decoration: TextDecoration.underline, ), - recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl().baseUrl}/docs/privacy.pdf')), ), ], ), @@ -257,21 +388,28 @@ class _AuthScreenState extends State { height: 48, width: MediaQuery.of(context).size.width - 88, child: FloatingActionButton.extended( + key: AppKeys.signInButtonAuthScreen, heroTag: 'authButton', highlightElevation: 0, elevation: 0, focusElevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400, - onPressed: state is AuthStateLoading + backgroundColor: (state is AuthStateLoading || _isLoading) ? Colors.black.withAlpha(50) : appTheme.blueA400, + onPressed: (state is AuthStateLoading || _isLoading) ? null : () async { + setState(() { + _isLoading = true; + }); final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text); if (isAuthenticated) { if (context.mounted) { - _reconnectWebSocket(context); + await _reconnectWebSocket(context); } } + setState(() { + _isLoading = false; + }); }, label: state is AuthStateLoading ? const CupertinoActivityIndicator() @@ -304,7 +442,7 @@ class _AuthScreenState extends State { children: [ const Text('Где взять QR-код?\n', style: TextStyle(fontSize: 18)), Text( - '1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"', + '1. Войдите на сайте ${sl().baseUrl} в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"', ), const SizedBox(height: 12), Row( @@ -367,7 +505,6 @@ class _AuthScreenState extends State { } } -// ignore: unused_element class _SocialButton extends StatelessWidget { const _SocialButton({required this.onTap, required this.child, required this.backgroundColor, required this.tooltip}); @@ -381,73 +518,19 @@ class _SocialButton extends StatelessWidget { return Tooltip( message: tooltip, child: InkWell( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(20), onTap: onTap, child: Ink( height: 48, width: (MediaQuery.of(context).size.width - 16 * 2 - 12 * 3) / 4, - decoration: BoxDecoration(color: backgroundColor, borderRadius: BorderRadius.circular(12)), + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: const Color(0xFFDBDBDB), width: 1), + ), child: Center(child: child), ), ), ); } } - -// ignore: unused_element -void _onVkPressed(BuildContext context) async { - final accessToken = await SocialAuthFacade.signInWithVk(context: context); - if (accessToken == null) return; - final isAuthenticated = await sl().getTokenBySocial(provider: 'vk', accessToken: accessToken); - if (isAuthenticated) { - if (AppRouter.navigatorKey.currentContext != null) { - // Reuse success flow - // ignore: use_build_context_synchronously - await _reconnectAfterAuth(); - } - } -} - -void _onOkPressed() async { - final accessToken = await SocialAuthFacade.signInWithOk(); - if (accessToken == null) return; - final isAuthenticated = await sl().getTokenBySocial(provider: 'ok', accessToken: accessToken); - if (isAuthenticated) { - await _reconnectAfterAuth(); - } -} - -void _onMailRuPressed() async { - final accessToken = await SocialAuthFacade.signInWithMailRu(); - if (accessToken == null) return; - final isAuthenticated = await sl().getTokenBySocial(provider: 'mailru', accessToken: accessToken); - if (isAuthenticated) { - await _reconnectAfterAuth(); - } -} - -void _onYandexPressed() async { - final accessToken = await SocialAuthFacade.signInWithYandex(); - if (accessToken == null) return; - final isAuthenticated = await sl().getTokenBySocial(provider: 'yandex', accessToken: accessToken); - if (isAuthenticated) { - await _reconnectAfterAuth(); - } -} - -Future _reconnectAfterAuth() async { - final context = AppRouter.navigatorKey.currentContext; - if (context == null) return; - try { - await sl().clear(); - await sl().initialize(); - await sl().getMyProfile(); - await sl().getChatRoomsWithLoading(); - await sl().connectToWebSocket(); - await sl().getNotifications(); - await sl().connectToWebSocket(); - await sl().registerPushToken(); - showSuccessToast('Успешный вход'); - } catch (_) {} - AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); -} diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart new file mode 100644 index 00000000..1791e24e --- /dev/null +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -0,0 +1,532 @@ +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; +import 'package:eatvio_app/core/di/cubit_registrations.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; +import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widget.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class RegisterViaSocialScreen extends StatefulWidget { + final AuthResultEntity? authResult; + final String? provider; + + const RegisterViaSocialScreen({super.key, this.authResult, this.provider}); + + @override + State createState() => _RegisterViaSocialScreenState(); +} + +class _RegisterViaSocialScreenState extends State { + final maskFormatter = MaskTextInputFormatter(mask: '+7 (###) ### ## ##', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy); + final maskFormatterForBirthday = MaskTextInputFormatter(mask: '##.##.####', filter: {"#": RegExp(r'[0-9]')}, type: MaskAutoCompletionType.lazy); + + // Контроллеры для полей ввода + final firstNameController = TextEditingController(); + final lastNameController = TextEditingController(); + final emailController = TextEditingController(); + final phoneController = TextEditingController(); + final dateOfBirthController = TextEditingController(); + final weightController = TextEditingController(); + final heightController = TextEditingController(); + bool emailExistError = false; + bool phoneExistError = false; + bool dateOfBirthError = false; + bool weightError = false; + bool heightError = false; + bool incorrectEmailFormatError = false; + + // Состояние для выбора пола + bool isMaleSelected = true; + + @override + void initState() { + super.initState(); + _initializeFieldsFromAuthResult(); + } + + Future _reconnectWebSocket(BuildContext context) async { + try { + await sl().clear(); + await sl().initialize(); + } catch (e) { + rethrow; + } + if (context.mounted) { + try { + await sl().getMyProfile(); + await sl().getChatRoomsWithLoading(); + await sl().connectToWebSocket(); + await sl().getNotifications(); + await sl().connectToWebSocket(); + await sl().registerPushToken(); + showSuccessToast('Успешная регистрация'); + } catch (e) { + rethrow; + } + } + AppRouter.appRouter.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); + } + + void _initializeFieldsFromAuthResult() { + if (widget.authResult != null) { + final authResult = widget.authResult!; + + if (authResult.firstName != null) { + firstNameController.text = authResult.firstName!; + } + if (authResult.lastName != null) { + lastNameController.text = authResult.lastName!; + } + if (authResult.email != null) { + emailController.text = authResult.email!; + } + if (authResult.phone != null) { + final cleanPhone = authResult.phone!.replaceAll(RegExp(r'[^\d]'), ''); + final normalizedPhone = cleanPhone.startsWith('8') ? '7${cleanPhone.substring(1)}' : cleanPhone; + final phoneWithCountryCode = normalizedPhone.startsWith('7') ? normalizedPhone : '7$normalizedPhone'; + final formattedPhone = maskFormatter.maskText('+$phoneWithCountryCode'); + phoneController.text = formattedPhone; + } + if (authResult.birthDate != null) { + final formattedDate = _formatBirthDate(authResult.birthDate!); + final maskedDate = maskFormatterForBirthday.maskText(formattedDate); + dateOfBirthController.text = maskedDate; + } + if (authResult.gender != null) { + isMaleSelected = authResult.gender == GenderEnum.male; + } + } + } + + String _formatBirthDate(String birthDate) { + try { + // Пытаемся распарсить различные форматы даты + DateTime? date; + + // Если дата в формате ISO (YYYY-MM-DD) + if (birthDate.contains('-')) { + date = DateTime.tryParse(birthDate); + } + // Если дата в формате DD.MM.YYYY + else if (birthDate.contains('.')) { + final parts = birthDate.split('.'); + if (parts.length == 3) { + final day = int.tryParse(parts[0]); + final month = int.tryParse(parts[1]); + final year = int.tryParse(parts[2]); + if (day != null && month != null && year != null) { + date = DateTime(year, month, day); + } + } + } + + if (date != null) { + // Форматируем в DD.MM.YYYY + final day = date.day.toString().padLeft(2, '0'); + final month = date.month.toString().padLeft(2, '0'); + final year = date.year.toString(); + return '$day.$month.$year'; + } + } catch (e) { + // Если не удалось распарсить, возвращаем исходную строку + } + + return birthDate; + } + + @override + void dispose() { + firstNameController.dispose(); + lastNameController.dispose(); + emailController.dispose(); + phoneController.dispose(); + dateOfBirthController.dispose(); + weightController.dispose(); + heightController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CubitRegistrations.sl(), + child: Scaffold( + appBar: AppBar(), + body: BlocBuilder( + builder: (context, state) { + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Center( + child: Text( + 'Регистрация', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1, color: Color(0xFF323F4B)), + ), + ), + const SizedBox(height: 24), + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Имя'), + const SizedBox(height: 4), + CustomTextFormField(controller: firstNameController, hintText: 'Введите имя', onChanged: (newValue) {}), + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Фамилия'), + const SizedBox(height: 4), + CustomTextFormField(controller: lastNameController, hintText: 'Введите фамилию', onChanged: (newValue) {}), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + const Text('Почта'), + const SizedBox(height: 4), + CustomTextFormField( + controller: emailController, + textInputType: TextInputType.emailAddress, + keyboardType: TextInputType.emailAddress, + hintText: 'Введите email', + textInputAction: TextInputAction.next, + hasError: emailExistError || incorrectEmailFormatError, + onChanged: (newValue) {}, + ), + if (emailExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такая почта уже существует.')], + if (incorrectEmailFormatError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат email')], + const SizedBox(height: 12), + const Text('Номер телефона'), + const SizedBox(height: 4), + CustomTextFormField( + textInputAction: TextInputAction.done, + hintText: '+7 (999) 999 99 99', + controller: phoneController, + keyboardType: TextInputType.number, + inputFormatters: [maskFormatter], + hasError: phoneExistError, + onChanged: (newValue) {}, + ), + if (phoneExistError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Такой номер телефона уже существует.')], + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Дата рождения'), + const SizedBox(height: 4), + CustomTextFormField( + textInputAction: TextInputAction.done, + hintText: '01.01.2000', + controller: dateOfBirthController, + keyboardType: TextInputType.number, + hasError: dateOfBirthError, + inputFormatters: [maskFormatterForBirthday], + onChanged: (newValue) {}, + ), + if (dateOfBirthError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Некорректный формат даты рождения')], + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Пол'), + const SizedBox(height: 4), + Row( + children: [ + Expanded(child: _genderButton('М', isMaleSelected)), + const SizedBox(width: 4), + Expanded(child: _genderButton('Ж', !isMaleSelected)), + ], + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Вес'), + const SizedBox(height: 4), + CustomTextFormField(controller: weightController, hintText: 'Введите вес', onChanged: (newValue) {}, hasError: weightError), + if (weightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Вес должен быть от 30 до 300 кг')], + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: MediaQuery.of(context).size.width / 2 - 22, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Рост'), + const SizedBox(height: 4), + CustomTextFormField(controller: heightController, hintText: 'Введите рост', onChanged: (newValue) {}, hasError: heightError), + if (heightError) ...[const SizedBox(height: 4), const ValidationErrorWidget(error: 'Рост должен быть от 100 до 250 см')], + ], + ), + ), + ], + ), + const SizedBox(height: 28), + const Center( + child: Text( + 'Проверьте правильность заполненных\nданных. Пол, рост и вес требуются для\nрасчета вашей дневной нормы калорий', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 28), + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => _handleRegistration(context), + child: Ink( + width: MediaQuery.of(context).size.width - 32, + height: 52, + decoration: BoxDecoration(color: state is OAuthStateLoading ? Colors.grey : appTheme.blueA400, borderRadius: BorderRadius.circular(20)), + child: Center( + child: state is OAuthStateLoading + ? const CircularProgressIndicator(color: Colors.white) + : const Text( + 'Зарегистрироваться', + style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18, color: Colors.white), + ), + ), + ), + ), + const SizedBox(height: 16), + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + text: 'Нажимая "Зарегистрироваться", Вы соглашаетесь\n', + style: const TextStyle(fontSize: 16, color: Colors.black), + children: [ + const TextSpan( + text: 'c ', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + if (!hidePlans) ...[ + TextSpan( + text: ' Публичной офертой', + style: TextStyle( + fontSize: 16, + color: appTheme.blueA400, + decorationColor: appTheme.blueA400, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl().baseUrl}oferta_bs.pdf')), + ), + const TextSpan( + text: ' и ', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + ], + TextSpan( + text: 'Политикой конфиденциальности и защитой персональных данных', + style: TextStyle(fontSize: 16, color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('${sl().baseUrl}/docs/privacy.pdf')), + ), + ], + ), + ), + ), + + // Обработка успешной регистрации + if (state is OAuthStateLoaded) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200), + ), + child: const Text( + 'Регистрация успешно завершена!', + style: TextStyle(color: Colors.green), + textAlign: TextAlign.center, + ), + ), + // Автоматический переход на главный экран после успешной регистрации + Builder( + builder: (context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _reconnectWebSocket(context); + }); + return const SizedBox.shrink(); + }, + ), + ], + ], + ), + ); + }, + ), + ), + ); + } + + Widget _genderButton(String gender, bool isActive) { + return InkWell( + onTap: () { + setState(() { + isMaleSelected = gender == 'М'; + }); + }, + borderRadius: BorderRadius.circular(20), + child: Ink( + height: 52, + decoration: BoxDecoration( + color: const Color(0xFFF6F6F6), + borderRadius: BorderRadius.circular(20), + border: isActive ? Border.all(width: 2, color: appTheme.blueA400) : null, + ), + child: Center( + child: Text( + gender, + style: TextStyle(color: isActive ? appTheme.blueA400 : const Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500), + ), + ), + ), + ); + } + + Future _handleRegistration(BuildContext context) async { + final clientID = widget.authResult?.clientId; + // Валидация полей + if (firstNameController.text.isEmpty || + lastNameController.text.isEmpty || + emailController.text.isEmpty || + phoneController.text.isEmpty || + dateOfBirthController.text.isEmpty || + weightController.text.isEmpty || + heightController.text.isEmpty) { + showErrorToast('Пожалуйста, заполните все поля'); + return; + } + + // Парсим числовые значения + int? weight; + int? height; + + weight = int.tryParse(weightController.text); + height = int.tryParse(heightController.text); + bool hasErrorWeightOrHeightError = false; + if (weight == null || height == null) { + setState(() { + weightError = true; + heightError = true; + }); + hasErrorWeightOrHeightError = true; + } + if (weight! < 30 || weight > 300) { + setState(() { + weightError = true; + }); + hasErrorWeightOrHeightError = true; + } + if (height! < 100 || height > 250) { + setState(() { + heightError = true; + }); + hasErrorWeightOrHeightError = true; + } + + if (hasErrorWeightOrHeightError) return; + + // Очищаем номер телефона от маски для отправки + final cleanPhone = phoneController.text.replaceAll(RegExp(r'[^\d+]'), ''); + + if (clientID == null) return; + // Вызываем OAuth регистрацию + final errors = await context.read().signUpViaSocialMedia( + provider: widget.provider ?? 'social', + accessToken: widget.authResult?.accessToken ?? 'dummy_token', + id: widget.authResult?.userId ?? 'dummy_id', + refreshToken: widget.authResult?.refreshToken ?? 'dummy_refresh_token', + firstName: firstNameController.text, + lastName: lastNameController.text, + email: emailController.text, + phone: cleanPhone, + dateOfBirth: dateOfBirthController.text, + gender: isMaleSelected ? 'М' : 'Ж', + weight: weight, + height: height, + clientId: clientID, + ); + if (errors != null) { + for (final error in errors) { + switch (error) { + case RegistrationValidationErrors.incorrectEmailFormat: + setState(() { + incorrectEmailFormatError = true; + }); + break; + case RegistrationValidationErrors.emailExist: + setState(() { + emailExistError = true; + }); + break; + case RegistrationValidationErrors.phoneExist: + setState(() { + phoneExistError = true; + }); + break; + case RegistrationValidationErrors.incorrectDateOfBirthFormat: + setState(() { + dateOfBirthError = true; + }); + break; + } + } + return; + } + showSuccessToast('Регистрация успешна'); + } +} diff --git a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart index 83296f0e..723f95e9 100644 --- a/lib/features/auth/presentation/pages/scan_qr_code_screen.dart +++ b/lib/features/auth/presentation/pages/scan_qr_code_screen.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; @@ -40,17 +40,11 @@ class _ScanQRCodeScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Сканирование QR-кода'), - centerTitle: true, - ), + appBar: AppBar(title: const Text('Сканирование QR-кода'), centerTitle: true), body: Column( children: [ Expanded( - child: QRView( - key: qrKey, - onQRViewCreated: _onQRViewCreated, - ), + child: QRView(key: qrKey, onQRViewCreated: _onQRViewCreated), ), ], ), @@ -68,17 +62,9 @@ class _ScanQRCodeScreenState extends State { final scannedUrl = scanData.code; final uri = Uri.parse(scannedUrl ?? ''); final scannedBaseUrl = (uri.hasAuthority) ? '${uri.scheme}://${uri.host}' : ''; - baseUrl = scannedBaseUrl; - sl().options.baseUrl = baseUrl; - final response = await Dio().getUri( - uri, - options: Options( - headers: { - 'accept': 'application/json', - 'content-type': 'application/json', - }, - ), - ); + sl().setBaseUrl(scannedBaseUrl); + sl().options.baseUrl = scannedBaseUrl; + final response = await Dio().getUri(uri, options: Options(headers: {'accept': 'application/json', 'content-type': 'application/json'})); final token = response.data['token']; await TokenStorage.saveTokenInStorage(token); try { @@ -97,10 +83,7 @@ class _ScanQRCodeScreenState extends State { } catch (e) { rethrow; } - AppRouter.navigatorKey.currentContext?.go( - '/feed-screen', - extra: {'needToInitializeWebSocket': false}, - ); + AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false}); showSuccessToast('Добро пожаловать!'); } catch (e) { await controller.resumeCamera(); diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index 84df92f6..a65f309c 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -1,39 +1,76 @@ import 'package:eatvio_app/features/auth/models/credentials_model.dart'; +import 'package:eatvio_app/features/auth/models/oauth_model.dart'; +import 'package:eatvio_app/features/auth/models/oauth_login_request.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart'; -import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; class AuthRepository { final AuthService authService; AuthRepository({required this.authService}); - Future getTokenByCredentials( - String email, - String password, - ) async { + Future getTokenByCredentials(String email, String password) async { try { return await authService.getTokenByCredentials( - credentials: CredentialsModel( - email: email, - password: password, - ), + credentials: CredentialsModel(email: email, password: password), ); } catch (e) { rethrow; } } - Future getTokenBySocial({ + + Future signInViaSocialMedia({ required String provider, required String accessToken, + required String refreshToken, + required String clientId, }) async { try { - return await authService.getTokenBySocial( - body: SocialExchangeRequest(provider: provider, accessToken: accessToken), + return await authService.signInViaSocialMedia( + body: OAuthLoginRequest(provider: provider, accessToken: accessToken, refreshToken: refreshToken, clientId: clientId), ); } catch (e) { rethrow; } } + + Future signUpViaSocialMedia({ + required String provider, + required String accessToken, + required String id, + required String refreshToken, + required String firstName, + required String lastName, + required String email, + required String phone, + required String dateOfBirth, + required String gender, + required int weight, + required int height, + required String clientId, + }) async { + try { + return (await authService.signUpViaSocialMedia( + body: OAuthModel( + provider: provider, + accessToken: accessToken, + id: id, + refreshToken: refreshToken, + firstName: firstName, + lastName: lastName, + email: email, + phone: phone, + dateOfBirth: dateOfBirth, + gender: gender == 'М' ? 'male' : 'female', + weight: weight, + height: height, + clientId: clientId, + ), + ))['token']! + as String; + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/auth/sdk/social_auth_facade.dart b/lib/features/auth/sdk/social_auth_facade.dart index e5761498..4d2e9745 100644 --- a/lib/features/auth/sdk/social_auth_facade.dart +++ b/lib/features/auth/sdk/social_auth_facade.dart @@ -1,125 +1,214 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; import 'dart:io'; +import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart'; +import 'package:eatvio_app/features/auth/models/yandex_profile_model.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_login_yandex/flutter_login_yandex.dart'; import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/env_config.dart'; +import 'package:go_router/go_router.dart'; +import 'package:http/http.dart' as http; import 'package:vk_id/vk_id.dart'; import 'package:webview_flutter/webview_flutter.dart'; -class VkAuthResult { - VkAuthResult({required this.accessToken, this.refreshToken, this.userId, this.firstName, this.lastName, this.email, this.phone, this.gender, this.birthDate}); - - final String accessToken; - final String? refreshToken; - final String? userId; - final String? firstName; - final String? lastName; - final String? email; - final String? phone; - final String? gender; - final String? birthDate; -} - class SocialAuthFacade { - static Future signInWithVk({required BuildContext context}) async { + static Future signInWithVk() async { + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return null; final result = await signInWithVkAndFetchProfile(context: context); return result?.accessToken; } - static Future signInWithVkAndFetchProfile({required BuildContext context}) async { - final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID']; - if (clientIdStr == null || clientIdStr.isEmpty) { + static Future signInWithVkAndFetchProfile({required BuildContext context}) async { + final clientIdStr = Platform.isIOS ? EnvConfig.vkClientIdIos.trim() : EnvConfig.vkClientIdAndroid.trim(); + if (clientIdStr.isEmpty) { return null; } try { - dynamic controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(); - final redirectScheme = 'vk$clientIdStr://'; - final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme); - if (redirectParams == null) return null; - final code = redirectParams['code']; - final deviceId = redirectParams['device_id']; - if (code == null || deviceId == null) return null; + final controller = VkIDController(clID: int.parse(clientIdStr)); + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: VkProvider.vkID, scopes: [VkScope.email, VkScope.phone]); - final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId); - final accessToken = _extractAccessToken(tokenResponse); - if (accessToken == null) return null; - return VkAuthResult(accessToken: accessToken); - } catch (_) { + final authUrl = authUri.value.toString(); + final codeVerifier = authUri.key; + final redirectScheme = 'vk$clientIdStr://'; + + final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); + if (redirectParams == null) { + return null; + } + return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr); + } catch (e) { return null; } } - static Future signInWithOk() async { + static Future _parseVKAuthResult({ + required Map redirectParams, + required VkIDController controller, + required String codeVerifier, + required String clientId, + }) async { + final code = redirectParams['code']; + final deviceId = redirectParams['device_id']; + final state = redirectParams['state']; + if (code == null || deviceId == null || state == null) { + return null; + } + final authRes = await controller.retrieveOAuthToken(authorizationCode: code, deviceId: deviceId, codeVerifier: codeVerifier, state: state); + final auth = authRes.result; + if (auth == null) { + var msg = "Retrieve OAuth token error"; + final err = authRes.error?.vkErr; + if (err != null) { + msg += ": ${err.error} - ${err.description}"; + } + log(msg); + return null; + } + final profileRes = await controller.getProfileInfo(); + final profile = profileRes.result; + if (profile == null) { + var msg = "Profile info load error"; + final err = profileRes.error?.vkErr; + if (err != null) { + msg += ": ${err.error} - ${err.description}"; + } + log(msg); + return null; + } + + return AuthResultEntity( + accessToken: auth.accessToken, + refreshToken: auth.refreshToken, + userId: profile.userId.toString(), + firstName: profile.firstName, + lastName: profile.lastName, + email: profile.email, + phone: profile.phone, + gender: GenderEnum.fromVkGender(profile.parsedSex), + birthDate: profile.bDayDt?.toString(), + clientId: clientId, + ); + } + + static Future signInWithOkAndFetchProfile() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; - return _signInWithVkIdProvider(context: context, provider: 'ok'); + return await _signInWithVkIdProvider(context: context, provider: VkProvider.okRu); } - static Future signInWithMailRu() async { + static Future signInWithMailRuAndFetchProfile() async { final context = AppRouter.navigatorKey.currentContext; if (context == null) return null; - return _signInWithVkIdProvider(context: context, provider: 'mail'); + return await _signInWithVkIdProvider(context: context, provider: VkProvider.mailRu); } - static Future signInWithYandex() async { - final clientId = dotenv.maybeGet('YANDEX_CLIENT_ID')?.trim() ?? Platform.environment['YANDEX_CLIENT_ID']; - final redirectUri = dotenv.maybeGet('YANDEX_REDIRECT_URI')?.trim() ?? Platform.environment['YANDEX_REDIRECT_URI']; + static Future signInWithYandex() async { + final clientId = EnvConfig.yandexClientId.trim(); + final redirectUri = EnvConfig.yandexRedirectUri.trim(); - if (clientId == null || clientId.isEmpty || redirectUri == null || redirectUri.isEmpty) { + if (clientId.isEmpty || redirectUri.isEmpty) { return null; } - try { - final plugin = FlutterLoginYandex(); - final response = await plugin.signIn(); - final token = (response is Map) ? response['token'] as String? : null; - return (token != null && token.isNotEmpty) ? token : null; - } catch (_) { - return null; - } - } - static Future _signInWithVkIdProvider({required BuildContext context, required String provider}) async { - final clientIdStr = dotenv.maybeGet('VK_CLIENT_ID')?.trim() ?? Platform.environment['VK_CLIENT_ID']; - if (clientIdStr == null || clientIdStr.isEmpty) { - return null; - } try { - dynamic controller = VkIDController(clID: int.parse(clientIdStr)); - final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider); - final redirectScheme = 'vk$clientIdStr://'; - final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUri.toString(), redirectSchemePrefix: redirectScheme); - if (redirectParams == null) return null; - final code = redirectParams['code']; - final deviceId = redirectParams['device_id']; - if (code == null || deviceId == null) return null; - final tokenResponse = await controller.exchangeAuthorizationCodeForToken(code: code, deviceId: deviceId, provider: provider); - final accessToken = _extractAccessToken(tokenResponse); - return accessToken; - } catch (_) { - return null; - } - } + final context = AppRouter.navigatorKey.currentContext; + if (context == null) return null; - static String? _extractAccessToken(dynamic tokenResponse) { - if (tokenResponse == null) return null; - if (tokenResponse is Map) { - return (tokenResponse['access_token'] as String?) ?? (tokenResponse['token'] as String?); + final authorizeUrl = Uri.https('oauth.yandex.ru', '/authorize', { + 'response_type': 'code', + 'client_id': clientId, + 'redirect_uri': redirectUri, + 'force_confirm': 'true', + }).toString(); + + final redirectSchemePrefix = redirectUri; + + final params = await _presentOAuthWebView(context: context, initialUrl: authorizeUrl, redirectSchemePrefix: redirectSchemePrefix); + + if (params == null) return null; + + final code = params['code']; + if (code == null || code.isEmpty) { + return null; + } + + final tokens = await _getTokensFomYandex(code: code, clientId: clientId, clientSecret: EnvConfig.yandexClientSecret); + if (tokens == null) { + return null; + } + final response = await http.get(Uri.parse('https://login.yandex.ru/info?format=json'), headers: {'Authorization': 'OAuth ${tokens.accessToken}'}); + + final Map json = jsonDecode(response.body); + final yandexProfileData = YandexProfileModel.fromJson(json); + return AuthResultEntity( + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + userId: yandexProfileData.id, + firstName: yandexProfileData.firstName, + lastName: yandexProfileData.lastName, + email: yandexProfileData.defaultEmail, + phone: yandexProfileData.defaultPhone.number, + gender: GenderEnum.fromString(yandexProfileData.sex), + birthDate: yandexProfileData.birthday, + clientId: clientId, + ); + } catch (e) { + log(e.toString()); } - try { - final token = (tokenResponse as dynamic).accessToken as String?; - if (token != null && token.isNotEmpty) return token; - } catch (_) {} - try { - final token = (tokenResponse as dynamic).token as String?; - if (token != null && token.isNotEmpty) return token; - } catch (_) {} return null; } + static Future<({String accessToken, String refreshToken})?> _getTokensFomYandex({ + required String code, + required String clientId, + required String clientSecret, + }) async { + final response = await http.post( + Uri.parse('https://oauth.yandex.ru/token'), + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: {'grant_type': 'authorization_code', 'code': code, 'client_id': clientId, 'client_secret': clientSecret}, + ); + + if (response.statusCode != 200) { + log('Yandex token exchange failed: ${response.body}'); + return null; + } + + final tokens = jsonDecode(response.body); + final accessToken = tokens['access_token'] as String; + final refreshToken = tokens['refresh_token'] as String; + return (accessToken: accessToken, refreshToken: refreshToken); + } + + static Future _signInWithVkIdProvider({required BuildContext context, required VkProvider provider}) async { + final clientIdStr = Platform.isIOS ? EnvConfig.vkClientIdIos.trim() : EnvConfig.vkClientIdAndroid.trim(); + if (clientIdStr.isEmpty) { + return null; + } + try { + final controller = VkIDController(clID: int.parse(clientIdStr)); + + final authUri = controller.generateAuthorizeLinkWithCodeVerifier(provider: provider, scopes: [VkScope.phone, VkScope.email]); + final codeVerifier = authUri.key; + final authUrl = authUri.value.toString(); + final redirectScheme = 'vk$clientIdStr://'; + final redirectParams = await _presentOAuthWebView(context: context, initialUrl: authUrl, redirectSchemePrefix: redirectScheme); + if (redirectParams == null) return null; + + final code = redirectParams['code']; + final deviceId = redirectParams['device_id']; + if (code == null || deviceId == null) return null; + + return await _parseVKAuthResult(redirectParams: redirectParams, controller: controller, codeVerifier: codeVerifier, clientId: clientIdStr); + } catch (e) { + return null; + } + } + static Future?> _presentOAuthWebView({ required BuildContext context, required String initialUrl, @@ -134,22 +223,49 @@ class SocialAuthFacade { if (url.startsWith(redirectSchemePrefix)) { final uri = Uri.parse(url); final params = {}..addAll(uri.queryParameters); - Navigator.of(context, rootNavigator: true).pop(params); + if (uri.fragment.isNotEmpty) { + final fragPairs = uri.fragment.split('&'); + for (final pair in fragPairs) { + final eq = pair.indexOf('='); + if (eq > 0 && eq < pair.length - 1) { + final key = Uri.decodeComponent(pair.substring(0, eq)); + final value = Uri.decodeComponent(pair.substring(eq + 1)); + params[key] = value; + } + } + } + if (GoRouter.of(context).canPop()) { + GoRouter.of(context).pop(params); + } return NavigationDecision.prevent; } return NavigationDecision.navigate; }, + onPageStarted: (url) {}, + onPageFinished: (url) {}, + onWebResourceError: (error) {}, ), - ) - ..loadRequest(Uri.parse(initialUrl)); + ); + try { + await controller.loadRequest(Uri.parse(initialUrl)); + } catch (e) { + return null; + } + if (!context.mounted) return null; return showDialog>( context: context, barrierDismissible: false, builder: (_) { + const borderRadius = BorderRadius.all(Radius.circular(16)); return Dialog( insetPadding: const EdgeInsets.all(16), - child: SizedBox(height: 560, child: WebViewWidget(controller: controller)), + shape: const RoundedRectangleBorder(borderRadius: borderRadius), + elevation: 0, + child: ClipRRect( + borderRadius: borderRadius, + child: SizedBox(height: 560, child: WebViewWidget(controller: controller)), + ), ); }, ); diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 7dabb65b..41d35da7 100644 --- a/lib/features/auth/services/auth_service.dart +++ b/lib/features/auth/services/auth_service.dart @@ -1,7 +1,8 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/features/auth/models/credentials_model.dart'; +import 'package:eatvio_app/features/auth/models/oauth_model.dart'; +import 'package:eatvio_app/features/auth/models/oauth_login_request.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; -import 'package:eatvio_app/features/auth/models/social_exchange_request.dart'; import 'package:retrofit/retrofit.dart'; part 'auth_service.g.dart'; @@ -11,12 +12,11 @@ abstract class AuthService { factory AuthService(Dio dio, {String? baseUrl}) = _AuthService; @POST('/api/login') - Future getTokenByCredentials({ - @Body() required CredentialsModel credentials, - }); + Future getTokenByCredentials({@Body() required CredentialsModel credentials}); - @POST('/api/social/login') - Future getTokenBySocial({ - @Body() required SocialExchangeRequest body, - }); + @POST('/api/oauth/callback') + Future> signUpViaSocialMedia({@Body() required OAuthModel body}); + + @POST('/api/oauth/login') + Future signInViaSocialMedia({@Body() required OAuthLoginRequest body}); } diff --git a/lib/features/blog_post/presentation/pages/blog_post_screen.dart b/lib/features/blog_post/presentation/pages/blog_post_screen.dart index 61b64f9e..76562bcd 100644 --- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart +++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart @@ -1,6 +1,6 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; @@ -207,7 +207,7 @@ class _BlogPostScreenState extends State { children: [ CircleAvatar( radius: 16, - foregroundImage: NetworkImage(baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')), + foregroundImage: NetworkImage(sl().baseUrl + (blogPostState.blogPostModel?.author.authorProfileIcon ?? '')), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), const SizedBox(width: 8), @@ -239,7 +239,7 @@ class _BlogPostScreenState extends State { ), const SizedBox(height: 8), if (blogPostState.blogPostModel?.image.isNotEmpty ?? false) - CachedNetworkImage(imageUrl: '$baseUrl/storage/${blogPostState.blogPostModel?.image ?? ''}'), + CachedNetworkImage(imageUrl: '${sl().baseUrl}/storage/${blogPostState.blogPostModel?.image ?? ''}'), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( @@ -267,7 +267,7 @@ class _BlogPostScreenState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/blog_post/${widget.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/blog_post/${widget.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart index df228298..6e93944a 100644 --- a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart +++ b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart @@ -1,61 +1,34 @@ -import 'dart:developer'; - +import 'package:eatvio_app/core/cubit/base_cubit.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart'; import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; -class BlogPostCreateCubit extends Cubit { +class BlogPostCreateCubit extends BaseCubit { final BlogPostRepository blogPostRepository; - BlogPostCreateCubit({ - required this.blogPostRepository, - }) : super(const BlogPostCreateState()); + BlogPostCreateCubit({required this.blogPostRepository}) : super(const BlogPostCreateState()); - Future createBlogPost({ - String? title, - String? videoUrl, - String? description, - XFile? image, - }) async { + Future createBlogPost({String? title, String? videoUrl, String? description, XFile? image}) async { try { emit(const BlogPostCreateState.loading()); - await blogPostRepository.createBlogPost( - title, - videoUrl, - description, - image, - ); + await blogPostRepository.createBlogPost(title, videoUrl, description, image); + emit(const BlogPostCreateState.loaded()); return true; - } catch (e, st) { - log(e.toString(), stackTrace: st); + } catch (error, stackTrace) { + handleError(error, stackTrace); emit(const BlogPostCreateState.loaded()); return false; } } - Future updateBlogPost({ - required int id, - String? title, - String? videoUrl, - String? description, - XFile? image, - String? oldImageUrl, - }) async { + Future updateBlogPost({required int id, String? title, String? videoUrl, String? description, XFile? image, String? oldImageUrl}) async { try { emit(const BlogPostCreateState.loading()); - await blogPostRepository.updateBlogPost( - id, - title, - videoUrl, - description, - image, - oldImageUrl, - ); + await blogPostRepository.updateBlogPost(id, title, videoUrl, description, image, oldImageUrl); emit(const BlogPostCreateState.loaded()); return true; - } catch (e, st) { - log(e.toString(), stackTrace: st); + } catch (error, stackTrace) { + handleError(error, stackTrace); emit(const BlogPostCreateState.loaded()); return false; } @@ -65,8 +38,8 @@ class BlogPostCreateCubit extends Cubit { try { await blogPostRepository.deleteBlogPost(id); return true; - } catch (e, st) { - log(e.toString(), stackTrace: st); + } catch (error, stackTrace) { + handleError(error, stackTrace); return false; } } diff --git a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart index 6be43b3a..40ad3dfa 100644 --- a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart +++ b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/permission_handler/permission_handler.dart'; import 'package:eatvio_app/core/theme/app_decoration.dart'; import 'package:eatvio_app/core/widgets/show_error_dialog.dart'; @@ -304,7 +304,7 @@ class _BlogPostCreateScreenState extends State { child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Image( - image: NetworkImage('$baseUrl/storage/${widget.feedBlogPostModel!.image}'), + image: NetworkImage('${sl().baseUrl}/storage/${widget.feedBlogPostModel!.image}'), ), ), ), diff --git a/lib/features/blog_post_create/repositories/blog_post_repository.dart b/lib/features/blog_post_create/repositories/blog_post_repository.dart index aeea8474..48f4f0c1 100644 --- a/lib/features/blog_post_create/repositories/blog_post_repository.dart +++ b/lib/features/blog_post_create/repositories/blog_post_repository.dart @@ -1,7 +1,8 @@ import 'dart:developer'; import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:http/http.dart' as http; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart'; @@ -36,7 +37,7 @@ class BlogPostRepository { Future downloadImage(String imageUrl) async { try { // Загрузка изображения - final response = await http.get(Uri.parse('$baseUrl/storage/$imageUrl')); + final response = await http.get(Uri.parse('${sl().baseUrl}/storage/$imageUrl')); if (response.statusCode == 200) { // Получение временной директории diff --git a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart index a67fbbaf..b1e846ca 100644 --- a/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart +++ b/lib/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart @@ -1,5 +1,6 @@ import 'package:badges/badges.dart' as badges; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; @@ -102,7 +103,7 @@ class _BottomNavigationBarWidgetState extends State { label: '', icon: CircleAvatar( foregroundImage: NetworkImage( - widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : baseUrl + widget.profileModel.profilePhotoUrl, + widget.profileModel.profilePhotoUrl.contains('https') ? widget.profileModel.profilePhotoUrl : sl().baseUrl + widget.profileModel.profilePhotoUrl, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: 16, diff --git a/lib/features/chat/presentation/pages/photo_view_screen.dart b/lib/features/chat/presentation/pages/photo_view_screen.dart index 1c17d40e..3cbdc744 100644 --- a/lib/features/chat/presentation/pages/photo_view_screen.dart +++ b/lib/features/chat/presentation/pages/photo_view_screen.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/app_decoration.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -88,7 +89,7 @@ class _PhotoViewScreenState extends State { color: Colors.white, ), title: Text( - widget.imageUrls[index].replaceAll('$baseUrl/storage/chat_files/', ''), + widget.imageUrls[index].replaceAll('${sl().baseUrl}/storage/chat_files/', ''), style: const TextStyle( color: Colors.white, fontSize: 12, diff --git a/lib/features/chat/presentation/widgets/order_card.dart b/lib/features/chat/presentation/widgets/order_card.dart index 31d99aad..2b7b50b2 100644 --- a/lib/features/chat/presentation/widgets/order_card.dart +++ b/lib/features/chat/presentation/widgets/order_card.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; @@ -47,7 +48,7 @@ class _OrderCardState extends State { ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.network( - '$baseUrl/storage/${widget.plan.image}', + '${sl().baseUrl}/storage/${widget.plan.image}', width: double.infinity, fit: BoxFit.cover, ), diff --git a/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart b/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart index d44920cb..734387f1 100644 --- a/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart +++ b/lib/features/consultant/become_consultant/presentation/pages/last_step_screen.dart @@ -1,4 +1,6 @@ +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:flutter/material.dart'; @@ -49,7 +51,7 @@ class _LastStepScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: TextButton( - onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '$baseUrl/docs/privacy.pdf' : '$baseUrl/oferta_bs.pdf')), + onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '${sl().baseUrl}/docs/privacy.pdf' : '${sl().baseUrl}/oferta_bs.pdf')), child: Text( hidePlans ? 'Политика конфиденциальности и пользовательское соглашение' : 'Договор офёрты', style: const TextStyle(height: 1, fontSize: 16, color: Color(0xFF323F4B), decoration: TextDecoration.underline), diff --git a/lib/features/consultant_rating/models/statistics_model.dart b/lib/features/consultant_rating/models/statistics_model.dart index bbac675b..1e04eebc 100644 --- a/lib/features/consultant_rating/models/statistics_model.dart +++ b/lib/features/consultant_rating/models/statistics_model.dart @@ -13,6 +13,7 @@ abstract class StatisticsModel with _$StatisticsModel { @JsonKey(name: 'count_diets') int? dietsCount, @JsonKey(name: 'count_consultants') int? consultationsCount, @JsonKey(name: 'count_products_nutritional_values') int? qwe, + @JsonKey(name: 'referral_link') String? refLink, }) = _StatisticsModel; factory StatisticsModel.fromJson(Map json) => _$StatisticsModelFromJson(json); diff --git a/lib/features/consultant_rating/presentation/consultant_rating_screen.dart b/lib/features/consultant_rating/presentation/consultant_rating_screen.dart index 8c6cb3d4..000367e0 100644 --- a/lib/features/consultant_rating/presentation/consultant_rating_screen.dart +++ b/lib/features/consultant_rating/presentation/consultant_rating_screen.dart @@ -1,6 +1,7 @@ import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; import 'package:eatvio_app/core/widgets/data_error_widget.dart'; import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart'; @@ -10,6 +11,7 @@ import 'package:eatvio_app/features/consultant_rating/presentation/widgets/ratin import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ConsultantRatingScreen extends StatelessWidget { @@ -53,8 +55,8 @@ class ConsultantRatingPageContent extends StatelessWidget { child: Column( children: [ const SizedBox(height: 16), - const RatingWidget( - rating: 1058, + RatingWidget( + rating: state.absoluteRating!, ), const SizedBox(height: 16), Padding( @@ -67,7 +69,10 @@ class ConsultantRatingPageContent extends StatelessWidget { side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400), ), - onPressed: () {}, + onPressed: () { + Clipboard.setData(ClipboardData(text: state.statistics?.refLink ?? '')); + showInfoToast(s.link_was_copied); + }, ), ), const SizedBox(height: 16), diff --git a/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart b/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart index 291f450c..d3ff72a9 100644 --- a/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart +++ b/lib/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart @@ -2,17 +2,23 @@ import 'dart:developer'; import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart'; import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart'; +import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ConsultantRatingCubit extends Cubit { final ConsultantRatingRepository repository; - ConsultantRatingCubit(this.repository) : super(ConsultantRatingState()); + final FeedRepository feedRepository; + final ProfileRepository profileRepository; + + ConsultantRatingCubit({required this.repository, required this.feedRepository, required this.profileRepository}) : super(ConsultantRatingState()); Future getConsultantStatistics({String? date}) async { try { emit(state.copyWith(isStatisticsLoading: true)); final statistics = await repository.getConsultantStatistics(date: date); - emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null)); + final absoluteRating = await feedRepository.getConsultants((await profileRepository.getMyProfile()).name); + emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null, absoluteRating: absoluteRating.first.totalScores)); } catch (e, st) { emit(state.copyWith(isStatisticsLoading: true, error: e.toString())); log(e.toString(), stackTrace: st); diff --git a/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart b/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart index 20cc458f..7c1e0942 100644 --- a/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart +++ b/lib/features/consultant_rating/presentation/cubit/consultant_rating_state.dart @@ -12,5 +12,6 @@ abstract class ConsultantRatingState with _$ConsultantRatingState { @Default(null) StatisticsModel? statistics, @Default(null) String? error, @Default([]) List ratings, + @Default(0) int? absoluteRating, }) = _ConsultantRatingState; } diff --git a/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart b/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart index 3be44129..ef148adf 100644 --- a/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart +++ b/lib/features/consultant_rating/presentation/widgets/statistic_widget.dart @@ -71,7 +71,7 @@ class StatisticWidget extends StatelessWidget { children: [ Expanded(child: StatisticBouble(number: statistics?.consultationsCount ?? 0, title: s.consultations_count)), const SizedBox(width: 8), - Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.active_consultations_count)), + Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.added_products_in_glossary)), ], ), const SizedBox( diff --git a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart index 6eb41358..757101ae 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart @@ -2,8 +2,10 @@ import 'dart:convert'; import 'dart:io'; import 'dart:developer'; import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_state.dart'; import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; @@ -53,7 +55,7 @@ class CreateDietCubit extends Cubit { name: diet.name, duration: diet.duration, type: type, - imageUrl: '$baseUrl/storage/${diet.image}', + imageUrl: '${sl().baseUrl}/storage/${diet.image}', ), ); emit(state.copyWith(dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration))); diff --git a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart index 4ca081d4..4541782b 100644 --- a/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart +++ b/lib/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart @@ -1,7 +1,8 @@ import 'dart:developer'; import 'dart:io'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/create_recipe/entities/recipe_create_entity.dart'; import 'package:eatvio_app/features/create_recipe/models/ingredient.dart' as i; import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart'; @@ -187,13 +188,13 @@ class CreateRecipeCubit extends Cubit { } Future recipeToEntity(Recipe recipe) async { - final file = await downloadImageAsFile('$baseUrl/storage/${recipe.image}'); + final file = await downloadImageAsFile('${sl().baseUrl}/storage/${recipe.image}'); final List steps = await Future.wait( recipe.steps!.map( (e) async => StepEntity( description: e.description, - image: e.image.isEmpty ? null : await downloadImageAsFile('$baseUrl/storage/${e.image}'), + image: e.image.isEmpty ? null : await downloadImageAsFile('${sl().baseUrl}/storage/${e.image}'), ), ), ); diff --git a/lib/features/dev_tools/presentation/dev_tools_screen.dart b/lib/features/dev_tools/presentation/dev_tools_screen.dart index 4922a87d..0be58bee 100644 --- a/lib/features/dev_tools/presentation/dev_tools_screen.dart +++ b/lib/features/dev_tools/presentation/dev_tools_screen.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/firebase_api.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; @@ -84,7 +84,7 @@ class _DevToolsScreenContentState extends State { void initState() { super.initState(); setState(() { - baseUrlForTest = baseUrl; + baseUrlForTest = sl().baseUrl; }); testsStates = List.generate(testCases.length, (index) => TestState.notRun); } @@ -111,8 +111,8 @@ class _DevToolsScreenContentState extends State { onPressed: () { setState(() { baseUrlForTest = controller.text; - baseUrl = baseUrlForTest.toLowerCase(); - sl().options.baseUrl = baseUrl; + sl().setBaseUrl(baseUrlForTest.toLowerCase()); + sl().options.baseUrl = baseUrlForTest.toLowerCase(); controller.clear(); }); }, @@ -122,23 +122,23 @@ class _DevToolsScreenContentState extends State { ), const SizedBox(height: 10), CustomOutlinedButton( - text: baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url', + text: sl().baseUrl == EnvConfig.devBaseUrl ? 'Set production base url' : 'Set dev2 base url', onPressed: () { setState(() { - if (baseUrl == EnvConfig.baseUrl) { + if (sl().baseUrl == EnvConfig.baseUrl) { baseUrlForTest = EnvConfig.devBaseUrl; } else { baseUrlForTest = EnvConfig.baseUrl; } - baseUrl = baseUrlForTest.toLowerCase(); - sl().options.baseUrl = baseUrl; + sl().setBaseUrl(baseUrlForTest.toLowerCase()); + sl().options.baseUrl = baseUrlForTest.toLowerCase(); controller.clear(); }); }, buttonStyle: const ButtonStyle(), ), - SizedBox(height: baseUrlForTest != baseUrl ? 40 : 10), - if (baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase()) + SizedBox(height: baseUrlForTest != sl().baseUrl ? 40 : 10), + if (sl().baseUrl.toLowerCase() != EnvConfig.baseUrl.toLowerCase()) ListView.builder( shrinkWrap: true, itemCount: testCases.length, diff --git a/lib/features/diary/presentation/widgets/chosen_diet_widget.dart b/lib/features/diary/presentation/widgets/chosen_diet_widget.dart index 3b5f23fd..622e0e5e 100644 --- a/lib/features/diary/presentation/widgets/chosen_diet_widget.dart +++ b/lib/features/diary/presentation/widgets/chosen_diet_widget.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; @@ -6,77 +7,84 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; class ChosenDietWidget extends StatelessWidget { const ChosenDietWidget({super.key}); @override Widget build(BuildContext context) { - return BlocBuilder(builder: (context, state) { - if (state is DiaryStateLoaded) { - if (state.diary.typeOfDiary.hasChosenDiet) { - return ChosenDietWidgetContent( - currentDietDay: state.diary.dayOfChosenDiet ?? 0, - dietName: state.diary.diet?.name ?? '', - dietDuration: state.diary.diet?.duration ?? 0, - ); + return BlocBuilder( + builder: (context, state) { + if (state is DiaryStateLoaded) { + if (state.diary.typeOfDiary.hasChosenDiet) { + return ChosenDietWidgetContent( + currentDietDay: state.diary.dayOfChosenDiet ?? 0, + dietName: state.diary.diet?.name ?? '', + dietDuration: state.diary.diet?.duration ?? 0, + dietId: state.diary.diet?.id ?? 0, + ); + } } - } - if (state is DiaryStateLoadedTomorrowWithDiet) { - if (state.diary.typeOfDiary.hasChosenDiet) { - return Column( - children: [ - const CustomDivider(), - const SizedBox(height: 16), - ChosenDietWidgetContent( - currentDietDay: state.diary.dayOfChosenDiet ?? 0, - dietName: state.diary.diet?.name ?? '', - dietDuration: state.diary.diet?.duration ?? 0, - ), - ], - ); + if (state is DiaryStateLoadedTomorrowWithDiet) { + if (state.diary.typeOfDiary.hasChosenDiet) { + return Column( + children: [ + const CustomDivider(), + const SizedBox(height: 16), + ChosenDietWidgetContent( + currentDietDay: state.diary.dayOfChosenDiet ?? 0, + dietName: state.diary.diet?.name ?? '', + dietDuration: state.diary.diet?.duration ?? 0, + dietId: state.diary.diet?.id ?? 0, + ), + ], + ); + } } - } - return const SizedBox.shrink(); - }); + return const SizedBox.shrink(); + }, + ); } } class ChosenDietWidgetContent extends StatelessWidget { - const ChosenDietWidgetContent({ - super.key, - required this.currentDietDay, - required this.dietName, - required this.dietDuration, - }); + const ChosenDietWidgetContent({super.key, required this.currentDietDay, required this.dietName, required this.dietDuration, required this.dietId}); final int currentDietDay; final String dietName; final int dietDuration; + final int dietId; @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); final s = S.of(context); - return Column( - children: [ - Row( - children: [ - SizedBox( - width: size.width * 0.4, - child: Text(dietName, style: TextStyles.textLMedium.copyWith(color: appTheme.black)), - ), - const Spacer(), - Container( - decoration: BoxDecoration(color: appTheme.dividerColor, borderRadius: BorderRadius.circular(12)), - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), - child: Text('${s.day} $currentDietDay из $dietDuration', style: TextStyles.textMSemibold.copyWith(color: appTheme.gray)), - ) - ], - ), - const SizedBox( - height: 16, - ) - ], + return GestureDetector( + onTap: () { + GoRouter.of(context).push(RouteConstants.aboutDietScreen, extra: {'id': dietId, 'navigateToComments': false}); + }, + child: Column( + children: [ + Row( + children: [ + SizedBox( + width: size.width * 0.4, + child: Text( + dietName, + style: TextStyles.textLMedium.copyWith(color: appTheme.black, decoration: TextDecoration.underline), + ), + ), + const Spacer(), + Container( + decoration: BoxDecoration(color: appTheme.dividerColor, borderRadius: BorderRadius.circular(12)), + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), + child: Text('${s.day} $currentDietDay из $dietDuration', style: TextStyles.textMSemibold.copyWith(color: appTheme.gray)), + ), + ], + ), + const SizedBox(height: 16), + ], + ), ); } } diff --git a/lib/features/diary/presentation/widgets/photo_gallery_widget.dart b/lib/features/diary/presentation/widgets/photo_gallery_widget.dart index 352b6519..87643817 100644 --- a/lib/features/diary/presentation/widgets/photo_gallery_widget.dart +++ b/lib/features/diary/presentation/widgets/photo_gallery_widget.dart @@ -1,8 +1,9 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/permission_handler/permission_handler.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/diary/models/diary_day_model.dart'; @@ -139,7 +140,7 @@ class _PhotoGalleryWidgetState extends State { ClipRRect( borderRadius: BorderRadius.circular(12), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex].path}', + imageUrl: '${sl().baseUrl}/storage/${state.diary.meals[mealIndex].photos[currentSelectedPhotoIndex].path}', ), ), if (!state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented) @@ -211,7 +212,7 @@ class _PhotoGalleryWidgetState extends State { height: 52, width: 52, child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${state.diary.meals[mealIndex].photos[i].path}', + imageUrl: '${sl().baseUrl}/storage/${state.diary.meals[mealIndex].photos[i].path}', fit: BoxFit.fitWidth, ), ), diff --git a/lib/features/diets/presentation/cubit/about_diet_cubit.dart b/lib/features/diets/presentation/cubit/about_diet_cubit.dart index 9d02534e..6ba7980f 100644 --- a/lib/features/diets/presentation/cubit/about_diet_cubit.dart +++ b/lib/features/diets/presentation/cubit/about_diet_cubit.dart @@ -19,9 +19,14 @@ class AboutDietCubit extends Cubit { String? userStartedDietName; List _selectedIndexes = []; - Future getDiet(String slug, {int? index}) async { + Future getDiet({String? slug, int? id, int? index}) async { try { - final diet = await dietsRepository.getDietBySlug(slug); + final Diet diet; + if (slug != null) { + diet = await dietsRepository.getDietBySlug(slug); + } else { + diet = await dietsRepository.getDiet(id!); + } if (index != null) { dietsRepository.emitCommentsStateToFeed(CommentsState(index: index, totalComments: diet.totalComments)); } @@ -32,10 +37,16 @@ class AboutDietCubit extends Cubit { } } - Future initDietPage(String slug) async { + Future initDietPage({String? slug, int? id}) async { try { emit(const AboutDietState.loading()); - final diet = await dietsRepository.getDietBySlug(slug); + final Diet diet; + if (slug != null) { + diet = await dietsRepository.getDietBySlug(slug); + } else { + diet = await dietsRepository.getDiet(id!); + } + final chosenDiets = await profileRepository.getChosenDiets(); final lastChosenDiet = chosenDiets.firstOrNull; if (lastChosenDiet != null && ['inited', 'started'].contains(lastChosenDiet.state)) { @@ -50,12 +61,7 @@ class AboutDietCubit extends Cubit { } } _selectedIndexes = List.generate(diet.dailySchedule.length, (index) => 0); - emit(AboutDietState.loaded( - diet: diet, - isStarted: isCurrentDietStarted, - hasStartedDiet: hasStartedDiet, - selectedIndexes: _selectedIndexes, - )); + emit(AboutDietState.loaded(diet: diet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes)); } catch (e) { log(e.toString()); emit(AboutDietState.error(message: e.toString())); @@ -68,41 +74,30 @@ class AboutDietCubit extends Cubit { final currentDiet = (state as DietStateLoaded).diet; if (currentDiet.wasLikedByMe) { - final updatedDiet = currentDiet.copyWith( - totalLikes: currentDiet.totalLikes - 1, - wasLikedByMe: false, - ); + final updatedDiet = currentDiet.copyWith(totalLikes: currentDiet.totalLikes - 1, wasLikedByMe: false); if (dietIndexInFeed != null) { dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); } emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes)); } else { - final updatedDiet = currentDiet.copyWith( - totalLikes: currentDiet.totalLikes + 1, - wasLikedByMe: true, - ); + final updatedDiet = currentDiet.copyWith(totalLikes: currentDiet.totalLikes + 1, wasLikedByMe: true); if (dietIndexInFeed != null) { dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: updatedDiet.wasLikedByMe, totalLikes: updatedDiet.totalLikes, index: dietIndexInFeed)); } emit(AboutDietState.loaded(diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet, selectedIndexes: _selectedIndexes)); } - EasyDebounce.debounce( - 'like_diet', - const Duration(seconds: 1), - () async { - try { - await dietsRepository.likeDiet(id); - } catch (e) { - log(e.toString()); - if (dietIndexInFeed != null) { - dietsRepository - .emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed)); - } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + EasyDebounce.debounce('like_diet', const Duration(seconds: 1), () async { + try { + await dietsRepository.likeDiet(id); + } catch (e) { + log(e.toString()); + if (dietIndexInFeed != null) { + dietsRepository.emitLikesStateToFeed(LikesState(wasLikedByMe: currentDiet.wasLikedByMe, totalLikes: currentDiet.totalLikes, index: dietIndexInFeed)); } - }, - ); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + } + }); } Future addToFavorite(int id, int? dietIndex) async { @@ -110,30 +105,24 @@ class AboutDietCubit extends Cubit { final currentDiet = (state as DietStateLoaded).diet; - final updatedDiet = currentDiet.copyWith( - isFavorite: true, - ); + final updatedDiet = currentDiet.copyWith(isFavorite: true); if (dietIndex != null) { dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); - EasyDebounce.debounce( - 'add_or_remove_diet_to_favorites', - const Duration(seconds: 1), - () async { - try { - await dietsRepository.addToFavorite(id); - } catch (e) { - log(e.toString()); - showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - if (dietIndex != null) { - dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); - } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + EasyDebounce.debounce('add_or_remove_diet_to_favorites', const Duration(seconds: 1), () async { + try { + await dietsRepository.addToFavorite(id); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); } - }, - ); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + } + }); } Future removeFromFavorite(int id, int? dietIndex) async { @@ -141,30 +130,24 @@ class AboutDietCubit extends Cubit { final currentDiet = (state as DietStateLoaded).diet; - final updatedDiet = currentDiet.copyWith( - isFavorite: false, - ); + final updatedDiet = currentDiet.copyWith(isFavorite: false); if (dietIndex != null) { dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: updatedDiet.isFavorite, index: dietIndex)); } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: updatedDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); - EasyDebounce.debounce( - 'add_or_remove_diet_to_favorites', - const Duration(seconds: 1), - () async { - try { - await dietsRepository.removeFromFavorite(id); - } catch (e) { - log(e.toString()); - showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); - if (dietIndex != null) { - dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); - } - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + EasyDebounce.debounce('add_or_remove_diet_to_favorites', const Duration(seconds: 1), () async { + try { + await dietsRepository.removeFromFavorite(id); + } catch (e) { + log(e.toString()); + showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); + if (dietIndex != null) { + dietsRepository.emitFavoriteStateToFeed(FavoriteState(isFavorite: currentDiet.isFavorite, index: dietIndex)); } - }, - ); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: currentDiet, isStarted: isCurrentDietStarted, hasStartedDiet: hasStartedDiet)); + } + }); } Future startDiet(DateTime date) async { @@ -199,13 +182,13 @@ class AboutDietCubit extends Cubit { void clearStartedDietsState() { if (state is! DietStateLoaded) return; final diet = (state as DietStateLoaded).diet; - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: diet)); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: diet)); } void emitCurrentStartedDietsState() { if (state is! DietStateLoaded) return; final diet = (state as DietStateLoaded).diet; - emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes,diet: diet, isStarted: true)); + emit(AboutDietState.loaded(selectedIndexes: _selectedIndexes, diet: diet, isStarted: true)); } Future getDietBySlug(String slug) async { diff --git a/lib/features/diets/presentation/pages/about_diet_screen.dart b/lib/features/diets/presentation/pages/about_diet_screen.dart index e4dd4a75..9057c59b 100644 --- a/lib/features/diets/presentation/pages/about_diet_screen.dart +++ b/lib/features/diets/presentation/pages/about_diet_screen.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; @@ -35,11 +35,12 @@ import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; class AboutDietScreen extends StatefulWidget { - final String slug; + final String? slug; + final int? id; final int? index; final bool navigateToComments; - const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug}); + const AboutDietScreen({super.key, required this.index, required this.navigateToComments, required this.slug, required this.id}); @override State createState() => _AboutDietScreenState(); @@ -79,7 +80,7 @@ class _AboutDietScreenState extends State { final s = S.of(context); return BlocProvider( - create: (context) => dietsCubit..initDietPage(widget.slug), + create: (context) => dietsCubit..initDietPage(slug: widget.slug, id: widget.id), child: BlocProvider( create: (BuildContext context) => commentsCubit, child: BlocConsumer( @@ -283,7 +284,7 @@ class _AboutDietScreenState extends State { replyId = null; }); Future.delayed(const Duration(milliseconds: 500), () { - dietsCubit.getDiet(dietState.diet.slug, index: widget.index); + dietsCubit.getDiet(slug: dietState.diet.slug, index: widget.index); }); FocusManager.instance.primaryFocus?.unfocus(); } @@ -353,7 +354,7 @@ class _AboutDietScreenState extends State { Stack( children: [ CachedNetworkImage( - imageUrl: '$baseUrl/storage/${dietState.diet.image}', + imageUrl: '${sl().baseUrl}/storage/${dietState.diet.image}', height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill, @@ -526,7 +527,7 @@ class _AboutDietScreenState extends State { ), ), body: DataErrorWidget( - onReload: () => context.read().getDiet(widget.slug, index: widget.index), + onReload: () => context.read().getDiet(slug: widget.slug, id: widget.id, index: widget.index), ), ); } diff --git a/lib/features/diets/presentation/widgets/author_card.dart b/lib/features/diets/presentation/widgets/author_card.dart index 6fff0b93..34131dac 100644 --- a/lib/features/diets/presentation/widgets/author_card.dart +++ b/lib/features/diets/presentation/widgets/author_card.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; @@ -37,45 +38,25 @@ class AuthorCard extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, image: DecorationImage( - image: NetworkImage(baseUrl + author.authorProfileIcon), + image: NetworkImage( + author.authorProfileIcon.contains('https') ? author.authorProfileIcon : sl().baseUrl + author.authorProfileIcon, + ), fit: BoxFit.cover, ), ), ), - subtitle: Text( - author.authorName, - style: TextStyles.textLSemibold.copyWith( - color: appTheme.black, - ), - ), - title: Text( - title, - style: titleStyle ?? TextStyles.textSSemibold.copyWith( - color: appTheme.gray, - ), - ), + subtitle: Text(author.authorName, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)), + title: Text(title, style: titleStyle ?? TextStyles.textSSemibold.copyWith(color: appTheme.gray)), ), Row( children: [ - Text( - 'Консультант', - style: TextStyles.textMMedium.copyWith( - color: appTheme.black, - ), - ), + Text('Консультант', style: TextStyles.textMMedium.copyWith(color: appTheme.black)), const SizedBox(width: 10), if (author.authorPlaceInRating != null) InfoBouble( radius: 12, backgroundColor: appTheme.whiteA700, - children: [ - Text( - '${author.authorPlaceInRating}-й в рейтинге', - style: TextStyles.textMMediumAuto.copyWith( - color: appTheme.blueA400, - ), - ), - ], + children: [Text('${author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400))], ), ], ), @@ -88,18 +69,8 @@ class AuthorCard extends StatelessWidget { child: Container( width: 28, height: 28, - decoration: BoxDecoration( - color: appTheme.whiteA700, - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: SvgPicture.asset( - 'assets/icons/arrow.svg', - width: 12, - height: 12, - fit: BoxFit.none, - ), - ), + decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(8)), + child: Center(child: SvgPicture.asset('assets/icons/arrow.svg', width: 12, height: 12, fit: BoxFit.none)), ), ), ], diff --git a/lib/features/diets/presentation/widgets/more_options.dart b/lib/features/diets/presentation/widgets/more_options.dart index 4c28e5c9..480b9915 100644 --- a/lib/features/diets/presentation/widgets/more_options.dart +++ b/lib/features/diets/presentation/widgets/more_options.dart @@ -1,5 +1,6 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; @@ -35,7 +36,7 @@ class MoreOptions extends StatelessWidget { onTap: disableActions == true ? null : () { - Clipboard.setData(ClipboardData(text: '$baseUrl/ru/$path')); + Clipboard.setData(ClipboardData(text: '${sl().baseUrl}/ru/$path')); showInfoToast(s.link_was_copied); }, child: Row( @@ -62,7 +63,7 @@ class MoreOptions extends StatelessWidget { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/$path',), + uri: Uri.parse('${sl().baseUrl}/ru/$path',), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/presentation/widgets/blog_post_widget.dart b/lib/features/feed/presentation/widgets/blog_post_widget.dart index e001a038..261e421a 100644 --- a/lib/features/feed/presentation/widgets/blog_post_widget.dart +++ b/lib/features/feed/presentation/widgets/blog_post_widget.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; @@ -64,7 +65,7 @@ class _BlogPostWidgetState extends State { authorName: state.feedBlogPostList[widget.index].author.authorName, authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https') ? state.feedBlogPostList[widget.index].author.authorProfileIcon - : baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, + : sl().baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe, ), ); @@ -75,7 +76,7 @@ class _BlogPostWidgetState extends State { foregroundImage: NetworkImage( state.feedBlogPostList[widget.index].author.authorProfileIcon.contains('https') ? state.feedBlogPostList[widget.index].author.authorProfileIcon - : baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, + : sl().baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), @@ -157,15 +158,15 @@ class _BlogPostWidgetState extends State { onTap: () { context.pushTransparentRoute( rootNavigator: true, - PhotoViewScreen(imageUrls: ['$baseUrl/storage/${state.feedBlogPostList[widget.index].image}'], index: 0), + PhotoViewScreen(imageUrls: ['${sl().baseUrl}/storage/${state.feedBlogPostList[widget.index].image}'], index: 0), ); }, child: Hero( - tag: '$baseUrl/storage/${state.feedBlogPostList[widget.index].image}', + tag: '${sl().baseUrl}/storage/${state.feedBlogPostList[widget.index].image}', child: CachedNetworkImage( fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, - imageUrl: '$baseUrl/storage/${state.feedBlogPostList[widget.index].image}', + imageUrl: '${sl().baseUrl}/storage/${state.feedBlogPostList[widget.index].image}', errorWidget: (context, str, object) => const SizedBox.shrink(), ), ), @@ -219,7 +220,7 @@ class _BlogPostWidgetState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}'), + uri: Uri.parse('${sl().baseUrl}/ru/blog_post/${state.feedBlogPostList[widget.index].id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/presentation/widgets/comments_widget.dart b/lib/features/feed/presentation/widgets/comments_widget.dart index 3617fe33..b313ecaa 100644 --- a/lib/features/feed/presentation/widgets/comments_widget.dart +++ b/lib/features/feed/presentation/widgets/comments_widget.dart @@ -1,5 +1,5 @@ +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/comments/presentation/comments_actions_bottom_sheet.dart'; -import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; @@ -99,13 +99,13 @@ class _CommentsWidgetState extends State { extra: Author( authorSlug: comment.authorSlug, authorName: comment.authorName, - authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon, + authorProfileIcon: comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : sl().baseUrl + comment.authorProfileIcon, authorIsMe: isMe, ), ); }, child: CircleAvatar( - foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon), + foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : sl().baseUrl + comment.authorProfileIcon), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: radius, ), diff --git a/lib/features/feed/presentation/widgets/consultant_widget.dart b/lib/features/feed/presentation/widgets/consultant_widget.dart index 3b4a4b80..57d34b07 100644 --- a/lib/features/feed/presentation/widgets/consultant_widget.dart +++ b/lib/features/feed/presentation/widgets/consultant_widget.dart @@ -1,5 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; @@ -14,10 +14,7 @@ import 'package:go_router/go_router.dart'; class ConsultantWidget extends StatelessWidget { final FeedConsultantModel consultant; - const ConsultantWidget({ - super.key, - required this.consultant, - }); + const ConsultantWidget({super.key, required this.consultant}); @override Widget build(BuildContext context) { @@ -36,10 +33,7 @@ class ConsultantWidget extends StatelessWidget { borderRadius: BorderRadius.circular(20), child: Ink( padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: const Color(0xFFF6F6F6), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -51,46 +45,32 @@ class ConsultantWidget extends StatelessWidget { children: [ Ink( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)), child: Text( '${consultant.ratingPlace}', - style: TextStyle( - fontSize: 20, - color: appTheme.blueA400, - fontWeight: FontWeight.w600, - ), + style: TextStyle(fontSize: 20, color: appTheme.blueA400, fontWeight: FontWeight.w600), ), ), const SizedBox(width: 8), - CircleAvatar( - foregroundImage: NetworkImage(baseUrl + consultant.photo), - backgroundImage: const AssetImage('assets/images/no_avatar.png'), - ), + if (consultant.photo.contains('https')) + CircleAvatar(foregroundImage: NetworkImage(consultant.photo), backgroundImage: const AssetImage('assets/images/no_avatar.png')) + else + CircleAvatar( + foregroundImage: NetworkImage(sl().baseUrl + consultant.photo), + backgroundImage: const AssetImage('assets/images/no_avatar.png'), + ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( consultant.name, - style: const TextStyle( - height: 1, - fontWeight: FontWeight.w500, - fontSize: 15, - color: Color(0xFF323F4B), - ), + style: const TextStyle(height: 1, fontWeight: FontWeight.w500, fontSize: 15, color: Color(0xFF323F4B)), ), const SizedBox(height: 4), Text( 'Рейтинг: ${consultant.totalScores}', - style: TextStyle( - color: appTheme.blueA400, - height: 1, - fontWeight: FontWeight.w500, - fontSize: 12, - ), + style: TextStyle(color: appTheme.blueA400, height: 1, fontWeight: FontWeight.w500, fontSize: 12), ), ], ), @@ -98,21 +78,13 @@ class ConsultantWidget extends StatelessWidget { ), Ink( padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: Colors.white, - ), - child: SvgPicture.asset( - 'assets/icons/go_to_arrow.svg', - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: Colors.white), + child: SvgPicture.asset('assets/icons/go_to_arrow.svg'), ), ], ), const SizedBox(height: 8), - Text( - consultant.description, - maxLines: 2, - ), + Text(consultant.description, maxLines: 2), if (consultant.badges.isNotEmpty) const SizedBox(height: 8), _badges(consultant.badges, context), ], @@ -128,16 +100,8 @@ class ConsultantWidget extends StatelessWidget { children: [ for (int i = 0; i < badges.length; i++) ...[ badges[i].image.contains('svg') - ? SvgPicture.network( - '$baseUrl/storage/${badges[i].image}', - width: 28, - height: 28, - ) - : CachedNetworkImage( - imageUrl: '$baseUrl/storage/${badges[i].image}', - width: 28, - height: 28, - ), + ? SvgPicture.network('${sl().baseUrl}/storage/${badges[i].image}', width: 28, height: 28) + : CachedNetworkImage(imageUrl: '${sl().baseUrl}/storage/${badges[i].image}', width: 28, height: 28), i < badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(), ], ], diff --git a/lib/features/feed/presentation/widgets/diets_widget.dart b/lib/features/feed/presentation/widgets/diets_widget.dart index d91cdd74..1f39ff72 100644 --- a/lib/features/feed/presentation/widgets/diets_widget.dart +++ b/lib/features/feed/presentation/widgets/diets_widget.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; import 'package:eatvio_app/core/widgets/info_row.dart'; @@ -56,7 +57,7 @@ class _DietsWidgetState extends State { ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.diet.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.diet.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), @@ -189,7 +190,7 @@ class _DietsWidgetState extends State { final box = context.findRenderObject() as RenderBox?; final result = await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/diet/${widget.diet.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/diet/${widget.diet.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/presentation/widgets/plan_widget.dart b/lib/features/feed/presentation/widgets/plan_widget.dart index 588d61d0..ae43d830 100644 --- a/lib/features/feed/presentation/widgets/plan_widget.dart +++ b/lib/features/feed/presentation/widgets/plan_widget.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; import 'package:eatvio_app/core/widgets/info_row.dart'; @@ -61,7 +62,7 @@ class _PlanWidgetState extends State { ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.plan.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.plan.image}', fit: BoxFit.cover, width: MediaQuery.of(context).size.width, height: 426, @@ -187,7 +188,7 @@ class _PlanWidgetState extends State { decoration: BoxDecoration( shape: BoxShape.circle, image: DecorationImage( - image: NetworkImage(baseUrl + widget.plan.author.authorProfileIcon), + image: NetworkImage(sl().baseUrl + widget.plan.author.authorProfileIcon), fit: BoxFit.cover, ), ), @@ -263,7 +264,7 @@ class _PlanWidgetState extends State { final box = context.findRenderObject() as RenderBox?; final result = await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/diet/${widget.plan.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/diet/${widget.plan.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/presentation/widgets/recipes_widget.dart b/lib/features/feed/presentation/widgets/recipes_widget.dart index c0dbeb39..5fb415b1 100644 --- a/lib/features/feed/presentation/widgets/recipes_widget.dart +++ b/lib/features/feed/presentation/widgets/recipes_widget.dart @@ -1,8 +1,9 @@ // ignore_for_file: dead_code import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; import 'package:eatvio_app/core/widgets/bookmark_button.dart'; @@ -75,7 +76,7 @@ class _RecipesWidgetState extends State { ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.recipe.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.recipe.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), @@ -231,7 +232,7 @@ class _RecipesWidgetState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/recipe/${widget.recipe.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/feed/repositories/feed_repository.dart b/lib/features/feed/repositories/feed_repository.dart index 108a8b11..e540486f 100644 --- a/lib/features/feed/repositories/feed_repository.dart +++ b/lib/features/feed/repositories/feed_repository.dart @@ -133,9 +133,9 @@ class FeedRepository { } } - Future> getConsultants() async { + Future> getConsultants([String? name]) async { try { - return (await feedService.getConsultants()).data; + return (await feedService.getConsultants(name: name)).data; } catch (e) { rethrow; } diff --git a/lib/features/feed/services/feed_service.dart b/lib/features/feed/services/feed_service.dart index 265c2d38..c756f8c7 100644 --- a/lib/features/feed/services/feed_service.dart +++ b/lib/features/feed/services/feed_service.dart @@ -16,15 +16,10 @@ abstract class FeedService { factory FeedService(Dio dio, {String baseUrl}) = _FeedService; @GET('/api/blogPosts/index') - Future getBlogPostList({ - @Query('page') required int page, - @Query('query_search') String? query, - }); + Future getBlogPostList({@Query('page') required int page, @Query('query_search') String? query}); @GET('/api/blogPosts/show/{postId}') - Future getBlogPost({ - @Path('postId') required int postId, - }); + Future getBlogPost({@Path('postId') required int postId}); @GET('/api/recipes/index') Future getRecipes({ @@ -53,88 +48,50 @@ abstract class FeedService { }); @GET('/api/blogPosts/{id}/comments/show') - Future getBlogPostComments({ - @Path() required String id, - }); + Future getBlogPostComments({@Path() required String id}); @POST('/api/blogPosts/{id}/comment/create') - Future sendBlogPostComment({ - @Path() required String id, - @Part() required String body, - @Part() int? parentCommentId, - }); + Future sendBlogPostComment({@Path() required String id, @Part() required String body, @Part() int? parentCommentId}); @GET('/api/diets/{id}/comments/show') - Future getDietsComments({ - @Path() required String id, - }); + Future getDietsComments({@Path() required String id}); @POST('/api/diets/{id}/comment/create') - Future sendDietsComment({ - @Path() required String id, - @Part() required String body, - @Part() int? parentCommentId, - }); + Future sendDietsComment({@Path() required String id, @Part() required String body, @Part() int? parentCommentId}); @GET('/api/plans/{slug}/comments/show') - Future getPlansComments({ - @Path() required String slug, - }); + Future getPlansComments({@Path() required String slug}); @POST('/api/plans/{slug}/comment/create') - Future sendPlansComment({ - @Path() required String slug, - @Part() required String body, - @Part() int? parentCommentId, - }); + Future sendPlansComment({@Path() required String slug, @Part() required String body, @Part() int? parentCommentId}); @GET('/api/recipes/{id}/comments/show') - Future getRecipesComments({ - @Path() required String id, - }); + Future getRecipesComments({@Path() required String id}); @POST('/api/recipes/{id}/comment/create') - Future sendRecipesComment({ - @Path() required String id, - @Part() required String body, - @Part() int? parentCommentId, - }); + Future sendRecipesComment({@Path() required String id, @Part() required String body, @Part() int? parentCommentId}); @POST('/api/blogPosts/{id}/like') - Future likeBlogPost({ - @Path() required int id, - }); + Future likeBlogPost({@Path() required int id}); @POST('/api/blogPosts/{id}/unliked') - Future unlikedBlogPost({ - @Path() required int id, - }); + Future unlikedBlogPost({@Path() required int id}); @GET('/api/consultants/index') - Future getConsultants(); + Future getConsultants({@Query('filters[name][\$startsWith]') String? name}); @POST('/api/blogPosts/{id}/view') - Future viewPost({ - @Path() required int id, - }); + Future viewPost({@Path() required int id}); @POST('/api/recipes/{slug}/view') - Future viewRecipe({ - @Path() required String slug, - }); + Future viewRecipe({@Path() required String slug}); @POST('/api/diets/{id}/view') - Future viewDiet({ - @Path() required int id, - }); + Future viewDiet({@Path() required int id}); @POST('/api/plans/{slug}/view') - Future viewPlan({ - @Path() required String slug, - }); + Future viewPlan({@Path() required String slug}); @POST('/api/views') - Future viewContent({ - @Body() required ViewModel viewModel, - }); + Future viewContent({@Body() required ViewModel viewModel}); } diff --git a/lib/features/glossary/presentation/pages/glossary_screen.dart b/lib/features/glossary/presentation/pages/glossary_screen.dart index 1c4dd4cd..a1315953 100644 --- a/lib/features/glossary/presentation/pages/glossary_screen.dart +++ b/lib/features/glossary/presentation/pages/glossary_screen.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -18,7 +19,7 @@ class _GlossaryScreenState extends State { _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..loadRequest(Uri.parse('$baseUrl/login_with_pass_key?id=${widget.passKey}')); + ..loadRequest(Uri.parse('${sl().baseUrl}/login_with_pass_key?id=${widget.passKey}')); } @override diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 77bbbd20..293f5485 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/token_storage.dart'; @@ -64,7 +65,7 @@ class MenuScreen extends StatelessWidget { if (profileModel.roles.contains('consultant')) _divider(context), if (profileModel.roles.contains('consultant')) _tile( - leading: 'assets/icons/finance.svg', + leading: 'assets/icons/consultant_rating_icon.svg', title: 'Мой рейтинг', onTap: () { GoRouter.of(context).push(ConsultantRatingScreen.path); @@ -139,7 +140,7 @@ class MenuScreen extends StatelessWidget { CupertinoDialogAction( onPressed: () async { try { - await sl().post('$baseUrl/api/profile/logout'); + await sl().post('${sl().baseUrl}/api/profile/logout'); } finally { await TokenStorage.removeTokenInStorage(); if (context.mounted) { diff --git a/lib/features/menu/presentation/widgets/blocked_user_widget.dart b/lib/features/menu/presentation/widgets/blocked_user_widget.dart index 494df21c..a107a4df 100644 --- a/lib/features/menu/presentation/widgets/blocked_user_widget.dart +++ b/lib/features/menu/presentation/widgets/blocked_user_widget.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; @@ -33,7 +34,7 @@ class _BlockedUserWidgetState extends State { child: Row( children: [ CircleAvatar( - foregroundImage: NetworkImage(baseUrl + widget.profile.profilePhotoUrl), + foregroundImage: NetworkImage(sl().baseUrl + widget.profile.profilePhotoUrl), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: 28, ), diff --git a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart index 965bd088..d6f41d7f 100644 --- a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart +++ b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; @@ -56,7 +57,7 @@ class _MyDietWidgetState extends State { child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.diet.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.diet.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), @@ -153,7 +154,7 @@ class _MyDietWidgetState extends State { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/diet/${widget.diet.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/diet/${widget.diet.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart index 7ecdc653..5d057be5 100644 --- a/lib/features/my_plans/presentation/widgets/my_plan_widget.dart +++ b/lib/features/my_plans/presentation/widgets/my_plan_widget.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; @@ -42,7 +43,7 @@ class _MyPlanWidgetState extends State { onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'navigateToComments': false}), child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), - child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width), + child: CachedNetworkImage(imageUrl: '${sl().baseUrl}/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width), ), ), Positioned( @@ -157,7 +158,7 @@ class _MyPlanWidgetState extends State { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/plan/${widget.plan.id}'), + uri: Uri.parse('${sl().baseUrl}/ru/plan/${widget.plan.id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/order_info/presentation/widgets/plan_info.dart b/lib/features/order_info/presentation/widgets/plan_info.dart index 6c5e387d..d8f91880 100644 --- a/lib/features/order_info/presentation/widgets/plan_info.dart +++ b/lib/features/order_info/presentation/widgets/plan_info.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; @@ -31,7 +32,7 @@ class PlanInfo extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(8), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/$imagePath', + imageUrl: '${sl().baseUrl}/storage/$imagePath', height: 220, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill, diff --git a/lib/features/plans/presentation/pages/about_plan_screen.dart b/lib/features/plans/presentation/pages/about_plan_screen.dart index b5905664..64a437c1 100644 --- a/lib/features/plans/presentation/pages/about_plan_screen.dart +++ b/lib/features/plans/presentation/pages/about_plan_screen.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; @@ -401,7 +401,7 @@ class _AboutPlanScreenState extends State { Stack( children: [ CachedNetworkImage( - imageUrl: '$baseUrl/storage/${state.planModel?.image}', + imageUrl: '${sl().baseUrl}/storage/${state.planModel?.image}', height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill, diff --git a/lib/features/profile/presentation/pages/profile_screen.dart b/lib/features/profile/presentation/pages/profile_screen.dart index 11ae9b7a..5c25dfe5 100644 --- a/lib/features/profile/presentation/pages/profile_screen.dart +++ b/lib/features/profile/presentation/pages/profile_screen.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_router.dart'; import 'package:eatvio_app/core/const.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; @@ -655,7 +656,7 @@ class _ProfileScreenState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)], ), - child: SvgPicture.network('$baseUrl/storage/${state.profile.badges[i].image}', width: 40, height: 40), + child: SvgPicture.network('${sl().baseUrl}/storage/${state.profile.badges[i].image}', width: 40, height: 40), ) : Tooltip( message: state.profile.badges[i].description, @@ -667,7 +668,7 @@ class _ProfileScreenState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [BoxShadow(color: Colors.black.withAlpha((255 * 0.2).toInt()), blurRadius: 32)], ), - child: CachedNetworkImage(imageUrl: '$baseUrl/storage/${state.profile.badges[i].image}', width: 40, height: 40), + child: CachedNetworkImage(imageUrl: '${sl().baseUrl}/storage/${state.profile.badges[i].image}', width: 40, height: 40), ), i < state.profile.badges.length - 1 ? const SizedBox(width: 8) : const SizedBox.shrink(), ], @@ -803,7 +804,7 @@ class _ProfileScreenState extends State { icon: 'assets/icons/eye.svg', text: 'Посмотреть', onTap: () async { - context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [baseUrl + state.profile.profilePhotoUrl], index: 0)); + context.pushTransparentRoute(rootNavigator: true, PhotoViewScreen(imageUrls: [sl().baseUrl + state.profile.profilePhotoUrl], index: 0)); }, ), if (widget.author == null || widget.author!.authorIsMe) ...[ @@ -885,7 +886,7 @@ class _ProfileScreenState extends State { child: CircleAvatar( radius: 32, foregroundImage: NetworkImage( - state.profile.profilePhotoUrl.contains('https') ? state.profile.profilePhotoUrl : baseUrl + state.profile.profilePhotoUrl, + state.profile.profilePhotoUrl.contains('https') ? state.profile.profilePhotoUrl : sl().baseUrl + state.profile.profilePhotoUrl, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), @@ -960,7 +961,7 @@ class _ProfileScreenState extends State { onTap: () { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( - ShareParams(uri: Uri.parse('$baseUrl/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size), + ShareParams(uri: Uri.parse('${sl().baseUrl}/ru/profile/${state.profile.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size), ); }, ), diff --git a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart index 0f0408d8..7c55c420 100644 --- a/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart +++ b/lib/features/profile/presentation/widgets/profile_blog_post_widget.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dismissible_page/dismissible_page.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_popup.dart'; import 'package:eatvio_app/core/widgets/feed_icon_button.dart'; @@ -69,7 +70,7 @@ class _ProfileBlogPostWidgetState extends State { foregroundImage: NetworkImage( state.blogPosts[widget.index].author.authorProfileIcon.contains('https') ? state.blogPosts[widget.index].author.authorProfileIcon - : baseUrl + state.blogPosts[widget.index].author.authorProfileIcon, + : sl().baseUrl + state.blogPosts[widget.index].author.authorProfileIcon, ), backgroundImage: const AssetImage('assets/images/no_avatar.png'), ), @@ -157,17 +158,17 @@ class _ProfileBlogPostWidgetState extends State { context.pushTransparentRoute( rootNavigator: true, PhotoViewScreen( - imageUrls: ['$baseUrl/storage/${state.blogPosts[widget.index].image}'], + imageUrls: ['${sl().baseUrl}/storage/${state.blogPosts[widget.index].image}'], index: 0, ), ); }, child: Hero( - tag: '$baseUrl/storage/${state.blogPosts[widget.index].image}', + tag: '${sl().baseUrl}/storage/${state.blogPosts[widget.index].image}', child: CachedNetworkImage( fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, - imageUrl: '$baseUrl/storage/${state.blogPosts[widget.index].image}', + imageUrl: '${sl().baseUrl}/storage/${state.blogPosts[widget.index].image}', errorWidget: (context, str, object) => const SizedBox.shrink(), ), ), @@ -226,7 +227,7 @@ class _ProfileBlogPostWidgetState extends State { final box = context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/blog_post/${state.blogPosts[widget.index].id}'), + uri: Uri.parse('${sl().baseUrl}/ru/blog_post/${state.blogPosts[widget.index].id}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/profile/presentation/widgets/sub_widget.dart b/lib/features/profile/presentation/widgets/sub_widget.dart index 27365fd2..d5d3a61a 100644 --- a/lib/features/profile/presentation/widgets/sub_widget.dart +++ b/lib/features/profile/presentation/widgets/sub_widget.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:eatvio_app/features/profile/models/sub_model.dart'; import 'package:eatvio_app/features/profile/presentation/widgets/sub_action_button_widget.dart'; @@ -36,7 +37,7 @@ class _SubWidgetState extends State { child: Row( children: [ CircleAvatar( - foregroundImage: NetworkImage(baseUrl + widget.subModel.profilePhotoUrl), + foregroundImage: NetworkImage(sl().baseUrl + widget.subModel.profilePhotoUrl), backgroundImage: const AssetImage('assets/images/no_avatar.png'), radius: 28, ), diff --git a/lib/features/recipe/presentation/pages/about_recipe_screen.dart b/lib/features/recipe/presentation/pages/about_recipe_screen.dart index f23ac880..efc19a12 100644 --- a/lib/features/recipe/presentation/pages/about_recipe_screen.dart +++ b/lib/features/recipe/presentation/pages/about_recipe_screen.dart @@ -2,7 +2,7 @@ import 'dart:developer'; import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/enums/complained_content.dart'; import 'package:eatvio_app/core/enums/product_type.dart'; import 'package:eatvio_app/core/utils/localization_utils.dart'; @@ -247,7 +247,7 @@ class _AboutRecipeScreenState extends State { Stack( children: [ CachedNetworkImage( - imageUrl: '$baseUrl/storage/${recipeState.recipe.image}', + imageUrl: '${sl().baseUrl}/storage/${recipeState.recipe.image}', height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill, diff --git a/lib/features/recipe/presentation/widgets/my_recipe_widget.dart b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart index 2610ca13..d791135a 100644 --- a/lib/features/recipe/presentation/widgets/my_recipe_widget.dart +++ b/lib/features/recipe/presentation/widgets/my_recipe_widget.dart @@ -1,5 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; @@ -71,7 +71,7 @@ class _MyRecipeWidgetState extends State { child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: CachedNetworkImage( - imageUrl: '$baseUrl/storage/${widget.recipe.image}', + imageUrl: '${sl().baseUrl}/storage/${widget.recipe.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width, ), @@ -162,7 +162,7 @@ class _MyRecipeWidgetState extends State { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( ShareParams( - uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.slug}'), + uri: Uri.parse('${sl().baseUrl}/ru/recipe/${widget.recipe.slug}'), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); diff --git a/lib/features/recipe/presentation/widgets/steps_widget.dart b/lib/features/recipe/presentation/widgets/steps_widget.dart index f4281c81..dbd45602 100644 --- a/lib/features/recipe/presentation/widgets/steps_widget.dart +++ b/lib/features/recipe/presentation/widgets/steps_widget.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; @@ -44,7 +45,7 @@ class StepsWidget extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Image.network( - '$baseUrl/storage/${recipe.steps![index].image}', + '${sl().baseUrl}/storage/${recipe.steps![index].image}', fit: BoxFit.cover, ), ), diff --git a/lib/features/registration/presentation/pages/verify_screen.dart b/lib/features/registration/presentation/pages/verify_screen.dart index 8d5df9df..867af06f 100644 --- a/lib/features/registration/presentation/pages/verify_screen.dart +++ b/lib/features/registration/presentation/pages/verify_screen.dart @@ -1,4 +1,5 @@ -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; @@ -89,7 +90,7 @@ class _VerifyScreenState extends State { }, ), ) - ..loadRequest(Uri.parse('$baseUrl/login_with_pass_key?id=${widget.passKey}')); + ..loadRequest(Uri.parse('${sl().baseUrl}/login_with_pass_key?id=${widget.passKey}')); } @override diff --git a/lib/features/registration/presentation/pages/webview_registration_screen.dart b/lib/features/registration/presentation/pages/webview_registration_screen.dart index 05c0ffc1..6cc777d2 100644 --- a/lib/features/registration/presentation/pages/webview_registration_screen.dart +++ b/lib/features/registration/presentation/pages/webview_registration_screen.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:developer'; -import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart'; @@ -54,7 +54,7 @@ class _WebViewRegistrationScreenState extends State { }, ), ) - ..loadRequest(Uri.parse('$baseUrl/registration/step1')); + ..loadRequest(Uri.parse('${sl().baseUrl}/registration/step1')); _injectJavaScript(); } diff --git a/lib/features/support/presentation/pages/support_screen.dart b/lib/features/support/presentation/pages/support_screen.dart index 6aa44a42..a01a88af 100644 --- a/lib/features/support/presentation/pages/support_screen.dart +++ b/lib/features/support/presentation/pages/support_screen.dart @@ -1,4 +1,6 @@ +import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/generated/l10n.dart'; @@ -71,7 +73,7 @@ class SupportScreen extends StatelessWidget { const SizedBox(height: 12), if (!hidePlans) ...[ InkWell( - onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/public_offer.pdf')), + onTap: () async => await launchUrl(Uri.parse('${sl().baseUrl}/docs/public_offer.pdf')), borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), @@ -81,7 +83,7 @@ class SupportScreen extends StatelessWidget { const SizedBox(height: 8), ], InkWell( - onTap: () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')), + onTap: () async => await launchUrl(Uri.parse('${sl().baseUrl}/docs/privacy.pdf')), borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), @@ -90,7 +92,7 @@ class SupportScreen extends StatelessWidget { ), const SizedBox(height: 8), InkWell( - onTap: () async => await launchUrl(Uri.parse('$baseUrl/company-details')), + onTap: () async => await launchUrl(Uri.parse('${sl().baseUrl}/company-details')), borderRadius: BorderRadius.circular(16), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4), diff --git a/lib/features/welcome/presentation/pages/welcome_screen.dart b/lib/features/welcome/presentation/pages/welcome_screen.dart index 31b4bf0f..d0b8f867 100644 --- a/lib/features/welcome/presentation/pages/welcome_screen.dart +++ b/lib/features/welcome/presentation/pages/welcome_screen.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/app_keys.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:flutter/material.dart'; @@ -79,6 +80,7 @@ class _WelcomeScreenState extends State { ), const SizedBox(height: 24), InkWell( + key: AppKeys.welcomeScreenButton, onTap: () async { final sharedPreferences = await SharedPreferences.getInstance(); await sharedPreferences.setBool('welcomeScreenWasShowed', true); diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index c054a4b5..dc9e4c16 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -66,6 +66,9 @@ class MessageLookup extends MessageLookupByLibrary { "add_card": MessageLookupByLibrary.simpleMessage("Привязать карту"), "add_cover": MessageLookupByLibrary.simpleMessage("Добавьте обложку"), "add_measure": MessageLookupByLibrary.simpleMessage("Добавить замер"), + "added_products_in_glossary": MessageLookupByLibrary.simpleMessage( + "Кол-во продуктов добавленных в глоссарий", + ), "adding_plan": MessageLookupByLibrary.simpleMessage("Добавление плана"), "additional_information": MessageLookupByLibrary.simpleMessage( "Дополнительная информация", diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 6144d39c..1055e664 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2400,6 +2400,16 @@ class S { ); } + /// `Кол-во продуктов добавленных в глоссарий` + String get added_products_in_glossary { + return Intl.message( + 'Кол-во продуктов добавленных в глоссарий', + name: 'added_products_in_glossary', + desc: '', + args: [], + ); + } + /// `Кол-во активных консультаций` String get active_consultations_count { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 57145fb9..1fc575a8 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -300,6 +300,7 @@ "published_recipes_count": "Кол-во опубликованных рецептов", "published_diets_count": "Кол-во опубликованных диет", "consultations_count": "Кол-во проведенных консулдьтаций", + "added_products_in_glossary": "Кол-во продуктов добавленных в глоссарий", "active_consultations_count": "Кол-во активных консультаций", "rating_history": "Начисление рейтинга", "something_went_wrong": "Упс! Что-то пошло не так", diff --git a/lib/main.dart b/lib/main.dart index d716f3fa..af6db638 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,53 +1,16 @@ -import 'dart:io' as io; +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/locator_service.dart' as di; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; -import 'package:eatvio_app/core/views/cubit/views_cubit.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; -import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; -import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; -import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; -import 'package:eatvio_app/features/internet_connection_checker/internet_connection_checker_screen.dart'; -import 'package:eatvio_app/firebase_options.dart'; -import 'package:eatvio_app/generated/l10n.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:eatvio_app/core/env_config.dart'; -import 'package:appmetrica_plugin/appmetrica_plugin.dart'; Future main() async { - WidgetsBinding widgetsBinding = SentryWidgetsFlutterBinding.ensureInitialized(); - FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - - await dotenv.load(fileName: ".env"); - - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - await FirebaseMessaging.instance.requestPermission(); - await di.init(); - - if (kDebugMode) { - io.HttpClient.enableTimelineLogging = true; - } - - AppMetrica.activate(AppMetricaConfig(EnvConfig.appMetricaApiKey)); - AppMetrica.reportEvent('Application started'); - - await SentryFlutter.init((options) { - options.dsn = EnvConfig.sentryDsn; - options.tracesSampleRate = 1.0; - options.debug = false; - }, appRunner: () => runApp(const MyApp())); + await AppInitializer.initialize(buildType: AppBuildType.prod); + await AppInitializer.runAppWithSentry(() => runApp(const MyApp())); } class MyApp extends StatelessWidget { @@ -56,30 +19,15 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiBlocProvider( - providers: [ - BlocProvider(create: (context) => sl()), - BlocProvider(create: (context) => sl()..getNotifications()), - BlocProvider(create: (context) => sl()..loadFeedBlogPostList()), - BlocProvider(create: (context) => sl()..getMyProfile()), - BlocProvider(create: (context) => sl()..getChatRoomsWithLoading()), - ], + providers: sl().globalBlocProviders, child: Portal( child: KeyboardDismisser( child: MaterialApp.router( theme: ThemeHelper().themeData(), routerConfig: AppRouter.appRouter, - builder: (context, child) => BlocProvider( - create: (context) => sl(), - child: InternetConnectionCheckerScreen(child: child!), - ), debugShowCheckedModeBanner: false, - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [Locale.fromSubtags(languageCode: 'ru')], + localizationsDelegates: sl().localizationDelegates, + supportedLocales: sl().supportedLocales, ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 98a4429f..adef8395 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.2.14+74 +version: 1.3.1+80 publish_to: 'none' environment: @@ -88,6 +88,8 @@ dev_dependencies: sdk: flutter flutter_lints: ^6.0.0 + integration_test: + sdk: flutter flutter: uses-material-design: true