fixes
This commit is contained in:
parent
76fadd931e
commit
9fc720c10d
6 changed files with 720 additions and 255 deletions
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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, _) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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: 'Статистика',
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue