diff --git a/lib/admin/add_package.dart b/lib/admin/add_package.dart index b14be75..c6639df 100644 --- a/lib/admin/add_package.dart +++ b/lib/admin/add_package.dart @@ -259,6 +259,15 @@ class AddPackage with Api { cardPackDto = cardPackDto.copyWith.previewCards(cards); }, ), + _TextParam( + 'Order (lower is higher)', + cardPackDto.order?.toString(), + (v) { + cardPackDto = cardPackDto.copyWith.order( + int.tryParse(v), + ); + }, + ), ], ), ), diff --git a/lib/admin/add_user.dart b/lib/admin/add_user.dart index 31fc3f1..e740bcb 100644 --- a/lib/admin/add_user.dart +++ b/lib/admin/add_user.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -90,88 +91,73 @@ class EditUser { ), ), StatefulBuilder(builder: (context, setState) { - return Stack( - alignment: Alignment.center, - children: [ - Padding( - padding: EdgeInsets.all(8.0), - child: Column( + return Padding( + padding: EdgeInsets.all(8.0), + child: Column( + children: [ + GestureDetector( + onTap: () { + if (userDto.id != null) { + Clipboard.setData(ClipboardData( + text: userDto.id.toString())); + } + }, + child: Text('ID: ${userDto.id}'), + ), + Text('Admin: ${userDto.admin}'), + TextField( + controller: nameController, + onChanged: (v) { + userDto = userDto.copyWith(name: v); + }, + decoration: InputDecoration(hintText: 'Name'), + maxLines: 1, + style: theme.displayMedium?.copyWith( + fontSize: theme.displayMedium!.fontSize!, + ), + ), + Row( children: [ - GestureDetector( - onTap: () { - if (userDto.id != null) { - Clipboard.setData(ClipboardData( - text: userDto.id.toString())); - } - }, - child: Row( - children: [ - Expanded(child: Text('ID ')), - Text('${userDto.id}'), - ], - ), - ), - Row( - children: [ - Expanded(child: Text('Admin ')), - Text('${userDto.admin}'), - ], - ), - TextField( - controller: nameController, - onChanged: (v) { - userDto = userDto.copyWith(name: v); - }, - decoration: InputDecoration(hintText: 'Name'), - maxLines: 1, - style: theme.displayMedium?.copyWith( - fontSize: theme.displayMedium!.fontSize!, - ), - ), - Row( - children: [ - Expanded(child: Text('Subscription ')), - StatefulBuilder( - builder: (context, setState) { - return Switch( - value: userDto.subscription ?? false, - onChanged: (v) { - setState(() { - userDto = userDto.copyWith - .subscription(v); - }); - }); - }), - ], - ), - TextField( - controller: packsController, - onChanged: (v) { - final packs = v - .split(',') - .map((s) => - int.tryParse(s.trim())?.toString()) - .whereNotNull() - .toList(); - userDto = userDto.copyWith(packs: packs); - final packsText = packs.join(','); - if (!packsController.text.startsWith(packsText)) { - packsController.text = packsText; - } - }, - decoration: - InputDecoration(hintText: 'Packs'), - maxLines: 1, - style: theme.displayMedium?.copyWith( - fontSize: theme.displayMedium!.fontSize!, - ), - ), + Expanded(child: Text('Subscription ')), + StatefulBuilder(builder: (context, setState) { + return Switch( + value: userDto.subscription ?? false, + onChanged: (v) { + setState(() { + userDto = + userDto.copyWith.subscription(v); + }); + }); + }), ], ), - ), - ], + TextField( + controller: packsController, + onChanged: (v) { + final packs = v + .split(',') + .map((s) => + int.tryParse(s.trim())?.toString()) + .whereNotNull() + .toList(); + userDto = userDto.copyWith(packs: packs); + final packsText = packs.join(','); + if (!packsController.text + .startsWith(packsText)) { + packsController.text = packsText; + } + }, + decoration: InputDecoration(hintText: 'Packs'), + maxLines: 1, + style: theme.displayMedium?.copyWith( + fontSize: theme.displayMedium!.fontSize!, + ), + ), + ], + ), ); }), + _Payments(adminApi, userDto), ], ), ), @@ -180,3 +166,64 @@ class EditUser { }); } } + +class _Payments extends StatelessWidget { + final AdminApi adminApi; + final UserDto userDto; + + _Payments(this.adminApi, this.userDto); + + @override + Widget build(BuildContext context) { + final sc = StreamController?>(); + final stream = sc.stream; + + return ExpansionTile( + title: Text('Транзакции'), + maintainState: true, + onExpansionChanged: (v) async { + if (v) { + sc.add(null); + final payments = + await adminApi.getUserPurchases(userDto.id.toString()); + sc.add(payments); + } + }, + children: [ + StreamBuilder?>( + stream: stream, + builder: (context, snapshot) { + final payments = snapshot.data; + if (payments == null) { + return Text('Loading...'); + } + if (payments.isEmpty) { + return Text('Нет покупок'); + } + return Column( + children: payments.map((p) { + return ListTile( + title: Text('${p.amount} ${p.currency}'), + subtitle: Text(dateString(p.date)), + trailing: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(p.status.name), + Text(p.paymentSystem.name), + Text( + 'packs: [${p.packs.join(',')}] ${p.subscription ? 'subscription' : ''}') + ], + ), + ); + }).toList(), + ); + }, + ), + ], + ); + } + + String dateString(DateTime date) => + '${date.year}.${date.month.toString().padLeft(2, '0')}.${date.day.toString().padLeft(2, '0')} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}:${date.second.toString().padLeft(2, '0')}'; +} diff --git a/lib/admin/admin_api.dart b/lib/admin/admin_api.dart index 6d393f0..1f8c9e4 100644 --- a/lib/admin/admin_api.dart +++ b/lib/admin/admin_api.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:async'; import 'dart:convert'; import 'package:dio/dio.dart'; @@ -6,8 +8,6 @@ import 'package:mnemo_cards/features/packs/packs_api.dart'; import 'package:mnemo_cards/managers/repository/api.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; -import '../di/locator.dart'; - class AdminApi with Api { final Dio _dio; @@ -65,6 +65,14 @@ class AdminApi with Api { return (jsonDecode(r.data!)['ids'] as String).split(',').cast(); } + Future> getUserPurchases(String userId) async { + final r = await _dio + .get('$path/user/purchases', queryParameters: {'id': userId}); + return (jsonDecode(r.data!)['payments'] as List) + .cast>() + .map(PaymentDto.fromJson) + .toList(); + } Future deleteUser(UserDto dto) async { final r = await _dio.post( diff --git a/lib/admin/all_cards.dart b/lib/admin/all_cards.dart index efb6599..fd1cdb1 100644 --- a/lib/admin/all_cards.dart +++ b/lib/admin/all_cards.dart @@ -44,7 +44,7 @@ class _AllCardsState extends State { _cache.clear(); while (loadingIds.isNotEmpty) { try { - final ids = loadingIds.take(10).toList(); + final ids = loadingIds.take(20).toList(); loadingIds.removeRange(0, ids.length); final cardsPage = await api.getCards(ids); for (final card in cardsPage) { diff --git a/lib/features/tests/test_widgets/input_buttons_test.dart b/lib/features/tests/test_widgets/input_buttons_test.dart index 251af07..7d72586 100644 --- a/lib/features/tests/test_widgets/input_buttons_test.dart +++ b/lib/features/tests/test_widgets/input_buttons_test.dart @@ -7,14 +7,12 @@ import 'package:mnemo_cards/features/tests/test_manager.dart'; import 'package:mnemo_cards/features/tests/test_widgets/test_image.dart'; import 'package:mnemo_cards/theme/themes.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; -import 'package:collection/collection.dart'; import '../../../di/locator.dart'; -import '../../../widgets/game_card_widget.dart'; import '../question_states/input_buttons_test_state.dart'; import 'test_button.dart'; class InputButtonsTestWidget extends StatelessWidget { - bool get isCorrect => state.answer.join() == model.answer; + bool get isCorrect => state.answer.join() == correctAnswer; final InputButtonsTestQuestionBody _inputButtonModel; InputButtonsTestQuestionBody get model => _inputButtonModel; @@ -22,6 +20,16 @@ class InputButtonsTestWidget extends StatelessWidget { InputButtonsTestState get state => manager.getState(model.id!) as InputButtonsTestState; + String get correctAnswer { + String correctAnswer = ''; + for (int i = 0; i < model.template.length; i++) { + if (model.template[i] == '_') { + correctAnswer += model.answer[i]; + } + } + return correctAnswer; + } + TestManager get manager => locator.testManager; Color get testColor => manager.color; @@ -40,57 +48,51 @@ class InputButtonsTestWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (model.image != null) - Container( - margin: const EdgeInsets.all(4.0), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: - BorderRadius.circular(16.0), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ + Container( + margin: const EdgeInsets.all(4.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.0), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ AspectRatio( - aspectRatio: 1, + aspectRatio: model.text == null ? 1 : 1.5, child: TestImageWidget( TestImage.image(model.image!), ), ), - if (model.text != null) - Container( - padding: EdgeInsets.only(top: 8.0), - child: Center( - child: Text( - model.text!, - style: theme.headlineLarge, + if (model.text != null) + Container( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Center( + child: Text( + model.text!, + style: theme.headlineLarge, + ), ), ), - ), - ], - ), - ) - else Flexible( - child: Center( - child: Text( - model.text!, - style: theme.headlineLarge, + ], + ), + ) + else + Flexible( + child: Center( + child: Text( + model.text!, + style: theme.headlineLarge, + ), ), ), - ), StatefulBuilder(builder: (context, setState) { void buttonTapped(int index, TestButtonDto button) { if (state.answer.fold(0, (p, n) => p + n.length) >= - model.answer.length) { + correctAnswer.length) { return; } setState(() { var answer = [...state.answer, button.text!]; - final ignoreSpaces = true; - if (ignoreSpaces && - model.answer.length > answer.length && - model.answer[answer.length] == ' ') { - answer.add(' '); - } manager.setState( model.id!, state.copyWith( @@ -106,7 +108,7 @@ class InputButtonsTestWidget extends StatelessWidget { ), ); }); - print('${state.answer} ${model.answer} ${state.isCorrect}'); + print('${state.answer} ${correctAnswer} ${state.isCorrect}'); if (isCorrect) { Future.delayed( Duration(milliseconds: 300), @@ -116,19 +118,19 @@ class InputButtonsTestWidget extends StatelessWidget { } int splitIndex = 0; - if (model.answer.length * 30 > constraints.maxWidth * 0.9) { - splitIndex = model.answer.indexOf( + if (model.template.length * 30 > constraints.maxWidth * 0.9) { + splitIndex = model.template.indexOf( ' ', - (model.answer.length / 2).ceil(), + (model.template.length / 2).ceil(), ); if (splitIndex < 10) { - splitIndex = model.answer.indexOf( + splitIndex = model.template.indexOf( ' ', - (model.answer.length / 3).ceil(), + (model.template.length / 3).ceil(), ); } if (splitIndex == -1) { - splitIndex = model.answer.indexOf( + splitIndex = model.template.indexOf( ' ', 0, ); @@ -138,16 +140,19 @@ class InputButtonsTestWidget extends StatelessWidget { splitIndex = 0; } - final letters = model.answer + int stateAnswerIndex = 0; + final letters = model.template .split('') - .mapIndexed( - (index, letter) => letter == ' ' - ? SizedBox(width: 10) - : _Letter( - text: state.answer.length > index - ? state.answer[index] + .map( + (letter) => letter == '_' + ? _Letter( + text: state.answer.length > stateAnswerIndex + ? state.answer[stateAnswerIndex++] : ' ', - color: testColor, + borderColor: testColor, + ) + : _Letter.readOnly( + text: letter, ), ) .toList(); @@ -175,14 +180,10 @@ class InputButtonsTestWidget extends StatelessWidget { var answer = state.answer.toList(); var answerIds = state.answerIds.toList(); if (answer.isNotEmpty) { - if (answer.last == ' ') { - answer.removeLast(); - } - if (answer.isNotEmpty) { - answer.removeLast(); - answerIds.removeLast(); - } + answer.removeLast(); + answerIds.removeLast(); } + setState(() { manager.setState( model.id!, @@ -195,6 +196,7 @@ class InputButtonsTestWidget extends StatelessWidget { ); }); }, + behavior: HitTestBehavior.opaque, child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, @@ -246,31 +248,44 @@ class InputButtonsTestWidget extends StatelessWidget { class _Letter extends StatelessWidget { final String text; - final Color? color; + final Color? borderColor; + final Color backgroundColor; + final double _width; + final double _padding; + final double _margin; - _Letter({this.text = '', this.color}); + _Letter({ + this.text = '', + this.borderColor, + this.backgroundColor = Colors.white, + }) : this._width = 32, + this._padding = 5, + this._margin = 2.0; + + _Letter.readOnly({ + this.text = '', + }) : this.borderColor = null, + this.backgroundColor = Colors.transparent, + this._width = text == ' ' ? 10 : 20, + this._padding = 0.0, + this._margin = 0.0; @override Widget build(BuildContext context) { return Container( - width: 30, + width: _width, height: 45, - margin: const EdgeInsets.all(2.0), - padding: const EdgeInsets.all(5.0), + alignment: Alignment.center, + margin: EdgeInsets.all(_margin), + padding: EdgeInsets.all(_padding), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( - color: Colors.white, + color: backgroundColor, borderRadius: BorderRadius.circular(6.0), - border: Border.all(color: color ?? borderGray), - boxShadow: const [ - // BoxShadow( - // color: Colors.black26, - // blurRadius: 4.0, - // ), - ], + border: borderColor != null ? Border.all(color: borderColor!) : null, ), child: Material( - color: Colors.white, + color: backgroundColor, child: Text( text, style: Theme.of(context).textTheme.titleLarge, diff --git a/lib/widgets/card_pack/card_pack_header.dart b/lib/widgets/card_pack/card_pack_header.dart index a6cd2e4..971de5c 100644 --- a/lib/widgets/card_pack/card_pack_header.dart +++ b/lib/widgets/card_pack/card_pack_header.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards/utils/color_helper.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards/utils/string_helper.dart'; import '../header.dart'; @@ -26,9 +27,11 @@ class CardPackHeader extends StatelessWidget { subtitle: subtitle, popText: 'к темам', hasPopButton: true, + color: null,// color?.withOpacity(0.5), ), - SizedBox( - height: 40.h, + Container( + color: null, //color?.withOpacity(0.5), + height: 25.h, ), Container( color: color ?? Colors.grey.withOpacity(0.5), diff --git a/lib/widgets/game_card_widget.dart b/lib/widgets/game_card_widget.dart index 3121e81..5feba70 100644 --- a/lib/widgets/game_card_widget.dart +++ b/lib/widgets/game_card_widget.dart @@ -74,7 +74,7 @@ class GameCardDecoration extends StatelessWidget { ), ], borderRadius: BorderRadius.circular( - 8 + (GameCardWidget.borderRadius - 8) * alpha), + 12 + (GameCardWidget.borderRadius - 12) * alpha), border: Border.all( color: (borderColor ?? borderGray).withOpacity(1.0 - alpha * alpha), ), @@ -209,8 +209,7 @@ class FrontCard extends StatelessWidget { alignment: Alignment.center, children: [ Padding( - padding: EdgeInsets.all( - 8.0 * imageScale * (alpha / 2 + 0.5)), + padding: EdgeInsets.all(8.0 * imageScale * (alpha / 2 + 0.5)), child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: ar > 1 @@ -302,7 +301,8 @@ class FrontCard extends StatelessWidget { clipBehavior: Clip.antiAlias, decoration: BoxDecoration( borderRadius: BorderRadius.circular( - GameCardWidget.borderRadius)), + GameCardWidget.borderRadius * imageScale, + )), child: ImageFade( key: ValueKey(card.id), image: image, diff --git a/lib/widgets/header.dart b/lib/widgets/header.dart index ac135f0..a1d6060 100644 --- a/lib/widgets/header.dart +++ b/lib/widgets/header.dart @@ -13,6 +13,7 @@ class Header extends StatelessWidget { final bool hasPopButton; final String? popText; final Widget? trail; + final Color? color; Header( this.title, { @@ -20,14 +21,16 @@ class Header extends StatelessWidget { this.hasPopButton = false, this.popText, this.trail, + this.color, }); @override Widget build(BuildContext context) { - final topPadding = 64.h - + final topPadding = 74.h - MediaQuery.of(context).padding.top - - (hasPopButton ? 20.0.h : 0.0); + (hasPopButton ? 30.0.h : 0.0); return Container( + color: color, alignment: Alignment.centerLeft, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -82,22 +85,22 @@ class Header extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (title != null) - Text( - title!, - style: TextStyle( - fontSize: 36.sp, - fontWeight: FontWeight.w500, - height: 0.85, + Text( + title!, + style: TextStyle( + fontSize: 36.sp, + fontWeight: FontWeight.w500, + height: 0.85, + ), + textAlign: TextAlign.start, ), - textAlign: TextAlign.start, - ), if (trail != null) trail!, ], ), ), if (subtitle != null) Container( - padding: EdgeInsets.only(top: 4.0.h, left: 22.0.w), + padding: EdgeInsets.only(top: 4.0.h, left: 22.0.w, bottom: 4.0.h), alignment: Alignment.centerLeft, child: Text( subtitle!, diff --git a/lib/widgets/packs_menu_widget.dart b/lib/widgets/packs_menu_widget.dart index 10a7918..588affc 100644 --- a/lib/widgets/packs_menu_widget.dart +++ b/lib/widgets/packs_menu_widget.dart @@ -102,9 +102,8 @@ class _PacksMenuWidgetState extends State { Colors.black, child: ListView( physics: BouncingScrollPhysics( - decelerationRate: ScrollDecelerationRate.fast, - parent: AlwaysScrollableScrollPhysics() - ), + decelerationRate: ScrollDecelerationRate.fast, + parent: AlwaysScrollableScrollPhysics()), scrollDirection: Axis.vertical, children: [ if (snapshot.data == null) @@ -178,6 +177,7 @@ class _PackButton4 extends StatelessWidget { border: Border.all( color: pack.color?.asColor?.withOpacity(0.9) ?? Colors.grey.withOpacity(0.5), + width: 1, ), borderRadius: BorderRadius.circular(12.0), ),