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
: 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,
);

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: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<ProfilePage> {
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<ProfilePage> {
_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<ProfilePage> {
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<ProfilePage> {
}
Widget _buildSettingsSection(BuildContext context) {
final theme = Theme.of(context);
return ScopeBuilder<AppScopeContainer>(
builder: (context, appScope) {
if (appScope == null) {
@ -367,17 +626,37 @@ class _ProfilePageState extends State<ProfilePage> {
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<ProfilePage> {
}
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<ProfilePage> {
)
: 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,
),
),
),
],

View file

@ -32,6 +32,7 @@ class StatisticsPage extends StatefulWidget {
class _StatisticsPageState extends State<StatisticsPage> with TickerProviderStateMixin {
late TabController _tabController;
bool _hasLoaded = false;
@override
void initState() {
@ -116,6 +117,18 @@ class _StatisticsPageState extends State<StatisticsPage> 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<StatisticsState>(
stateReadable: userScope.statisticsStateManager,
builder: (context, state, _) {

View file

@ -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<CardViewer> {
late PageController _pageController;
late int _currentIndex;
final Map<int, bool> _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<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
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<UserScope>(
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<CardViewer> {
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 {

View file

@ -25,12 +25,8 @@ class _MainShellState extends State<MainShell> {
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<MainShell> {
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: 'Статистика',

View file

@ -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(