This commit is contained in:
Dmitry 2024-08-25 00:09:58 +03:00
parent 6587883e52
commit 16e8096047
19 changed files with 871 additions and 252 deletions

View file

@ -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<PromoCodesCampaignDto> _campaigns = [];
final availableStatuses = [
PromoCodeCampaignStatus.created,
PromoCodeCampaignStatus.preparing,
PromoCodeCampaignStatus.ready,
PromoCodeCampaignStatus.active,
PromoCodeCampaignStatus.disabled,
];
AddPromocodeCampaign() {
this.adminApi = getIt.get<AdminApi>();
_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<bool> _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<int?>(
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<MnemoCardsProduct?> _showProductDialog(
BuildContext context,
MnemoCardsProduct? initialProduct,
) async {
var prod = initialProduct ?? MnemoCardsProduct();
final result = await showDialog<MnemoCardsProduct?>(
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: [],
);
}
}

View file

@ -136,4 +136,29 @@ class AdminApi with Api {
); );
return r.statusCode == 200; return r.statusCode == 200;
} }
Future<bool> deletePromoCodeCampaign(String id) async {
final r = await _dio.post<String>(
'$path/promocode/delete/$id',
);
return r.statusCode == 200;
}
Future<bool> addPromoCodeCampaign(PromoCodesCampaignDto dto) async {
final r = await _dio.post<String>(
'$path/promocode/add',
data: dto.encode(),
);
return r.statusCode == 200;
}
Future<List<PromoCodesCampaignDto>> getPromoCodeCampaigns() async {
final r = await _dio.get<String>(
'$path/promocode/list',
);
return (jsonDecode(r.data!)['campaigns'] as List)
.cast<Map<String, Object?>>()
.map(PromoCodesCampaignDto.fromJson)
.toList();
}
} }

70
lib/admin/date_param.dart Normal file
View file

@ -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',
),
),
),
],
),
);
}
}

64
lib/admin/enum_param.dart Normal file
View file

@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class EnumParam<T extends Enum> extends StatelessWidget {
final String title;
final String? description;
final T initial;
final List<T> 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<T>(
value: value,
items: values
.map(
(v) => DropdownMenuItem(
child: Text(v.name),
value: v,
),
)
.toList(),
onChanged: (v) => setState(() {
value = v!;
onChanged(v!);
}),
);
}),
)
],
),
);
}
}

32
lib/admin/int_param.dart Normal file
View file

@ -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)),
),
),
],
),
);
}
}

View file

