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,
durationInMilliseconds: 200,
),
// CustomRoute(
// page: ExplorePage.page,
// transitionsBuilder: TransitionsBuilders.noTransition,
// durationInMilliseconds: 200,
// ),
// CustomRoute(
// page: ProfilePage.page,
// transitionsBuilder: TransitionsBuilders.noTransition,
// durationInMilliseconds: 200,
// ),
]),
CustomRoute(
page: ProfilePage.page,

View file

@ -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,
// ),
// ),
],
),
),

View file

@ -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) {}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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(),
],
),

View file

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

View file

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

View file

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

View file

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

View file

@ -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),
),
],
),

View file

@ -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(() {});
}
}
}

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