stuff
This commit is contained in:
parent
7c055f64f3
commit
7aecd2dd09
44 changed files with 1578 additions and 680 deletions
BIN
icons/down.png
Normal file
BIN
icons/down.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 976 B |
BIN
icons/heart_fill.png
Normal file
BIN
icons/heart_fill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
|
|
@ -274,7 +274,9 @@ class EditCard {
|
||||||
TextField(
|
TextField(
|
||||||
controller: backController,
|
controller: backController,
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
gameCardDto = gameCardDto.copyWith(back: v);
|
gameCardDto = gameCardDto.copyWith(
|
||||||
|
back: v,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Задняя сторона',
|
hintText: 'Задняя сторона',
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,10 @@ class AdminApi with Api {
|
||||||
AdminApi(this._dio);
|
AdminApi(this._dio);
|
||||||
|
|
||||||
Future<bool> addCard(GameCardDto dto) async {
|
Future<bool> addCard(GameCardDto dto) async {
|
||||||
final r = await _dio.post<String>('$path/cards/add',
|
final r = await _dio.post<String>(
|
||||||
data: jsonEncode(dto.toJson()));
|
'$path/cards/add',
|
||||||
|
data: jsonEncode(dto.toJson()),
|
||||||
|
);
|
||||||
return r.statusCode == 200;
|
return r.statusCode == 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ import '../managers/user_manager.dart';
|
||||||
GetIt getIt = GetIt.I;
|
GetIt getIt = GetIt.I;
|
||||||
|
|
||||||
Future<void> setInjections() async {
|
Future<void> setInjections() async {
|
||||||
getIt.registerSingletonAsync<DioProvider>(() => DioProvider().init());
|
getIt.registerSingletonAsync<DioProvider>(
|
||||||
|
() => DioProvider().init(),
|
||||||
|
);
|
||||||
getIt.registerLazySingleton<HttpRepository>(
|
getIt.registerLazySingleton<HttpRepository>(
|
||||||
() => HttpRepository(getIt.get<DioProvider>().dio),
|
() => HttpRepository(getIt.get<DioProvider>().dio),
|
||||||
);
|
);
|
||||||
|
|
@ -96,4 +98,5 @@ Future<void> setInjections() async {
|
||||||
getIt.registerLazySingleton(
|
getIt.registerLazySingleton(
|
||||||
() => PurchaseDetailsStreamSubscription(),
|
() => PurchaseDetailsStreamSubscription(),
|
||||||
);
|
);
|
||||||
|
await getIt.allReady();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:mnemo_cards/features/packs/preview_pack_holder.dart';
|
||||||
import 'package:mnemo_cards/features/packs/preview_packs_poller.dart';
|
import 'package:mnemo_cards/features/packs/preview_packs_poller.dart';
|
||||||
import 'package:mnemo_cards/managers/favorite_cards.dart';
|
import 'package:mnemo_cards/managers/favorite_cards.dart';
|
||||||
import 'package:mnemo_cards/managers/repository/http_repository.dart';
|
import 'package:mnemo_cards/managers/repository/http_repository.dart';
|
||||||
|
|
@ -32,6 +33,8 @@ class Locator {
|
||||||
|
|
||||||
PackUpdater get packUpdater => _getIt.get<PackUpdater>();
|
PackUpdater get packUpdater => _getIt.get<PackUpdater>();
|
||||||
|
|
||||||
|
PreviewPackHolder get previewPackHolder => _getIt.get<PreviewPackHolder>();
|
||||||
|
|
||||||
PreviewPacksPoller get previewPackPoller => _getIt.get<PreviewPacksPoller>();
|
PreviewPacksPoller get previewPackPoller => _getIt.get<PreviewPacksPoller>();
|
||||||
|
|
||||||
TestManager get testManager => _getIt.get<TestManager>();
|
TestManager get testManager => _getIt.get<TestManager>();
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,11 @@
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
|
|
||||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
|
import 'dart:ui' as _i11;
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart' as _i9;
|
import 'package:auto_route/auto_route.dart' as _i9;
|
||||||
import 'package:flutter/cupertino.dart' as _i11;
|
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/features/tests/test_page.dart' as _i8;
|
||||||
import 'package:mnemo_cards/main.dart' as _i6;
|
import 'package:mnemo_cards/main.dart' as _i6;
|
||||||
import 'package:mnemo_cards/pages/auth_page.dart' as _i1;
|
import 'package:mnemo_cards/pages/auth_page.dart' as _i1;
|
||||||
|
|
@ -36,9 +39,11 @@ abstract class $AppRouter extends _i9.RootStackRouter {
|
||||||
return _i9.AutoRoutePage<dynamic>(
|
return _i9.AutoRoutePage<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: _i2.CardGamePage(
|
child: _i2.CardGamePage(
|
||||||
cardPack: args.cardPack,
|
cards: args.cards,
|
||||||
|
color: args.color,
|
||||||
startCardId: args.startCardId,
|
startCardId: args.startCardId,
|
||||||
infinite: args.infinite,
|
infinite: args.infinite,
|
||||||
|
shuffle: args.shuffle,
|
||||||
key: args.key,
|
key: args.key,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -48,7 +53,7 @@ abstract class $AppRouter extends _i9.RootStackRouter {
|
||||||
return _i9.AutoRoutePage<dynamic>(
|
return _i9.AutoRoutePage<dynamic>(
|
||||||
routeData: routeData,
|
routeData: routeData,
|
||||||
child: _i3.CardPackPage(
|
child: _i3.CardPackPage(
|
||||||
args.cardPackId,
|
args.previewDto,
|
||||||
key: args.key,
|
key: args.key,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -105,17 +110,21 @@ class AuthPage extends _i9.PageRouteInfo<void> {
|
||||||
/// [_i2.CardGamePage]
|
/// [_i2.CardGamePage]
|
||||||
class CardGamePage extends _i9.PageRouteInfo<CardGamePageArgs> {
|
class CardGamePage extends _i9.PageRouteInfo<CardGamePageArgs> {
|
||||||
CardGamePage({
|
CardGamePage({
|
||||||
required _i10.CardPackDto cardPack,
|
required List<_i10.GameCardDto> cards,
|
||||||
|
required _i11.Color? color,
|
||||||
int? startCardId,
|
int? startCardId,
|
||||||
bool infinite = false,
|
bool infinite = false,
|
||||||
_i11.Key? key,
|
bool shuffle = true,
|
||||||
|
_i12.Key? key,
|
||||||
List<_i9.PageRouteInfo>? children,
|
List<_i9.PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
CardGamePage.name,
|
CardGamePage.name,
|
||||||
args: CardGamePageArgs(
|
args: CardGamePageArgs(
|
||||||
cardPack: cardPack,
|
cards: cards,
|
||||||
|
color: color,
|
||||||
startCardId: startCardId,
|
startCardId: startCardId,
|
||||||
infinite: infinite,
|
infinite: infinite,
|
||||||
|
shuffle: shuffle,
|
||||||
key: key,
|
key: key,
|
||||||
),
|
),
|
||||||
initialChildren: children,
|
initialChildren: children,
|
||||||
|
|
@ -129,23 +138,29 @@ class CardGamePage extends _i9.PageRouteInfo<CardGamePageArgs> {
|
||||||
|
|
||||||
class CardGamePageArgs {
|
class CardGamePageArgs {
|
||||||
const CardGamePageArgs({
|
const CardGamePageArgs({
|
||||||
required this.cardPack,
|
required this.cards,
|
||||||
|
required this.color,
|
||||||
this.startCardId,
|
this.startCardId,
|
||||||
this.infinite = false,
|
this.infinite = false,
|
||||||
|
this.shuffle = true,
|
||||||
this.key,
|
this.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final _i10.CardPackDto cardPack;
|
final List<_i10.GameCardDto> cards;
|
||||||
|
|
||||||
|
final _i11.Color? color;
|
||||||
|
|
||||||
final int? startCardId;
|
final int? startCardId;
|
||||||
|
|
||||||
final bool infinite;
|
final bool infinite;
|
||||||
|
|
||||||
final _i11.Key? key;
|
final bool shuffle;
|
||||||
|
|
||||||
|
final _i12.Key? key;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'CardGamePageArgs{cardPack: $cardPack, startCardId: $startCardId, infinite: $infinite, key: $key}';
|
return 'CardGamePageArgs{cards: $cards, color: $color, startCardId: $startCardId, infinite: $infinite, shuffle: $shuffle, key: $key}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,13 +168,13 @@ class CardGamePageArgs {
|
||||||
/// [_i3.CardPackPage]
|
/// [_i3.CardPackPage]
|
||||||
class CardPackPage extends _i9.PageRouteInfo<CardPackPageArgs> {
|
class CardPackPage extends _i9.PageRouteInfo<CardPackPageArgs> {
|
||||||
CardPackPage({
|
CardPackPage({
|
||||||
required String cardPackId,
|
required _i10.CardPackPreviewDto previewDto,
|
||||||
_i11.Key? key,
|
_i13.Key? key,
|
||||||
List<_i9.PageRouteInfo>? children,
|
List<_i9.PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
CardPackPage.name,
|
CardPackPage.name,
|
||||||
args: CardPackPageArgs(
|
args: CardPackPageArgs(
|
||||||
cardPackId: cardPackId,
|
previewDto: previewDto,
|
||||||
key: key,
|
key: key,
|
||||||
),
|
),
|
||||||
initialChildren: children,
|
initialChildren: children,
|
||||||
|
|
@ -173,17 +188,17 @@ class CardPackPage extends _i9.PageRouteInfo<CardPackPageArgs> {
|
||||||
|
|
||||||
class CardPackPageArgs {
|
class CardPackPageArgs {
|
||||||
const CardPackPageArgs({
|
const CardPackPageArgs({
|
||||||
required this.cardPackId,
|
required this.previewDto,
|
||||||
this.key,
|
this.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String cardPackId;
|
final _i10.CardPackPreviewDto previewDto;
|
||||||
|
|
||||||
final _i11.Key? key;
|
final _i13.Key? key;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'CardPackPageArgs{cardPackId: $cardPackId, key: $key}';
|
return 'CardPackPageArgs{previewDto: $previewDto, key: $key}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
173
lib/features/analytics/analytics.dart
Normal file
173
lib/features/analytics/analytics.dart
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
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_common/mnemo_cards_common.dart';
|
||||||
|
|
||||||
|
class Analytics {
|
||||||
|
static final _analytics = FirebaseAnalytics.instance;
|
||||||
|
static final _crashlytics = FirebaseCrashlytics.instance;
|
||||||
|
|
||||||
|
static Future<void> setUser(UserDto user) {
|
||||||
|
return _analytics.setUserId(id: user.id?.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> login(ExternalIdType idType) {
|
||||||
|
return _analytics.logLogin(loginMethod: idType.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> logout(UserDto? dto) async {
|
||||||
|
await _analytics.logEvent(
|
||||||
|
name: 'logout',
|
||||||
|
parameters: {
|
||||||
|
'id': dto?.id,
|
||||||
|
}.asFirebaseMap,
|
||||||
|
);
|
||||||
|
return _analytics.setUserId(id: dto?.id?.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> profilePageOpened(UserDto? dto) {
|
||||||
|
return _analytics.logScreenView(
|
||||||
|
screenClass: 'profile',
|
||||||
|
screenName: 'profile',
|
||||||
|
parameters: {
|
||||||
|
'id': dto?.id,
|
||||||
|
}.asFirebaseMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> availablePackOpened(CardPackDto dto) {
|
||||||
|
return _analytics.logScreenView(
|
||||||
|
screenClass: 'pack',
|
||||||
|
screenName: 'available',
|
||||||
|
parameters: {
|
||||||
|
'id': dto.id,
|
||||||
|
'title': dto.title,
|
||||||
|
'version': dto.version,
|
||||||
|
}.asFirebaseMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> buyPackOpened(CardPackBuyDto dto) {
|
||||||
|
return _analytics.logScreenView(
|
||||||
|
screenClass: 'pack',
|
||||||
|
screenName: 'buy',
|
||||||
|
parameters: {
|
||||||
|
'id': dto.id,
|
||||||
|
'title': dto.title,
|
||||||
|
'version': dto.version,
|
||||||
|
'price': dto.price,
|
||||||
|
}.asFirebaseMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> buyPackPressed(CardPackBuyDto dto, PaymentSystem system) {
|
||||||
|
return _analytics.logEvent(
|
||||||
|
name: 'buy',
|
||||||
|
parameters: {
|
||||||
|
'id': dto.id,
|
||||||
|
'title': dto.title,
|
||||||
|
'version': dto.version,
|
||||||
|
'price': dto.price,
|
||||||
|
'system': system.name,
|
||||||
|
}.asFirebaseMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> buyError(
|
||||||
|
CardPackBuyDto? dto,
|
||||||
|
PaymentSystem system, {
|
||||||
|
Map<String, String?>? data,
|
||||||
|
String? message,
|
||||||
|
}) {
|
||||||
|
return _analytics.logEvent(
|
||||||
|
name: 'buy_error',
|
||||||
|
parameters: {
|
||||||
|
'id': dto?.id.toString(),
|
||||||
|
'system': system.name,
|
||||||
|
'version': dto?.version.toString(),
|
||||||
|
'price': dto?.price.toString(),
|
||||||
|
if (message != null) 'message': message,
|
||||||
|
if (data != null) ...data,
|
||||||
|
}.asFirebaseMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> buyEvent(
|
||||||
|
CardPackBuyDto? dto,
|
||||||
|
PaymentSystem system, {
|
||||||
|
Map<String, String?>? data,
|
||||||
|
String? message,
|
||||||
|
}) {
|
||||||
|
return _analytics.logEvent(
|
||||||
|
name: 'buy_event',
|
||||||
|
parameters: {
|
||||||
|
'id': dto?.id,
|
||||||
|
'system': system.name,
|
||||||
|
'version': dto?.version,
|
||||||
|
'price': dto?.price,
|
||||||
|
if (message != null) 'message': message,
|
||||||
|
if (data != null) ...data,
|
||||||
|
}.asFirebaseMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> buyComplete(
|
||||||
|
CardPackBuyDto? dto,
|
||||||
|
PaymentSystem system, {
|
||||||
|
Map<String, String?>? data,
|
||||||
|
String? message,
|
||||||
|
}) {
|
||||||
|
return _analytics.logEvent(
|
||||||
|
name: 'buy_complete',
|
||||||
|
parameters: {
|
||||||
|
'id': dto?.id,
|
||||||
|
'system': system.name,
|
||||||
|
'version': dto?.version,
|
||||||
|
'price': dto?.price,
|
||||||
|
if (message != null) 'message': message,
|
||||||
|
if (data != null) ...data,
|
||||||
|
}.asFirebaseMap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> loadingPackOpened(CardPackDto dto, int timeMillis) {
|
||||||
|
return _analytics.logScreenView(
|
||||||
|
screenClass: 'pack',
|
||||||
|
screenName: 'loading',
|
||||||
|
parameters: {
|
||||||
|
'id': dto.id,
|
||||||
|
'title': dto.title,
|
||||||
|
'version': dto.version,
|
||||||
|
'time': timeMillis,
|
||||||
|
}.asFirebaseMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> testOpened(TestDto dto) {
|
||||||
|
return _analytics.logScreenView(
|
||||||
|
screenClass: 'test',
|
||||||
|
screenName: 'test',
|
||||||
|
parameters: {
|
||||||
|
'id': dto.id,
|
||||||
|
'name': dto.name,
|
||||||
|
'version': dto.version,
|
||||||
|
'questions': dto.questions.length,
|
||||||
|
}.asFirebaseMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
static crashlyticsError(Object? e, StackTrace? s) =>
|
||||||
|
_crashlytics.recordError(e, s);
|
||||||
|
|
||||||
|
static error({Map<String, String?>? data, String? message}) {
|
||||||
|
try {
|
||||||
|
_analytics.logEvent(
|
||||||
|
name: 'error',
|
||||||
|
parameters: {
|
||||||
|
'msg': message,
|
||||||
|
if (data != null) ...data,
|
||||||
|
}.asFirebaseMap,
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
_crashlytics.recordError(e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on Map {
|
||||||
|
Map<String, Object?> get asFirebaseMap => (Map.from(this)
|
||||||
|
..removeWhere((k, v) => v == null))
|
||||||
|
.map((k, v) => MapEntry(k, (v is String || v is num) ? v : v.toString()));
|
||||||
|
}
|
||||||
|
|
@ -6,21 +6,34 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
|
||||||
class ImagesHolder {
|
class ImagesHolder {
|
||||||
Map<String, MemoryImage> _images = {};
|
Map<String, MemoryImage> _images = {};
|
||||||
|
Map<String, MemoryImage> _packImages = {};
|
||||||
DateTime? unlockTime;
|
DateTime? unlockTime;
|
||||||
|
|
||||||
ImagesHolder();
|
ImagesHolder();
|
||||||
|
|
||||||
void setPackImages(Map<String, MemoryImage> images) {
|
void setPackImages(Map<String, MemoryImage> images) {
|
||||||
_images = Map.from(images);
|
_packImages = Map.from(images);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() => _images.clear();
|
void setImage(String key, MemoryImage image) {
|
||||||
|
_images[key] = image;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
_images.clear();
|
||||||
|
_packImages.clear();
|
||||||
|
}
|
||||||
|
|
||||||
MemoryImage? get(String id) {
|
MemoryImage? get(String id) {
|
||||||
final memoryImage = _images[id];
|
final memoryImage = getPackImage(id) ?? _images[id];
|
||||||
|
|
||||||
return memoryImage;
|
return memoryImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MemoryImage? getPackImage(String id) {
|
||||||
|
final memoryImage = _packImages[id];
|
||||||
|
return memoryImage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ImagesHolderStringExt on String {
|
extension ImagesHolderStringExt on String {
|
||||||
|
|
@ -30,7 +43,3 @@ extension ImagesHolderStringExt on String {
|
||||||
extension ImagesHolderCardExt on GameCardDto {
|
extension ImagesHolderCardExt on GameCardDto {
|
||||||
MemoryImage? get memoryImage => locator.imagesHolder.get(id.toString());
|
MemoryImage? get memoryImage => locator.imagesHolder.get(id.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ImagesHolderIntExt on int {
|
|
||||||
MemoryImage? get memoryImage => locator.imagesHolder.get(toString());
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -120,8 +120,10 @@ class PackCacheManager {
|
||||||
await _sharedPreferences.remove('packs');
|
await _sharedPreferences.remove('packs');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> savePack(CardPackDto dto,
|
Future<void> savePack(
|
||||||
{Map<String, MemoryImage>? images}) async {
|
CardPackDto dto, {
|
||||||
|
Map<String, MemoryImage>? images,
|
||||||
|
}) async {
|
||||||
log('Saving pack ${dto.id}');
|
log('Saving pack ${dto.id}');
|
||||||
final dir = await _packDirectory(dto.id);
|
final dir = await _packDirectory(dto.id);
|
||||||
final json = jsonEncode(dto);
|
final json = jsonEncode(dto);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:mnemo_cards/di/locator.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/features/packs/packs_api.dart';
|
import 'package:mnemo_cards/features/packs/packs_api.dart';
|
||||||
import 'package:mnemo_cards/features/packs/preview_pack_holder.dart';
|
import 'package:mnemo_cards/features/packs/preview_pack_holder.dart';
|
||||||
|
import 'package:mnemo_cards/main.dart';
|
||||||
import 'package:mnemo_cards/managers/repository/repository.dart';
|
import 'package:mnemo_cards/managers/repository/repository.dart';
|
||||||
import 'package:mnemo_cards/utils/iterable_helper.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';
|
||||||
|
|
@ -44,25 +46,43 @@ class PackManager {
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<GetCardPackResponse> getCardPackStream(String id) async* {
|
Stream<GetCardPackResponse> getCardPackStream(
|
||||||
|
String id,
|
||||||
|
bool isAvailable,
|
||||||
|
) async* {
|
||||||
|
if (isAvailable) {
|
||||||
try {
|
try {
|
||||||
yield await _cacheManager.loadPackFromCache(id);
|
yield await _cacheManager.loadPackFromCache(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err.toString());
|
yield GetCardPackResponse.loading();
|
||||||
log('Cant load pack from cache, fetching pack');
|
log('Cant load pack from cache, fetching pack', error: err);
|
||||||
try {
|
try {
|
||||||
final dto = await _packsApi.getPackDto(id).catchError((_) => null);
|
await locator.packUpdater.updatePackAndSaveImages(id);
|
||||||
if (dto != null) {
|
yield await _cacheManager.loadPackFromCache(id);
|
||||||
yield dto;
|
} on Object catch (e, s) {
|
||||||
|
log('Cant fetch pack', error: e, stackTrace: s);
|
||||||
|
} finally {
|
||||||
|
log('Updating');
|
||||||
|
unawaited(locator.packUpdater.updateAvailablePacks());
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
final buyDto = await _packsApi.getPackBuy(id);
|
yield GetCardPackResponse.loading();
|
||||||
|
final buyDto = await _packsApi.getPackBuy(id).catchError((_) => null);
|
||||||
if (buyDto != null) {
|
if (buyDto != null) {
|
||||||
yield buyDto;
|
yield buyDto;
|
||||||
|
} else {
|
||||||
|
log('Buy dto is null, fetching pack');
|
||||||
|
try {
|
||||||
|
await locator.packUpdater.updatePackAndSaveImages(id);
|
||||||
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
log(err.toString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
yield* _packHolder.asStream
|
yield* _packHolder.asStream
|
||||||
.map((event) =>
|
.map((event) =>
|
||||||
|
|
@ -73,6 +93,7 @@ class PackManager {
|
||||||
Future<void> clearCache() async {
|
Future<void> clearCache() async {
|
||||||
_packHolder.clear();
|
_packHolder.clear();
|
||||||
await _cacheManager.clearCache();
|
await _cacheManager.clearCache();
|
||||||
|
await globalSharedPreferences.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<CardPackDto>?> get packsStream =>
|
Stream<List<CardPackDto>?> get packsStream =>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class PackUpdater {
|
||||||
await updateAvailablePacks();
|
await updateAvailablePacks();
|
||||||
try {
|
try {
|
||||||
final r = await _packsApi.getPacksPreviews(null);
|
final r = await _packsApi.getPacksPreviews(null);
|
||||||
_previewPackHolder.setPreviewPacks(r);
|
_previewPackHolder.setPreviewPacks(r, save: true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +79,7 @@ class PackUpdater {
|
||||||
log('UPDATE ${action.id}');
|
log('UPDATE ${action.id}');
|
||||||
shouldUpdateHolder = true;
|
shouldUpdateHolder = true;
|
||||||
try {
|
try {
|
||||||
final updatedPack = await _updatePackAndSaveImages(action.id);
|
final updatedPack = await updatePackAndSaveImages(action.id);
|
||||||
updatedPacks[action.id] = updatedPack;
|
updatedPacks[action.id] = updatedPack;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('Error while download cards for ${action.id} $e');
|
log('Error while download cards for ${action.id} $e');
|
||||||
|
|
@ -117,7 +117,7 @@ class PackUpdater {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<CardPackDto> _updatePackAndSaveImages(String packId) async {
|
Future<CardPackDto> updatePackAndSaveImages(String packId) async {
|
||||||
try {
|
try {
|
||||||
log('Updating with cards ${packId}', name: 'updatePackWithCards');
|
log('Updating with cards ${packId}', name: 'updatePackWithCards');
|
||||||
final packDto = await _packsApi.getPackDto(packId);
|
final packDto = await _packsApi.getPackDto(packId);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
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';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../../main.dart';
|
||||||
|
|
||||||
class PreviewPackHolder {
|
class PreviewPackHolder {
|
||||||
final StreamController<PreviewPackState?> _streamController =
|
final StreamController<PreviewPackState?> _streamController =
|
||||||
|
|
@ -10,8 +15,52 @@ class PreviewPackHolder {
|
||||||
|
|
||||||
PreviewPackHolder();
|
PreviewPackHolder();
|
||||||
|
|
||||||
void setPreviewPacks(List<CardPackPreviewDto> packs) {
|
Future<void> init() async {
|
||||||
|
if (_state == null) {
|
||||||
|
await loadFromCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> loadFromCache() async {
|
||||||
|
final sp = globalSharedPreferences;
|
||||||
|
final previewPacksStrings = sp.getStringList('preview_packs');
|
||||||
|
if (previewPacksStrings == null || previewPacksStrings.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final List<CardPackPreviewDto> previewPacks = [];
|
||||||
|
previewPacksStrings.forEach((string) {
|
||||||
|
try {
|
||||||
|
final pack = CardPackPreviewDto.fromJson(
|
||||||
|
jsonDecode(string) as Map<String, Object?>,
|
||||||
|
);
|
||||||
|
previewPacks.add(pack);
|
||||||
|
} catch (e, s) {
|
||||||
|
log('Cant load preview pack', error: e, stackTrace: s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setPreviewPacks(previewPacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> savePreviewPacks() async {
|
||||||
|
final packs = state?.previewPacks ?? [];
|
||||||
|
final stringsList = <String>[];
|
||||||
|
packs.forEach((pack) {
|
||||||
|
try {
|
||||||
|
final string = jsonEncode(pack);
|
||||||
|
stringsList.add(string);
|
||||||
|
} catch (e, s) {
|
||||||
|
log('Cant load preview pack', error: e, stackTrace: s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final sp = globalSharedPreferences;
|
||||||
|
sp.setStringList('preview_packs', stringsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPreviewPacks(List<CardPackPreviewDto> packs, {bool save = false}) {
|
||||||
_state = PreviewPackState(packs);
|
_state = PreviewPackState(packs);
|
||||||
|
if (save) {
|
||||||
|
savePreviewPacks();
|
||||||
|
}
|
||||||
_streamController.add(_state);
|
_streamController.add(_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ class PreviewPacksPoller {
|
||||||
try {
|
try {
|
||||||
polling = true;
|
polling = true;
|
||||||
final r = await _api.getPacksPreviews(null);
|
final r = await _api.getPacksPreviews(null);
|
||||||
_previewPackHolder.setPreviewPacks(r);
|
_previewPackHolder.setPreviewPacks(r, save: true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'package:webview_flutter/webview_flutter.dart';
|
||||||
import '../../di/injector.dart';
|
import '../../di/injector.dart';
|
||||||
import '../../di/locator.dart';
|
import '../../di/locator.dart';
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
|
import '../analytics/analytics.dart';
|
||||||
|
|
||||||
enum StoreItemType {
|
enum StoreItemType {
|
||||||
ads,
|
ads,
|
||||||
|
|
@ -25,15 +26,34 @@ class InAppPurchaseService {
|
||||||
InAppPurchase get instance => _inAppPurchase;
|
InAppPurchase get instance => _inAppPurchase;
|
||||||
|
|
||||||
Future<bool> buyPack(CardPackBuyDto dto) async {
|
Future<bool> buyPack(CardPackBuyDto dto) async {
|
||||||
|
try {
|
||||||
final pack = await getPackStoreProduct(dto);
|
final pack = await getPackStoreProduct(dto);
|
||||||
final result = await buyItemInStore(pack.first);
|
final result = await buyItemInStore(pack.first);
|
||||||
return result;
|
return result;
|
||||||
|
} catch (e, s) {
|
||||||
|
log('err', error: e, stackTrace: s);
|
||||||
|
Analytics.buyError(
|
||||||
|
dto,
|
||||||
|
PaymentSystem.google,
|
||||||
|
data: {
|
||||||
|
'e': e.toString(),
|
||||||
|
's': s.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> buyPackWithYooMoney(CardPackBuyDto dto, BuildContext? context) async {
|
Future<bool> buyPackWithYooMoney(
|
||||||
|
CardPackBuyDto dto, BuildContext? context) async {
|
||||||
final paymentUrl =
|
final paymentUrl =
|
||||||
await locator.repository.createPayment(dto.id, PaymentSystem.yookassa);
|
await locator.repository.createPayment(dto.id, PaymentSystem.yookassa);
|
||||||
if (paymentUrl == null) {
|
if (paymentUrl == null) {
|
||||||
|
Analytics.buyError(
|
||||||
|
dto,
|
||||||
|
PaymentSystem.yookassa,
|
||||||
|
message: 'payment url is null',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final controller = WebViewController()
|
final controller = WebViewController()
|
||||||
|
|
@ -57,9 +77,18 @@ class InAppPurchaseService {
|
||||||
).then((_) async {
|
).then((_) async {
|
||||||
final success = await locator.repository.checkUserPayment();
|
final success = await locator.repository.checkUserPayment();
|
||||||
if (success) {
|
if (success) {
|
||||||
|
Analytics.buyComplete(
|
||||||
|
dto,
|
||||||
|
PaymentSystem.yookassa,
|
||||||
|
);
|
||||||
locator.packUpdater.updateAvailablePacks();
|
locator.packUpdater.updateAvailablePacks();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
Analytics.buyError(
|
||||||
|
dto,
|
||||||
|
PaymentSystem.yookassa,
|
||||||
|
message: 'payment not verified',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -67,6 +96,11 @@ class InAppPurchaseService {
|
||||||
Future<List<ProductDetails>> getPackStoreProduct(CardPackBuyDto dto) async {
|
Future<List<ProductDetails>> getPackStoreProduct(CardPackBuyDto dto) async {
|
||||||
final bool isAvailable = await _inAppPurchase.isAvailable();
|
final bool isAvailable = await _inAppPurchase.isAvailable();
|
||||||
if (!isAvailable) {
|
if (!isAvailable) {
|
||||||
|
Analytics.buyError(
|
||||||
|
dto,
|
||||||
|
PaymentSystem.google,
|
||||||
|
message: 'in app purchase not available',
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
final ids = {
|
final ids = {
|
||||||
|
|
@ -74,6 +108,11 @@ class InAppPurchaseService {
|
||||||
if (Platform.isIOS) dto.appStoreId,
|
if (Platform.isIOS) dto.appStoreId,
|
||||||
}.whereNotNull().toSet();
|
}.whereNotNull().toSet();
|
||||||
if (ids.isEmpty) {
|
if (ids.isEmpty) {
|
||||||
|
Analytics.buyError(
|
||||||
|
dto,
|
||||||
|
PaymentSystem.google,
|
||||||
|
message: 'product id not set',
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,6 +121,14 @@ class InAppPurchaseService {
|
||||||
|
|
||||||
if (productDetailResponse.error != null ||
|
if (productDetailResponse.error != null ||
|
||||||
productDetailResponse.productDetails.isEmpty) {
|
productDetailResponse.productDetails.isEmpty) {
|
||||||
|
Analytics.buyError(
|
||||||
|
dto,
|
||||||
|
PaymentSystem.google,
|
||||||
|
data: {
|
||||||
|
'msg': 'cant get product details ${productDetailResponse.error}',
|
||||||
|
'not_found': productDetailResponse.notFoundIDs.join(','),
|
||||||
|
},
|
||||||
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,14 +151,34 @@ class PurchaseDetailsStreamSubscription {
|
||||||
|
|
||||||
Future<void> onPurchased(PurchaseDetails purchaseDetails) async {
|
Future<void> onPurchased(PurchaseDetails purchaseDetails) async {
|
||||||
try {
|
try {
|
||||||
|
Analytics.buyEvent(
|
||||||
|
null,
|
||||||
|
PaymentSystem.google,
|
||||||
|
message: 'onPurchased',
|
||||||
|
data: {
|
||||||
|
'status': purchaseDetails.status.name,
|
||||||
|
'product': purchaseDetails.productID,
|
||||||
|
'date': purchaseDetails.transactionDate,
|
||||||
|
},
|
||||||
|
);
|
||||||
final result = await locator.repository.checkPayment(
|
final result = await locator.repository.checkPayment(
|
||||||
purchaseDetails.productID,
|
purchaseDetails.productID,
|
||||||
purchaseDetails.verificationData.serverVerificationData,
|
purchaseDetails.verificationData.serverVerificationData,
|
||||||
PaymentSystem.google,
|
PaymentSystem.google,
|
||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
|
Analytics.buyComplete(
|
||||||
|
null,
|
||||||
|
PaymentSystem.google,
|
||||||
|
message: purchaseDetails.productID,
|
||||||
|
);
|
||||||
log('Purchased');
|
log('Purchased');
|
||||||
} else {
|
} else {
|
||||||
|
Analytics.buyError(
|
||||||
|
null,
|
||||||
|
PaymentSystem.yookassa,
|
||||||
|
message: 'payment not verified ${purchaseDetails.productID}',
|
||||||
|
);
|
||||||
throw Exception('Cant verify purchase');
|
throw Exception('Cant verify purchase');
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ part 'input_buttons_test_state.g.dart';
|
||||||
@CopyWith()
|
@CopyWith()
|
||||||
class InputButtonsTestState extends TestQuestionState {
|
class InputButtonsTestState extends TestQuestionState {
|
||||||
final List<String> answer;
|
final List<String> answer;
|
||||||
final List<int> answerIndexes;
|
final List<String> answerIds;
|
||||||
final bool isCorrect;
|
final bool isCorrect;
|
||||||
final bool isAnswered;
|
final bool isAnswered;
|
||||||
final int seed;
|
final int seed;
|
||||||
|
|
@ -19,7 +19,7 @@ class InputButtonsTestState extends TestQuestionState {
|
||||||
InputButtonsTestState({
|
InputButtonsTestState({
|
||||||
required this.seed,
|
required this.seed,
|
||||||
this.answer = const [],
|
this.answer = const [],
|
||||||
this.answerIndexes = const [],
|
this.answerIds = const [],
|
||||||
this.isCorrect = false,
|
this.isCorrect = false,
|
||||||
this.isAnswered = false,
|
this.isAnswered = false,
|
||||||
super.testType = TestQuestionType.simple,
|
super.testType = TestQuestionType.simple,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ abstract class _$InputButtonsTestStateCWProxy {
|
||||||
|
|
||||||
InputButtonsTestState answer(List<String> answer);
|
InputButtonsTestState answer(List<String> answer);
|
||||||
|
|
||||||
InputButtonsTestState answerIndexes(List<int> answerIndexes);
|
InputButtonsTestState answerIds(List<String> answerIds);
|
||||||
|
|
||||||
InputButtonsTestState isCorrect(bool isCorrect);
|
InputButtonsTestState isCorrect(bool isCorrect);
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ abstract class _$InputButtonsTestStateCWProxy {
|
||||||
InputButtonsTestState call({
|
InputButtonsTestState call({
|
||||||
int? seed,
|
int? seed,
|
||||||
List<String>? answer,
|
List<String>? answer,
|
||||||
List<int>? answerIndexes,
|
List<String>? answerIds,
|
||||||
bool? isCorrect,
|
bool? isCorrect,
|
||||||
bool? isAnswered,
|
bool? isAnswered,
|
||||||
TestQuestionType? testType,
|
TestQuestionType? testType,
|
||||||
|
|
@ -49,8 +49,8 @@ class _$InputButtonsTestStateCWProxyImpl
|
||||||
InputButtonsTestState answer(List<String> answer) => this(answer: answer);
|
InputButtonsTestState answer(List<String> answer) => this(answer: answer);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InputButtonsTestState answerIndexes(List<int> answerIndexes) =>
|
InputButtonsTestState answerIds(List<String> answerIds) =>
|
||||||
this(answerIndexes: answerIndexes);
|
this(answerIds: answerIds);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
InputButtonsTestState isCorrect(bool isCorrect) => this(isCorrect: isCorrect);
|
InputButtonsTestState isCorrect(bool isCorrect) => this(isCorrect: isCorrect);
|
||||||
|
|
@ -74,7 +74,7 @@ class _$InputButtonsTestStateCWProxyImpl
|
||||||
InputButtonsTestState call({
|
InputButtonsTestState call({
|
||||||
Object? seed = const $CopyWithPlaceholder(),
|
Object? seed = const $CopyWithPlaceholder(),
|
||||||
Object? answer = const $CopyWithPlaceholder(),
|
Object? answer = const $CopyWithPlaceholder(),
|
||||||
Object? answerIndexes = const $CopyWithPlaceholder(),
|
Object? answerIds = const $CopyWithPlaceholder(),
|
||||||
Object? isCorrect = const $CopyWithPlaceholder(),
|
Object? isCorrect = const $CopyWithPlaceholder(),
|
||||||
Object? isAnswered = const $CopyWithPlaceholder(),
|
Object? isAnswered = const $CopyWithPlaceholder(),
|
||||||
Object? testType = const $CopyWithPlaceholder(),
|
Object? testType = const $CopyWithPlaceholder(),
|
||||||
|
|
@ -88,11 +88,10 @@ class _$InputButtonsTestStateCWProxyImpl
|
||||||
? _value.answer
|
? _value.answer
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: answer as List<String>,
|
: answer as List<String>,
|
||||||
answerIndexes:
|
answerIds: answerIds == const $CopyWithPlaceholder() || answerIds == null
|
||||||
answerIndexes == const $CopyWithPlaceholder() || answerIndexes == null
|
? _value.answerIds
|
||||||
? _value.answerIndexes
|
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: answerIndexes as List<int>,
|
: answerIds as List<String>,
|
||||||
isCorrect: isCorrect == const $CopyWithPlaceholder() || isCorrect == null
|
isCorrect: isCorrect == const $CopyWithPlaceholder() || isCorrect == null
|
||||||
? _value.isCorrect
|
? _value.isCorrect
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
|
@ -129,8 +128,8 @@ InputButtonsTestState _$InputButtonsTestStateFromJson(
|
||||||
?.map((e) => e as String)
|
?.map((e) => e as String)
|
||||||
.toList() ??
|
.toList() ??
|
||||||
const [],
|
const [],
|
||||||
answerIndexes: (json['answerIndexes'] as List<dynamic>?)
|
answerIds: (json['answerIds'] as List<dynamic>?)
|
||||||
?.map((e) => (e as num).toInt())
|
?.map((e) => e as String)
|
||||||
.toList() ??
|
.toList() ??
|
||||||
const [],
|
const [],
|
||||||
isCorrect: json['isCorrect'] as bool? ?? false,
|
isCorrect: json['isCorrect'] as bool? ?? false,
|
||||||
|
|
@ -145,7 +144,7 @@ Map<String, dynamic> _$InputButtonsTestStateToJson(
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'testType': _$TestQuestionTypeEnumMap[instance.testType]!,
|
'testType': _$TestQuestionTypeEnumMap[instance.testType]!,
|
||||||
'answer': instance.answer,
|
'answer': instance.answer,
|
||||||
'answerIndexes': instance.answerIndexes,
|
'answerIds': instance.answerIds,
|
||||||
'isCorrect': instance.isCorrect,
|
'isCorrect': instance.isCorrect,
|
||||||
'isAnswered': instance.isAnswered,
|
'isAnswered': instance.isAnswered,
|
||||||
'seed': instance.seed,
|
'seed': instance.seed,
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ import 'package:flutter/widgets.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/theme/themes.dart';
|
import 'package:mnemo_cards/theme/themes.dart';
|
||||||
import 'package:mnemo_cards/utils/iterable_helper.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 '../../managers/repository/http_repository.dart';
|
import '../../managers/repository/http_repository.dart';
|
||||||
import 'question_states/test_question_state.dart';
|
import 'question_states/test_question_state.dart';
|
||||||
|
|
@ -28,6 +30,18 @@ class TestManager extends ChangeNotifier {
|
||||||
return tests;
|
return tests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<TestDto>> loadTestsStream({String? packId}) {
|
||||||
|
return Stream.periodic(
|
||||||
|
Duration(seconds: 60),
|
||||||
|
)
|
||||||
|
.startWith(null)
|
||||||
|
.asyncMap(
|
||||||
|
(tests) async => loadTests(packId: packId)
|
||||||
|
.catchError((_) => Future.value(<TestDto>[])),
|
||||||
|
)
|
||||||
|
.where((test) => test.isNotEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
TestQuestionState? getState(int id) => testHolder.testQuestionState(id);
|
TestQuestionState? getState(int id) => testHolder.testQuestionState(id);
|
||||||
|
|
||||||
Iterable<TestQuestionState> get states => testHolder.state.questions.values;
|
Iterable<TestQuestionState> get states => testHolder.state.questions.values;
|
||||||
|
|
@ -37,7 +51,15 @@ class TestManager extends ChangeNotifier {
|
||||||
if (packId != null) {
|
if (packId != null) {
|
||||||
await packCacheManager.loadPackFromCache(packId);
|
await packCacheManager.loadPackFromCache(packId);
|
||||||
}
|
}
|
||||||
final test = await _repository.getTest(id);
|
TestDto? test;
|
||||||
|
try {
|
||||||
|
test = await _repository.getTest(id);
|
||||||
|
} on Object catch (e, s) {
|
||||||
|
log('error when loading test ${id}');
|
||||||
|
}
|
||||||
|
if (test == null) {
|
||||||
|
appRouter.maybePop();
|
||||||
|
}
|
||||||
questions.clear();
|
questions.clear();
|
||||||
questions.addAll(test!.questions);
|
questions.addAll(test!.questions);
|
||||||
color = test.color?.asColor ?? borderGray;
|
color = test.color?.asColor ?? borderGray;
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ class _TestPageState extends State<TestPage> {
|
||||||
if (!s.hasData || s.requireData!.questions.isEmpty) {
|
if (!s.hasData || s.requireData!.questions.isEmpty) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Text('Loading'),
|
child: Text('Загружаю тест...'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,106 +1,64 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:mnemo_cards/features/tests/test_manager.dart';
|
import 'package:mnemo_cards/features/tests/test_manager.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/theme/themes.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 '../../../di/locator.dart';
|
import '../../../di/locator.dart';
|
||||||
import '../../../widgets/game_card_widget.dart';
|
import '../../../widgets/game_card_widget.dart';
|
||||||
import '../question_states/input_buttons_test_state.dart';
|
import '../question_states/input_buttons_test_state.dart';
|
||||||
import 'test_button.dart';
|
import 'test_button.dart';
|
||||||
|
|
||||||
class InputButtonsTestWidget extends StatelessWidget {
|
class InputButtonsTestWidget extends StatelessWidget {
|
||||||
final InputButtonsTestQuestionBody model;
|
|
||||||
|
|
||||||
bool get isCorrect => state.answer.join() == model.answer;
|
bool get isCorrect => state.answer.join() == model.answer;
|
||||||
|
final InputButtonsTestQuestionBody _inputButtonModel;
|
||||||
|
|
||||||
|
InputButtonsTestQuestionBody get model => _inputButtonModel;
|
||||||
|
|
||||||
InputButtonsTestState get state =>
|
InputButtonsTestState get state =>
|
||||||
manager.getState(model.id!) as InputButtonsTestState;
|
manager.getState(model.id!) as InputButtonsTestState;
|
||||||
|
|
||||||
TestManager get manager => locator.testManager;
|
TestManager get manager => locator.testManager;
|
||||||
|
|
||||||
InputButtonsTestWidget(this.model, {super.key});
|
Color get testColor => manager.color;
|
||||||
|
|
||||||
Widget _letter(BuildContext context, String text) => Padding(
|
InputButtonsTestWidget(this._inputButtonModel, {super.key});
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: Material(
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
boxShadow: const [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black26,
|
|
||||||
blurRadius: 4.0,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _emptyCell(BuildContext context) => Padding(
|
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: Material(
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
|
||||||
boxShadow: const [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black26,
|
|
||||||
blurRadius: 4.0,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
' ',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context).textTheme;
|
final theme = Theme.of(context).textTheme;
|
||||||
final random = Random(state.seed);
|
// final random = Random(state.seed);
|
||||||
final buttons = model.buttons..shuffle(random);
|
final buttons = model.buttons; //..shuffle(random);
|
||||||
return LayoutBuilder(builder: (context, constraints) {
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Column(
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
if (model.image != null)
|
||||||
flex: 2,
|
Container(
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.all(4.0),
|
margin: const EdgeInsets.all(4.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius:
|
borderRadius:
|
||||||
BorderRadius.circular(GameCardWidget.borderRadius),
|
BorderRadius.circular(16.0),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (model.image != null)
|
AspectRatio(
|
||||||
TestImageWidget(
|
aspectRatio: 1,
|
||||||
|
child: TestImageWidget(
|
||||||
TestImage.image(model.image!),
|
TestImage.image(model.image!),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (model.text != null)
|
if (model.text != null)
|
||||||
Padding(
|
Container(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: EdgeInsets.only(top: 8.0),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
model.text!,
|
model.text!,
|
||||||
|
|
@ -110,20 +68,34 @@ class InputButtonsTestWidget extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
else Flexible(
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
model.text!,
|
||||||
|
style: theme.headlineLarge,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
StatefulBuilder(builder: (context, setState) {
|
StatefulBuilder(builder: (context, setState) {
|
||||||
void buttonTapped((int, TestButtonDto) p) {
|
void buttonTapped(int index, TestButtonDto button) {
|
||||||
if (state.answer.fold(0, (p, n) => p + n.length) >=
|
if (state.answer.fold(0, (p, n) => p + n.length) >=
|
||||||
model.answer.length) {
|
model.answer.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
|
var answer = [...state.answer, button.text!];
|
||||||
|
final ignoreSpaces = true;
|
||||||
|
if (ignoreSpaces &&
|
||||||
|
model.answer.length > answer.length &&
|
||||||
|
model.answer[answer.length] == ' ') {
|
||||||
|
answer.add(' ');
|
||||||
|
}
|
||||||
manager.setState(
|
manager.setState(
|
||||||
model.id!,
|
model.id!,
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
answer: [...state.answer, p.$2.text!],
|
answer: answer,
|
||||||
|
answerIds: [...state.answerIds, button.id],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
manager.setState(
|
manager.setState(
|
||||||
|
|
@ -143,35 +115,91 @@ class InputButtonsTestWidget extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int splitIndex = 0;
|
||||||
|
if (model.answer.length * 30 > constraints.maxWidth * 0.9) {
|
||||||
|
splitIndex = model.answer.indexOf(
|
||||||
|
' ',
|
||||||
|
(model.answer.length / 2).ceil(),
|
||||||
|
);
|
||||||
|
if (splitIndex < 10) {
|
||||||
|
splitIndex = model.answer.indexOf(
|
||||||
|
' ',
|
||||||
|
(model.answer.length / 3).ceil(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (splitIndex == -1) {
|
||||||
|
splitIndex = model.answer.indexOf(
|
||||||
|
' ',
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (splitIndex < 0) {
|
||||||
|
splitIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final letters = model.answer
|
||||||
|
.split('')
|
||||||
|
.mapIndexed(
|
||||||
|
(index, letter) => letter == ' '
|
||||||
|
? SizedBox(width: 10)
|
||||||
|
: _Letter(
|
||||||
|
text: state.answer.length > index
|
||||||
|
? state.answer[index]
|
||||||
|
: ' ',
|
||||||
|
color: testColor,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
final rows = [
|
||||||
|
if (splitIndex > 0)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: letters.sublist(0, splitIndex),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: letters.sublist(splitIndex),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
var answer = state.answer.toList();
|
||||||
|
var answerIds = state.answerIds.toList();
|
||||||
|
if (answer.isNotEmpty) {
|
||||||
|
if (answer.last == ' ') {
|
||||||
|
answer.removeLast();
|
||||||
|
}
|
||||||
|
if (answer.isNotEmpty) {
|
||||||
|
answer.removeLast();
|
||||||
|
answerIds.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
manager.setState(
|
manager.setState(
|
||||||
model.id!,
|
model.id!,
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
isAnswered: false,
|
// isAnswered: false,
|
||||||
isCorrect: false,
|
// isCorrect: false,
|
||||||
answer: [],
|
answer: answer,
|
||||||
answerIndexes: [],
|
answerIds: answerIds,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
...state.answer.map((e) => _letter(context, e)),
|
children: rows,
|
||||||
...model.answer
|
|
||||||
.substring(
|
|
||||||
state.answer.fold(0, (p, n) => p + n.length),
|
|
||||||
)
|
|
||||||
.characters
|
|
||||||
.map((e) => _emptyCell(context)),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -181,17 +209,20 @@ class InputButtonsTestWidget extends StatelessWidget {
|
||||||
AbsorbPointer(
|
AbsorbPointer(
|
||||||
absorbing: state.isCorrect,
|
absorbing: state.isCorrect,
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 8.0,
|
|
||||||
runSpacing: 8.0,
|
|
||||||
children: [
|
children: [
|
||||||
...buttons.indexed.map(
|
...buttons.mapIndexed(
|
||||||
(p) => Container(
|
(index, button) => Container(
|
||||||
width: p.$2.text!.length > 3 ? null : 80,
|
margin: EdgeInsets.all(4.0),
|
||||||
height: 60,
|
width: button.text!.length > 3
|
||||||
child: TestButton(
|
? null
|
||||||
text: p.$2.text,
|
: constraints.maxWidth / 6,
|
||||||
onTap: () => buttonTapped(p),
|
height: 50,
|
||||||
color: state.answer == p.$2
|
child: state.answerIds.contains(button.id)
|
||||||
|
? SizedBox.shrink()
|
||||||
|
: TestButton(
|
||||||
|
text: button.text,
|
||||||
|
onTap: () => buttonTapped(index, button),
|
||||||
|
color: state.answer == button
|
||||||
? isCorrect
|
? isCorrect
|
||||||
? Colors.green[200]
|
? Colors.green[200]
|
||||||
: Colors.red[200]
|
: Colors.red[200]
|
||||||
|
|
@ -207,8 +238,45 @@ class InputButtonsTestWidget extends StatelessWidget {
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Letter extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
_Letter({this.text = '', this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: 30,
|
||||||
|
height: 45,
|
||||||
|
margin: const EdgeInsets.all(2.0),
|
||||||
|
padding: const EdgeInsets.all(5.0),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(6.0),
|
||||||
|
border: Border.all(color: color ?? borderGray),
|
||||||
|
boxShadow: const [
|
||||||
|
// BoxShadow(
|
||||||
|
// color: Colors.black26,
|
||||||
|
// blurRadius: 4.0,
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.white,
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
const ADMIN_BUILD = true;
|
const ADMIN_BUILD = false;
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
|
@ -5,7 +6,9 @@ import 'package:auto_route/annotations.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:jailbreak_root_detection/jailbreak_root_detection.dart';
|
import 'package:jailbreak_root_detection/jailbreak_root_detection.dart';
|
||||||
import 'package:mnemo_cards/di/locator.dart';
|
import 'package:mnemo_cards/di/locator.dart';
|
||||||
|
|
@ -26,7 +29,7 @@ import 'managers/repository/repository.dart';
|
||||||
|
|
||||||
final scaffoldKey = GlobalKey<ScaffoldState>();
|
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||||
SharedPreferences? globalSharedPreferences;
|
late final SharedPreferences globalSharedPreferences;
|
||||||
|
|
||||||
final appRouter = AppRouter(ADMIN_BUILD);
|
final appRouter = AppRouter(ADMIN_BUILD);
|
||||||
|
|
||||||
|
|
@ -37,28 +40,49 @@ Future<bool> _screenOff() async =>
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
|
print('ensureInitialized ${stopwatch.elapsedMilliseconds}');
|
||||||
|
SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.portraitUp,
|
||||||
|
]);
|
||||||
androidDeviceInfo = await DeviceInfoPlugin().androidInfo;
|
androidDeviceInfo = await DeviceInfoPlugin().androidInfo;
|
||||||
|
print('deviceInfo ${stopwatch.elapsedMilliseconds}');
|
||||||
final isNotTrust = await JailbreakRootDetection.instance.isNotTrust;
|
final isNotTrust = await JailbreakRootDetection.instance.isNotTrust;
|
||||||
|
print('No trunst ${stopwatch.elapsedMilliseconds}');
|
||||||
if (isNotTrust && !ADMIN_BUILD) {
|
if (isNotTrust && !ADMIN_BUILD) {
|
||||||
print('Not trusted');
|
print('Not trusted');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final off = await _screenOff();
|
final off = await _screenOff();
|
||||||
|
print('screenshot off ${stopwatch.elapsedMilliseconds}');
|
||||||
if (!off) {
|
if (!off) {
|
||||||
print('Cant disable screenhsot');
|
print('Cant disable screenhsot');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FlutterError.onError = (errorDetails) {
|
||||||
|
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
globalSharedPreferences = await SharedPreferences.getInstance();
|
globalSharedPreferences = await SharedPreferences.getInstance();
|
||||||
|
print('global sp ${stopwatch.elapsedMilliseconds}');
|
||||||
await setInjections();
|
await setInjections();
|
||||||
await initFirebase();
|
print('injections ${stopwatch.elapsedMilliseconds}');
|
||||||
|
unawaited(initFirebase());
|
||||||
|
print('initing... ${stopwatch.elapsedMilliseconds}');
|
||||||
await locator.packCacheManager.init();
|
await locator.packCacheManager.init();
|
||||||
|
await locator.previewPackHolder.init();
|
||||||
|
await locator.previewPackPoller.init();
|
||||||
await locator.packManager.init();
|
await locator.packManager.init();
|
||||||
await locator.userManager.init();
|
await locator.userManager.init();
|
||||||
await locator.favoriteCardsController.init();
|
await locator.favoriteCardsController.init();
|
||||||
await locator.previewPackPoller.init();
|
await locator.previewPackPoller.init();
|
||||||
await locator.packUpdater.init();
|
await locator.packUpdater.init();
|
||||||
|
print('initing complete ${stopwatch.elapsedMilliseconds}');
|
||||||
await MobileAds.initialize();
|
await MobileAds.initialize();
|
||||||
|
print('mobile ads ${stopwatch.elapsedMilliseconds}');
|
||||||
|
print('runApp ${stopwatch.elapsedMilliseconds}');
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
} on Object catch (e, s) {
|
} on Object catch (e, s) {
|
||||||
log(e.toString(), stackTrace: s);
|
log(e.toString(), stackTrace: s);
|
||||||
|
|
@ -70,7 +94,7 @@ Future<void> initFirebase() async {
|
||||||
try {
|
try {
|
||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
options: DefaultFirebaseOptions.currentPlatform,
|
options: DefaultFirebaseOptions.currentPlatform,
|
||||||
).timeout(Duration(seconds: 2));
|
);
|
||||||
await FirebaseRepository.update();
|
await FirebaseRepository.update();
|
||||||
} on Object catch (e, s) {
|
} on Object catch (e, s) {
|
||||||
log('Firebase not inited', error: e, stackTrace: s);
|
log('Firebase not inited', error: e, stackTrace: s);
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ class FavoriteCardsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> get state {
|
List<String> get state {
|
||||||
state = (_sharedPreferences.getStringList('favorites') ?? []);
|
_state = (_sharedPreferences.getStringList('favorites') ?? []);
|
||||||
return _state;
|
return _state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:mnemo_cards/flags.dart';
|
||||||
|
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
import 'firebase_config_repository.dart';
|
import 'firebase_config_repository.dart';
|
||||||
|
|
||||||
mixin Api {
|
mixin Api {
|
||||||
String get path =>
|
String get path =>
|
||||||
kReleaseMode || (globalSharedPreferences?.getBool('env') ?? true)
|
!ADMIN_BUILD || (globalSharedPreferences.getBool('env') ?? true)
|
||||||
//prod
|
//prod
|
||||||
? 'https://89.19.210.178:8080' //FirebaseRepository.appConfig.path
|
? 'https://89.19.210.178:8080' //FirebaseRepository.appConfig.path
|
||||||
: androidDeviceInfo!.isPhysicalDevice
|
: androidDeviceInfo!.isPhysicalDevice
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
|
||||||
import '../domain/router/app_router.dart';
|
import '../domain/router/app_router.dart';
|
||||||
import '../domain/router/app_router.gr.dart';
|
import '../domain/router/app_router.gr.dart';
|
||||||
|
import '../features/analytics/analytics.dart';
|
||||||
import 'user_state_holder.dart';
|
import 'user_state_holder.dart';
|
||||||
|
|
||||||
class UserManager {
|
class UserManager {
|
||||||
|
|
@ -48,6 +49,7 @@ class UserManager {
|
||||||
if (await _googleSignIn.isSignedIn()) {
|
if (await _googleSignIn.isSignedIn()) {
|
||||||
await _googleSignIn.signOut();
|
await _googleSignIn.signOut();
|
||||||
}
|
}
|
||||||
|
Analytics.logout(userStateHolder.user);
|
||||||
userStateHolder.clear();
|
userStateHolder.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,9 +58,10 @@ class UserManager {
|
||||||
ExternalIdType idType,
|
ExternalIdType idType,
|
||||||
String? name,
|
String? name,
|
||||||
) async {
|
) async {
|
||||||
final p = await _repository.createOrGetUser(externalId, idType, name);
|
final (user, token) =
|
||||||
_setAuthToken(p.$2);
|
await _repository.createOrGetUser(externalId, idType, name);
|
||||||
userStateHolder.setUser(p.$1);
|
_setAuthToken(token);
|
||||||
|
userStateHolder.setUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setAuthToken(String authToken) async {
|
void _setAuthToken(String authToken) async {
|
||||||
|
|
@ -111,6 +114,7 @@ class UserManager {
|
||||||
final name = account.displayName;
|
final name = account.displayName;
|
||||||
final token =
|
final token =
|
||||||
await account.authentication.then((value) => value.idToken);
|
await account.authentication.then((value) => value.idToken);
|
||||||
|
Analytics.login(ExternalIdType.google);
|
||||||
await _createUser(token!, ExternalIdType.google, name);
|
await _createUser(token!, ExternalIdType.google, name);
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import 'package:flutter/widgets.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';
|
||||||
|
|
||||||
|
import '../features/analytics/analytics.dart';
|
||||||
|
|
||||||
|
|
||||||
class UserStateHolder extends ChangeNotifier {
|
class UserStateHolder extends ChangeNotifier {
|
||||||
UserDto? _dto;
|
UserDto? _dto;
|
||||||
|
|
@ -17,6 +19,8 @@ class UserStateHolder extends ChangeNotifier {
|
||||||
|
|
||||||
void setUser(UserDto dto) {
|
void setUser(UserDto dto) {
|
||||||
_dto = dto.copyWith();
|
_dto = dto.copyWith();
|
||||||
|
Analytics.setUser(dto);
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
_streamController.add(_dto);
|
_streamController.add(_dto);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../admin/all_cards.dart';
|
import '../admin/all_cards.dart';
|
||||||
import '../di/locator.dart';
|
import '../di/locator.dart';
|
||||||
|
import '../features/analytics/analytics.dart';
|
||||||
import '../theme/themes.dart';
|
import '../theme/themes.dart';
|
||||||
import '../widgets/shared_pref_button.dart';
|
import '../widgets/shared_pref_button.dart';
|
||||||
import '../widgets/text_button.dart';
|
import '../widgets/text_button.dart';
|
||||||
|
|
@ -21,6 +22,7 @@ import '../widgets/text_button.dart';
|
||||||
class ProfilePage extends StatelessWidget {
|
class ProfilePage extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
Analytics.profilePageOpened(locator.userManager.userStateHolder.user);
|
||||||
final theme = Theme.of(context).textTheme;
|
final theme = Theme.of(context).textTheme;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
|
@ -30,7 +32,7 @@ class ProfilePage extends StatelessWidget {
|
||||||
stream: locator.userManager.userStateHolder.asStream,
|
stream: locator.userManager.userStateHolder.asStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
var title = snapshot.data?.name;
|
var title = snapshot.data?.name;
|
||||||
if (title == null || title.length > 10) {
|
if (true || title == null || title.length > 10) {
|
||||||
title = 'Привет!';
|
title = 'Привет!';
|
||||||
}
|
}
|
||||||
return Header(
|
return Header(
|
||||||
|
|
@ -63,7 +65,8 @@ class ProfilePage extends StatelessWidget {
|
||||||
if (ADMIN_BUILD)
|
if (ADMIN_BUILD)
|
||||||
MaterialButton(
|
MaterialButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(context: context, builder: (c) => const AllCards());
|
showDialog(
|
||||||
|
context: context, builder: (c) => const AllCards());
|
||||||
},
|
},
|
||||||
child: Text('ALL CARDS'),
|
child: Text('ALL CARDS'),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,12 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:mnemo_cards/features/packs/images_holder.dart';
|
import 'package:mnemo_cards/features/packs/images_holder.dart';
|
||||||
import 'package:mnemo_cards/flags.dart';
|
|
||||||
import 'package:mnemo_cards/utils/color_helper.dart';
|
import 'package:mnemo_cards/utils/color_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';
|
||||||
|
|
||||||
import '../../admin/add_card.dart';
|
|
||||||
import '../../di/locator.dart';
|
|
||||||
import '../../domain/router/app_router.dart';
|
|
||||||
import '../../features/card_flipper/flip_card_controllers.dart';
|
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';
|
||||||
|
|
@ -22,14 +15,16 @@ import '../../main.dart';
|
||||||
import '../game_card_widget.dart';
|
import '../game_card_widget.dart';
|
||||||
|
|
||||||
class CardGameMainWidget extends StatelessWidget {
|
class CardGameMainWidget extends StatelessWidget {
|
||||||
final CardPackDto cardPack;
|
final List<GameCardDto> cards;
|
||||||
|
final Color? color;
|
||||||
final int? startCardId;
|
final int? startCardId;
|
||||||
final CardSwiperController cardSwiperController;
|
final CardSwiperController cardSwiperController;
|
||||||
final FlipCardController flipCardController;
|
final FlipCardController flipCardController;
|
||||||
final int length;
|
final int length;
|
||||||
|
|
||||||
const CardGameMainWidget(
|
const CardGameMainWidget(
|
||||||
this.cardPack,
|
this.cards,
|
||||||
|
this.color,
|
||||||
this.startCardId,
|
this.startCardId,
|
||||||
this.cardSwiperController,
|
this.cardSwiperController,
|
||||||
this.flipCardController,
|
this.flipCardController,
|
||||||
|
|
@ -38,23 +33,9 @@ class CardGameMainWidget extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final numberOfCardsDisplayed = min(2, cardPack.cards.length);
|
final numberOfCardsDisplayed = min(2, cards.length);
|
||||||
final initialScale = 0.9;
|
final initialScale = 0.9;
|
||||||
int lastThreshold = 0;
|
|
||||||
List<GameCardDto> cards = cardPack.cards;
|
|
||||||
if (startCardId != null) {
|
|
||||||
final startCardIndex = cards.indexWhere(
|
|
||||||
(card) => card.id == startCardId,
|
|
||||||
);
|
|
||||||
if (startCardIndex >= 0) {
|
|
||||||
cards = [
|
|
||||||
...cards.sublist(startCardIndex),
|
|
||||||
...cards.sublist(0, startCardIndex),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cards = cards.toList()..shuffle();
|
|
||||||
}
|
|
||||||
GameCardDto? activeCard() => cardSwiperController.currentIndex >= 0
|
GameCardDto? activeCard() => cardSwiperController.currentIndex >= 0
|
||||||
? cards[cardSwiperController.currentIndex % cards.length]
|
? cards[cardSwiperController.currentIndex % cards.length]
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -75,12 +56,7 @@ class CardGameMainWidget extends StatelessWidget {
|
||||||
return StatefulBuilder(builder: (context, setState) {
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
_EndWidget(cardSwiperController, flipCardController, () {
|
_EndWidget(),
|
||||||
setState(() {
|
|
||||||
enabled = false;
|
|
||||||
});
|
|
||||||
AutoRouter.of(context).maybePop();
|
|
||||||
}),
|
|
||||||
if (enabled)
|
if (enabled)
|
||||||
Hero(
|
Hero(
|
||||||
tag: activeCard()?.id ?? 'empty',
|
tag: activeCard()?.id ?? 'empty',
|
||||||
|
|
@ -127,21 +103,21 @@ class CardGameMainWidget extends StatelessWidget {
|
||||||
cardIndex);
|
cardIndex);
|
||||||
if (cardSwiperController.currentIndex ==
|
if (cardSwiperController.currentIndex ==
|
||||||
index) {
|
index) {
|
||||||
lastThreshold = percentThresholdX;
|
// lastThreshold = percentThresholdX;
|
||||||
}
|
}
|
||||||
print('${cards[index].translation} $scale');
|
print('${cards[index].translation} $scale');
|
||||||
final cardWidget = Center(
|
final cardWidget = Center(
|
||||||
child: GameCardWidget(
|
child: GameCardWidget(
|
||||||
cards[index],
|
cards[index],
|
||||||
cards[index].id.memoryImage,
|
cards[index].memoryImage,
|
||||||
flipCardController,
|
flipCardController,
|
||||||
flipCardController.state
|
flipCardController.state
|
||||||
// || cards[index].isAd
|
// || cards[index].isAd
|
||||||
,
|
,
|
||||||
isFirstCard,
|
isFirstCard,
|
||||||
isFirstCard
|
isFirstCard
|
||||||
? cardPack.color?.asColor
|
? color
|
||||||
: cardPack.color?.asColor?.darken(
|
: color?.darken(
|
||||||
(scale - 1) /
|
(scale - 1) /
|
||||||
(initialScale - 1) *
|
(initialScale - 1) *
|
||||||
0.075,
|
0.075,
|
||||||
|
|
@ -164,38 +140,10 @@ class CardGameMainWidget extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EndWidget extends StatelessWidget {
|
class _EndWidget extends StatelessWidget {
|
||||||
final CardSwiperController cardSwiperController;
|
_EndWidget();
|
||||||
final FlipCardController flipCardController;
|
|
||||||
final VoidCallback callback;
|
|
||||||
|
|
||||||
_EndWidget(
|
|
||||||
this.cardSwiperController,
|
|
||||||
this.flipCardController,
|
|
||||||
this.callback,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Center(
|
return Container();
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text('Ok: ${cardSwiperController.swipedLeftIds.length}'),
|
|
||||||
Text('Repeat: ${cardSwiperController.swipedRightIds.length}'),
|
|
||||||
MaterialButton(
|
|
||||||
onPressed: () {
|
|
||||||
cardSwiperController.restart();
|
|
||||||
},
|
|
||||||
child: Text('restart'),
|
|
||||||
),
|
|
||||||
MaterialButton(
|
|
||||||
onPressed: () {
|
|
||||||
callback();
|
|
||||||
},
|
|
||||||
child: Text('to pack'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,13 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/tests/progress_widget.dart';
|
import 'package:mnemo_cards/features/tests/progress_widget.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/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';
|
||||||
import '../../di/locator.dart';
|
import '../../di/locator.dart';
|
||||||
import '../../features/card_flipper/flip_card.dart';
|
|
||||||
import '../../features/card_flipper/flip_card_controllers.dart';
|
import '../../features/card_flipper/flip_card_controllers.dart';
|
||||||
import '../../features/card_swiper/flutter_card_swiper.dart';
|
import '../../features/card_swiper/flutter_card_swiper.dart';
|
||||||
import 'card_game_main.dart';
|
import 'card_game_main.dart';
|
||||||
|
|
@ -20,13 +15,17 @@ import 'card_game_main.dart';
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class CardGamePage extends StatefulWidget {
|
class CardGamePage extends StatefulWidget {
|
||||||
final int? startCardId;
|
final int? startCardId;
|
||||||
final CardPackDto cardPack;
|
final List<GameCardDto> cards;
|
||||||
|
final Color? color;
|
||||||
final bool infinite;
|
final bool infinite;
|
||||||
|
final bool shuffle;
|
||||||
|
|
||||||
const CardGamePage({
|
const CardGamePage({
|
||||||
required this.cardPack,
|
required this.cards,
|
||||||
|
required this.color,
|
||||||
this.startCardId,
|
this.startCardId,
|
||||||
this.infinite = false,
|
this.infinite = false,
|
||||||
|
this.shuffle = true,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -38,17 +37,17 @@ class _CardGamePageState extends State<CardGamePage> {
|
||||||
final CardSwiperController cardSwiperController = CardSwiperController();
|
final CardSwiperController cardSwiperController = CardSwiperController();
|
||||||
final FlipCardController flipCardController = FlipCardController();
|
final FlipCardController flipCardController = FlipCardController();
|
||||||
|
|
||||||
|
List<GameCardDto> shuffledCards = [];
|
||||||
|
|
||||||
GameCardDto? get activeCard {
|
GameCardDto? get activeCard {
|
||||||
final index = cardSwiperController.currentIndex;
|
final index = cardSwiperController.currentIndex;
|
||||||
final cards = widget.cardPack.cards;
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
return cards[index % cards.length];
|
return shuffledCards[index % shuffledCards.length];
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int get gameLength =>
|
int get gameLength => widget.infinite ? 10000000 : shuffledCards.length;
|
||||||
widget.infinite ? 10000000 : widget.cardPack.cards.length;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -95,9 +94,8 @@ class _CardGamePageState extends State<CardGamePage> {
|
||||||
cardSwiperController.progress,
|
cardSwiperController.progress,
|
||||||
Result.correct,
|
Result.correct,
|
||||||
),
|
),
|
||||||
widget.cardPack.cards.length,
|
shuffledCards.length,
|
||||||
widget.cardPack.color?.asColor ??
|
widget.color ?? borderGray,
|
||||||
borderGray,
|
|
||||||
timer: timer,
|
timer: timer,
|
||||||
popText: widget.infinite ? 'к теме' : null,
|
popText: widget.infinite ? 'к теме' : null,
|
||||||
key: ValueKey('CARDS PROGRESS'),
|
key: ValueKey('CARDS PROGRESS'),
|
||||||
|
|
@ -116,7 +114,8 @@ class _CardGamePageState extends State<CardGamePage> {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
CardGameMainWidget(
|
CardGameMainWidget(
|
||||||
widget.cardPack,
|
shuffledCards,
|
||||||
|
widget.color,
|
||||||
widget.startCardId,
|
widget.startCardId,
|
||||||
cardSwiperController,
|
cardSwiperController,
|
||||||
flipCardController,
|
flipCardController,
|
||||||
|
|
@ -186,7 +185,12 @@ class _CardGamePageState extends State<CardGamePage> {
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.all(10.0.w),
|
padding: EdgeInsets.all(10.0.w),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
"icons/heart.png",
|
locator.favoriteCardsController
|
||||||
|
.isFavoriteCard(
|
||||||
|
activeCard?.id,
|
||||||
|
)
|
||||||
|
? "icons/heart_fill.png"
|
||||||
|
: "icons/heart.png",
|
||||||
width: 29.w,
|
width: 29.w,
|
||||||
height: 29.h,
|
height: 29.h,
|
||||||
color: locator
|
color: locator
|
||||||
|
|
@ -249,6 +253,31 @@ class _CardGamePageState extends State<CardGamePage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
shuffledCards = widget.cards.toList();
|
||||||
|
if (widget.shuffle) {
|
||||||
|
shuffledCards..shuffle();
|
||||||
|
}
|
||||||
|
if (widget.startCardId != null) {
|
||||||
|
final startCardIndex = shuffledCards.indexWhere(
|
||||||
|
(card) => card.id == widget.startCardId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (startCardIndex >= 0) {
|
||||||
|
shuffledCards = [
|
||||||
|
...shuffledCards.sublist(startCardIndex),
|
||||||
|
...shuffledCards.sublist(0, startCardIndex),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
shuffledCards = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shuffledCards = widget.cards.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// cardSwiperController.removeListener(cardSwiperListener);
|
// cardSwiperController.removeListener(cardSwiperListener);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,20 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/animation.dart';
|
import 'package:flutter/animation.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.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/main.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_common/mnemo_cards_common.dart';
|
import 'package:mnemo_cards_common/mnemo_cards_common.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/yandex_ads/yandex_ads.dart';
|
import '../../features/yandex_ads/yandex_ads.dart';
|
||||||
import '../game_card_widget.dart';
|
import '../game_card_widget.dart';
|
||||||
|
|
||||||
|
|
@ -19,67 +23,113 @@ import '../../theme/themes.dart';
|
||||||
import 'card_pack_header.dart';
|
import 'card_pack_header.dart';
|
||||||
import 'package:mnemo_cards/utils/string_helper.dart';
|
import 'package:mnemo_cards/utils/string_helper.dart';
|
||||||
|
|
||||||
|
import 'cards_view.dart';
|
||||||
|
import 'cards_view_controller.dart';
|
||||||
|
|
||||||
|
final _mainScrollController = ScrollController();
|
||||||
|
|
||||||
class AvailablePack extends StatelessWidget {
|
class AvailablePack extends StatelessWidget {
|
||||||
final CardPackDto cardPack;
|
final CardPackDto cardPack;
|
||||||
final _CardsViewController _cardsViewController = _CardsViewController();
|
late final CardsViewController _cardsViewController =
|
||||||
|
CardsViewController(cardPack);
|
||||||
|
|
||||||
AvailablePack(this.cardPack);
|
AvailablePack(this.cardPack);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
Analytics.availablePackOpened(cardPack);
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
CardPackHeader.fromDto(cardPack),
|
CardPackHeader.fromDto(cardPack),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
controller: _mainScrollController,
|
||||||
children: [
|
children: [
|
||||||
StatefulBuilder(builder: (context, setState) {
|
StatefulBuilder(builder: (context, setState) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.symmetric(vertical: 0.h),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
color: cardPack.color?.asColor?.withOpacity(0.1),
|
||||||
bottom: BorderSide(
|
|
||||||
color: cardPack.color?.asColor ??
|
|
||||||
Colors.grey.withOpacity(0.5)),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: _CardsView(_cardsViewController, cardPack),
|
child: CardsView(_cardsViewController, cardPack),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: 1,
|
||||||
|
color: cardPack.color?.asColor ??
|
||||||
|
Colors.grey.withOpacity(0.5),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 10.h,
|
height: 10.h,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
vertical: 8.0.h,
|
|
||||||
horizontal: 10.w,
|
horizontal: 10.w,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
_Button('view.png', () async {
|
StreamBuilder(
|
||||||
_cardsViewController.toggleExpanded();
|
stream: _cardsViewController.asStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
return AnimatedRotation(
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
turns: (snapshot.data?.expanded ?? false)
|
||||||
|
? 0.5
|
||||||
|
: 0.0,
|
||||||
|
child: _Button(
|
||||||
|
'down.png',
|
||||||
|
() async {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
_cardsViewController.setExpanded(
|
||||||
|
!snapshot.requireData.expanded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
_Button('shuffle.png', () async {
|
_Button('shuffle.png', () async {
|
||||||
final index =
|
final cards = _cardsViewController.state.packCards;
|
||||||
Random().nextInt(cardPack.cards.length);
|
if (cards.isEmpty) return;
|
||||||
|
final index = (_cardsViewController.offsetCards +
|
||||||
|
Random().nextInt(
|
||||||
|
(cards.length / 2).ceil(),
|
||||||
|
) +
|
||||||
|
3) %
|
||||||
|
cards.length;
|
||||||
await _cardsViewController.animateTo(index);
|
await _cardsViewController.animateTo(index);
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
PageRouteInfo(
|
PageRouteInfo(
|
||||||
CardGamePage.name,
|
CardGamePage.name,
|
||||||
args: CardGamePageArgs(
|
args: CardGamePageArgs(
|
||||||
cardPack: cardPack,
|
cards: cards,
|
||||||
startCardId: cardPack.cards[index].id,
|
color: cardPack.color?.asColor,
|
||||||
|
startCardId: cards[index].id,
|
||||||
infinite: true,
|
infinite: true,
|
||||||
|
shuffle: true,
|
||||||
// id: cardPack.dto.id.toString(),
|
// id: cardPack.dto.id.toString(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
// _Button('chat.png', () {}),
|
// _Button('chat.png', () {}),
|
||||||
_Button('heart.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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -124,14 +174,17 @@ class _TestsSection extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder(
|
return StreamBuilder(
|
||||||
future: locator.testManager.loadTests(packId: packId),
|
stream: locator.testManager.loadTestsStream(packId: packId),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
if (snapshot.hasData)
|
if (snapshot.hasData)
|
||||||
...snapshot.data!.map(
|
...snapshot.data!.mapIndexed(
|
||||||
(dto) => _TestButton(
|
(int index, TestDto dto) => FadeInWidget(
|
||||||
|
delay: Duration(milliseconds: index * 200),
|
||||||
|
animationDuration: const Duration(milliseconds: 200),
|
||||||
|
child: _TestButton(
|
||||||
title: dto.name,
|
title: dto.name,
|
||||||
subtitle: null,
|
subtitle: null,
|
||||||
time: dto.time,
|
time: dto.time,
|
||||||
|
|
@ -144,6 +197,7 @@ class _TestsSection extends StatelessWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -152,12 +206,18 @@ class _TestsSection extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AdTile extends StatelessWidget {
|
class _AdTile extends StatelessWidget {
|
||||||
const _AdTile();
|
const _AdTile({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context).textTheme;
|
final theme = Theme.of(context).textTheme;
|
||||||
return Container(
|
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,
|
height: 80,
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -168,12 +228,11 @@ class _AdTile extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
child: AdWidget(bannerAd: banner),
|
||||||
return AdWidget(
|
),
|
||||||
bannerAd: YandexAds.createBanner(constraints)..loadAd());
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,10 +240,7 @@ class _Button extends StatelessWidget {
|
||||||
final String asset;
|
final String asset;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
_Button(
|
_Button(this.asset, this.onTap, {super.key});
|
||||||
this.asset,
|
|
||||||
this.onTap,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -305,206 +361,3 @@ class _TestButton extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,22 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:mnemo_cards/utils/string_helper.dart';
|
import 'package:mnemo_cards/utils/string_helper.dart';
|
||||||
import 'package:flutter/material.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_common/mnemo_cards_common.dart';
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
import '../../di/injector.dart';
|
||||||
|
import '../../features/analytics/analytics.dart';
|
||||||
|
import '../../features/packs/pack_holder.dart';
|
||||||
|
import '../../main.dart';
|
||||||
import '../items/builder.dart';
|
import '../items/builder.dart';
|
||||||
|
|
||||||
import '../../di/locator.dart';
|
import '../../di/locator.dart';
|
||||||
import '../../domain/router/app_router.dart';
|
import '../../domain/router/app_router.dart';
|
||||||
import '../../theme/themes.dart';
|
import '../../theme/themes.dart';
|
||||||
import 'card_pack_header.dart';
|
import 'card_pack_header.dart';
|
||||||
|
import 'loading_pack.dart';
|
||||||
|
|
||||||
class BuyPack extends StatelessWidget {
|
class BuyPack extends StatelessWidget {
|
||||||
final CardPackBuyDto dto;
|
final CardPackBuyDto dto;
|
||||||
|
|
@ -18,9 +25,15 @@ class BuyPack extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
bool loading = false;
|
||||||
|
Analytics.buyPackOpened(dto);
|
||||||
return Container(
|
return Container(
|
||||||
// color: dto.color?.asColor,
|
// color: dto.color?.asColor,
|
||||||
child: Column(
|
child: StatefulBuilder(builder: (context, setState) {
|
||||||
|
if (loading) {
|
||||||
|
return _Loading(dto);
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
CardPackHeader(dto.title, dto.subtitle, dto.color?.asColor),
|
CardPackHeader(dto.title, dto.subtitle, dto.color?.asColor),
|
||||||
|
|
@ -28,8 +41,8 @@ class BuyPack extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding:
|
padding: EdgeInsets.symmetric(
|
||||||
EdgeInsets.symmetric(horizontal: 10.0.w, vertical: 18.h),
|
horizontal: 10.0.w, vertical: 18.h),
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
final cardWidth = constraints.maxWidth / 3;
|
final cardWidth = constraints.maxWidth / 3;
|
||||||
final imageWidth = cardWidth * 0.9;
|
final imageWidth = cardWidth * 0.9;
|
||||||
|
|
@ -72,20 +85,75 @@ class BuyPack extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_BuyButton(dto),
|
_BuyButton(dto, (v) {
|
||||||
|
setState(() {
|
||||||
|
loading = v;
|
||||||
|
});
|
||||||
|
}),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 16.h,
|
height: 16.h,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Loading extends StatelessWidget {
|
||||||
|
final CardPackBuyDto dto;
|
||||||
|
|
||||||
|
_Loading(this.dto);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final memoryImage = locator.imagesHolder.get('pack_${dto.id}');
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
CardPackHeader(dto.title, dto.subtitle, dto.color?.asColor),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (memoryImage != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Image(
|
||||||
|
image: memoryImage,
|
||||||
|
width: 100,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
Text('Загрузка...'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BuyButton extends StatelessWidget {
|
class _BuyButton extends StatelessWidget {
|
||||||
final CardPackBuyDto dto;
|
final CardPackBuyDto dto;
|
||||||
|
final Function(bool) callback;
|
||||||
|
|
||||||
_BuyButton(this.dto);
|
_BuyButton(this.dto, this.callback);
|
||||||
|
|
||||||
|
Future<void> _updatePack() async {
|
||||||
|
try {
|
||||||
|
callback(true);
|
||||||
|
await locator.packUpdater.updatePackAndSaveImages(dto.id);
|
||||||
|
await locator.packCacheManager.loadPackFromCache(dto.id);
|
||||||
|
} catch (e, s) {
|
||||||
|
log(
|
||||||
|
'Updating from buy pack',
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
appRouter.maybePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -101,7 +169,11 @@ class _BuyButton extends StatelessWidget {
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (locator.userManager.hasUser) {
|
if (locator.userManager.hasUser) {
|
||||||
final result = locator.purchaseService.buyPack(dto);
|
Analytics.buyPackPressed(dto, PaymentSystem.google);
|
||||||
|
final result = await locator.purchaseService.buyPack(dto);
|
||||||
|
if (result) {
|
||||||
|
_updatePack();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
AppRouter.openAuth();
|
AppRouter.openAuth();
|
||||||
}
|
}
|
||||||
|
|
@ -145,11 +217,15 @@ class _BuyButton extends StatelessWidget {
|
||||||
TextButton(
|
TextButton(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
overlayColor:
|
overlayColor:
|
||||||
MaterialStateColor.resolveWith((states) => Colors.white),
|
WidgetStateColor.resolveWith((states) => Colors.white),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (locator.userManager.hasUser) {
|
if (locator.userManager.hasUser) {
|
||||||
await locator.purchaseService.buyPackWithYooMoney(dto, context);
|
Analytics.buyPackPressed(dto, PaymentSystem.yookassa);
|
||||||
|
if (await locator.purchaseService
|
||||||
|
.buyPackWithYooMoney(dto, context)) {
|
||||||
|
_updatePack();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
AppRouter.openAuth();
|
AppRouter.openAuth();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,22 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:auto_route/annotations.dart';
|
import 'package:auto_route/annotations.dart';
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
||||||
import 'package:mnemo_cards/domain/router/app_router.dart';
|
|
||||||
import 'package:mnemo_cards/domain/router/app_router.gr.dart';
|
|
||||||
import 'package:mnemo_cards/features/packs/images_holder.dart';
|
|
||||||
import 'package:mnemo_cards/features/yandex_ads/yandex_ads.dart';
|
|
||||||
import 'package:mnemo_cards/utils/iterable_helper.dart';
|
|
||||||
import 'package:mnemo_cards/utils/string_helper.dart';
|
|
||||||
import 'package:mnemo_cards/widgets/game_card_widget.dart';
|
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
import 'package:yandex_mobileads/mobile_ads.dart';
|
import '../../features/analytics/analytics.dart';
|
||||||
import '../../main.dart';
|
|
||||||
import '../items/builder.dart';
|
|
||||||
|
|
||||||
import '../../di/locator.dart';
|
import '../../di/locator.dart';
|
||||||
import '../../theme/themes.dart';
|
|
||||||
import '../header.dart';
|
|
||||||
import '../mnemo_text.dart';
|
|
||||||
import 'available_pack.dart';
|
import 'available_pack.dart';
|
||||||
import 'buy_pack_page.dart';
|
import 'buy_pack_page.dart';
|
||||||
import 'card_pack_header.dart';
|
import 'card_pack_header.dart';
|
||||||
|
import 'loading_pack.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class CardPackPage extends StatefulWidget {
|
class CardPackPage extends StatefulWidget {
|
||||||
final String cardPackId;
|
final CardPackPreviewDto previewDto;
|
||||||
|
|
||||||
const CardPackPage(this.cardPackId, {super.key});
|
const CardPackPage(this.previewDto, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _CardPackPage();
|
State<StatefulWidget> createState() => _CardPackPage();
|
||||||
|
|
@ -43,14 +28,27 @@ class _CardPackPage extends State<CardPackPage> {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: StreamBuilder<GetCardPackResponse>(
|
child: StreamBuilder<GetCardPackResponse>(
|
||||||
stream: locator.packManager.getCardPackStream(widget.cardPackId),
|
stream: locator.packManager.getCardPackStream(
|
||||||
|
widget.previewDto.id,
|
||||||
|
widget.previewDto.isAvailable,
|
||||||
|
),
|
||||||
initialData: null,
|
initialData: null,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) return _Loading();
|
if (!snapshot.hasData) return SizedBox.shrink();
|
||||||
|
if (snapshot.requireData.responseType ==
|
||||||
|
GetCardPackResponseType.loading)
|
||||||
|
return Loading(widget.previewDto);
|
||||||
final response = snapshot.requireData;
|
final response = snapshot.requireData;
|
||||||
if (response is CardPackBuyDto)
|
if (response is CardPackBuyDto)
|
||||||
return BuyPack(response);
|
return BuyPack(response);
|
||||||
else if (response is CardPackDto) return AvailablePack(response);
|
else if (response is CardPackDto) {
|
||||||
|
return AvailablePack(response);
|
||||||
|
}
|
||||||
|
Analytics.error(
|
||||||
|
data: {'response': jsonEncode(snapshot.data?.toJson() ?? '{}')},
|
||||||
|
message: 'cant load pack ${widget.previewDto.id}',
|
||||||
|
);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
CardPackHeader.fromDto(null),
|
CardPackHeader.fromDto(null),
|
||||||
|
|
@ -71,19 +69,3 @@ class _CardPackPage extends State<CardPackPage> {
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Loading extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
CardPackHeader.fromDto(null),
|
|
||||||
Expanded(
|
|
||||||
child: Center(
|
|
||||||
child: Text('Loading...'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
263
lib/widgets/card_pack/cards_view.dart
Normal file
263
lib/widgets/card_pack/cards_view.dart
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
import 'package:mnemo_cards/features/packs/images_holder.dart';
|
||||||
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
|
import '../../di/locator.dart';
|
||||||
|
import '../../utils/string_helper.dart';
|
||||||
|
import '../../domain/router/app_router.gr.dart';
|
||||||
|
import '../../main.dart';
|
||||||
|
import '../game_card_widget.dart';
|
||||||
|
import 'cards_view_controller.dart';
|
||||||
|
|
||||||
|
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;
|
||||||
|
var _subscription = CompositeSubscription();
|
||||||
|
|
||||||
|
late AnimationController _animationController;
|
||||||
|
|
||||||
|
bool expanded = false;
|
||||||
|
bool favorite = false;
|
||||||
|
|
||||||
|
final wrapKey = GlobalKey();
|
||||||
|
|
||||||
|
void _onStateChanged(CardsViewState state) async {
|
||||||
|
favorite = state.favorite;
|
||||||
|
if (state.expanded != expanded) {
|
||||||
|
expanded = state.expanded;
|
||||||
|
if (expanded) {
|
||||||
|
await controller.animateTo(0);
|
||||||
|
_animationController.animateTo(
|
||||||
|
1.0,
|
||||||
|
duration:
|
||||||
|
Duration(milliseconds: (100 * state.packCards.length / 3).ceil()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_animationController.animateBack(
|
||||||
|
0.0,
|
||||||
|
duration:
|
||||||
|
Duration(milliseconds: (100 * state.packCards.length / 3).ceil()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(CardsView oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_subscription.cancel();
|
||||||
|
_subscription = CompositeSubscription();
|
||||||
|
controller.asStream.listen(_onStateChanged).addTo(_subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
expanded = controller.state.expanded;
|
||||||
|
favorite = controller.state.favorite;
|
||||||
|
_animationController = AnimationController(vsync: this);
|
||||||
|
_subscription.cancel();
|
||||||
|
_subscription = CompositeSubscription();
|
||||||
|
controller.asStream.listen(_onStateChanged).addTo(_subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscription.cancel();
|
||||||
|
_animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onCardTap(List<GameCardDto> cards, GameCardDto card) async {
|
||||||
|
final playing = appRouter.push(
|
||||||
|
PageRouteInfo(
|
||||||
|
CardGamePage.name,
|
||||||
|
args: CardGamePageArgs(
|
||||||
|
cards: cards,
|
||||||
|
color: cardPack.color?.asColor,
|
||||||
|
startCardId: card.id,
|
||||||
|
infinite: true,
|
||||||
|
shuffle: false,
|
||||||
|
// id: cardPack.dto.id.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final id = await playing;
|
||||||
|
// if (id is int) {
|
||||||
|
// final index = cardPack.cards.indexWhere((c) => c.id == id);
|
||||||
|
// if (index >= 0) {
|
||||||
|
// print('jump to $index');
|
||||||
|
// controller.jumpTo(index);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cardWidth = MediaQuery.of(context).size.width / 3;
|
||||||
|
final imageWidth = cardWidth * 0.93;
|
||||||
|
|
||||||
|
return StreamBuilder(
|
||||||
|
stream: controller.cardsStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return SizedBox();
|
||||||
|
}
|
||||||
|
final packCards = snapshot.requireData;
|
||||||
|
|
||||||
|
if (packCards.isEmpty) {
|
||||||
|
return _NoCards(
|
||||||
|
favorite: favorite,
|
||||||
|
height: cardWidth * 4 / 3,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.imageWidth = imageWidth;
|
||||||
|
final animation = _animationController.view;
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: animation,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
SingleChildScrollView(
|
||||||
|
controller: controller.cardsScrollController,
|
||||||
|
scrollDirection:
|
||||||
|
animation.value > 0 ? Axis.vertical : Axis.horizontal,
|
||||||
|
physics: animation.value > 0
|
||||||
|
? NeverScrollableScrollPhysics()
|
||||||
|
: null,
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
height: cardWidth * 4 / 3 +
|
||||||
|
((packCards.length / 3).ceil() * 4 / 3 * cardWidth -
|
||||||
|
cardWidth * 4 / 3) *
|
||||||
|
animation.value,
|
||||||
|
child: Wrap(
|
||||||
|
key: wrapKey,
|
||||||
|
children: List.generate(packCards.length, (index) {
|
||||||
|
final card = packCards[index];
|
||||||
|
final memoryImage = card.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,
|
||||||
|
(card) => onCardTap(packCards, card),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CardBuilder extends StatelessWidget {
|
||||||
|
final GameCardDto card;
|
||||||
|
final CardPackDto cardPack;
|
||||||
|
final MemoryImage image;
|
||||||
|
final Function(GameCardDto) onTap;
|
||||||
|
|
||||||
|
_CardBuilder(
|
||||||
|
this.cardPack,
|
||||||
|
this.card,
|
||||||
|
this.image,
|
||||||
|
this.onTap,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
key: ObjectKey(card),
|
||||||
|
onTap: () => onTap(card),
|
||||||
|
child: Hero(
|
||||||
|
key: ValueKey('${card.id}'),
|
||||||
|
tag: card.id,
|
||||||
|
child: GameCardDecoration(
|
||||||
|
alpha: 0.0,
|
||||||
|
borderColor: cardPack.color?.asColor,
|
||||||
|
child: FrontCard(
|
||||||
|
card,
|
||||||
|
image,
|
||||||
|
alpha: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
createRectTween: (a, b) => RectTween(begin: a, end: b),
|
||||||
|
flightShuttleBuilder: (flightC, a, dir, fromC, toC) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: a,
|
||||||
|
builder: (context, child) => GameCardDecoration(
|
||||||
|
alpha: a.value,
|
||||||
|
child: FrontCard(
|
||||||
|
card,
|
||||||
|
card.memoryImage!,
|
||||||
|
alpha: a.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NoCards extends StatelessWidget {
|
||||||
|
final bool favorite;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
_NoCards({
|
||||||
|
required this.favorite,
|
||||||
|
required this.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: height,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
favorite ? 'Нет любимых карточек' : 'Пока нет карточек',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16.sp,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
126
lib/widgets/card_pack/cards_view_controller.dart
Normal file
126
lib/widgets/card_pack/cards_view_controller.dart
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:mnemo_cards/features/packs/images_holder.dart';
|
||||||
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
|
import '../../di/locator.dart';
|
||||||
|
|
||||||
|
class CardsViewController extends ChangeNotifier {
|
||||||
|
bool _expanded = false;
|
||||||
|
bool _favorite = false;
|
||||||
|
double imageWidth = 0.0;
|
||||||
|
List<GameCardDto> cards = [];
|
||||||
|
|
||||||
|
final CardPackDto cardPack;
|
||||||
|
|
||||||
|
final ScrollController _cardsScrollController =
|
||||||
|
ScrollController(keepScrollOffset: true);
|
||||||
|
|
||||||
|
ScrollController get cardsScrollController => _cardsScrollController;
|
||||||
|
|
||||||
|
StreamController<CardsViewState> _streamController =
|
||||||
|
StreamController.broadcast();
|
||||||
|
|
||||||
|
Stream<CardsViewState> get asStream =>
|
||||||
|
_streamController.stream.startWith(state).distinct();
|
||||||
|
|
||||||
|
Stream<bool> get expandedStream => asStream.map((s) => s.expanded).distinct();
|
||||||
|
|
||||||
|
Stream<bool> get favoriteStream => asStream.map((s) => s.favorite).distinct();
|
||||||
|
|
||||||
|
Stream<List<GameCardDto>> get cardsStream =>
|
||||||
|
asStream.map((s) => s.packCards).distinct((p, n) =>
|
||||||
|
(p.length == n.length) &&
|
||||||
|
p.indexed.every((p) => n[p.$1].id == p.$2.id));
|
||||||
|
|
||||||
|
CardsViewState get state => CardsViewState(
|
||||||
|
expanded: _expanded,
|
||||||
|
favorite: _favorite,
|
||||||
|
packCards: cards,
|
||||||
|
);
|
||||||
|
|
||||||
|
List<GameCardDto> _filterPackCards() => cardPack.cards
|
||||||
|
.where(
|
||||||
|
(card) =>
|
||||||
|
!_favorite ||
|
||||||
|
locator.favoriteCardsController.isFavoriteCard(card.id),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
void setExpanded(bool expanded) {
|
||||||
|
if (cards.isNotEmpty) {
|
||||||
|
_expanded = expanded;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFavorite(bool favorite) {
|
||||||
|
_favorite = favorite;
|
||||||
|
_updateCards();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addToStream() {
|
||||||
|
_streamController.add(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
double get offset => _cardsScrollController.offset;
|
||||||
|
|
||||||
|
int get offsetCards => (_cardsScrollController.offset / imageWidth).floor();
|
||||||
|
|
||||||
|
void toggleExpanded() => setExpanded(!_expanded);
|
||||||
|
|
||||||
|
CardsViewController(this.cardPack) {
|
||||||
|
addListener(_addToStream);
|
||||||
|
_updateCards();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateCards() {
|
||||||
|
cards = _filterPackCards();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> animateTo(int index) {
|
||||||
|
final millis =
|
||||||
|
((_cardsScrollController.offset / imageWidth - index).abs() * 30.0)
|
||||||
|
.ceil();
|
||||||
|
return _cardsScrollController.animateTo(
|
||||||
|
imageWidth * index,
|
||||||
|
duration: Duration(milliseconds: max(millis, 100)),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void jumpTo(int index) {
|
||||||
|
return _cardsScrollController.jumpTo(
|
||||||
|
imageWidth * index,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CardsViewState {
|
||||||
|
final bool expanded;
|
||||||
|
final bool favorite;
|
||||||
|
final List<GameCardDto> packCards;
|
||||||
|
|
||||||
|
CardsViewState({
|
||||||
|
required this.expanded,
|
||||||
|
required this.favorite,
|
||||||
|
required this.packCards,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => expanded.hashCode ^ expanded.hashCode ^ packCards.length;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return (other is CardsViewState) &&
|
||||||
|
expanded == other.expanded &&
|
||||||
|
favorite == other.favorite &&
|
||||||
|
(packCards.length == other.packCards.length) &&
|
||||||
|
packCards.indexed.every((p) => other.packCards[p.$1].id == p.$2.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
lib/widgets/card_pack/heart_button.dart
Normal file
10
lib/widgets/card_pack/heart_button.dart
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
class HeartButton extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// TODO: implement build
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
lib/widgets/card_pack/loading_pack.dart
Normal file
44
lib/widgets/card_pack/loading_pack.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
|
||||||
|
import '../../di/locator.dart';
|
||||||
|
import '../../utils/string_helper.dart';
|
||||||
|
import 'card_pack_header.dart';
|
||||||
|
|
||||||
|
class Loading extends StatelessWidget {
|
||||||
|
final CardPackPreviewDto previewDto;
|
||||||
|
|
||||||
|
Loading(this.previewDto);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final memoryImage = locator.imagesHolder.get('pack_${previewDto.id}');
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
CardPackHeader(
|
||||||
|
previewDto.title,
|
||||||
|
previewDto.subtitle,
|
||||||
|
previewDto.color?.asColor,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (memoryImage != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Image(
|
||||||
|
image: memoryImage,
|
||||||
|
width: 100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text('Загрузка...'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lib/widgets/fade_in_widget.dart
Normal file
29
lib/widgets/fade_in_widget.dart
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
class FadeInWidget extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
final Duration? delay;
|
||||||
|
final Future? future;
|
||||||
|
final Duration animationDuration;
|
||||||
|
|
||||||
|
FadeInWidget({
|
||||||
|
required this.child,
|
||||||
|
this.delay = const Duration(milliseconds: 200),
|
||||||
|
this.future,
|
||||||
|
this.animationDuration = const Duration(milliseconds: 200),
|
||||||
|
}) : assert(future != null || delay != null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder(
|
||||||
|
future: future ?? Future.delayed(delay!),
|
||||||
|
builder: (context, s) {
|
||||||
|
return AnimatedOpacity(
|
||||||
|
duration: animationDuration,
|
||||||
|
opacity: s.connectionState == ConnectionState.done ? 1.0 : 0.0,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,13 +47,23 @@ class GameCardWidget extends StatefulWidget {
|
||||||
class GameCardDecoration extends StatelessWidget {
|
class GameCardDecoration extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final double alpha;
|
final double alpha;
|
||||||
|
final Color? borderColor;
|
||||||
|
|
||||||
GameCardDecoration({required this.child, this.alpha = 1.0});
|
GameCardDecoration({
|
||||||
|
required this.child,
|
||||||
|
this.alpha = 1.0,
|
||||||
|
this.borderColor,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Container(
|
Widget build(BuildContext context) => Container(
|
||||||
padding: EdgeInsets.all(8.0 * alpha),
|
padding: EdgeInsets.all(8.0 * alpha),
|
||||||
margin: EdgeInsets.all(4.0 * alpha),
|
margin: EdgeInsets.only(
|
||||||
|
left: 10.0 * alpha,
|
||||||
|
right: 10.0 * alpha,
|
||||||
|
bottom: 10.0 * alpha,
|
||||||
|
top: 0.0 * alpha,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
|
|
@ -66,7 +76,7 @@ class GameCardDecoration extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
8 + (GameCardWidget.borderRadius - 8) * alpha),
|
8 + (GameCardWidget.borderRadius - 8) * alpha),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: borderGray.withOpacity(1.0 - alpha * alpha),
|
color: (borderColor ?? borderGray).withOpacity(1.0 - alpha * alpha),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
|
|
@ -160,7 +170,7 @@ class _BackWidget extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: MnemoText(
|
child: MnemoText(
|
||||||
card.back ?? card.translation ?? '',
|
card.back?.isNotEmpty == true ? card.back : card.translation ?? '',
|
||||||
textStyle: theme.displayMedium!.copyWith(color: Colors.white),
|
textStyle: theme.displayMedium!.copyWith(color: Colors.white),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
|
|
@ -199,8 +209,8 @@ class FrontCard extends StatelessWidget {
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding:
|
padding: EdgeInsets.all(
|
||||||
EdgeInsets.all(8.0 * imageScale * imageScale * alpha),
|
8.0 * imageScale * (alpha / 2 + 0.5)),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
mainAxisAlignment: ar > 1
|
mainAxisAlignment: ar > 1
|
||||||
|
|
@ -213,7 +223,7 @@ class FrontCard extends StatelessWidget {
|
||||||
child: MnemoText(
|
child: MnemoText(
|
||||||
card.original ?? '',
|
card.original ?? '',
|
||||||
textStyle: TextStyle(
|
textStyle: TextStyle(
|
||||||
fontSize: 27 * textScale,
|
fontSize: 15 * textScale + 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
|
@ -229,11 +239,9 @@ class FrontCard extends StatelessWidget {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (alpha > 0)
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final sp =
|
final sp = await SharedPreferences.getInstance();
|
||||||
await SharedPreferences.getInstance();
|
|
||||||
if (sp.getBool('sound_on') != false &&
|
if (sp.getBool('sound_on') != false &&
|
||||||
card.translation != null) {
|
card.translation != null) {
|
||||||
FlutterTts flutterTts = FlutterTts();
|
FlutterTts flutterTts = FlutterTts();
|
||||||
|
|
@ -247,13 +255,12 @@ class FrontCard extends StatelessWidget {
|
||||||
child: MnemoText(
|
child: MnemoText(
|
||||||
card.translation?.toLowerCase(),
|
card.translation?.toLowerCase(),
|
||||||
textStyle: TextStyle(
|
textStyle: TextStyle(
|
||||||
fontSize: 20 * textScale * alpha,
|
fontSize: 12 * textScale * alpha + 8,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
color: theme.headlineSmall!.color!
|
color: theme.headlineSmall!.color!
|
||||||
.withOpacity(0.5 * alpha),
|
.withOpacity(0.5),
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
// group: smallSize,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (alpha > 0 &&
|
if (alpha > 0 &&
|
||||||
|
|
@ -289,12 +296,17 @@ class FrontCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(
|
Container(
|
||||||
width: constrains.maxHeight / ar * imageScale,
|
width: constrains.maxHeight / ar * imageScale,
|
||||||
height: constrains.maxHeight / ar * imageScale,
|
height: constrains.maxHeight / ar * imageScale,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
GameCardWidget.borderRadius)),
|
||||||
child: ImageFade(
|
child: ImageFade(
|
||||||
key: ValueKey(card.id),
|
key: ValueKey(card.id),
|
||||||
image: image,
|
image: image,
|
||||||
|
// fix for elapsedInSeconds < 0 in debug mode
|
||||||
syncDuration: Duration.zero,
|
syncDuration: Duration.zero,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ typedef ImageFadeLoadingBuilder = Widget Function(
|
||||||
class ImageFade extends StatefulWidget {
|
class ImageFade extends StatefulWidget {
|
||||||
/// Creates a widget that displays a [placeholder] widget while a specified [image] loads,
|
/// Creates a widget that displays a [placeholder] widget while a specified [image] loads,
|
||||||
/// then cross-fades to the loaded image.
|
/// then cross-fades to the loaded image.
|
||||||
const ImageFade({
|
const
|
||||||
|
ImageFade({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.placeholder,
|
this.placeholder,
|
||||||
this.image,
|
this.image,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ class TextItemBuilder extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: item.hasBorder
|
border: item.hasBorder
|
||||||
? Border.all(
|
? Border.all(
|
||||||
|
|
@ -50,6 +51,7 @@ class TextItemBuilder extends StatelessWidget {
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
),
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
if (item.subtitle != null)
|
if (item.subtitle != null)
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,13 @@ class _PacksMenuWidgetState extends State<PacksMenuWidget> {
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: locator.previewPackPoller.poll,
|
onRefresh: locator.previewPackPoller.poll,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
color: snapshot.data?.firstOrNull?.color?.asColor ??
|
||||||
|
Colors.black,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
physics: BouncingScrollPhysics(
|
||||||
|
decelerationRate: ScrollDecelerationRate.fast,
|
||||||
|
),
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
children: [
|
children: [
|
||||||
if (snapshot.data == null)
|
if (snapshot.data == null)
|
||||||
|
|
@ -127,7 +133,9 @@ class _PacksMenuWidgetState extends State<PacksMenuWidget> {
|
||||||
if (pack.imageBase64 != null) {
|
if (pack.imageBase64 != null) {
|
||||||
try {
|
try {
|
||||||
final bytes = base64Decode(pack.imageBase64!);
|
final bytes = base64Decode(pack.imageBase64!);
|
||||||
await precacheImage(MemoryImage(bytes), context);
|
final image = MemoryImage(bytes);
|
||||||
|
locator.imagesHolder.setImage('pack_${pack.id}', image);
|
||||||
|
await precacheImage(image, context);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('${pack.id} cover not cached');
|
log('${pack.id} cover not cached');
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +162,9 @@ class _PackButton4 extends StatelessWidget {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
PageRouteInfo(
|
PageRouteInfo(
|
||||||
CardPackPage.name,
|
CardPackPage.name,
|
||||||
args: CardPackPageArgs(cardPackId: pack.id),
|
args: CardPackPageArgs(
|
||||||
|
previewDto: pack,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -183,7 +193,8 @@ class _PackButton4 extends StatelessWidget {
|
||||||
image: pack.imageBase64 == null
|
image: pack.imageBase64 == null
|
||||||
? null
|
? null
|
||||||
: DecorationImage(
|
: DecorationImage(
|
||||||
image: MemoryImage(
|
image: locator.imagesHolder.get('pack_${pack.id}') ??
|
||||||
|
MemoryImage(
|
||||||
base64Decode(pack.imageBase64!),
|
base64Decode(pack.imageBase64!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -276,7 +287,7 @@ class _PackButton4Shimmer extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Shimmer.fromColors(
|
return Shimmer.fromColors(
|
||||||
baseColor: Colors.white,
|
baseColor: Colors.white,
|
||||||
highlightColor: Colors.white,
|
highlightColor: borderGray,
|
||||||
child: Container(
|
child: Container(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
height: cardHeight,
|
height: cardHeight,
|
||||||
|
|
|
||||||
48
pubspec.lock
48
pubspec.lock
|
|
@ -13,10 +13,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _flutterfire_internals
|
name: _flutterfire_internals
|
||||||
sha256: "2350805d7afefb0efe7acd325cb19d3ae8ba4039b906eade3807ffb69938a01f"
|
sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.33"
|
version: "1.3.35"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -353,14 +353,38 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.0.3"
|
version: "8.0.3"
|
||||||
|
firebase_analytics:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: firebase_analytics
|
||||||
|
sha256: dbf1e7ab22cfb1f4a4adb103b46a26276b4edc593d4a78ef6fb942bafc92e035
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.10.7"
|
||||||
|
firebase_analytics_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_analytics_platform_interface
|
||||||
|
sha256: "3729b74f8cf1d974a27ba70332ecb55ff5ff560edc8164a6469f4a055b429c37"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.10.8"
|
||||||
|
firebase_analytics_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_analytics_web
|
||||||
|
sha256: "019cd7eee74254d33fbd2e29229367ce33063516bf6b3258a341d89e3b0f1655"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.7+7"
|
||||||
firebase_core:
|
firebase_core:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_core
|
name: firebase_core
|
||||||
sha256: "372d94ced114b9c40cb85e18c50ac94a7e998c8eec630c50d7aec047847d27bf"
|
sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.31.0"
|
version: "2.32.0"
|
||||||
firebase_core_platform_interface:
|
firebase_core_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -377,6 +401,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.17.0"
|
version: "2.17.0"
|
||||||
|
firebase_crashlytics:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: firebase_crashlytics
|
||||||
|
sha256: "9897c01efaa950d2f6da8317d12452749a74dc45f33b46390a14cfe28067f271"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.5.7"
|
||||||
|
firebase_crashlytics_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: firebase_crashlytics_platform_interface
|
||||||
|
sha256: "16a71e08fbf6e00382816e1b13397898c29a54fa0ad969c2c2a3b82a704877f0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.6.35"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,8 @@ dependencies:
|
||||||
device_info_plus: ^9.1.2
|
device_info_plus: ^9.1.2
|
||||||
package_info_plus: ^8.0.0
|
package_info_plus: ^8.0.0
|
||||||
yandex_mobileads: ^6.1.0
|
yandex_mobileads: ^6.1.0
|
||||||
|
firebase_crashlytics: ^3.5.7
|
||||||
|
firebase_analytics: ^10.10.7
|
||||||
in_app_purchase: ^3.2.0
|
in_app_purchase: ^3.2.0
|
||||||
dot_navigation_bar: ^1.0.2
|
dot_navigation_bar: ^1.0.2
|
||||||
story: ^1.1.0
|
story: ^1.1.0
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue