This commit is contained in:
Dmitry 2024-06-15 18:42:18 +03:00
parent 9c48864e52
commit 5e7428a0b3
20 changed files with 665 additions and 210 deletions

View file

@ -25,16 +25,6 @@ class AppRouter extends $AppRouter {
transitionsBuilder: TransitionsBuilders.noTransition, transitionsBuilder: TransitionsBuilders.noTransition,
durationInMilliseconds: 200, durationInMilliseconds: 200,
), ),
// CustomRoute(
// page: ExplorePage.page,
// transitionsBuilder: TransitionsBuilders.noTransition,
// durationInMilliseconds: 200,
// ),
// CustomRoute(
// page: ProfilePage.page,
// transitionsBuilder: TransitionsBuilders.noTransition,
// durationInMilliseconds: 200,
// ),
]), ]),
CustomRoute( CustomRoute(
page: ProfilePage.page, page: ProfilePage.page,

View file

@ -119,14 +119,14 @@ class TestCompleteWidget extends StatelessWidget {
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
Text( // Text(
'1:47', // '1:47',
style: TextStyle( // style: TextStyle(
fontSize: 20, // fontSize: 20,
fontWeight: FontWeight.w500, // fontWeight: FontWeight.w500,
height: 0.85, // height: 0.85,
), // ),
), // ),
], ],
), ),
), ),

View file

@ -5,6 +5,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.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/managers/audio_player.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
@ -59,7 +60,9 @@ class _TestPageState extends State<TestPage> {
print('AUDIO $_lastAudioQuestionId ${question.id}'); print('AUDIO $_lastAudioQuestionId ${question.id}');
_lastAudioQuestionId = question.id; _lastAudioQuestionId = question.id;
try { try {
if (globalSharedPreferences.getBool('auto_play_sound') != false) {
AudioPlayer.playAudio((question as dynamic).audio); AudioPlayer.playAudio((question as dynamic).audio);
}
} catch (e, s) {} } catch (e, s) {}
} }
} }

View file