@ -3,10 +3,11 @@ import 'package:flutter/widgets.dart';
class TextParam extends StatelessWidget { class TextParam extends StatelessWidget {
final String title; final String title;
final String? description;
final String? initial; final String? initial;
final Function(String v) onChanged; final Function(String v) onChanged;
TextParam(this.title, this.initial, this.onChanged); TextParam(this.title, this.initial, this.onChanged, {this.description});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -14,8 +15,21 @@ class TextParam extends StatelessWidget {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Row(
children: [ children: [
Text(title), Column(
SizedBox(width: 2,), mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title),
if (description != null)
Text(
description!,
style: TextStyle(fontSize: 12),
)
],
),
SizedBox(
width: 2,
),
Expanded( Expanded(
child: TextField( child: TextField(
controller: TextEditingController(text: initial), controller: TextEditingController(text: initial),

View file

@ -146,15 +146,15 @@ class PackCacheManager {
Future<void> savePack( Future<void> savePack(
CardPackDto dto, { CardPackDto dto, {
Map<String, MemoryImage>? images, Map<String, List<int>>? images,
}) async { }) async {
log('Saving pack ${dto.id}'); log('Saving pack ${dto.id}');
final dir = await _packDirectory(dto.id); final dir = await _packDirectory(dto.id);
final json = dto.encode(); final json = dto.encode();
log('Saving dto ${dto.id}'); log('Saving dto ${dto.id}');
File('${dir.path}/pack.json') final file = File('${dir.path}/pack.json');
..createSync(recursive: true) await file.create(recursive: true);
..writeAsStringSync(json); await file.writeAsString(json);
if (images != null) { if (images != null) {
log('Saving card images ${dto.id}'); log('Saving card images ${dto.id}');
@ -163,7 +163,7 @@ class PackCacheManager {
imageDir.deleteSync(recursive: true); imageDir.deleteSync(recursive: true);
} }
for (final card in dto.cards) { for (final card in dto.cards) {
final bytes = images[card.id.toString()]?.bytes; final bytes = images[card.id.toString()];
if (bytes != null) { if (bytes != null) {
File('${imageDir.path}/${card.id}') File('${imageDir.path}/${card.id}')
..createSync(recursive: true) ..createSync(recursive: true)

View file

@ -3,6 +3,7 @@ import 'dart:developer';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:archive/archive_io.dart'; import 'package:archive/archive_io.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:mnemo_cards/features/packs/loading_packs_holder.dart'; import 'package:mnemo_cards/features/packs/loading_packs_holder.dart';
import 'package:mnemo_cards/features/packs/preview_packs_poller.dart'; import 'package:mnemo_cards/features/packs/preview_packs_poller.dart';
@ -48,11 +49,18 @@ class PackUpdater {
if (_updateCompleter?.isCompleted ?? true) { if (_updateCompleter?.isCompleted ?? true) {
_updateCompleter = Completer(); _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) { if (_updateCompleter?.isCompleted == false) {
_updateCompleter?.complete(); _updateCompleter?.complete();
} }
})); }
return _updateCompleter!.future; return _updateCompleter!.future;
} }
@ -142,33 +150,17 @@ class PackUpdater {
loadingDto.copyWith(message: 'Распаковываем карточки...'), loadingDto.copyWith(message: 'Распаковываем карточки...'),
); );
final appVersion = (await PackageInfo.fromPlatform()).version; final appVersion = (await PackageInfo.fromPlatform()).version;
final password = TokenGenerator.generateArchivePassword( final images = await compute(
appVersion: appVersion, _decodeArchive,
packId: packId, _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<String, MemoryImage> images = {};
// todo optimize, make really async
Future<void> setImage(ArchiveFile file) async {
final fileData = file.content as List<int>;
final data = Uint8List.fromList(fileData);
images[file.name] = MemoryImage(data);
}
await Future.wait([
for (final file in imagesArchive) setImage(file),
]);
_loadingPacksHolder.addLoadingPack( _loadingPacksHolder.addLoadingPack(
loadingDto.copyWith(message: 'Почти всё'), loadingDto.copyWith(message: 'Почти всё'),
); );
print('loaded images ${s.elapsedMilliseconds} mills');
await _cacheManager.savePack(packDto, images: images); await _cacheManager.savePack(packDto, images: images);
print('saved pack ${s.elapsedMilliseconds} mills'); print('saved pack ${s.elapsedMilliseconds} mills');
log('Pack ${packId} updated with cards'); log('Pack ${packId} updated with cards');
@ -194,3 +186,35 @@ class PackUpdater {
} }
} }
} }
Map<String, List<int>> _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<String, List<int>>();
for (final file in imagesArchive) {
images[file.name] = file.content as List<int>;
}
print('loaded images ${s.elapsedMilliseconds} mills');
return images;
}
class _Envelope {
final List<int> data;
final String packId;
final String appVersion;
_Envelope({
required this.data,
required this.packId,
required this.appVersion,
});
}

View file

@ -1,6 +1,7 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:mnemo_cards/features/worker/worker_ext.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
import '../../managers/repository/api.dart'; import '../../managers/repository/api.dart';
@ -19,7 +20,7 @@ class PacksApi with Api {
queryParameters: packData, queryParameters: packData,
); );
final previews = final previews =
(r.data! as String).decodeList(CardPackPreviewDto.fromJson).toList(); (r.data! as String).isolatedDecodeList(CardPackPreviewDto.fromJson);
return previews; return previews;
} }
@ -31,7 +32,8 @@ class PacksApi with Api {
'$path/packs/actions', '$path/packs/actions',
queryParameters: packData, queryParameters: packData,
); );
final actions = (r.data! as String).decodeList(CardPackAction.fromJson); final actions =
(r.data! as String).isolatedDecodeList(CardPackAction.fromJson);
return actions; return actions;
} }
@ -44,7 +46,7 @@ class PacksApi with Api {
receiveTimeout: Duration(seconds: 5), 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 // buy page dto if not available
@ -56,7 +58,7 @@ class PacksApi with Api {
receiveTimeout: Duration(seconds: 5), 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 /// cards zip archive for available pack

View file

@ -68,6 +68,8 @@ class GooglePurchaseHandler implements PurchaseHandler {
} }
await _buyInStore(products[index]); await _buyInStore(products[index]);
return; return;
case MnemoCardsProductType.discount:
return;
case MnemoCardsProductType.unknown: case MnemoCardsProductType.unknown:
Analytics.buyError( Analytics.buyError(
null, null,

View file

@ -55,6 +55,7 @@ class PurchaseDeeplinkHandler implements DeeplinkHandler {
shouldUpdateHolder: true, shouldUpdateHolder: true,
); );
} else { } else {
locator.packUpdater.updateAvailablePacks();
locator.userManager.updateUser(); locator.userManager.updateUser();
showInfoDialog('Ура! Теперь у вас есть подписка!'); showInfoDialog('Ура! Теперь у вас есть подписка!');
} }

View file

@ -0,0 +1,22 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
extension JsonStringExt<T> on String {
Future<T> isolatedDecode<T>([T Function(Map<String, dynamic>)? fromJson]) =>
compute(
(_) => this.decode(fromJson),
null,
debugLabel: 'decode: $T',
);
Future<List<T>> isolatedDecodeList<T>(
[T Function(Map<String, dynamic>)? fromJson]) =>
compute(
(_) => this.decodeList(fromJson),
null,
debugLabel: 'decodeList: $T',
);
}

View file

@ -90,10 +90,10 @@ void main() async {
locator.userManager.init().then((_) => print('userManager inited')), locator.userManager.init().then((_) => print('userManager inited')),
locator.favoriteCardsController.init().then((_) => print('favoriteCardsController inited')), locator.favoriteCardsController.init().then((_) => print('favoriteCardsController inited')),
locator.previewPackPoller.init().then((_) => print('previewPackPoller inited')), locator.previewPackPoller.init().then((_) => print('previewPackPoller inited')),
locator.packUpdater.init().then((_) => print('packUpdater inited')),
locator.deeplinkManager.init().then((_) => print('deeplinkManager 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()); if (!kIsWeb) unawaited(MobileAds.initialize());
print('mobile ads ${stopwatch.elapsedMilliseconds}'); print('mobile ads ${stopwatch.elapsedMilliseconds}');
print('runApp ${stopwatch.elapsedMilliseconds}'); print('runApp ${stopwatch.elapsedMilliseconds}');

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; 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/repository/repository.dart';
import 'package:mnemo_cards/managers/user_manager.dart'; import 'package:mnemo_cards/managers/user_manager.dart';
import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart';
@ -30,7 +31,7 @@ class HttpRepository extends Repository with Api {
'$path/user/promocode', '$path/user/promocode',
data: PromoCodeDto(code: code).encode(), 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) { } on DioException catch (e, s) {
log('User create error', error: e, stackTrace: s); log('User create error', error: e, stackTrace: s);
rethrow; rethrow;
@ -74,7 +75,7 @@ class HttpRepository extends Repository with Api {
Future<UserDto?> getUser() async { Future<UserDto?> getUser() async {
final r = await _dio.get<String>('$path/user'); final r = await _dio.get<String>('$path/user');
if (r.statusCode == 200) { if (r.statusCode == 200) {
return (r.data as String).decode(UserDto.fromJson); return (r.data as String).isolatedDecode(UserDto.fromJson);
} }
return null; return null;
} }
@ -102,7 +103,7 @@ class HttpRepository extends Repository with Api {
final r = await _dio.get<String>( final r = await _dio.get<String>(
'$path/tests/$packId', '$path/tests/$packId',
); );
return (r.data as String).decodeList(TestDto.fromJson); return (r.data as String).isolatedDecodeList(TestDto.fromJson);
} }
@override @override
@ -111,7 +112,7 @@ class HttpRepository extends Repository with Api {
'$path/test/$id', '$path/test/$id',
); );
try { try {
return (r.data as String).decode(TestDto.fromJson); return (r.data as String).isolatedDecode(TestDto.fromJson);
} catch (e, s) { } catch (e, s) {
log(e.toString(), stackTrace: s); log(e.toString(), stackTrace: s);
return null; return null;
@ -156,7 +157,7 @@ class HttpRepository extends Repository with Api {
'system': paymentSystem.name, 'system': paymentSystem.name,
}, },
); );
return (r.data as String).decode(YookassaPaymentDto.fromJson); return (r.data as String).isolatedDecode(YookassaPaymentDto.fromJson);
} }
Future<bool> deleteUser(String id) async { Future<bool> deleteUser(String id) async {

View file

@ -4,6 +4,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:mnemo_cards/admin/add_plan.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.dart';
import 'package:mnemo_cards/domain/router/app_router.gr.dart'; import 'package:mnemo_cards/domain/router/app_router.gr.dart';
import 'package:mnemo_cards/features/yandex_ads/yandex_ads.dart'; import 'package:mnemo_cards/features/yandex_ads/yandex_ads.dart';
@ -51,8 +52,12 @@ class ProfilePage extends StatelessWidget {
trail: GestureDetector( trail: GestureDetector(
onTap: () async { onTap: () async {
await locator.userManager.logout(); await locator.userManager.logout();
final showAdmin = SHOW_ADMIN;
final sp = await SharedPreferences.getInstance(); final sp = await SharedPreferences.getInstance();
sp.clear(); sp.clear();
if (showAdmin) {
globalSharedPreferences.setBool('show_admin', true);
}
AppRouter.openAuthOrProfile(); AppRouter.openAuthOrProfile();
}, },
child: Row( child: Row(
@ -72,71 +77,11 @@ class ProfilePage extends StatelessWidget {
height: 8.h, height: 8.h,
), ),
Expanded( Expanded(
child: Column( child: ListView(
mainAxisAlignment: MainAxisAlignment.spaceBetween, physics: FixedExtentScrollPhysics(),
children: [ children: [
if (SHOW_ADMIN) ...[ SizedBox(
Row( height: 300.h,
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<bool>(
builder: (v, _) =>
(v ?? false) ? Text('PROD') : Text('TEST'),
spKey: 'env',
setOnTap: (v) => !(v ?? false),
),
),
],
),
],
Expanded(
child: StreamBuilder( child: StreamBuilder(
stream: locator.userManager.userStateHolder.asStream, stream: locator.userManager.userStateHolder.asStream,
builder: (context, snapshot) { builder: (context, snapshot) {
@ -155,7 +100,6 @@ class ProfilePage extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0), padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Divider( Divider(
height: 5, height: 5,
@ -167,11 +111,11 @@ class ProfilePage extends StatelessWidget {
color: borderGray.withOpacity(0.2), color: borderGray.withOpacity(0.2),
), ),
const MobileBuySubscriptionWidget(), const MobileBuySubscriptionWidget(),
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
if (SHOW_ADMIN) ...[ if (SHOW_ADMIN) ...[
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
SimpleTile( SimpleTile(
title: Text('Посмотреть рекламу'), title: Text('Посмотреть рекламу'),
onTap: () { onTap: () {

View file

@ -9,7 +9,11 @@ import 'package:mnemo_cards_frontend_common/mnemo_cards_frontend_common.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.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/admin_api.dart';
import '../admin/all_cards.dart';
import '../admin/all_users.dart';
import '../di/injector.dart'; import '../di/injector.dart';
import '../di/locator.dart'; import '../di/locator.dart';
import '../features/analytics/analytics.dart'; import '../features/analytics/analytics.dart';
@ -34,154 +38,186 @@ class SettingsPage extends StatelessWidget {
SizedBox( SizedBox(
height: 8.h, height: 8.h,
), ),
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
Expanded( Expanded(
child: Padding( child: ListView(
padding: EdgeInsets.symmetric(horizontal: 10.0.w), children: [
child: Column( Expanded(
mainAxisAlignment: MainAxisAlignment.end, child: Padding(
children: [ padding: EdgeInsets.symmetric(horizontal: 10.0.w),
Divider( child: Column(
height: 1, mainAxisAlignment: MainAxisAlignment.end,
color: borderGray.withOpacity(0.2), children: [
), SimpleTile.text(
SimpleTile.text( onLongTap: () async {
onLongTap: () async { final r = await locator.userManager.deleteUser();
final r = await locator.userManager.deleteUser(); if (!r) {
if (!r) { showInfoDialog(
showInfoDialog( 'Не удалось удалить аккаунт\nПопробуйте перелогиниться',
'Не удалось удалить аккаунт\nПопробуйте перелогиниться', );
); }
} },
}, text: 'Удалить аккаунт (зажми)',
text: 'Удалить аккаунт (зажми)',
),
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
MobileSharedPrefButton<bool>.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<bool>.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<bool>.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<bool>.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<double>.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<bool>.builder(
spKey: 'show_admin',
builder: (enabled, _) => AbsorbPointer(
child: SwitchTile(
text: 'Show admin',
value: enabled ?? false,
), ),
), MobileSharedPrefButton<bool>.builder(
setOnTap: (v) { spKey: 'dark_theme',
final val = !(v ?? false); builder: (enabled, _) => AbsorbPointer(
if (val && !getIt.isRegistered<AdminApi>()) { child: SwitchTile(
getIt.registerLazySingleton<AdminApi>( text: 'Темная тема',
() => AdminApi(getIt.get<DioProvider>().dio)); value: enabled ?? false,
print('set admin'); ),
} ),
return val; setOnTap: (v) {
}, final darkTheme = !(v ?? false);
themeNotifier.value =
darkTheme ? ThemeMode.dark : ThemeMode.light;
return darkTheme;
},
),
MobileSharedPrefButton<bool>.builder(
spKey: 'sound_on',
builder: (enabled, _) => AbsorbPointer(
child: SwitchTile(
text: 'Звук',
value: enabled ?? false,
),
),
setOnTap: (v) => !(v ?? false),
),
MobileSharedPrefButton<bool>.builder(
spKey: 'auto_play_sound_view',
builder: (enabled, _) => AbsorbPointer(
child: SwitchTile(
text: 'Авто воспроизведение при просмотре',
value: enabled ?? false,
),
),
setOnTap: (v) => !(v ?? false),
),
MobileSharedPrefButton<bool>.builder(
spKey: 'auto_play_sound_tests',
builder: (enabled, _) => AbsorbPointer(
child: SwitchTile(
text: 'Авто воспроизведение в тестах',
value: enabled ?? false,
),
),
setOnTap: (v) => !(v ?? false),
),
SimpleTile.text(
text: 'Скорость чтения',
trail: MobileSharedPrefButton<double>.builder(
spKey: 'sound_speed',
builder: (v, s) => MnemoSlider(
onChanged: (value) => s(value),
value: v ?? 1.0,
),
shouldRebuild: (v) => false,
),
),
if (ADMIN_BUILD)
MobileSharedPrefButton<bool>.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<AdminApi>()) {
getIt.registerLazySingleton<AdminApi>(
() => AdminApi(getIt.get<DioProvider>().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( const BigBackButton(),
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(),
],
),
], ],
), ),
), ),
); );
} }
List<Widget> _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<bool>(
builder: (enabled, _) => AbsorbPointer(
child: SwitchTile(
text: 'Use prod',
value: enabled ?? false,
),
),
spKey: 'env',
setOnTap: (v) => !(v ?? false),
),
];
Future<void> clearAndExit() async { Future<void> clearAndExit() async {
final sp = await SharedPreferences.getInstance(); final sp = await SharedPreferences.getInstance();
sp.clear(); sp.clear();

View file

@ -426,7 +426,7 @@ packages:
source: hosted source: hosted
version: "0.5.9+1" version: "0.5.9+1"
firebase_core: firebase_core:
dependency: "direct main" dependency: transitive
description: description:
name: firebase_core name: firebase_core
sha256: "3187f4f8e49968573fd7403011dca67ba95aae419bc0d8131500fae160d94f92" sha256: "3187f4f8e49968573fd7403011dca67ba95aae419bc0d8131500fae160d94f92"

View file

@ -23,7 +23,7 @@ dependencies:
shared_preferences: ^2.2.3 shared_preferences: ^2.2.3
flutter_secure_storage: ^9.2.2 flutter_secure_storage: ^9.2.2
rxdart: rxdart:
firebase_core: ^3.3.0 # firebase_core: ^3.3.0
# firebase_crashlytics: # firebase_crashlytics:
# firebase_storage: # firebase_storage:
cloud_firestore: ^5.2.1 cloud_firestore: ^5.2.1