ыегаа

This commit is contained in:
Dmitry 2024-06-16 22:01:33 +03:00
parent fc99d384bc
commit 775ba356ef
25 changed files with 585 additions and 247 deletions

BIN
icons/gift.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

BIN
icons/next.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

BIN
icons/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

BIN
icons/tg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -158,6 +158,15 @@ class EditUser {
);
}),
_Payments(adminApi, userDto),
Text(
'Stats: ${userDto.userDataDto?.allWordsStatistics?.total} words',
),
Text(
'Stats: ${userDto.userDataDto?.allTestsStatistics?.tests.length} tests',
),
Text(
'Last test session: ${userDto.userDataDto?.allTestsStatistics?.lastSessionToken}',
),
],
),
),

View file

@ -7,7 +7,6 @@ import 'package:mnemo_cards/features/packs/packs_api.dart';
import 'package:mnemo_cards/managers/repository/api.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
class AdminApi with Api {
final Dio _dio;

View file

@ -83,19 +83,19 @@ class _AllUsersState extends State<AllUsers> {
}
List<UserDto> buildUsersList() {
final filteredCards = users.where((dto) {
final filteredUsers = users.where((dto) {
final filterOk = filter.isEmpty ||
dto.name.toString().toLowerCase().contains(filter) ||
dto.id.toString().toLowerCase().contains(filter) ||
dto.packs.any((p) => p.toLowerCase().contains(filter));
return filterOk;
});
return filteredCards.toList();
return filteredUsers.toList();
}
@override
Widget build(BuildContext context) {
final filteredCards = buildUsersList();
final filteredUsers = buildUsersList();
return Scaffold(
body: SafeArea(
child: Stack(
@ -110,9 +110,9 @@ class _AllUsersState extends State<AllUsers> {
_currentId == null
? 'Все'
: _currentUser?.name ?? 'Пак ${_currentUser?.id}',
subtitle: filteredCards.isEmpty
subtitle: filteredUsers.isEmpty
? 'Нет пользователей'
: '${filteredCards.length} пользователя',
: '${filteredUsers.length} пользователя',
hasPopButton: true,
trail: IconButton(
onPressed: () async {
@ -148,8 +148,8 @@ class _AllUsersState extends State<AllUsers> {
],
),
);
else if (filteredCards.length > index - 1)
return userWidget(filteredCards[index - 1]);
else if (filteredUsers.length > index - 1)
return userWidget(filteredUsers[index - 1]);
else
return null;
}),

View file

@ -31,6 +31,11 @@ class AppRouter extends $AppRouter {
transitionsBuilder: TransitionsBuilders.slideLeftWithFade,
durationInMilliseconds: 200,
),
CustomRoute(
page: SettingsPage.page,
transitionsBuilder: TransitionsBuilders.slideLeftWithFade,
durationInMilliseconds: 200,
),
CustomRoute(
// initial: !locator.userManager.hasUser,
page: AuthPage.page,

View file

@ -8,33 +8,34 @@
// coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:ui' as _i10;
import 'dart:ui' as _i11;
import 'package:auto_route/auto_route.dart' as _i8;
import 'package:flutter/material.dart' as _i11;
import 'package:mnemo_cards/features/tests/test_page.dart' as _i7;
import 'package:auto_route/auto_route.dart' as _i9;
import 'package:flutter/material.dart' as _i12;
import 'package:mnemo_cards/features/tests/test_page.dart' as _i8;
import 'package:mnemo_cards/main.dart' as _i5;
import 'package:mnemo_cards/pages/auth_page.dart' as _i1;
import 'package:mnemo_cards/pages/home_page.dart' as _i4;
import 'package:mnemo_cards/pages/profile_page.dart' as _i6;
import 'package:mnemo_cards/pages/settgins_page.dart' as _i7;
import 'package:mnemo_cards/widgets/card_game/card_game_page.dart' as _i2;
import 'package:mnemo_cards/widgets/card_pack/card_pack_page.dart' as _i3;
import 'package:mnemo_cards_common/mnemo_cards_common.dart' as _i9;
import 'package:mnemo_cards_common/mnemo_cards_common.dart' as _i10;
abstract class $AppRouter extends _i8.RootStackRouter {
abstract class $AppRouter extends _i9.RootStackRouter {
$AppRouter({super.navigatorKey});
@override
final Map<String, _i8.PageFactory> pagesMap = {
final Map<String, _i9.PageFactory> pagesMap = {
AuthPage.name: (routeData) {
return _i8.AutoRoutePage<dynamic>(
return _i9.AutoRoutePage<dynamic>(
routeData: routeData,
child: _i1.AuthPage(),
);
},
CardGamePage.name: (routeData) {
final args = routeData.argsAs<CardGamePageArgs>();
return _i8.AutoRoutePage<dynamic>(
return _i9.AutoRoutePage<dynamic>(
routeData: routeData,
child: _i2.CardGamePage(
cards: args.cards,
@ -48,7 +49,7 @@ abstract class $AppRouter extends _i8.RootStackRouter {
},
CardPackPage.name: (routeData) {
final args = routeData.argsAs<CardPackPageArgs>();
return _i8.AutoRoutePage<dynamic>(
return _i9.AutoRoutePage<dynamic>(
routeData: routeData,
child: _i3.CardPackPage(
args.previewDto,
@ -57,28 +58,34 @@ abstract class $AppRouter extends _i8.RootStackRouter {
);
},
HomePage.name: (routeData) {
return _i8.AutoRoutePage<dynamic>(
return _i9.AutoRoutePage<dynamic>(
routeData: routeData,
child: _i4.HomePage(),
);
},
MainTabsPage.name: (routeData) {
return _i8.AutoRoutePage<dynamic>(
return _i9.AutoRoutePage<dynamic>(
routeData: routeData,
child: const _i5.MainTabsPage(),
);
},
ProfilePage.name: (routeData) {
return _i8.AutoRoutePage<dynamic>(
return _i9.AutoRoutePage<dynamic>(
routeData: routeData,
child: _i6.ProfilePage(),
);
},
SettingsPage.name: (routeData) {
return _i9.AutoRoutePage<dynamic>(
routeData: routeData,
child: _i7.SettingsPage(),
);
},
TestPage.name: (routeData) {
final args = routeData.argsAs<TestPageArgs>();
return _i8.AutoRoutePage<dynamic>(
return _i9.AutoRoutePage<dynamic>(
routeData: routeData,
child: _i7.TestPage(args.testId),
child: _i8.TestPage(args.testId),
);
},
};
@ -86,8 +93,8 @@ abstract class $AppRouter extends _i8.RootStackRouter {
/// generated route for
/// [_i1.AuthPage]
class AuthPage extends _i8.PageRouteInfo<void> {
const AuthPage({List<_i8.PageRouteInfo>? children})
class AuthPage extends _i9.PageRouteInfo<void> {
const AuthPage({List<_i9.PageRouteInfo>? children})
: super(
AuthPage.name,
initialChildren: children,
@ -95,20 +102,20 @@ class AuthPage extends _i8.PageRouteInfo<void> {
static const String name = 'AuthPage';
static const _i8.PageInfo<void> page = _i8.PageInfo<void>(name);
static const _i9.PageInfo<void> page = _i9.PageInfo<void>(name);
}
/// generated route for
/// [_i2.CardGamePage]
class CardGamePage extends _i8.PageRouteInfo<CardGamePageArgs> {
class CardGamePage extends _i9.PageRouteInfo<CardGamePageArgs> {
CardGamePage({
required List<_i9.GameCardDto> cards,
required _i10.Color? color,
required List<_i10.GameCardDto> cards,
required _i11.Color? color,
int? startCardId,
bool infinite = false,
bool shuffle = true,
_i11.Key? key,
List<_i8.PageRouteInfo>? children,
_i12.Key? key,
List<_i9.PageRouteInfo>? children,
}) : super(
CardGamePage.name,
args: CardGamePageArgs(
@ -124,8 +131,8 @@ class CardGamePage extends _i8.PageRouteInfo<CardGamePageArgs> {
static const String name = 'CardGamePage';
static const _i8.PageInfo<CardGamePageArgs> page =
_i8.PageInfo<CardGamePageArgs>(name);
static const _i9.PageInfo<CardGamePageArgs> page =
_i9.PageInfo<CardGamePageArgs>(name);
}
class CardGamePageArgs {
@ -138,9 +145,9 @@ class CardGamePageArgs {
this.key,
});
final List<_i9.GameCardDto> cards;
final List<_i10.GameCardDto> cards;
final _i10.Color? color;
final _i11.Color? color;
final int? startCardId;
@ -148,7 +155,7 @@ class CardGamePageArgs {
final bool shuffle;
final _i11.Key? key;
final _i12.Key? key;
@override
String toString() {
@ -158,11 +165,11 @@ class CardGamePageArgs {
/// generated route for
/// [_i3.CardPackPage]
class CardPackPage extends _i8.PageRouteInfo<CardPackPageArgs> {
class CardPackPage extends _i9.PageRouteInfo<CardPackPageArgs> {
CardPackPage({
required _i9.CardPackPreviewDto previewDto,
_i11.Key? key,
List<_i8.PageRouteInfo>? children,
required _i10.CardPackPreviewDto previewDto,
_i12.Key? key,
List<_i9.PageRouteInfo>? children,
}) : super(
CardPackPage.name,
args: CardPackPageArgs(
@ -174,8 +181,8 @@ class CardPackPage extends _i8.PageRouteInfo<CardPackPageArgs> {
static const String name = 'CardPackPage';
static const _i8.PageInfo<CardPackPageArgs> page =
_i8.PageInfo<CardPackPageArgs>(name);
static const _i9.PageInfo<CardPackPageArgs> page =
_i9.PageInfo<CardPackPageArgs>(name);
}
class CardPackPageArgs {
@ -184,9 +191,9 @@ class CardPackPageArgs {
this.key,
});
final _i9.CardPackPreviewDto previewDto;
final _i10.CardPackPreviewDto previewDto;
final _i11.Key? key;
final _i12.Key? key;
@override
String toString() {
@ -196,8 +203,8 @@ class CardPackPageArgs {
/// generated route for
/// [_i4.HomePage]
class HomePage extends _i8.PageRouteInfo<void> {
const HomePage({List<_i8.PageRouteInfo>? children})
class HomePage extends _i9.PageRouteInfo<void> {
const HomePage({List<_i9.PageRouteInfo>? children})
: super(
HomePage.name,
initialChildren: children,
@ -205,13 +212,13 @@ class HomePage extends _i8.PageRouteInfo<void> {
static const String name = 'HomePage';
static const _i8.PageInfo<void> page = _i8.PageInfo<void>(name);
static const _i9.PageInfo<void> page = _i9.PageInfo<void>(name);
}
/// generated route for
/// [_i5.MainTabsPage]
class MainTabsPage extends _i8.PageRouteInfo<void> {
const MainTabsPage({List<_i8.PageRouteInfo>? children})
class MainTabsPage extends _i9.PageRouteInfo<void> {
const MainTabsPage({List<_i9.PageRouteInfo>? children})
: super(
MainTabsPage.name,
initialChildren: children,
@ -219,13 +226,13 @@ class MainTabsPage extends _i8.PageRouteInfo<void> {
static const String name = 'MainTabsPage';
static const _i8.PageInfo<void> page = _i8.PageInfo<void>(name);
static const _i9.PageInfo<void> page = _i9.PageInfo<void>(name);
}
/// generated route for
/// [_i6.ProfilePage]
class ProfilePage extends _i8.PageRouteInfo<void> {
const ProfilePage({List<_i8.PageRouteInfo>? children})
class ProfilePage extends _i9.PageRouteInfo<void> {
const ProfilePage({List<_i9.PageRouteInfo>? children})
: super(
ProfilePage.name,
initialChildren: children,
@ -233,15 +240,29 @@ class ProfilePage extends _i8.PageRouteInfo<void> {
static const String name = 'ProfilePage';
static const _i8.PageInfo<void> page = _i8.PageInfo<void>(name);
static const _i9.PageInfo<void> page = _i9.PageInfo<void>(name);
}
/// generated route for
/// [_i7.TestPage]
class TestPage extends _i8.PageRouteInfo<TestPageArgs> {
/// [_i7.SettingsPage]
class SettingsPage extends _i9.PageRouteInfo<void> {
const SettingsPage({List<_i9.PageRouteInfo>? children})
: super(
SettingsPage.name,
initialChildren: children,
);
static const String name = 'SettingsPage';
static const _i9.PageInfo<void> page = _i9.PageInfo<void>(name);
}
/// generated route for
/// [_i8.TestPage]
class TestPage extends _i9.PageRouteInfo<TestPageArgs> {
TestPage({
required String testId,
List<_i8.PageRouteInfo>? children,
List<_i9.PageRouteInfo>? children,
}) : super(
TestPage.name,
args: TestPageArgs(testId: testId),
@ -250,8 +271,8 @@ class TestPage extends _i8.PageRouteInfo<TestPageArgs> {
static const String name = 'TestPage';
static const _i8.PageInfo<TestPageArgs> page =
_i8.PageInfo<TestPageArgs>(name);
static const _i9.PageInfo<TestPageArgs> page =
_i9.PageInfo<TestPageArgs>(name);
}
class TestPageArgs {

View file

@ -35,6 +35,15 @@ class Analytics {
}.asFirebaseMap);
}
static Future<void> settingsPageOpened(UserDto? dto) {
return _analytics.logScreenView(
screenClass: 'settings',
screenName: 'settings',
parameters: {
'id': dto?.id,
}.asFirebaseMap);
}
static Future<void> availablePackOpened(CardPackDto dto) {
return _analytics.logScreenView(
screenClass: 'pack',

View file

@ -160,7 +160,7 @@ InputButtonsTestState _$InputButtonsTestStateFromJson(
isSkipped: json['isSkipped'] as bool? ?? false,
testType:
$enumDecodeNullable(_$TestQuestionTypeEnumMap, json['testType']) ??
TestQuestionType.simple,
TestQuestionType.input_buttons,
);
Map<String, dynamic> _$InputButtonsTestStateToJson(

View file

@ -43,9 +43,9 @@ class TestCompleteWidget extends StatelessWidget {
.compareTo(n.correctCount.toDouble() / n.totalCount),
);
return PopScope(
onPopInvoked: (v) {
onPopInvoked: (v) async {
if (v) {
locator.testManager.sendStatistics();
await locator.testManager.sendStatistics();
locator.testManager.triggerPoll();
}
},

View file

@ -60,7 +60,7 @@ class _TestPageState extends State<TestPage> {
print('AUDIO $_lastAudioQuestionId ${question.id}');
_lastAudioQuestionId = question.id;
try {
if (globalSharedPreferences.getBool('auto_play_sound') != false) {
if (globalSharedPreferences.getBool('auto_play_sound_tests') != false) {
AudioPlayer.playAudio((question as dynamic).audio);
}
} catch (e, s) {}

View file

@ -100,9 +100,13 @@ Future<void> _fillSp() async {
if (soundSpeed == null) {
globalSharedPreferences.setDouble('sound_speed', 1.0);
}
final autoPlay = globalSharedPreferences.getBool('auto_play_sound');
final autoPlay = globalSharedPreferences.getBool('auto_play_sound_tests');
if (autoPlay == null) {
globalSharedPreferences.setBool('auto_play_sound', true);
globalSharedPreferences.setBool('auto_play_sound_tests', true);
}
final autoPlayView = globalSharedPreferences.getBool('auto_play_sound_view');
if (autoPlayView == null) {
globalSharedPreferences.setBool('auto_play_sound_view', false);
}
}

View file

@ -23,6 +23,20 @@ class HttpRepository extends Repository with Api {
@override
Future<void> updateUser(UserDto dto) async {}
@override
Future<PromoCodeDto> applyPromocode(String code) async {
try {
final r = await _dio.post<String>(
'$path/user/promocode',
data: PromoCodeDto(code: code).encode(),
);
return (r.data as String).decode(PromoCodeDto.fromJson);
} on DioException catch (e, s) {
log('User create error', error: e, stackTrace: s);
rethrow;
}
}
@override
Future<(UserDto, String)> createOrGetUser(
String externalId,

View file

@ -10,6 +10,8 @@ abstract class Repository {
Future<void> updateUser(UserDto dto);
Future<void> applyPromocode(String promocode);
Future<(UserDto, String)> createOrGetUser(
String externalId,
ExternalIdType idType,
@ -24,7 +26,7 @@ abstract class Repository {
Future<void> addTestStatistics(TestStatisticsDto testStatistics);
Future<List<TestDto>> getTests();
Future<List<TestDto>> getTests();
Future<TestDto?> getTest(String id);

View file

@ -74,6 +74,11 @@ class UserManager {
await _sharedPreferences!.remove('authToken');
}
Future<PromoCodeDto> applyPromocode(String code) async {
final data = await _repository.applyPromocode(code);
return data;
}
Future<UserDto?> updateUser() async {
String? authToken = _sharedPreferences!.getString('authToken');
if (authToken != null) {

View file

@ -5,7 +5,9 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:mnemo_cards/domain/router/app_router.dart';
import 'package:mnemo_cards/domain/router/app_router.gr.dart';
import 'package:mnemo_cards/flags.dart';
import 'package:mnemo_cards/main.dart';
import 'package:mnemo_cards/widgets/header.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -17,6 +19,7 @@ import '../di/locator.dart';
import '../features/analytics/analytics.dart';
import '../theme/themes.dart';
import '../widgets/big_back_button.dart';
import '../widgets/enter_promo_code_widget.dart';
import '../widgets/shared_pref_button.dart';
import '../widgets/simple_tile.dart';
import '../widgets/slider.dart';
@ -59,123 +62,112 @@ class ProfilePage extends StatelessWidget {
),
),
),
SizedBox(height: 8.h,),
SizedBox(
height: 8.h,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
StreamBuilder(
stream: locator.userManager.userStateHolder.asStream,
builder: (context, snapshot) {
final userData = snapshot.data?.userDataDto;
return Column(
children: [
if (ADMIN_BUILD)
MaterialButton(
onPressed: () {
showDialog(
context: context,
builder: (c) => const AllCards());
},
child: Text('ALL CARDS'),
),
if (ADMIN_BUILD)
MaterialButton(
onPressed: () {
showDialog(
context: context,
builder: (c) => const AllUsers());
},
child: Text('ALL USERS'),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: UserStatistics(userData),
),
SharedPrefButton<bool>.builder(
spKey: 'sound_on',
builder: (enabled, _) => AbsorbPointer(
child: SwitchTile(
text: 'Звук',
value: enabled ?? false,
),
),
setOnTap: (v) => !(v ?? false),
),
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
SharedPrefButton<bool>.builder(
spKey: 'auto_play_sound',
builder: (enabled, _) => AbsorbPointer(
child: SwitchTile(
text: 'Авто воспроизведение в тестах',
value: enabled ?? false,
),
),
setOnTap: (v) => !(v ?? false),
),
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
SimpleTile(
text: 'Скорость чтения',
trail: SharedPrefButton<double>.builder(
spKey: 'sound_speed',
builder: (v, s) => MnemoSlider(
onChanged: (value) => s(value),
value: v ?? 1.0,
),
shouldRebuild: (v) => false,
),
),
],
),
)
],
);
},
if (ADMIN_BUILD)
Row(
children: [
Expanded(
child: MaterialButton(
onPressed: () {
showDialog(
context: context,
builder: (c) => const AllCards());
},
child: Text('ALL CARDS'),
),
),
Expanded(
child: MaterialButton(
onPressed: () {
showDialog(
context: context,
builder: (c) => const AllUsers());
},
child: Text('ALL USERS'),
),
),
],
),
Expanded(
child: StreamBuilder(
stream: locator.userManager.userStateHolder.asStream,
builder: (context, snapshot) {
final userData = snapshot.data?.userDataDto;
return Padding(
padding: const EdgeInsets.only(
left: 10.0,
right: 10.0,
bottom: 8.0,
),
child: UserStatistics(userData),
);
},
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 10.0.w),
child: LinkButton(
'Написать в тг',
() => launchUrl(
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
const EnterPromoCodeWidget(),
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
SimpleTile.text(
text: 'Написать в телеграм',
onTap: () => launchUrl(
Uri.parse('https://t.me/mnemo_cards_bot'),
),
),
),
SizedBox(
height: 10.0.h,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10.0.w),
child: LinkButton(
'Политика конфиденциальности',
() {
launchUrl(
Uri.parse(
'https://www.freeprivacypolicy.com/live/1c2bce51-86c6-4a36-b226-b6c6f50ebf0f'),
);
},
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
),
],
SimpleTile(
onTap: () => appRouter.push(
PageRouteInfo(SettingsPage.name),
),
title: Row(
children: [
Image.asset(
'icons/settings.png',
width: 16.w,
),
SizedBox(width: 8.w),
Expanded(child: Text('Настройки там')),
],
),
trail: Image.asset(
'icons/next.png',
width: 16.w,
),
),
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
SizedBox(
height: 10.0.h,
),
],
),
),
],
),
),
const BigBackButton(),
if (MediaQuery.of(context).viewInsets.bottom < 30.0)
const BigBackButton(),
],
),
),

View file

@ -0,0 +1,157 @@
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:mnemo_cards/domain/router/app_router.dart';
import 'package:mnemo_cards/flags.dart';
import 'package:mnemo_cards/widgets/header.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
import '../admin/all_cards.dart';
import '../admin/all_users.dart';
import '../di/locator.dart';
import '../features/analytics/analytics.dart';
import '../theme/themes.dart';
import '../widgets/big_back_button.dart';
import '../widgets/enter_promo_code_widget.dart';
import '../widgets/shared_pref_button.dart';
import '../widgets/simple_tile.dart';
import '../widgets/slider.dart';
import '../widgets/switch_button.dart';
import '../widgets/switch_tile.dart';
import '../widgets/text_button.dart';
import '../widgets/user_statistics.dart';
@RoutePage()
class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
Analytics.settingsPageOpened(locator.userManager.userStateHolder.user);
return Scaffold(
body: SafeArea(
child: Column(
children: [
Header(
'Настройки',
popText: 'назад',
hasPopButton: true,
),
SizedBox(
height: 8.h,
),
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 10.0.w),
child: Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
SharedPrefButton<bool>.builder(
spKey: 'sound_on',
builder: (enabled, _) => AbsorbPointer(
child: SwitchTile(
text: 'Звук',
value: enabled ?? false,
),
),
setOnTap: (v) => !(v ?? false),
),
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
SharedPrefButton<bool>.builder(
spKey: 'auto_play_sound_view',
builder: (enabled, _) => AbsorbPointer(
child: SwitchTile(
text: 'Авто воспроизведение при просмотре',
value: enabled ?? false,
),
),
setOnTap: (v) => !(v ?? false),
),
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
SharedPrefButton<bool>.builder(
spKey: 'auto_play_sound_tests',
builder: (enabled, _) => AbsorbPointer(
child: SwitchTile(
text: 'Авто воспроизведение в тестах',
value: enabled ?? false,
),
),
setOnTap: (v) => !(v ?? false),
),
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
SimpleTile.text(
text: 'Скорость чтения',
trail: SharedPrefButton<double>.builder(
spKey: 'sound_speed',
builder: (v, s) => MnemoSlider(
onChanged: (value) => s(value),
value: v ?? 1.0,
),
shouldRebuild: (v) => false,
),
),
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
],
),
),
),
),
Column(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 32.0.h,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10.0.w),
child: LinkButton(
'Политика конфиденциальности',
() {
launchUrl(
Uri.parse(
'https://www.freeprivacypolicy.com/live/1c2bce51-86c6-4a36-b226-b6c6f50ebf0f'),
);
},
),
),
],
),
const BigBackButton(),
],
),
],
),
),
);
}
Future<void> clearAndExit() async {
final sp = await SharedPreferences.getInstance();
sp.clear();
exit(0);
}
const SettingsPage();
}

View file

@ -1,9 +1,12 @@
import 'dart:async';
import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:mnemo_cards/features/tests/progress_widget.dart';
import 'package:mnemo_cards/main.dart';
import 'package:mnemo_cards/managers/audio_player.dart';
import 'package:mnemo_cards/theme/themes.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:rxdart/rxdart.dart';
@ -37,6 +40,8 @@ class _CardGamePageState extends State<CardGamePage> {
final CardSwiperController cardSwiperController = CardSwiperController();
final FlipCardController flipCardController = FlipCardController();
StreamSubscription? _sub;
List<GameCardDto> shuffledCards = [];
GameCardDto? get activeCard {
@ -49,6 +54,15 @@ class _CardGamePageState extends State<CardGamePage> {
int get gameLength => widget.infinite ? 10000000 : shuffledCards.length;
void playSound(int index) {
if (globalSharedPreferences.getBool('auto_play_sound_view') == true) {
final text = activeCard?.original;
if (text != null) {
AudioPlayer.playAudio(text, lang: 'es-ES');
}
}
}
@override
Widget build(BuildContext context) {
// todo move to controller
@ -212,11 +226,8 @@ class _CardGamePageState extends State<CardGamePage> {
flex: 3,
child: InkWell(
onTap: () async {
final direction = Random().nextBool()
? CardSwiperState.swipeLeft
: CardSwiperState.swipeRight;
cardSwiperController.swipe(
direction: direction,
direction: CardSwiperState.swipeLeft,
);
},
child: Container(
@ -276,10 +287,12 @@ class _CardGamePageState extends State<CardGamePage> {
} else {
shuffledCards = widget.cards.toList();
}
_sub = cardSwiperController.asStream.distinct().listen(playSound);
}
@override
void dispose() {
_sub?.cancel().then((_) => _sub = null);
// cardSwiperController.removeListener(cardSwiperListener);
cardSwiperController.dispose();
super.dispose();

View file

@ -0,0 +1,88 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:mnemo_cards/di/locator.dart';
import 'package:mnemo_cards/theme/themes.dart';
import 'package:mnemo_cards/widgets/simple_tile.dart';
class EnterPromoCodeWidget extends StatefulWidget {
const EnterPromoCodeWidget();
@override
State<StatefulWidget> createState() => _EnterPromoCodeState();
}
class _EnterPromoCodeState extends State<EnterPromoCodeWidget> {
final TextEditingController controller = TextEditingController();
String? message;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SimpleTile(
title: Row(
children: [
Text('Промокод'),
SizedBox(
width: 16.w,
),
Expanded(
child: TextField(
// cursorHeight: 17.h,
controller: controller,
textAlignVertical: TextAlignVertical.center,
style: TextStyle(fontSize: 17.sp),
decoration: InputDecoration(
contentPadding: EdgeInsets.all(8.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
SizedBox(
width: 8.w,
),
],
),
trail: GestureDetector(
onTap: () async {
final result = await locator.userManager.applyPromocode(
controller.text,
);
if (result.success == true) {
controller.clear();
}
setState(() {
message = result.message;
});
FocusScope.of(context).unfocus();
},
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: progressBlue,
borderRadius: BorderRadius.circular(12.0),
),
child: Image.asset(
'icons/gift.png',
width: 17.w,
),
),
),
),
),
if (message != null && message!.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(message!),
),
],
);
}
}

View file

@ -1,24 +1,41 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:mnemo_cards/theme/themes.dart';
import 'package:mnemo_cards/widgets/switch_button.dart';
class SimpleTile extends StatelessWidget {
final Widget? trail;
final String text;
final Widget title;
final VoidCallback? onTap;
SimpleTile({
required this.text,
required this.title,
this.trail,
this.onTap,
});
SimpleTile.text({
required String text,
this.trail,
this.onTap,
}) : title = Text(text);
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(child: Text(text)),
if (trail != null) trail!,
],
return GestureDetector(
onTap: onTap,
child: Container(
height: 50.h,
padding: EdgeInsets.symmetric(vertical: 4.h),
alignment: Alignment.centerLeft,
child: Row(
children: [
Expanded(child: title),
if (trail != null) trail!,
],
),
),
);
}
}

View file

@ -24,16 +24,17 @@ class MnemoSlider extends StatelessWidget {
var v = this.value;
return Container(
decoration: BoxDecoration(
border: Border.all(color: borderGray),
borderRadius: BorderRadius.circular(12.0)),
padding: EdgeInsets.symmetric(vertical: 1.0, horizontal: 2.0),
border: Border.all(color: borderGray),
borderRadius: BorderRadius.circular(12.0),
),
padding: EdgeInsets.symmetric(vertical: 3, horizontal: 2.0),
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.white,
inactiveTrackColor: Colors.white,
thumbColor: progressBlue,
thumbShape: _RectSliderThumbShape(
enabledThumbRadius: 18.0,
enabledThumbRadius: 19.0,
borderRadius: Radius.circular(10),
textBuilder: (v) {
final value = ((v * (max - min) + min) * 10).toInt();
@ -45,7 +46,7 @@ class MnemoSlider extends StatelessWidget {
},
),
trackShape: RoundedRectSliderTrackShape(),
trackHeight: 36.h,
trackHeight: 1.h,
overlayColor: Colors.transparent,
overlappingShapeStrokeColor: Colors.transparent,
overlayShape: RoundSliderOverlayShape(overlayRadius: 2.0),
@ -119,19 +120,20 @@ class _RectSliderThumbShape extends SliderComponentShape {
}
@override
void paint(PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {
final Canvas canvas = context.canvas;
final Tween<double> radiusTween = Tween<double>(
begin: _disabledThumbRadius,
@ -151,7 +153,7 @@ class _RectSliderThumbShape extends SliderComponentShape {
);
final double evaluatedElevation =
elevationTween.evaluate(activationAnimation);
elevationTween.evaluate(activationAnimation);
final Path path = Path()
..addArc(
Rect.fromCenter(
@ -172,7 +174,7 @@ class _RectSliderThumbShape extends SliderComponentShape {
}
final TextPainter textPainter =
TextPainter(textDirection: TextDirection.rtl);
TextPainter(textDirection: TextDirection.rtl);
textPainter.text = TextSpan(
text: textBuilder?.call(value) ?? '',
style: TextStyle(
@ -190,8 +192,7 @@ class _RectSliderThumbShape extends SliderComponentShape {
Rect.fromCircle(center: center, radius: radius),
borderRadius ?? Radius.zero,
),
Paint()
..color = borderGray,
Paint()..color = borderGray,
);
// Use drawRect instead of drawCircle
canvas.drawRRect(
@ -199,8 +200,7 @@ class _RectSliderThumbShape extends SliderComponentShape {
Rect.fromCircle(center: center, radius: radius - 1),
borderRadius ?? Radius.zero,
),
Paint()
..color = color,
Paint()..color = color,
);
textPainter.paint(canvas, textCenter);

View file

@ -51,51 +51,54 @@ class UserStatistics extends StatelessWidget {
],
),
),
Container(
height: 200.h,
decoration: BoxDecoration(
border: Border.all(color: borderGray),
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.symmetric(horizontal: 8.0.w),
child: words.isEmpty
? Center(
child: AutoSizeText('Тут будут все пройденные слова'),
)
: ListView.builder(
itemCount: words.length,
shrinkWrap: true,
itemBuilder: (context, index) {
final word = words[index];
return Padding(
padding: EdgeInsets.symmetric(vertical: 2.0.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${word.word}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
Flexible(
child: SizedBox(
width: MediaQuery.of(context).size.width / 2.5,
child: HorizontalProgressWidget(
(word.correct * 100).floor(),
(word.total * 100).floor(),
learnedWords.contains(word.word) ? golden : progressBlue,
borderColor: borderGray,
height: 20.h,
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: borderGray),
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.symmetric(horizontal: 8.0.w),
child: words.isEmpty
? Center(
child: AutoSizeText('Тут будут все пройденные слова'),
)
: ListView.builder(
itemCount: words.length,
shrinkWrap: true,
itemBuilder: (context, index) {
final word = words[index];
return Padding(
padding: EdgeInsets.symmetric(vertical: 2.0.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${word.word}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
},
),
Flexible(
child: SizedBox(
width: MediaQuery.of(context).size.width / 2.5,
child: HorizontalProgressWidget(
(word.correct * 100).floor(),
(word.total * 100).floor(),
learnedWords.contains(word.word)
? golden
: progressBlue,
borderColor: borderGray,
height: 20.h,
),
),
),
],
),
);
},
),
),
),
],
);