From 9fc720c10dfbe9a96fee11bbc2afeedcc1df2793 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 27 Nov 2025 23:49:42 +0300 Subject: [PATCH] fixes --- .../lib/packs/pack_dto_converter.dart | 4 +- .../pages/profile/profile_page.dart | 615 +++++++++++++----- .../pages/statistics/statistics_page.dart | 13 + .../lib/presentation/widgets/card_viewer.dart | 293 +++++++-- .../lib/presentation/widgets/main_shell.dart | 33 +- .../widgets/pack_card_vertical.dart | 17 +- 6 files changed, 720 insertions(+), 255 deletions(-) diff --git a/mnemo_cards_backend/lib/packs/pack_dto_converter.dart b/mnemo_cards_backend/lib/packs/pack_dto_converter.dart index 03f4637..15bcc7e 100644 --- a/mnemo_cards_backend/lib/packs/pack_dto_converter.dart +++ b/mnemo_cards_backend/lib/packs/pack_dto_converter.dart @@ -55,10 +55,10 @@ class PackDtoConverter { ? null : canOpenForAdVal ? AssetPackTip( - 'asset:icons/ad.webp', + 'icons/ad.webp', position: PackTipPosition.bottomRight, ) - : AssetPackTip('asset:icons/lock.webp', + : AssetPackTip('icons/lock.webp', position: PackTipPosition.bottomRight,), version: model.version, ); diff --git a/mnemo_cards_web_v2/lib/presentation/pages/profile/profile_page.dart b/mnemo_cards_web_v2/lib/presentation/pages/profile/profile_page.dart index 543dea3..827bf5d 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/profile/profile_page.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/profile/profile_page.dart @@ -8,9 +8,6 @@ import 'package:yx_scope_flutter/yx_scope_flutter.dart'; import 'package:yx_state_flutter/yx_state_flutter.dart'; import 'package:mnemo_cards_web_v2/di/app_scope/app_scope_container.dart'; -import 'package:mnemo_cards_web_v2/domain/services/statistics_service.dart'; -import 'package:mnemo_cards_web_v2/presentation/widgets/simple_chart.dart'; -import 'package:mnemo_cards_web_v2/presentation/widgets/stats_card.dart'; /// Profile page /// @@ -156,9 +153,6 @@ class _ProfilePageState extends State { return const Center(child: CircularProgressIndicator()); } - // Get statistics - final statistics = userScope.statisticsService.getStatistics(user); - return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( @@ -168,22 +162,26 @@ class _ProfilePageState extends State { _buildUserHeader(context, user), const SizedBox(height: 24), - // Statistics cards - _buildStatisticsSection(context, statistics), - const SizedBox(height: 24), - - // Daily progress chart - SimpleChart(data: statistics.dailyProgress), - const SizedBox(height: 24), - // Account info card _buildAccountInfoCard(context, user), const SizedBox(height: 24), + // Subscription section + _buildSubscriptionSection(context, user), + const SizedBox(height: 24), + // Settings section _buildSettingsSection(context), const SizedBox(height: 24), + // Data & Privacy section + _buildDataPrivacySection(context), + const SizedBox(height: 24), + + // About section + _buildAboutSection(context), + const SizedBox(height: 24), + // Logout button _buildLogoutButton(context), ], @@ -197,157 +195,416 @@ class _ProfilePageState extends State { final theme = Theme.of(context); return Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - // Avatar - CircleAvatar( - radius: 32, - backgroundColor: theme.colorScheme.primary, - child: Text( - (user.name?.isNotEmpty == true - ? user.name![0] - : user.email?.isNotEmpty == true - ? user.email![0] - : 'U') - .toUpperCase(), - style: TextStyle( - fontSize: 28, - color: theme.colorScheme.onPrimary, - fontWeight: FontWeight.bold, + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + theme.colorScheme.primary.withOpacity(0.1), + theme.colorScheme.primary.withOpacity(0.05), + ], + ), + ), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + children: [ + // Avatar with shadow + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: theme.colorScheme.primary.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], ), - ), - ), - const SizedBox(width: 16), - // User info - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - user.name ?? user.email ?? 'User', - style: theme.textTheme.titleLarge?.copyWith( + child: CircleAvatar( + radius: 40, + backgroundColor: theme.colorScheme.primary, + child: Text( + (user.name?.isNotEmpty == true + ? user.name![0] + : user.email?.isNotEmpty == true + ? user.email![0] + : 'U') + .toUpperCase(), + style: TextStyle( + fontSize: 32, + color: theme.colorScheme.onPrimary, fontWeight: FontWeight.bold, ), ), - if (user.email != null) ...[ - const SizedBox(height: 4), - Text( - user.email!, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodySmall?.color, - ), - ), - ], - if (user.subscription == true) ...[ - const SizedBox(height: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.star, - size: 16, - color: theme.colorScheme.onPrimaryContainer, - ), - const SizedBox(width: 4), - Text( - 'Premium', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onPrimaryContainer, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - ], - ], + ), ), - ), - ], + const SizedBox(width: 20), + // User info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + user.name ?? user.email ?? 'User', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + fontSize: 22, + ), + ), + if (user.email != null) ...[ + const SizedBox(height: 6), + Text( + user.email!, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + fontSize: 14, + ), + ), + ], + if (user.subscription == true) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + theme.colorScheme.primaryContainer, + theme.colorScheme.primaryContainer.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: theme.colorScheme.primary.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.star, + size: 18, + color: theme.colorScheme.onPrimaryContainer, + ), + const SizedBox(width: 6), + Text( + 'Premium', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.bold, + fontSize: 13, + ), + ), + ], + ), + ), + ], + ], + ), + ), + ], + ), ), ), ); } - Widget _buildStatisticsSection( - BuildContext context, - UserStatistics statistics, - ) { - // Format study time - final hours = statistics.studyTime.inHours; - final minutes = statistics.studyTime.inMinutes % 60; - final studyTimeStr = hours > 0 ? '${hours}h ${minutes}m' : '${minutes}m'; + Widget _buildSubscriptionSection(BuildContext context, UserDto user) { + final theme = Theme.of(context); - return Row( - children: [ - Expanded( - child: StatsCard( - icon: Icons.school, - label: 'Learned Words', - value: statistics.learnedWords.toString(), - color: Colors.blue, - ), - ), - const SizedBox(width: 12), - Expanded( - child: StatsCard( - icon: Icons.quiz, - label: 'Tests Completed', - value: statistics.testsCompleted.toString(), - color: Colors.green, - ), - ), - const SizedBox(width: 12), - Expanded( - child: StatsCard( - icon: Icons.timer, - label: 'Study Time', - value: studyTimeStr, - color: Colors.orange, - ), - ), - ], - ); - } - - Widget _buildAccountInfoCard(BuildContext context, UserDto user) { return Card( - elevation: 2, + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'Account Info', - style: Theme.of(context).textTheme.titleMedium?.copyWith( + padding: const EdgeInsets.all(20.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: theme.colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.star, + color: theme.colorScheme.primary, + size: 24, + ), + ), + const SizedBox(width: 12), + Text( + 'Подписка', + style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), + ), + ], + ), + ), + const Divider(height: 1), + Padding( + padding: const EdgeInsets.all(16.0), + child: user.subscription == true + ? Row( + children: [ + Icon( + Icons.check_circle, + color: Colors.green, + size: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Premium активна', + style: theme.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'У вас есть доступ ко всем функциям', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ) + : Row( + children: [ + Icon( + Icons.info_outline, + color: theme.colorScheme.onSurfaceVariant, + size: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Бесплатная версия', + style: theme.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Обновитесь до Premium для доступа ко всем функциям', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildDataPrivacySection(BuildContext context) { + final theme = Theme.of(context); + + return Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: theme.colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.security, + color: theme.colorScheme.primary, + size: 24, + ), + ), + const SizedBox(width: 12), + Text( + 'Данные и приватность', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.download), + title: const Text('Экспорт данных'), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: () { + // TODO: Implement data export + }, + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.delete_outline), + title: const Text('Удалить аккаунт'), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: () { + // TODO: Implement account deletion + }, + ), + ], + ), + ); + } + + Widget _buildAboutSection(BuildContext context) { + final theme = Theme.of(context); + + return Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: theme.colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.info, + color: theme.colorScheme.primary, + size: 24, + ), + ), + const SizedBox(width: 12), + Text( + 'О приложении', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.description), + title: const Text('Версия'), + trailing: const Text('1.0.0'), + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.privacy_tip), + title: const Text('Политика конфиденциальности'), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: () { + // TODO: Open privacy policy + }, + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.description), + title: const Text('Условия использования'), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: () { + // TODO: Open terms of service + }, + ), + ], + ), + ); + } + + Widget _buildAccountInfoCard(BuildContext context, UserDto user) { + final theme = Theme.of(context); + + return Card( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: theme.colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.account_circle, + color: theme.colorScheme.primary, + size: 24, + ), + ), + const SizedBox(width: 12), + Text( + 'Информация об аккаунте', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], ), ), const Divider(height: 1), _StatRow( icon: Icons.collections_bookmark, - label: 'Packs', + label: 'Паки', value: user.packs.length.toString(), + color: Colors.blue, ), const Divider(height: 1), _StatRow( icon: Icons.shopping_bag, - label: 'Purchases', + label: 'Покупки', value: user.purchases.length.toString(), + color: Colors.green, ), ], ), @@ -355,6 +612,8 @@ class _ProfilePageState extends State { } Widget _buildSettingsSection(BuildContext context) { + final theme = Theme.of(context); + return ScopeBuilder( builder: (context, appScope) { if (appScope == null) { @@ -367,17 +626,37 @@ class _ProfilePageState extends State { final isDark = themeState.mode == ThemeMode.dark; return Card( - elevation: 2, + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'Settings', - style: Theme.of(context).textTheme.titleMedium?.copyWith( + padding: const EdgeInsets.all(20.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: theme.colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.settings, + color: theme.colorScheme.primary, + size: 24, + ), + ), + const SizedBox(width: 12), + Text( + 'Настройки', + style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), + ), + ], ), ), const Divider(height: 1), @@ -417,6 +696,8 @@ class _ProfilePageState extends State { } Widget _buildLogoutButton(BuildContext context) { + final theme = Theme.of(context); + return SizedBox( width: double.infinity, child: ElevatedButton.icon( @@ -429,10 +710,20 @@ class _ProfilePageState extends State { ) : const Icon(Icons.logout), label: Text( - _isLoggingOut ? 'Logging out...' : 'Logout', + _isLoggingOut ? 'Выход...' : 'Выйти', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), style: ElevatedButton.styleFrom( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 2, + backgroundColor: theme.colorScheme.errorContainer, + foregroundColor: theme.colorScheme.onErrorContainer, ), ), ); @@ -445,31 +736,53 @@ class _StatRow extends StatelessWidget { required this.icon, required this.label, required this.value, + this.color, }); final IconData icon; final String label; final String value; + final Color? color; @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final iconColor = color ?? theme.colorScheme.primary; + return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 16.0), child: Row( children: [ - Icon(icon, size: 24), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: iconColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, size: 22, color: iconColor), + ), const SizedBox(width: 16), Expanded( child: Text( label, - style: const TextStyle(fontSize: 16), + style: theme.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), ), ), - Text( - value, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: iconColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + value, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: iconColor, + ), ), ), ], diff --git a/mnemo_cards_web_v2/lib/presentation/pages/statistics/statistics_page.dart b/mnemo_cards_web_v2/lib/presentation/pages/statistics/statistics_page.dart index 4615799..4b7dde0 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/statistics/statistics_page.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/statistics/statistics_page.dart @@ -32,6 +32,7 @@ class StatisticsPage extends StatefulWidget { class _StatisticsPageState extends State with TickerProviderStateMixin { late TabController _tabController; + bool _hasLoaded = false; @override void initState() { @@ -116,6 +117,18 @@ class _StatisticsPageState extends State with TickerProviderStat return const Center(child: Text('User scope not available')); } + // Загружаем статистику при первом отображении страницы + if (!_hasLoaded) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + userScope.statisticsStateManager.loadStatistics(); + setState(() { + _hasLoaded = true; + }); + } + }); + } + return StateBuilder( stateReadable: userScope.statisticsStateManager, builder: (context, state, _) { diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart b/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart index a44874e..3b2bcf0 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart @@ -1,8 +1,11 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:yx_scope_flutter/yx_scope_flutter.dart'; +import '../../di/user_scope/user_scope.dart'; import '../../domain/config/api_config_v2.dart'; import '../../presentation/theme/app_colors.dart'; import 'mnemo_text.dart'; @@ -35,17 +38,24 @@ class _CardViewerState extends State { late PageController _pageController; late int _currentIndex; final Map _flippedCards = {}; + final FocusNode _focusNode = FocusNode(); @override void initState() { super.initState(); _currentIndex = widget.initialIndex; _pageController = PageController(initialPage: widget.initialIndex); + + // Устанавливаем фокус для обработки клавиатуры + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode.requestFocus(); + }); } @override void dispose() { _pageController.dispose(); + _focusNode.dispose(); super.dispose(); } @@ -57,95 +67,213 @@ class _CardViewerState extends State { }); } + void _goToPrevious() { + if (widget.cards.isEmpty) return; + + final newIndex = _currentIndex == 0 + ? widget.cards.length - 1 // Циклический переход: с первой на последнюю + : _currentIndex - 1; + + _pageController.animateToPage( + newIndex, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + + void _goToNext() { + if (widget.cards.isEmpty) return; + + final newIndex = _currentIndex == widget.cards.length - 1 + ? 0 // Циклический переход: с последней на первую + : _currentIndex + 1; + + _pageController.animateToPage( + newIndex, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + + bool _handleKeyEvent(KeyEvent event) { + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.space) { + _goToNext(); + return true; + } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + _goToPrevious(); + return true; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + _goToNext(); + return true; + } + } + return false; + } + @override Widget build(BuildContext context) { final packColor = widget.packColor ?? AppColors.borderGray; final theme = Theme.of(context); final colorScheme = theme.colorScheme; - return Scaffold( - backgroundColor: theme.scaffoldBackgroundColor, - body: SafeArea( - child: Stack( - children: [ - // PageView с карточками - PageView.builder( - controller: _pageController, - itemCount: widget.cards.length, - onPageChanged: (index) { - setState(() { - _currentIndex = index; - }); - }, - itemBuilder: (context, index) { - return AnimatedBuilder( - animation: _pageController, - builder: (context, child) { - double value = 1.0; - if (_pageController.position.haveDimensions) { - value = (_pageController.page ?? 0) - index; - value = (1 - (value.abs() * 0.3)).clamp(0.7, 1.0); + return KeyboardListener( + focusNode: _focusNode, + onKeyEvent: _handleKeyEvent, + child: Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + body: SafeArea( + child: Stack( + children: [ + // PageView с карточками + PageView.builder( + controller: _pageController, + itemCount: widget.cards.length, + onPageChanged: (index) { + setState(() { + _currentIndex = index; + }); + // Обновляем фокус для обработки клавиатуры + _focusNode.requestFocus(); + }, + itemBuilder: (context, index) { + return AnimatedBuilder( + animation: _pageController, + builder: (context, child) { + double value = 1.0; + if (_pageController.position.haveDimensions) { + value = (_pageController.page ?? 0) - index; + value = (1 - (value.abs() * 0.3)).clamp(0.7, 1.0); + } + + return Center( + child: Transform.scale( + scale: value, + child: _buildCard( + widget.cards[index], + index, + packColor, + ), + ), + ); + }, + ); + }, + ), + + // Кнопка закрытия + Positioned( + top: 16, + left: 16, + child: IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: colorScheme.surface.withOpacity(0.7), + shape: BoxShape.circle, + ), + child: Icon( + Icons.close, + color: colorScheme.onSurface, + ), + ), + ), + ), + + // Кнопка избранного + Positioned( + top: 16, + right: 80, + child: ScopeBuilder( + builder: (context, userScope) { + if (userScope == null) { + return const SizedBox.shrink(); } - return Center( - child: Transform.scale( - scale: value, - child: _buildCard( - widget.cards[index], - index, - packColor, + if (widget.cards.isEmpty) { + return const SizedBox.shrink(); + } + + final currentCard = widget.cards[_currentIndex]; + final isFavorite = userScope.favoritesStateManager.isFavorite(currentCard.id); + + return IconButton( + onPressed: () async { + await userScope.favoritesStateManager.toggleFavorite(currentCard.id); + if (mounted) { + setState(() {}); + } + }, + icon: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: colorScheme.surface.withOpacity(0.9), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: colorScheme.shadow.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + isFavorite ? Icons.favorite : Icons.favorite_border, + color: isFavorite ? Colors.red : colorScheme.onSurface, + size: 24, ), ), ); }, - ); - }, - ), - - // Кнопка закрытия - Positioned( - top: 16, - left: 16, - child: IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: Container( - padding: const EdgeInsets.all(8), + ), + ), + + // Индикатор текущей карточки + Positioned( + top: 16, + right: 16, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), decoration: BoxDecoration( color: colorScheme.surface.withOpacity(0.7), - shape: BoxShape.circle, + borderRadius: BorderRadius.circular(20), ), - child: Icon( - Icons.close, - color: colorScheme.onSurface, + child: Text( + '${_currentIndex + 1} / ${widget.cards.length}', + style: TextStyle( + color: colorScheme.onSurface, + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), ), ), - ), - - // Индикатор текущей карточки - Positioned( - top: 16, - right: 16, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - color: colorScheme.surface.withOpacity(0.7), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${_currentIndex + 1} / ${widget.cards.length}', - style: TextStyle( - color: colorScheme.onSurface, - fontSize: 16, - fontWeight: FontWeight.w600, - ), + + // Кнопки навигации + Positioned( + bottom: 40, + left: 20, + child: _buildNavigationButton( + icon: Icons.arrow_back, + onPressed: _goToPrevious, + colorScheme: colorScheme, ), ), - ), - ], + Positioned( + bottom: 40, + right: 20, + child: _buildNavigationButton( + icon: Icons.arrow_forward, + onPressed: _goToNext, + colorScheme: colorScheme, + ), + ), + ], + ), ), ), ); @@ -211,6 +339,33 @@ class _CardViewerState extends State { isFront: false, ); } + + Widget _buildNavigationButton({ + required IconData icon, + required VoidCallback onPressed, + required ColorScheme colorScheme, + }) { + return Container( + decoration: BoxDecoration( + color: colorScheme.surface.withOpacity(0.9), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: colorScheme.shadow.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: IconButton( + onPressed: onPressed, + icon: Icon(icon), + iconSize: 28, + color: colorScheme.onSurface, + padding: const EdgeInsets.all(12), + ), + ); + } } class _CardSide extends StatelessWidget { diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/main_shell.dart b/mnemo_cards_web_v2/lib/presentation/widgets/main_shell.dart index 9444e47..b7c4c6e 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/main_shell.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/main_shell.dart @@ -25,12 +25,8 @@ class _MainShellState extends State { case 0: context.go('/home'); case 1: - context.go('/games'); - case 2: - context.go('/tasks'); - case 3: context.go('/statistics'); - case 4: + case 2: context.go('/profile'); } } @@ -40,39 +36,38 @@ class _MainShellState extends State { final location = GoRouterState.of(context).uri.path; if (location.startsWith('/home')) { _selectedIndex = 0; - } else if (location.startsWith('/games')) { - _selectedIndex = 1; - } else if (location.startsWith('/tasks')) { - _selectedIndex = 2; } else if (location.startsWith('/statistics')) { - _selectedIndex = 3; + _selectedIndex = 1; } else if (location.startsWith('/profile')) { - _selectedIndex = 4; + _selectedIndex = 2; } } @override Widget build(BuildContext context) { _updateSelectedIndex(context); + final theme = Theme.of(context); return Scaffold( body: widget.child, bottomNavigationBar: BottomNavigationBar( currentIndex: _selectedIndex, onTap: _onItemTapped, + type: BottomNavigationBarType.fixed, + selectedItemColor: theme.colorScheme.primary, + unselectedItemColor: theme.colorScheme.onSurfaceVariant, + backgroundColor: theme.colorScheme.surface, + selectedLabelStyle: const TextStyle( + fontWeight: FontWeight.w600, + ), + unselectedLabelStyle: const TextStyle( + fontWeight: FontWeight.w500, + ), items: const [ BottomNavigationBarItem( icon: Icon(Icons.home), label: 'Темы', ), - BottomNavigationBarItem( - icon: Icon(Icons.games), - label: 'Игры', - ), - BottomNavigationBarItem( - icon: Icon(Icons.assignment), - label: 'Задания', - ), BottomNavigationBarItem( icon: Icon(Icons.analytics_outlined), label: 'Статистика', diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_vertical.dart b/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_vertical.dart index c18fd51..c2cd3ed 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_vertical.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_vertical.dart @@ -172,20 +172,9 @@ class PackCardVertical extends StatelessWidget { _ => Alignment.topRight, }; - final borderRadius = switch (tip.position) { - PackTipPosition.topRight => const BorderRadius.only( - topRight: Radius.circular(12.0), - bottomLeft: Radius.circular(12.0), - ), - PackTipPosition.bottomRight => const BorderRadius.only( - bottomRight: Radius.circular(12.0), - topLeft: Radius.circular(12.0), - ), - _ => const BorderRadius.only( - topRight: Radius.circular(12.0), - bottomLeft: Radius.circular(12.0), - ), - }; + final borderRadius = const BorderRadius.all( + Radius.circular(12.0), + ); return Positioned.fill( child: Align(