2024-05-22 20:48:44 +00:00
|
|
|
|
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/admin_api.dart';
|
|
|
|
|
|
import 'package:mnemo_cards/di/injector.dart';
|
|
|
|
|
|
import 'package:mnemo_cards/managers/repository/dio_provider.dart';
|
|
|
|
|
|
import 'package:mnemo_cards/theme/themes.dart';
|
|
|
|
|
|
import 'package:mnemo_cards/widgets/header.dart';
|
|
|
|
|
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
|
|
|
|
|
|
|
|
|
|
import '../main.dart';
|
|
|
|
|
|
import 'add_package.dart';
|
|
|
|
|
|
|
|
|
|
|
|
class AllCards extends StatefulWidget {
|
|
|
|
|
|
@override
|
|
|
|
|
|
State<StatefulWidget> createState() => _AllCardsState();
|
|
|
|
|
|
|
|
|
|
|
|
const AllCards();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _AllCardsState extends State<AllCards> {
|
|
|
|
|
|
static List<String> ids = [];
|
|
|
|
|
|
static Set<GameCardDto> cards = {};
|
|
|
|
|
|
static String filter = '';
|
|
|
|
|
|
static List<CardPackPreviewDto> _packPreviews = [];
|
|
|
|
|
|
static EditCardPackDto? _currentPack;
|
|
|
|
|
|
static String? _currentId;
|
|
|
|
|
|
static List<String> _selectedCards = [];
|
|
|
|
|
|
static Map<String, String> _cache = {};
|
|
|
|
|
|
|
|
|
|
|
|
bool get selectingMode => _selectedCards.isNotEmpty;
|
|
|
|
|
|
|
|
|
|
|
|
Dio get dio => getIt.get<DioProvider>().dio;
|
|
|
|
|
|
|
|
|
|
|
|
AdminApi get api => getIt.get<AdminApi>();
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> loadCards() async {
|
|
|
|
|
|
var loadingIds = ids.toList();
|
|
|
|
|
|
cards.clear();
|
|
|
|
|
|
_cache.clear();
|
|
|
|
|
|
while (loadingIds.isNotEmpty) {
|
|
|
|
|
|
try {
|
2024-05-31 14:35:41 +00:00
|
|
|
|
final ids = loadingIds.take(20).toList();
|
2024-05-22 20:48:44 +00:00
|
|
|
|
loadingIds.removeRange(0, ids.length);
|
|
|
|
|
|
final cardsPage = await api.getCards(ids);
|
|
|
|
|
|
for (final card in cardsPage) {
|
|
|
|
|
|
final imageString =
|
|
|
|
|
|
card.image?.substring(0, min(100, card.image!.length)) ?? '';
|
|
|
|
|
|
_cache[card.id.toString()] =
|
|
|
|
|
|
'${card.id}${card.mnemo}${card.transcription}${card.transcriptionMnemo}${card.original}${card.translation}${imageString}';
|
|
|
|
|
|
}
|
|
|
|
|
|
cards.addAll(cardsPage);
|
|
|
|
|
|
setState(() {});
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
print(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> loadIds() async {
|
|
|
|
|
|
ids = await api.getCardIds();
|
|
|
|
|
|
setState(() {});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> loadPacks() async {
|
|
|
|
|
|
_packPreviews = await api.packs();
|
|
|
|
|
|
clearCurrentPack();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> loadCurrentPack() async {
|
|
|
|
|
|
if (_currentId != null) {
|
|
|
|
|
|
_currentPack = await api.getEditPack(_currentId!);
|
|
|
|
|
|
setState(() {});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void clearCurrentPack() {
|
|
|
|
|
|
_currentId = null;
|
|
|
|
|
|
_currentPack = null;
|
|
|
|
|
|
setState(() {});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> editPack(EditCardPackDto pack) async {
|
|
|
|
|
|
final result = await api.editPack(pack);
|
|
|
|
|
|
if (!result) {
|
|
|
|
|
|
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
|
|
|
|
content: Text('ERROR'),
|
|
|
|
|
|
));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar(
|
|
|
|
|
|
content: Text('Success!!!'),
|
|
|
|
|
|
));
|
|
|
|
|
|
loadCurrentPack();
|
|
|
|
|
|
_selectedCards.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void addOrEditPack() => AddPackage(_currentPack, api).addPack(context);
|
|
|
|
|
|
|
|
|
|
|
|
List<GameCardDto> buildCards() {
|
|
|
|
|
|
final filteredCards = cards.where((dto) {
|
|
|
|
|
|
final filterOk =
|
|
|
|
|
|
filter.isEmpty || _cache[dto.id.toString()]?.contains(filter) == true;
|
|
|
|
|
|
final packFilter =
|
|
|
|
|
|
_currentPack?.addCardIds?.contains(dto.id.toString()) ?? true;
|
|
|
|
|
|
return filterOk && packFilter;
|
|
|
|
|
|
});
|
|
|
|
|
|
return filteredCards.toList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final filteredCards = buildCards();
|
|
|
|
|
|
return Scaffold(
|
|
|
|
|
|
body: SafeArea(
|
|
|
|
|
|
child: Stack(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
ListView.builder(
|
|
|
|
|
|
itemExtentBuilder: (index, dims) => index >= 1 ? 160.h : 350,
|
|
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
|
|
if (index == 0)
|
|
|
|
|
|
return Column(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Header(
|
|
|
|
|
|
_currentId == null
|
|
|
|
|
|
? 'Все карточки'
|
|
|
|
|
|
: _currentPack?.title ??
|
|
|
|
|
|
'Пак ${_currentPack?.id}',
|
|
|
|
|
|
subtitle: filteredCards.isEmpty
|
|
|
|
|
|
? 'Нет карточек'
|
|
|
|
|
|
: '${filteredCards.length} карточек',
|
|
|
|
|
|
hasPopButton: true,
|
|
|
|
|
|
trail: IconButton(
|
|
|
|
|
|
onPressed: () async {
|
|
|
|
|
|
setState(() {});
|
|
|
|
|
|
await loadIds();
|
|
|
|
|
|
await loadPacks();
|
|
|
|
|
|
await loadCards();
|
|
|
|
|
|
},
|
|
|
|
|
|
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: () => EditCard(null, api).addCard(context),
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
color: Colors.green.withOpacity(0.1),
|
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
|
child: Icon(Icons.add, size: 30),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
if (filteredCards.length >= (index - 1) * 3) {
|
|
|
|
|
|
final cardIndex = (index - 1) * 3;
|
|
|
|
|
|
return Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
if (filteredCards.length > cardIndex)
|
|
|
|
|
|
cardWidget(filteredCards[cardIndex]),
|
|
|
|
|
|
if (filteredCards.length > cardIndex + 1)
|
|
|
|
|
|
cardWidget(filteredCards[cardIndex + 1]),
|
|
|
|
|
|
if (filteredCards.length > cardIndex + 2)
|
|
|
|
|
|
cardWidget(filteredCards[cardIndex + 2]),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
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: () {
|
|
|
|
|
|
_selectedCards.clear();
|
|
|
|
|
|
setState(() {});
|
|
|
|
|
|
},
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
color: Colors.blue,
|
|
|
|
|
|
padding: EdgeInsets.all(8.0),
|
|
|
|
|
|
child: Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Text('${_selectedCards.length} '),
|
|
|
|
|
|
Icon(Icons.clear)
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
if (_currentPack != null)
|
|
|
|
|
|
if (_selectedCards.every((c) =>
|
|
|
|
|
|
_currentPack!.addCardIds?.contains(c) ?? false))
|
|
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
SizedBox(width: 8.0),
|
|
|
|
|
|
GestureDetector(
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
final updated =
|
|
|
|
|
|
_currentPack!.copyWith.removeCardIds(
|
|
|
|
|
|
_selectedCards.toList(),
|
|
|
|
|
|
);
|
|
|
|
|
|
editPack(updated);
|
|
|
|
|
|
},
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
padding: EdgeInsets.all(8.0),
|
|
|
|
|
|
color: Colors.blue,
|
|
|
|
|
|
child: Text('Убрать из пака'),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
SizedBox(width: 8.0),
|
|
|
|
|
|
GestureDetector(
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
final updated =
|
|
|
|
|
|
_currentPack!.copyWith.previewCards(
|
|
|
|
|
|
_selectedCards.toList(),
|
|
|
|
|
|
);
|
|
|
|
|
|
editPack(updated);
|
|
|
|
|
|
},
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
padding: EdgeInsets.all(8.0),
|
|
|
|
|
|
color: Colors.blue,
|
|
|
|
|
|
child: Text('Как превью'),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
|
|
|
|
|
else
|
|
|
|
|
|
MaterialButton(
|
|
|
|
|
|
onPressed: () {
|
|
|
|
|
|
final updated = _currentPack!.copyWith.addCardIds(
|
|
|
|
|
|
_selectedCards.toList(),
|
|
|
|
|
|
);
|
|
|
|
|
|
editPack(updated);
|
|
|
|
|
|
},
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
padding: EdgeInsets.all(8.0),
|
|
|
|
|
|
color: Colors.blue,
|
|
|
|
|
|
child: Text('Добавить в пак'),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget cardWidget(GameCardDto card) {
|
|
|
|
|
|
void toggleSelect() {
|
|
|
|
|
|
final id = card.id.toString();
|
|
|
|
|
|
if (_selectedCards.contains(id)) {
|
|
|
|
|
|
_selectedCards.remove(id);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
_selectedCards.add(card.id.toString());
|
|
|
|
|
|
}
|
|
|
|
|
|
setState(() {});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
onTap: () {
|
|
|
|
|
|
if (selectingMode) {
|
|
|
|
|
|
toggleSelect();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
EditCard(card, api).addCard(context);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onLongPress: toggleSelect,
|
|
|
|
|
|
child: _Card(card, api, _selectedCards.contains(card.id.toString())),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void _onPackChanged(String? id) {
|
|
|
|
|
|
if (id == null) {
|
|
|
|
|
|
clearCurrentPack();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
_currentId = id;
|
|
|
|
|
|
loadCurrentPack();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Widget dropdownButton(BuildContext context) {
|
|
|
|
|
|
return Row(
|
|
|
|
|
|
children: [
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: DropdownButton(
|
|
|
|
|
|
items: [..._packPreviews, null]
|
|
|
|
|
|
.map((p) => DropdownMenuItem<String?>(
|
|
|
|
|
|
child: Padding(
|
|
|
|
|
|
padding: const EdgeInsets.all(8.0),
|
|
|
|
|
|
child: p == null
|
|
|
|
|
|
? Text('Все паки')
|
|
|
|
|
|
: Text('${p.title} ${p.cards}'),
|
|
|
|
|
|
),
|
|
|
|
|
|
value: p?.id,
|
|
|
|
|
|
))
|
|
|
|
|
|
.toList(),
|
|
|
|
|
|
value: _currentId,
|
|
|
|
|
|
onChanged: (v) => _onPackChanged(v),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IconButton(
|
|
|
|
|
|
onPressed: addOrEditPack,
|
|
|
|
|
|
icon: _currentPack == null ? Icon(Icons.add) : Icon(Icons.edit),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class _Card extends StatelessWidget {
|
|
|
|
|
|
final GameCardDto cardDto;
|
|
|
|
|
|
final AdminApi api;
|
|
|
|
|
|
final bool selected;
|
|
|
|
|
|
|
|
|
|
|
|
_Card(this.cardDto, this.api, this.selected);
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final image = cardDto.image != null ? base64.decode(cardDto.image!) : null;
|
|
|
|
|
|
return Container(
|
|
|
|
|
|
padding: EdgeInsets.all(8.0),
|
|
|
|
|
|
width: MediaQuery.of(context).size.width / 3,
|
|
|
|
|
|
height: 160.h,
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
border: Border.all(color: selected ? Colors.blue : borderGray),
|
|
|
|
|
|
color: selected ? Colors.blue.withOpacity(0.2) : null,
|
|
|
|
|
|
),
|
|
|
|
|
|
child: Column(children: [
|
|
|
|
|
|
Text(cardDto.translation ?? ''),
|
|
|
|
|
|
Text(cardDto.original ?? ''),
|
|
|
|
|
|
if (image != null)
|
|
|
|
|
|
Expanded(
|
|
|
|
|
|
child: Image.memory(
|
|
|
|
|
|
key: ObjectKey(cardDto.image),
|
|
|
|
|
|
gaplessPlayback: true,
|
|
|
|
|
|
image,
|
|
|
|
|
|
fit: BoxFit.scaleDown,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
]),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|