pm
This commit is contained in:
parent
6587883e52
commit
16e8096047
19 changed files with 871 additions and 252 deletions
Binary file not shown.
382
lib/admin/add_promocode_campaign.dart
Normal file
382
lib/admin/add_promocode_campaign.dart
Normal 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: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
70
lib/admin/date_param.dart
Normal 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
64
lib/admin/enum_param.dart
Normal 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
32
lib/admin/int_param.dart
Normal 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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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('Ура! Теперь у вас есть подписка!');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
lib/features/worker/worker_ext.dart
Normal file
22
lib/features/worker/worker_ext.dart
Normal 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',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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}');
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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: () {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue