276 lines
8.5 KiB
Dart
276 lines
8.5 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:auto_route/auto_route.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:mnemo_cards/features/packs/images_holder.dart';
|
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:reorderables/reorderables.dart';
|
|
import 'package:rxdart/rxdart.dart';
|
|
|
|
import '../../di/locator.dart';
|
|
import '../../utils/string_helper.dart';
|
|
import '../../domain/router/app_router.gr.dart';
|
|
import '../../main.dart';
|
|
import '../feedback_builder.dart';
|
|
import '../game_card_widget.dart';
|
|
import 'cards_view_controller.dart';
|
|
|
|
class CardsView extends StatefulWidget {
|
|
final CardPackDto cardPack;
|
|
final CardsViewController controller;
|
|
final List<int> cardsOrder = [];
|
|
|
|
CardsView(this.controller, this.cardPack);
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _CardsViewState();
|
|
}
|
|
|
|
class _CardsViewState extends State<CardsView>
|
|
with SingleTickerProviderStateMixin {
|
|
CardPackDto get cardPack => widget.cardPack;
|
|
|
|
CardsViewController get controller => widget.controller;
|
|
var _subscription = CompositeSubscription();
|
|
|
|
late AnimationController _animationController;
|
|
|
|
bool expanded = false;
|
|
bool favorite = false;
|
|
|
|
final wrapKey = GlobalKey();
|
|
|
|
void _onStateChanged(CardsViewState state) async {
|
|
favorite = state.favorite;
|
|
if (state.expanded != expanded) {
|
|
expanded = state.expanded;
|
|
if (expanded) {
|
|
await controller.animateTo(0);
|
|
_animationController.animateTo(
|
|
1.0,
|
|
duration:
|
|
Duration(milliseconds: (100 * state.packCards.length / 3).ceil()),
|
|
);
|
|
} else {
|
|
_animationController.animateBack(
|
|
0.0,
|
|
duration:
|
|
Duration(milliseconds: (100 * state.packCards.length / 3).ceil()),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(CardsView oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
_subscription.cancel();
|
|
_subscription = CompositeSubscription();
|
|
controller.asStream.listen(_onStateChanged).addTo(_subscription);
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
expanded = controller.state.expanded;
|
|
favorite = controller.state.favorite;
|
|
_animationController = AnimationController(vsync: this);
|
|
_subscription.cancel();
|
|
_subscription = CompositeSubscription();
|
|
controller.asStream.listen(_onStateChanged).addTo(_subscription);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_subscription.cancel();
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void onCardTap(List<GameCardDto> cards, GameCardDto card) async {
|
|
final playing = appRouter.push(
|
|
PageRouteInfo(
|
|
CardGamePage.name,
|
|
args: CardGamePageArgs(
|
|
cards: cards,
|
|
color: cardPack.color?.asColor,
|
|
startCardId: card.id,
|
|
infinite: true,
|
|
shuffle: false,
|
|
// id: cardPack.dto.id.toString(),
|
|
),
|
|
),
|
|
);
|
|
final id = await playing;
|
|
// if (id is int) {
|
|
// final index = cardPack.cards.indexWhere((c) => c.id == id);
|
|
// if (index >= 0) {
|
|
// print('jump to $index');
|
|
// controller.jumpTo(index);
|
|
// }
|
|
// }
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final cardWidth = MediaQuery.of(context).size.width / 3;
|
|
final imageWidth = cardWidth * 0.93;
|
|
|
|
return StreamBuilder(
|
|
stream: controller.cardsStream,
|
|
builder: (context, snapshot) {
|
|
if (!snapshot.hasData) {
|
|
return SizedBox();
|
|
}
|
|
final packCards = snapshot.requireData;
|
|
|
|
if (packCards.isEmpty) {
|
|
return _NoCards(
|
|
favorite: favorite,
|
|
height: cardWidth * 4 / 3,
|
|
);
|
|
}
|
|
|
|
controller.imageWidth = imageWidth;
|
|
final animation = _animationController.view;
|
|
return AnimatedBuilder(
|
|
animation: animation,
|
|
builder: (context, child) {
|
|
return Column(
|
|
children: [
|
|
SingleChildScrollView(
|
|
controller: controller.cardsScrollController,
|
|
scrollDirection:
|
|
animation.value > 0 ? Axis.vertical : Axis.horizontal,
|
|
physics: animation.value > 0
|
|
? NeverScrollableScrollPhysics()
|
|
: null,
|
|
child: Container(
|
|
alignment: Alignment.topCenter,
|
|
height: cardWidth * 4 / 3 +
|
|
((packCards.length / 3).ceil() * 4 / 3 * cardWidth -
|
|
cardWidth * 4 / 3) *
|
|
animation.value,
|
|
child: Builder(builder: (context) {
|
|
return ReorderableWrap(
|
|
key: wrapKey,
|
|
onReorder: (int from, int to) {
|
|
controller.onOrderChanged(from, to);
|
|
},
|
|
buildDraggableFeedback: FeedbackBuilder.simple,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: (cardWidth - imageWidth - 1),
|
|
vertical: (cardWidth - imageWidth - 1),
|
|
),
|
|
runSpacing: (cardWidth - imageWidth - 1) * 2,
|
|
spacing: (cardWidth - imageWidth - 1) * 2,
|
|
children: List.generate(packCards.length, (index) {
|
|
final card = packCards[index];
|
|
final memoryImage = card.memoryImage;
|
|
if (memoryImage == null) {
|
|
return SizedBox();
|
|
}
|
|
return Container(
|
|
height: 2 * imageWidth - cardWidth * 2 / 3 + 2,
|
|
width: 2 * imageWidth - cardWidth + 2,
|
|
alignment: Alignment.topCenter,
|
|
child: Container(
|
|
height: imageWidth * 4 / 3,
|
|
child: _CardBuilder(
|
|
cardPack,
|
|
card,
|
|
memoryImage,
|
|
(card) => onCardTap(packCards, card),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
class _CardBuilder extends StatelessWidget {
|
|
final GameCardDto card;
|
|
final CardPackDto cardPack;
|
|
final MemoryImage image;
|
|
final Function(GameCardDto) onTap;
|
|
|
|
_CardBuilder(
|
|
this.cardPack,
|
|
this.card,
|
|
this.image,
|
|
this.onTap,
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
key: ObjectKey(card),
|
|
onTap: () => onTap(card),
|
|
child: Hero(
|
|
key: ValueKey('${card.id}'),
|
|
tag: card.id,
|
|
child: GameCardDecoration(
|
|
alpha: 0.0,
|
|
borderColor: cardPack.color?.asColor,
|
|
child: FrontCard(
|
|
card,
|
|
image,
|
|
alpha: 0,
|
|
),
|
|
),
|
|
createRectTween: (a, b) => RectTween(begin: a, end: b),
|
|
flightShuttleBuilder: (flightC, a, dir, fromC, toC) {
|
|
return AnimatedBuilder(
|
|
animation: a,
|
|
builder: (context, child) => GameCardDecoration(
|
|
alpha: a.value,
|
|
borderColor: cardPack.color?.asColor,
|
|
child: FrontCard(
|
|
card,
|
|
card.memoryImage!,
|
|
alpha: a.value,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _NoCards extends StatelessWidget {
|
|
final bool favorite;
|
|
final double height;
|
|
|
|
_NoCards({
|
|
required this.favorite,
|
|
required this.height,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
height: height,
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
favorite ? 'Нет любимых карточек' : 'Пока нет карточек',
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
}
|
|
}
|