476 lines
15 KiB
Dart
476 lines
15 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:collection/collection.dart';
|
|
import 'package:auto_route/auto_route.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:mnemo_cards/main.dart';
|
|
import 'package:mnemo_cards/utils/iterable_helper.dart';
|
|
import 'package:mnemo_cards/widgets/fade_in_widget.dart';
|
|
import 'package:mnemo_cards/widgets/feedback_builder.dart';
|
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
|
import 'package:reorderables/reorderables.dart';
|
|
import 'package:yandex_mobileads/mobile_ads.dart';
|
|
import '../../domain/router/app_router.gr.dart';
|
|
import '../../features/analytics/analytics.dart';
|
|
import '../../features/yandex_ads/yandex_ads.dart';
|
|
|
|
import '../../di/locator.dart';
|
|
import '../../theme/themes.dart';
|
|
import '../horizontal_progress.dart';
|
|
import 'card_pack_header.dart';
|
|
import 'package:mnemo_cards/utils/string_helper.dart';
|
|
|
|
import 'cards_view.dart';
|
|
import 'cards_view_controller.dart';
|
|
|
|
final _mainScrollController = ScrollController();
|
|
final _reorderableTests = GlobalKey();
|
|
|
|
class AvailablePack extends StatelessWidget {
|
|
final CardPackDto cardPack;
|
|
late final CardsViewController _cardsViewController =
|
|
CardsViewController(cardPack);
|
|
|
|
AvailablePack(this.cardPack);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Analytics.availablePackOpened(cardPack);
|
|
return Column(
|
|
children: [
|
|
CardPackHeader.fromDto(cardPack),
|
|
Expanded(
|
|
child: ListView(
|
|
controller: _mainScrollController,
|
|
children: [
|
|
StatefulBuilder(builder: (context, setState) {
|
|
return Column(
|
|
children: [
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: cardPack.color?.asColor?.withOpacity(0.1),
|
|
),
|
|
alignment: Alignment.center,
|
|
clipBehavior: Clip.antiAlias,
|
|
child: CardsView(_cardsViewController, cardPack),
|
|
),
|
|
Container(
|
|
height: 1,
|
|
color: cardPack.color?.asColor ??
|
|
Colors.grey.withOpacity(0.5),
|
|
),
|
|
SizedBox(
|
|
height: 10.h,
|
|
),
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 10.w,
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
StreamBuilder<bool>(
|
|
stream: _cardsViewController.asStream
|
|
.map((s) => s.expanded),
|
|
builder: (context, snapshot) {
|
|
return _DownButton(
|
|
!(snapshot.data ?? false),
|
|
() async {
|
|
if (snapshot.hasData) {
|
|
_cardsViewController.setExpanded(
|
|
!snapshot.requireData,
|
|
);
|
|
}
|
|
},
|
|
key: ValueKey('DownButton'),
|
|
);
|
|
}),
|
|
_Button('shuffle.png', () async {
|
|
final cards = _cardsViewController.state.packCards;
|
|
if (cards.isEmpty) return;
|
|
final index = (_cardsViewController.offsetCards +
|
|
Random().nextInt(
|
|
(cards.length / 2).ceil(),
|
|
) +
|
|
3) %
|
|
cards.length;
|
|
await _cardsViewController.animateTo(index);
|
|
AutoRouter.of(context).push(
|
|
PageRouteInfo(
|
|
CardGamePage.name,
|
|
args: CardGamePageArgs(
|
|
cards: cards,
|
|
color: cardPack.color?.asColor,
|
|
startCardId: cards[index].id,
|
|
infinite: true,
|
|
shuffle: true,
|
|
// id: cardPack.dto.id.toString(),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
// _Button('chat.png', () {}),
|
|
StreamBuilder(
|
|
stream: _cardsViewController.asStream,
|
|
builder: (context, snapshot) {
|
|
return _Button(
|
|
(snapshot.data?.favorite ?? false)
|
|
? 'heart_fill.png'
|
|
: 'heart.png', () {
|
|
if (snapshot.hasData) {
|
|
_cardsViewController.setFavorite(
|
|
!snapshot.requireData.favorite,
|
|
);
|
|
}
|
|
});
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
_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) {
|
|
final order = (globalSharedPreferences
|
|
.getString('tests_order_${packId}')
|
|
?.decodeList<String>() ??
|
|
<String>[])
|
|
.toList();
|
|
|
|
void saveOrder() {
|
|
globalSharedPreferences.setString(
|
|
'tests_order_${packId}',
|
|
order.encode(),
|
|
);
|
|
}
|
|
|
|
return StreamBuilder(
|
|
stream: locator.testManager.loadTestsStream(packId: packId),
|
|
builder: (context, snapshot) {
|
|
final tests = snapshot.data ?? [];
|
|
if (tests.isEmpty) {
|
|
return SizedBox.shrink();
|
|
}
|
|
|
|
if (order.isEmpty) {
|
|
order.addAll(tests.map((t) => t.name));
|
|
saveOrder();
|
|
}
|
|
return FadeInWidget(
|
|
animationDuration: Duration(milliseconds: 400),
|
|
child: StatefulBuilder(builder: (context, setState) {
|
|
tests..sortInOrder(order, (t) => t.name);
|
|
return ReorderableListView.builder(
|
|
key: _reorderableTests,
|
|
itemExtent: 130.h + 2 * 5.h,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 10.w,
|
|
vertical: 5.h,
|
|
),
|
|
shrinkWrap: true,
|
|
scrollController: _mainScrollController,
|
|
// physics: NeverScrollableScrollPhysics(),
|
|
proxyDecorator: (_, index, animation) {
|
|
final dto = tests[index];
|
|
return Material(
|
|
elevation: 5,
|
|
borderRadius: BorderRadius.circular(16),
|
|
color: Colors.transparent,
|
|
child: _TestButton(
|
|
title: dto.name,
|
|
subtitle: null,
|
|
stats: dto.statistics,
|
|
color: dto.color?.asColor ?? borderGray,
|
|
time: dto.time,
|
|
timeSubtitle: dto.timeSubtitle,
|
|
onTap: () {},
|
|
),
|
|
);
|
|
},
|
|
onReorder: (int from, int to) {
|
|
if (to - from > 1) {
|
|
to--;
|
|
}
|
|
order.move(from, to);
|
|
saveOrder();
|
|
setState(() {});
|
|
},
|
|
itemCount: tests.length,
|
|
itemBuilder: (context, index) {
|
|
final dto = tests[index];
|
|
return Padding(
|
|
key: ValueKey(dto.id),
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 5.0.h,
|
|
),
|
|
child: _TestButton(
|
|
title: dto.name,
|
|
subtitle: null,
|
|
stats: dto.statistics,
|
|
color: dto.color?.asColor ?? borderGray,
|
|
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) {
|
|
return LayoutBuilder(builder: (context, constraints) {
|
|
final banner = YandexAds.createBanner(
|
|
BoxConstraints.tight(Size(constraints.maxWidth - 16, 80 - 8)));
|
|
return FadeInWidget(
|
|
future: banner.loadAd(),
|
|
animationDuration: Duration(milliseconds: 1000),
|
|
child: 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: AdWidget(bannerAd: banner),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
class _Button extends StatelessWidget {
|
|
final String asset;
|
|
final VoidCallback onTap;
|
|
|
|
_Button(this.asset, this.onTap, {super.key});
|
|
|
|
@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 _DownButton extends StatelessWidget {
|
|
final VoidCallback onTap;
|
|
final bool isDown;
|
|
|
|
_DownButton(bool this.isDown, this.onTap, {super.key});
|
|
|
|
@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: AnimatedRotation(
|
|
duration: Duration(milliseconds: 300),
|
|
turns: isDown ? 0.0 : 0.5,
|
|
child: Image.asset(
|
|
'icons/down.png',
|
|
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;
|
|
final Color color;
|
|
final TestStatisticsDto? stats;
|
|
|
|
_TestButton({
|
|
required this.title,
|
|
required this.onTap,
|
|
required this.color,
|
|
this.subtitle,
|
|
this.time,
|
|
this.timeSubtitle,
|
|
this.stats,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
height: 130.h,
|
|
alignment: Alignment.center,
|
|
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)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child: Text(
|
|
subtitle!,
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w300,
|
|
height: 0.85,
|
|
),
|
|
),
|
|
),
|
|
Spacer(),
|
|
if (stats != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 50.0),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: HorizontalProgressWidget(
|
|
stats!.words.incorrect.round(),
|
|
stats!.words.total.round(),
|
|
color,
|
|
hasBorder: true,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
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),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|