b
This commit is contained in:
parent
9c48864e52
commit
5e7428a0b3
20 changed files with 665 additions and 210 deletions
Binary file not shown.
|
|
@ -25,16 +25,6 @@ class AppRouter extends $AppRouter {
|
|||
transitionsBuilder: TransitionsBuilders.noTransition,
|
||||
durationInMilliseconds: 200,
|
||||
),
|
||||
// CustomRoute(
|
||||
// page: ExplorePage.page,
|
||||
// transitionsBuilder: TransitionsBuilders.noTransition,
|
||||
// durationInMilliseconds: 200,
|
||||
// ),
|
||||
// CustomRoute(
|
||||
// page: ProfilePage.page,
|
||||
// transitionsBuilder: TransitionsBuilders.noTransition,
|
||||
// durationInMilliseconds: 200,
|
||||
// ),
|
||||
]),
|
||||
CustomRoute(
|
||||
page: ProfilePage.page,
|
||||
|
|
|
|||
|
|
@ -119,14 +119,14 @@ class TestCompleteWidget extends StatelessWidget {
|
|||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'1:47',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 0.85,
|
||||
),
|
||||
),
|
||||
// Text(
|
||||
// '1:47',
|
||||
// style: TextStyle(
|
||||
// fontSize: 20,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// height: 0.85,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:auto_route/auto_route.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:mnemo_cards/main.dart';
|
||||
import 'package:mnemo_cards/managers/audio_player.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
|
@ -59,7 +60,9 @@ class _TestPageState extends State<TestPage> {
|
|||
print('AUDIO $_lastAudioQuestionId ${question.id}');
|
||||
_lastAudioQuestionId = question.id;
|
||||
try {
|
||||
AudioPlayer.playAudio((question as dynamic).audio);
|
||||
if (globalSharedPreferences.getBool('auto_play_sound') != false) {
|
||||
AudioPlayer.playAudio((question as dynamic).audio);
|
||||
}
|
||||
} catch (e, s) {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ void main() async {
|
|||
|
||||
try {
|
||||
globalSharedPreferences = await SharedPreferences.getInstance();
|
||||
await _fillSp();
|
||||
print('global sp ${stopwatch.elapsedMilliseconds}');
|
||||
await setInjections();
|
||||
print('injections ${stopwatch.elapsedMilliseconds}');
|
||||
|
|
@ -90,6 +91,21 @@ void main() async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _fillSp() async {
|
||||
final soundOn = globalSharedPreferences.getBool('sound_on');
|
||||
if (soundOn == null) {
|
||||
globalSharedPreferences.setBool('sound_on', true);
|
||||
}
|
||||
final soundSpeed = globalSharedPreferences.getDouble('sound_speed');
|
||||
if (soundSpeed == null) {
|
||||
globalSharedPreferences.setDouble('sound_on', 1.0);
|
||||
}
|
||||
final autoPlay = globalSharedPreferences.getBool('auto_play_sound');
|
||||
if (autoPlay == null) {
|
||||
globalSharedPreferences.setBool('auto_play_sound', true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initFirebase() async {
|
||||
try {
|
||||
await Firebase.initializeApp(
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ import 'package:flutter_tts/flutter_tts.dart';
|
|||
import '../main.dart';
|
||||
|
||||
class AudioPlayer {
|
||||
static bool get soundOn =>
|
||||
globalSharedPreferences.getBool('sound_on') ?? true;
|
||||
|
||||
static Future<void> playAudio(String? audio, {String? lang}) async {
|
||||
if (globalSharedPreferences.getBool('sound_on') != false && audio != null) {
|
||||
if (soundOn && audio != null) {
|
||||
FlutterTts flutterTts = FlutterTts();
|
||||
var playText = audio;
|
||||
if (audio.contains('_')) {
|
||||
|
|
@ -17,7 +20,12 @@ class AudioPlayer {
|
|||
if (lang != null) {
|
||||
await flutterTts.setLanguage(lang);
|
||||
}
|
||||
await flutterTts.setSpeechRate(0.5);
|
||||
var speed = globalSharedPreferences.getDouble('sound_speed');
|
||||
if (speed == null) {
|
||||
speed = 1.0;
|
||||
globalSharedPreferences.setDouble('sound_speed', 1.0);
|
||||
}
|
||||
await flutterTts.setSpeechRate(speed);
|
||||
flutterTts.speak(playText);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ class UserManager {
|
|||
await _sharedPreferences!.remove('authToken');
|
||||
}
|
||||
|
||||
Future<UserDto?> _updateUser() async {
|
||||
Future<UserDto?> updateUser() async {
|
||||
String? authToken = _sharedPreferences!.getString('authToken');
|
||||
if (authToken != null) {
|
||||
_setAuthToken(authToken);
|
||||
|
|
@ -100,7 +100,7 @@ class UserManager {
|
|||
Future<void> init() async {
|
||||
_sharedPreferences = await SharedPreferences.getInstance();
|
||||
try {
|
||||
await _updateUser();
|
||||
await updateUser();
|
||||
} on Object catch (e, s) {
|
||||
log('User manager init error', error: e, stackTrace: s);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@ class AuthPage extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
if (ADMIN_BUILD)
|
||||
SharedPrefButton(
|
||||
enabledWidget: Text('PROD'),
|
||||
disabledWidget: Text('TEST'),
|
||||
SharedPrefButton<bool>(
|
||||
builder: (v, _) => (v ?? false) ? Text('PROD') : Text('TEST'),
|
||||
spKey: 'env',
|
||||
setOnTap: (v) => !(v ?? false),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
|
|
|
|||
|
|
@ -18,167 +18,162 @@ import '../features/analytics/analytics.dart';
|
|||
import '../theme/themes.dart';
|
||||
import '../widgets/big_back_button.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 ProfilePage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Analytics.profilePageOpened(locator.userManager.userStateHolder.user);
|
||||
locator.userManager.updateUser();
|
||||
final theme = Theme.of(context).textTheme;
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
StreamBuilder(
|
||||
stream: locator.userManager.userStateHolder.asStream,
|
||||
builder: (context, snapshot) {
|
||||
var title = snapshot.data?.name;
|
||||
if (true || title == null || title.length > 10) {
|
||||
title = 'Привет!';
|
||||
}
|
||||
return Header(
|
||||
title,
|
||||
popText: 'назад',
|
||||
hasPopButton: true,
|
||||
trail: GestureDetector(
|
||||
onTap: () async {
|
||||
await locator.userManager.logout();
|
||||
final sp = await SharedPreferences.getInstance();
|
||||
sp.clear();
|
||||
AppRouter.openAuthOrProfile();
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Image.asset(
|
||||
'icons/exit.png',
|
||||
width: 25.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
Header(
|
||||
'Привет!',
|
||||
popText: 'назад',
|
||||
hasPopButton: true,
|
||||
trail: GestureDetector(
|
||||
onTap: () async {
|
||||
await locator.userManager.logout();
|
||||
final sp = await SharedPreferences.getInstance();
|
||||
sp.clear();
|
||||
AppRouter.openAuthOrProfile();
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Image.asset(
|
||||
'icons/exit.png',
|
||||
width: 25.w,
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(
|
||||
height: 40.h,
|
||||
),
|
||||
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: EdgeInsets.symmetric(horizontal: 10.0.w),
|
||||
child: Text(
|
||||
'Скоро тут будет прогресс, ачивки, настройки и другая важная информация',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0.w),
|
||||
child: Text(
|
||||
'А сейчас есть переключатель звука:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SharedPrefButton(
|
||||
enabledWidget: Image.asset(
|
||||
'icons/sound_on.png',
|
||||
width: 140.w,
|
||||
height: 120.h,
|
||||
),
|
||||
disabledWidget: Image.asset(
|
||||
'icons/sound_off.png',
|
||||
width: 140.w,
|
||||
height: 120.h,
|
||||
),
|
||||
spKey: 'sound_on',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0.w),
|
||||
child: LinkButton(
|
||||
'Написать в тг',
|
||||
() => launchUrl(
|
||||
Uri.parse('https://t.me/mnemo_cards_bot'),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 30.0.h,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0.w),
|
||||
child: Row(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: LinkButton(
|
||||
'Очистить кэш',
|
||||
() async {
|
||||
await locator.packManager.clearCache();
|
||||
locator.testManager.clearStates();
|
||||
await locator.packUpdater.updateAvailablePacks();
|
||||
},
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
Flexible(
|
||||
child: LinkButton(
|
||||
'Страница в сторе',
|
||||
() {},
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0.w),
|
||||
child: LinkButton(
|
||||
'Написать в тг',
|
||||
() => 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'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 30.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'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Padding(
|
||||
// padding: EdgeInsets.symmetric(horizontal: 10.0.w),
|
||||
// child: LinkButton(
|
||||
// 'Условия использования',
|
||||
// () {
|
||||
//
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
const BigBackButton(),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ import 'package:flutter/material.dart';
|
|||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
const Color peach = Color(0xffffc994);
|
||||
const Color golden = Color(0xffffea00);
|
||||
const Color green = Color(0xff3d5309);
|
||||
const Color greenAccent = Color(0xff688d11);
|
||||
const Color black = Color(0xff000000);
|
||||
const Color mainBackground = Color(0xffffffff);
|
||||
const Color white = Color(0xffffffff);
|
||||
const Color yellow = Colors.yellowAccent;
|
||||
const Color progressBlue = Color(0xff8CCBFF);
|
||||
const Color menuBlue = Color(0xff99C4E9);
|
||||
const Color backgroundBlue = Color(0xFFf0f0f0);
|
||||
const Color testBlue = Color(0xffD0EAFF);
|
||||
|
|
@ -16,9 +19,9 @@ final lightTheme = ThemeData(
|
|||
brightness: Brightness.light,
|
||||
useMaterial3: true,
|
||||
primaryColor: menuBlue,
|
||||
scaffoldBackgroundColor: white,
|
||||
scaffoldBackgroundColor: mainBackground,
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: white,
|
||||
backgroundColor: mainBackground,
|
||||
),
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: menuBlue,
|
||||
|
|
|
|||
|
|
@ -435,7 +435,7 @@ class _TestButton extends StatelessWidget {
|
|||
stats!.words.correct.round(),
|
||||
stats!.words.total.round(),
|
||||
color,
|
||||
hasBorder: true,
|
||||
borderColor: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'dart:ui';
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
import 'package:mnemo_cards/di/locator.dart';
|
||||
import 'package:mnemo_cards/features/yandex_ads/yandex_ads.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
|
@ -353,25 +352,19 @@ class FrontCard extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (alpha > 0)
|
||||
if (alpha > 0 && AudioPlayer.soundOn)
|
||||
Opacity(
|
||||
opacity: alpha,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(4.0.h * alpha),
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.volume_up,
|
||||
size: (26 * textScale).toDouble(),
|
||||
icon: Image.asset(
|
||||
'icons/sound_on.png',
|
||||
width: 24 * textScale,
|
||||
),
|
||||
onPressed: () async {
|
||||
final sp = await SharedPreferences.getInstance();
|
||||
if (sp.getBool('sound_on') != false) {
|
||||
FlutterTts flutterTts = FlutterTts();
|
||||
await flutterTts.setLanguage('es-ES');
|
||||
await flutterTts.setSpeechRate(0.5);
|
||||
flutterTts.speak(card.original!);
|
||||
}
|
||||
AudioPlayer.playAudio(card.original, lang: 'es-ES');
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,23 +7,25 @@ class HorizontalProgressWidget extends StatelessWidget {
|
|||
final int value;
|
||||
final int length;
|
||||
final Color color;
|
||||
final bool hasBorder;
|
||||
final Color? borderColor;
|
||||
final double? height;
|
||||
|
||||
HorizontalProgressWidget(
|
||||
this.value,
|
||||
this.length,
|
||||
this.color, {
|
||||
this.hasBorder = false,
|
||||
this.borderColor,
|
||||
this.height,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 16.h,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
height: height ?? 16.h,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: white,
|
||||
border: hasBorder ? Border.all(color: color) : null,
|
||||
border: borderColor != null ? Border.all(color: borderColor!) : null,
|
||||
borderRadius: BorderRadius.circular(16.h),
|
||||
),
|
||||
child: AnimatedFractionallySizedBox(
|
||||
|
|
@ -31,7 +33,10 @@ class HorizontalProgressWidget extends StatelessWidget {
|
|||
widthFactor: length == 0 ? 0 : value.toDouble() / length,
|
||||
duration: Duration(milliseconds: 600),
|
||||
child: Container(
|
||||
color: color,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.h),
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -85,10 +85,11 @@ class _PacksMenuWidgetState extends State<PacksMenuWidget> {
|
|||
height: 40.h,
|
||||
),
|
||||
),
|
||||
SharedPrefButton(
|
||||
enabledWidget: Text('PROD'),
|
||||
disabledWidget: Text('TEST'),
|
||||
SharedPrefButton<bool>(
|
||||
builder: (v, _) =>
|
||||
(v ?? false) ? Text('PROD') : Text('TEST'),
|
||||
spKey: 'env',
|
||||
setOnTap: (v) => !(v ?? false),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,30 +2,39 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:mnemo_cards/main.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class SharedPrefButton extends StatefulWidget {
|
||||
final Function(bool)? onClick;
|
||||
class SharedPrefButton<T> extends StatefulWidget {
|
||||
final Function(T)? onClick;
|
||||
final String spKey;
|
||||
final Widget enabledWidget;
|
||||
final Widget disabledWidget;
|
||||
final bool toggleOnTap;
|
||||
final T Function(T? current)? setOnTap;
|
||||
final bool Function(T? current)? shouldRebuild;
|
||||
final Widget Function(T? value, ValueSetter<T> setValue)? builder;
|
||||
|
||||
SharedPrefButton({
|
||||
required this.enabledWidget,
|
||||
required this.disabledWidget,
|
||||
this.builder,
|
||||
required this.spKey,
|
||||
this.toggleOnTap = true,
|
||||
this.setOnTap,
|
||||
this.onClick,
|
||||
this.shouldRebuild,
|
||||
});
|
||||
|
||||
SharedPrefButton.builder({
|
||||
this.builder,
|
||||
required this.spKey,
|
||||
this.setOnTap,
|
||||
this.onClick,
|
||||
this.shouldRebuild,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SharedPrefButtonState();
|
||||
State<StatefulWidget> createState() => _SharedPrefButtonState<T>();
|
||||
}
|
||||
|
||||
class _SharedPrefButtonState extends State<SharedPrefButton> {
|
||||
class _SharedPrefButtonState<T> extends State<SharedPrefButton<T>> {
|
||||
SharedPreferences? _sp;
|
||||
bool value = false;
|
||||
T? value;
|
||||
|
||||
StreamSubscription? _streamSubscription;
|
||||
|
||||
|
|
@ -40,27 +49,69 @@ class _SharedPrefButtonState extends State<SharedPrefButton> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
_sp = globalSharedPreferences;
|
||||
value = _sp!.getBool(widget.spKey) ?? false;
|
||||
_streamSubscription = Stream.periodic(const Duration(milliseconds: 500),
|
||||
(_) => _sp?.getBool(widget.spKey) ?? false).distinct().listen((v) {
|
||||
value = v;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
value = valueFromSp;
|
||||
_streamSubscription =
|
||||
Stream.periodic(const Duration(milliseconds: 500), (_) => valueFromSp)
|
||||
.distinct()
|
||||
.listen((v) {
|
||||
if (value != v) {
|
||||
value = v;
|
||||
if (mounted && widget.shouldRebuild?.call(value) != false) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
T? get valueFromSp {
|
||||
if (T == bool) {
|
||||
return (_sp?.getBool(widget.spKey) ?? false).as<T?>();
|
||||
} else if (T == int) {
|
||||
return _sp?.getInt(widget.spKey).as<T?>();
|
||||
} else if (T == num || T == double) {
|
||||
return _sp?.getDouble(widget.spKey).as<T?>();
|
||||
} else if (T == String) {
|
||||
return _sp?.getString(widget.spKey).as<T?>();
|
||||
} else if (T == Iterable<String>) {
|
||||
return _sp?.getStringList(widget.spKey).as<T?>();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> setValue(T value) async {
|
||||
if (T == bool) {
|
||||
_sp?.setBool(widget.spKey, value as bool);
|
||||
} else if (T == int) {
|
||||
_sp?.setInt(widget.spKey, value as int);
|
||||
} else if (T == num || T == double) {
|
||||
_sp?.setDouble(widget.spKey, value as double);
|
||||
} else if (T == String) {
|
||||
_sp?.setString(widget.spKey, value as String);
|
||||
} else if (T == Iterable<String>) {
|
||||
_sp?.setStringList(widget.spKey, (value as Iterable<String>).toList());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: widget.toggleOnTap
|
||||
? () {
|
||||
value = !value;
|
||||
_sp?.setBool(widget.spKey, value);
|
||||
setState(() {});
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: widget.setOnTap != null
|
||||
? () async {
|
||||
value = widget.setOnTap!.call(value);
|
||||
if (value != null) {
|
||||
_setValue(value!);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: value ? widget.enabledWidget : widget.disabledWidget,
|
||||
child: widget.builder?.call(value, _setValue),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _setValue(T value) async {
|
||||
await setValue(value!);
|
||||
if (widget.shouldRebuild?.call(value) != false) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
lib/widgets/simple_tile.dart
Normal file
24
lib/widgets/simple_tile.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.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;
|
||||
|
||||
SimpleTile({
|
||||
required this.text,
|
||||
this.trail,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: Text(text)),
|
||||
if (trail != null) trail!,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
208
lib/widgets/slider.dart
Normal file
208
lib/widgets/slider.dart
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:mnemo_cards/theme/themes.dart';
|
||||
|
||||
class MnemoSlider extends StatelessWidget {
|
||||
final double value;
|
||||
final void Function(double)? onChanged;
|
||||
final double min;
|
||||
final double max;
|
||||
|
||||
MnemoSlider({
|
||||
required this.value,
|
||||
this.onChanged,
|
||||
this.min = 0.2,
|
||||
this.max = 2.0,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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),
|
||||
child: SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
activeTrackColor: Colors.white,
|
||||
inactiveTrackColor: Colors.white,
|
||||
thumbColor: progressBlue,
|
||||
thumbShape: _RectSliderThumbShape(
|
||||
enabledThumbRadius: 18.0,
|
||||
borderRadius: Radius.circular(10),
|
||||
textBuilder: (v) {
|
||||
final value = ((v * (max - min) + min) * 10).toInt();
|
||||
if (value % 10 == 0) {
|
||||
return '${(value / 10).toStringAsFixed(0)}x';
|
||||
} else {
|
||||
return (value / 10).toStringAsFixed(1);
|
||||
}
|
||||
},
|
||||
),
|
||||
trackShape: RoundedRectSliderTrackShape(),
|
||||
trackHeight: 36.h,
|
||||
overlayColor: Colors.transparent,
|
||||
overlappingShapeStrokeColor: Colors.transparent,
|
||||
overlayShape: RoundSliderOverlayShape(overlayRadius: 2.0),
|
||||
),
|
||||
child: StatefulBuilder(builder: (context, setState) {
|
||||
return Slider(
|
||||
min: min,
|
||||
max: max,
|
||||
value: v,
|
||||
onChanged: (updated) {
|
||||
setState(() {
|
||||
v = updated;
|
||||
});
|
||||
onChanged?.call((updated * 100).toInt() / 100.0);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RectSliderThumbShape extends SliderComponentShape {
|
||||
/// Create a slider thumb that draws a Rect.
|
||||
const _RectSliderThumbShape({
|
||||
this.enabledThumbRadius = 10.0,
|
||||
this.disabledThumbRadius,
|
||||
this.elevation = 1.0,
|
||||
this.pressedElevation = 6.0,
|
||||
this.borderRadius,
|
||||
this.textBuilder,
|
||||
});
|
||||
|
||||
final String Function(double v)? textBuilder;
|
||||
|
||||
/// The preferred radius of the round thumb shape when the slider is enabled.
|
||||
///
|
||||
/// If it is not provided, then the Material Design default of 10 is used.
|
||||
final double enabledThumbRadius;
|
||||
|
||||
/// The preferred radius of the round thumb shape when the slider is disabled.
|
||||
///
|
||||
/// If no disabledRadius is provided, then it is equal to the
|
||||
/// [enabledThumbRadius]
|
||||
final double? disabledThumbRadius;
|
||||
final Radius? borderRadius;
|
||||
|
||||
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius;
|
||||
|
||||
/// The resting elevation adds shadow to the unpressed thumb.
|
||||
///
|
||||
/// The default is 1.
|
||||
///
|
||||
/// Use 0 for no shadow. The higher the value, the larger the shadow. For
|
||||
/// example, a value of 12 will create a very large shadow.
|
||||
///
|
||||
final double elevation;
|
||||
|
||||
/// The pressed elevation adds shadow to the pressed thumb.
|
||||
///
|
||||
/// The default is 6.
|
||||
///
|
||||
/// Use 0 for no shadow. The higher the value, the larger the shadow. For
|
||||
/// example, a value of 12 will create a very large shadow.
|
||||
final double pressedElevation;
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
|
||||
return Size.fromRadius(
|
||||
isEnabled == true ? enabledThumbRadius : _disabledThumbRadius);
|
||||
}
|
||||
|
||||
@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,
|
||||
}) {
|
||||
final Canvas canvas = context.canvas;
|
||||
final Tween<double> radiusTween = Tween<double>(
|
||||
begin: _disabledThumbRadius,
|
||||
end: enabledThumbRadius,
|
||||
);
|
||||
final ColorTween colorTween = ColorTween(
|
||||
begin: sliderTheme.disabledThumbColor,
|
||||
end: sliderTheme.thumbColor,
|
||||
);
|
||||
|
||||
final Color color = colorTween.evaluate(enableAnimation)!;
|
||||
final double radius = radiusTween.evaluate(enableAnimation);
|
||||
|
||||
final Tween<double> elevationTween = Tween<double>(
|
||||
begin: elevation,
|
||||
end: pressedElevation,
|
||||
);
|
||||
|
||||
final double evaluatedElevation =
|
||||
elevationTween.evaluate(activationAnimation);
|
||||
final Path path = Path()
|
||||
..addArc(
|
||||
Rect.fromCenter(
|
||||
center: center, width: 2 * radius, height: 2 * radius),
|
||||
0,
|
||||
pi * 2);
|
||||
|
||||
bool paintShadows = false;
|
||||
assert(() {
|
||||
if (debugDisableShadows) {
|
||||
paintShadows = false;
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
if (paintShadows) {
|
||||
canvas.drawShadow(path, Colors.black, evaluatedElevation, true);
|
||||
}
|
||||
|
||||
final TextPainter textPainter =
|
||||
TextPainter(textDirection: TextDirection.rtl);
|
||||
textPainter.text = TextSpan(
|
||||
text: textBuilder?.call(value) ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: radius,
|
||||
color: Colors.black,
|
||||
));
|
||||
textPainter.layout();
|
||||
|
||||
final Offset textCenter = Offset(center.dx - (textPainter.width / 2),
|
||||
center.dy - (textPainter.height / 2));
|
||||
|
||||
// Use drawRect instead of drawCircle
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromCircle(center: center, radius: radius),
|
||||
borderRadius ?? Radius.zero,
|
||||
),
|
||||
Paint()
|
||||
..color = borderGray,
|
||||
);
|
||||
// Use drawRect instead of drawCircle
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromCircle(center: center, radius: radius - 1),
|
||||
borderRadius ?? Radius.zero,
|
||||
),
|
||||
Paint()
|
||||
..color = color,
|
||||
);
|
||||
|
||||
textPainter.paint(canvas, textCenter);
|
||||
}
|
||||
}
|
||||
26
lib/widgets/switch_button.dart
Normal file
26
lib/widgets/switch_button.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mnemo_cards/theme/themes.dart';
|
||||
|
||||
class SwitchButton extends StatelessWidget {
|
||||
final bool value;
|
||||
final void Function(bool)? onChanged;
|
||||
|
||||
SwitchButton({
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: progressBlue,
|
||||
activeTrackColor: white,
|
||||
inactiveTrackColor: white,
|
||||
trackOutlineColor: WidgetStatePropertyAll(borderGray),
|
||||
inactiveThumbColor: borderGray,
|
||||
);
|
||||
}
|
||||
}
|
||||
29
lib/widgets/switch_tile.dart
Normal file
29
lib/widgets/switch_tile.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mnemo_cards/theme/themes.dart';
|
||||
import 'package:mnemo_cards/widgets/switch_button.dart';
|
||||
|
||||
class SwitchTile extends StatelessWidget {
|
||||
final bool value;
|
||||
final void Function(bool)? onChanged;
|
||||
final String text;
|
||||
|
||||
SwitchTile({
|
||||
required this.text,
|
||||
required this.value,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: Text(text, overflow: TextOverflow.ellipsis,)),
|
||||
SwitchButton(
|
||||
value: value,
|
||||
onChanged: onChanged ?? (v) {},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
103
lib/widgets/user_statistics.dart
Normal file
103
lib/widgets/user_statistics.dart
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
|
||||
import '../theme/themes.dart';
|
||||
import 'horizontal_progress.dart';
|
||||
|
||||
class UserStatistics extends StatelessWidget {
|
||||
final UserDataDto? userDataDto;
|
||||
|
||||
UserStatistics(this.userDataDto);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final words = userDataDto?.allWordsStatistics?.words ?? [];
|
||||
words.sort(
|
||||
(p, n) => (n.correct / n.total).compareTo(p.correct / p.total),
|
||||
);
|
||||
final learnedWords = words
|
||||
.where((w) => w.correct > 10 && w.correct / w.total > 0.9)
|
||||
.map((e) => e.word)
|
||||
.toSet();
|
||||
final learnedWordsLength = learnedWords.length;
|
||||
final lastDigit = learnedWordsLength % 10;
|
||||
final String? totalWordsText;
|
||||
if (learnedWordsLength == 0) {
|
||||
totalWordsText = null;
|
||||
} else if (lastDigit == 0 || lastDigit >= 5) {
|
||||
totalWordsText = 'Ты уже знаешь $learnedWordsLength слов!';
|
||||
} else if (lastDigit == 1) {
|
||||
totalWordsText = 'Ты уже знаешь $learnedWordsLength слово!';
|
||||
} else {
|
||||
totalWordsText = 'Ты уже знаешь $learnedWordsLength слова!';
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (totalWordsText != null)
|
||||
Text(
|
||||
totalWordsText,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue