This commit is contained in:
Dmitry 2025-11-27 23:49:42 +03:00
parent 76fadd931e
commit 9fc720c10d
6 changed files with 720 additions and 255 deletions

View file

@ -55,10 +55,10 @@ class PackDtoConverter {
? null ? null
: canOpenForAdVal : canOpenForAdVal
? AssetPackTip( ? AssetPackTip(
'asset:icons/ad.webp', 'icons/ad.webp',
position: PackTipPosition.bottomRight, position: PackTipPosition.bottomRight,
) )
: AssetPackTip('asset:icons/lock.webp', : AssetPackTip('icons/lock.webp',
position: PackTipPosition.bottomRight,), position: PackTipPosition.bottomRight,),
version: model.version, version: model.version,
); );

View file

@ -8,9 +8,6 @@ import 'package:yx_scope_flutter/yx_scope_flutter.dart';
import 'package:yx_state_flutter/yx_state_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/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 /// Profile page
/// ///
@ -156,9 +153,6 @@ class _ProfilePageState extends State<ProfilePage> {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
// Get statistics
final statistics = userScope.statisticsService.getStatistics(user);
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
@ -168,22 +162,26 @@ class _ProfilePageState extends State<ProfilePage> {
_buildUserHeader(context, user), _buildUserHeader(context, user),
const SizedBox(height: 24), 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 // Account info card
_buildAccountInfoCard(context, user), _buildAccountInfoCard(context, user),
const SizedBox(height: 24), const SizedBox(height: 24),
// Subscription section
_buildSubscriptionSection(context, user),
const SizedBox(height: 24),
// Settings section // Settings section
_buildSettingsSection(context), _buildSettingsSection(context),
const SizedBox(height: 24), const SizedBox(height: 24),
// Data & Privacy section
_buildDataPrivacySection(context),
const SizedBox(height: 24),
// About section
_buildAboutSection(context),
const SizedBox(height: 24),
// Logout button // Logout button
_buildLogoutButton(context), _buildLogoutButton(context),
], ],
@ -197,14 +195,40 @@ class _ProfilePageState extends State<ProfilePage> {
final theme = Theme.of(context); final theme = Theme.of(context);
return Card( return Card(
elevation: 2, 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( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(20.0),
child: Row( child: Row(
children: [ children: [
// Avatar // Avatar with shadow
CircleAvatar( Container(
radius: 32, decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: CircleAvatar(
radius: 40,
backgroundColor: theme.colorScheme.primary, backgroundColor: theme.colorScheme.primary,
child: Text( child: Text(
(user.name?.isNotEmpty == true (user.name?.isNotEmpty == true
@ -214,13 +238,14 @@ class _ProfilePageState extends State<ProfilePage> {
: 'U') : 'U')
.toUpperCase(), .toUpperCase(),
style: TextStyle( style: TextStyle(
fontSize: 28, fontSize: 32,
color: theme.colorScheme.onPrimary, color: theme.colorScheme.onPrimary,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
const SizedBox(width: 16), ),
const SizedBox(width: 20),
// User info // User info
Expanded( Expanded(
child: Column( child: Column(
@ -230,42 +255,57 @@ class _ProfilePageState extends State<ProfilePage> {
user.name ?? user.email ?? 'User', user.name ?? user.email ?? 'User',
style: theme.textTheme.titleLarge?.copyWith( style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 22,
), ),
), ),
if (user.email != null) ...[ if (user.email != null) ...[
const SizedBox(height: 4), const SizedBox(height: 6),
Text( Text(
user.email!, user.email!,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: theme.textTheme.bodySmall?.color, color: theme.colorScheme.onSurfaceVariant,
fontSize: 14,
), ),
), ),
], ],
if (user.subscription == true) ...[ if (user.subscription == true) ...[
const SizedBox(height: 8), const SizedBox(height: 12),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 12,
vertical: 4, vertical: 6,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer, gradient: LinearGradient(
borderRadius: BorderRadius.circular(12), 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( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon( Icon(
Icons.star, Icons.star,
size: 16, size: 18,
color: theme.colorScheme.onPrimaryContainer, color: theme.colorScheme.onPrimaryContainer,
), ),
const SizedBox(width: 4), const SizedBox(width: 6),
Text( Text(
'Premium', 'Premium',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onPrimaryContainer, color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 13,
), ),
), ),
], ],
@ -278,76 +318,293 @@ class _ProfilePageState extends State<ProfilePage> {
], ],
), ),
), ),
),
); );
} }
Widget _buildStatisticsSection( Widget _buildSubscriptionSection(BuildContext context, UserDto user) {
BuildContext context, final theme = Theme.of(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';
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( return Card(
elevation: 2, elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(20.0),
child: Text( child: Row(
'Account Info', children: [
style: Theme.of(context).textTheme.titleMedium?.copyWith( 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, 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), const Divider(height: 1),
_StatRow( _StatRow(
icon: Icons.collections_bookmark, icon: Icons.collections_bookmark,
label: 'Packs', label: 'Паки',
value: user.packs.length.toString(), value: user.packs.length.toString(),
color: Colors.blue,
), ),
const Divider(height: 1), const Divider(height: 1),
_StatRow( _StatRow(
icon: Icons.shopping_bag, icon: Icons.shopping_bag,
label: 'Purchases', label: 'Покупки',
value: user.purchases.length.toString(), value: user.purchases.length.toString(),
color: Colors.green,
), ),
], ],
), ),
@ -355,6 +612,8 @@ class _ProfilePageState extends State<ProfilePage> {
} }
Widget _buildSettingsSection(BuildContext context) { Widget _buildSettingsSection(BuildContext context) {
final theme = Theme.of(context);
return ScopeBuilder<AppScopeContainer>( return ScopeBuilder<AppScopeContainer>(
builder: (context, appScope) { builder: (context, appScope) {
if (appScope == null) { if (appScope == null) {
@ -367,18 +626,38 @@ class _ProfilePageState extends State<ProfilePage> {
final isDark = themeState.mode == ThemeMode.dark; final isDark = themeState.mode == ThemeMode.dark;
return Card( return Card(
elevation: 2, elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(20.0),
child: Text( child: Row(
'Settings', children: [
style: Theme.of(context).textTheme.titleMedium?.copyWith( 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, fontWeight: FontWeight.bold,
), ),
), ),
],
),
), ),
const Divider(height: 1), const Divider(height: 1),
ListTile( ListTile(
@ -417,6 +696,8 @@ class _ProfilePageState extends State<ProfilePage> {
} }
Widget _buildLogoutButton(BuildContext context) { Widget _buildLogoutButton(BuildContext context) {
final theme = Theme.of(context);
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton.icon( child: ElevatedButton.icon(
@ -429,10 +710,20 @@ class _ProfilePageState extends State<ProfilePage> {
) )
: const Icon(Icons.logout), : const Icon(Icons.logout),
label: Text( label: Text(
_isLoggingOut ? 'Logging out...' : 'Logout', _isLoggingOut ? 'Выход...' : 'Выйти',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
), ),
style: ElevatedButton.styleFrom( 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.icon,
required this.label, required this.label,
required this.value, required this.value,
this.color,
}); });
final IconData icon; final IconData icon;
final String label; final String label;
final String value; final String value;
final Color? color;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final iconColor = color ?? theme.colorScheme.primary;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 16.0),
child: Row( child: Row(
children: [ 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), const SizedBox(width: 16),
Expanded( Expanded(
child: Text( child: Text(
label, label,
style: const TextStyle(fontSize: 16), style: theme.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500,
), ),
), ),
Text( ),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
value, value,
style: const TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: iconColor,
),
), ),
), ),
], ],

View file

@ -32,6 +32,7 @@ class StatisticsPage extends StatefulWidget {
class _StatisticsPageState extends State<StatisticsPage> with TickerProviderStateMixin { class _StatisticsPageState extends State<StatisticsPage> with TickerProviderStateMixin {
late TabController _tabController; late TabController _tabController;
bool _hasLoaded = false;
@override @override
void initState() { void initState() {
@ -116,6 +117,18 @@ class _StatisticsPageState extends State<StatisticsPage> with TickerProviderStat
return const Center(child: Text('User scope not available')); return const Center(child: Text('User scope not available'));
} }
// Загружаем статистику при первом отображении страницы
if (!_hasLoaded) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
userScope.statisticsStateManager.loadStatistics();
setState(() {
_hasLoaded = true;
});
}
});
}
return StateBuilder<StatisticsState>( return StateBuilder<StatisticsState>(
stateReadable: userScope.statisticsStateManager, stateReadable: userScope.statisticsStateManager,
builder: (context, state, _) { builder: (context, state, _) {

View file

@ -1,8 +1,11 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.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 '../../domain/config/api_config_v2.dart';
import '../../presentation/theme/app_colors.dart'; import '../../presentation/theme/app_colors.dart';
import 'mnemo_text.dart'; import 'mnemo_text.dart';
@ -35,17 +38,24 @@ class _CardViewerState extends State<CardViewer> {
late PageController _pageController; late PageController _pageController;
late int _currentIndex; late int _currentIndex;
final Map<int, bool> _flippedCards = {}; final Map<int, bool> _flippedCards = {};
final FocusNode _focusNode = FocusNode();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_currentIndex = widget.initialIndex; _currentIndex = widget.initialIndex;
_pageController = PageController(initialPage: widget.initialIndex); _pageController = PageController(initialPage: widget.initialIndex);
// Устанавливаем фокус для обработки клавиатуры
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
} }
@override @override
void dispose() { void dispose() {
_pageController.dispose(); _pageController.dispose();
_focusNode.dispose();
super.dispose(); super.dispose();
} }
@ -57,13 +67,60 @@ class _CardViewerState extends State<CardViewer> {
}); });
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final packColor = widget.packColor ?? AppColors.borderGray; final packColor = widget.packColor ?? AppColors.borderGray;
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
return Scaffold( return KeyboardListener(
focusNode: _focusNode,
onKeyEvent: _handleKeyEvent,
child: Scaffold(
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
body: SafeArea( body: SafeArea(
child: Stack( child: Stack(
@ -76,6 +133,8 @@ class _CardViewerState extends State<CardViewer> {
setState(() { setState(() {
_currentIndex = index; _currentIndex = index;
}); });
// Обновляем фокус для обработки клавиатуры
_focusNode.requestFocus();
}, },
itemBuilder: (context, index) { itemBuilder: (context, index) {
return AnimatedBuilder( return AnimatedBuilder(
@ -122,6 +181,54 @@ class _CardViewerState extends State<CardViewer> {
), ),
), ),
// Кнопка избранного
Positioned(
top: 16,
right: 80,
child: ScopeBuilder<UserScope>(
builder: (context, userScope) {
if (userScope == null) {
return const SizedBox.shrink();
}
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( Positioned(
top: 16, top: 16,
@ -145,9 +252,30 @@ class _CardViewerState extends State<CardViewer> {
), ),
), ),
), ),
// Кнопки навигации
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<CardViewer> {
isFront: false, 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 { class _CardSide extends StatelessWidget {

View file

@ -25,12 +25,8 @@ class _MainShellState extends State<MainShell> {
case 0: case 0:
context.go('/home'); context.go('/home');
case 1: case 1:
context.go('/games');
case 2:
context.go('/tasks');
case 3:
context.go('/statistics'); context.go('/statistics');
case 4: case 2:
context.go('/profile'); context.go('/profile');
} }
} }
@ -40,39 +36,38 @@ class _MainShellState extends State<MainShell> {
final location = GoRouterState.of(context).uri.path; final location = GoRouterState.of(context).uri.path;
if (location.startsWith('/home')) { if (location.startsWith('/home')) {
_selectedIndex = 0; _selectedIndex = 0;
} else if (location.startsWith('/games')) {
_selectedIndex = 1;
} else if (location.startsWith('/tasks')) {
_selectedIndex = 2;
} else if (location.startsWith('/statistics')) { } else if (location.startsWith('/statistics')) {
_selectedIndex = 3; _selectedIndex = 1;
} else if (location.startsWith('/profile')) { } else if (location.startsWith('/profile')) {
_selectedIndex = 4; _selectedIndex = 2;
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_updateSelectedIndex(context); _updateSelectedIndex(context);
final theme = Theme.of(context);
return Scaffold( return Scaffold(
body: widget.child, body: widget.child,
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex, currentIndex: _selectedIndex,
onTap: _onItemTapped, 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 [ items: const [
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.home), icon: Icon(Icons.home),
label: 'Темы', label: 'Темы',
), ),
BottomNavigationBarItem(
icon: Icon(Icons.games),
label: 'Игры',
),
BottomNavigationBarItem(
icon: Icon(Icons.assignment),
label: 'Задания',
),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.analytics_outlined), icon: Icon(Icons.analytics_outlined),
label: 'Статистика', label: 'Статистика',

View file

@ -172,20 +172,9 @@ class PackCardVertical extends StatelessWidget {
_ => Alignment.topRight, _ => Alignment.topRight,
}; };
final borderRadius = switch (tip.position) { final borderRadius = const BorderRadius.all(
PackTipPosition.topRight => const BorderRadius.only( Radius.circular(12.0),
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),
),
};
return Positioned.fill( return Positioned.fill(
child: Align( child: Align(