@ -66,6 +66,7 @@ void main() async {
try { try {
globalSharedPreferences = await SharedPreferences.getInstance(); globalSharedPreferences = await SharedPreferences.getInstance();
await _fillSp();
print('global sp ${stopwatch.elapsedMilliseconds}'); print('global sp ${stopwatch.elapsedMilliseconds}');
await setInjections(); await setInjections();
print('injections ${stopwatch.elapsedMilliseconds}'); 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 { Future<void> initFirebase() async {
try { try {
await Firebase.initializeApp( await Firebase.initializeApp(

View file

@ -3,8 +3,11 @@ import 'package:flutter_tts/flutter_tts.dart';
import '../main.dart'; import '../main.dart';
class AudioPlayer { class AudioPlayer {
static bool get soundOn =>
globalSharedPreferences.getBool('sound_on') ?? true;
static Future<void> playAudio(String? audio, {String? lang}) async { static Future<void> playAudio(String? audio, {String? lang}) async {
if (globalSharedPreferences.getBool('sound_on') != false && audio != null) { if (soundOn && audio != null) {
FlutterTts flutterTts = FlutterTts(); FlutterTts flutterTts = FlutterTts();
var playText = audio; var playText = audio;
if (audio.contains('_')) { if (audio.contains('_')) {
@ -17,7 +20,12 @@ class AudioPlayer {
if (lang != null) { if (lang != null) {
await flutterTts.setLanguage(lang); 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); flutterTts.speak(playText);
} }
} }

View file

@ -74,7 +74,7 @@ class UserManager {
await _sharedPreferences!.remove('authToken'); await _sharedPreferences!.remove('authToken');
} }
Future<UserDto?> _updateUser() async { Future<UserDto?> updateUser() async {
String? authToken = _sharedPreferences!.getString('authToken'); String? authToken = _sharedPreferences!.getString('authToken');
if (authToken != null) { if (authToken != null) {
_setAuthToken(authToken); _setAuthToken(authToken);
@ -100,7 +100,7 @@ class UserManager {
Future<void> init() async { Future<void> init() async {
_sharedPreferences = await SharedPreferences.getInstance(); _sharedPreferences = await SharedPreferences.getInstance();
try { try {
await _updateUser(); await updateUser();
} on Object catch (e, s) { } on Object catch (e, s) {
log('User manager init error', error: e, stackTrace: s); log('User manager init error', error: e, stackTrace: s);
} }

View file

@ -42,10 +42,10 @@ class AuthPage extends StatelessWidget {
), ),
), ),
if (ADMIN_BUILD) if (ADMIN_BUILD)
SharedPrefButton( SharedPrefButton<bool>(
enabledWidget: Text('PROD'), builder: (v, _) => (v ?? false) ? Text('PROD') : Text('TEST'),
disabledWidget: Text('TEST'),
spKey: 'env', spKey: 'env',
setOnTap: (v) => !(v ?? false),
), ),
Padding( Padding(
padding: padding:

View file

@ -18,27 +18,26 @@ import '../features/analytics/analytics.dart';
import '../theme/themes.dart'; import '../theme/themes.dart';
import '../widgets/big_back_button.dart'; import '../widgets/big_back_button.dart';
import '../widgets/shared_pref_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/text_button.dart';
import '../widgets/user_statistics.dart';
@RoutePage() @RoutePage()
class ProfilePage extends StatelessWidget { class ProfilePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Analytics.profilePageOpened(locator.userManager.userStateHolder.user); Analytics.profilePageOpened(locator.userManager.userStateHolder.user);
locator.userManager.updateUser();
final theme = Theme.of(context).textTheme; final theme = Theme.of(context).textTheme;
return Scaffold( return Scaffold(
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
StreamBuilder( Header(
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: 'назад', popText: 'назад',
hasPopButton: true, hasPopButton: true,
trail: GestureDetector( trail: GestureDetector(
@ -59,16 +58,23 @@ class ProfilePage extends StatelessWidget {
], ],
), ),
), ),
);
}),
SizedBox(
height: 40.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) if (ADMIN_BUILD)
MaterialButton( MaterialButton(
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, builder: (c) => const AllCards()); context: context,
builder: (c) => const AllCards());
}, },
child: Text('ALL CARDS'), child: Text('ALL CARDS'),
), ),
@ -76,49 +82,69 @@ class ProfilePage extends StatelessWidget {
MaterialButton( MaterialButton(
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, builder: (c) => const AllUsers()); context: context,
builder: (c) => const AllUsers());
}, },
child: Text('ALL USERS'), child: Text('ALL USERS'),
), ),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 10.0.w), padding:
child: Text( const EdgeInsets.symmetric(horizontal: 10.0),
'Скоро тут будет прогресс, ачивки, настройки и другая важная информация', child: Column(
style: TextStyle( children: [
fontWeight: FontWeight.w500,
fontSize: 16,
color: Colors.black,
),
textAlign: TextAlign.center,
),
),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 10.0.w), padding: const EdgeInsets.only(bottom: 8.0),
child: Text( child: UserStatistics(userData),
'А сейчас есть переключатель звука:',
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,
), ),
SharedPrefButton<bool>.builder(
spKey: 'sound_on', 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,
),
),
],
),
)
],
);
},
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 10.0.w), padding: EdgeInsets.symmetric(horizontal: 10.0.w),
child: LinkButton( child: LinkButton(
@ -129,34 +155,7 @@ class ProfilePage extends StatelessWidget {
), ),
), ),
SizedBox( SizedBox(
height: 30.0.h, height: 10.0.h,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10.0.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: LinkButton(
'Очистить кэш',
() async {
await locator.packManager.clearCache();
locator.testManager.clearStates();
await locator.packUpdater.updateAvailablePacks();
},
),
),
Flexible(
child: LinkButton(
'Страница в сторе',
() {},
),
),
],
),
),
SizedBox(
height: 30.0.h,
), ),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 10.0.w), padding: EdgeInsets.symmetric(horizontal: 10.0.w),
@ -170,15 +169,11 @@ class ProfilePage extends StatelessWidget {
}, },
), ),
), ),
// Padding( ],
// padding: EdgeInsets.symmetric(horizontal: 10.0.w), ),
// child: LinkButton( ],
// 'Условия использования', ),
// () { ),
//
// },
// ),
// ),
const BigBackButton(), const BigBackButton(),
], ],
), ),

