This commit is contained in:
Dmitry 2024-05-31 17:35:41 +03:00
parent 910c7a786f
commit b39e074835
9 changed files with 259 additions and 174 deletions

View file

@ -259,6 +259,15 @@ class AddPackage with Api {
cardPackDto = cardPackDto.copyWith.previewCards(cards); cardPackDto = cardPackDto.copyWith.previewCards(cards);
}, },
), ),
_TextParam(
'Order (lower is higher)',
cardPackDto.order?.toString(),
(v) {
cardPackDto = cardPackDto.copyWith.order(
int.tryParse(v),
);
},
),
], ],
), ),
), ),

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
@ -90,10 +91,7 @@ class EditUser {
), ),
), ),
StatefulBuilder(builder: (context, setState) { StatefulBuilder(builder: (context, setState) {
return Stack( return Padding(
alignment: Alignment.center,
children: [
Padding(
padding: EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: Column( child: Column(
children: [ children: [
@ -104,19 +102,9 @@ class EditUser {
text: userDto.id.toString())); text: userDto.id.toString()));
} }
}, },
child: Row( child: Text('ID: ${userDto.id}'),
children: [
Expanded(child: Text('ID ')),
Text('${userDto.id}'),
],
),
),
Row(
children: [
Expanded(child: Text('Admin ')),
Text('${userDto.admin}'),
],
), ),
Text('Admin: ${userDto.admin}'),
TextField( TextField(
controller: nameController, controller: nameController,
onChanged: (v) { onChanged: (v) {
@ -131,14 +119,13 @@ class EditUser {
Row( Row(
children: [ children: [
Expanded(child: Text('Subscription ')), Expanded(child: Text('Subscription ')),
StatefulBuilder( StatefulBuilder(builder: (context, setState) {
builder: (context, setState) {
return Switch( return Switch(
value: userDto.subscription ?? false, value: userDto.subscription ?? false,
onChanged: (v) { onChanged: (v) {
setState(() { setState(() {
userDto = userDto.copyWith userDto =
.subscription(v); userDto.copyWith.subscription(v);
}); });
}); });
}), }),
@ -155,12 +142,12 @@ class EditUser {
.toList(); .toList();
userDto = userDto.copyWith(packs: packs); userDto = userDto.copyWith(packs: packs);
final packsText = packs.join(','); final packsText = packs.join(',');
if (!packsController.text.startsWith(packsText)) { if (!packsController.text
.startsWith(packsText)) {
packsController.text = packsText; packsController.text = packsText;
} }
}, },
decoration: decoration: InputDecoration(hintText: 'Packs'),
InputDecoration(hintText: 'Packs'),
maxLines: 1, maxLines: 1,
style: theme.displayMedium?.copyWith( style: theme.displayMedium?.copyWith(
fontSize: theme.displayMedium!.fontSize!, fontSize: theme.displayMedium!.fontSize!,
@ -168,10 +155,9 @@ class EditUser {
), ),
], ],
), ),
),
],
); );
}), }),
_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<List<PaymentDto>?>();
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<List<PaymentDto>?>(
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')}';
}

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dio/dio.dart'; 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/managers/repository/api.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import '../di/locator.dart';
class AdminApi with Api { class AdminApi with Api {
final Dio _dio; final Dio _dio;
@ -65,6 +65,14 @@ class AdminApi with Api {
return (jsonDecode(r.data!)['ids'] as String).split(',').cast(); return (jsonDecode(r.data!)['ids'] as String).split(',').cast();
} }
Future<List<PaymentDto>> getUserPurchases(String userId) async {
final r = await _dio
.get<String>('$path/user/purchases', queryParameters: {'id': userId});
return (jsonDecode(r.data!)['payments'] as List)
.cast<Map<String, Object?>>()
.map(PaymentDto.fromJson)
.toList();
}
Future<bool> deleteUser(UserDto dto) async { Future<bool> deleteUser(UserDto dto) async {
final r = await _dio.post<String>( final r = await _dio.post<String>(

View file

@ -44,7 +44,7 @@ class _AllCardsState extends State<AllCards> {
_cache.clear(); _cache.clear();
while (loadingIds.isNotEmpty) { while (loadingIds.isNotEmpty) {
try { try {
final ids = loadingIds.take(10).toList(); final ids = loadingIds.take(20).toList();
loadingIds.removeRange(0, ids.length); loadingIds.removeRange(0, ids.length);
final cardsPage = await api.getCards(ids); final cardsPage = await api.getCards(ids);
for (final card in cardsPage) { for (final card in cardsPage) {

View file

@ -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/features/tests/test_widgets/test_image.dart';
import 'package:mnemo_cards/theme/themes.dart'; import 'package:mnemo_cards/theme/themes.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import 'package:collection/collection.dart';
import '../../../di/locator.dart'; import '../../../di/locator.dart';
import '../../../widgets/game_card_widget.dart';
import '../question_states/input_buttons_test_state.dart'; import '../question_states/input_buttons_test_state.dart';
import 'test_button.dart'; import 'test_button.dart';
class InputButtonsTestWidget extends StatelessWidget { class InputButtonsTestWidget extends StatelessWidget {
bool get isCorrect => state.answer.join() == model.answer; bool get isCorrect => state.answer.join() == correctAnswer;
final InputButtonsTestQuestionBody _inputButtonModel; final InputButtonsTestQuestionBody _inputButtonModel;
InputButtonsTestQuestionBody get model => _inputButtonModel; InputButtonsTestQuestionBody get model => _inputButtonModel;
@ -22,6 +20,16 @@ class InputButtonsTestWidget extends StatelessWidget {
InputButtonsTestState get state => InputButtonsTestState get state =>
manager.getState(model.id!) as InputButtonsTestState; 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; TestManager get manager => locator.testManager;
Color get testColor => manager.color; Color get testColor => manager.color;
@ -44,21 +52,20 @@ class InputButtonsTestWidget extends StatelessWidget {
margin: const EdgeInsets.all(4.0), margin: const EdgeInsets.all(4.0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: borderRadius: BorderRadius.circular(16.0),
BorderRadius.circular(16.0),
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
AspectRatio( AspectRatio(
aspectRatio: 1, aspectRatio: model.text == null ? 1 : 1.5,
child: TestImageWidget( child: TestImageWidget(
TestImage.image(model.image!), TestImage.image(model.image!),
), ),
), ),
if (model.text != null) if (model.text != null)
Container( Container(
padding: EdgeInsets.only(top: 8.0), padding: EdgeInsets.symmetric(vertical: 8.0),
child: Center( child: Center(
child: Text( child: Text(
model.text!, model.text!,
@ -69,7 +76,8 @@ class InputButtonsTestWidget extends StatelessWidget {
], ],
), ),
) )
else Flexible( else
Flexible(
child: Center( child: Center(
child: Text( child: Text(
model.text!, model.text!,
@ -80,17 +88,11 @@ class InputButtonsTestWidget extends StatelessWidget {
StatefulBuilder(builder: (context, setState) { StatefulBuilder(builder: (context, setState) {
void buttonTapped(int index, TestButtonDto button) { void buttonTapped(int index, TestButtonDto button) {
if (state.answer.fold(0, (p, n) => p + n.length) >= if (state.answer.fold(0, (p, n) => p + n.length) >=
model.answer.length) { correctAnswer.length) {
return; return;
} }
setState(() { setState(() {
var answer = [...state.answer, button.text!]; var answer = [...state.answer, button.text!];
final ignoreSpaces = true;
if (ignoreSpaces &&
model.answer.length > answer.length &&
model.answer[answer.length] == ' ') {
answer.add(' ');
}
manager.setState( manager.setState(
model.id!, model.id!,
state.copyWith( 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) { if (isCorrect) {
Future.delayed( Future.delayed(
Duration(milliseconds: 300), Duration(milliseconds: 300),
@ -116,19 +118,19 @@ class InputButtonsTestWidget extends StatelessWidget {
} }
int splitIndex = 0; int splitIndex = 0;
if (model.answer.length * 30 > constraints.maxWidth * 0.9) { if (model.template.length * 30 > constraints.maxWidth * 0.9) {
splitIndex = model.answer.indexOf( splitIndex = model.template.indexOf(
' ', ' ',
(model.answer.length / 2).ceil(), (model.template.length / 2).ceil(),
); );
if (splitIndex < 10) { if (splitIndex < 10) {
splitIndex = model.answer.indexOf( splitIndex = model.template.indexOf(
' ', ' ',
(model.answer.length / 3).ceil(), (model.template.length / 3).ceil(),
); );
} }
if (splitIndex == -1) { if (splitIndex == -1) {
splitIndex = model.answer.indexOf( splitIndex = model.template.indexOf(
' ', ' ',
0, 0,
); );
@ -138,16 +140,19 @@ class InputButtonsTestWidget extends StatelessWidget {
splitIndex = 0; splitIndex = 0;
} }
final letters = model.answer int stateAnswerIndex = 0;
final letters = model.template
.split('') .split('')
.mapIndexed( .map(
(index, letter) => letter == ' ' (letter) => letter == '_'
? SizedBox(width: 10) ? _Letter(
: _Letter( text: state.answer.length > stateAnswerIndex
text: state.answer.length > index ? state.answer[stateAnswerIndex++]
? state.answer[index]
: ' ', : ' ',
color: testColor, borderColor: testColor,
)
: _Letter.readOnly(
text: letter,
), ),
) )
.toList(); .toList();
@ -174,15 +179,11 @@ class InputButtonsTestWidget extends StatelessWidget {
onTap: () { onTap: () {
var answer = state.answer.toList(); var answer = state.answer.toList();
var answerIds = state.answerIds.toList(); var answerIds = state.answerIds.toList();
if (answer.isNotEmpty) {
if (answer.last == ' ') {
answer.removeLast();
}
if (answer.isNotEmpty) { if (answer.isNotEmpty) {
answer.removeLast(); answer.removeLast();
answerIds.removeLast(); answerIds.removeLast();
} }
}
setState(() { setState(() {
manager.setState( manager.setState(
model.id!, model.id!,
@ -195,6 +196,7 @@ class InputButtonsTestWidget extends StatelessWidget {
); );
}); });
}, },
behavior: HitTestBehavior.opaque,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -246,31 +248,44 @@ class InputButtonsTestWidget extends StatelessWidget {
class _Letter extends StatelessWidget { class _Letter extends StatelessWidget {
final String text; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
width: 30, width: _width,
height: 45, height: 45,
margin: const EdgeInsets.all(2.0), alignment: Alignment.center,
padding: const EdgeInsets.all(5.0), margin: EdgeInsets.all(_margin),
padding: EdgeInsets.all(_padding),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: backgroundColor,
borderRadius: BorderRadius.circular(6.0), borderRadius: BorderRadius.circular(6.0),
border: Border.all(color: color ?? borderGray), border: borderColor != null ? Border.all(color: borderColor!) : null,
boxShadow: const [
// BoxShadow(
// color: Colors.black26,
// blurRadius: 4.0,
// ),
],
), ),
child: Material( child: Material(
color: Colors.white, color: backgroundColor,
child: Text( child: Text(
text, text,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.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_common/mnemo_cards_common.dart';
import 'package:mnemo_cards/utils/string_helper.dart'; import 'package:mnemo_cards/utils/string_helper.dart';
import '../header.dart'; import '../header.dart';
@ -26,9 +27,11 @@ class CardPackHeader extends StatelessWidget {
subtitle: subtitle, subtitle: subtitle,
popText: 'к темам', popText: 'к темам',
hasPopButton: true, hasPopButton: true,
color: null,// color?.withOpacity(0.5),
), ),
SizedBox( Container(
height: 40.h, color: null, //color?.withOpacity(0.5),
height: 25.h,
), ),
Container( Container(
color: color ?? Colors.grey.withOpacity(0.5), color: color ?? Colors.grey.withOpacity(0.5),

View file

@ -74,7 +74,7 @@ class GameCardDecoration extends StatelessWidget {
), ),
], ],
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
8 + (GameCardWidget.borderRadius - 8) * alpha), 12 + (GameCardWidget.borderRadius - 12) * alpha),
border: Border.all( border: Border.all(
color: (borderColor ?? borderGray).withOpacity(1.0 - alpha * alpha), color: (borderColor ?? borderGray).withOpacity(1.0 - alpha * alpha),
), ),
@ -209,8 +209,7 @@ class FrontCard extends StatelessWidget {
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
Padding( Padding(
padding: EdgeInsets.all( padding: EdgeInsets.all(8.0 * imageScale * (alpha / 2 + 0.5)),
8.0 * imageScale * (alpha / 2 + 0.5)),
child: Column( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: ar > 1 mainAxisAlignment: ar > 1
@ -302,7 +301,8 @@ class FrontCard extends StatelessWidget {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
GameCardWidget.borderRadius)), GameCardWidget.borderRadius * imageScale,
)),
child: ImageFade( child: ImageFade(
key: ValueKey(card.id), key: ValueKey(card.id),
image: image, image: image,

View file

@ -13,6 +13,7 @@ class Header extends StatelessWidget {
final bool hasPopButton; final bool hasPopButton;
final String? popText; final String? popText;
final Widget? trail; final Widget? trail;
final Color? color;
Header( Header(
this.title, { this.title, {
@ -20,14 +21,16 @@ class Header extends StatelessWidget {
this.hasPopButton = false, this.hasPopButton = false,
this.popText, this.popText,
this.trail, this.trail,
this.color,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final topPadding = 64.h - final topPadding = 74.h -
MediaQuery.of(context).padding.top - MediaQuery.of(context).padding.top -
(hasPopButton ? 20.0.h : 0.0); (hasPopButton ? 30.0.h : 0.0);
return Container( return Container(
color: color,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -97,7 +100,7 @@ class Header extends StatelessWidget {
), ),
if (subtitle != null) if (subtitle != null)
Container( 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, alignment: Alignment.centerLeft,
child: Text( child: Text(
subtitle!, subtitle!,

View file

@ -103,8 +103,7 @@ class _PacksMenuWidgetState extends State<PacksMenuWidget> {
child: ListView( child: ListView(
physics: BouncingScrollPhysics( physics: BouncingScrollPhysics(
decelerationRate: ScrollDecelerationRate.fast, decelerationRate: ScrollDecelerationRate.fast,
parent: AlwaysScrollableScrollPhysics() parent: AlwaysScrollableScrollPhysics()),
),
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
children: [ children: [
if (snapshot.data == null) if (snapshot.data == null)
@ -178,6 +177,7 @@ class _PackButton4 extends StatelessWidget {
border: Border.all( border: Border.all(
color: pack.color?.asColor?.withOpacity(0.9) ?? color: pack.color?.asColor?.withOpacity(0.9) ??
Colors.grey.withOpacity(0.5), Colors.grey.withOpacity(0.5),
width: 1,
), ),
borderRadius: BorderRadius.circular(12.0), borderRadius: BorderRadius.circular(12.0),
), ),