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;
}
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,19 +3,33 @@ 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) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title),
SizedBox(width: 2,),
if (description != null)
Text(
description!,
style: TextStyle(fontSize: 12),
)
],
),
SizedBox(
width: 2,
),
Expanded(
child: TextField(
controller: TextEditingController(text: initial),

View file

@ -146,15 +146,15 @@ class PackCacheManager {
Future<void> savePack(
CardPackDto dto, {
Map<String, MemoryImage>? images,
Map<String, List<int>>? 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)

View file

@ -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,
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<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(
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<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 '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

View file

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

View file

@ -55,6 +55,7 @@ class PurchaseDeeplinkHandler implements DeeplinkHandler {
shouldUpdateHolder: true,
);
} else {
locator.packUpdater.updateAvailablePacks();
locator.userManager.updateUser();
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,9 +90,9 @@ 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')),
]);
await locator.packUpdater.init().then((_) => print('packUpdater inited'));
print('initing complete ${stopwatch.elapsedMilliseconds}');
if (!kIsWeb) unawaited(MobileAds.initialize());
print('mobile ads ${stopwatch.elapsedMilliseconds}');

View file

@ -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<UserDto?> getUser() async {
final r = await _dio.get<String>('$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<String>(
'$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<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_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<bool>(
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(),
if (SHOW_ADMIN) ...[
Divider(
height: 5,
color: borderGray.withOpacity(0.2),
),
if (SHOW_ADMIN) ...[
SimpleTile(
title: Text('Посмотреть рекламу'),
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: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,16 +38,19 @@ class SettingsPage extends StatelessWidget {
SizedBox(
height: 8.h,
),
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
Expanded(
child: ListView(
children: [
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();
@ -55,10 +62,6 @@ class SettingsPage extends StatelessWidget {
},
text: 'Удалить аккаунт (зажми)',
),
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
MobileSharedPrefButton<bool>.builder(
spKey: 'dark_theme',
builder: (enabled, _) => AbsorbPointer(
@ -74,10 +77,6 @@ class SettingsPage extends StatelessWidget {
return darkTheme;
},
),
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
MobileSharedPrefButton<bool>.builder(
spKey: 'sound_on',
builder: (enabled, _) => AbsorbPointer(
@ -88,10 +87,6 @@ class SettingsPage extends StatelessWidget {
),
setOnTap: (v) => !(v ?? false),
),
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
MobileSharedPrefButton<bool>.builder(
spKey: 'auto_play_sound_view',
builder: (enabled, _) => AbsorbPointer(
@ -102,10 +97,6 @@ class SettingsPage extends StatelessWidget {
),
setOnTap: (v) => !(v ?? false),
),
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
MobileSharedPrefButton<bool>.builder(
spKey: 'auto_play_sound_tests',
builder: (enabled, _) => AbsorbPointer(
@ -116,10 +107,6 @@ class SettingsPage extends StatelessWidget {
),
setOnTap: (v) => !(v ?? false),
),
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
SimpleTile.text(
text: 'Скорость чтения',
trail: MobileSharedPrefButton<double>.builder(
@ -131,10 +118,6 @@ class SettingsPage extends StatelessWidget {
shouldRebuild: (v) => false,
),
),
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
if (ADMIN_BUILD)
MobileSharedPrefButton<bool>.builder(
spKey: 'show_admin',
@ -154,7 +137,14 @@ class SettingsPage extends StatelessWidget {
return val;
},
),
],
if (ADMIN_BUILD) ..._adminTiles(context),
].expand((widget) => [
widget,
Divider(
height: 1,
color: borderGray.withOpacity(0.2),
),
]).toList(),
),
),
),
@ -173,15 +163,61 @@ class SettingsPage extends StatelessWidget {
},
),
),
const BigBackButton(),
],
),
],
),
),
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 {
final sp = await SharedPreferences.getInstance();
sp.clear();

View file

@ -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"

View file

@ -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