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 |
|
|
@ -79,7 +79,7 @@ class EditCard {
|
|||
final TextEditingController mnemoController =
|
||||
TextEditingController(text: gameCardDto.mnemo);
|
||||
final TextEditingController backController =
|
||||
TextEditingController(text: gameCardDto.back);
|
||||
TextEditingController(text: gameCardDto.back);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
|
|
@ -274,7 +274,9 @@ class EditCard {
|
|||
TextField(
|
||||
controller: backController,
|
||||
onChanged: (v) {
|
||||
gameCardDto = gameCardDto.copyWith(back: v);
|
||||
gameCardDto = gameCardDto.copyWith(
|
||||
back: v,
|
||||
);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Задняя сторона',
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ class AdminApi with Api {
|
|||
AdminApi(this._dio);
|
||||
|
||||
Future<bool> addCard(GameCardDto dto) async {
|
||||
final r = await _dio.post<String>('$path/cards/add',
|
||||
data: jsonEncode(dto.toJson()));
|
||||
final r = await _dio.post<String>(
|
||||
'$path/cards/add',
|
||||
data: jsonEncode(dto.toJson()),
|
||||
);
|
||||
return r.statusCode == 200;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ import '../managers/user_manager.dart';
|
|||
GetIt getIt = GetIt.I;
|
||||
|
||||
Future<void> setInjections() async {
|
||||
getIt.registerSingletonAsync<DioProvider>(() => DioProvider().init());
|
||||
getIt.registerSingletonAsync<DioProvider>(
|
||||
() => DioProvider().init(),
|
||||
);
|
||||
getIt.registerLazySingleton<HttpRepository>(
|
||||
() => HttpRepository(getIt.get<DioProvider>().dio),
|
||||
);
|
||||
|
|
@ -96,4 +98,5 @@ Future<void> setInjections() async {
|
|||
getIt.registerLazySingleton(
|
||||
() => PurchaseDetailsStreamSubscription(),
|
||||
);
|
||||
await getIt.allReady();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
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/managers/favorite_cards.dart';
|
||||
import 'package:mnemo_cards/managers/repository/http_repository.dart';
|
||||
|
|
@ -32,6 +33,8 @@ class Locator {
|
|||
|
||||
PackUpdater get packUpdater => _getIt.get<PackUpdater>();
|
||||
|
||||
PreviewPackHolder get previewPackHolder => _getIt.get<PreviewPackHolder>();
|
||||
|
||||
PreviewPacksPoller get previewPackPoller => _getIt.get<PreviewPacksPoller>();
|
||||
|
||||
TestManager get testManager => _getIt.get<TestManager>();
|
||||
|
|
|
|||
|
|
@ -8,8 +8,11 @@
|
|||
// coverage:ignore-file
|
||||
|
||||
// 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: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/main.dart' as _i6;
|
||||
import 'package:mnemo_cards/pages/auth_page.dart' as _i1;
|
||||
|
|
@ -36,9 +39,11 @@ abstract class $AppRouter extends _i9.RootStackRouter {
|
|||
return _i9.AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: _i2.CardGamePage(
|
||||
cardPack: args.cardPack,
|
||||
cards: args.cards,
|
||||
color: args.color,
|
||||
startCardId: args.startCardId,
|
||||
infinite: args.infinite,
|
||||
shuffle: args.shuffle,
|
||||
key: args.key,
|
||||
),
|
||||
);
|
||||
|
|
@ -48,7 +53,7 @@ abstract class $AppRouter extends _i9.RootStackRouter {
|
|||
return _i9.AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: _i3.CardPackPage(
|
||||
args.cardPackId,
|
||||
args.previewDto,
|
||||
key: args.key,
|
||||
),
|
||||
);
|
||||
|
|
@ -105,17 +110,21 @@ class AuthPage extends _i9.PageRouteInfo<void> {
|
|||
/// [_i2.CardGamePage]
|
||||
class CardGamePage extends _i9.PageRouteInfo<CardGamePageArgs> {
|
||||
CardGamePage({
|
||||
required _i10.CardPackDto cardPack,
|
||||
required List<_i10.GameCardDto> cards,
|
||||
required _i11.Color? color,
|
||||
int? startCardId,
|
||||
bool infinite = false,
|
||||
_i11.Key? key,
|
||||
bool shuffle = true,
|
||||
_i12.Key? key,
|
||||
List<_i9.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
CardGamePage.name,
|
||||
args: CardGamePageArgs(
|
||||
cardPack: cardPack,
|
||||
cards: cards,
|
||||
color: color,
|
||||
startCardId: startCardId,
|
||||
infinite: infinite,
|
||||
shuffle: shuffle,
|
||||
key: key,
|
||||
),
|
||||
initialChildren: children,
|
||||
|
|
@ -129,23 +138,29 @@ class CardGamePage extends _i9.PageRouteInfo<CardGamePageArgs> {
|
|||
|
||||
class CardGamePageArgs {
|
||||
const CardGamePageArgs({
|
||||
required this.cardPack,
|
||||
required this.cards,
|
||||
required this.color,
|
||||
this.startCardId,
|
||||
this.infinite = false,
|
||||
this.shuffle = true,
|
||||
this.key,
|
||||
});
|
||||
|
||||
final _i10.CardPackDto cardPack;
|
||||
final List<_i10.GameCardDto> cards;
|
||||
|
||||
final _i11.Color? color;
|
||||
|
||||
final int? startCardId;
|
||||
|
||||
final bool infinite;
|
||||
|
||||
final _i11.Key? key;
|
||||
final bool shuffle;
|
||||
|
||||
final _i12.Key? key;
|
||||
|
||||
@override
|
||||
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]
|
||||
class CardPackPage extends _i9.PageRouteInfo<CardPackPageArgs> {
|
||||
CardPackPage({
|
||||
required String cardPackId,
|
||||
_i11.Key? key,
|
||||
required _i10.CardPackPreviewDto previewDto,
|
||||
_i13.Key? key,
|
||||
List<_i9.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
CardPackPage.name,
|
||||
args: CardPackPageArgs(
|
||||
cardPackId: cardPackId,
|
||||
previewDto: previewDto,
|
||||
key: key,
|
||||
),
|
||||
initialChildren: children,
|
||||
|
|
@ -173,17 +188,17 @@ class CardPackPage extends _i9.PageRouteInfo<CardPackPageArgs> {
|
|||
|
||||
class CardPackPageArgs {
|
||||
const CardPackPageArgs({
|
||||
required this.cardPackId,
|
||||
required this.previewDto,
|
||||
this.key,
|
||||
});
|
||||
|
||||
final String cardPackId;
|
||||
final _i10.CardPackPreviewDto previewDto;
|
||||
|
||||
final _i11.Key? key;
|
||||
final _i13.Key? key;
|
||||
|
||||
@override
|
||||
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 {
|
||||
Map<String, MemoryImage> _images = {};
|
||||
Map<String, MemoryImage> _packImages = {};
|
||||
DateTime? unlockTime;
|
||||
|
||||
ImagesHolder();
|
||||
|
||||
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) {
|
||||
final memoryImage = _images[id];
|
||||
final memoryImage = getPackImage(id) ?? _images[id];
|
||||
|
||||
return memoryImage;
|
||||
}
|
||||
|
||||
MemoryImage? getPackImage(String id) {
|
||||
final memoryImage = _packImages[id];
|
||||
return memoryImage;
|
||||
}
|
||||
}
|
||||
|
||||
extension ImagesHolderStringExt on String {
|
||||
|
|
@ -30,7 +43,3 @@ extension ImagesHolderStringExt on String {
|
|||
extension ImagesHolderCardExt on GameCardDto {
|
||||
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');
|
||||
}
|
||||
|
||||
Future<void> savePack(CardPackDto dto,
|
||||
{Map<String, MemoryImage>? images}) async {
|
||||
Future<void> savePack(
|
||||
CardPackDto dto, {
|
||||
Map<String, MemoryImage>? images,
|
||||
}) async {
|
||||
log('Saving pack ${dto.id}');
|
||||
final dir = await _packDirectory(dto.id);
|
||||
final json = jsonEncode(dto);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'dart:async';
|
||||
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/packs_api.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/utils/iterable_helper.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
|
|
@ -44,24 +46,42 @@ class PackManager {
|
|||
// });
|
||||
}
|
||||
|
||||
Stream<GetCardPackResponse> getCardPackStream(String id) async* {
|
||||
try {
|
||||
yield await _cacheManager.loadPackFromCache(id);
|
||||
} catch (err) {
|
||||
log(err.toString());
|
||||
log('Cant load pack from cache, fetching pack');
|
||||
Stream<GetCardPackResponse> getCardPackStream(
|
||||
String id,
|
||||
bool isAvailable,
|
||||
) async* {
|
||||
if (isAvailable) {
|
||||
try {
|
||||
final dto = await _packsApi.getPackDto(id).catchError((_) => null);
|
||||
if (dto != null) {
|
||||
yield dto;
|
||||
} else {
|
||||
final buyDto = await _packsApi.getPackBuy(id);
|
||||
if (buyDto != null) {
|
||||
yield buyDto;
|
||||
}
|
||||
}
|
||||
yield await _cacheManager.loadPackFromCache(id);
|
||||
} catch (err) {
|
||||
log(err.toString());
|
||||
yield GetCardPackResponse.loading();
|
||||
log('Cant load pack from cache, fetching pack', error: err);
|
||||
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());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
yield GetCardPackResponse.loading();
|
||||
final buyDto = await _packsApi.getPackBuy(id).catchError((_) => null);
|
||||
if (buyDto != null) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
yield* _packHolder.asStream
|
||||
|
|
@ -73,6 +93,7 @@ class PackManager {
|
|||
Future<void> clearCache() async {
|
||||
_packHolder.clear();
|
||||
await _cacheManager.clearCache();
|
||||
await globalSharedPreferences.clear();
|
||||
}
|
||||
|
||||
Stream<List<CardPackDto>?> get packsStream =>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class PackUpdater {
|
|||
await updateAvailablePacks();
|
||||
try {
|
||||
final r = await _packsApi.getPacksPreviews(null);
|
||||
_previewPackHolder.setPreviewPacks(r);
|
||||
_previewPackHolder.setPreviewPacks(r, save: true);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ class PackUpdater {
|
|||
log('UPDATE ${action.id}');
|
||||
shouldUpdateHolder = true;
|
||||
try {
|
||||
final updatedPack = await _updatePackAndSaveImages(action.id);
|
||||
final updatedPack = await updatePackAndSaveImages(action.id);
|
||||
updatedPacks[action.id] = updatedPack;
|
||||
} catch (e) {
|
||||
log('Error while download cards for ${action.id} $e');
|
||||
|
|
@ -117,7 +117,7 @@ class PackUpdater {
|
|||
return false;
|
||||
}
|
||||
|
||||
Future<CardPackDto> _updatePackAndSaveImages(String packId) async {
|
||||
Future<CardPackDto> updatePackAndSaveImages(String packId) async {
|
||||
try {
|
||||
log('Updating with cards ${packId}', name: 'updatePackWithCards');
|
||||
final packDto = await _packsApi.getPackDto(packId);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
|
||||
class PreviewPackHolder {
|
||||
final StreamController<PreviewPackState?> _streamController =
|
||||
|
|
@ -10,8 +15,52 @@ class 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);
|
||||
if (save) {
|
||||
savePreviewPacks();
|
||||
}
|
||||
_streamController.add(_state);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class PreviewPacksPoller {
|
|||
try {
|
||||
polling = true;
|
||||
final r = await _api.getPacksPreviews(null);
|
||||
_previewPackHolder.setPreviewPacks(r);
|
||||
_previewPackHolder.setPreviewPacks(r, save: true);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:webview_flutter/webview_flutter.dart';
|
|||
import '../../di/injector.dart';
|
||||
import '../../di/locator.dart';
|
||||
import '../../main.dart';
|
||||
import '../analytics/analytics.dart';
|
||||
|
||||
enum StoreItemType {
|
||||
ads,
|
||||
|
|
@ -25,15 +26,34 @@ class InAppPurchaseService {
|
|||
InAppPurchase get instance => _inAppPurchase;
|
||||
|
||||
Future<bool> buyPack(CardPackBuyDto dto) async {
|
||||
final pack = await getPackStoreProduct(dto);
|
||||
final result = await buyItemInStore(pack.first);
|
||||
return result;
|
||||
try {
|
||||
final pack = await getPackStoreProduct(dto);
|
||||
final result = await buyItemInStore(pack.first);
|
||||
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 =
|
||||
await locator.repository.createPayment(dto.id, PaymentSystem.yookassa);
|
||||
if (paymentUrl == null) {
|
||||
Analytics.buyError(
|
||||
dto,
|
||||
PaymentSystem.yookassa,
|
||||
message: 'payment url is null',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
final controller = WebViewController()
|
||||
|
|
@ -57,9 +77,18 @@ class InAppPurchaseService {
|
|||
).then((_) async {
|
||||
final success = await locator.repository.checkUserPayment();
|
||||
if (success) {
|
||||
Analytics.buyComplete(
|
||||
dto,
|
||||
PaymentSystem.yookassa,
|
||||
);
|
||||
locator.packUpdater.updateAvailablePacks();
|
||||
return true;
|
||||
}
|
||||
Analytics.buyError(
|
||||
dto,
|
||||
PaymentSystem.yookassa,
|
||||
message: 'payment not verified',
|
||||
);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
|
@ -67,6 +96,11 @@ class InAppPurchaseService {
|
|||
Future<List<ProductDetails>> getPackStoreProduct(CardPackBuyDto dto) async {
|
||||
final bool isAvailable = await _inAppPurchase.isAvailable();
|
||||
if (!isAvailable) {
|
||||
Analytics.buyError(
|
||||
dto,
|
||||
PaymentSystem.google,
|
||||
message: 'in app purchase not available',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
final ids = {
|
||||
|
|
@ -74,6 +108,11 @@ class InAppPurchaseService {
|
|||
if (Platform.isIOS) dto.appStoreId,
|
||||
}.whereNotNull().toSet();
|
||||
if (ids.isEmpty) {
|
||||
Analytics.buyError(
|
||||
dto,
|
||||
PaymentSystem.google,
|
||||
message: 'product id not set',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +121,14 @@ class InAppPurchaseService {
|
|||
|
||||
if (productDetailResponse.error != null ||
|
||||
productDetailResponse.productDetails.isEmpty) {
|
||||
Analytics.buyError(
|
||||
dto,
|
||||
PaymentSystem.google,
|
||||
data: {
|
||||
'msg': 'cant get product details ${productDetailResponse.error}',
|
||||
'not_found': productDetailResponse.notFoundIDs.join(','),
|
||||
},
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -104,14 +151,34 @@ class PurchaseDetailsStreamSubscription {
|
|||
|
||||
Future<void> onPurchased(PurchaseDetails purchaseDetails) async {
|
||||
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(
|
||||
purchaseDetails.productID,
|
||||
purchaseDetails.verificationData.serverVerificationData,
|
||||
PaymentSystem.google,
|
||||
);
|
||||
if (result) {
|
||||
Analytics.buyComplete(
|
||||
null,
|
||||
PaymentSystem.google,
|
||||
message: purchaseDetails.productID,
|
||||
);
|
||||
log('Purchased');
|
||||
} else {
|
||||
Analytics.buyError(
|
||||
null,
|
||||
PaymentSystem.yookassa,
|
||||
message: 'payment not verified ${purchaseDetails.productID}',
|
||||
);
|
||||
throw Exception('Cant verify purchase');
|
||||
}
|
||||
} catch (e, s) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ part 'input_buttons_test_state.g.dart';
|
|||
@CopyWith()
|
||||
class InputButtonsTestState extends TestQuestionState {
|
||||
final List<String> answer;
|
||||
final List<int> answerIndexes;
|
||||
final List<String> answerIds;
|
||||
final bool isCorrect;
|
||||
final bool isAnswered;
|
||||
final int seed;
|
||||
|
|
@ -19,7 +19,7 @@ class InputButtonsTestState extends TestQuestionState {
|
|||
InputButtonsTestState({
|
||||
required this.seed,
|
||||
this.answer = const [],
|
||||
this.answerIndexes = const [],
|
||||
this.answerIds = const [],
|
||||
this.isCorrect = false,
|
||||
this.isAnswered = false,
|
||||
super.testType = TestQuestionType.simple,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ abstract class _$InputButtonsTestStateCWProxy {
|
|||
|
||||
InputButtonsTestState answer(List<String> answer);
|
||||
|
||||
InputButtonsTestState answerIndexes(List<int> answerIndexes);
|
||||
InputButtonsTestState answerIds(List<String> answerIds);
|
||||
|
||||
InputButtonsTestState isCorrect(bool isCorrect);
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ abstract class _$InputButtonsTestStateCWProxy {
|
|||
InputButtonsTestState call({
|
||||
int? seed,
|
||||
List<String>? answer,
|
||||
List<int>? answerIndexes,
|
||||
List<String>? answerIds,
|
||||
bool? isCorrect,
|
||||
bool? isAnswered,
|
||||
TestQuestionType? testType,
|
||||
|
|
@ -49,8 +49,8 @@ class _$InputButtonsTestStateCWProxyImpl
|
|||
InputButtonsTestState answer(List<String> answer) => this(answer: answer);
|
||||
|
||||
@override
|
||||
InputButtonsTestState answerIndexes(List<int> answerIndexes) =>
|
||||
this(answerIndexes: answerIndexes);
|
||||
InputButtonsTestState answerIds(List<String> answerIds) =>
|
||||
this(answerIds: answerIds);
|
||||
|
||||
@override
|
||||
InputButtonsTestState isCorrect(bool isCorrect) => this(isCorrect: isCorrect);
|
||||
|
|
@ -74,7 +74,7 @@ class _$InputButtonsTestStateCWProxyImpl
|
|||
InputButtonsTestState call({
|
||||
Object? seed = const $CopyWithPlaceholder(),
|
||||
Object? answer = const $CopyWithPlaceholder(),
|
||||
Object? answerIndexes = const $CopyWithPlaceholder(),
|
||||
Object? answerIds = const $CopyWithPlaceholder(),
|
||||
Object? isCorrect = const $CopyWithPlaceholder(),
|
||||
Object? isAnswered = const $CopyWithPlaceholder(),
|
||||
Object? testType = const $CopyWithPlaceholder(),
|
||||
|
|
@ -88,11 +88,10 @@ class _$InputButtonsTestStateCWProxyImpl
|
|||
? _value.answer
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: answer as List<String>,
|
||||
answerIndexes:
|
||||
answerIndexes == const $CopyWithPlaceholder() || answerIndexes == null
|
||||
? _value.answerIndexes
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: answerIndexes as List<int>,
|
||||
answerIds: answerIds == const $CopyWithPlaceholder() || answerIds == null
|
||||
? _value.answerIds
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: answerIds as List<String>,
|
||||
isCorrect: isCorrect == const $CopyWithPlaceholder() || isCorrect == null
|
||||
? _value.isCorrect
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
|
|
@ -129,8 +128,8 @@ InputButtonsTestState _$InputButtonsTestStateFromJson(
|
|||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
answerIndexes: (json['answerIndexes'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
answerIds: (json['answerIds'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
isCorrect: json['isCorrect'] as bool? ?? false,
|
||||
|
|
@ -145,7 +144,7 @@ Map<String, dynamic> _$InputButtonsTestStateToJson(
|
|||
<String, dynamic>{
|
||||
'testType': _$TestQuestionTypeEnumMap[instance.testType]!,
|
||||
'answer': instance.answer,
|
||||
'answerIndexes': instance.answerIndexes,
|
||||
'answerIds': instance.answerIds,
|
||||
'isCorrect': instance.isCorrect,
|
||||
'isAnswered': instance.isAnswered,
|
||||
'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_widgets/test_image.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/utils/iterable_helper.dart';
|
||||
import 'package:mnemo_cards/utils/string_helper.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
import '../../managers/repository/http_repository.dart';
|
||||
import 'question_states/test_question_state.dart';
|
||||
|
|
@ -28,6 +30,18 @@ class TestManager extends ChangeNotifier {
|
|||
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);
|
||||
|
||||
Iterable<TestQuestionState> get states => testHolder.state.questions.values;
|
||||
|
|
@ -37,7 +51,15 @@ class TestManager extends ChangeNotifier {
|
|||
if (packId != null) {
|
||||
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.addAll(test!.questions);
|
||||
color = test.color?.asColor ?? borderGray;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class _TestPageState extends State<TestPage> {
|
|||
if (!s.hasData || s.requireData!.questions.isEmpty) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Text('Loading'),
|
||||
child: Text('Загружаю тест...'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,129 +1,101 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mnemo_cards/features/tests/test_manager.dart';
|
||||
import 'package:mnemo_cards/features/tests/test_widgets/test_image.dart';
|
||||
import 'package:mnemo_cards/theme/themes.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import '../../../di/locator.dart';
|
||||
import '../../../widgets/game_card_widget.dart';
|
||||
import '../question_states/input_buttons_test_state.dart';
|
||||
import 'test_button.dart';
|
||||
|
||||
class InputButtonsTestWidget extends StatelessWidget {
|
||||
final InputButtonsTestQuestionBody model;
|
||||
|
||||
bool get isCorrect => state.answer.join() == model.answer;
|
||||
final InputButtonsTestQuestionBody _inputButtonModel;
|
||||
|
||||
InputButtonsTestQuestionBody get model => _inputButtonModel;
|
||||
|
||||
InputButtonsTestState get state =>
|
||||
manager.getState(model.id!) as InputButtonsTestState;
|
||||
|
||||
TestManager get manager => locator.testManager;
|
||||
|
||||
InputButtonsTestWidget(this.model, {super.key});
|
||||
Color get testColor => manager.color;
|
||||
|
||||
Widget _letter(BuildContext context, String text) => 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(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
InputButtonsTestWidget(this._inputButtonModel, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context).textTheme;
|
||||
final random = Random(state.seed);
|
||||
final buttons = model.buttons..shuffle(random);
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
// final random = Random(state.seed);
|
||||
final buttons = model.buttons; //..shuffle(random);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius:
|
||||
BorderRadius.circular(GameCardWidget.borderRadius),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (model.image != null)
|
||||
TestImageWidget(
|
||||
TestImage.image(model.image!),
|
||||
if (model.image != null)
|
||||
Container(
|
||||
margin: const EdgeInsets.all(4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius:
|
||||
BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: TestImageWidget(
|
||||
TestImage.image(model.image!),
|
||||
),
|
||||
),
|
||||
if (model.text != null)
|
||||
Container(
|
||||
padding: EdgeInsets.only(top: 8.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
model.text!,
|
||||
style: theme.headlineLarge,
|
||||
),
|
||||
if (model.text != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
model.text!,
|
||||
style: theme.headlineLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else Flexible(
|
||||
child: Center(
|
||||
child: Text(
|
||||
model.text!,
|
||||
style: theme.headlineLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
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) >=
|
||||
model.answer.length) {
|
||||
return;
|
||||
}
|
||||
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(
|
||||
model.id!,
|
||||
state.copyWith(
|
||||
answer: [...state.answer, p.$2.text!],
|
||||
answer: answer,
|
||||
answerIds: [...state.answerIds, button.id],
|
||||
),
|
||||
);
|
||||
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(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
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(() {
|
||||
manager.setState(
|
||||
model.id!,
|
||||
state.copyWith(
|
||||
isAnswered: false,
|
||||
isCorrect: false,
|
||||
answer: [],
|
||||
answerIndexes: [],
|
||||
// isAnswered: false,
|
||||
// isCorrect: false,
|
||||
answer: answer,
|
||||
answerIds: answerIds,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
...state.answer.map((e) => _letter(context, e)),
|
||||
...model.answer
|
||||
.substring(
|
||||
state.answer.fold(0, (p, n) => p + n.length),
|
||||
)
|
||||
.characters
|
||||
.map((e) => _emptyCell(context)),
|
||||
],
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: rows,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -181,22 +209,25 @@ class InputButtonsTestWidget extends StatelessWidget {
|
|||
AbsorbPointer(
|
||||
absorbing: state.isCorrect,
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
...buttons.indexed.map(
|
||||
(p) => Container(
|
||||
width: p.$2.text!.length > 3 ? null : 80,
|
||||
height: 60,
|
||||
child: TestButton(
|
||||
text: p.$2.text,
|
||||
onTap: () => buttonTapped(p),
|
||||
color: state.answer == p.$2
|
||||
? isCorrect
|
||||
? Colors.green[200]
|
||||
: Colors.red[200]
|
||||
: Colors.white,
|
||||
),
|
||||
...buttons.mapIndexed(
|
||||
(index, button) => Container(
|
||||
margin: EdgeInsets.all(4.0),
|
||||
width: button.text!.length > 3
|
||||
? null
|
||||
: constraints.maxWidth / 6,
|
||||
height: 50,
|
||||
child: state.answerIds.contains(button.id)
|
||||
? SizedBox.shrink()
|
||||
: TestButton(
|
||||
text: button.text,
|
||||
onTap: () => buttonTapped(index, button),
|
||||
color: state.answer == button
|
||||
? isCorrect
|
||||
? Colors.green[200]
|
||||
: Colors.red[200]
|
||||
: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -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:io';
|
||||
|
||||
|
|
@ -5,7 +6,9 @@ import 'package:auto_route/annotations.dart';
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:jailbreak_root_detection/jailbreak_root_detection.dart';
|
||||
import 'package:mnemo_cards/di/locator.dart';
|
||||
|
|
@ -26,7 +29,7 @@ import 'managers/repository/repository.dart';
|
|||
|
||||
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
SharedPreferences? globalSharedPreferences;
|
||||
late final SharedPreferences globalSharedPreferences;
|
||||
|
||||
final appRouter = AppRouter(ADMIN_BUILD);
|
||||
|
||||
|
|
@ -37,28 +40,49 @@ Future<bool> _screenOff() async =>
|
|||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
final stopwatch = Stopwatch()..start();
|
||||
print('ensureInitialized ${stopwatch.elapsedMilliseconds}');
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
]);
|
||||
androidDeviceInfo = await DeviceInfoPlugin().androidInfo;
|
||||
print('deviceInfo ${stopwatch.elapsedMilliseconds}');
|
||||
final isNotTrust = await JailbreakRootDetection.instance.isNotTrust;
|
||||
print('No trunst ${stopwatch.elapsedMilliseconds}');
|
||||
if (isNotTrust && !ADMIN_BUILD) {
|
||||
print('Not trusted');
|
||||
return;
|
||||
}
|
||||
final off = await _screenOff();
|
||||
print('screenshot off ${stopwatch.elapsedMilliseconds}');
|
||||
if (!off) {
|
||||
print('Cant disable screenhsot');
|
||||
return;
|
||||
}
|
||||
|
||||
FlutterError.onError = (errorDetails) {
|
||||
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
|
||||
};
|
||||
|
||||
try {
|
||||
globalSharedPreferences = await SharedPreferences.getInstance();
|
||||
print('global sp ${stopwatch.elapsedMilliseconds}');
|
||||
await setInjections();
|
||||
await initFirebase();
|
||||
print('injections ${stopwatch.elapsedMilliseconds}');
|
||||
unawaited(initFirebase());
|
||||
print('initing... ${stopwatch.elapsedMilliseconds}');
|
||||
await locator.packCacheManager.init();
|
||||
await locator.previewPackHolder.init();
|
||||
await locator.previewPackPoller.init();
|
||||
await locator.packManager.init();
|
||||
await locator.userManager.init();
|
||||
await locator.favoriteCardsController.init();
|
||||
await locator.previewPackPoller.init();
|
||||
await locator.packUpdater.init();
|
||||
print('initing complete ${stopwatch.elapsedMilliseconds}');
|
||||
await MobileAds.initialize();
|
||||
print('mobile ads ${stopwatch.elapsedMilliseconds}');
|
||||
print('runApp ${stopwatch.elapsedMilliseconds}');
|
||||
runApp(const MyApp());
|
||||
} on Object catch (e, s) {
|
||||
log(e.toString(), stackTrace: s);
|
||||
|
|
@ -70,7 +94,7 @@ Future<void> initFirebase() async {
|
|||
try {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
).timeout(Duration(seconds: 2));
|
||||
);
|
||||
await FirebaseRepository.update();
|
||||
} on Object catch (e, s) {
|
||||
log('Firebase not inited', error: e, stackTrace: s);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class FavoriteCardsController {
|
|||
}
|
||||
|
||||
List<String> get state {
|
||||
state = (_sharedPreferences.getStringList('favorites') ?? []);
|
||||
_state = (_sharedPreferences.getStringList('favorites') ?? []);
|
||||
return _state;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mnemo_cards/flags.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import 'firebase_config_repository.dart';
|
||||
|
||||
mixin Api {
|
||||
String get path =>
|
||||
kReleaseMode || (globalSharedPreferences?.getBool('env') ?? true)
|
||||
!ADMIN_BUILD || (globalSharedPreferences.getBool('env') ?? true)
|
||||
//prod
|
||||
? 'https://89.19.210.178:8080' //FirebaseRepository.appConfig.path
|
||||
: 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.gr.dart';
|
||||
import '../features/analytics/analytics.dart';
|
||||
import 'user_state_holder.dart';
|
||||
|
||||
class UserManager {
|
||||
|
|
@ -48,6 +49,7 @@ class UserManager {
|
|||
if (await _googleSignIn.isSignedIn()) {
|
||||
await _googleSignIn.signOut();
|
||||
}
|
||||
Analytics.logout(userStateHolder.user);
|
||||
userStateHolder.clear();
|
||||
}
|
||||
|
||||
|
|
@ -56,9 +58,10 @@ class UserManager {
|
|||
ExternalIdType idType,
|
||||
String? name,
|
||||
) async {
|
||||
final p = await _repository.createOrGetUser(externalId, idType, name);
|
||||
_setAuthToken(p.$2);
|
||||
userStateHolder.setUser(p.$1);
|
||||
final (user, token) =
|
||||
await _repository.createOrGetUser(externalId, idType, name);
|
||||
_setAuthToken(token);
|
||||
userStateHolder.setUser(user);
|
||||
}
|
||||
|
||||
void _setAuthToken(String authToken) async {
|
||||
|
|
@ -111,6 +114,7 @@ class UserManager {
|
|||
final name = account.displayName;
|
||||
final token =
|
||||
await account.authentication.then((value) => value.idToken);
|
||||
Analytics.login(ExternalIdType.google);
|
||||
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:rxdart/rxdart.dart';
|
||||
|
||||
import '../features/analytics/analytics.dart';
|
||||
|
||||
|
||||
class UserStateHolder extends ChangeNotifier {
|
||||
UserDto? _dto;
|
||||
|
|
@ -17,6 +19,8 @@ class UserStateHolder extends ChangeNotifier {
|
|||
|
||||
void setUser(UserDto dto) {
|
||||
_dto = dto.copyWith();
|
||||
Analytics.setUser(dto);
|
||||
|
||||
notifyListeners();
|
||||
_streamController.add(_dto);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import 'package:url_launcher/url_launcher.dart';
|
|||
|
||||
import '../admin/all_cards.dart';
|
||||
import '../di/locator.dart';
|
||||
import '../features/analytics/analytics.dart';
|
||||
import '../theme/themes.dart';
|
||||
import '../widgets/shared_pref_button.dart';
|
||||
import '../widgets/text_button.dart';
|
||||
|
|
@ -21,6 +22,7 @@ import '../widgets/text_button.dart';
|
|||
class ProfilePage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Analytics.profilePageOpened(locator.userManager.userStateHolder.user);
|
||||
final theme = Theme.of(context).textTheme;
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
|
|
@ -30,7 +32,7 @@ class ProfilePage extends StatelessWidget {
|
|||
stream: locator.userManager.userStateHolder.asStream,
|
||||
builder: (context, snapshot) {
|
||||
var title = snapshot.data?.name;
|
||||
if (title == null || title.length > 10) {
|
||||
if (true || title == null || title.length > 10) {
|
||||
title = 'Привет!';
|
||||
}
|
||||
return Header(
|
||||
|
|
@ -63,7 +65,8 @@ class ProfilePage extends StatelessWidget {
|
|||
if (ADMIN_BUILD)
|
||||
MaterialButton(
|
||||
onPressed: () {
|
||||
showDialog(context: context, builder: (c) => const AllCards());
|
||||
showDialog(
|
||||
context: context, builder: (c) => const AllCards());
|
||||
},
|
||||
child: Text('ALL CARDS'),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,19 +1,12 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.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/string_helper.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.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_swiper/src/allowed_swipe_direction.dart';
|
||||
import '../../features/card_swiper/src/card_swiper.dart';
|
||||
|
|
@ -22,14 +15,16 @@ import '../../main.dart';
|
|||
import '../game_card_widget.dart';
|
||||
|
||||
class CardGameMainWidget extends StatelessWidget {
|
||||
final CardPackDto cardPack;
|
||||
final List<GameCardDto> cards;
|
||||
final Color? color;
|
||||
final int? startCardId;
|
||||
final CardSwiperController cardSwiperController;
|
||||
final FlipCardController flipCardController;
|
||||
final int length;
|
||||
|
||||
const CardGameMainWidget(
|
||||
this.cardPack,
|
||||
this.cards,
|
||||
this.color,
|
||||
this.startCardId,
|
||||
this.cardSwiperController,
|
||||
this.flipCardController,
|
||||
|
|
@ -38,23 +33,9 @@ class CardGameMainWidget extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final numberOfCardsDisplayed = min(2, cardPack.cards.length);
|
||||
final numberOfCardsDisplayed = min(2, cards.length);
|
||||
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
|
||||
? cards[cardSwiperController.currentIndex % cards.length]
|
||||
: null;
|
||||
|
|
@ -75,12 +56,7 @@ class CardGameMainWidget extends StatelessWidget {
|
|||
return StatefulBuilder(builder: (context, setState) {
|
||||
return Stack(
|
||||
children: [
|
||||
_EndWidget(cardSwiperController, flipCardController, () {
|
||||
setState(() {
|
||||
enabled = false;
|
||||
});
|
||||
AutoRouter.of(context).maybePop();
|
||||
}),
|
||||
_EndWidget(),
|
||||
if (enabled)
|
||||
Hero(
|
||||
tag: activeCard()?.id ?? 'empty',
|
||||
|
|
@ -127,21 +103,21 @@ class CardGameMainWidget extends StatelessWidget {
|
|||
cardIndex);
|
||||
if (cardSwiperController.currentIndex ==
|
||||
index) {
|
||||
lastThreshold = percentThresholdX;
|
||||
// lastThreshold = percentThresholdX;
|
||||
}
|
||||
print('${cards[index].translation} $scale');
|
||||
final cardWidget = Center(
|
||||
child: GameCardWidget(
|
||||
cards[index],
|
||||
cards[index].id.memoryImage,
|
||||
cards[index].memoryImage,
|
||||
flipCardController,
|
||||
flipCardController.state
|
||||
// || cards[index].isAd
|
||||
,
|
||||
isFirstCard,
|
||||
isFirstCard
|
||||
? cardPack.color?.asColor
|
||||
: cardPack.color?.asColor?.darken(
|
||||
? color
|
||||
: color?.darken(
|
||||
(scale - 1) /
|
||||
(initialScale - 1) *
|
||||
0.075,
|
||||
|
|
@ -164,38 +140,10 @@ class CardGameMainWidget extends StatelessWidget {
|
|||
}
|
||||
|
||||
class _EndWidget extends StatelessWidget {
|
||||
final CardSwiperController cardSwiperController;
|
||||
final FlipCardController flipCardController;
|
||||
final VoidCallback callback;
|
||||
|
||||
_EndWidget(
|
||||
this.cardSwiperController,
|
||||
this.flipCardController,
|
||||
this.callback,
|
||||
);
|
||||
_EndWidget();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.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/utils/string_helper.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import '../../di/locator.dart';
|
||||
import '../../features/card_flipper/flip_card.dart';
|
||||
import '../../features/card_flipper/flip_card_controllers.dart';
|
||||
import '../../features/card_swiper/flutter_card_swiper.dart';
|
||||
import 'card_game_main.dart';
|
||||
|
|
@ -20,13 +15,17 @@ import 'card_game_main.dart';
|
|||
@RoutePage()
|
||||
class CardGamePage extends StatefulWidget {
|
||||
final int? startCardId;
|
||||
final CardPackDto cardPack;
|
||||
final List<GameCardDto> cards;
|
||||
final Color? color;
|
||||
final bool infinite;
|
||||
final bool shuffle;
|
||||
|
||||
const CardGamePage({
|
||||
required this.cardPack,
|
||||
required this.cards,
|
||||
required this.color,
|
||||
this.startCardId,
|
||||
this.infinite = false,
|
||||
this.shuffle = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -38,17 +37,17 @@ class _CardGamePageState extends State<CardGamePage> {
|
|||
final CardSwiperController cardSwiperController = CardSwiperController();
|
||||
final FlipCardController flipCardController = FlipCardController();
|
||||
|
||||
List<GameCardDto> shuffledCards = [];
|
||||
|
||||
GameCardDto? get activeCard {
|
||||
final index = cardSwiperController.currentIndex;
|
||||
final cards = widget.cardPack.cards;
|
||||
if (index >= 0) {
|
||||
return cards[index % cards.length];
|
||||
return shuffledCards[index % shuffledCards.length];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int get gameLength =>
|
||||
widget.infinite ? 10000000 : widget.cardPack.cards.length;
|
||||
int get gameLength => widget.infinite ? 10000000 : shuffledCards.length;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -95,9 +94,8 @@ class _CardGamePageState extends State<CardGamePage> {
|
|||
cardSwiperController.progress,
|
||||
Result.correct,
|
||||
),
|
||||
widget.cardPack.cards.length,
|
||||
widget.cardPack.color?.asColor ??
|
||||
borderGray,
|
||||
shuffledCards.length,
|
||||
widget.color ?? borderGray,
|
||||
timer: timer,
|
||||
popText: widget.infinite ? 'к теме' : null,
|
||||
key: ValueKey('CARDS PROGRESS'),
|
||||
|
|
@ -116,7 +114,8 @@ class _CardGamePageState extends State<CardGamePage> {
|
|||
child: Stack(
|
||||
children: [
|
||||
CardGameMainWidget(
|
||||
widget.cardPack,
|
||||
shuffledCards,
|
||||
widget.color,
|
||||
widget.startCardId,
|
||||
cardSwiperController,
|
||||
flipCardController,
|
||||
|
|
@ -186,7 +185,12 @@ class _CardGamePageState extends State<CardGamePage> {
|
|||
),
|
||||
padding: EdgeInsets.all(10.0.w),
|
||||
child: Image.asset(
|
||||
"icons/heart.png",
|
||||
locator.favoriteCardsController
|
||||
.isFavoriteCard(
|
||||
activeCard?.id,
|
||||
)
|
||||
? "icons/heart_fill.png"
|
||||
: "icons/heart.png",
|
||||
width: 29.w,
|
||||
height: 29.h,
|
||||
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
|
||||
void dispose() {
|
||||
// cardSwiperController.removeListener(cardSwiperListener);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:mnemo_cards/features/packs/images_holder.dart';
|
||||
import 'package:mnemo_cards/main.dart';
|
||||
import 'package:mnemo_cards/utils/iterable_helper.dart';
|
||||
import 'package:mnemo_cards/widgets/fade_in_widget.dart';
|
||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||
import 'package:yandex_mobileads/mobile_ads.dart';
|
||||
import '../../domain/router/app_router.gr.dart';
|
||||
import '../../features/analytics/analytics.dart';
|
||||
import '../../features/yandex_ads/yandex_ads.dart';
|
||||
import '../game_card_widget.dart';
|
||||
|
||||
|
|
@ -19,67 +23,113 @@ import '../../theme/themes.dart';
|
|||
import 'card_pack_header.dart';
|
||||
import 'package:mnemo_cards/utils/string_helper.dart';
|
||||
|
||||
import 'cards_view.dart';
|
||||
import 'cards_view_controller.dart';
|
||||
|
||||
final _mainScrollController = ScrollController();
|
||||
|
||||
class AvailablePack extends StatelessWidget {
|
||||
final CardPackDto cardPack;
|
||||
final _CardsViewController _cardsViewController = _CardsViewController();
|
||||
late final CardsViewController _cardsViewController =
|
||||
CardsViewController(cardPack);
|
||||
|
||||
AvailablePack(this.cardPack);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Analytics.availablePackOpened(cardPack);
|
||||
return Column(
|
||||
children: [
|
||||
CardPackHeader.fromDto(cardPack),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
controller: _mainScrollController,
|
||||
children: [
|
||||
StatefulBuilder(builder: (context, setState) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 0.h),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: cardPack.color?.asColor ??
|
||||
Colors.grey.withOpacity(0.5)),
|
||||
),
|
||||
color: cardPack.color?.asColor?.withOpacity(0.1),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: _CardsView(_cardsViewController, cardPack),
|
||||
child: CardsView(_cardsViewController, cardPack),
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
color: cardPack.color?.asColor ??
|
||||
Colors.grey.withOpacity(0.5),
|
||||
),
|
||||
SizedBox(
|
||||
height: 10.h,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 8.0.h,
|
||||
horizontal: 10.w,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_Button('view.png', () async {
|
||||
_cardsViewController.toggleExpanded();
|
||||
}),
|
||||
StreamBuilder(
|
||||
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 {
|
||||
final index =
|
||||
Random().nextInt(cardPack.cards.length);
|
||||
final cards = _cardsViewController.state.packCards;
|
||||
if (cards.isEmpty) return;
|
||||
final index = (_cardsViewController.offsetCards +
|
||||
Random().nextInt(
|
||||
(cards.length / 2).ceil(),
|
||||
) +
|
||||
3) %
|
||||
cards.length;
|
||||
await _cardsViewController.animateTo(index);
|
||||
AutoRouter.of(context).push(
|
||||
PageRouteInfo(
|
||||
CardGamePage.name,
|
||||
args: CardGamePageArgs(
|
||||
cardPack: cardPack,
|
||||
startCardId: cardPack.cards[index].id,
|
||||
cards: cards,
|
||||
color: cardPack.color?.asColor,
|
||||
startCardId: cards[index].id,
|
||||
infinite: true,
|
||||
shuffle: true,
|
||||
// id: cardPack.dto.id.toString(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
// _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,24 +174,28 @@ class _TestsSection extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: locator.testManager.loadTests(packId: packId),
|
||||
return StreamBuilder(
|
||||
stream: locator.testManager.loadTestsStream(packId: packId),
|
||||
builder: (context, snapshot) {
|
||||
return Column(
|
||||
children: [
|
||||
if (snapshot.hasData)
|
||||
...snapshot.data!.map(
|
||||
(dto) => _TestButton(
|
||||
title: dto.name,
|
||||
subtitle: null,
|
||||
time: dto.time,
|
||||
timeSubtitle: dto.timeSubtitle,
|
||||
onTap: () {
|
||||
AutoRouter.of(context).push(
|
||||
PageRouteInfo(TestPage.name,
|
||||
args: TestPageArgs(testId: dto.id!)),
|
||||
);
|
||||
},
|
||||
...snapshot.data!.mapIndexed(
|
||||
(int index, TestDto dto) => FadeInWidget(
|
||||
delay: Duration(milliseconds: index * 200),
|
||||
animationDuration: const Duration(milliseconds: 200),
|
||||
child: _TestButton(
|
||||
title: dto.name,
|
||||
subtitle: null,
|
||||
time: dto.time,
|
||||
timeSubtitle: dto.timeSubtitle,
|
||||
onTap: () {
|
||||
AutoRouter.of(context).push(
|
||||
PageRouteInfo(TestPage.name,
|
||||
args: TestPageArgs(testId: dto.id!)),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -152,28 +206,33 @@ class _TestsSection extends StatelessWidget {
|
|||
}
|
||||
|
||||
class _AdTile extends StatelessWidget {
|
||||
const _AdTile();
|
||||
const _AdTile({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context).textTheme;
|
||||
return Container(
|
||||
height: 80,
|
||||
margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 4.0,
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final banner = YandexAds.createBanner(
|
||||
BoxConstraints.tight(Size(constraints.maxWidth - 16, 80 - 8)));
|
||||
return FadeInWidget(
|
||||
future: banner.loadAd(),
|
||||
animationDuration: Duration(milliseconds: 1000),
|
||||
child: Container(
|
||||
height: 80,
|
||||
margin: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 4.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: Center(
|
||||
child: AdWidget(bannerAd: banner),
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: Center(
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return AdWidget(
|
||||
bannerAd: YandexAds.createBanner(constraints)..loadAd());
|
||||
}),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,10 +240,7 @@ class _Button extends StatelessWidget {
|
|||
final String asset;
|
||||
final VoidCallback onTap;
|
||||
|
||||
_Button(
|
||||
this.asset,
|
||||
this.onTap,
|
||||
);
|
||||
_Button(this.asset, this.onTap, {super.key});
|
||||
|
||||
@override
|
||||
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:developer';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:mnemo_cards/utils/string_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.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 '../../di/locator.dart';
|
||||
import '../../domain/router/app_router.dart';
|
||||
import '../../theme/themes.dart';
|
||||
import 'card_pack_header.dart';
|
||||
import 'loading_pack.dart';
|
||||
|
||||
class BuyPack extends StatelessWidget {
|
||||
final CardPackBuyDto dto;
|
||||
|
|
@ -18,74 +25,135 @@ class BuyPack extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool loading = false;
|
||||
Analytics.buyPackOpened(dto);
|
||||
return Container(
|
||||
// color: dto.color?.asColor,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CardPackHeader(dto.title, dto.subtitle, dto.color?.asColor),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 10.0.w, vertical: 18.h),
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
final cardWidth = constraints.maxWidth / 3;
|
||||
final imageWidth = cardWidth * 0.9;
|
||||
return SizedBox(
|
||||
height: 150.h,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: dto.cards.length,
|
||||
itemExtent: cardWidth,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final card = dto.cards[index];
|
||||
if (card.image != null) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
width: imageWidth,
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: cardWidth - imageWidth - 1,
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
border: Border.all(
|
||||
color: borderGray,
|
||||
child: StatefulBuilder(builder: (context, setState) {
|
||||
if (loading) {
|
||||
return _Loading(dto);
|
||||
}
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CardPackHeader(dto.title, dto.subtitle, dto.color?.asColor),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 10.0.w, vertical: 18.h),
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
final cardWidth = constraints.maxWidth / 3;
|
||||
final imageWidth = cardWidth * 0.9;
|
||||
return SizedBox(
|
||||
height: 150.h,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: dto.cards.length,
|
||||
itemExtent: cardWidth,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final card = dto.cards[index];
|
||||
if (card.image != null) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
width: imageWidth,
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: cardWidth - imageWidth - 1,
|
||||
vertical: 1,
|
||||
),
|
||||
),
|
||||
child: Image.memory(base64Decode(card.image!)),
|
||||
);
|
||||
}
|
||||
return SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
SizedBox(
|
||||
height: 32.h,
|
||||
),
|
||||
...?dto.items?.build(),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
border: Border.all(
|
||||
color: borderGray,
|
||||
),
|
||||
),
|
||||
child: Image.memory(base64Decode(card.image!)),
|
||||
);
|
||||
}
|
||||
return SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
SizedBox(
|
||||
height: 32.h,
|
||||
),
|
||||
...?dto.items?.build(),
|
||||
],
|
||||
),
|
||||
),
|
||||
_BuyButton(dto, (v) {
|
||||
setState(() {
|
||||
loading = v;
|
||||
});
|
||||
}),
|
||||
SizedBox(
|
||||
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('Загрузка...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
_BuyButton(dto),
|
||||
SizedBox(
|
||||
height: 16.h,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BuyButton extends StatelessWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -101,7 +169,11 @@ class _BuyButton extends StatelessWidget {
|
|||
InkWell(
|
||||
onTap: () async {
|
||||
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 {
|
||||
AppRouter.openAuth();
|
||||
}
|
||||
|
|
@ -145,11 +217,15 @@ class _BuyButton extends StatelessWidget {
|
|||
TextButton(
|
||||
style: ButtonStyle(
|
||||
overlayColor:
|
||||
MaterialStateColor.resolveWith((states) => Colors.white),
|
||||
WidgetStateColor.resolveWith((states) => Colors.white),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (locator.userManager.hasUser) {
|
||||
await locator.purchaseService.buyPackWithYooMoney(dto, context);
|
||||
Analytics.buyPackPressed(dto, PaymentSystem.yookassa);
|
||||
if (await locator.purchaseService
|
||||
.buyPackWithYooMoney(dto, context)) {
|
||||
_updatePack();
|
||||
}
|
||||
} else {
|
||||
AppRouter.openAuth();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,22 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:auto_route/annotations.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:mnemo_cards/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:yandex_mobileads/mobile_ads.dart';
|
||||
import '../../main.dart';
|
||||
import '../items/builder.dart';
|
||||
import '../../features/analytics/analytics.dart';
|
||||
|
||||
import '../../di/locator.dart';
|
||||
import '../../theme/themes.dart';
|
||||
import '../header.dart';
|
||||
import '../mnemo_text.dart';
|
||||
import 'available_pack.dart';
|
||||
import 'buy_pack_page.dart';
|
||||
import 'card_pack_header.dart';
|
||||
import 'loading_pack.dart';
|
||||
|
||||
@RoutePage()
|
||||
class CardPackPage extends StatefulWidget {
|
||||
final String cardPackId;
|
||||
final CardPackPreviewDto previewDto;
|
||||
|
||||
const CardPackPage(this.cardPackId, {super.key});
|
||||
const CardPackPage(this.previewDto, {super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _CardPackPage();
|
||||
|
|
@ -43,14 +28,27 @@ class _CardPackPage extends State<CardPackPage> {
|
|||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: StreamBuilder<GetCardPackResponse>(
|
||||
stream: locator.packManager.getCardPackStream(widget.cardPackId),
|
||||
stream: locator.packManager.getCardPackStream(
|
||||
widget.previewDto.id,
|
||||
widget.previewDto.isAvailable,
|
||||
),
|
||||
initialData: null,
|
||||
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;
|
||||
if (response is CardPackBuyDto)
|
||||
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(
|
||||
children: [
|
||||
CardPackHeader.fromDto(null),
|
||||
|
|
@ -71,19 +69,3 @@ class _CardPackPage extends State<CardPackPage> {
|
|||
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 {
|
||||
final Widget child;
|
||||
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
|
||||
Widget build(BuildContext context) => Container(
|
||||
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(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
|
|
@ -66,7 +76,7 @@ class GameCardDecoration extends StatelessWidget {
|
|||
borderRadius: BorderRadius.circular(
|
||||
8 + (GameCardWidget.borderRadius - 8) * alpha),
|
||||
border: Border.all(
|
||||
color: borderGray.withOpacity(1.0 - alpha * alpha),
|
||||
color: (borderColor ?? borderGray).withOpacity(1.0 - alpha * alpha),
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
|
|
@ -160,7 +170,7 @@ class _BackWidget extends StatelessWidget {
|
|||
),
|
||||
child: Center(
|
||||
child: MnemoText(
|
||||
card.back ?? card.translation ?? '',
|
||||
card.back?.isNotEmpty == true ? card.back : card.translation ?? '',
|
||||
textStyle: theme.displayMedium!.copyWith(color: Colors.white),
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
|
|
@ -199,8 +209,8 @@ class FrontCard extends StatelessWidget {
|
|||
alignment: Alignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsets.all(8.0 * imageScale * imageScale * alpha),
|
||||
padding: EdgeInsets.all(
|
||||
8.0 * imageScale * (alpha / 2 + 0.5)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: ar > 1
|
||||
|
|
@ -213,7 +223,7 @@ class FrontCard extends StatelessWidget {
|
|||
child: MnemoText(
|
||||
card.original ?? '',
|
||||
textStyle: TextStyle(
|
||||
fontSize: 27 * textScale,
|
||||
fontSize: 15 * textScale + 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
|
|
@ -229,33 +239,30 @@ class FrontCard extends StatelessWidget {
|
|||
}
|
||||
},
|
||||
),
|
||||
if (alpha > 0)
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final sp =
|
||||
await SharedPreferences.getInstance();
|
||||
if (sp.getBool('sound_on') != false &&
|
||||
card.translation != null) {
|
||||
FlutterTts flutterTts = FlutterTts();
|
||||
await flutterTts.setLanguage('ru');
|
||||
await flutterTts.setSpeechRate(0.5);
|
||||
flutterTts.speak(
|
||||
card.translation!,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: MnemoText(
|
||||
card.translation?.toLowerCase(),
|
||||
textStyle: TextStyle(
|
||||
fontSize: 20 * textScale * alpha,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: theme.headlineSmall!.color!
|
||||
.withOpacity(0.5 * alpha),
|
||||
),
|
||||
maxLines: 1,
|
||||
// group: smallSize,
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final sp = await SharedPreferences.getInstance();
|
||||
if (sp.getBool('sound_on') != false &&
|
||||
card.translation != null) {
|
||||
FlutterTts flutterTts = FlutterTts();
|
||||
await flutterTts.setLanguage('ru');
|
||||
await flutterTts.setSpeechRate(0.5);
|
||||
flutterTts.speak(
|
||||
card.translation!,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: MnemoText(
|
||||
card.translation?.toLowerCase(),
|
||||
textStyle: TextStyle(
|
||||
fontSize: 12 * textScale * alpha + 8,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: theme.headlineSmall!.color!
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
if (alpha > 0 &&
|
||||
ar > 1 &&
|
||||
card.transcription != null &&
|
||||
|
|
@ -289,12 +296,17 @@ class FrontCard extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
Container(
|
||||
width: constrains.maxHeight / ar * imageScale,
|
||||
height: constrains.maxHeight / ar * imageScale,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
GameCardWidget.borderRadius)),
|
||||
child: ImageFade(
|
||||
key: ValueKey(card.id),
|
||||
image: image,
|
||||
// fix for elapsedInSeconds < 0 in debug mode
|
||||
syncDuration: Duration.zero,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ typedef ImageFadeLoadingBuilder = Widget Function(
|
|||
class ImageFade extends StatefulWidget {
|
||||
/// Creates a widget that displays a [placeholder] widget while a specified [image] loads,
|
||||
/// then cross-fades to the loaded image.
|
||||
const ImageFade({
|
||||
const
|
||||
ImageFade({
|
||||
Key? key,
|
||||
this.placeholder,
|
||||
this.image,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class TextItemBuilder extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: item.hasBorder
|
||||
? Border.all(
|
||||
|
|
@ -50,6 +51,7 @@ class TextItemBuilder extends StatelessWidget {
|
|||
fontSize: 24,
|
||||
color: Colors.black,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (item.subtitle != null)
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -97,7 +97,13 @@ class _PacksMenuWidgetState extends State<PacksMenuWidget> {
|
|||
padding: const EdgeInsets.all(8.0),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: locator.previewPackPoller.poll,
|
||||
backgroundColor: Colors.white,
|
||||
color: snapshot.data?.firstOrNull?.color?.asColor ??
|
||||
Colors.black,
|
||||
child: ListView(
|
||||
physics: BouncingScrollPhysics(
|
||||
decelerationRate: ScrollDecelerationRate.fast,
|
||||
),
|
||||
scrollDirection: Axis.vertical,
|
||||
children: [
|
||||
if (snapshot.data == null)
|
||||
|
|
@ -127,7 +133,9 @@ class _PacksMenuWidgetState extends State<PacksMenuWidget> {
|
|||
if (pack.imageBase64 != null) {
|
||||
try {
|
||||
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) {
|
||||
log('${pack.id} cover not cached');
|
||||
}
|
||||
|
|
@ -154,7 +162,9 @@ class _PackButton4 extends StatelessWidget {
|
|||
AutoRouter.of(context).push(
|
||||
PageRouteInfo(
|
||||
CardPackPage.name,
|
||||
args: CardPackPageArgs(cardPackId: pack.id),
|
||||
args: CardPackPageArgs(
|
||||
previewDto: pack,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
@ -183,9 +193,10 @@ class _PackButton4 extends StatelessWidget {
|
|||
image: pack.imageBase64 == null
|
||||
? null
|
||||
: DecorationImage(
|
||||
image: MemoryImage(
|
||||
base64Decode(pack.imageBase64!),
|
||||
),
|
||||
image: locator.imagesHolder.get('pack_${pack.id}') ??
|
||||
MemoryImage(
|
||||
base64Decode(pack.imageBase64!),
|
||||
),
|
||||
),
|
||||
),
|
||||
margin: EdgeInsets.all(2.0.w),
|
||||
|
|
@ -276,7 +287,7 @@ class _PackButton4Shimmer extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: Colors.white,
|
||||
highlightColor: Colors.white,
|
||||
highlightColor: borderGray,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: cardHeight,
|
||||
|
|
|
|||
48
pubspec.lock
48
pubspec.lock
|
|
@ -13,10 +13,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "2350805d7afefb0efe7acd325cb19d3ae8ba4039b906eade3807ffb69938a01f"
|
||||
sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.33"
|
||||
version: "1.3.35"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -353,14 +353,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "372d94ced114b9c40cb85e18c50ac94a7e998c8eec630c50d7aec047847d27bf"
|
||||
sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.31.0"
|
||||
version: "2.32.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -377,6 +401,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ dependencies:
|
|||
device_info_plus: ^9.1.2
|
||||
package_info_plus: ^8.0.0
|
||||
yandex_mobileads: ^6.1.0
|
||||
firebase_crashlytics: ^3.5.7
|
||||
firebase_analytics: ^10.10.7
|
||||
in_app_purchase: ^3.2.0
|
||||
dot_navigation_bar: ^1.0.2
|
||||
story: ^1.1.0
|
||||
|
|
|
|||
Loading…
Reference in a new issue