diff --git a/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk b/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk index a81adf9..dfabb7f 100644 Binary files a/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk and b/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk differ diff --git a/lib/domain/router/app_router.dart b/lib/domain/router/app_router.dart index aa71ba2..508e6dc 100644 --- a/lib/domain/router/app_router.dart +++ b/lib/domain/router/app_router.dart @@ -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, diff --git a/lib/features/tests/test_complete_widget.dart b/lib/features/tests/test_complete_widget.dart index d39cacd..c9c21b2 100644 --- a/lib/features/tests/test_complete_widget.dart +++ b/lib/features/tests/test_complete_widget.dart @@ -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, + // ), + // ), ], ), ), diff --git a/lib/features/tests/test_page.dart b/lib/features/tests/test_page.dart index 6586967..5dbafdb 100644 --- a/lib/features/tests/test_page.dart +++ b/lib/features/tests/test_page.dart @@ -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 { 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) {} } } diff --git a/lib/main.dart b/lib/main.dart index 88d0262..ed4cd33 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 _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 initFirebase() async { try { await Firebase.initializeApp( diff --git a/lib/managers/audio_player.dart b/lib/managers/audio_player.dart index 70a547d..e11b259 100644 --- a/lib/managers/audio_player.dart +++ b/lib/managers/audio_player.dart @@ -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 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); } } diff --git a/lib/managers/user_manager.dart b/lib/managers/user_manager.dart index 20df53a..10331d8 100644 --- a/lib/managers/user_manager.dart +++ b/lib/managers/user_manager.dart @@ -74,7 +74,7 @@ class UserManager { await _sharedPreferences!.remove('authToken'); } - Future _updateUser() async { + Future updateUser() async { String? authToken = _sharedPreferences!.getString('authToken'); if (authToken != null) { _setAuthToken(authToken); @@ -100,7 +100,7 @@ class UserManager { Future 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); } diff --git a/lib/pages/auth_page.dart b/lib/pages/auth_page.dart index 5b34d5c..21375a5 100644 --- a/lib/pages/auth_page.dart +++ b/lib/pages/auth_page.dart @@ -42,10 +42,10 @@ class AuthPage extends StatelessWidget { ), ), if (ADMIN_BUILD) - SharedPrefButton( - enabledWidget: Text('PROD'), - disabledWidget: Text('TEST'), + SharedPrefButton( + builder: (v, _) => (v ?? false) ? Text('PROD') : Text('TEST'), spKey: 'env', + setOnTap: (v) => !(v ?? false), ), Padding( padding: diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart index 4efa731..9b27198 100644 --- a/lib/pages/profile_page.dart +++ b/lib/pages/profile_page.dart @@ -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.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.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.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(), ], ), diff --git a/lib/theme/themes.dart b/lib/theme/themes.dart index 7bd01d1..4660414 100644 --- a/lib/theme/themes.dart +++ b/lib/theme/themes.dart @@ -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, diff --git a/lib/widgets/card_pack/available_pack.dart b/lib/widgets/card_pack/available_pack.dart index 871da2e..97484dc 100644 --- a/lib/widgets/card_pack/available_pack.dart +++ b/lib/widgets/card_pack/available_pack.dart @@ -435,7 +435,7 @@ class _TestButton extends StatelessWidget { stats!.words.correct.round(), stats!.words.total.round(), color, - hasBorder: true, + borderColor: color, ), ), ], diff --git a/lib/widgets/game_card_widget.dart b/lib/widgets/game_card_widget.dart index d574eb8..d8cd502 100644 --- a/lib/widgets/game_card_widget.dart +++ b/lib/widgets/game_card_widget.dart @@ -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'); }, ), ), diff --git a/lib/widgets/horizontal_progress.dart b/lib/widgets/horizontal_progress.dart index e956341..5cdaa91 100644 --- a/lib/widgets/horizontal_progress.dart +++ b/lib/widgets/horizontal_progress.dart @@ -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, + ), ), ), ); diff --git a/lib/widgets/packs_menu_widget.dart b/lib/widgets/packs_menu_widget.dart index ce456d5..b296125 100644 --- a/lib/widgets/packs_menu_widget.dart +++ b/lib/widgets/packs_menu_widget.dart @@ -85,10 +85,11 @@ class _PacksMenuWidgetState extends State { height: 40.h, ), ), - SharedPrefButton( - enabledWidget: Text('PROD'), - disabledWidget: Text('TEST'), + SharedPrefButton( + builder: (v, _) => + (v ?? false) ? Text('PROD') : Text('TEST'), spKey: 'env', + setOnTap: (v) => !(v ?? false), ), ], ), diff --git a/lib/widgets/shared_pref_button.dart b/lib/widgets/shared_pref_button.dart index 211bd03..d4a8f4e 100644 --- a/lib/widgets/shared_pref_button.dart +++ b/lib/widgets/shared_pref_button.dart @@ -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 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 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 createState() => _SharedPrefButtonState(); + State createState() => _SharedPrefButtonState(); } -class _SharedPrefButtonState extends State { +class _SharedPrefButtonState extends State> { SharedPreferences? _sp; - bool value = false; + T? value; StreamSubscription? _streamSubscription; @@ -40,27 +49,69 @@ class _SharedPrefButtonState extends State { 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(); + } else if (T == int) { + return _sp?.getInt(widget.spKey).as(); + } else if (T == num || T == double) { + return _sp?.getDouble(widget.spKey).as(); + } else if (T == String) { + return _sp?.getString(widget.spKey).as(); + } else if (T == Iterable) { + return _sp?.getStringList(widget.spKey).as(); + } + return null; + } + + Future 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) { + _sp?.setStringList(widget.spKey, (value as Iterable).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 _setValue(T value) async { + await setValue(value!); + if (widget.shouldRebuild?.call(value) != false) { + setState(() {}); + } + } } diff --git a/lib/widgets/simple_tile.dart b/lib/widgets/simple_tile.dart new file mode 100644 index 0000000..d8f2228 --- /dev/null +++ b/lib/widgets/simple_tile.dart @@ -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!, + ], + ); + } +} diff --git a/lib/widgets/slider.dart b/lib/widgets/slider.dart new file mode 100644 index 0000000..4e78ea8 --- /dev/null +++ b/lib/widgets/slider.dart @@ -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 activationAnimation, + required Animation 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 radiusTween = Tween( + 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 elevationTween = Tween( + 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); + } +} diff --git a/lib/widgets/switch_button.dart b/lib/widgets/switch_button.dart new file mode 100644 index 0000000..a1de011 --- /dev/null +++ b/lib/widgets/switch_button.dart @@ -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, + ); + } +} diff --git a/lib/widgets/switch_tile.dart b/lib/widgets/switch_tile.dart new file mode 100644 index 0000000..5a43bce --- /dev/null +++ b/lib/widgets/switch_tile.dart @@ -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) {}, + ), + ], + ); + } +} diff --git a/lib/widgets/user_statistics.dart b/lib/widgets/user_statistics.dart new file mode 100644 index 0000000..12f2a17 --- /dev/null +++ b/lib/widgets/user_statistics.dart @@ -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, + ), + ), + ), + ], + ), + ); + }, + ), + ), + ], + ); + } +}