View file

@ -2,11 +2,14 @@ import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
const Color peach = Color(0xffffc994); const Color peach = Color(0xffffc994);
const Color golden = Color(0xffffea00);
const Color green = Color(0xff3d5309); const Color green = Color(0xff3d5309);
const Color greenAccent = Color(0xff688d11); const Color greenAccent = Color(0xff688d11);
const Color black = Color(0xff000000); const Color black = Color(0xff000000);
const Color mainBackground = Color(0xffffffff);
const Color white = Color(0xffffffff); const Color white = Color(0xffffffff);
const Color yellow = Colors.yellowAccent; const Color yellow = Colors.yellowAccent;
const Color progressBlue = Color(0xff8CCBFF);
const Color menuBlue = Color(0xff99C4E9); const Color menuBlue = Color(0xff99C4E9);
const Color backgroundBlue = Color(0xFFf0f0f0); const Color backgroundBlue = Color(0xFFf0f0f0);
const Color testBlue = Color(0xffD0EAFF); const Color testBlue = Color(0xffD0EAFF);
@ -16,9 +19,9 @@ final lightTheme = ThemeData(
brightness: Brightness.light, brightness: Brightness.light,
useMaterial3: true, useMaterial3: true,
primaryColor: menuBlue, primaryColor: menuBlue,
scaffoldBackgroundColor: white, scaffoldBackgroundColor: mainBackground,
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
backgroundColor: white, backgroundColor: mainBackground,
), ),
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
seedColor: menuBlue, seedColor: menuBlue,

View file

@ -435,7 +435,7 @@ class _TestButton extends StatelessWidget {
stats!.words.correct.round(), stats!.words.correct.round(),
stats!.words.total.round(), stats!.words.total.round(),
color, color,
hasBorder: true, borderColor: color,
), ),
), ),
], ],

View file

@ -4,7 +4,6 @@ import 'dart:ui';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.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/di/locator.dart';
import 'package:mnemo_cards/features/yandex_ads/yandex_ads.dart'; import 'package:mnemo_cards/features/yandex_ads/yandex_ads.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
@ -353,25 +352,19 @@ class FrontCard extends StatelessWidget {
), ),
), ),
), ),
if (alpha > 0) if (alpha > 0 && AudioPlayer.soundOn)
Opacity( Opacity(
opacity: alpha, opacity: alpha,
child: Container( child: Container(
padding: EdgeInsets.all(4.0.h * alpha), padding: EdgeInsets.all(4.0.h * alpha),
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: IconButton( child: IconButton(
icon: Icon( icon: Image.asset(
Icons.volume_up, 'icons/sound_on.png',
size: (26 * textScale).toDouble(), width: 24 * textScale,
), ),
onPressed: () async { onPressed: () async {
final sp = await SharedPreferences.getInstance(); AudioPlayer.playAudio(card.original, lang: 'es-ES');
if (sp.getBool('sound_on') != false) {
FlutterTts flutterTts = FlutterTts();
await flutterTts.setLanguage('es-ES');
await flutterTts.setSpeechRate(0.5);
flutterTts.speak(card.original!);
}
}, },
), ),
), ),

View file

@ -7,23 +7,25 @@ class HorizontalProgressWidget extends StatelessWidget {
final int value; final int value;
final int length; final int length;
final Color color; final Color color;
final bool hasBorder; final Color? borderColor;
final double? height;
HorizontalProgressWidget( HorizontalProgressWidget(
this.value, this.value,
this.length, this.length,
this.color, { this.color, {
this.hasBorder = false, this.borderColor,
this.height,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: 16.h, height: height ?? 16.h,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
color: white, color: white,
border: hasBorder ? Border.all(color: color) : null, border: borderColor != null ? Border.all(color: borderColor!) : null,
borderRadius: BorderRadius.circular(16.h), borderRadius: BorderRadius.circular(16.h),
), ),
child: AnimatedFractionallySizedBox( child: AnimatedFractionallySizedBox(
@ -31,9 +33,12 @@ class HorizontalProgressWidget extends StatelessWidget {
widthFactor: length == 0 ? 0 : value.toDouble() / length, widthFactor: length == 0 ? 0 : value.toDouble() / length,
duration: Duration(milliseconds: 600), duration: Duration(milliseconds: 600),
child: Container( child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.h),
color: color, color: color,
), ),
), ),
),
); );
} }
} }

