users panel
This commit is contained in:
parent
7aecd2dd09
commit
910c7a786f
7 changed files with 463 additions and 2 deletions
182
lib/admin/add_user.dart
Normal file
182
lib/admin/add_user.dart
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:mnemo_cards/admin/admin_api.dart';
|
||||||
|
import 'package:mnemo_cards/domain/router/app_router.dart';
|
||||||
|
import 'package:mnemo_cards/features/packs/images_holder.dart';
|
||||||
|
import 'package:mnemo_cards/utils/iterable_helper.dart';
|
||||||
|
import 'package:mnemo_cards/widgets/game_card_widget.dart';
|
||||||
|
import 'package:mnemo_cards/widgets/header.dart';
|
||||||
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
|
||||||
|
import '../di/locator.dart';
|
||||||
|
import '../main.dart';
|
||||||
|
import '../widgets/image_fade.dart';
|
||||||
|
|
||||||
|
class EditUser {
|
||||||
|
UserDto userDto;
|
||||||
|
AdminApi adminApi;
|
||||||
|
bool imageChanged = false;
|
||||||
|
|
||||||
|
EditUser(UserDto userDto, this.adminApi) : userDto = userDto;
|
||||||
|
|
||||||
|
void _saveUser() async {
|
||||||
|
final dto = userDto;
|
||||||
|
final result = await adminApi.editUser(dto);
|
||||||
|
if (!result) {
|
||||||
|
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
||||||
|
content: Text('ERROR'),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
||||||
|
content: Text('Success!!!'),
|
||||||
|
));
|
||||||
|
appRouter.maybePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _deleteCard() async {
|
||||||
|
final result = await adminApi.deleteUser(userDto);
|
||||||
|
if (!result) {
|
||||||
|
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
||||||
|
content: Text('ERROR'),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
||||||
|
content: Text('Success!!!'),
|
||||||
|
));
|
||||||
|
appRouter.maybePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addUser(BuildContext context) {
|
||||||
|
final theme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
|
final TextEditingController nameController =
|
||||||
|
TextEditingController(text: userDto.name);
|
||||||
|
|
||||||
|
final TextEditingController packsController =
|
||||||
|
TextEditingController(text: userDto.packs.join(','));
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (c) {
|
||||||
|
return Center(
|
||||||
|
child: Scaffold(
|
||||||
|
body: Material(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
Header(
|
||||||
|
'${userDto.id}',
|
||||||
|
hasPopButton: true,
|
||||||
|
trail: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _saveUser,
|
||||||
|
icon: Icon(Icons.save),
|
||||||
|
),
|
||||||
|
if (userDto.id != null && userDto.id! >= 0)
|
||||||
|
GestureDetector(
|
||||||
|
onLongPress: _deleteCard,
|
||||||
|
child: Icon(Icons.delete),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
StatefulBuilder(builder: (context, setState) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
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!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,6 +47,41 @@ class AdminApi with Api {
|
||||||
.cast();
|
.cast();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<UserDto>> getUsers(List<String> ids) async {
|
||||||
|
final r = await _dio.get<String>(
|
||||||
|
'$path/users',
|
||||||
|
queryParameters: {'ids': ids.join(',')},
|
||||||
|
);
|
||||||
|
return (jsonDecode(r.data!) as List)
|
||||||
|
.map((e) => UserDto.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList()
|
||||||
|
.cast();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getUserIds() async {
|
||||||
|
final r = await _dio.get<String>(
|
||||||
|
'$path/users/ids',
|
||||||
|
);
|
||||||
|
return (jsonDecode(r.data!)['ids'] as String).split(',').cast();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Future<bool> deleteUser(UserDto dto) async {
|
||||||
|
final r = await _dio.post<String>(
|
||||||
|
'$path/users/delete',
|
||||||
|
data: jsonEncode({'id': dto.id.toString()}),
|
||||||
|
);
|
||||||
|
return r.statusCode == 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> editUser(UserDto dto) async {
|
||||||
|
final r = await _dio.post<String>(
|
||||||
|
'$path/users/add',
|
||||||
|
data: jsonEncode(dto.toJson()),
|
||||||
|
);
|
||||||
|
return r.statusCode == 200;
|
||||||
|
}
|
||||||
|
|
||||||
Future<EditCardPackDto?> getEditPack(String id) async {
|
Future<EditCardPackDto?> getEditPack(String id) async {
|
||||||
final r = await _dio.get<String>(
|
final r = await _dio.get<String>(
|
||||||
'$path/pack/edit/$id',
|
'$path/pack/edit/$id',
|
||||||
|
|
|
||||||
234
lib/admin/all_users.dart
Normal file
234
lib/admin/all_users.dart
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:mnemo_cards/admin/add_card.dart';
|
||||||
|
import 'package:mnemo_cards/admin/add_user.dart';
|
||||||
|
import 'package:mnemo_cards/admin/admin_api.dart';
|
||||||
|
import 'package:mnemo_cards/di/injector.dart';
|
||||||
|
import 'package:mnemo_cards/di/locator.dart';
|
||||||
|
import 'package:mnemo_cards/managers/repository/dio_provider.dart';
|
||||||
|
import 'package:mnemo_cards/widgets/header.dart';
|
||||||
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
|
||||||
|
import '../main.dart';
|
||||||
|
|
||||||
|
class AllUsers extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _AllUsersState();
|
||||||
|
|
||||||
|
const AllUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AllUsersState extends State<AllUsers> {
|
||||||
|
static List<String> ids = [];
|
||||||
|
static Set<UserDto> users = {};
|
||||||
|
static String filter = '';
|
||||||
|
static UserDto? _currentUser;
|
||||||
|
static String? _currentId;
|
||||||
|
|
||||||
|
bool get selectingMode => false; //_selectedCards.isNotEmpty;
|
||||||
|
|
||||||
|
Dio get dio => getIt.get<DioProvider>().dio;
|
||||||
|
|
||||||
|
AdminApi get api => getIt.get<AdminApi>();
|
||||||
|
|
||||||
|
Future<void> loadUsers() async {
|
||||||
|
var loadingIds = ids.toList();
|
||||||
|
users.clear();
|
||||||
|
while (loadingIds.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final ids = loadingIds.take(10).toList();
|
||||||
|
loadingIds.removeRange(0, ids.length);
|
||||||
|
final usersPage = await api.getUsers(ids);
|
||||||
|
users.addAll(usersPage);
|
||||||
|
setState(() {});
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadIds() async {
|
||||||
|
ids = await api.getUserIds();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadCurrentUser() async {
|
||||||
|
if (_currentId != null) {
|
||||||
|
_currentUser = (await api.getUsers([_currentId!])).firstOrNull;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearCurrentUser() {
|
||||||
|
_currentId = null;
|
||||||
|
_currentUser = null;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> editUser(UserDto user) async {
|
||||||
|
final result = await api.editUser(user);
|
||||||
|
if (!result) {
|
||||||
|
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
||||||
|
content: Text('ERROR'),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
||||||
|
content: Text('Success!!!'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<UserDto> buildUsersList() {
|
||||||
|
final filteredCards = users.where((dto) {
|
||||||
|
final filterOk = filter.isEmpty ||
|
||||||
|
dto.name.toString().toLowerCase().contains(filter) ||
|
||||||
|
dto.id.toString().toLowerCase().contains(filter) ||
|
||||||
|
dto.packs.any((p) => p.toLowerCase().contains(filter));
|
||||||
|
return filterOk;
|
||||||
|
});
|
||||||
|
return filteredCards.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final filteredCards = buildUsersList();
|
||||||
|
return Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
ListView.builder(itemBuilder: (context, index) {
|
||||||
|
if (index == 0)
|
||||||
|
return Container(
|
||||||
|
height: 270,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Header(
|
||||||
|
_currentId == null
|
||||||
|
? 'Все'
|
||||||
|
: _currentUser?.name ?? 'Пак ${_currentUser?.id}',
|
||||||
|
subtitle: filteredCards.isEmpty
|
||||||
|
? 'Нет пользователей'
|
||||||
|
: '${filteredCards.length} пользователя',
|
||||||
|
hasPopButton: true,
|
||||||
|
trail: IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {});
|
||||||
|
await loadIds();
|
||||||
|
await loadUsers();
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.sync)),
|
||||||
|
),
|
||||||
|
dropdownButton(context),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(hintText: 'Поиск'),
|
||||||
|
onChanged: (v) {
|
||||||
|
if (filter != v.toLowerCase()) {
|
||||||
|
filter = v.toLowerCase();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Expanded(
|
||||||
|
// child: GestureDetector(
|
||||||
|
// onTap: () => EditUser(null, api).addCard(context),
|
||||||
|
// child: Container(
|
||||||
|
// color: Colors.green.withOpacity(0.1),
|
||||||
|
// alignment: Alignment.center,
|
||||||
|
// child: Icon(Icons.add, size: 30),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
else if (filteredCards.length > index - 1)
|
||||||
|
return userWidget(filteredCards[index - 1]);
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
if (selectingMode)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Container(
|
||||||
|
height: 100,
|
||||||
|
padding: EdgeInsets.all(10.0),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
color: Colors.blue,
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [Icon(Icons.clear)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget userWidget(UserDto user) {
|
||||||
|
void toggleSelect() {
|
||||||
|
final id = user.id.toString();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (selectingMode) {
|
||||||
|
toggleSelect();
|
||||||
|
} else {
|
||||||
|
EditUser(user, api).addUser(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: toggleSelect,
|
||||||
|
child: _User(user, api, false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget dropdownButton(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: null,
|
||||||
|
icon: _currentUser == null ? Icon(Icons.add) : Icon(Icons.edit),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _User extends StatelessWidget {
|
||||||
|
final UserDto userDto;
|
||||||
|
final AdminApi api;
|
||||||
|
final bool selected;
|
||||||
|
|
||||||
|
_User(this.userDto, this.api, this.selected);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isCurrent = locator.userManager.userStateHolder.user?.id == userDto.id;
|
||||||
|
return ListTile(
|
||||||
|
title: Text('${userDto.id}${isCurrent ? ' (you)' : ''}'),
|
||||||
|
subtitle: Text(userDto.name ?? ''),
|
||||||
|
trailing: userDto.admin ? Text('admin') : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
|
|
||||||
const ADMIN_BUILD = false;
|
const ADMIN_BUILD = true;
|
||||||
|
|
@ -12,6 +12,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../admin/all_cards.dart';
|
import '../admin/all_cards.dart';
|
||||||
|
import '../admin/all_users.dart';
|
||||||
import '../di/locator.dart';
|
import '../di/locator.dart';
|
||||||
import '../features/analytics/analytics.dart';
|
import '../features/analytics/analytics.dart';
|
||||||
import '../theme/themes.dart';
|
import '../theme/themes.dart';
|
||||||
|
|
@ -70,6 +71,14 @@ class ProfilePage extends StatelessWidget {
|
||||||
},
|
},
|
||||||
child: Text('ALL CARDS'),
|
child: Text('ALL CARDS'),
|
||||||
),
|
),
|
||||||
|
if (ADMIN_BUILD)
|
||||||
|
MaterialButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context, builder: (c) => const AllUsers());
|
||||||
|
},
|
||||||
|
child: Text('ALL USERS'),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10.0.w),
|
padding: EdgeInsets.symmetric(horizontal: 10.0.w),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ class _PacksMenuWidgetState extends State<PacksMenuWidget> {
|
||||||
child: ListView(
|
child: ListView(
|
||||||
physics: BouncingScrollPhysics(
|
physics: BouncingScrollPhysics(
|
||||||
decelerationRate: ScrollDecelerationRate.fast,
|
decelerationRate: ScrollDecelerationRate.fast,
|
||||||
|
parent: AlwaysScrollableScrollPhysics()
|
||||||
),
|
),
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.0+4
|
version: 1.0.0+5
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.1.3 <4.0.0'
|
sdk: '>=3.1.3 <4.0.0'
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue