users panel

This commit is contained in:
Dmitry 2024-05-29 17:26:44 +03:00
parent 7aecd2dd09
commit 910c7a786f
7 changed files with 463 additions and 2 deletions

182
lib/admin/add_user.dart Normal file
View 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!,
),
),
],
),
),
],
);
}),
],
),
),
),
);
});
}
}

View file

@ -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
View 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,
);
}
}

View file

@ -1,2 +1,2 @@
const ADMIN_BUILD = false; const ADMIN_BUILD = true;

View file

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

View file

@ -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: [

View file

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