View file

@ -85,10 +85,11 @@ class _PacksMenuWidgetState extends State<PacksMenuWidget> {
height: 40.h, height: 40.h,
), ),
), ),
SharedPrefButton( SharedPrefButton<bool>(
enabledWidget: Text('PROD'), builder: (v, _) =>
disabledWidget: Text('TEST'), (v ?? false) ? Text('PROD') : Text('TEST'),
spKey: 'env', spKey: 'env',
setOnTap: (v) => !(v ?? false),
), ),
], ],
), ),

View file

@ -2,30 +2,39 @@ import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:mnemo_cards/main.dart'; import 'package:mnemo_cards/main.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class SharedPrefButton extends StatefulWidget { class SharedPrefButton<T> extends StatefulWidget {
final Function(bool)? onClick; final Function(T)? onClick;
final String spKey; final String spKey;
final Widget enabledWidget; final T Function(T? current)? setOnTap;
final Widget disabledWidget; final bool Function(T? current)? shouldRebuild;
final bool toggleOnTap; final Widget Function(T? value, ValueSetter<T> setValue)? builder;
SharedPrefButton({ SharedPrefButton({
required this.enabledWidget, this.builder,
required this.disabledWidget,
required this.spKey, required this.spKey,
this.toggleOnTap = true, this.setOnTap,
this.onClick, this.onClick,
this.shouldRebuild,
});
SharedPrefButton.builder({
this.builder,
required this.spKey,
this.setOnTap,
this.onClick,
this.shouldRebuild,
}); });
@override @override
State<StatefulWidget> createState() => _SharedPrefButtonState(); State<StatefulWidget> createState() => _SharedPrefButtonState<T>();
} }
class _SharedPrefButtonState extends State<SharedPrefButton> { class _SharedPrefButtonState<T> extends State<SharedPrefButton<T>> {
SharedPreferences? _sp; SharedPreferences? _sp;
bool value = false; T? value;
StreamSubscription? _streamSubscription; StreamSubscription? _streamSubscription;
@ -40,27 +49,69 @@ class _SharedPrefButtonState extends State<SharedPrefButton> {
void initState() { void initState() {
super.initState(); super.initState();
_sp = globalSharedPreferences; _sp = globalSharedPreferences;
value = _sp!.getBool(widget.spKey) ?? false; value = valueFromSp;
_streamSubscription = Stream.periodic(const Duration(milliseconds: 500), _streamSubscription =
(_) => _sp?.getBool(widget.spKey) ?? false).distinct().listen((v) { Stream.periodic(const Duration(milliseconds: 500), (_) => valueFromSp)
.distinct()
.listen((v) {
if (value != v) {
value = v; value = v;
if (mounted) { if (mounted && widget.shouldRebuild?.call(value) != false) {
setState(() {}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: widget.toggleOnTap behavior: HitTestBehavior.opaque,
? () { onTap: widget.setOnTap != null
value = !value; ? () async {
_sp?.setBool(widget.spKey, value); value = widget.setOnTap!.call(value);
setState(() {}); if (value != null) {
_setValue(value!);
}
} }
: null, : 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(() {});
}
}
} }

View 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
View 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);
}
}

View 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,
);
}
}

View 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) {},
),
],
);
}
}

View 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,
),
),
),
],
),
);
},
),
),
],
);
}
}