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);
},
),
_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: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<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 '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<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 {
final r = await _dio.post<String>(

View file

@ -44,7 +44,7 @@ class _AllCardsState extends State<AllCards> {
_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) {

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

View file

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

View file

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

View file

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

View file

@ -102,9 +102,8 @@ class _PacksMenuWidgetState extends State<PacksMenuWidget> {
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),
),