From c4c42e4af50c109c5e6069a76f7d398fcb9d330c Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sun, 9 Jun 2024 02:34:38 +0300 Subject: [PATCH] fixes --- lib/domain/router/app_router.dart | 17 +- lib/domain/router/app_router.gr.dart | 140 +++++++--------- lib/features/analytics/analytics.dart | 27 ++-- lib/features/packs/images_holder.dart | 5 +- lib/features/packs/pack_cache_manager.dart | 49 ++++-- lib/features/packs/pack_manager.dart | 7 - lib/features/packs/pack_updater.dart | 14 +- lib/features/tests/progress_widget.dart | 4 +- .../input_buttons_test_state.dart | 4 +- .../input_buttons_test_state.g.dart | 13 ++ .../question_states/simple_test_state.dart | 2 + .../question_states/simple_test_state.g.dart | 13 ++ .../question_states/test_question_state.dart | 5 +- .../test_question_state.g.dart | 23 +++ lib/features/tests/scroll_physics.dart | 149 ++++++++++++++++++ lib/features/tests/test_manager.dart | 5 + lib/features/tests/test_page.dart | 44 ++++-- lib/features/tests/test_progress_widget.dart | 6 +- lib/features/tests/test_state_holder.dart | 25 +++ .../test_widgets/input_buttons_test.dart | 9 +- lib/managers/user_manager.dart | 29 ++-- lib/pages/explore_page.dart | 48 ------ lib/widgets/card_pack/available_pack.dart | 3 - lib/widgets/card_pack/card_pack_page.dart | 12 +- lib/widgets/game_card_widget.dart | 9 +- pubspec.lock | 8 + pubspec.yaml | 1 + 27 files changed, 435 insertions(+), 236 deletions(-) create mode 100644 lib/features/tests/scroll_physics.dart delete mode 100644 lib/pages/explore_page.dart diff --git a/lib/domain/router/app_router.dart b/lib/domain/router/app_router.dart index 21ce789..aa71ba2 100644 --- a/lib/domain/router/app_router.dart +++ b/lib/domain/router/app_router.dart @@ -25,11 +25,11 @@ class AppRouter extends $AppRouter { transitionsBuilder: TransitionsBuilders.noTransition, durationInMilliseconds: 200, ), - CustomRoute( - page: ExplorePage.page, - transitionsBuilder: TransitionsBuilders.noTransition, - durationInMilliseconds: 200, - ), + // CustomRoute( + // page: ExplorePage.page, + // transitionsBuilder: TransitionsBuilders.noTransition, + // durationInMilliseconds: 200, + // ), // CustomRoute( // page: ProfilePage.page, // transitionsBuilder: TransitionsBuilders.noTransition, @@ -71,6 +71,13 @@ class AppRouter extends $AppRouter { } } + static void closeAuthPage() { + final router = main.appRouter; + if (router.isRouteActive(AuthPage.name)) { + router.replace(PageRouteInfo(HomePage.name)); + } + } + static void openAuthOrProfile() { if (locator.userManager.hasUser) { openProfile(); diff --git a/lib/domain/router/app_router.gr.dart b/lib/domain/router/app_router.gr.dart index b991536..627a8f6 100644 --- a/lib/domain/router/app_router.gr.dart +++ b/lib/domain/router/app_router.gr.dart @@ -8,35 +8,33 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:ui' as _i11; +import 'dart:ui' as _i10; -import 'package:auto_route/auto_route.dart' as _i9; -import 'package:flutter/cupertino.dart' as _i13; -import 'package:flutter/material.dart' as _i12; -import 'package:mnemo_cards/features/tests/test_page.dart' as _i8; -import 'package:mnemo_cards/main.dart' as _i6; +import 'package:auto_route/auto_route.dart' as _i8; +import 'package:flutter/material.dart' as _i11; +import 'package:mnemo_cards/features/tests/test_page.dart' as _i7; +import 'package:mnemo_cards/main.dart' as _i5; import 'package:mnemo_cards/pages/auth_page.dart' as _i1; -import 'package:mnemo_cards/pages/explore_page.dart' as _i4; -import 'package:mnemo_cards/pages/home_page.dart' as _i5; -import 'package:mnemo_cards/pages/profile_page.dart' as _i7; +import 'package:mnemo_cards/pages/home_page.dart' as _i4; +import 'package:mnemo_cards/pages/profile_page.dart' as _i6; import 'package:mnemo_cards/widgets/card_game/card_game_page.dart' as _i2; import 'package:mnemo_cards/widgets/card_pack/card_pack_page.dart' as _i3; -import 'package:mnemo_cards_common/mnemo_cards_common.dart' as _i10; +import 'package:mnemo_cards_common/mnemo_cards_common.dart' as _i9; -abstract class $AppRouter extends _i9.RootStackRouter { +abstract class $AppRouter extends _i8.RootStackRouter { $AppRouter({super.navigatorKey}); @override - final Map pagesMap = { + final Map pagesMap = { AuthPage.name: (routeData) { - return _i9.AutoRoutePage( + return _i8.AutoRoutePage( routeData: routeData, child: _i1.AuthPage(), ); }, CardGamePage.name: (routeData) { final args = routeData.argsAs(); - return _i9.AutoRoutePage( + return _i8.AutoRoutePage( routeData: routeData, child: _i2.CardGamePage( cards: args.cards, @@ -50,7 +48,7 @@ abstract class $AppRouter extends _i9.RootStackRouter { }, CardPackPage.name: (routeData) { final args = routeData.argsAs(); - return _i9.AutoRoutePage( + return _i8.AutoRoutePage( routeData: routeData, child: _i3.CardPackPage( args.previewDto, @@ -58,35 +56,29 @@ abstract class $AppRouter extends _i9.RootStackRouter { ), ); }, - ExplorePage.name: (routeData) { - return _i9.AutoRoutePage( - routeData: routeData, - child: _i4.ExplorePage(), - ); - }, HomePage.name: (routeData) { - return _i9.AutoRoutePage( + return _i8.AutoRoutePage( routeData: routeData, - child: _i5.HomePage(), + child: _i4.HomePage(), ); }, MainTabsPage.name: (routeData) { - return _i9.AutoRoutePage( + return _i8.AutoRoutePage( routeData: routeData, - child: const _i6.MainTabsPage(), + child: const _i5.MainTabsPage(), ); }, ProfilePage.name: (routeData) { - return _i9.AutoRoutePage( + return _i8.AutoRoutePage( routeData: routeData, - child: _i7.ProfilePage(), + child: _i6.ProfilePage(), ); }, TestPage.name: (routeData) { final args = routeData.argsAs(); - return _i9.AutoRoutePage( + return _i8.AutoRoutePage( routeData: routeData, - child: _i8.TestPage(args.testId), + child: _i7.TestPage(args.testId), ); }, }; @@ -94,8 +86,8 @@ abstract class $AppRouter extends _i9.RootStackRouter { /// generated route for /// [_i1.AuthPage] -class AuthPage extends _i9.PageRouteInfo { - const AuthPage({List<_i9.PageRouteInfo>? children}) +class AuthPage extends _i8.PageRouteInfo { + const AuthPage({List<_i8.PageRouteInfo>? children}) : super( AuthPage.name, initialChildren: children, @@ -103,20 +95,20 @@ class AuthPage extends _i9.PageRouteInfo { static const String name = 'AuthPage'; - static const _i9.PageInfo page = _i9.PageInfo(name); + static const _i8.PageInfo page = _i8.PageInfo(name); } /// generated route for /// [_i2.CardGamePage] -class CardGamePage extends _i9.PageRouteInfo { +class CardGamePage extends _i8.PageRouteInfo { CardGamePage({ - required List<_i10.GameCardDto> cards, - required _i11.Color? color, + required List<_i9.GameCardDto> cards, + required _i10.Color? color, int? startCardId, bool infinite = false, bool shuffle = true, - _i12.Key? key, - List<_i9.PageRouteInfo>? children, + _i11.Key? key, + List<_i8.PageRouteInfo>? children, }) : super( CardGamePage.name, args: CardGamePageArgs( @@ -132,8 +124,8 @@ class CardGamePage extends _i9.PageRouteInfo { static const String name = 'CardGamePage'; - static const _i9.PageInfo page = - _i9.PageInfo(name); + static const _i8.PageInfo page = + _i8.PageInfo(name); } class CardGamePageArgs { @@ -146,9 +138,9 @@ class CardGamePageArgs { this.key, }); - final List<_i10.GameCardDto> cards; + final List<_i9.GameCardDto> cards; - final _i11.Color? color; + final _i10.Color? color; final int? startCardId; @@ -156,7 +148,7 @@ class CardGamePageArgs { final bool shuffle; - final _i12.Key? key; + final _i11.Key? key; @override String toString() { @@ -166,11 +158,11 @@ class CardGamePageArgs { /// generated route for /// [_i3.CardPackPage] -class CardPackPage extends _i9.PageRouteInfo { +class CardPackPage extends _i8.PageRouteInfo { CardPackPage({ - required _i10.CardPackPreviewDto previewDto, - _i13.Key? key, - List<_i9.PageRouteInfo>? children, + required _i9.CardPackPreviewDto previewDto, + _i11.Key? key, + List<_i8.PageRouteInfo>? children, }) : super( CardPackPage.name, args: CardPackPageArgs( @@ -182,8 +174,8 @@ class CardPackPage extends _i9.PageRouteInfo { static const String name = 'CardPackPage'; - static const _i9.PageInfo page = - _i9.PageInfo(name); + static const _i8.PageInfo page = + _i8.PageInfo(name); } class CardPackPageArgs { @@ -192,9 +184,9 @@ class CardPackPageArgs { this.key, }); - final _i10.CardPackPreviewDto previewDto; + final _i9.CardPackPreviewDto previewDto; - final _i13.Key? key; + final _i11.Key? key; @override String toString() { @@ -203,23 +195,9 @@ class CardPackPageArgs { } /// generated route for -/// [_i4.ExplorePage] -class ExplorePage extends _i9.PageRouteInfo { - const ExplorePage({List<_i9.PageRouteInfo>? children}) - : super( - ExplorePage.name, - initialChildren: children, - ); - - static const String name = 'ExplorePage'; - - static const _i9.PageInfo page = _i9.PageInfo(name); -} - -/// generated route for -/// [_i5.HomePage] -class HomePage extends _i9.PageRouteInfo { - const HomePage({List<_i9.PageRouteInfo>? children}) +/// [_i4.HomePage] +class HomePage extends _i8.PageRouteInfo { + const HomePage({List<_i8.PageRouteInfo>? children}) : super( HomePage.name, initialChildren: children, @@ -227,13 +205,13 @@ class HomePage extends _i9.PageRouteInfo { static const String name = 'HomePage'; - static const _i9.PageInfo page = _i9.PageInfo(name); + static const _i8.PageInfo page = _i8.PageInfo(name); } /// generated route for -/// [_i6.MainTabsPage] -class MainTabsPage extends _i9.PageRouteInfo { - const MainTabsPage({List<_i9.PageRouteInfo>? children}) +/// [_i5.MainTabsPage] +class MainTabsPage extends _i8.PageRouteInfo { + const MainTabsPage({List<_i8.PageRouteInfo>? children}) : super( MainTabsPage.name, initialChildren: children, @@ -241,13 +219,13 @@ class MainTabsPage extends _i9.PageRouteInfo { static const String name = 'MainTabsPage'; - static const _i9.PageInfo page = _i9.PageInfo(name); + static const _i8.PageInfo page = _i8.PageInfo(name); } /// generated route for -/// [_i7.ProfilePage] -class ProfilePage extends _i9.PageRouteInfo { - const ProfilePage({List<_i9.PageRouteInfo>? children}) +/// [_i6.ProfilePage] +class ProfilePage extends _i8.PageRouteInfo { + const ProfilePage({List<_i8.PageRouteInfo>? children}) : super( ProfilePage.name, initialChildren: children, @@ -255,15 +233,15 @@ class ProfilePage extends _i9.PageRouteInfo { static const String name = 'ProfilePage'; - static const _i9.PageInfo page = _i9.PageInfo(name); + static const _i8.PageInfo page = _i8.PageInfo(name); } /// generated route for -/// [_i8.TestPage] -class TestPage extends _i9.PageRouteInfo { +/// [_i7.TestPage] +class TestPage extends _i8.PageRouteInfo { TestPage({ required String testId, - List<_i9.PageRouteInfo>? children, + List<_i8.PageRouteInfo>? children, }) : super( TestPage.name, args: TestPageArgs(testId: testId), @@ -272,8 +250,8 @@ class TestPage extends _i9.PageRouteInfo { static const String name = 'TestPage'; - static const _i9.PageInfo page = - _i9.PageInfo(name); + static const _i8.PageInfo page = + _i8.PageInfo(name); } class TestPageArgs { diff --git a/lib/features/analytics/analytics.dart b/lib/features/analytics/analytics.dart index 9eac5f6..4853136 100644 --- a/lib/features/analytics/analytics.dart +++ b/lib/features/analytics/analytics.dart @@ -1,6 +1,7 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:mnemo_cards/managers/user_manager.dart'; +import 'package:mnemo_cards/utils/iterable_helper.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; class Analytics { @@ -148,22 +149,18 @@ class Analytics { }.asFirebaseMap); } - static crashlyticsError(Object? e, StackTrace? s) => - _crashlytics.recordError(e, s); - - static error({Map? data, String? message}) { - try { - _analytics.logEvent( - name: 'error', - parameters: { - 'msg': message, - if (data != null) ...data, - }.asFirebaseMap, + static crashlyticsError({ + Object? e, + StackTrace? s, + String? message, + Map? data, + }) => + _crashlytics.recordError( + e, + s, + reason: message, + information: data?.asFirebaseMap.entries.toList() ?? [], ); - } catch (e, s) { - _crashlytics.recordError(e, s); - } - } } extension on Map { diff --git a/lib/features/packs/images_holder.dart b/lib/features/packs/images_holder.dart index 93f6ada..287813f 100644 --- a/lib/features/packs/images_holder.dart +++ b/lib/features/packs/images_holder.dart @@ -7,12 +7,14 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart'; class ImagesHolder { Map _images = {}; Map _packImages = {}; + String? packId; DateTime? unlockTime; ImagesHolder(); - void setPackImages(Map images) { + void setPackImages(Map images, String packId) { _packImages = Map.from(images); + this.packId = packId; } void setImage(String key, MemoryImage image) { @@ -22,6 +24,7 @@ class ImagesHolder { void clear() { _images.clear(); _packImages.clear(); + packId = null; } MemoryImage? get(String id) { diff --git a/lib/features/packs/pack_cache_manager.dart b/lib/features/packs/pack_cache_manager.dart index af85a22..d99f1f6 100644 --- a/lib/features/packs/pack_cache_manager.dart +++ b/lib/features/packs/pack_cache_manager.dart @@ -10,6 +10,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../di/locator.dart'; +import '../analytics/analytics.dart'; // persist class PackCacheManager { @@ -21,6 +22,24 @@ class PackCacheManager { _sharedPreferences = await SharedPreferences.getInstance(); } + Future markPackForUpdate(String packId) async { + try { + final dir = await _packDirectory(packId); + final file = File('${dir.path}/pack.json'); + if (file.existsSync()) { + final json = jsonEncode(await file.readAsString()) as Map; + json['id'] = '0'; + await file.writeAsString(jsonEncode(json)); + } + } catch (e, s) { + Analytics.crashlyticsError( + e: e, + s: s, + message: 'Cant mark pack for update', + ); + } + } + Future loadPackFromCache( String packId, { bool withCardImages = true, @@ -41,23 +60,25 @@ class PackCacheManager { log('Cards ${packId}: ${pack.cards.length}'); if (pack.cards.isNotEmpty) { log('Loading cards ${packId}'); - for (final dto in pack.cards) { - final file = File('${packDir.path}/images/${dto.id}'); - if (file.existsSync()) { - try { - images[dto.id.toString()] = - MemoryImage(await file.readAsBytes()); - } catch (e) { - throw Exception('Cant load pack $packId $e'); + await Future.wait( + pack.cards.map((dto) async { + final file = File('${packDir.path}/images/${dto.id}'); + if (await file.exists()) { + try { + images[dto.id.toString()] = + MemoryImage(await file.readAsBytes()); + } catch (e) { + markPackForUpdate(packId); + throw Exception('Cant read file $packId $e'); + } + } else { + log('Cant load card ${dto.id} from $packId, skiping'); } - } else { - log('Cant load card ${dto.id} from $packId, skiping'); - continue; - } - } + }), + ); } log('Pack loaded ${packId}'); - locator.imagesHolder.setPackImages(images); + locator.imagesHolder.setPackImages(images, packId); } return pack; } catch (e, s) { diff --git a/lib/features/packs/pack_manager.dart b/lib/features/packs/pack_manager.dart index 76c4b9b..591a844 100644 --- a/lib/features/packs/pack_manager.dart +++ b/lib/features/packs/pack_manager.dart @@ -61,9 +61,6 @@ class PackManager { yield await _cacheManager.loadPackFromCache(id); } on Object catch (e, s) { log('Cant fetch pack', error: e, stackTrace: s); - } finally { - log('Updating'); - unawaited(locator.packUpdater.updateAvailablePacks()); } } } else { @@ -78,9 +75,6 @@ class PackManager { yield await _cacheManager.loadPackFromCache(id); } on Object catch (e, s) { log('Cant fetch pack', error: e, stackTrace: s); - } finally { - log('Updating'); - unawaited(locator.packUpdater.updateAvailablePacks()); } } } @@ -93,7 +87,6 @@ class PackManager { Future clearCache() async { _packHolder.clear(); await _cacheManager.clearCache(); - await globalSharedPreferences.clear(); } Stream?> get packsStream => diff --git a/lib/features/packs/pack_updater.dart b/lib/features/packs/pack_updater.dart index 2a5bbee..85c6b82 100644 --- a/lib/features/packs/pack_updater.dart +++ b/lib/features/packs/pack_updater.dart @@ -30,7 +30,7 @@ class PackUpdater { Future init() async { final pollStream = Stream.periodic(const Duration(seconds: 60)); - pollStream.startWith(-1).listen((event) async { + pollStream.listen((event) async { await updateAvailablePacks(); try { final r = await _packsApi.getPacksPreviews(null); @@ -105,18 +105,6 @@ class PackUpdater { } } - Future updatePackInfoWithCardsAndSave(String packId) async { - try { - // todo - // final pack = await _updatePackInfoWithCards(packId); - // await _cacheManager.savePack(pack, updateCards: true); - return true; - } catch (e) { - log(e.toString()); - } - return false; - } - Future updatePackAndSaveImages(String packId) async { try { log('Updating with cards ${packId}', name: 'updatePackWithCards'); diff --git a/lib/features/tests/progress_widget.dart b/lib/features/tests/progress_widget.dart index 9ef81c5..3bbebaf 100644 --- a/lib/features/tests/progress_widget.dart +++ b/lib/features/tests/progress_widget.dart @@ -39,6 +39,7 @@ enum Result { correct, wrong, skiped, + not_visited, } class _ProgressWidgetState extends State { @@ -99,7 +100,8 @@ class _ProgressWidgetState extends State { widthFactor: length == 0 ? 0 : (results - .where((r) => r != Result.skiped) + .where((r) => + r != Result.not_visited) .length) / length, duration: Duration(milliseconds: 300), diff --git a/lib/features/tests/question_states/input_buttons_test_state.dart b/lib/features/tests/question_states/input_buttons_test_state.dart index 6cde7d4..4fbf3ce 100644 --- a/lib/features/tests/question_states/input_buttons_test_state.dart +++ b/lib/features/tests/question_states/input_buttons_test_state.dart @@ -14,6 +14,7 @@ class InputButtonsTestState extends TestQuestionState { final List answerIds; final bool isCorrect; final bool isAnswered; + final bool isSkipped; final int seed; InputButtonsTestState({ @@ -22,6 +23,7 @@ class InputButtonsTestState extends TestQuestionState { this.answerIds = const [], this.isCorrect = false, this.isAnswered = false, + this.isSkipped = false, super.testType = TestQuestionType.simple, - }) {} + }); } diff --git a/lib/features/tests/question_states/input_buttons_test_state.g.dart b/lib/features/tests/question_states/input_buttons_test_state.g.dart index ae9cd54..fa99ce8 100644 --- a/lib/features/tests/question_states/input_buttons_test_state.g.dart +++ b/lib/features/tests/question_states/input_buttons_test_state.g.dart @@ -17,6 +17,8 @@ abstract class _$InputButtonsTestStateCWProxy { InputButtonsTestState isAnswered(bool isAnswered); + InputButtonsTestState isSkipped(bool isSkipped); + InputButtonsTestState testType(TestQuestionType testType); /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `InputButtonsTestState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. @@ -31,6 +33,7 @@ abstract class _$InputButtonsTestStateCWProxy { List? answerIds, bool? isCorrect, bool? isAnswered, + bool? isSkipped, TestQuestionType? testType, }); } @@ -59,6 +62,9 @@ class _$InputButtonsTestStateCWProxyImpl InputButtonsTestState isAnswered(bool isAnswered) => this(isAnswered: isAnswered); + @override + InputButtonsTestState isSkipped(bool isSkipped) => this(isSkipped: isSkipped); + @override InputButtonsTestState testType(TestQuestionType testType) => this(testType: testType); @@ -77,6 +83,7 @@ class _$InputButtonsTestStateCWProxyImpl Object? answerIds = const $CopyWithPlaceholder(), Object? isCorrect = const $CopyWithPlaceholder(), Object? isAnswered = const $CopyWithPlaceholder(), + Object? isSkipped = const $CopyWithPlaceholder(), Object? testType = const $CopyWithPlaceholder(), }) { return InputButtonsTestState( @@ -101,6 +108,10 @@ class _$InputButtonsTestStateCWProxyImpl ? _value.isAnswered // ignore: cast_nullable_to_non_nullable : isAnswered as bool, + isSkipped: isSkipped == const $CopyWithPlaceholder() || isSkipped == null + ? _value.isSkipped + // ignore: cast_nullable_to_non_nullable + : isSkipped as bool, testType: testType == const $CopyWithPlaceholder() || testType == null ? _value.testType // ignore: cast_nullable_to_non_nullable @@ -134,6 +145,7 @@ InputButtonsTestState _$InputButtonsTestStateFromJson( const [], isCorrect: json['isCorrect'] as bool? ?? false, isAnswered: json['isAnswered'] as bool? ?? false, + isSkipped: json['isSkipped'] as bool? ?? false, testType: $enumDecodeNullable(_$TestQuestionTypeEnumMap, json['testType']) ?? TestQuestionType.simple, @@ -147,6 +159,7 @@ Map _$InputButtonsTestStateToJson( 'answerIds': instance.answerIds, 'isCorrect': instance.isCorrect, 'isAnswered': instance.isAnswered, + 'isSkipped': instance.isSkipped, 'seed': instance.seed, }; diff --git a/lib/features/tests/question_states/simple_test_state.dart b/lib/features/tests/question_states/simple_test_state.dart index e28871d..10ebddf 100644 --- a/lib/features/tests/question_states/simple_test_state.dart +++ b/lib/features/tests/question_states/simple_test_state.dart @@ -13,6 +13,7 @@ class SimpleTestState extends TestQuestionState { final String answer; final bool isCorrect; final bool isAnswered; + final bool isSkipped; final int seed; SimpleTestState({ @@ -20,6 +21,7 @@ class SimpleTestState extends TestQuestionState { this.answer = '', this.isCorrect = false, this.isAnswered = false, + this.isSkipped = false, super.testType = TestQuestionType.simple, }); } diff --git a/lib/features/tests/question_states/simple_test_state.g.dart b/lib/features/tests/question_states/simple_test_state.g.dart index 8e79c04..4993756 100644 --- a/lib/features/tests/question_states/simple_test_state.g.dart +++ b/lib/features/tests/question_states/simple_test_state.g.dart @@ -15,6 +15,8 @@ abstract class _$SimpleTestStateCWProxy { SimpleTestState isAnswered(bool isAnswered); + SimpleTestState isSkipped(bool isSkipped); + SimpleTestState testType(TestQuestionType testType); /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `SimpleTestState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. @@ -28,6 +30,7 @@ abstract class _$SimpleTestStateCWProxy { String? answer, bool? isCorrect, bool? isAnswered, + bool? isSkipped, TestQuestionType? testType, }); } @@ -50,6 +53,9 @@ class _$SimpleTestStateCWProxyImpl implements _$SimpleTestStateCWProxy { @override SimpleTestState isAnswered(bool isAnswered) => this(isAnswered: isAnswered); + @override + SimpleTestState isSkipped(bool isSkipped) => this(isSkipped: isSkipped); + @override SimpleTestState testType(TestQuestionType testType) => this(testType: testType); @@ -67,6 +73,7 @@ class _$SimpleTestStateCWProxyImpl implements _$SimpleTestStateCWProxy { Object? answer = const $CopyWithPlaceholder(), Object? isCorrect = const $CopyWithPlaceholder(), Object? isAnswered = const $CopyWithPlaceholder(), + Object? isSkipped = const $CopyWithPlaceholder(), Object? testType = const $CopyWithPlaceholder(), }) { return SimpleTestState( @@ -87,6 +94,10 @@ class _$SimpleTestStateCWProxyImpl implements _$SimpleTestStateCWProxy { ? _value.isAnswered // ignore: cast_nullable_to_non_nullable : isAnswered as bool, + isSkipped: isSkipped == const $CopyWithPlaceholder() || isSkipped == null + ? _value.isSkipped + // ignore: cast_nullable_to_non_nullable + : isSkipped as bool, testType: testType == const $CopyWithPlaceholder() || testType == null ? _value.testType // ignore: cast_nullable_to_non_nullable @@ -111,6 +122,7 @@ SimpleTestState _$SimpleTestStateFromJson(Map json) => answer: json['answer'] as String? ?? '', isCorrect: json['isCorrect'] as bool? ?? false, isAnswered: json['isAnswered'] as bool? ?? false, + isSkipped: json['isSkipped'] as bool? ?? false, testType: $enumDecodeNullable(_$TestQuestionTypeEnumMap, json['testType']) ?? TestQuestionType.simple, @@ -122,6 +134,7 @@ Map _$SimpleTestStateToJson(SimpleTestState instance) => 'answer': instance.answer, 'isCorrect': instance.isCorrect, 'isAnswered': instance.isAnswered, + 'isSkipped': instance.isSkipped, 'seed': instance.seed, }; diff --git a/lib/features/tests/question_states/test_question_state.dart b/lib/features/tests/question_states/test_question_state.dart index 5ac7a5d..440687d 100644 --- a/lib/features/tests/question_states/test_question_state.dart +++ b/lib/features/tests/question_states/test_question_state.dart @@ -9,9 +9,12 @@ class TestQuestionState { bool get isCorrect => false; - bool get isAnswered => false; + final bool isAnswered; + final bool isSkipped; const TestQuestionState({ required this.testType, + this.isAnswered = false, + this.isSkipped = false, }); } diff --git a/lib/features/tests/question_states/test_question_state.g.dart b/lib/features/tests/question_states/test_question_state.g.dart index d2b0159..ab3c9b8 100644 --- a/lib/features/tests/question_states/test_question_state.g.dart +++ b/lib/features/tests/question_states/test_question_state.g.dart @@ -9,6 +9,10 @@ part of 'test_question_state.dart'; abstract class _$TestQuestionStateCWProxy { TestQuestionState testType(TestQuestionType testType); + TestQuestionState isAnswered(bool isAnswered); + + TestQuestionState isSkipped(bool isSkipped); + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TestQuestionState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// /// Usage @@ -17,6 +21,8 @@ abstract class _$TestQuestionStateCWProxy { /// ```` TestQuestionState call({ TestQuestionType? testType, + bool? isAnswered, + bool? isSkipped, }); } @@ -30,6 +36,12 @@ class _$TestQuestionStateCWProxyImpl implements _$TestQuestionStateCWProxy { TestQuestionState testType(TestQuestionType testType) => this(testType: testType); + @override + TestQuestionState isAnswered(bool isAnswered) => this(isAnswered: isAnswered); + + @override + TestQuestionState isSkipped(bool isSkipped) => this(isSkipped: isSkipped); + @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TestQuestionState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. @@ -40,12 +52,23 @@ class _$TestQuestionStateCWProxyImpl implements _$TestQuestionStateCWProxy { /// ```` TestQuestionState call({ Object? testType = const $CopyWithPlaceholder(), + Object? isAnswered = const $CopyWithPlaceholder(), + Object? isSkipped = const $CopyWithPlaceholder(), }) { return TestQuestionState( testType: testType == const $CopyWithPlaceholder() || testType == null ? _value.testType // ignore: cast_nullable_to_non_nullable : testType as TestQuestionType, + isAnswered: + isAnswered == const $CopyWithPlaceholder() || isAnswered == null + ? _value.isAnswered + // ignore: cast_nullable_to_non_nullable + : isAnswered as bool, + isSkipped: isSkipped == const $CopyWithPlaceholder() || isSkipped == null + ? _value.isSkipped + // ignore: cast_nullable_to_non_nullable + : isSkipped as bool, ); } } diff --git a/lib/features/tests/scroll_physics.dart b/lib/features/tests/scroll_physics.dart new file mode 100644 index 0000000..15a32ee --- /dev/null +++ b/lib/features/tests/scroll_physics.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; + +/// This [ScrollPhysics] allows you to block the movement in the horizontal axis. +/// You can block both right and left movements. +/// +/// {@tool sample} +/// +/// This sample shows a [HorizontalBlockedScrollPhysics] blocking left movement +/// +/// ```dart +/// HorizontalBlockedScrollPhysics(blockLeftMovent: true, blockRightMovement: false); +/// ``` +/// {@end-tool} +class HorizontalBlockedScrollPhysics extends ScrollPhysics { + /// If [true] it blocks the left movement. + final bool blockLeftMovement; + + /// If [true] it blocks the right movement. + final bool blockRightMovement; + + const HorizontalBlockedScrollPhysics({ + ScrollPhysics? parent, + this.blockLeftMovement = false, + this.blockRightMovement = false, + }) : super(parent: parent); + + @override + HorizontalBlockedScrollPhysics applyTo(ScrollPhysics? ancestor) { + return HorizontalBlockedScrollPhysics( + parent: buildParent(ancestor), + blockLeftMovement: blockLeftMovement, + blockRightMovement: blockRightMovement, + ); + } + + @override + double applyBoundaryConditions(ScrollMetrics position, double value) { + assert(() { + if (value == position.pixels) { + throw FlutterError( + '$runtimeType.applyBoundaryConditions() was called redundantly.\n' + 'The proposed new position, $value, is exactly equal to the current position of the ' + 'given ${position.runtimeType}, ${position.pixels}.\n' + 'The applyBoundaryConditions method should only be called when the value is ' + 'going to actually change the pixels, otherwise it is redundant.\n' + 'The physics object in question was:\n' + ' $this\n' + 'The position object in question was:\n' + ' $position\n'); + } + return true; + }()); + + if (value < position.pixels && + position.pixels <= position.minScrollExtent) { + return value - position.pixels; + } + + if (position.maxScrollExtent <= position.pixels && + position.pixels < value) { + // overscroll + return value - position.pixels; + } + + if (value < position.minScrollExtent && + position.minScrollExtent < position.pixels) { + // hit top edge + return value - position.minScrollExtent; + } + + if (position.pixels < position.maxScrollExtent && + position.maxScrollExtent < value) { + // hit bottom edge + return value - position.maxScrollExtent; + } + + // If true, movement goes to the left. If it's a swipe, it goes to the left. + var isMovingLeft = value > position.pixels; + var screenIndex = (value / position.viewportDimension).floor(); + var pointInScreen = value - (screenIndex * position.viewportDimension); + // If true, the middle point of the screen is in the left side of the screen. + // This will be useful in order to not block some movements when in returning position. + var isPointInScreenLeftRange = + pointInScreen < (position.viewportDimension / 2); + var delta = value - position.pixels; + + // We're moving left and we want to block. + if (isMovingLeft && blockLeftMovement && isPointInScreenLeftRange) { + if (pointInScreen.abs() < delta.abs()) { + // fix for strong movements + return pointInScreen; + } + return delta; + } + + // We're moving right and we want to block. + if (!isMovingLeft && blockRightMovement && !isPointInScreenLeftRange) { + return delta; + } + + return super.applyBoundaryConditions(position, value); + } +} + +/// This [ScrollPhysics] blocks the left movement in the horizontal axis allowing only movements to the right. +/// +/// {@tool sample} +/// +/// This sample shows a [LeftBlockedScrollPhysics] blocking left movement +/// +/// ```dart +/// LeftBlockedScrollPhysics(); +/// ``` +/// {@end-tool} +class LeftBlockedScrollPhysics extends HorizontalBlockedScrollPhysics { + const LeftBlockedScrollPhysics({ + ScrollPhysics? parent, + }) : super(parent: parent, blockLeftMovement: true); + + @override + LeftBlockedScrollPhysics applyTo(ScrollPhysics? ancestor) { + return LeftBlockedScrollPhysics( + parent: buildParent(ancestor), + ); + } +} + +// This [ScrollPhysics] blocks the right movement in the horizontal axis allowing only movements to the left. +/// +/// {@tool sample} +/// +/// This sample shows a [RightBlockedScrollPhysics] blocking right movement +/// +/// ```dart +/// RightBlockedScrollPhysics(); +/// ``` +/// {@end-tool} +class RightBlockedScrollPhysics extends HorizontalBlockedScrollPhysics { + const RightBlockedScrollPhysics({ + ScrollPhysics? parent, + }) : super(parent: parent, blockRightMovement: true); + + @override + RightBlockedScrollPhysics applyTo(ScrollPhysics? ancestor) { + return RightBlockedScrollPhysics( + parent: buildParent(ancestor), + ); + } +} \ No newline at end of file diff --git a/lib/features/tests/test_manager.dart b/lib/features/tests/test_manager.dart index d195306..637d8fc 100644 --- a/lib/features/tests/test_manager.dart +++ b/lib/features/tests/test_manager.dart @@ -30,6 +30,11 @@ class TestManager extends ChangeNotifier { return tests; } + void skip(int id) { + testHolder.skip(id); + notifyListeners(); + } + Stream> loadTestsStream({String? packId}) { return Stream.periodic( Duration(seconds: 60), diff --git a/lib/features/tests/test_page.dart b/lib/features/tests/test_page.dart index c032652..e69c411 100644 --- a/lib/features/tests/test_page.dart +++ b/lib/features/tests/test_page.dart @@ -1,11 +1,15 @@ +import 'dart:developer'; +import 'dart:math' as math; + import 'package:auto_route/auto_route.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mnemo_cards/features/tests/question_states/test_question_state.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:pie_chart/pie_chart.dart'; import '../../di/locator.dart'; +import 'scroll_physics.dart'; import 'test_progress_widget.dart'; import 'test_widgets/input_buttons_test.dart'; import 'test_widgets/simple_test.dart'; @@ -21,13 +25,39 @@ class TestPage extends StatefulWidget { } class _TestPageState extends State { + + PageController get pageController => locator.testManager.pageController; + @override void initState() { super.initState(); locator.testManager.setActiveTest(widget.testId); locator.testManager.preloadImages(context); + pageController.addListener(_pageScrollListener); } + @override + void dispose() { + pageController.removeListener(_pageScrollListener); + super.dispose(); + } + + void _pageScrollListener() { + final index = pageController.page?.floor() ?? 0; + if (index > 0) { + final prevId = + locator.testManager.questions[index - 1].id!; + final state = + locator.testManager.getState(prevId); + if (state?.isAnswered == false) { + log('Question skipped'); + locator.testManager.skip(prevId); + } + } + } + + int lastTestIndex = 0; + @override Widget build(BuildContext context) { return Scaffold( @@ -60,16 +90,11 @@ class _TestPageState extends State { ), Expanded( child: PageView.builder( - physics: NeverScrollableScrollPhysics(), //ClampingScrollPhysics(), + physics: RightBlockedScrollPhysics(), controller: locator.testManager.pageController, itemBuilder: (context, index) { - if (index > 0) { - final state = locator.testManager.getState( - locator.testManager.questions[index - 1].id!, - ); - if (state?.isCorrect != true) { - return null; - } + if (index > locator.testManager.questions.length) { + return null; } if (index == locator.testManager.questions.length) { return Column( @@ -152,6 +177,7 @@ class _TestPageState extends State { ); } + lastTestIndex = math.max(index, lastTestIndex); return switch (locator.testManager .getTest(index) .questionType) { diff --git a/lib/features/tests/test_progress_widget.dart b/lib/features/tests/test_progress_widget.dart index 8b9640b..f335af5 100644 --- a/lib/features/tests/test_progress_widget.dart +++ b/lib/features/tests/test_progress_widget.dart @@ -35,10 +35,12 @@ class _TestProgressWidgetState extends State { results.add(Result.correct); } else { // results.add(Result.wrong); - results.add(Result.skiped); + results.add(Result.not_visited); } + } else if (state.isSkipped) { + results.add(Result.correct); } else { - results.add(Result.skiped); + results.add(Result.not_visited); } } if (mounted) { diff --git a/lib/features/tests/test_state_holder.dart b/lib/features/tests/test_state_holder.dart index d575ee0..11bc77a 100644 --- a/lib/features/tests/test_state_holder.dart +++ b/lib/features/tests/test_state_holder.dart @@ -15,6 +15,31 @@ class TestHolder { TestHolder() : __state = _TestState({}); + void skip(int id) { + final state = testQuestionState(id); + switch (state?.testType) { + case TestQuestionType.simple: + state as SimpleTestState; + setQuestionState(id, state.copyWith(isSkipped: true)); + break; + case TestQuestionType.input_buttons: + state as InputButtonsTestState; + setQuestionState(id, state.copyWith(isSkipped: true)); + break; + case TestQuestionType.matrix: + // TODO: Handle this case. + break; + case TestQuestionType.match: + // TODO: Handle this case. + break; + case TestQuestionType.undefined: + // TODO: Handle this case. + case null: + // TODO: Handle this case. + break; + } + } + void setQuestionState(int id, TestQuestionState state) => _state = _TestState( Map.from(_state.questions..[id] = state), ); diff --git a/lib/features/tests/test_widgets/input_buttons_test.dart b/lib/features/tests/test_widgets/input_buttons_test.dart index 7569124..dbaa3d2 100644 --- a/lib/features/tests/test_widgets/input_buttons_test.dart +++ b/lib/features/tests/test_widgets/input_buttons_test.dart @@ -1,12 +1,7 @@ -import 'dart:math'; - import 'package:collection/collection.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:mnemo_cards/features/tests/test_manager.dart'; import 'package:mnemo_cards/features/tests/test_widgets/common_test_question_widget.dart'; -import 'package:mnemo_cards/features/tests/test_widgets/test_image.dart'; -import 'package:mnemo_cards/theme/themes.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import '../../../di/locator.dart'; import '../question_states/input_buttons_test_state.dart'; @@ -39,8 +34,6 @@ class InputButtonsTestWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context).textTheme; - // final random = Random(state.seed); final buttons = model.buttons; //..shuffle(random); return Padding( padding: const EdgeInsets.all(8.0), @@ -49,6 +42,7 @@ class InputButtonsTestWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( + flex: 2, child: CommonTestQuestionWidget( text: model.text, image: model.image, @@ -142,6 +136,7 @@ class InputButtonsTestWidget extends StatelessWidget { ]; return Expanded( + flex: 3, child: Column( children: [ Expanded( diff --git a/lib/managers/user_manager.dart b/lib/managers/user_manager.dart index 4751e72..2d10ad0 100644 --- a/lib/managers/user_manager.dart +++ b/lib/managers/user_manager.dart @@ -59,7 +59,6 @@ class UserManager { ExternalIdType idType, String? name, ) async { - await locator.packCacheManager.clearCache(); final (user, token) = await _repository.createOrGetUser(externalId, idType, name); _setAuthToken(token); @@ -75,14 +74,14 @@ class UserManager { await _sharedPreferences!.remove('authToken'); } - Future _updateUser() async { + Future _updateUser() async { String? authToken = _sharedPreferences!.getString('authToken'); if (authToken != null) { _setAuthToken(authToken); try { final fetchedUser = await _repository.getUser(); userStateHolder.setUser(fetchedUser!); - return; + return fetchedUser; } catch (e) { _clearAuthToken(); AppRouter.openAuthOrProfile(); @@ -95,12 +94,11 @@ class UserManager { // final id = deviceInfo.id; // await _createUser(id, ExternalIdType.device); // } + return null; } Future init() async { _sharedPreferences = await SharedPreferences.getInstance(); - - //todo add ios try { await _updateUser(); } on Object catch (e, s) { @@ -120,12 +118,15 @@ class UserManager { await _createUser(token!, ExternalIdType.google, name); } })) - ..add(userStateHolder.asNullableStream.listen((user) async { + ..add(userStateHolder.asNullableStream + .distinct((a, b) => a?.id == b?.id) + .listen((user) async { try { if (user == null) { - await _updateUser(); + AppRouter.openAuthOrProfile(); } else { try { + AppRouter.closeAuthPage(); await locator.previewPackPoller.poll(); await locator.packManager.clearCache(); await locator.packUpdater.updateAvailablePacks(); @@ -144,19 +145,7 @@ class UserManager { } } } - })) - ..add( - userStateHolder.asNullableStream - .map((user) => user != null) - .distinct() - .listen((hasUser) { - if (hasUser) { - appRouter.replace(PageRouteInfo(HomePage.name)); - } else { - AppRouter.openAuthOrProfile(); - } - }), - ); + })); } Future dispose() async { diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart deleted file mode 100644 index fbe5e86..0000000 --- a/lib/pages/explore_page.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:mnemo_cards/di/locator.dart'; - -import '../widgets/packs_menu_widget.dart'; - -@RoutePage() -class ExplorePage extends StatelessWidget { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context).textTheme; - return Column( - children: [ - Container( - alignment: Alignment.center, - padding: const EdgeInsets.all(12.0), - child: Text( - 'Blah - blah', - style: theme.headlineSmall, - ), - ), - Expanded( - child: PacksMenuWidget( - locator.repository, - locator.packManager, - ), - ), - TextButton( - onPressed: () async { - await locator.packManager.clearCache(); - locator.testManager.clearStates(); - await locator.packUpdater.updateAvailablePacks(); - }, - child: Text('Clear cache'), - ), - TextButton( - onPressed: () async { - locator.userManager.login(); - }, - child: Text('Login'), - ), - ], - ); - } - - const ExplorePage(); -} diff --git a/lib/widgets/card_pack/available_pack.dart b/lib/widgets/card_pack/available_pack.dart index b37d12e..d100723 100644 --- a/lib/widgets/card_pack/available_pack.dart +++ b/lib/widgets/card_pack/available_pack.dart @@ -36,9 +36,6 @@ class AvailablePack extends StatelessWidget { CardPackHeader.fromDto(cardPack), Expanded( child: ListView( - physics: BouncingScrollPhysics( - decelerationRate: ScrollDecelerationRate.fast, - parent: AlwaysScrollableScrollPhysics()), controller: _mainScrollController, children: [ StatefulBuilder(builder: (context, setState) { diff --git a/lib/widgets/card_pack/card_pack_page.dart b/lib/widgets/card_pack/card_pack_page.dart index 9d10178..0fe9671 100644 --- a/lib/widgets/card_pack/card_pack_page.dart +++ b/lib/widgets/card_pack/card_pack_page.dart @@ -34,17 +34,17 @@ class _CardPackPage extends State { ), initialData: null, builder: (context, snapshot) { - if (!snapshot.hasData) return SizedBox.shrink(); + if (!snapshot.hasData) return Loading(widget.previewDto); if (snapshot.requireData.responseType == GetCardPackResponseType.loading) return Loading(widget.previewDto); final response = snapshot.requireData; - if (response is CardPackBuyDto) + if (response is CardPackBuyDto) { return BuyPack(response); - else if (response is CardPackDto) { + } else if (response is CardPackDto) { return AvailablePack(response); } - Analytics.error( + Analytics.crashlyticsError( data: {'response': jsonEncode(snapshot.data?.toJson() ?? '{}')}, message: 'cant load pack ${widget.previewDto.id}', ); @@ -54,7 +54,9 @@ class _CardPackPage extends State { CardPackHeader.fromDto(null), Expanded( child: Center( - child: Text('Error'), + child: Text( + 'Ошибка, попробуйте еще раз или попробуйте обновить приложение', + ), ), ), ], diff --git a/lib/widgets/game_card_widget.dart b/lib/widgets/game_card_widget.dart index 02efea9..c08dd0a 100644 --- a/lib/widgets/game_card_widget.dart +++ b/lib/widgets/game_card_widget.dart @@ -201,7 +201,8 @@ class FrontCard extends StatelessWidget { ar = 16 / 9; } final textScale = min(1.0, ar / targetTextAr) * alpha * alpha; - final imageScale = 0.9 * min(1.0, pow(ar / targetImageAr, 1.5 * alpha)); + final imageScale = + 0.9 * min(1.0, pow(ar / targetImageAr, 1.5 * alpha)); return Material( borderOnForeground: false, type: MaterialType.transparency, @@ -310,9 +311,11 @@ class FrontCard extends StatelessWidget { syncDuration: Duration.zero, ), ), - if (card.mnemo?.isNotEmpty == true) GestureDetector( onTap: () async { + if (card.mnemo?.trim().isNotEmpty != true) { + return; + } final sp = await SharedPreferences.getInstance(); if (sp.getBool('sound_on') != false) { FlutterTts flutterTts = FlutterTts(); @@ -323,7 +326,7 @@ class FrontCard extends StatelessWidget { } }, child: MnemoText( - card.mnemo, + card.mnemo?.isNotEmpty == true ? card.mnemo : ' ', textStyle: TextStyle( fontSize: 20 * textScale, fontWeight: FontWeight.w400, diff --git a/pubspec.lock b/pubspec.lock index 92ce8a6..801c949 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -983,6 +983,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + reorderables: + dependency: "direct main" + description: + name: reorderables + sha256: "004a886e4878df1ee27321831c838bc1c976311f4ca6a74ce7d561e506540a77" + url: "https://pub.dev" + source: hosted + version: "0.6.0" retrofit: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b324bf6..6538727 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: firebase_analytics: ^10.10.7 in_app_purchase: ^3.2.0 dot_navigation_bar: ^1.0.2 + reorderables: ^0.6.0 story: ^1.1.0 flutter_screenutil: ^5.9.0 pie_chart: ^5.4.0