draggable
This commit is contained in:
parent
69930bd9df
commit
a6a7ea6c8c
13 changed files with 420 additions and 162 deletions
Binary file not shown.
|
|
@ -198,7 +198,10 @@ class _CardSwiperState<T extends Widget> extends State<CardSwiper>
|
||||||
final _undoableIndex = Undoable<int?>(null);
|
final _undoableIndex = Undoable<int?>(null);
|
||||||
final Queue<CardSwiperDirection> _directionHistory = Queue();
|
final Queue<CardSwiperDirection> _directionHistory = Queue();
|
||||||
|
|
||||||
int? get _currentIndex => _undoableIndex.state;
|
bool usePrevIndex = false;
|
||||||
|
|
||||||
|
int? get _currentIndex =>
|
||||||
|
usePrevIndex ? _undoableIndex.previousState : _undoableIndex.state;
|
||||||
|
|
||||||
int? get _nextIndex => getValidIndexOffset(1);
|
int? get _nextIndex => getValidIndexOffset(1);
|
||||||
|
|
||||||
|
|
@ -319,13 +322,13 @@ class _CardSwiperState<T extends Widget> extends State<CardSwiper>
|
||||||
onPanUpdate: (tapInfo) {
|
onPanUpdate: (tapInfo) {
|
||||||
if (!widget.isDisabled &&
|
if (!widget.isDisabled &&
|
||||||
tapInfo.delta.dx.abs() > 2 * tapInfo.delta.dy.abs()) {
|
tapInfo.delta.dx.abs() > 2 * tapInfo.delta.dy.abs()) {
|
||||||
setState(
|
setState(() {
|
||||||
() => _cardAnimation.update(
|
_cardAnimation.update(
|
||||||
tapInfo.delta.dx,
|
tapInfo.delta.dx,
|
||||||
tapInfo.delta.dy,
|
tapInfo.delta.dy,
|
||||||
_tappedOnTop,
|
_tappedOnTop,
|
||||||
),
|
);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPanEnd: (tapInfo) {
|
onPanEnd: (tapInfo) {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:mnemo_cards/features/packs/preview_packs_poller.dart';
|
|
||||||
import 'package:mnemo_cards/features/tests/test_state_holder.dart';
|
import 'package:mnemo_cards/features/tests/test_state_holder.dart';
|
||||||
import 'package:mnemo_cards/features/tests/test_widgets/test_image.dart';
|
import 'package:mnemo_cards/features/tests/test_widgets/test_image.dart';
|
||||||
import 'package:mnemo_cards/features/packs/pack_cache_manager.dart';
|
import 'package:mnemo_cards/features/packs/pack_cache_manager.dart';
|
||||||
import 'package:mnemo_cards/main.dart';
|
import 'package:mnemo_cards/main.dart';
|
||||||
import 'package:mnemo_cards/theme/themes.dart';
|
import 'package:mnemo_cards/theme/themes.dart';
|
||||||
import 'package:mnemo_cards/utils/iterable_helper.dart';
|
|
||||||
import 'package:mnemo_cards/utils/string_helper.dart';
|
import 'package:mnemo_cards/utils/string_helper.dart';
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
@ -56,7 +55,22 @@ class TestManager extends ChangeNotifier {
|
||||||
(tests) async => pollTests(packId: packId)
|
(tests) async => pollTests(packId: packId)
|
||||||
.catchError((_) => Future.value(<TestDto>[])),
|
.catchError((_) => Future.value(<TestDto>[])),
|
||||||
)
|
)
|
||||||
.where((test) => test.isNotEmpty);
|
.where((test) => test.isNotEmpty)
|
||||||
|
.distinct((p, n) {
|
||||||
|
if (p.length == n.length) {
|
||||||
|
for (int i = 0; i < p.length; i++) {
|
||||||
|
if (p[i].id != n[i].id ||
|
||||||
|
p[i].version != n[i].version ||
|
||||||
|
!ListEquality().equals(
|
||||||
|
p[i].statistics?.words.words,
|
||||||
|
n[i].statistics?.words.words,
|
||||||
|
)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
TestQuestionState? getState(int id) => testHolder.testQuestionState(id);
|
TestQuestionState? getState(int id) => testHolder.testQuestionState(id);
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,10 @@ class _TestProgressWidgetState extends State<TestProgressWidget> {
|
||||||
super.initState();
|
super.initState();
|
||||||
locator.testManager.pageController.removeListener(_updateDataListener);
|
locator.testManager.pageController.removeListener(_updateDataListener);
|
||||||
locator.testManager.pageController.addListener(_updateDataListener);
|
locator.testManager.pageController.addListener(_updateDataListener);
|
||||||
timer = Stream.periodic(
|
// timer = Stream.periodic(
|
||||||
Duration(seconds: 1),
|
// Duration(seconds: 1),
|
||||||
(tick) => Duration(seconds: 300 - tick - 1),
|
// (tick) => Duration(seconds: 300 - tick - 1),
|
||||||
).startWith(Duration(seconds: 300)).take(300);
|
// ).startWith(Duration(seconds: 300)).take(300);
|
||||||
_updateData();
|
_updateData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ extension NullableIterableHelper<T> on Iterable<T?> {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ListHelper<T> on List<T> {
|
extension ListHelper<T> on List<T> {
|
||||||
|
|
||||||
// List<T> shuffle([int? seed]) {
|
// List<T> shuffle([int? seed]) {
|
||||||
// Random r = Random(seed);
|
// Random r = Random(seed);
|
||||||
// var items = [...this];
|
// var items = [...this];
|
||||||
|
|
@ -26,4 +25,37 @@ extension ListHelper<T> on List<T> {
|
||||||
// return items;
|
// return items;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
}
|
void move(int from, int to) {
|
||||||
|
RangeError.checkValidIndex(from, this, "from", length);
|
||||||
|
RangeError.checkValidIndex(to, this, "to", length);
|
||||||
|
var element = this[from];
|
||||||
|
if (from < to) {
|
||||||
|
this.setRange(from, to, this, from + 1);
|
||||||
|
} else {
|
||||||
|
this.setRange(to + 1, from + 1, this, to);
|
||||||
|
}
|
||||||
|
this[to] = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sortInOrder<P>(List<P> order, P valueGetter(T v)) => sort((p, n) {
|
||||||
|
final pVal = valueGetter(p);
|
||||||
|
final nVal = valueGetter(n);
|
||||||
|
final pIndex = order.indexOf(pVal);
|
||||||
|
final nIndex = order.indexOf(nVal);
|
||||||
|
if (pIndex == -1) {
|
||||||
|
return nIndex == -1
|
||||||
|
? () {
|
||||||
|
if (pVal is num) {
|
||||||
|
return pVal.compareTo(nVal as num);
|
||||||
|
} else if (pVal is String) {
|
||||||
|
return pVal.compareTo(nVal as String);
|
||||||
|
}
|
||||||
|
return pVal.toString().compareTo(nVal.toString());
|
||||||
|
}.call()
|
||||||
|
: 1;
|
||||||
|
} else if (nIndex == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return pIndex.compareTo(nIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import '../../features/card_flipper/flip_card_controllers.dart';
|
||||||
import '../../features/card_swiper/src/allowed_swipe_direction.dart';
|
import '../../features/card_swiper/src/allowed_swipe_direction.dart';
|
||||||
import '../../features/card_swiper/src/card_swiper.dart';
|
import '../../features/card_swiper/src/card_swiper.dart';
|
||||||
import '../../features/card_swiper/src/card_swiper_controller.dart';
|
import '../../features/card_swiper/src/card_swiper_controller.dart';
|
||||||
|
import '../../features/card_swiper/src/enums.dart';
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
import '../game_card_widget.dart';
|
import '../game_card_widget.dart';
|
||||||
|
|
||||||
|
|
@ -84,9 +85,7 @@ class CardGameMainWidget extends StatelessWidget {
|
||||||
cardsCount: length,
|
cardsCount: length,
|
||||||
numberOfCardsDisplayed: numberOfCardsDisplayed,
|
numberOfCardsDisplayed: numberOfCardsDisplayed,
|
||||||
allowedSwipeDirection:
|
allowedSwipeDirection:
|
||||||
AllowedSwipeDirection.symmetric(
|
AllowedSwipeDirection.all(),
|
||||||
horizontal: true,
|
|
||||||
),
|
|
||||||
scale: initialScale,
|
scale: initialScale,
|
||||||
backCardOffset: Offset(0, cardHeight * 0.1 / 2),
|
backCardOffset: Offset(0, cardHeight * 0.1 / 2),
|
||||||
isLoop: false,
|
isLoop: false,
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,9 @@ class _CardGamePageState extends State<CardGamePage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// todo move to controller
|
// todo move to controller
|
||||||
final timer = Stream.periodic(Duration(seconds: 1), (tick) {
|
// final timer = Stream.periodic(Duration(seconds: 1), (tick) {
|
||||||
return Duration(seconds: tick + 1);
|
// return Duration(seconds: tick + 1);
|
||||||
}).asBroadcastStream().take(3600).startWith(Duration.zero);
|
// }).asBroadcastStream().take(3600).startWith(Duration.zero);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: PopScope(
|
body: PopScope(
|
||||||
|
|
@ -96,7 +96,6 @@ class _CardGamePageState extends State<CardGamePage> {
|
||||||
),
|
),
|
||||||
shuffledCards.length,
|
shuffledCards.length,
|
||||||
widget.color ?? borderGray,
|
widget.color ?? borderGray,
|
||||||
timer: timer,
|
|
||||||
popText: widget.infinite ? 'к теме' : null,
|
popText: widget.infinite ? 'к теме' : null,
|
||||||
key: ValueKey('CARDS PROGRESS'),
|
key: ValueKey('CARDS PROGRESS'),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,12 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.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/fade_in_widget.dart';
|
||||||
|
import 'package:mnemo_cards/widgets/feedback_builder.dart';
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
import 'package:reorderables/reorderables.dart';
|
||||||
import 'package:yandex_mobileads/mobile_ads.dart';
|
import 'package:yandex_mobileads/mobile_ads.dart';
|
||||||
import '../../domain/router/app_router.gr.dart';
|
import '../../domain/router/app_router.gr.dart';
|
||||||
import '../../features/analytics/analytics.dart';
|
import '../../features/analytics/analytics.dart';
|
||||||
|
|
@ -21,6 +25,7 @@ import 'cards_view.dart';
|
||||||
import 'cards_view_controller.dart';
|
import 'cards_view_controller.dart';
|
||||||
|
|
||||||
final _mainScrollController = ScrollController();
|
final _mainScrollController = ScrollController();
|
||||||
|
final _reorderableTests = GlobalKey();
|
||||||
|
|
||||||
class AvailablePack extends StatelessWidget {
|
class AvailablePack extends StatelessWidget {
|
||||||
final CardPackDto cardPack;
|
final CardPackDto cardPack;
|
||||||
|
|
@ -65,24 +70,20 @@ class AvailablePack extends StatelessWidget {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
StreamBuilder(
|
StreamBuilder<bool>(
|
||||||
stream: _cardsViewController.asStream,
|
stream: _cardsViewController.asStream
|
||||||
|
.map((s) => s.expanded),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return AnimatedRotation(
|
return _DownButton(
|
||||||
duration: Duration(milliseconds: 300),
|
!(snapshot.data ?? false),
|
||||||
turns: (snapshot.data?.expanded ?? false)
|
() async {
|
||||||
? 0.5
|
if (snapshot.hasData) {
|
||||||
: 0.0,
|
_cardsViewController.setExpanded(
|
||||||
child: _Button(
|
!snapshot.requireData,
|
||||||
'down.png',
|
);
|
||||||
() async {
|
}
|
||||||
if (snapshot.hasData) {
|
},
|
||||||
_cardsViewController.setExpanded(
|
key: ValueKey('DownButton'),
|
||||||
!snapshot.requireData.expanded,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
_Button('shuffle.png', () async {
|
_Button('shuffle.png', () async {
|
||||||
|
|
@ -168,16 +169,78 @@ class _TestsSection extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return StreamBuilder(
|
||||||
stream: locator.testManager.loadTestsStream(packId: packId),
|
stream: locator.testManager.loadTestsStream(packId: packId),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return Column(
|
final tests = snapshot.data ?? [];
|
||||||
children: [
|
if (tests.isEmpty) {
|
||||||
if (snapshot.hasData)
|
return SizedBox.shrink();
|
||||||
...snapshot.data!.mapIndexed(
|
}
|
||||||
(int index, TestDto dto) => FadeInWidget(
|
|
||||||
delay: Duration(milliseconds: index * 200),
|
if (order.isEmpty) {
|
||||||
animationDuration: const Duration(milliseconds: 200),
|
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(
|
child: _TestButton(
|
||||||
title: dto.name,
|
title: dto.name,
|
||||||
subtitle: null,
|
subtitle: null,
|
||||||
|
|
@ -192,9 +255,10 @@ class _TestsSection extends StatelessWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
],
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -261,6 +325,40 @@ class _Button extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
class _TestButton extends StatelessWidget {
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final String title;
|
final String title;
|
||||||
|
|
@ -283,99 +381,94 @@ class _TestButton extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return GestureDetector(
|
||||||
padding: EdgeInsets.symmetric(
|
onTap: onTap,
|
||||||
vertical: 5.0.h,
|
child: Container(
|
||||||
horizontal: 10.w,
|
height: 130.h,
|
||||||
),
|
alignment: Alignment.center,
|
||||||
child: InkWell(
|
decoration: BoxDecoration(
|
||||||
onTap: onTap,
|
color: Colors.white,
|
||||||
child: Container(
|
border: Border.all(color: borderGray),
|
||||||
height: 120.h,
|
borderRadius: BorderRadius.circular(16.0),
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: Colors.white,
|
padding: EdgeInsets.only(
|
||||||
border: Border.all(color: borderGray),
|
top: 12.0.h,
|
||||||
borderRadius: BorderRadius.circular(16.0),
|
left: 10.0.w,
|
||||||
),
|
right: 10.0.w,
|
||||||
padding: EdgeInsets.only(
|
bottom: 20.h,
|
||||||
top: 12.0.h,
|
),
|
||||||
left: 10.0.w,
|
child: Row(
|
||||||
right: 10.0.w,
|
children: [
|
||||||
bottom: 20.h,
|
Expanded(
|
||||||
),
|
child: Column(
|
||||||
child: Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Expanded(
|
children: [
|
||||||
child: Column(
|
Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
title,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
style: TextStyle(
|
||||||
children: [
|
fontSize: 25.sp,
|
||||||
Text(
|
fontWeight: FontWeight.w500,
|
||||||
title,
|
height: 0.85,
|
||||||
style: TextStyle(
|
),
|
||||||
fontSize: 25.sp,
|
),
|
||||||
fontWeight: FontWeight.w500,
|
if (subtitle != null)
|
||||||
height: 0.85,
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
subtitle!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16.sp,
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
height: 0.85,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (subtitle != null)
|
Spacer(),
|
||||||
Padding(
|
if (stats != null)
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
Padding(
|
||||||
child: Text(
|
padding: const EdgeInsets.only(right: 50.0),
|
||||||
subtitle!,
|
child: Row(
|
||||||
style: TextStyle(
|
children: [
|
||||||
fontSize: 16.sp,
|
Expanded(
|
||||||
fontWeight: FontWeight.w300,
|
child: HorizontalProgressWidget(
|
||||||
height: 0.85,
|
stats!.words.incorrect.round(),
|
||||||
),
|
stats!.words.total.round(),
|
||||||
),
|
color,
|
||||||
),
|
hasBorder: true,
|
||||||
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,
|
Container(
|
||||||
child: Column(
|
width: 75.w,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Column(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
if (time != null)
|
children: [
|
||||||
Text(
|
if (time != null)
|
||||||
time!,
|
Text(
|
||||||
style: TextStyle(
|
time!,
|
||||||
fontSize: 40.sp,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w400,
|
fontSize: 40.sp,
|
||||||
height: 0.85),
|
fontWeight: FontWeight.w400,
|
||||||
),
|
height: 0.85),
|
||||||
if (timeSubtitle != null)
|
),
|
||||||
Text(
|
if (timeSubtitle != null)
|
||||||
timeSubtitle!,
|
Text(
|
||||||
style: TextStyle(
|
timeSubtitle!,
|
||||||
fontSize: 24.sp,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w400,
|
fontSize: 24.sp,
|
||||||
height: 0.85),
|
fontWeight: FontWeight.w400,
|
||||||
),
|
height: 0.85),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,27 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:mnemo_cards/features/packs/images_holder.dart';
|
import 'package:mnemo_cards/features/packs/images_holder.dart';
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:reorderables/reorderables.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
import '../../di/locator.dart';
|
import '../../di/locator.dart';
|
||||||
import '../../utils/string_helper.dart';
|
import '../../utils/string_helper.dart';
|
||||||
import '../../domain/router/app_router.gr.dart';
|
import '../../domain/router/app_router.gr.dart';
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
|
import '../feedback_builder.dart';
|
||||||
import '../game_card_widget.dart';
|
import '../game_card_widget.dart';
|
||||||
import 'cards_view_controller.dart';
|
import 'cards_view_controller.dart';
|
||||||
|
|
||||||
class CardsView extends StatefulWidget {
|
class CardsView extends StatefulWidget {
|
||||||
final CardPackDto cardPack;
|
final CardPackDto cardPack;
|
||||||
final CardsViewController controller;
|
final CardsViewController controller;
|
||||||
|
final List<int> cardsOrder = [];
|
||||||
|
|
||||||
CardsView(this.controller, this.cardPack);
|
CardsView(this.controller, this.cardPack);
|
||||||
|
|
||||||
|
|
@ -150,34 +154,42 @@ class _CardsViewState extends State<CardsView>
|
||||||
((packCards.length / 3).ceil() * 4 / 3 * cardWidth -
|
((packCards.length / 3).ceil() * 4 / 3 * cardWidth -
|
||||||
cardWidth * 4 / 3) *
|
cardWidth * 4 / 3) *
|
||||||
animation.value,
|
animation.value,
|
||||||
child: Wrap(
|
child: Builder(builder: (context) {
|
||||||
key: wrapKey,
|
return ReorderableWrap(
|
||||||
children: List.generate(packCards.length, (index) {
|
key: wrapKey,
|
||||||
final card = packCards[index];
|
onReorder: (int from, int to) {
|
||||||
final memoryImage = card.memoryImage;
|
controller.onOrderChanged(from, to);
|
||||||
if (memoryImage == null) {
|
},
|
||||||
return SizedBox();
|
buildDraggableFeedback: FeedbackBuilder.simple,
|
||||||
}
|
padding: EdgeInsets.symmetric(
|
||||||
return Container(
|
horizontal: (cardWidth - imageWidth - 1),
|
||||||
height: cardWidth * 4 / 3,
|
vertical: (cardWidth - imageWidth - 1),
|
||||||
width: cardWidth,
|
),
|
||||||
padding: EdgeInsets.symmetric(
|
runSpacing: (cardWidth - imageWidth - 1) * 2,
|
||||||
horizontal: cardWidth - imageWidth - 1,
|
spacing: (cardWidth - imageWidth - 1) * 2,
|
||||||
vertical: cardWidth - imageWidth - 1,
|
children: List.generate(packCards.length, (index) {
|
||||||
),
|
final card = packCards[index];
|
||||||
alignment: Alignment.topCenter,
|
final memoryImage = card.memoryImage;
|
||||||
child: Container(
|
if (memoryImage == null) {
|
||||||
height: imageWidth * 4 / 3,
|
return SizedBox();
|
||||||
child: _CardBuilder(
|
}
|
||||||
cardPack,
|
return Container(
|
||||||
card,
|
height: 2 * imageWidth - cardWidth * 2 / 3 + 2,
|
||||||
memoryImage,
|
width: 2 * imageWidth - cardWidth + 2,
|
||||||
(card) => onCardTap(packCards, card),
|
alignment: Alignment.topCenter,
|
||||||
|
child: Container(
|
||||||
|
height: imageWidth * 4 / 3,
|
||||||
|
child: _CardBuilder(
|
||||||
|
cardPack,
|
||||||
|
card,
|
||||||
|
memoryImage,
|
||||||
|
(card) => onCardTap(packCards, card),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}),
|
||||||
}),
|
);
|
||||||
),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:mnemo_cards/features/packs/images_holder.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:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
|
|
@ -13,6 +15,10 @@ class CardsViewController extends ChangeNotifier {
|
||||||
bool _favorite = false;
|
bool _favorite = false;
|
||||||
double imageWidth = 0.0;
|
double imageWidth = 0.0;
|
||||||
List<GameCardDto> cards = [];
|
List<GameCardDto> cards = [];
|
||||||
|
List<int> allCardsOrder = [];
|
||||||
|
List<int> favoriteCardsOrder = [];
|
||||||
|
|
||||||
|
List<int> get cardsOrder => _favorite ? favoriteCardsOrder : allCardsOrder;
|
||||||
|
|
||||||
final CardPackDto cardPack;
|
final CardPackDto cardPack;
|
||||||
|
|
||||||
|
|
@ -57,6 +63,61 @@ class CardsViewController extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _saveCardsOrder() async {
|
||||||
|
if (_favorite) {
|
||||||
|
await globalSharedPreferences.setString(
|
||||||
|
'sort_fav_${cardPack.id}',
|
||||||
|
favoriteCardsOrder.encode(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await globalSharedPreferences.setString(
|
||||||
|
'sort_${cardPack.id}',
|
||||||
|
allCardsOrder.encode(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadCardsOrder() {
|
||||||
|
favoriteCardsOrder = globalSharedPreferences
|
||||||
|
.getString(
|
||||||
|
'sort_fav_${cardPack.id}',
|
||||||
|
)
|
||||||
|
?.decodeList<int>() ??
|
||||||
|
[];
|
||||||
|
allCardsOrder = globalSharedPreferences
|
||||||
|
.getString(
|
||||||
|
'sort_${cardPack.id}',
|
||||||
|
)
|
||||||
|
?.decodeList<int>() ??
|
||||||
|
[];
|
||||||
|
if (allCardsOrder.isEmpty) {
|
||||||
|
allCardsOrder = cardPack.cards.map((c) => c.id).toList();
|
||||||
|
}
|
||||||
|
if (favoriteCardsOrder.isEmpty) {
|
||||||
|
favoriteCardsOrder = allCardsOrder.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCardsOrder(List<int> order) {
|
||||||
|
if (_favorite) {
|
||||||
|
favoriteCardsOrder = order.toList();
|
||||||
|
} else {
|
||||||
|
allCardsOrder = order.toList();
|
||||||
|
}
|
||||||
|
_saveCardsOrder();
|
||||||
|
_updateCards();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onOrderChanged(int from, int to) {
|
||||||
|
if (_favorite) {
|
||||||
|
favoriteCardsOrder.move(from, to);
|
||||||
|
} else {
|
||||||
|
allCardsOrder.move(from, to);
|
||||||
|
}
|
||||||
|
_saveCardsOrder();
|
||||||
|
_updateCards();
|
||||||
|
}
|
||||||
|
|
||||||
void setFavorite(bool favorite) {
|
void setFavorite(bool favorite) {
|
||||||
_favorite = favorite;
|
_favorite = favorite;
|
||||||
_updateCards();
|
_updateCards();
|
||||||
|
|
@ -75,11 +136,23 @@ class CardsViewController extends ChangeNotifier {
|
||||||
|
|
||||||
CardsViewController(this.cardPack) {
|
CardsViewController(this.cardPack) {
|
||||||
addListener(_addToStream);
|
addListener(_addToStream);
|
||||||
|
_loadCardsOrder();
|
||||||
_updateCards();
|
_updateCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateCards() {
|
void _updateCards() {
|
||||||
cards = _filterPackCards();
|
cards = _filterPackCards();
|
||||||
|
cards
|
||||||
|
..sort((p, n) {
|
||||||
|
final pIndex = cardsOrder.indexOf(p.id);
|
||||||
|
final nIndex = cardsOrder.indexOf(n.id);
|
||||||
|
if (pIndex == -1) {
|
||||||
|
return nIndex == -1 ? p.id.compareTo(n.id) : 1;
|
||||||
|
} else if (nIndex == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return pIndex.compareTo(nIndex);
|
||||||
|
});
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ class FadeInWidget extends StatelessWidget {
|
||||||
this.delay = const Duration(milliseconds: 200),
|
this.delay = const Duration(milliseconds: 200),
|
||||||
this.future,
|
this.future,
|
||||||
this.animationDuration = const Duration(milliseconds: 200),
|
this.animationDuration = const Duration(milliseconds: 200),
|
||||||
|
super.key,
|
||||||
}) : assert(future != null || delay != null);
|
}) : assert(future != null || delay != null);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
31
lib/widgets/feedback_builder.dart
Normal file
31
lib/widgets/feedback_builder.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FeedbackBuilder {
|
||||||
|
static Widget tests(Widget child, int index, Animation<double> animation) =>
|
||||||
|
Transform(
|
||||||
|
transform: new Matrix4.rotationZ(0),
|
||||||
|
child: Material(
|
||||||
|
child: child,
|
||||||
|
elevation: 1.0,
|
||||||
|
color: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(16.0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
static Widget simple(
|
||||||
|
BuildContext context,
|
||||||
|
BoxConstraints constraints,
|
||||||
|
Widget child,
|
||||||
|
) {
|
||||||
|
return Transform(
|
||||||
|
transform: new Matrix4.rotationZ(0.05),
|
||||||
|
// alignment: FractionalOffset.topLeft,
|
||||||
|
child: Material(
|
||||||
|
child: child,
|
||||||
|
elevation: 6.0,
|
||||||
|
color: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -168,6 +168,7 @@ class _PackButton4 extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: EdgeInsets.all(4.0.h),
|
margin: EdgeInsets.all(4.0.h),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue