367 lines
12 KiB
Dart
367 lines
12 KiB
Dart
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 {
|
||
final ids = loadingIds.take(10).toList();
|
||
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,
|
||
),
|
||
),
|
||
]),
|
||
);
|
||
}
|
||
}
|