510 lines
15 KiB
Dart
510 lines
15 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:auto_route/auto_route.dart';
|
|
import 'package:flutter/animation.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/main.dart';
|
|
import 'package:mnemo_cards/utils/iterable_helper.dart';
|
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
import 'package:yandex_mobileads/mobile_ads.dart';
|
|
import '../../domain/router/app_router.gr.dart';
|
|
import '../../features/yandex_ads/yandex_ads.dart';
|
|
import '../game_card_widget.dart';
|
|
|
|
import '../../di/locator.dart';
|
|
import '../../theme/themes.dart';
|
|
import 'card_pack_header.dart';
|
|
import 'package:mnemo_cards/utils/string_helper.dart';
|
|
|
|
class AvailablePack extends StatelessWidget {
|
|
final CardPackDto cardPack;
|
|
final _CardsViewController _cardsViewController = _CardsViewController();
|
|
|
|
AvailablePack(this.cardPack);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
CardPackHeader.fromDto(cardPack),
|
|
Expanded(
|
|
child: ListView(
|
|
children: [
|
|
StatefulBuilder(builder: (context, setState) {
|
|
return Column(
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.symmetric(vertical: 0.h),
|
|
decoration: BoxDecoration(
|
|
border: Border(
|
|
bottom: BorderSide(
|
|
color: cardPack.color?.asColor ??
|
|
Colors.grey.withOpacity(0.5)),
|
|
),
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: _CardsView(_cardsViewController, cardPack),
|
|
),
|
|
SizedBox(
|
|
height: 10.h,
|
|
),
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 8.0.h,
|
|
horizontal: 10.w,
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
_Button('view.png', () async {
|
|
_cardsViewController.toggleExpanded();
|
|
}),
|
|
_Button('shuffle.png', () async {
|
|
final index =
|
|
Random().nextInt(cardPack.cards.length);
|
|
await _cardsViewController.animateTo(index);
|
|
AutoRouter.of(context).push(
|
|
PageRouteInfo(
|
|
CardGamePage.name,
|
|
args: CardGamePageArgs(
|
|
cardPack: cardPack,
|
|
startCardId: cardPack.cards[index].id,
|
|
infinite: true,
|
|
// id: cardPack.dto.id.toString(),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
// _Button('chat.png', () {}),
|
|
_Button('heart.png', () {}),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
_AdTile(),
|
|
_TestsSection(cardPack.id.toString()),
|
|
// _TestButton(
|
|
// title: 'Быстрый тест',
|
|
// subtitle: 'Лучший результат: 99 / 100',
|
|
// time: '3',
|
|
// timeSubtitle: 'мин',
|
|
// onTap: () {},
|
|
// ),
|
|
// _TestButton(
|
|
// title: 'Полный тест',
|
|
// subtitle: 'Лучший результат: 99 / 100',
|
|
// time: '10',
|
|
// timeSubtitle: 'мин',
|
|
// onTap: () {},
|
|
// ),
|
|
],
|
|
),
|
|
),
|
|
Hero(
|
|
tag: 'bottom',
|
|
child: SizedBox(
|
|
height: 0,
|
|
width: 300.w,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TestsSection extends StatelessWidget {
|
|
final String packId;
|
|
|
|
const _TestsSection(this.packId);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FutureBuilder(
|
|
future: locator.testManager.loadTests(packId: packId),
|
|
builder: (context, snapshot) {
|
|
return Column(
|
|
children: [
|
|
if (snapshot.hasData)
|
|
...snapshot.data!.map(
|
|
(dto) => _TestButton(
|
|
title: dto.name,
|
|
subtitle: null,
|
|
time: dto.time,
|
|
timeSubtitle: dto.timeSubtitle,
|
|
onTap: () {
|
|
AutoRouter.of(context).push(
|
|
PageRouteInfo(TestPage.name,
|
|
args: TestPageArgs(testId: dto.id!)),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AdTile extends StatelessWidget {
|
|
const _AdTile();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context).textTheme;
|
|
return Container(
|
|
height: 80,
|
|
margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: Colors.white,
|
|
width: 4.0,
|
|
),
|
|
borderRadius: BorderRadius.circular(12.0),
|
|
),
|
|
child: Center(
|
|
child: LayoutBuilder(builder: (context, constraints) {
|
|
return AdWidget(
|
|
bannerAd: YandexAds.createBanner(constraints)..loadAd());
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Button extends StatelessWidget {
|
|
final String asset;
|
|
final VoidCallback onTap;
|
|
|
|
_Button(
|
|
this.asset,
|
|
this.onTap,
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: 110.w,
|
|
height: 60.h,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: borderGray),
|
|
borderRadius: BorderRadius.circular(16.0),
|
|
),
|
|
alignment: Alignment.center,
|
|
child: Image.asset(
|
|
'icons/$asset',
|
|
width: 29.w,
|
|
// height: 29.h,
|
|
fit: BoxFit.scaleDown,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TestButton extends StatelessWidget {
|
|
final VoidCallback onTap;
|
|
final String title;
|
|
final String? subtitle;
|
|
final String? time;
|
|
final String? timeSubtitle;
|
|
|
|
_TestButton({
|
|
required this.title,
|
|
required this.onTap,
|
|
this.subtitle,
|
|
this.time,
|
|
this.timeSubtitle,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 5.0.h,
|
|
horizontal: 10.w,
|
|
),
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
child: Container(
|
|
height: 120.h,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: borderGray),
|
|
borderRadius: BorderRadius.circular(16.0),
|
|
),
|
|
padding: EdgeInsets.only(
|
|
top: 12.0.h,
|
|
left: 10.0.w,
|
|
right: 10.0.w,
|
|
bottom: 20.h,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 25.sp,
|
|
fontWeight: FontWeight.w500,
|
|
height: 0.85,
|
|
),
|
|
),
|
|
if (subtitle != null)
|
|
Text(
|
|
subtitle!,
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w300,
|
|
height: 0.85,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
width: 75.w,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (time != null)
|
|
Text(
|
|
time!,
|
|
style: TextStyle(
|
|
fontSize: 40.sp,
|
|
fontWeight: FontWeight.w400,
|
|
height: 0.85),
|
|
),
|
|
if (timeSubtitle != null)
|
|
Text(
|
|
timeSubtitle!,
|
|
style: TextStyle(
|
|
fontSize: 24.sp,
|
|
fontWeight: FontWeight.w400,
|
|
height: 0.85),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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(
|
|
tag: card.id,
|
|
child: GameCardDecoration(
|
|
alpha: 0.0,
|
|
child: FrontCard(
|
|
card,
|
|
image,
|
|
alpha: 0,
|
|
),
|
|
),
|
|
createRectTween: (a, b) => RectTween(begin: a, end: b),
|
|
flightShuttleBuilder: (flightC, a, dir, fromC, toC) => AnimatedBuilder(
|
|
animation: a,
|
|
builder: (context, child) => GameCardDecoration(
|
|
alpha: a.value,
|
|
child: FrontCard(
|
|
card,
|
|
card.memoryImage!,
|
|
alpha: a.value,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CardsView extends StatefulWidget {
|
|
final CardPackDto cardPack;
|
|
final _CardsViewController controller;
|
|
|
|
_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;
|
|
|
|
late AnimationController _animationController;
|
|
|
|
bool get expanded => controller._expanded;
|
|
|
|
void _onStateChanged() {
|
|
if (expanded) {
|
|
_animationController.animateTo(1.0);
|
|
} else {
|
|
_animationController.animateBack(0.0);
|
|
}
|
|
// setState(() {});
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(_CardsView oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
controller.addListener(_onStateChanged);
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animationController = AnimationController(
|
|
vsync: this,
|
|
duration: Duration(milliseconds: 500),
|
|
reverseDuration: Duration(milliseconds: 500));
|
|
controller.addListener(_onStateChanged);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
controller.removeListener(_onStateChanged);
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void onCardTap(GameCardDto card) async {
|
|
final playing = appRouter.push(
|
|
PageRouteInfo(
|
|
CardGamePage.name,
|
|
args: CardGamePageArgs(
|
|
cardPack: cardPack,
|
|
startCardId: card.id,
|
|
infinite: true,
|
|
// 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;
|
|
final packCards = cardPack.cards;
|
|
controller.imageWidth = imageWidth;
|
|
final animation = _animationController.view;
|
|
return AnimatedBuilder(
|
|
animation: animation,
|
|
builder: (context, child) {
|
|
return 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 * 4 / 9 * cardWidth - cardWidth * 4 / 3) *
|
|
animation.value,
|
|
child: Wrap(
|
|
children: List.generate(packCards.length, (index) {
|
|
final card = packCards[index];
|
|
final memoryImage = card.id.memoryImage;
|
|
if (memoryImage == null) return SizedBox();
|
|
return Container(
|
|
height: cardWidth * 4 / 3,
|
|
width: cardWidth,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: cardWidth - imageWidth - 1,
|
|
vertical: cardWidth - imageWidth - 1,
|
|
),
|
|
alignment: Alignment.topCenter,
|
|
child: Container(
|
|
height: imageWidth * 4 / 3,
|
|
child: _CardBuilder(
|
|
cardPack, card, memoryImage, onCardTap)),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
class _CardsViewController extends ChangeNotifier {
|
|
bool _expanded = false;
|
|
final ScrollController _cardsScrollController =
|
|
ScrollController(keepScrollOffset: true);
|
|
|
|
double imageWidth = 0.0;
|
|
|
|
void setExpanded(bool expanded) {
|
|
if (_expanded != expanded) {
|
|
_expanded = expanded;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
void toggleExpanded() => setExpanded(!_expanded);
|
|
|
|
_CardsViewController();
|
|
|
|
Future<void> animateTo(int index) {
|
|
final millis =
|
|
((_cardsScrollController.offset / imageWidth - index).abs() * 30.0)
|
|
.ceil();
|
|
return _cardsScrollController.animateTo(
|
|
imageWidth * index,
|
|
duration: Duration(milliseconds: millis),
|
|
curve: Curves.easeInOut,
|
|
);
|
|
}
|
|
|
|
void jumpTo(int index) {
|
|
final millis =
|
|
((_cardsScrollController.offset / imageWidth - index).abs() * 30.0)
|
|
.ceil();
|
|
return _cardsScrollController.jumpTo(
|
|
imageWidth * index,
|
|
);
|
|
}
|
|
}
|