diff --git a/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk b/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk index 65d0520..5f668bb 100644 Binary files a/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk and b/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk differ diff --git a/lib/admin/add_promocode_campaign.dart b/lib/admin/add_promocode_campaign.dart new file mode 100644 index 0000000..97ed61f --- /dev/null +++ b/lib/admin/add_promocode_campaign.dart @@ -0,0 +1,382 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; + +import 'package:mnemo_cards/admin/admin_api.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; + +import '../di/injector.dart'; +import '../main.dart'; +import 'date_param.dart'; +import 'enum_param.dart'; +import 'int_param.dart'; +import 'text_param.dart'; + +class AddPromocodeCampaign { + late PromoCodesCampaignDto campaignDto; + late AdminApi adminApi; + List _campaigns = []; + + final availableStatuses = [ + PromoCodeCampaignStatus.created, + PromoCodeCampaignStatus.preparing, + PromoCodeCampaignStatus.ready, + PromoCodeCampaignStatus.active, + PromoCodeCampaignStatus.disabled, + ]; + + AddPromocodeCampaign() { + this.adminApi = getIt.get(); + _init(null); + } + + void _init(PromoCodesCampaignDto? dto) { + final now = DateTime.now(); + campaignDto = dto ?? + PromoCodesCampaignDto( + products: [], + activationsPerCode: 1, + generationSize: 100, + start: now, + finish: now.add(Duration(days: 30)), + status: PromoCodeCampaignStatus.disabled, + name: null, + template: r'$s$s$s$s$s$s$s$s', + ); + } + + Future _loadCampaigns() async { + try { + _campaigns = await adminApi.getPromoCodeCampaigns(); + } catch (e, s) { + log('Error', error: e, stackTrace: s); + print('Error $e'); + return false; + } + return true; + } + + void _savePromoCodeCampaign() async { + final result = await adminApi.addPromoCodeCampaign(campaignDto); + if (!result) { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('ERROR'), + )); + } else { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('Success!!!'), + )); + appRouter.maybePop(); + } + } + + void _deletePromoCodeCampaign() async { + if (campaignDto.id == null) return; + final result = + await adminApi.deletePromoCodeCampaign(campaignDto.id.toString()); + if (!result) { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('ERROR'), + )); + } else { + ScaffoldMessenger.of(scaffoldKey.currentContext!).showSnackBar(SnackBar( + content: Text('Success!!!'), + )); + appRouter.maybePop(); + } + } + + void addCampaign(BuildContext context) { + showDialog( + context: context, + builder: (c) { + return FutureBuilder( + future: _loadCampaigns(), + builder: (context, snapshot) { + if (snapshot.data != true) { + return Center( + child: Material( + child: Center(child: CircularProgressIndicator()), + ), + ); + } + return Center( + child: Scaffold( + body: Material( + child: StatefulBuilder(builder: (context, setCampaign) { + return ListView( + children: [ + Header( + '${campaignDto.id}', + hasPopButton: true, + trail: Row( + children: [ + IconButton( + onPressed: () async { + await _loadCampaigns(); + setCampaign(() {}); + }, + icon: Icon(Icons.sync), + ), + IconButton( + onPressed: _savePromoCodeCampaign, + icon: Icon(Icons.save), + ), + if (campaignDto.id != null) + IconButton( + onPressed: () async { + campaignDto = + campaignDto.copyWith(id: null); + _savePromoCodeCampaign(); + }, + icon: Icon(Icons.copy), + ), + if (campaignDto.id != null) + GestureDetector( + onLongPress: _deletePromoCodeCampaign, + child: Icon(Icons.delete), + ) + ], + ), + ), + DropdownMenu( + initialSelection: null, + onSelected: (int? value) { + setCampaign(() { + _init( + _campaigns + .firstWhereOrNull((v) => v.id == value), + ); + }); + }, + dropdownMenuEntries: [ + ..._campaigns, + null, + ].map((campaign) { + return DropdownMenuEntry( + value: campaign?.id, + label: campaign == null + ? 'New' + : '${campaign.name ?? campaign.promoCodes?.firstOrNull ?? campaign.id} ${campaign.status.name}', + ); + }).toList(), + ), + StatefulBuilder(builder: (context, setState) { + return Stack( + alignment: Alignment.center, + children: [ + Padding( + padding: EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + TextParam( + 'Name', + campaignDto.name, + (v) { + campaignDto = + campaignDto.copyWith( + name: v, + ); + }, + ), + TextParam( + 'Template', + campaignDto.template, + (v) { + campaignDto = + campaignDto.copyWith( + template: v, + ); + }, + description: + r'$d - digit $l - letter', + ), + IntParam( + 'Generation size', + campaignDto.generationSize, + (v) { + campaignDto = + campaignDto.copyWith( + generationSize: v); + }, + ), + IntParam( + 'Activations per code', + campaignDto.activationsPerCode, + (v) { + campaignDto = + campaignDto.copyWith( + activationsPerCode: v, + ); + }, + ), + DateParam( + 'Start', + campaignDto.start, + (v) { + setState(() { + campaignDto = campaignDto + .copyWith(start: v); + }); + }, + ), + DateParam( + 'Finish', + campaignDto.finish, + (v) { + setState(() { + campaignDto = campaignDto + .copyWith(finish: v); + }); + }, + ), + DateParam( + 'Finish', + campaignDto.finish, + (v) { + setState(() { + campaignDto = campaignDto + .copyWith(finish: v); + }); + }, + ), + EnumParam( + 'Status', + PromoCodeCampaignStatus.values, + campaignDto.status, (v) { + setState(() { + campaignDto = campaignDto + .copyWith(status: v); + }); + }), + Text( + 'Created to generate promocodes\n' + 'Disabled to disable\n' + 'Dont use other statuses!', + style: TextStyle(fontSize: 12), + ), + Text('Products:'), + ListView.builder( + shrinkWrap: true, + itemCount: + campaignDto.products.length + + 1, + itemBuilder: (context, index) { + if (index == + campaignDto + .products.length) { + return ListTile( + title: Text('Add'), + onTap: () async { + final p = + await _showProductDialog( + context, null); + if (p != null) { + setState(() { + campaignDto.products + .add(p); + }); + } + }, + ); + } + final item = + campaignDto.products[index]; + return ListTile( + title: Text( + '${item.type.name} ${item.id ?? ''}'), + onTap: () async { + final p = + await _showProductDialog( + context, + item, + ); + if (p != null) { + setState(() { + campaignDto.products + .add(p); + }); + } + }, + trailing: IconButton( + icon: Icon(Icons.delete), + onPressed: () { + setState(() { + campaignDto.products + .remove(item); + }); + }, + ), + ); + }, + ), + ], + ), + ], + ), + ), + ], + ); + }), + ], + ); + }), + ), + ), + ); + }); + }); + } + + Future _showProductDialog( + BuildContext context, + MnemoCardsProduct? initialProduct, + ) async { + var prod = initialProduct ?? MnemoCardsProduct(); + final result = await showDialog( + context: context, + builder: (c) => Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Material( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextParam('Id', prod.id, (v) { + prod = prod.copyWith(id: v); + }), + EnumParam( + 'Type', + MnemoCardsProductType.values, + prod.type, + (v) { + prod = prod.copyWith(type: v); + }, + ), + MaterialButton( + onPressed: () { + Navigator.of(context).pop(prod); + }, + child: Text('Save'), + ), + ], + ), + ), + ), + ), + ); + return result; + } + + Widget uiEditor() { + return Column( + children: [], + ); + } +} diff --git a/lib/admin/admin_api.dart b/lib/admin/admin_api.dart index 93df968..4910804 100644 --- a/lib/admin/admin_api.dart +++ b/lib/admin/admin_api.dart @@ -136,4 +136,29 @@ class AdminApi with Api { ); return r.statusCode == 200; } + + Future deletePromoCodeCampaign(String id) async { + final r = await _dio.post( + '$path/promocode/delete/$id', + ); + return r.statusCode == 200; + } + + Future addPromoCodeCampaign(PromoCodesCampaignDto dto) async { + final r = await _dio.post( + '$path/promocode/add', + data: dto.encode(), + ); + return r.statusCode == 200; + } + + Future> getPromoCodeCampaigns() async { + final r = await _dio.get( + '$path/promocode/list', + ); + return (jsonDecode(r.data!)['campaigns'] as List) + .cast>() + .map(PromoCodesCampaignDto.fromJson) + .toList(); + } } diff --git a/lib/admin/date_param.dart b/lib/admin/date_param.dart new file mode 100644 index 0000000..a99226b --- /dev/null +++ b/lib/admin/date_param.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +class DateParam extends StatelessWidget { + final String title; + final DateTime? initial; + final Function(DateTime? v) onChanged; + + DateParam(this.title, this.initial, this.onChanged); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Text(title), + SizedBox( + width: 2, + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + final date = await showDatePicker( + context: context, + initialEntryMode: DatePickerEntryMode.calendar, + initialDate: initial ?? DateTime.now(), + firstDate: DateTime.utc(2000), + lastDate: DateTime.utc(2050), + ).then((selectedDate) { + // After selecting the date, display the time picker. + if (selectedDate != null) { + return showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ).then((selectedTime) { + // Handle the selected date and time here. + if (selectedTime != null) { + DateTime selectedDateTime = DateTime( + selectedDate.year, + selectedDate.month, + selectedDate.day, + selectedTime.hour, + selectedTime.minute, + ); + return selectedDateTime; + } + return null; + }); + } + return null; + }); + onChanged(date); + }, + child: Expanded( + child: Text( + initial?.let( + (d) => + '${d.year}.${d.month}.${d.day} ${d.hour}:${d.minute}', + ) ?? + 'No date', + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/admin/enum_param.dart b/lib/admin/enum_param.dart new file mode 100644 index 0000000..d1f205d --- /dev/null +++ b/lib/admin/enum_param.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class EnumParam extends StatelessWidget { + final String title; + final String? description; + final T initial; + final List values; + final Function(T v) onChanged; + + EnumParam( + this.title, + this.values, + this.initial, + this.onChanged, { + this.description, + }); + + @override + Widget build(BuildContext context) { + T value = initial; + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title), + if (description != null) + Text( + description!, + style: TextStyle(fontSize: 12), + ) + ], + ), + SizedBox( + width: 2, + ), + Expanded( + child: StatefulBuilder(builder: (context, setState) { + return DropdownButton( + value: value, + items: values + .map( + (v) => DropdownMenuItem( + child: Text(v.name), + value: v, + ), + ) + .toList(), + onChanged: (v) => setState(() { + value = v!; + onChanged(v!); + }), + ); + }), + ) + ], + ), + ); + } +} diff --git a/lib/admin/int_param.dart b/lib/admin/int_param.dart new file mode 100644 index 0000000..9451701 --- /dev/null +++ b/lib/admin/int_param.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class IntParam extends StatelessWidget { + final String title; + final int? initial; + final Function(int? v) onChanged; + + IntParam(this.title, this.initial, this.onChanged); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Text(title), + SizedBox( + width: 2, + ), + Expanded( + child: TextField( + controller: + TextEditingController(text: initial?.toString() ?? ''), + onChanged: (v) => onChanged(int.tryParse(v)), + ), + ), + ], + ), + ); + } +} diff --git a/lib/admin/text_param.dart b/lib/admin/text_param.dart index 29cbcea..b6e8ffe 100644 --- a/lib/admin/text_param.dart +++ b/lib/admin/text_param.dart @@ -3,10 +3,11 @@ import 'package:flutter/widgets.dart'; class TextParam extends StatelessWidget { final String title; + final String? description; final String? initial; final Function(String v) onChanged; - TextParam(this.title, this.initial, this.onChanged); + TextParam(this.title, this.initial, this.onChanged, {this.description}); @override Widget build(BuildContext context) { @@ -14,8 +15,21 @@ class TextParam extends StatelessWidget { padding: const EdgeInsets.all(8.0), child: Row( children: [ - Text(title), - SizedBox(width: 2,), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title), + if (description != null) + Text( + description!, + style: TextStyle(fontSize: 12), + ) + ], + ), + SizedBox( + width: 2, + ), Expanded( child: TextField( controller: TextEditingController(text: initial), diff --git a/lib/features/packs/pack_cache_manager.dart b/lib/features/packs/pack_cache_manager.dart index cbc9b58..7af5b0e 100644 --- a/lib/features/packs/pack_cache_manager.dart +++ b/lib/features/packs/pack_cache_manager.dart @@ -146,15 +146,15 @@ class PackCacheManager { Future savePack( CardPackDto dto, { - Map? images, + Map>? images, }) async { log('Saving pack ${dto.id}'); final dir = await _packDirectory(dto.id); final json = dto.encode(); log('Saving dto ${dto.id}'); - File('${dir.path}/pack.json') - ..createSync(recursive: true) - ..writeAsStringSync(json); + final file = File('${dir.path}/pack.json'); + await file.create(recursive: true); + await file.writeAsString(json); if (images != null) { log('Saving card images ${dto.id}'); @@ -163,7 +163,7 @@ class PackCacheManager { imageDir.deleteSync(recursive: true); } for (final card in dto.cards) { - final bytes = images[card.id.toString()]?.bytes; + final bytes = images[card.id.toString()]; if (bytes != null) { File('${imageDir.path}/${card.id}') ..createSync(recursive: true) diff --git a/lib/features/packs/pack_updater.dart b/lib/features/packs/pack_updater.dart index 38c8133..2256dae 100644 --- a/lib/features/packs/pack_updater.dart +++ b/lib/features/packs/pack_updater.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'dart:typed_data'; import 'package:archive/archive_io.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:mnemo_cards/features/packs/loading_packs_holder.dart'; import 'package:mnemo_cards/features/packs/preview_packs_poller.dart'; @@ -48,11 +49,18 @@ class PackUpdater { if (_updateCompleter?.isCompleted ?? true) { _updateCompleter = Completer(); } - unawaited(_updateAvailablePacks().whenComplete(() { + try { + print('Updating packs started'); + await _updateAvailablePacks(); + } catch (e, s) { + log('Error when updating packs', error: e, stackTrace: s); + print('Error when updating pack: ${e}'); + } finally { + print('Updating packs complete'); if (_updateCompleter?.isCompleted == false) { _updateCompleter?.complete(); } - })); + } return _updateCompleter!.future; } @@ -142,33 +150,17 @@ class PackUpdater { loadingDto.copyWith(message: 'Распаковываем карточки...'), ); final appVersion = (await PackageInfo.fromPlatform()).version; - final password = TokenGenerator.generateArchivePassword( - appVersion: appVersion, - packId: packId, + final images = await compute( + _decodeArchive, + _Envelope( + data: imagesData, + packId: packId, + appVersion: appVersion, + ), ); - print('generate token ${s.elapsedMilliseconds} mills'); - final imagesArchive = ZipDecoder().decodeBytes( - imagesData, - password: password, - ); - print('deflate archive ${s.elapsedMilliseconds} mills'); - Map images = {}; - - // todo optimize, make really async - Future setImage(ArchiveFile file) async { - final fileData = file.content as List; - final data = Uint8List.fromList(fileData); - images[file.name] = MemoryImage(data); - } - - await Future.wait([ - for (final file in imagesArchive) setImage(file), - ]); - _loadingPacksHolder.addLoadingPack( loadingDto.copyWith(message: 'Почти всё'), ); - print('loaded images ${s.elapsedMilliseconds} mills'); await _cacheManager.savePack(packDto, images: images); print('saved pack ${s.elapsedMilliseconds} mills'); log('Pack ${packId} updated with cards'); @@ -194,3 +186,35 @@ class PackUpdater { } } } + +Map> _decodeArchive(_Envelope envelope) { + final s = Stopwatch()..start(); + final password = TokenGenerator.generateArchivePassword( + appVersion: envelope.appVersion, + packId: envelope.packId, + ); + print('generate token ${s.elapsedMilliseconds} mills'); + final imagesArchive = ZipDecoder().decodeBytes( + envelope.data, + password: password, + ); + print('deflate archive ${s.elapsedMilliseconds} mills'); + final images = Map>(); + for (final file in imagesArchive) { + images[file.name] = file.content as List; + } + print('loaded images ${s.elapsedMilliseconds} mills'); + return images; +} + +class _Envelope { + final List data; + final String packId; + final String appVersion; + + _Envelope({ + required this.data, + required this.packId, + required this.appVersion, + }); +} diff --git a/lib/features/packs/packs_api.dart b/lib/features/packs/packs_api.dart index 650df21..9ece823 100644 --- a/lib/features/packs/packs_api.dart +++ b/lib/features/packs/packs_api.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:dio/dio.dart'; +import 'package:mnemo_cards/features/worker/worker_ext.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import '../../managers/repository/api.dart'; @@ -19,7 +20,7 @@ class PacksApi with Api { queryParameters: packData, ); final previews = - (r.data! as String).decodeList(CardPackPreviewDto.fromJson).toList(); + (r.data! as String).isolatedDecodeList(CardPackPreviewDto.fromJson); return previews; } @@ -31,7 +32,8 @@ class PacksApi with Api { '$path/packs/actions', queryParameters: packData, ); - final actions = (r.data! as String).decodeList(CardPackAction.fromJson); + final actions = + (r.data! as String).isolatedDecodeList(CardPackAction.fromJson); return actions; } @@ -44,7 +46,7 @@ class PacksApi with Api { receiveTimeout: Duration(seconds: 5), ), ); - return (r.data as String).decode(CardPackDto.fromJson); + return (r.data as String).isolatedDecode(CardPackDto.fromJson); } // buy page dto if not available @@ -56,7 +58,7 @@ class PacksApi with Api { receiveTimeout: Duration(seconds: 5), ), ); - return (r.data as String).decode(CardPackBuyDto.fromJson); + return (r.data as String).isolatedDecode(CardPackBuyDto.fromJson); } /// cards zip archive for available pack diff --git a/lib/features/purchase/google_purchase_handler.dart b/lib/features/purchase/google_purchase_handler.dart index 06a7194..9965212 100644 --- a/lib/features/purchase/google_purchase_handler.dart +++ b/lib/features/purchase/google_purchase_handler.dart @@ -68,6 +68,8 @@ class GooglePurchaseHandler implements PurchaseHandler { } await _buyInStore(products[index]); return; + case MnemoCardsProductType.discount: + return; case MnemoCardsProductType.unknown: Analytics.buyError( null, diff --git a/lib/features/purchase/purchase_deeplink_handler.dart b/lib/features/purchase/purchase_deeplink_handler.dart index ff1da9d..5ac0f96 100644 --- a/lib/features/purchase/purchase_deeplink_handler.dart +++ b/lib/features/purchase/purchase_deeplink_handler.dart @@ -55,6 +55,7 @@ class PurchaseDeeplinkHandler implements DeeplinkHandler { shouldUpdateHolder: true, ); } else { + locator.packUpdater.updateAvailablePacks(); locator.userManager.updateUser(); showInfoDialog('Ура! Теперь у вас есть подписка!'); } diff --git a/lib/features/worker/worker_ext.dart b/lib/features/worker/worker_ext.dart new file mode 100644 index 0000000..7c2ea8c --- /dev/null +++ b/lib/features/worker/worker_ext.dart @@ -0,0 +1,22 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; + +extension JsonStringExt on String { + + Future isolatedDecode([T Function(Map)? fromJson]) => + compute( + (_) => this.decode(fromJson), + null, + debugLabel: 'decode: $T', + ); + + Future> isolatedDecodeList( + [T Function(Map)? fromJson]) => + compute( + (_) => this.decodeList(fromJson), + null, + debugLabel: 'decodeList: $T', + ); +} diff --git a/lib/main.dart b/lib/main.dart index 3dfb203..41fa42f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -90,10 +90,10 @@ void main() async { locator.userManager.init().then((_) => print('userManager inited')), locator.favoriteCardsController.init().then((_) => print('favoriteCardsController inited')), locator.previewPackPoller.init().then((_) => print('previewPackPoller inited')), - locator.packUpdater.init().then((_) => print('packUpdater inited')), locator.deeplinkManager.init().then((_) => print('deeplinkManager inited')), ]); - print('initing complete ${stopwatch.elapsedMilliseconds}'); + await locator.packUpdater.init().then((_) => print('packUpdater inited')); + print('initing complete ${stopwatch.elapsedMilliseconds}'); if (!kIsWeb) unawaited(MobileAds.initialize()); print('mobile ads ${stopwatch.elapsedMilliseconds}'); print('runApp ${stopwatch.elapsedMilliseconds}'); diff --git a/lib/managers/repository/http_repository.dart b/lib/managers/repository/http_repository.dart index 7425f50..142f365 100644 --- a/lib/managers/repository/http_repository.dart +++ b/lib/managers/repository/http_repository.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:developer'; import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:mnemo_cards/features/worker/worker_ext.dart'; import 'package:mnemo_cards/managers/repository/repository.dart'; import 'package:mnemo_cards/managers/user_manager.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; @@ -30,7 +31,7 @@ class HttpRepository extends Repository with Api { '$path/user/promocode', data: PromoCodeDto(code: code).encode(), ); - return (r.data as String).decode(PromoCodeDto.fromJson); + return (r.data as String).isolatedDecode(PromoCodeDto.fromJson); } on DioException catch (e, s) { log('User create error', error: e, stackTrace: s); rethrow; @@ -74,7 +75,7 @@ class HttpRepository extends Repository with Api { Future getUser() async { final r = await _dio.get('$path/user'); if (r.statusCode == 200) { - return (r.data as String).decode(UserDto.fromJson); + return (r.data as String).isolatedDecode(UserDto.fromJson); } return null; } @@ -102,7 +103,7 @@ class HttpRepository extends Repository with Api { final r = await _dio.get( '$path/tests/$packId', ); - return (r.data as String).decodeList(TestDto.fromJson); + return (r.data as String).isolatedDecodeList(TestDto.fromJson); } @override @@ -111,7 +112,7 @@ class HttpRepository extends Repository with Api { '$path/test/$id', ); try { - return (r.data as String).decode(TestDto.fromJson); + return (r.data as String).isolatedDecode(TestDto.fromJson); } catch (e, s) { log(e.toString(), stackTrace: s); return null; @@ -156,7 +157,7 @@ class HttpRepository extends Repository with Api { 'system': paymentSystem.name, }, ); - return (r.data as String).decode(YookassaPaymentDto.fromJson); + return (r.data as String).isolatedDecode(YookassaPaymentDto.fromJson); } Future deleteUser(String id) async { diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart index 469c11b..6ce5daf 100644 --- a/lib/pages/profile_page.dart +++ b/lib/pages/profile_page.dart @@ -4,6 +4,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:mnemo_cards/admin/add_plan.dart'; +import 'package:mnemo_cards/admin/add_promocode_campaign.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/yandex_ads/yandex_ads.dart'; @@ -51,8 +52,12 @@ class ProfilePage extends StatelessWidget { trail: GestureDetector( onTap: () async { await locator.userManager.logout(); + final showAdmin = SHOW_ADMIN; final sp = await SharedPreferences.getInstance(); sp.clear(); + if (showAdmin) { + globalSharedPreferences.setBool('show_admin', true); + } AppRouter.openAuthOrProfile(); }, child: Row( @@ -72,71 +77,11 @@ class ProfilePage extends StatelessWidget { height: 8.h, ), Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: ListView( + physics: FixedExtentScrollPhysics(), children: [ - if (SHOW_ADMIN) ...[ - Row( - children: [ - Expanded( - child: MaterialButton( - onPressed: () { - // showErrorDialog('Технические шоколадки...\n\nПопробуйте позже или обновите приложение до последней версии'); - - showDialog( - context: context, - builder: (c) => const AllCards()); - }, - child: Text('Cards'), - ), - ), - Expanded( - child: MaterialButton( - onPressed: () { - showDialog( - context: context, - builder: (c) => const AllUsers()); - }, - child: Text('Users'), - ), - ), - Expanded( - child: MaterialButton( - onPressed: () { - AddPlan().addPlan(context); - }, - child: Text('Plans'), - ), - ), - ], - ), - Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () { - locator.packManager.clearCache(); - }, - child: Container( - alignment: Alignment.center, - margin: EdgeInsets.all(8.0), - child: Text('Clear cache'), - height: 40.h, - ), - ), - ), - Expanded( - child: MobileSharedPrefButton( - builder: (v, _) => - (v ?? false) ? Text('PROD') : Text('TEST'), - spKey: 'env', - setOnTap: (v) => !(v ?? false), - ), - ), - ], - ), - ], - Expanded( + SizedBox( + height: 300.h, child: StreamBuilder( stream: locator.userManager.userStateHolder.asStream, builder: (context, snapshot) { @@ -155,7 +100,6 @@ class ProfilePage extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0), child: Column( - mainAxisSize: MainAxisSize.min, children: [ Divider( height: 5, @@ -167,11 +111,11 @@ class ProfilePage extends StatelessWidget { color: borderGray.withOpacity(0.2), ), const MobileBuySubscriptionWidget(), + Divider( + height: 5, + color: borderGray.withOpacity(0.2), + ), if (SHOW_ADMIN) ...[ - Divider( - height: 5, - color: borderGray.withOpacity(0.2), - ), SimpleTile( title: Text('Посмотреть рекламу'), onTap: () { diff --git a/lib/pages/settgins_page.dart b/lib/pages/settgins_page.dart index 3d81130..95c3b28 100644 --- a/lib/pages/settgins_page.dart +++ b/lib/pages/settgins_page.dart @@ -9,7 +9,11 @@ import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; +import '../admin/add_plan.dart'; +import '../admin/add_promocode_campaign.dart'; import '../admin/admin_api.dart'; +import '../admin/all_cards.dart'; +import '../admin/all_users.dart'; import '../di/injector.dart'; import '../di/locator.dart'; import '../features/analytics/analytics.dart'; @@ -34,154 +38,186 @@ class SettingsPage extends StatelessWidget { SizedBox( height: 8.h, ), + Divider( + height: 1, + color: borderGray.withOpacity(0.2), + ), Expanded( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 10.0.w), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Divider( - height: 1, - color: borderGray.withOpacity(0.2), - ), - SimpleTile.text( - onLongTap: () async { - final r = await locator.userManager.deleteUser(); - if (!r) { - showInfoDialog( - 'Не удалось удалить аккаунт\nПопробуйте перелогиниться', - ); - } - }, - text: 'Удалить аккаунт (зажми)', - ), - Divider( - height: 1, - color: borderGray.withOpacity(0.2), - ), - MobileSharedPrefButton.builder( - spKey: 'dark_theme', - builder: (enabled, _) => AbsorbPointer( - child: SwitchTile( - text: 'Темная тема', - value: enabled ?? false, - ), - ), - setOnTap: (v) { - final darkTheme = !(v ?? false); - themeNotifier.value = - darkTheme ? ThemeMode.dark : ThemeMode.light; - return darkTheme; - }, - ), - Divider( - height: 1, - color: borderGray.withOpacity(0.2), - ), - MobileSharedPrefButton.builder( - spKey: 'sound_on', - builder: (enabled, _) => AbsorbPointer( - child: SwitchTile( - text: 'Звук', - value: enabled ?? false, - ), - ), - setOnTap: (v) => !(v ?? false), - ), - Divider( - height: 1, - color: borderGray.withOpacity(0.2), - ), - MobileSharedPrefButton.builder( - spKey: 'auto_play_sound_view', - builder: (enabled, _) => AbsorbPointer( - child: SwitchTile( - text: 'Авто воспроизведение при просмотре', - value: enabled ?? false, - ), - ), - setOnTap: (v) => !(v ?? false), - ), - Divider( - height: 1, - color: borderGray.withOpacity(0.2), - ), - MobileSharedPrefButton.builder( - spKey: 'auto_play_sound_tests', - builder: (enabled, _) => AbsorbPointer( - child: SwitchTile( - text: 'Авто воспроизведение в тестах', - value: enabled ?? false, - ), - ), - setOnTap: (v) => !(v ?? false), - ), - Divider( - height: 1, - color: borderGray.withOpacity(0.2), - ), - SimpleTile.text( - text: 'Скорость чтения', - trail: MobileSharedPrefButton.builder( - spKey: 'sound_speed', - builder: (v, s) => MnemoSlider( - onChanged: (value) => s(value), - value: v ?? 1.0, - ), - shouldRebuild: (v) => false, - ), - ), - Divider( - height: 1, - color: borderGray.withOpacity(0.2), - ), - if (ADMIN_BUILD) - MobileSharedPrefButton.builder( - spKey: 'show_admin', - builder: (enabled, _) => AbsorbPointer( - child: SwitchTile( - text: 'Show admin', - value: enabled ?? false, + child: ListView( + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10.0.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SimpleTile.text( + onLongTap: () async { + final r = await locator.userManager.deleteUser(); + if (!r) { + showInfoDialog( + 'Не удалось удалить аккаунт\nПопробуйте перелогиниться', + ); + } + }, + text: 'Удалить аккаунт (зажми)', ), - ), - setOnTap: (v) { - final val = !(v ?? false); - if (val && !getIt.isRegistered()) { - getIt.registerLazySingleton( - () => AdminApi(getIt.get().dio)); - print('set admin'); - } - return val; - }, + MobileSharedPrefButton.builder( + spKey: 'dark_theme', + builder: (enabled, _) => AbsorbPointer( + child: SwitchTile( + text: 'Темная тема', + value: enabled ?? false, + ), + ), + setOnTap: (v) { + final darkTheme = !(v ?? false); + themeNotifier.value = + darkTheme ? ThemeMode.dark : ThemeMode.light; + return darkTheme; + }, + ), + MobileSharedPrefButton.builder( + spKey: 'sound_on', + builder: (enabled, _) => AbsorbPointer( + child: SwitchTile( + text: 'Звук', + value: enabled ?? false, + ), + ), + setOnTap: (v) => !(v ?? false), + ), + MobileSharedPrefButton.builder( + spKey: 'auto_play_sound_view', + builder: (enabled, _) => AbsorbPointer( + child: SwitchTile( + text: 'Авто воспроизведение при просмотре', + value: enabled ?? false, + ), + ), + setOnTap: (v) => !(v ?? false), + ), + MobileSharedPrefButton.builder( + spKey: 'auto_play_sound_tests', + builder: (enabled, _) => AbsorbPointer( + child: SwitchTile( + text: 'Авто воспроизведение в тестах', + value: enabled ?? false, + ), + ), + setOnTap: (v) => !(v ?? false), + ), + SimpleTile.text( + text: 'Скорость чтения', + trail: MobileSharedPrefButton.builder( + spKey: 'sound_speed', + builder: (v, s) => MnemoSlider( + onChanged: (value) => s(value), + value: v ?? 1.0, + ), + shouldRebuild: (v) => false, + ), + ), + if (ADMIN_BUILD) + MobileSharedPrefButton.builder( + spKey: 'show_admin', + builder: (enabled, _) => AbsorbPointer( + child: SwitchTile( + text: 'Show admin', + value: enabled ?? false, + ), + ), + setOnTap: (v) { + final val = !(v ?? false); + if (val && !getIt.isRegistered()) { + getIt.registerLazySingleton( + () => AdminApi(getIt.get().dio)); + print('set admin'); + } + return val; + }, + ), + if (ADMIN_BUILD) ..._adminTiles(context), + ].expand((widget) => [ + widget, + Divider( + height: 1, + color: borderGray.withOpacity(0.2), + ), + ]).toList(), ), - ], - ), + ), + ), + Column( + children: [ + Padding( + padding: + EdgeInsets.only(left: 10.0.w, right: 10.w, top: 32.h), + child: LinkButton( + 'Политика конфиденциальности', + () { + launchUrl( + Uri.parse( + 'https://www.freeprivacypolicy.com/live/1c2bce51-86c6-4a36-b226-b6c6f50ebf0f'), + ); + }, + ), + ), + ], + ), + ], ), ), - Column( - children: [ - Padding( - padding: - EdgeInsets.only(left: 10.0.w, right: 10.w, top: 32.h), - child: LinkButton( - 'Политика конфиденциальности', - () { - launchUrl( - Uri.parse( - 'https://www.freeprivacypolicy.com/live/1c2bce51-86c6-4a36-b226-b6c6f50ebf0f'), - ); - }, - ), - ), - const BigBackButton(), - ], - ), + const BigBackButton(), ], ), ), ); } + List _adminTiles(BuildContext context) => [ + SimpleTile.text( + text: 'Cards', + onTap: () { + showDialog(context: context, builder: (c) => const AllCards()); + }, + ), + SimpleTile.text( + text: 'Users', + onTap: () { + showDialog(context: context, builder: (c) => const AllUsers()); + }, + ), + SimpleTile.text( + text: 'Plans', + onTap: () { + AddPlan().addPlan(context); + }, + ), + SimpleTile.text( + text: 'Promo codes', + onTap: () { + AddPromocodeCampaign().addCampaign(context); + }, + ), + SimpleTile.text( + text: 'Clear cache', + onTap: () { + locator.packManager.clearCache(); + }, + ), + MobileSharedPrefButton( + builder: (enabled, _) => AbsorbPointer( + child: SwitchTile( + text: 'Use prod', + value: enabled ?? false, + ), + ), + spKey: 'env', + setOnTap: (v) => !(v ?? false), + ), + ]; + Future clearAndExit() async { final sp = await SharedPreferences.getInstance(); sp.clear(); diff --git a/pubspec.lock b/pubspec.lock index e3a636b..563f3ed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -426,7 +426,7 @@ packages: source: hosted version: "0.5.9+1" firebase_core: - dependency: "direct main" + dependency: transitive description: name: firebase_core sha256: "3187f4f8e49968573fd7403011dca67ba95aae419bc0d8131500fae160d94f92" diff --git a/pubspec.yaml b/pubspec.yaml index da083c5..73b8c20 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: shared_preferences: ^2.2.3 flutter_secure_storage: ^9.2.2 rxdart: - firebase_core: ^3.3.0 +# firebase_core: ^3.3.0 # firebase_crashlytics: # firebase_storage: cloud_firestore: ^5.2.1