Compare commits

...

31 Commits

Author SHA1 Message Date
misha
dc400ef5b7 feat(personalDiets): fixed notification and chat banner 2025-11-10 13:50:06 +03:00
misha
992cb38cdd feat(personalDiets): fixed ui bugs and added new notification type for personal diet 2025-11-06 08:00:30 +03:00
ff946a5938 Merge pull request 'feat(personalDiets)' (#561) from misha/eatvio_app:personalDiets into main
Reviewed-on: eatvio/eatvio_app#561
2025-10-31 10:27:20 +00:00
misha
a72c40d8ee Merge remote-tracking branch 'upstream/main' into personalDiets 2025-10-31 13:26:05 +03:00
misha
6ed7faed15 feat(personalDiets): addde base url from app config for consultation widget 2025-10-31 13:23:13 +03:00
misha
16fccaea8e Merge remote-tracking branch 'upstream/main' into personalDiets 2025-10-31 12:41:36 +03:00
Username1548
a723673d23 Merge branch 'main' of http://gitea.office.local:3000/eatvio/eatvio_app 2025-10-31 12:39:49 +03:00
Username1548
89f697f7a6 changed app version 2025-10-31 12:38:02 +03:00
d883c9e19a Merge pull request 'feat(marketing)' (#745) from misha/eatvio_app:issue-689 into main
Reviewed-on: eatvio/eatvio_app#745
2025-10-31 09:10:18 +00:00
misha
884830f232 Merge remote-tracking branch 'upstream/main' into issue-689 2025-10-31 12:08:02 +03:00
mso7nueh
7b297f954e Merge pull request 'Issue-746' (#747) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#747
2025-10-31 08:59:06 +00:00
mso7nueh
dbd92a65ea feat(md): Issue-746 2025-10-31 00:20:21 +03:00
mso7nueh
f3af743487 merge upstream 2025-10-30 19:27:36 +00:00
Username1548
c95f1988ae commit to create new build 2025-10-30 17:13:58 +03:00
mso7nueh
35a5e6d6be merge upstream 2025-10-30 09:13:28 +00:00
mso7nueh
977b7150fe feat(md): Issue-746 2025-10-29 17:27:05 +03:00
5b81014e72 Merge pull request 'feat(InfoByOrder)' (#750) from misha/eatvio_app:issue-681 into main
Reviewed-on: eatvio/eatvio_app#750
2025-10-29 01:39:05 +00:00
mso7nueh
8799375880 feat(md): Issue-746 2025-10-27 17:07:20 +03:00
mso7nueh
fb5a0d52bc feat(feed): mdToHtml 2025-10-24 18:55:01 +03:00
misha
2e3172bc45 Merge remote-tracking branch 'upstream/main' into personalDiets 2025-10-24 13:21:03 +03:00
misha
41313f09af feat(marketing): added marketing bottom sheet 2025-10-23 17:25:12 +03:00
misha
46841fb502 feat(personalDiets): fixed PlanState widget in order to show personal diet state 2025-10-02 13:49:58 +03:00
misha
8e72350152 Merge remote-tracking branch 'upstream/main' into personalDiets 2025-10-02 12:25:40 +03:00
Username1548
8c2cd5eedd Merge remote-tracking branch 'upstream/main' into personalDiets 2025-09-05 11:28:51 +03:00
Username1548
996f2ecd70 Merge branch 'main' into personalDiets 2025-07-16 17:04:21 +07:00
Username1548
19787f459e feat(personalDiet): fixed wrong path for attach diet method 2025-07-16 17:03:46 +07:00
Username1548
7e64b24a8c added layout for suggestions page 2025-07-15 21:49:47 +07:00
Username1548
a07d085ba7 feat(personalDiets): preapared chats for consultations with personal diets 2025-07-14 20:24:40 +07:00
Username1548
5616bedd25 feat(personalDiets): diets suggestions 2025-07-14 13:44:44 +07:00
Username1548
0a3d9e577d Merge branch 'main' into personalDiets 2025-07-14 12:17:37 +07:00
Username1548
55e3abec99 feat(personal diets): added personal diet option to plans 2025-07-14 12:15:26 +07:00
69 changed files with 6183 additions and 3925 deletions

9
assets/icons/logo_2.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg width="89" height="90" viewBox="0 0 89 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M45.0119 0C65.9947 0 83 9.05026 83 29.6764C83 50.2792 65.9947 67 45.0119 67C24.0053 67 7 50.2792 7 29.6764C7 9.05026 24.0053 0 45.0119 0Z" fill="#43A6F2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.8844 43.5162C39.8585 43.5162 34.9274 42.5623 30.3282 40.7209C25.7291 38.8573 21.5803 36.1284 18.1428 32.7119C16.5544 31.1589 16.6493 28.6741 18.3087 27.2098C19.9919 25.7234 22.6234 25.8122 24.2118 27.3651C26.867 30.0052 30.0675 32.1129 33.6235 33.5327C37.1796 34.9748 40.9964 35.7069 44.8844 35.7069C48.7486 35.7069 52.5655 34.9748 56.1215 33.5327C59.6776 32.1129 62.878 30.0052 65.5332 27.3651C67.1216 25.8122 69.7531 25.7234 71.4363 27.2098C73.0958 28.6741 73.1906 31.1589 71.6022 32.7119C68.1647 36.1284 64.016 38.8573 59.4168 40.7209C54.8176 42.5623 49.8866 43.5162 44.8844 43.5162Z" fill="white"/>
<path d="M9.69721 80.8957L3.51318 88.877C2.93793 89.6257 2.28049 90 1.54087 90C0.513623 90 0 89.3382 0 88.0147V77.3663C0 75.7888 0.56841 75 1.70523 75C2.84205 75 3.41046 75.7888 3.41046 77.3663V84.2246L9.71775 76.0829C10.2793 75.361 10.882 75 11.5257 75C12.5803 75 13.1077 75.7487 13.1077 77.246V87.6337C13.1077 89.2112 12.5393 90 11.4024 90C10.2656 90 9.69721 89.2112 9.69721 87.6337V80.8957Z" fill="#323F4B"/>
<path d="M23.0017 78.2888C21.769 78.2888 21.0019 78.2553 20.7006 78.1885C19.7829 78.0013 19.3241 77.5201 19.3241 76.7447C19.3241 75.7019 20.1322 75.1805 21.7484 75.1805H27.6859C29.3021 75.1805 30.1102 75.7019 30.1102 76.7447C30.1102 77.5201 29.6514 78.0013 28.7337 78.1885C28.4324 78.2553 27.6654 78.2888 26.4327 78.2888V87.6337C26.4327 89.2112 25.8642 90 24.7274 90C23.5769 90 23.0017 89.2112 23.0017 87.6337V78.2888Z" fill="#323F4B"/>
<path d="M38.6564 89.8195C37.7935 89.8195 37.1909 89.6791 36.8485 89.3984C36.506 89.1043 36.3348 88.5428 36.3348 87.7139V77.5267C36.3348 76.6578 36.506 76.0495 36.8485 75.7019C37.1909 75.3543 37.8072 75.1805 38.6975 75.1805H41.4916C42.8476 75.1805 43.9981 75.4545 44.9432 76.0027C46.0937 76.6578 46.6689 77.6003 46.6689 78.8302C46.6689 80.4211 46.08 81.4973 44.9021 82.0588V82.119C45.7787 82.3864 46.4087 82.7807 46.7922 83.3021C47.1757 83.8102 47.3675 84.512 47.3675 85.4078C47.3675 86.8917 46.8401 88.0214 45.7855 88.7968C44.8541 89.4786 43.6009 89.8195 42.0258 89.8195H38.6564ZM39.7453 83.5027V86.9519H41.4505C42.1764 86.9519 42.7175 86.8583 43.0736 86.6711C43.5393 86.4171 43.7721 85.9626 43.7721 85.3075C43.7721 84.5455 43.5461 84.0374 43.0941 83.7834C42.7654 83.5963 42.197 83.5027 41.3889 83.5027H39.7453ZM39.7453 78.0281V80.8957H41.2245C41.8957 80.8957 42.3682 80.8155 42.6421 80.6551C43.0256 80.4412 43.2174 80.0401 43.2174 79.4519C43.2174 78.5027 42.5531 78.0281 41.2245 78.0281H39.7453Z" fill="#323F4B"/>
<path d="M64.1493 80.8957L57.9653 88.877C57.39 89.6257 56.7326 90 55.993 90C54.9657 90 54.4521 89.3382 54.4521 88.0147V77.3663C54.4521 75.7888 55.0205 75 56.1573 75C57.2941 75 57.8626 75.7888 57.8626 77.3663V84.2246L64.1698 76.0829C64.7314 75.361 65.3341 75 65.9778 75C67.0324 75 67.5598 75.7487 67.5598 77.246V87.6337C67.5598 89.2112 66.9913 90 65.8545 90C64.7177 90 64.1493 89.2112 64.1493 87.6337V80.8957Z" fill="#323F4B"/>
<path d="M74.6391 82.5201C74.6391 80.3275 75.2691 78.5428 76.5292 77.1658C77.8304 75.7219 79.5904 75 81.8093 75C84.0418 75 85.8087 75.7152 87.1099 77.1457C88.37 78.5227 89 80.3142 89 82.5201C89 84.7126 88.37 86.4973 87.1099 87.8743C85.795 89.2914 84.0281 90 81.8093 90C79.5904 90 77.8304 89.2848 76.5292 87.8543C75.2691 86.4906 74.6391 84.7126 74.6391 82.5201ZM78.255 82.5602C78.255 83.7634 78.5358 84.7594 79.0973 85.5481C79.7274 86.4706 80.6314 86.9318 81.8093 86.9318C82.9872 86.9318 83.898 86.4773 84.5417 85.5682C85.1033 84.766 85.3841 83.7634 85.3841 82.5602C85.3841 81.3168 85.117 80.2941 84.5828 79.492C83.9391 78.5428 83.0146 78.0682 81.8093 78.0682C80.604 78.0682 79.6863 78.5428 79.0563 79.492C78.5221 80.2941 78.255 81.3168 78.255 82.5602Z" fill="#323F4B"/>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
assets/images/logo_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -406,7 +406,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.2.3;
MARKETING_VERSION = 1.3.3;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
@@ -557,7 +557,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.2.3;
MARKETING_VERSION = 1.3.3;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
@@ -596,7 +596,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.2.3;
MARKETING_VERSION = 1.3.3;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>81</string>
<string>82</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -0,0 +1,502 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
class MarkdownEditor extends StatefulWidget {
const MarkdownEditor({
super.key,
this.initialText,
this.hintText,
this.hasError = false,
this.minLines = 4,
this.maxLines = 6,
this.heightPreview = 120,
this.onChanged,
this.onToolbarVisibilityChanged,
this.showInlineToolbar = true,
});
final String? initialText;
final String? hintText;
final bool hasError;
final int minLines;
final int maxLines;
final double heightPreview;
final void Function(String value)? onChanged;
final void Function(bool visible)? onToolbarVisibilityChanged;
final bool showInlineToolbar;
@override
State<MarkdownEditor> createState() => MarkdownEditorState();
}
class MarkdownEditorState extends State<MarkdownEditor> {
late final TextEditingController _controller;
final FocusNode _focusNode = FocusNode();
bool _isPreview = false;
bool _enablePanel = false;
late final KeyboardVisibilityController _keyboardVisibilityController;
late final StreamSubscription<bool> _keyboardSubscription;
bool get isToolbarVisible => _enablePanel || _isPreview;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialText ?? '');
_keyboardVisibilityController = KeyboardVisibilityController();
_keyboardSubscription = _keyboardVisibilityController.onChange.listen((visible) {
setState(() {
_enablePanel = visible;
});
widget.onToolbarVisibilityChanged?.call(isToolbarVisible);
});
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onToolbarVisibilityChanged?.call(isToolbarVisible);
});
}
@override
void dispose() {
_keyboardSubscription.cancel();
_focusNode.dispose();
_controller.dispose();
super.dispose();
}
bool _hasFormatting(String prefix, String suffix) {
final text = _controller.text;
final selection = _controller.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
return selectedText.startsWith(prefix) && selectedText.endsWith(suffix);
} else {
final cursorPosition = selection.baseOffset;
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
final beforeCursor = text.substring(cursorPosition - prefix.length, cursorPosition);
final afterCursor = text.substring(cursorPosition, cursorPosition + suffix.length);
return beforeCursor == prefix && afterCursor == suffix;
}
}
return false;
}
void _removeFormatting(String prefix, String suffix) {
final text = _controller.text;
final selection = _controller.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
final unformattedText = selectedText.substring(prefix.length, selectedText.length - suffix.length);
final newText = text.replaceRange(selection.start, selection.end, unformattedText);
_controller.text = newText;
_controller.selection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + unformattedText.length);
} else {
final cursorPosition = selection.baseOffset;
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
final beforeCursor = text.substring(0, cursorPosition - prefix.length);
final afterCursor = text.substring(cursorPosition + suffix.length);
final newText = beforeCursor + afterCursor;
_controller.text = newText;
_controller.selection = TextSelection.collapsed(offset: cursorPosition - prefix.length);
}
}
widget.onChanged?.call(_controller.text);
}
void _applyMarkdownFormat(String prefix, String suffix) {
final text = _controller.text;
final selection = _controller.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
final formattedText = '$prefix$selectedText$suffix';
final newText = text.replaceRange(selection.start, selection.end, formattedText);
_controller.text = newText;
_controller.selection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + formattedText.length);
} else {
final cursorPosition = selection.baseOffset;
final newText = '${text.substring(0, cursorPosition)}$prefix$suffix${text.substring(cursorPosition)}';
_controller.text = newText;
_controller.selection = TextSelection.collapsed(offset: cursorPosition + prefix.length);
}
widget.onChanged?.call(_controller.text);
}
void _applyHeading() {
if (_hasFormatting('# ', '')) {
_removeFormatting('# ', '');
} else {
_applyMarkdownFormat('# ', '');
}
}
void _applyBold() {
if (_hasFormatting('**', '**')) {
_removeFormatting('**', '**');
} else {
_applyMarkdownFormat('**', '**');
}
}
void _applyItalic() {
if (_hasFormatting('*', '*')) {
_removeFormatting('*', '*');
} else {
_applyMarkdownFormat('*', '*');
}
}
void _applyNumberedList() {
if (_hasFormatting('1. ', '')) {
_removeFormatting('1. ', '');
} else {
_applyMarkdownFormat('1. ', '');
}
}
void _applyBulletedList() {
if (_hasFormatting('- ', '')) {
_removeFormatting('- ', '');
} else {
_applyMarkdownFormat('- ', '');
}
}
void _applyLink() {
final text = _controller.text;
final selection = _controller.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
final linkRegex = RegExp(r'^\[(.*?)\]\((.*?)\)$');
if (linkRegex.hasMatch(selectedText)) {
final match = linkRegex.firstMatch(selectedText);
final linkText = match?.group(1) ?? selectedText;
final newText = text.replaceRange(selection.start, selection.end, linkText);
_controller.text = newText;
_controller.selection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + linkText.length);
} else {
final formattedText = '[$selectedText](url)';
final newText = text.replaceRange(selection.start, selection.end, formattedText);
_controller.text = newText;
_controller.selection = TextSelection(baseOffset: selection.start + selectedText.length + 2, extentOffset: selection.start + selectedText.length + 5);
}
} else {
_applyMarkdownFormat('[', '](url)');
}
widget.onChanged?.call(_controller.text);
}
void _applyHorizontalRule() {
final cursorPosition = _controller.selection.baseOffset;
final text = _controller.text;
final newText = '${text.substring(0, cursorPosition)}\n---\n${text.substring(cursorPosition)}';
_controller.text = newText;
_controller.selection = TextSelection.collapsed(offset: cursorPosition + 5);
widget.onChanged?.call(_controller.text);
}
void _togglePreview() {
setState(() {
_isPreview = !_isPreview;
});
if (!_isPreview) {
Future.delayed(const Duration(milliseconds: 100), () {
_focusNode.requestFocus();
});
} else {
_focusNode.unfocus();
}
widget.onToolbarVisibilityChanged?.call(isToolbarVisible);
}
Widget _mdButton(IconData icon, VoidCallback onTap) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
height: 36,
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
child: Icon(icon),
),
),
);
}
Widget _mdTextButton(String text, VoidCallback onTap) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
height: 36,
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
child: Center(
child: Text(
text,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: Colors.black87),
),
),
),
),
);
}
Widget _buildMarkdownPreview(BuildContext context) {
return Container(
height: widget.heightPreview,
padding: const EdgeInsets.all(16),
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
color: const Color(0xFFF6F6F6),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFE0E0E0)),
),
child: SingleChildScrollView(child: _parseMarkdown(_controller.text)),
);
}
Widget buildToolbar(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: MediaQuery.of(context).size.width - 76,
height: 52,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0))],
),
child: Row(
children: [
const SizedBox(width: 8),
_mdTextButton('H', _applyHeading),
const SizedBox(width: 8),
_mdButton(Icons.format_bold, _applyBold),
const SizedBox(width: 8),
_mdButton(Icons.format_italic, _applyItalic),
const SizedBox(width: 8),
_mdButton(Icons.format_list_numbered, _applyNumberedList),
const SizedBox(width: 8),
_mdButton(Icons.format_list_bulleted, _applyBulletedList),
const SizedBox(width: 8),
_mdButton(Icons.link, _applyLink),
const SizedBox(width: 8),
_mdButton(Icons.horizontal_rule, _applyHorizontalRule),
const SizedBox(width: 8),
],
),
),
const SizedBox(width: 8),
Container(
width: 52,
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0))],
),
child: Row(children: [_mdButton(_isPreview ? Icons.edit : Icons.visibility, _togglePreview)]),
),
],
);
}
Widget _parseMarkdown(String text) {
if (text.isEmpty) {
return Text(widget.hintText ?? '');
}
final lines = text.split('\n');
final widgets = <Widget>[];
for (int i = 0; i < lines.length; i++) {
final line = lines[i];
if (line.trim().isEmpty) {
widgets.add(const SizedBox(height: 8));
continue;
}
if (line.startsWith('# ')) {
widgets.add(
Text(
line.substring(2),
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (line.startsWith('## ')) {
widgets.add(
Text(
line.substring(3),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (line.startsWith('### ')) {
widgets.add(
Text(
line.substring(4),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (line.startsWith('- ') || line.startsWith('* ')) {
widgets.add(
Padding(
padding: const EdgeInsets.only(left: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('• ', style: TextStyle(fontSize: 16)),
Expanded(child: _parseInlineMarkdown(line.substring(2))),
],
),
),
);
} else if (RegExp(r'^\d+\. ').hasMatch(line)) {
final match = RegExp(r'^(\d+)\. ').firstMatch(line);
final number = match?.group(1) ?? '1';
widgets.add(
Padding(
padding: const EdgeInsets.only(left: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$number. ', style: const TextStyle(fontSize: 16)),
Expanded(child: _parseInlineMarkdown(line.substring(number.length + 2))),
],
),
),
);
} else if (line.trim() == '---') {
widgets.add(const Divider(color: Colors.grey, thickness: 1));
} else {
widgets.add(_parseInlineMarkdown(line));
}
if (i < lines.length - 1) {
widgets.add(const SizedBox(height: 4));
}
}
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: widgets);
}
Widget _parseInlineMarkdown(String text) {
final spans = <TextSpan>[];
final regex = RegExp(r'(\*\*.*?\*\*|\*.*?\*|\[.*?\]\(.*?\))');
int lastEnd = 0;
for (final match in regex.allMatches(text)) {
if (match.start > lastEnd) {
spans.add(
TextSpan(
text: text.substring(lastEnd, match.start),
style: const TextStyle(fontSize: 16, color: Colors.black),
),
);
}
final matchedText = match.group(0)!;
if (matchedText.startsWith('**') && matchedText.endsWith('**')) {
spans.add(
TextSpan(
text: matchedText.substring(2, matchedText.length - 2),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (matchedText.startsWith('*') && matchedText.endsWith('*')) {
spans.add(
TextSpan(
text: matchedText.substring(1, matchedText.length - 1),
style: const TextStyle(fontSize: 16, fontStyle: FontStyle.italic, color: Colors.black),
),
);
} else if (matchedText.startsWith('[') && matchedText.contains('](')) {
final linkMatch = RegExp(r'\[(.*?)\]\((.*?)\)').firstMatch(matchedText);
if (linkMatch != null) {
final linkText = linkMatch.group(1)!;
spans.add(
TextSpan(
text: linkText,
style: const TextStyle(fontSize: 16, color: Colors.blue, decoration: TextDecoration.underline),
),
);
}
}
lastEnd = match.end;
}
if (lastEnd < text.length) {
spans.add(
TextSpan(
text: text.substring(lastEnd),
style: const TextStyle(fontSize: 16, color: Colors.black),
),
);
}
return RichText(text: TextSpan(children: spans));
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_isPreview)
_buildMarkdownPreview(context)
else
CustomTextFormField(
controller: _controller,
focusNode: _focusNode,
maxLines: widget.maxLines,
minLines: widget.minLines,
hintText: widget.hintText,
hasError: widget.hasError,
onChanged: (value) => widget.onChanged?.call(value),
),
const SizedBox(height: 8),
if (widget.showInlineToolbar && (isToolbarVisible || !_isPreview))
Row(
children: [
Expanded(
child: Container(
height: 52,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0)),
],
),
child: Row(
children: [
const SizedBox(width: 8),
_mdTextButton('H', _applyHeading),
const SizedBox(width: 8),
_mdButton(Icons.format_bold, _applyBold),
const SizedBox(width: 8),
_mdButton(Icons.format_italic, _applyItalic),
const SizedBox(width: 8),
_mdButton(Icons.format_list_numbered, _applyNumberedList),
const SizedBox(width: 8),
_mdButton(Icons.format_list_bulleted, _applyBulletedList),
const SizedBox(width: 8),
_mdButton(Icons.link, _applyLink),
const SizedBox(width: 8),
_mdButton(Icons.horizontal_rule, _applyHorizontalRule),
const SizedBox(width: 8),
],
),
),
),
const SizedBox(width: 8),
Container(
width: 52,
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: const Color(0x00000000).withAlpha((255 * 0.2).toInt()), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0)),
],
),
child: Row(children: [_mdButton(_isPreview ? Icons.edit : Icons.visibility, _togglePreview)]),
),
],
),
],
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart';
import 'package:eatvio_app/core/env_config.dart';
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
import 'package:eatvio_app/core/locator_service.dart';
@@ -40,6 +41,7 @@ class AppConfig {
BlocProvider<ChatsCubit>(create: (context) => sl<ChatsCubit>()..getChatRoomsWithLoading()),
BlocProvider<ViewsCubit>(create: (context) => sl<ViewsCubit>()),
BlocProvider<InternetConnectionCubit>(lazy: false, create: (context) => sl<InternetConnectionCubit>()),
BlocProvider<MarketingCubit>(create: (context) => sl<MarketingCubit>()),
];
/// Делегаты локализации

View File

@@ -0,0 +1,29 @@
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit_state.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MarketingCubit extends Cubit<MarketingCubitState> {
final Prefs prefs;
MarketingCubit(this.prefs) : super(const MarketingCubitState());
Future<bool> checkRegistrationDate() async {
try {
final milliseconds = prefs.getInt(PrefsKeys.registrationDate);
final marketingBottomSheetHasBeenShown = prefs.getBool(PrefsKeys.marketingBottomSheetHasBeenShown);
if (milliseconds == null || marketingBottomSheetHasBeenShown == true) {
return false;
}
final date = DateTime.fromMillisecondsSinceEpoch(milliseconds);
final today = DateTime.now();
final daysCount = today.difference(date).inDays;
final needToShowMarketingBottomSheet = daysCount >= 14;
if (needToShowMarketingBottomSheet) {
await prefs.setBool(PrefsKeys.marketingBottomSheetHasBeenShown, true);
}
return needToShowMarketingBottomSheet;
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,3 @@
class MarketingCubitState {
const MarketingCubitState();
}

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart';
import 'package:eatvio_app/core/internet_connection_cubit/internet_connection_cubit.dart';
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
@@ -38,6 +39,7 @@ import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cub
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/order_info/cubit/order_info_cubit.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
import 'package:eatvio_app/features/plans/presentation/cubit/about_plan_cubit.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
@@ -74,10 +76,10 @@ class CubitRegistrations {
static void _registerFactoryCubits() {
// Auth related cubits
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl()));
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl()));
sl.registerFactory<OAuthCubit>(() => OAuthCubit(authRepository: sl(), prefs: sl()));
// Feed related cubits
sl.registerFactory<FeedCubit>(() => FeedCubit(feedRepository: sl()));
sl.registerFactory<RecipesCubit>(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
@@ -143,5 +145,7 @@ class CubitRegistrations {
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
sl.registerFactory<UserDetailsCubit>(() => UserDetailsCubit(profileRepository: sl()));
sl.registerFactory<MenuCubit>(() => MenuCubit(sl()));
sl.registerFactory<MarketingCubit>(() => MarketingCubit(sl()));
sl.registerFactory<PersonalDietSuggestionCubit>(() => PersonalDietSuggestionCubit(dietRepository: sl(), personalDietSuggestionService: sl()));
}
}

View File

@@ -23,6 +23,7 @@ import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.da
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
import 'package:eatvio_app/features/notifications/repository/notifications_repository.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart';
import 'package:eatvio_app/features/plans/repositories/plan_repository.dart';
import 'package:eatvio_app/features/plans/repositories/plans_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
@@ -68,6 +69,7 @@ class RepositoryRegistrations {
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
sl.registerLazySingleton<PersonalDietSuggestionRepository>(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl()));
// Content repositories
sl.registerLazySingleton<BlogPostRepository>(() => BlogPostRepository(blogPostService: sl()));

View File

@@ -25,6 +25,7 @@ import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
import 'package:eatvio_app/features/notifications/service/notifications_service.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/services/personal_diet_suggestion_service.dart';
import 'package:eatvio_app/features/plans/services/plan_service.dart';
import 'package:eatvio_app/features/plans/services/plans_service.dart';
import 'package:eatvio_app/features/profile/services/profile_service.dart';
@@ -76,6 +77,7 @@ class ServiceRegistrations {
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
sl.registerLazySingleton<PersonalDietSuggestionService>(() => PersonalDietSuggestionService(sl()));
// Content services
sl.registerLazySingleton<CreateRecipeService>(() => CreateRecipeService(sl()));

View File

@@ -0,0 +1,11 @@
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/prefs_keys.dart';
mixin RegistrationDateCacheMixin {
late final Prefs prefs;
Future<void> cacheRegistrationDate(DateTime date) async {
await prefs.setInt(PrefsKeys.registrationDate, date.millisecondsSinceEpoch);
}
}

View File

@@ -1 +1 @@
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed }
enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown }

View File

@@ -29,6 +29,7 @@ import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.
import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart';
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
@@ -239,6 +240,11 @@ class OtherRoutes {
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
},
),
GoRoute(
path: PersonalDietSuggestionScreen.path,
name: PersonalDietSuggestionScreen.name,
builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int,),
),
];
}
}

View File

@@ -249,4 +249,10 @@ abstract class TextStyles {
fontWeight: FontWeight.w500,
height: 1.255,
);
static const textFS24FW700 = TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
height: 1.255,
);
}

View File

@@ -101,7 +101,7 @@ class PrimaryColors {
// Blue
Color get blue400 => const Color(0XFF43A6F2);
Color get blueA400 => const Color(0XFF2F80ED);
Color get blueA400 => const Color(0XFF2F80ED);
// BlueGray
Color get blueGray400 => const Color(0XFF7B8794);
@@ -141,6 +141,8 @@ class PrimaryColors {
Color get gray60003 => const Color(0XFF707070);
Color get gray60005 => const Color(0xFF6C7C93);
// Green
Color get greenA700 => const Color(0XFF019A3E);
@@ -148,6 +150,8 @@ class PrimaryColors {
Color get green => const Color(0XFF0BB450);
Color get transparentGreen => const Color.fromARGB(12, 1, 154, 63);
// LightGreen
Color get lightGreen300 => const Color(0XFFB2DF8A);

View File

@@ -0,0 +1,124 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:share_plus/share_plus.dart';
void showMarketingBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).viewInsets.bottom + MediaQuery.of(context).padding.bottom + 20),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700),
child: const MarketingBottomSheet(),
),
);
}
class MarketingBottomSheet extends StatelessWidget {
const MarketingBottomSheet({super.key});
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsetsGeometry.all(4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
height: 180,
width: double.infinity,
decoration: const BoxDecoration(
image: DecorationImage(image: AssetImage('assets/images/logo_bg.png'), fit: BoxFit.cover),
),
child: Center(child: SvgPicture.asset('assets/icons/logo_2.svg', height: 90)),
),
),
const SizedBox(height: 16),
Text(s.congratulations, style: TextStyles.textFS24FW700.copyWith(color: appTheme.blueA400)),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Container(
decoration: BoxDecoration(color: appTheme.lightGray0, borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Row(
children: [
const Spacer(),
Container(
decoration: BoxDecoration(shape: BoxShape.circle, color: appTheme.blueA400),
padding: const EdgeInsets.all(8),
child: Text('14', style: TextStyles.textFS24FW700.copyWith(color: appTheme.whiteA700)),
),
const SizedBox(width: 12),
RichText(
text: TextSpan(
text: s.youWithUs,
style: TextStyles.textSMedium.copyWith(color: appTheme.gray60005),
children: [
TextSpan(
text: s.fourteenDays,
style: TextStyles.textLMedium.copyWith(color: appTheme.black),
),
],
),
),
const Spacer(),
],
),
),
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(
s.weAreLookingFor,
textAlign: TextAlign.center,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
),
),
Text(
s.letsInvite,
textAlign: TextAlign.center,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
child: CustomOutlinedButton(
text: s.invite,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
leftIcon: Padding(
padding: const EdgeInsets.only(right: 12),
child: Transform.rotate(
angle: 3.14,
child: SvgPicture.asset('assets/icons/share.svg', width: 14, height: 14, colorFilter: ColorFilter.mode(appTheme.whiteA700, BlendMode.srcIn)),
),
),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
SharePlus.instance.share(ShareParams(uri: Uri.parse('https://promo.eatvio.ru/')));
},
),
),
Text(
s.eatvioIsAHelthyHabbit,
textAlign: TextAlign.center,
style: TextStyles.textSMedium.copyWith(color: appTheme.blueA400),
),
const SizedBox(height: 16),
],
),
);
}
}

View File

@@ -9,9 +9,10 @@ import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
class OptionWidget extends StatelessWidget {
const OptionWidget({super.key, required this.option, this.disableDuration = false});
const OptionWidget({super.key, required this.option, this.disableDuration = false, required this.duration});
final PacketOptions? option;
final bool disableDuration;
final int duration;
@override
Widget build(BuildContext context) {
if (option == null) {
@@ -34,8 +35,12 @@ class OptionWidget extends StatelessWidget {
),
const SizedBox(height: 12),
if (!disableDuration) ...[
const _OptionDurationWidget(duration: 7),
const SizedBox(height: 16),
_OptionDurationWidget(duration: duration),
SizedBox(height: option?.hasPersonalDiet ?? false ? 8 : 12),
],
if ((option?.hasPersonalDiet ?? false) && !disableDuration) ...[
const _OptionHasPersonalDietWidget(),
const SizedBox(height: 12),
],
const CustomDivider(),
const SizedBox(height: 16),
@@ -52,7 +57,6 @@ class OptionWidget extends StatelessWidget {
}
}
class _OptionDurationWidget extends StatelessWidget {
const _OptionDurationWidget({required this.duration});
final int duration;
@@ -92,3 +96,43 @@ class _OptionDurationWidget extends StatelessWidget {
);
}
}
class _OptionHasPersonalDietWidget extends StatelessWidget {
const _OptionHasPersonalDietWidget();
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(4), color: appTheme.transparentGreen),
child: Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: appTheme.green,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(100),
bottomRight: Radius.circular(100),
),
),
),
),
const SizedBox(
width: 12,
),
Text(
s.personalDietCreating,
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
),
const SizedBox(
width: 12,
),
],
),
);
}
}

View File

@@ -1,14 +1,18 @@
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart';
import 'package:eatvio_app/features/auth/repositories/auth_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class OAuthCubit extends Cubit<OAuthState> {
class OAuthCubit extends Cubit<OAuthState> with RegistrationDateCacheMixin {
final AuthRepository authRepository;
OAuthCubit({required this.authRepository}) : super(const OAuthState());
OAuthCubit({required this.authRepository, required Prefs prefs}) : super(const OAuthState()) {
this.prefs = prefs;
}
Future<void> signInViaSocialMedia({required String provider, required String accessToken, required String refreshToken, required String clientId}) async {
try {
@@ -63,6 +67,7 @@ class OAuthCubit extends Cubit<OAuthState> {
clientId: clientId,
);
TokenStorage.saveTokenInStorage(token);
cacheRegistrationDate(DateTime.now());
emit(OAuthState.loaded(token: token));
} on DioException catch (e) {
emit(OAuthState.error(errorMessage: e.toString()));

View File

@@ -11,7 +11,9 @@ class BlogPostCubit extends Cubit<BlogPostState> {
Future<void> getBlogPost(int id) async {
try {
emit(state.copyWith(isLoading: true));
final postModel = await feedRepository.getBlogPost(id: id);
emit(state.copyWith(blogPostModel: postModel, isLoading: false));
} catch (e, st) {
log(e.toString(), stackTrace: st);

View File

@@ -18,6 +18,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class BlogPostScreen extends StatefulWidget {
final int id;
@@ -231,10 +232,35 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
),
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Html(
data: blogPostState.blogPostModel?.description.replaceAll('\n', '<br>'),
style: {"*": Style(color: Colors.black87)},
data: md.markdownToHtml(blogPostState.blogPostModel!.description.replaceAll('\n', '<br>')),
style: {
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
const SizedBox(height: 8),

View File

@@ -25,10 +25,7 @@ import 'package:permission_handler/permission_handler.dart';
class BlogPostCreateScreen extends StatefulWidget {
final FeedBlogPostModel? feedBlogPostModel;
const BlogPostCreateScreen({
super.key,
this.feedBlogPostModel,
});
const BlogPostCreateScreen({super.key, this.feedBlogPostModel});
@override
State<BlogPostCreateScreen> createState() => _BlogPostCreateScreenState();
@@ -43,6 +40,9 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
final descriptionController = TextEditingController();
final videoUrlController = TextEditingController();
bool isLoading = false;
bool enableMdPanel = false;
bool isPreviewMode = false;
final FocusNode descriptionFocusNode = FocusNode();
@override
void initState() {
@@ -51,6 +51,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
setState(() {
shrinkImage = visible;
enableMdPanel = visible;
});
});
if (widget.feedBlogPostModel != null) {
@@ -63,9 +64,326 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
@override
void dispose() {
keyboardSubscription.cancel();
descriptionFocusNode.dispose();
super.dispose();
}
bool _hasFormatting(String prefix, String suffix) {
final text = descriptionController.text;
final selection = descriptionController.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
return selectedText.startsWith(prefix) && selectedText.endsWith(suffix);
} else {
final cursorPosition = selection.baseOffset;
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
final beforeCursor = text.substring(cursorPosition - prefix.length, cursorPosition);
final afterCursor = text.substring(cursorPosition, cursorPosition + suffix.length);
return beforeCursor == prefix && afterCursor == suffix;
}
}
return false;
}
void _removeFormatting(String prefix, String suffix) {
final text = descriptionController.text;
final selection = descriptionController.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
final unformattedText = selectedText.substring(prefix.length, selectedText.length - suffix.length);
final newText = text.replaceRange(selection.start, selection.end, unformattedText);
descriptionController.text = newText;
final newSelection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + unformattedText.length);
descriptionController.selection = newSelection;
} else {
final cursorPosition = selection.baseOffset;
if (cursorPosition >= prefix.length && text.length >= cursorPosition + suffix.length) {
final beforeCursor = text.substring(0, cursorPosition - prefix.length);
final afterCursor = text.substring(cursorPosition + suffix.length);
final newText = beforeCursor + afterCursor;
descriptionController.text = newText;
final newCursorPosition = cursorPosition - prefix.length;
descriptionController.selection = TextSelection.collapsed(offset: newCursorPosition);
}
}
}
void _applyMarkdownFormat(String prefix, String suffix) {
final text = descriptionController.text;
final selection = descriptionController.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
final formattedText = '$prefix$selectedText$suffix';
final newText = text.replaceRange(selection.start, selection.end, formattedText);
descriptionController.text = newText;
final newSelection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + formattedText.length);
descriptionController.selection = newSelection;
} else {
final cursorPosition = selection.baseOffset;
final newText = '${text.substring(0, cursorPosition)}$prefix$suffix${text.substring(cursorPosition)}';
descriptionController.text = newText;
final newCursorPosition = cursorPosition + prefix.length;
descriptionController.selection = TextSelection.collapsed(offset: newCursorPosition);
}
}
void _applyHeading() {
if (_hasFormatting('# ', '')) {
_removeFormatting('# ', '');
} else {
_applyMarkdownFormat('# ', '');
}
}
void _applyBold() {
if (_hasFormatting('**', '**')) {
_removeFormatting('**', '**');
} else {
_applyMarkdownFormat('**', '**');
}
}
void _applyItalic() {
if (_hasFormatting('*', '*')) {
_removeFormatting('*', '*');
} else {
_applyMarkdownFormat('*', '*');
}
}
void _applyNumberedList() {
if (_hasFormatting('1. ', '')) {
_removeFormatting('1. ', '');
} else {
_applyMarkdownFormat('1. ', '');
}
}
void _applyBulletedList() {
if (_hasFormatting('- ', '')) {
_removeFormatting('- ', '');
} else {
_applyMarkdownFormat('- ', '');
}
}
void _applyLink() {
final text = descriptionController.text;
final selection = descriptionController.selection;
if (selection.isValid && !selection.isCollapsed) {
final selectedText = text.substring(selection.start, selection.end);
final linkRegex = RegExp(r'^\[(.*?)\]\((.*?)\)$');
if (linkRegex.hasMatch(selectedText)) {
final match = linkRegex.firstMatch(selectedText);
final linkText = match?.group(1) ?? selectedText;
final newText = text.replaceRange(selection.start, selection.end, linkText);
descriptionController.text = newText;
final newSelection = TextSelection(baseOffset: selection.start, extentOffset: selection.start + linkText.length);
descriptionController.selection = newSelection;
} else {
final formattedText = '[$selectedText](url)';
final newText = text.replaceRange(selection.start, selection.end, formattedText);
descriptionController.text = newText;
final newSelection = TextSelection(baseOffset: selection.start + selectedText.length + 2, extentOffset: selection.start + selectedText.length + 5);
descriptionController.selection = newSelection;
}
} else {
_applyMarkdownFormat('[', '](url)');
}
}
void _applyHorizontalRule() {
final cursorPosition = descriptionController.selection.baseOffset;
final text = descriptionController.text;
final newText = '${text.substring(0, cursorPosition)}\n---\n${text.substring(cursorPosition)}';
descriptionController.text = newText;
final newCursorPosition = cursorPosition + 5;
descriptionController.selection = TextSelection.collapsed(offset: newCursorPosition);
}
void _togglePreview() {
setState(() {
isPreviewMode = !isPreviewMode;
});
if (!isPreviewMode) {
Future.delayed(const Duration(milliseconds: 100), () {
descriptionFocusNode.requestFocus();
});
} else {
descriptionFocusNode.unfocus();
}
}
Widget _buildMarkdownPreview() {
return Container(
height: 120,
padding: const EdgeInsets.all(16),
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
color: const Color(0xFFF6F6F6),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFE0E0E0)),
),
child: SingleChildScrollView(child: _parseMarkdown(descriptionController.text)),
);
}
Widget _parseMarkdown(String text) {
if (text.isEmpty) {
return const Text(
'Описание',
style: TextStyle(fontSize: 16, color: Color(0xFF323F4B), fontWeight: FontWeight.w500),
);
}
final lines = text.split('\n');
final widgets = <Widget>[];
for (int i = 0; i < lines.length; i++) {
final line = lines[i];
if (line.trim().isEmpty) {
widgets.add(const SizedBox(height: 8));
continue;
}
if (line.startsWith('# ')) {
widgets.add(
Text(
line.substring(2),
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (line.startsWith('## ')) {
widgets.add(
Text(
line.substring(3),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (line.startsWith('### ')) {
widgets.add(
Text(
line.substring(4),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (line.startsWith('- ') || line.startsWith('* ')) {
widgets.add(
Padding(
padding: const EdgeInsets.only(left: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('• ', style: TextStyle(fontSize: 16)),
Expanded(child: _parseInlineMarkdown(line.substring(2))),
],
),
),
);
} else if (RegExp(r'^\d+\. ').hasMatch(line)) {
final match = RegExp(r'^(\d+)\. ').firstMatch(line);
final number = match?.group(1) ?? '1';
widgets.add(
Padding(
padding: const EdgeInsets.only(left: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$number. ', style: const TextStyle(fontSize: 16)),
Expanded(child: _parseInlineMarkdown(line.substring(number.length + 2))),
],
),
),
);
} else if (line.trim() == '---') {
widgets.add(const Divider(color: Colors.grey, thickness: 1));
} else {
widgets.add(_parseInlineMarkdown(line));
}
if (i < lines.length - 1) {
widgets.add(const SizedBox(height: 4));
}
}
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: widgets);
}
Widget _parseInlineMarkdown(String text) {
final spans = <TextSpan>[];
final regex = RegExp(r'(\*\*.*?\*\*|\*.*?\*|\[.*?\]\(.*?\))');
int lastEnd = 0;
for (final match in regex.allMatches(text)) {
if (match.start > lastEnd) {
spans.add(
TextSpan(
text: text.substring(lastEnd, match.start),
style: const TextStyle(fontSize: 16, color: Colors.black),
),
);
}
final matchedText = match.group(0)!;
if (matchedText.startsWith('**') && matchedText.endsWith('**')) {
spans.add(
TextSpan(
text: matchedText.substring(2, matchedText.length - 2),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black),
),
);
} else if (matchedText.startsWith('*') && matchedText.endsWith('*')) {
spans.add(
TextSpan(
text: matchedText.substring(1, matchedText.length - 1),
style: const TextStyle(fontSize: 16, fontStyle: FontStyle.italic, color: Colors.black),
),
);
} else if (matchedText.startsWith('[') && matchedText.contains('](')) {
final linkMatch = RegExp(r'\[(.*?)\]\((.*?)\)').firstMatch(matchedText);
if (linkMatch != null) {
final linkText = linkMatch.group(1)!;
spans.add(
TextSpan(
text: linkText,
style: const TextStyle(fontSize: 16, color: Colors.blue, decoration: TextDecoration.underline),
),
);
}
}
lastEnd = match.end;
}
if (lastEnd < text.length) {
spans.add(
TextSpan(
text: text.substring(lastEnd),
style: const TextStyle(fontSize: 16, color: Colors.black),
),
);
}
return RichText(text: TextSpan(children: spans));
}
@override
Widget build(BuildContext context) {
return BlocProvider<BlogPostCreateCubit>(
@@ -103,13 +421,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo_camera),
SizedBox(width: 8),
Text('Фото'),
],
),
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
),
CupertinoActionSheetAction(
onPressed: () async {
@@ -127,97 +439,102 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
context: context,
);
},
child: const Row(
children: [
Icon(Icons.photo),
SizedBox(width: 8),
Text('Галерея'),
],
),
child: const Row(children: [Icon(Icons.photo), SizedBox(width: 8), Text('Галерея')]),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.of(context).pop(),
isDestructiveAction: true,
child: const Text('Отмена'),
),
cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена')),
);
},
);
}
Widget _mdButton(IconData icon, Function() onTap) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
height: 36,
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
child: Icon(icon),
),
),
);
}
Widget _mdTextButton(String text, Function() onTap) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
height: 36,
decoration: BoxDecoration(color: const Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(10)),
child: Center(
child: Text(
text,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: Colors.black87),
),
),
),
),
);
}
Scaffold _buildBlogPostCreate(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: SizedBox(
width: MediaQuery.of(context).size.width - 32,
child: FloatingActionButton.extended(
key: AppKeys.blogPostCreateScreenCreateButton,
heroTag: 'createBlogPost',
highlightElevation: 0,
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
backgroundColor: appTheme.blueA400,
label: isLoading
? const CircularProgressIndicator()
: const Text(
'Создать',
style: TextStyle(
color: Colors.white,
floatingActionButton: enableMdPanel || isPreviewMode
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!isPreviewMode) ...[
Container(
width: MediaQuery.of(context).size.width - 76,
height: 52,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: const Color(0x00000000).withValues(alpha: 0.2), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0)),
],
),
child: Row(
children: [
const SizedBox(width: 8),
_mdTextButton('H', _applyHeading),
const SizedBox(width: 8),
_mdButton(Icons.format_bold, _applyBold),
const SizedBox(width: 8),
_mdButton(Icons.format_italic, _applyItalic),
const SizedBox(width: 8),
_mdButton(Icons.format_list_numbered, _applyNumberedList),
const SizedBox(width: 8),
_mdButton(Icons.format_list_bulleted, _applyBulletedList),
const SizedBox(width: 8),
_mdButton(Icons.link, _applyLink),
const SizedBox(width: 8),
_mdButton(Icons.horizontal_rule, _applyHorizontalRule),
const SizedBox(width: 8),
],
),
),
const SizedBox(width: 8),
],
Container(
width: 52,
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0x00000000).withValues(alpha: 0.2), spreadRadius: 0, blurRadius: 32, offset: const Offset(0, 0))],
),
child: Row(children: [_mdButton(isPreviewMode ? Icons.edit : Icons.visibility, _togglePreview)]),
),
onPressed: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
if (widget.feedBlogPostModel != null) {
final result = await blogPostCubit.updateBlogPost(
id: widget.feedBlogPostModel!.id,
title: titleController.text,
description: descriptionController.text,
videoUrl: videoUrlController.text,
image: image,
oldImageUrl: widget.feedBlogPostModel!.image,
);
setState(() {
isLoading = false;
});
if (!result) {
if (!context.mounted) return;
showErrorDialog(context, title: S.current.could_not_update_blog_post);
} else {
if (!context.mounted) return;
context.pop();
}
} else {
final result = await blogPostCubit.createBlogPost(
title: titleController.text,
description: descriptionController.text,
videoUrl: videoUrlController.text,
image: image,
);
setState(() {
isLoading = false;
});
if (!result) {
if (!context.mounted) return;
showErrorDialog(context, title: S.current.could_not_publish_blog_post);
} else {
if (!context.mounted) return;
context.pop();
}
}
},
),
),
],
)
: null,
appBar: AppBar(),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
@@ -226,19 +543,19 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
const SizedBox(height: 16),
const Text('Заголовок'),
const SizedBox(height: 8),
CustomTextFormField(
key: AppKeys.blogPostCreateScreenTitleTextField,
controller: titleController,
),
CustomTextFormField(key: AppKeys.blogPostCreateScreenTitleTextField, controller: titleController),
const SizedBox(height: 16),
const Text('Описание'),
const SizedBox(height: 8),
CustomTextFormField(
key: AppKeys.blogPostCreateScreenContentTextField,
controller: descriptionController,
maxLines: 6,
minLines: 4,
),
isPreviewMode
? _buildMarkdownPreview()
: CustomTextFormField(
key: AppKeys.blogPostCreateScreenContentTextField,
controller: descriptionController,
focusNode: descriptionFocusNode,
maxLines: 6,
minLines: 4,
),
const SizedBox(height: 16),
if (videoUrlController.text.isEmpty) const Text('Изображение поста'),
if (videoUrlController.text.isEmpty) const SizedBox(height: 8),
@@ -247,17 +564,75 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
if (image == null && widget.feedBlogPostModel?.image == null) const Text('Ссылка на видео'),
if (image == null && widget.feedBlogPostModel?.image == null) const SizedBox(height: 8),
if (image == null && widget.feedBlogPostModel?.image == null)
CustomTextFormField(
key: AppKeys.blogPostCreateScreenVideoLinkTextField,
controller: videoUrlController,
),
CustomTextFormField(key: AppKeys.blogPostCreateScreenVideoLinkTextField, controller: videoUrlController),
const SizedBox(height: 80),
_createBlogPostButton(),
],
),
),
);
}
Widget _createBlogPostButton() {
return InkWell(
onTap: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
if (widget.feedBlogPostModel != null) {
final result = await blogPostCubit.updateBlogPost(
id: widget.feedBlogPostModel!.id,
title: titleController.text,
description: descriptionController.text,
videoUrl: videoUrlController.text,
image: image,
oldImageUrl: widget.feedBlogPostModel!.image,
);
setState(() {
isLoading = false;
});
if (!result) {
if (!mounted) return;
showErrorDialog(context, title: S.current.could_not_update_blog_post);
} else {
if (!mounted) return;
context.pop();
}
} else {
final result = await blogPostCubit.createBlogPost(
title: titleController.text,
description: descriptionController.text,
videoUrl: videoUrlController.text,
image: image,
);
setState(() {
isLoading = false;
});
if (!result) {
if (!mounted) return;
showErrorDialog(context, title: S.current.could_not_publish_blog_post);
} else {
if (!mounted) return;
context.pop();
}
}
},
borderRadius: BorderRadius.circular(16),
child: Ink(
width: MediaQuery.of(context).size.width - 32,
height: 52,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: appTheme.blueA400),
child: isLoading
? const Center(child: CircularProgressIndicator())
: const Center(
child: Text('Создать', style: TextStyle(color: Colors.white)),
),
),
);
}
GestureDetector _buildAddPhoto() {
return GestureDetector(
onTap: () {
@@ -270,22 +645,14 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
height: shrinkImage ? 52 : 128,
curve: Curves.easeIn,
duration: const Duration(milliseconds: 150),
decoration: AppDecoration.fillGray.copyWith(
borderRadius: BorderRadius.circular(16),
),
decoration: AppDecoration.fillGray.copyWith(borderRadius: BorderRadius.circular(16)),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.photo_library,
color: appTheme.blueGray300,
),
Icon(Icons.photo_library, color: appTheme.blueGray300),
const SizedBox(width: 8),
Text(
"Добавить фото",
selectionColor: appTheme.blueA400,
),
Text("Добавить фото", selectionColor: appTheme.blueA400),
],
),
),
@@ -294,13 +661,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
width: shrinkImage ? 52 : MediaQuery.of(context).size.width - 32,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image(
image: FileImage(
File(
image!.path,
),
),
),
child: Image(image: FileImage(File(image!.path))),
),
),
if (widget.feedBlogPostModel?.image != null && image == null)
@@ -308,9 +669,7 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
width: shrinkImage ? 52 : MediaQuery.of(context).size.width - 32,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image(
image: NetworkImage('${sl<AppConfig>().baseUrl}/storage/${widget.feedBlogPostModel!.image}'),
),
child: Image(image: NetworkImage('${sl<AppConfig>().baseUrl}/storage/${widget.feedBlogPostModel!.image}')),
),
),
],

View File

@@ -114,13 +114,21 @@ class _ChatScreenState extends State<ChatScreen> {
ChosenPlanStateEnum.statePendingPayment,
ChosenPlanStateEnum.stateInCoordination,
].contains(chosenPlan?.chosenPlanState);
PacketOptions? chosenOption;
if (chosenPlan?.optionType == 'options1') {
chosenOption = chosenPlan?.plan.packet1Options;
} else if (chosenPlan?.optionType == 'options2') {
chosenOption = chosenPlan?.plan.packet2Options;
} else if (chosenPlan?.optionType == 'options3') {
chosenOption = chosenPlan?.plan.packet3Options;
}
final showAboutOrderButton =
state.chatRoom.chosenPlan != null && state.chatRoom.chosenPlan!.chosenPlanState != ChosenPlanStateEnum.stateCanceledByTheInitiator;
BannerType? bannerType;
if (chosenPlan == null || chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateCanceledByTheInitiator) {
bannerType = null;
} else if (amIConsultant && !state.hasCard) {
} else if (amIConsultant && !state.hasCard && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
bannerType = BannerType.card;
} else if (amIConsultant && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
bannerType = BannerType.date;
@@ -131,16 +139,10 @@ class _ChatScreenState extends State<ChatScreen> {
bannerType = BannerType.collectCharge;
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateLastDayFinished && !amIConsultant) {
bannerType = BannerType.finishPlan;
} else if (chosenOption?.hasPersonalDiet == true && amIConsultant && chosenPlan.chosenPlanState != ChosenPlanStateEnum.stateDone) {
bannerType = BannerType.setPersonalDiet;
}
PacketOptions? chosenOption;
if (chosenPlan?.optionType == 'options1') {
chosenOption = chosenPlan?.plan.packet1Options;
} else if (chosenPlan?.optionType == 'options2') {
chosenOption = chosenPlan?.plan.packet2Options;
} else if (chosenPlan?.optionType == 'options3') {
chosenOption = chosenPlan?.plan.packet3Options;
}
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
@@ -317,6 +319,7 @@ class _ChatScreenState extends State<ChatScreen> {
chosenPlanId: chosenPlan?.id,
planName: chosenPlan?.plan.name,
planPrice: chosenOption?.price.toDouble(),
hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,
),
),
],

View File

@@ -9,17 +9,28 @@ import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_svg/flutter_svg.dart';
class ChosePlanDateWidget extends StatefulWidget {
const ChosePlanDateWidget({super.key, required this.chatCubit});
const ChosePlanDateWidget({super.key, required this.chatCubit, required this.hasPersonalDiet});
final ChatRoomCubit chatCubit;
final bool hasPersonalDiet;
@override
State<ChosePlanDateWidget> createState() => _ChosePlanDateWidget();
}
class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
final startDate = [DateTime.now()];
late final List<DateTime> startDate;
bool visible = false;
@override
void initState() {
super.initState();
if (widget.hasPersonalDiet) {
startDate = [DateTime.now().add(const Duration(days: 2))];
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -27,64 +38,129 @@ class _ChosePlanDateWidget extends State<ChosePlanDateWidget> {
if (widget.chatCubit.state is ChatRoomStateLoaded) {
planName = (widget.chatCubit.state as ChatRoomStateLoaded).chatRoom.chosenPlan?.plan.name;
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 4),
const BottomSheetHandlerBar(),
const SizedBox(height: 12),
Text(
planName ?? '',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
return GestureDetector(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 4),
const BottomSheetHandlerBar(),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
planName ?? '',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
),
if (widget.hasPersonalDiet) ...[
const SizedBox(
width: 8,
),
PortalTarget(
closeDuration: const Duration(milliseconds: 20),
anchor: const Aligned(
shiftToWithinBound: AxisFlag(x: true, y: true),
follower: Alignment.topRight,
target: Alignment.bottomLeft,
),
portalFollower: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: visible ? appTheme.whiteA700 : Colors.transparent,
boxShadow: visible
? [
BoxShadow(
color: appTheme.black900.withValues(alpha: 0.15),
blurRadius: 4,
offset: const Offset(0, 4),
),
]
: null,
),
child: AnimatedDefaultTextStyle(
style: TextStyles.textMMediumAuto.copyWith(
color: visible ? appTheme.black : Colors.transparent,
),
duration: const Duration(milliseconds: 300),
child: Text(
s.chosenPlanStartDateDescr,
),
),
),
),
child: GestureDetector(
onTap: () {
setState(() {
visible = !visible;
});
},
child: SvgPicture.asset(
'assets/icons/question_mark.svg',
width: 22,
height: 22,
colorFilter: ColorFilter.mode(
appTheme.blueA400,
BlendMode.srcIn,
),
),
),
)
]
],
),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Text(
s.chose_plan_start_date_2,
textAlign: TextAlign.center,
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Text(
s.chose_plan_start_date_2,
textAlign: TextAlign.center,
style: TextStyles.textMMedium.copyWith(
color: appTheme.black,
),
),
),
),
const SizedBox(height: 12),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: CustomDivider(),
),
CalendarWidget(
startDate: startDate,
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 28),
child: CustomOutlinedButton(
onPressed: () async {
final result = await widget.chatCubit.choseStartPlanDate(startDate[0]);
if (result) {
showSuccessToast('Дата установлена');
if (!context.mounted) return;
Navigator.pop(context);
} else {
showErrorToast('Не удалось установить дату');
if (!context.mounted) return;
Navigator.pop(context);
}
},
height: 60,
text: s.confirm,
buttonTextStyle: TextStyles.textLSemibold.copyWith(
color: appTheme.whiteA700,
),
buttonStyle: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
const SizedBox(height: 12),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: CustomDivider(),
),
CalendarWidget(
startDate: startDate,
),
Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 28),
child: CustomOutlinedButton(
onPressed: () async {
final result = await widget.chatCubit.choseStartPlanDate(startDate[0]);
if (result) {
showSuccessToast('Дата установлена');
if (!context.mounted) return;
Navigator.pop(context);
} else {
showErrorToast('Не удалось установить дату');
if (!context.mounted) return;
Navigator.pop(context);
}
},
height: 60,
text: s.confirm,
buttonTextStyle: TextStyles.textLSemibold.copyWith(
color: appTheme.whiteA700,
),
buttonStyle: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
),
),
),
),
],
],
),
);
}
}

View File

@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'
import 'package:eatvio_app/features/chat/presentation/widgets/chose_plan_date_widget.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/complete_plan_bottom_sheet.dart';
import 'package:eatvio_app/features/chat/presentation/widgets/plan_complain_bottom_sheet.dart';
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
@@ -17,12 +18,12 @@ import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
class NotificationBanner extends StatefulWidget {
const NotificationBanner({super.key, required this.bannerType, this.planPrice, this.planName, this.chosenPlanId});
const NotificationBanner({super.key, required this.bannerType, this.planPrice, this.planName, this.chosenPlanId, this.hasPersonalDiet = false});
final BannerType bannerType;
final double? planPrice;
final String? planName;
final int? chosenPlanId;
final bool hasPersonalDiet;
@override
State<NotificationBanner> createState() => _NotificationBannerState();
}
@@ -68,7 +69,8 @@ class _NotificationBannerState extends State<NotificationBanner> {
BannerType.date => [
SizedBox(
width: size.width * 0.6,
child: Text(s.choose_plan_start_date, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
child: Text(widget.hasPersonalDiet == true ? s.choose_plan_start_date_with_personal_diet : s.choose_plan_start_date,
style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
GestureDetector(
@@ -78,6 +80,7 @@ class _NotificationBannerState extends State<NotificationBanner> {
context: context,
builder: (context) => ChosePlanDateWidget(
chatCubit: cubit,
hasPersonalDiet: widget.hasPersonalDiet,
),
);
},
@@ -155,6 +158,20 @@ class _NotificationBannerState extends State<NotificationBanner> {
),
const SizedBox(height: 8),
],
BannerType.setPersonalDiet => [
SizedBox(
width: size.width * 0.6,
child: Text(s.createPersonalDiet, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(MyDietsScreen.name);
},
child: Text(s.goToMyDiets, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
),
const SizedBox(height: 16),
],
},
),
),
@@ -242,4 +259,4 @@ class _StarIcon extends StatelessWidget {
}
}
enum BannerType { date, card, payment, collectCharge, finishPlan }
enum BannerType { date, card, payment, collectCharge, finishPlan, setPersonalDiet }

View File

@@ -22,6 +22,7 @@ abstract class ConsultationModel with _$ConsultationModel {
@JsonKey(name: 'end_date', fromJson: parseDate) required DateTime endDate,
@JsonKey(name: 'consultant_id') required int consultantId,
required List<DiaryModel> diaries,
@JsonKey(name: 'personal_diet_id') required int? personalDietId,
}) = _ConsultationModel;
factory ConsultationModel.fromJson(Map<String, dynamic> json) => _$ConsultationModelFromJson(json);

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/common/widgets/markdown_editor.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -54,6 +55,8 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
final nameController = TextEditingController();
final descriptionController = TextEditingController();
final scaffoldKey = GlobalKey<ScaffoldState>();
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
bool _mdToolbarVisible = false;
@override
void initState() {
@@ -76,9 +79,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(
title: state.dietData.name ?? 'Диета',
),
child: SuccessPublishedDietOrPlanBottomSheet(title: state.dietData.name ?? 'Диета'),
);
});
} else if (state.drafted) {
@@ -87,15 +88,15 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
context.pop();
showBottomSheetWithBlur(
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(
title: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках',
),
child: SuccessPublishedDietOrPlanBottomSheet(title: '${state.dietData.name ?? 'Диета'} сохраненна в черновиках'),
);
});
}
return Scaffold(
backgroundColor: appTheme.gray100,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null ? _mdKey.currentState!.buildToolbar(context) : null,
appBar: AppBar(
centerTitle: true,
title: Text(
@@ -124,34 +125,23 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
context.read<CreateDietCubit>().setName(value);
},
),
if (state.dietNameError != null) ...[
const SizedBox(
height: 4,
),
ValidationErrorWidget(error: state.dietNameError ?? ''),
],
if (state.dietNameError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.dietNameError ?? '')],
const SizedBox(height: 12),
SizedBox(
height: 132,
child: CustomTextFormField(
hasError: state.dietDescriptionError != null,
controller: descriptionController,
textAlignVertical: TextAlignVertical.top,
maxLines: null,
minLines: null,
expands: true,
hintText: s.enter_diet_description,
onChanged: (value) {
context.read<CreateDietCubit>().setDetails(value);
},
),
MarkdownEditor(
key: _mdKey,
initialText: descriptionController.text,
hintText: s.enter_diet_description,
hasError: state.dietDescriptionError != null,
minLines: 6,
maxLines: 8,
showInlineToolbar: false,
onToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
onChanged: (value) {
descriptionController.text = value;
context.read<CreateDietCubit>().setDetails(value);
},
),
if (state.dietDescriptionError != null) ...[
const SizedBox(
height: 4,
),
ValidationErrorWidget(error: state.dietDescriptionError ?? ''),
],
if (state.dietDescriptionError != null) ...[const SizedBox(height: 4), ValidationErrorWidget(error: state.dietDescriptionError ?? '')],
const SizedBox(height: 12),
const DietDurationDropDownWidget(),
const SizedBox(height: 12),
@@ -171,9 +161,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
child: Center(
child: Text(
'${s.diet_plan} (${state.dietData.duration ?? 0} ${getDayForm(state.dietData.duration ?? 0, s)})',
style: TextStyles.headingXLSemibold.copyWith(
color: appTheme.black,
),
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
),
@@ -196,9 +184,7 @@ class _CreateDietScreenContentState extends State<CreateDietScreenContent> {
onPressed: () async {
context.read<CreateDietCubit>().setDietState(DietState.draft);
if (widget.diet != null) {
final result = await context.read<CreateDietCubit>().updateDiet(
widget.diet!.id,
);
final result = await context.read<CreateDietCubit>().updateDiet(widget.diet!.id);
if (result == 'error') {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}');

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/common/widgets/markdown_editor.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -28,16 +29,21 @@ class CreatePlanScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<CreatePlanCubit>(),
child: const CreatePlanScreenContent(),
);
return BlocProvider(create: (context) => sl<CreatePlanCubit>(), child: const CreatePlanScreenContent());
}
}
class CreatePlanScreenContent extends StatelessWidget {
class CreatePlanScreenContent extends StatefulWidget {
const CreatePlanScreenContent({super.key});
@override
State<CreatePlanScreenContent> createState() => _CreatePlanScreenContentState();
}
class _CreatePlanScreenContentState extends State<CreatePlanScreenContent> {
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
bool _mdToolbarVisible = false;
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -51,15 +57,14 @@ class CreatePlanScreenContent extends StatelessWidget {
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
child: SuccessPublishedDietOrPlanBottomSheet(
title: state.planData.name ?? 'План',
isPlan: true,
),
child: SuccessPublishedDietOrPlanBottomSheet(title: state.planData.name ?? 'План', isPlan: true),
);
});
}
return Scaffold(
backgroundColor: appTheme.gray100,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null ? _mdKey.currentState!.buildToolbar(context) : null,
appBar: AppBar(
centerTitle: true,
title: Text(
@@ -104,10 +109,7 @@ class CreatePlanScreenContent extends StatelessWidget {
},
maxLength: 150,
),
if (state.validationState.titleHasError) ...[
const SizedBox(height: 4),
ValidationErrorWidget(error: s.atLeast3Symbols),
],
if (state.validationState.titleHasError) ...[const SizedBox(height: 4), ValidationErrorWidget(error: s.atLeast3Symbols)],
],
);
},
@@ -140,20 +142,17 @@ class CreatePlanScreenContent extends StatelessWidget {
builder: (context, state) {
return Column(
children: [
SizedBox(
height: 132,
child: CustomTextFormField(
hasError: state.validationState.descriptionHasError,
textAlignVertical: TextAlignVertical.top,
maxLength: 1000,
maxLines: null,
minLines: null,
expands: true,
hintText: s.plan_desc_example,
onChanged: (value) {
context.read<CreatePlanCubit>().setDetails(value);
},
),
MarkdownEditor(
key: _mdKey,
hintText: s.plan_desc_example,
hasError: state.validationState.descriptionHasError,
minLines: 6,
maxLines: 8,
showInlineToolbar: false,
onToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
onChanged: (value) {
context.read<CreatePlanCubit>().setDetails(value);
},
),
if (state.validationState.descriptionHasError) ...[
const SizedBox(height: 4),
@@ -253,10 +252,7 @@ class CreatePlanScreenContent extends StatelessWidget {
}
return const Padding(
padding: EdgeInsets.only(bottom: 24.0),
child: Align(
alignment: Alignment.bottomCenter,
child: SelectExactOptionWidget(),
),
child: Align(alignment: Alignment.bottomCenter, child: SelectExactOptionWidget()),
);
},
),

View File

@@ -67,6 +67,13 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
validateSelectedOptionItems();
}
void togglePersonalizedDietOptionState() {
final options = [...state.planData.options];
options[state.selectedOptionIndex ?? 0] =
options[state.selectedOptionIndex ?? 0].copyWith(hasPersonalDiet: !options[state.selectedOptionIndex ?? 0].hasPersonalDiet);
emit(state.copyWith(planData: state.planData.copyWith(options: options)));
}
void setDuration(int index) {
emit(
state.copyWith(
@@ -418,18 +425,21 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
'description_md': state.planData.options[0].description,
'items': Map.fromIterable(state.planData.options[0].items),
'has_personal_diet': state.planData.options[0].hasPersonalDiet ? '1' : '0',
},
'packet2_options': {
'short': state.planData.options[1].short,
'price': int.parse((state.planData.options[1].price as double).toStringAsFixed(0)),
'description_md': state.planData.options[1].description,
'items': Map.fromIterable(state.planData.options[1].items),
'has_personal_diet': state.planData.options[1].hasPersonalDiet ? '1' : '0',
},
'packet3_options': {
'short': state.planData.options[2].short,
'price': int.parse((state.planData.options[2].price as double).toStringAsFixed(0)),
'description_md': state.planData.options[2].description,
'items': Map.fromIterable(state.planData.options[2].items),
'has_personal_diet': state.planData.options[2].hasPersonalDiet ? '1' : '0',
},
});
} else {
@@ -440,6 +450,7 @@ class CreatePlanCubit extends Cubit<CreatePlanState> {
'price': int.parse((state.planData.options[0].price as double).toStringAsFixed(0)),
'description_md': state.planData.options[0].description,
'items': Map.fromIterable(state.planData.options[0].items),
'has_personal_diet': state.planData.options[0].hasPersonalDiet ? '1' : '0',
},
});
}

View File

@@ -90,7 +90,7 @@ class _OptionWidgetState extends State<OptionWidget> {
2 => thirdOptionCustomerPriceController,
_ => secondOptionCustomerPriceController,
};
final price = double.tryParse(customerPriceController.text.replaceAll('₽', ''));
creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
firstOptionTitleController.text = state.planData.options[selectedOptionIndex].short;
@@ -255,33 +255,31 @@ class _OptionWidgetState extends State<OptionWidget> {
),
const SizedBox(height: 16),
const CustomDivider(),
// const SizedBox(height: 16),
// Row(
// children: [
// Checkbox(
// value: isChecked,
// activeColor: appTheme.gray,
// checkColor: appTheme.gray,
// fillColor: const WidgetStatePropertyAll(Colors.transparent),
// side: WidgetStateBorderSide.resolveWith(
// (Set<WidgetState> states) {
// return BorderSide(width: 1, color: appTheme.gray);
// },
// ),
// onChanged: (bool? value) {
// setState(() {
// isChecked = value ?? false;
// });
// },
// ),
// Expanded(
// child: Text(
// s.option_personal_diet_desc,
// style: TextStyles.textMMedium.copyWith(color: appTheme.black),
// ),
// ),
// ],
// ),
const SizedBox(height: 16),
Row(
children: [
Checkbox(
value: state.planData.options[selectedOptionIndex].hasPersonalDiet,
activeColor: appTheme.gray,
checkColor: appTheme.gray,
fillColor: const WidgetStatePropertyAll(Colors.transparent),
side: WidgetStateBorderSide.resolveWith(
(Set<WidgetState> states) {
return BorderSide(width: 1, color: appTheme.gray);
},
),
onChanged: (bool? value) {
context.read<CreatePlanCubit>().togglePersonalizedDietOptionState();
},
),
Expanded(
child: Text(
s.option_personal_diet_desc,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
],
),
],
),
);

View File

@@ -8,6 +8,7 @@ import 'package:eatvio_app/core/widgets/success_published_recipe_bottom_sheet.da
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_state.dart';
import 'package:eatvio_app/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart';
import 'package:eatvio_app/common/widgets/markdown_editor.dart';
import 'package:eatvio_app/features/create_recipe/presentation/widgets/ingredients_widget.dart';
import 'package:eatvio_app/features/create_recipe/presentation/widgets/steps_widget.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
@@ -30,6 +31,8 @@ class RecipeCreateScreen extends StatefulWidget {
class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
late final CreateRecipeCubit createRecipeCubit;
final GlobalKey<MarkdownEditorState> _mdKey = GlobalKey<MarkdownEditorState>();
bool _mdToolbarVisible = false;
@override
void initState() {
@@ -54,6 +57,10 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
} else {
return Scaffold(
backgroundColor: const Color(0xFFF0F2F5),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: _mdToolbarVisible && _mdKey.currentState != null
? _mdKey.currentState!.buildToolbar(context)
: null,
appBar: AppBar(
title: widget.recipe != null ? const Text('Редактирование рецепта') : const Text('Добавление рецепта'),
centerTitle: true,
@@ -65,7 +72,11 @@ class _RecipeCreateScreenState extends State<RecipeCreateScreen> {
child: Column(
spacing: 16,
children: [
InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit),
InfoAboutRecipeWidget(
createRecipeCubit: createRecipeCubit,
markdownEditorKey: _mdKey,
onMarkdownToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
),
const IngredientsWidget(),
StepsWidget(createRecipeCubit: createRecipeCubit),
BlocBuilder<CreateRecipeCubit, CreateRecipeState>(

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/common/widgets/markdown_editor.dart';
import 'package:eatvio_app/app_keys.dart';
import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
@@ -16,8 +17,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class InfoAboutRecipeWidget extends StatefulWidget {
final CreateRecipeCubit createRecipeCubit;
final GlobalKey<MarkdownEditorState>? markdownEditorKey;
final void Function(bool visible)? onMarkdownToolbarVisibilityChanged;
const InfoAboutRecipeWidget({super.key, required this.createRecipeCubit});
const InfoAboutRecipeWidget({super.key, required this.createRecipeCubit, this.markdownEditorKey, this.onMarkdownToolbarVisibilityChanged});
@override
State<InfoAboutRecipeWidget> createState() => _InfoAboutRecipeWidgetState();
@@ -105,19 +108,24 @@ class _InfoAboutRecipeWidgetState extends State<InfoAboutRecipeWidget> {
builder: (context, state) {
return Column(
children: [
CustomTextFormField(
KeyedSubtree(
key: AppKeys.recipeCreateScreenDescriptionField,
child: MarkdownEditor(
key: widget.markdownEditorKey,
hintText: 'Расскажите, почему вы выбрали именно этот рецепт? Опишите, каким будет готовое блюдо и в чём его достоинства.',
minLines: 5,
initialValue: state.entity.description,
maxLines: 5,
initialText: state.entity.description,
hasError: state.descriptionValidationError != null,
showInlineToolbar: false,
onToolbarVisibilityChanged: widget.onMarkdownToolbarVisibilityChanged,
onChanged: (value) {
widget.createRecipeCubit.replaceDescription(value);
if (state.descriptionValidationError != null) {
widget.createRecipeCubit.validateDescription();
}
},
),
),
if (state.descriptionValidationError != null) ...[
const SizedBox(height: 4),

View File

@@ -29,7 +29,7 @@ class AuthorCard extends StatelessWidget {
child: Stack(
children: [
Padding(
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 24),
padding: const EdgeInsets.only(left: 16, top: 16, bottom: 24, right: 16),
child: Column(
children: [
ListTile(
@@ -68,20 +68,23 @@ class AuthorCard extends StatelessWidget {
if (author.authorPlaceInRating != null)
InfoBouble(
radius: 12,
backgroundColor: appTheme.whiteA700,
backgroundColor: appTheme.cardBackground,
children: [
Text('${author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400)),
],
),
] else
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(UserDetailsScreen.name, extra: customerProfile!.slug);
},
child: InfoBouble(
radius: 12,
backgroundColor: appTheme.whiteA700,
children: [Text('Анкета пользователя', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400))],
Expanded(
child: GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(UserDetailsScreen.name, extra: customerProfile!.slug);
},
child: InfoBouble(
radius: 12,
backgroundColor: appTheme.cardBackground,
mainAxisAlignment: MainAxisAlignment.center,
children: [Text('Анкета пользователя', style: TextStyles.textMMediumAuto.copyWith(color: appTheme.blueA400))],
),
),
),
],

View File

@@ -13,8 +13,15 @@ class CalendarWidget extends StatefulWidget {
class _CalendarWidgetState extends State<CalendarWidget> {
CalendarFormat _calendarFormat = CalendarFormat.week;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay = DateTime.now();
late DateTime _focusedDay;
DateTime? _selectedDay;
@override
void initState() {
super.initState();
_selectedDay = widget.startDate.isEmpty ? DateTime.now() : widget.startDate[0];
_focusedDay = widget.startDate.isEmpty ? DateTime.now() : widget.startDate[0];
}
@override
Widget build(BuildContext context) {
return Column(
@@ -23,7 +30,7 @@ class _CalendarWidgetState extends State<CalendarWidget> {
Padding(
padding: const EdgeInsets.only(top: 5),
child: TableCalendar(
firstDay: DateTime.now(),
firstDay: widget.startDate.isEmpty ? DateTime.now() : widget.startDate[0],
lastDay: DateTime.utc(2030, 3, 14),
locale: 'ru_RU',
startingDayOfWeek: StartingDayOfWeek.monday,

View File

@@ -2,20 +2,25 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:flutter/material.dart';
class InfoBouble extends StatelessWidget {
const InfoBouble({super.key, required this.children, this.backgroundColor, this.radius = 100, this.padding});
const InfoBouble({
super.key,
required this.children,
this.backgroundColor,
this.radius = 100,
this.padding,
this.mainAxisAlignment = MainAxisAlignment.start,
});
final List<Widget> children;
final Color? backgroundColor;
final double radius;
final EdgeInsetsGeometry? padding;
final MainAxisAlignment mainAxisAlignment;
@override
Widget build(BuildContext context) {
return Container(
padding: padding ?? const EdgeInsets.symmetric(horizontal: 12, vertical: 8.5),
decoration: BoxDecoration(
color: backgroundColor ?? appTheme.gray50,
borderRadius: BorderRadius.circular(radius),
),
child: Row(mainAxisSize: MainAxisSize.min, children: children),
decoration: BoxDecoration(color: backgroundColor ?? appTheme.gray50, borderRadius: BorderRadius.circular(radius)),
child: Row(mainAxisSize: MainAxisSize.min, mainAxisAlignment: mainAxisAlignment, children: children),
);
}
}

View File

@@ -67,11 +67,19 @@ abstract class PacketOptions with _$PacketOptions {
@JsonKey(name: "price") required dynamic price,
@JsonKey(name: "short") required String short,
@JsonKey(name: "description_md") required String description,
@JsonKey(name: "has_personal_diet", fromJson: _parseHasPersonalDietField) required bool hasPersonalDiet,
}) = _PacketOptions;
factory PacketOptions.fromJson(Map<String, dynamic> json) => _$PacketOptionsFromJson(json);
factory PacketOptions.empty() => PacketOptions(items: [], price: double.parse('1200'), short: '', description: '');
factory PacketOptions.empty() => PacketOptions(items: [], price: double.parse('1200'), short: '', description: '', hasPersonalDiet: false);
}
bool _parseHasPersonalDietField(dynamic json) {
if (json == null) return false;
if (json == '1') return true;
if (json == '0') return false;
return json as bool;
}
List<String> _parseItems(dynamic json) {

View File

@@ -1,9 +1,11 @@
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/app_keys.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/cubit/marketing/marketing_cubit.dart';
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
import 'package:eatvio_app/core/widgets/add_item_button.dart';
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
import 'package:eatvio_app/core/widgets/marketing_bottom_sheet.dart';
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/consultants_cubit/consultants_cubit.dart';
@@ -71,6 +73,7 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
void initState() {
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
consultantsScrollController.addListener(_onScrollConsultants);
_checkRegistrationDate(context);
super.initState();
}
@@ -78,6 +81,15 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
}
void _checkRegistrationDate(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final needToShowMarketingBottomSheet = await context.read<MarketingCubit>().checkRegistrationDate();
if (needToShowMarketingBottomSheet && context.mounted) {
showMarketingBottomSheet(context);
}
});
}
@override
void dispose() {
blogPostsScrollController.dispose();

View File

@@ -17,6 +17,7 @@ import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class BlogPostWidget extends StatefulWidget {
final int index;
@@ -57,10 +58,7 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
children: [
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: state.feedBlogPostList[widget.index].author.authorSlug,
);
GoRouter.of(context).pushNamed('profileScreen', extra: state.feedBlogPostList[widget.index].author.authorSlug);
},
child: CircleAvatar(
radius: 20,
@@ -126,14 +124,39 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
const SizedBox(height: 8),
if (state.feedBlogPostList[widget.index].description.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Html(
data:
(state.feedBlogPostList[widget.index].description.length > 200
? state.feedBlogPostList[widget.index].description.substring(0, 200)
: state.feedBlogPostList[widget.index].description)
.replaceAll('\n', '<br>'),
style: {"*": Style(fontSize: FontSize(16), color: Colors.black87)},
data: md.markdownToHtml(
(state.feedBlogPostList[widget.index].description.length > 200
? state.feedBlogPostList[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
: state.feedBlogPostList[widget.index].description.replaceAll('\n', '<br>')),
),
style: {
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
if (state.feedBlogPostList[widget.index].description.length > 200)

View File

@@ -17,15 +17,12 @@ import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class DietsWidget extends StatefulWidget {
class DietsWidget extends StatefulWidget {
final Diet diet;
final int index;
const DietsWidget({
super.key,
required this.diet,
required this.index,
});
const DietsWidget({super.key, required this.diet, required this.index});
@override
State<DietsWidget> createState() => _DietsWidgetState();
@@ -33,18 +30,11 @@ class DietsWidget extends StatefulWidget {
class _DietsWidgetState extends State<DietsWidget> {
bool isMoreOptionsVisible = false;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => GoRouter.of(context).pushNamed(
'aboutDietScreen',
extra: {
'slug': widget.diet.slug,
'index': widget.index,
'navigateToComments': false,
},
),
onTap: () => GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'slug': widget.diet.slug, 'index': widget.index, 'navigateToComments': false}),
child: Material(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
@@ -104,12 +94,7 @@ class _DietsWidgetState extends State<DietsWidget> {
bottom: 16,
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
child: InfoRow(
diet: widget.diet,
itemsBackgroundColor: appTheme.whiteA700,
disableDescriptionOfTypes: true,
type: ProductType.diet,
),
child: InfoRow(diet: widget.diet, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.diet),
),
),
],
@@ -120,26 +105,48 @@ class _DietsWidgetState extends State<DietsWidget> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.diet.name,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
child: Text(widget.diet.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
),
if (widget.diet.details.isNotEmpty) ...[
const SizedBox(
height: 12,
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.diet.details.length > 200 ? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...' : widget.diet.details.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
widget.diet.details.length > 200
? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.diet.details.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
"body": Style(
margin: Margins.zero,
color: appTheme.black,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
@@ -149,12 +156,7 @@ class _DietsWidgetState extends State<DietsWidget> {
padding: EdgeInsets.only(left: 16, top: 8),
child: Text(
'Показать ещё',
style: TextStyle(
height: 1,
color: Color(0xFF8B8B8B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
),
),
],
@@ -172,18 +174,14 @@ class _DietsWidgetState extends State<DietsWidget> {
await context.read<DietsCubit>().likeDiet(widget.diet.id, widget.index);
},
),
const SizedBox(
width: 12,
),
const SizedBox(width: 12),
FeedIconButton(
icon: Icons.chat_bubble_outline,
text: widget.diet.totalComments.toString(),
onTap: () =>
GoRouter.of(context).pushNamed('aboutDietScreen', extra: {'slug': widget.diet.slug, 'index': widget.index, 'navigateToComments': true}),
),
const SizedBox(
width: 12,
),
const SizedBox(width: 12),
FeedIconButton(
svgIcon: SvgPicture.asset('assets/icons/repost.svg'),
onTap: () async {

View File

@@ -19,16 +19,13 @@ import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class PlanWidget extends StatefulWidget {
final PlanModel plan;
final int index;
const PlanWidget({
super.key,
required this.plan,
required this.index,
});
const PlanWidget({super.key, required this.plan, required this.index});
@override
State<PlanWidget> createState() => _PlanWidgetState();
@@ -41,14 +38,7 @@ class _PlanWidgetState extends State<PlanWidget> {
Widget build(BuildContext context) {
final s = S.of(context);
return InkWell(
onTap: () => GoRouter.of(context).pushNamed(
'aboutPlanScreen',
extra: {
'slug': widget.plan.slug,
'index': widget.index,
'navigateToComments': false,
},
),
onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': false}),
child: Material(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
@@ -109,12 +99,7 @@ class _PlanWidgetState extends State<PlanWidget> {
bottom: 16,
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
child: InfoRow(
plan: widget.plan,
itemsBackgroundColor: appTheme.whiteA700,
disableDescriptionOfTypes: true,
type: ProductType.plan,
),
child: InfoRow(plan: widget.plan, itemsBackgroundColor: appTheme.whiteA700, disableDescriptionOfTypes: true, type: ProductType.plan),
),
),
],
@@ -125,26 +110,43 @@ class _PlanWidgetState extends State<PlanWidget> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.plan.name,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black, fontSize: 18),
),
child: Text(widget.plan.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black, fontSize: 18)),
),
if (widget.plan.details.isNotEmpty) ...[
const SizedBox(
height: 12,
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.plan.details.length > 200 ? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...' : widget.plan.details.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
widget.plan.details.length > 200
? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.plan.details.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
margin: Margins.zero,
color: appTheme.black,
fontSize: FontSize(16),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
@@ -153,12 +155,7 @@ class _PlanWidgetState extends State<PlanWidget> {
padding: EdgeInsets.only(left: 16, top: 8),
child: Text(
'Показать ещё',
style: TextStyle(
height: 1,
color: Color(0xFF8B8B8B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
),
),
],
@@ -167,10 +164,7 @@ class _PlanWidgetState extends State<PlanWidget> {
const SizedBox(height: 16),
GestureDetector(
onTap: () {
GoRouter.of(context).pushNamed(
'profileScreen',
extra: widget.plan.author.authorSlug,
);
GoRouter.of(context).pushNamed('profileScreen', extra: widget.plan.author.authorSlug);
},
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
@@ -181,10 +175,7 @@ class _PlanWidgetState extends State<PlanWidget> {
height: 46,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: NetworkImage(sl<AppConfig>().baseUrl + widget.plan.author.authorProfileIcon),
fit: BoxFit.cover,
),
image: DecorationImage(image: NetworkImage(sl<AppConfig>().baseUrl + widget.plan.author.authorProfileIcon), fit: BoxFit.cover),
),
),
subtitle: Row(
@@ -194,12 +185,7 @@ class _PlanWidgetState extends State<PlanWidget> {
backgroundColor: appTheme.gray50,
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 10),
children: [
Text(
'${widget.plan.author.authorPlaceInRating}-й в рейтинге',
style: TextStyles.textXSSemibold.copyWith(
color: appTheme.blueA400,
),
),
Text('${widget.plan.author.authorPlaceInRating}-й в рейтинге', style: TextStyles.textXSSemibold.copyWith(color: appTheme.blueA400)),
],
),
const Spacer(),
@@ -212,21 +198,14 @@ class _PlanWidgetState extends State<PlanWidget> {
),
title: Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
widget.plan.author.authorName,
style: TextStyles.textSSemibold.copyWith(
color: appTheme.black,
),
),
child: Text(widget.plan.author.authorName, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)),
),
),
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.only(
left: 16,
),
padding: const EdgeInsets.only(left: 16),
child: Row(
children: [
FeedIconButton(
@@ -238,20 +217,17 @@ class _PlanWidgetState extends State<PlanWidget> {
await context.read<PlansCubit>().likePlan(widget.plan.slug, widget.index);
},
),
const SizedBox(
width: 12,
),
const SizedBox(width: 12),
FeedIconButton(
icon: Icons.chat_bubble_outline,
text: widget.plan.totalComments.toString(),
onTap: () {
return GoRouter.of(context)
.pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': true});
onTap: () {
return GoRouter.of(
context,
).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'index': widget.index, 'navigateToComments': true});
},
),
const SizedBox(
width: 12,
),
const SizedBox(width: 12),
FeedIconButton(
svgIcon: SvgPicture.asset('assets/icons/repost.svg'),
onTap: () async {

View File

@@ -20,16 +20,13 @@ import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class RecipesWidget extends StatefulWidget {
final int index;
final Recipe recipe;
const RecipesWidget({
super.key,
required this.recipe,
required this.index,
});
const RecipesWidget({super.key, required this.recipe, required this.index});
@override
State<RecipesWidget> createState() => _RecipesWidgetState();
@@ -56,14 +53,8 @@ class _RecipesWidgetState extends State<RecipesWidget> {
}
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => GoRouter.of(context).pushNamed(
'aboutRecipesScreen',
extra: {
'recipe': widget.recipe.slug,
'index': widget.index,
'navigateToComments': false,
},
),
onTap: () =>
GoRouter.of(context).pushNamed('aboutRecipesScreen', extra: {'recipe': widget.recipe.slug, 'index': widget.index, 'navigateToComments': false}),
child: Material(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
@@ -139,10 +130,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.recipe.title ?? '',
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
child: Text(widget.recipe.title ?? '', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
),
if (widget.recipe.videoUrl != null && widget.recipe.videoUrl!.isNotEmpty) ...[
const SizedBox(height: 12),
@@ -152,10 +140,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
children: [
SvgPicture.asset('assets/icons/play_icon.svg', width: 16, height: 16),
const SizedBox(width: 8),
Text(
s.there_is_a_video,
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
),
Text(s.there_is_a_video, style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
],
),
),
@@ -165,17 +150,41 @@ class _RecipesWidgetState extends State<RecipesWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.recipe.description.length > 200
? widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')
: widget.recipe.description.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
widget.recipe.description.length > 200
? widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')
: widget.recipe.description.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
color: appTheme.black,
margin: Margins.all(0),
fontFamily: TextStyles.textSMedium.fontFamily,
"body": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight,
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
@@ -184,12 +193,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
padding: EdgeInsets.only(left: 16, top: 8),
child: Text(
'Показать ещё',
style: TextStyle(
height: 1,
color: Color(0xFF8B8B8B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
),
),
const SizedBox(height: 12),
@@ -211,14 +215,9 @@ class _RecipesWidgetState extends State<RecipesWidget> {
FeedIconButton(
icon: Icons.chat_bubble_outline,
text: widget.recipe.commentsCount.toString(),
onTap: () => GoRouter.of(context).pushNamed(
'aboutRecipesScreen',
extra: {
'recipe': widget.recipe.slug,
'index': widget.index,
'navigateToComments': true,
},
),
onTap: () => GoRouter.of(
context,
).pushNamed('aboutRecipesScreen', extra: {'recipe': widget.recipe.slug, 'index': widget.index, 'navigateToComments': true}),
),
const SizedBox(width: 8),
FeedIconButton(
@@ -239,19 +238,11 @@ class _RecipesWidgetState extends State<RecipesWidget> {
},
),
const Spacer(),
Icon(
Icons.visibility_rounded,
size: 16,
color: appTheme.gray500,
),
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
const SizedBox(width: 4),
Text(
widget.recipe.viewsCount.toString(),
style: TextStyle(
fontSize: 12,
color: appTheme.gray500,
fontWeight: FontWeight.w400,
),
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
),
],
),
@@ -268,26 +259,17 @@ class _RecipesWidgetState extends State<RecipesWidget> {
Container _iconButton(String iconPath, [String? text]) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(16),
),
decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(16)),
height: 32,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
iconPath,
),
SvgPicture.asset(iconPath),
if (text != null) const SizedBox(width: 8),
if (text != null)
Text(
text,
style: const TextStyle(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w500,
),
style: const TextStyle(fontSize: 12, color: Colors.black, fontWeight: FontWeight.w500),
),
],
),

View File

@@ -17,6 +17,7 @@ import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.d
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart';
import 'package:eatvio_app/features/my_diets/presentation/widgets/diet_deleted_bottom_sheet.dart';
import 'package:eatvio_app/features/my_diets/presentation/widgets/diet_drafetd_bottom_sheet.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/personal_diet_suggestion_screen.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -24,14 +25,12 @@ import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class MyDietWidget extends StatefulWidget {
final Diet diet;
const MyDietWidget({
super.key,
required this.diet,
});
const MyDietWidget({super.key, required this.diet});
@override
State<MyDietWidget> createState() => _MyDietWidgetState();
@@ -42,10 +41,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
Widget build(BuildContext context) {
final s = S.of(context);
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -78,26 +74,48 @@ class _MyDietWidgetState extends State<MyDietWidget> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.diet.name,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
child: Text(widget.diet.name, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
),
if (widget.diet.details.isNotEmpty) ...[
const SizedBox(
height: 12,
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.diet.details.length > 200 ? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...' : widget.diet.details.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
widget.diet.details.length > 200
? '${widget.diet.details.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.diet.details.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
"body": Style(
margin: Margins.zero,
color: appTheme.black,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
@@ -129,6 +147,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время');
}
} else if (state.selectedStateIndex == 2) {
GoRouter.of(context).pushNamed(PersonalDietSuggestionScreen.name, extra: widget.diet.id);
} else {
GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: widget.diet);
}
@@ -230,11 +249,7 @@ class _MyDietWidgetState extends State<MyDietWidget> {
},
height: 52,
width: 52,
leftIcon: SvgPicture.asset(
'assets/icons/trash_can.svg',
width: 20,
height: 20,
),
leftIcon: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 20),
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.red),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),

View File

@@ -18,6 +18,7 @@ import 'package:flutter_html/flutter_html.dart';
// Removed SVG icon usage after switching to text buttons
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
import 'package:markdown/markdown.dart' as md;
class MyPlanWidget extends StatefulWidget {
final PlanModel plan;
@@ -43,7 +44,11 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
onTap: () => GoRouter.of(context).pushNamed('aboutPlanScreen', extra: {'slug': widget.plan.slug, 'navigateToComments': false}),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.plan.image}', fit: BoxFit.fitWidth, width: MediaQuery.of(context).size.width),
child: CachedNetworkImage(
imageUrl: '${sl<AppConfig>().baseUrl}/storage/${widget.plan.image}',
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
),
),
Positioned(
@@ -68,16 +73,41 @@ class _MyPlanWidgetState extends State<MyPlanWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.plan.details.length > 200
? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.plan.details.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
widget.plan.details.length > 200
? '${widget.plan.details.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.plan.details.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
"body": Style(
margin: Margins.zero,
color: appTheme.black,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),

View File

@@ -316,7 +316,7 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode
type: type,
planName: json['plan_name'],
customerName: json['client_name'],
planId: json['plan_id'],
planId: json['chosen_plan_id'],
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
);
@@ -1695,6 +1695,35 @@ class DiaryReminderForUsersNotification implements BaseNotificationModel {
}
}
class PersonalDietIsDefinedToCustomerNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
@override
final DateTime createdAt;
final String dietName;
final int dietId;
PersonalDietIsDefinedToCustomerNotification({
required this.type,
required this.isRead,
required this.createdAt,
required this.dietName,
required this.dietId,
});
factory PersonalDietIsDefinedToCustomerNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead, String? createdAt}) {
return PersonalDietIsDefinedToCustomerNotification(
type: type,
isRead: isRead ?? false,
createdAt: parseDate(createdAt ?? json['created_at'] ?? DateTime.now().toIso8601String()),
dietName: json['diet_name'] ?? '',
dietId: json['diet_id'],
);
}
}
enum NotificationType {
// Diet related
dietCancelled,
@@ -1726,6 +1755,7 @@ enum NotificationType {
userDailyReportCompleted,
chosenPlanFinishedToConsultant,
stayOnTrackToCustomer,
personalDietIsDefinedToCustomer,
// Comments
entityCommented,
@@ -1834,6 +1864,7 @@ enum NotificationType {
'chosen-plan-finished-to-consultant' => NotificationType.chosenPlanFinishedToConsultant,
'stay-on-track-to-customer' => NotificationType.stayOnTrackToCustomer,
'diary-reminder-for-users' => NotificationType.diaryReminderForUsers,
'personal-diet-is-defined-to-customer' => NotificationType.personalDietIsDefinedToCustomer,
// Legacy mapping
'App\\Notifications\\Plan\\PlanSubscriptionStartedToAuthor' => NotificationType.planSubscriptionStartedToAuthor,

View File

@@ -261,6 +261,12 @@ class NotificationsCubit extends Cubit<NotificationsState> {
createdAt: createdAt,
),
NotificationType.diaryReminderForUsers => DiaryReminderForUsersNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
NotificationType.personalDietIsDefinedToCustomer => PersonalDietIsDefinedToCustomerNotification.fromJson(
notificationJson,
type,
isRead: isRead,
createdAt: createdAt,
),
NotificationType.unknown => UnknownNotification(type: type),
};
} catch (e) {

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
@@ -113,7 +114,11 @@ class NotificationWidget extends StatelessWidget {
notification: notification as ProductStatusUpdatedForAuthorNotification,
),
DiaryReminderForUsersNotification() => _DiaryReminderForUsersWidget(notification: notification as DiaryReminderForUsersNotification),
PersonalDietIsDefinedToCustomerNotification() => _PersonalDietIsDefinedToCustomerWidget(
notification: notification as PersonalDietIsDefinedToCustomerNotification,
),
UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification),
// TODO: Handle this case.
};
}
}
@@ -364,34 +369,38 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget {
// CardSettingsScreen
class _BaseNotificationWidget extends StatelessWidget {
const _BaseNotificationWidget({required this.notificationsText, required this.isRead, required this.createdAt});
const _BaseNotificationWidget({required this.notificationsText, required this.isRead, required this.createdAt, this.onTap});
final String notificationsText;
final bool isRead;
final DateTime createdAt;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(notificationsText, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
if (!isRead)
Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
width: 8,
height: 8,
return GestureDetector(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(notificationsText, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
),
],
),
const SizedBox(height: 4),
Text(
'${createdAt.day} ${_getMonthName(createdAt.month)} ${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}',
style: TextStyles.textSMedium.copyWith(color: appTheme.gray600),
),
],
if (!isRead)
Container(
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
width: 8,
height: 8,
),
],
),
const SizedBox(height: 4),
Text(
'${createdAt.day} ${_getMonthName(createdAt.month)} ${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}',
style: TextStyles.textSMedium.copyWith(color: appTheme.gray600),
),
],
),
);
}
@@ -1053,3 +1062,17 @@ class _DiaryReminderForUsersWidget extends StatelessWidget {
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Напоминание о заполнении дневника', createdAt: notification.createdAt);
}
}
class _PersonalDietIsDefinedToCustomerWidget extends StatelessWidget {
const _PersonalDietIsDefinedToCustomerWidget({required this.notification});
final PersonalDietIsDefinedToCustomerNotification notification;
@override
Widget build(BuildContext context) {
return _BaseNotificationWidget(
isRead: notification.isRead,
onTap: () => GoRouter.of(context).push(RouteConstants.aboutDietScreen, extra: {'id': notification.dietId, 'navigateToComments': false}),
notificationsText: 'Персональная диета "${notification.dietName}" установлена',
createdAt: notification.createdAt,
);
}
}

View File

@@ -407,6 +407,12 @@ class NotificationsService {
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.personalDietIsDefinedToCustomer => PersonalDietIsDefinedToCustomerNotification.fromJson(
notificationJson['data'],
type,
isRead: readDate != null,
createdAt: createdAt,
),
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)),
};
} catch (e) {

View File

@@ -132,9 +132,9 @@ class _InfoByOrderScreenContentState extends State<InfoByOrderScreenContent> {
const SizedBox(height: 8),
PlanInfo(title: plan.plan.name, description: plan.plan.details, imagePath: plan.plan.image),
const SizedBox(height: 8),
OptionWidget(option: chosenOption, disableDuration: true),
OptionWidget(option: chosenOption, disableDuration: true, duration: plan.plan.duration),
const SizedBox(height: 8),
PlanState(state: plan.chosenPlanState),
PlanState(state: plan.chosenPlanState, hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,),
const SizedBox(height: 20),
if (widget.successfulPayment == null)
CustomOutlinedButton(

View File

@@ -11,11 +11,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class PaymentBanner extends StatefulWidget {
const PaymentBanner({
super.key,
required this.successfulPayment,
});
const PaymentBanner({super.key, required this.successfulPayment});
final bool successfulPayment;
@override
@@ -29,8 +25,9 @@ class _PaymentBannerState extends State<PaymentBanner> {
final s = S.of(context);
final size = MediaQuery.sizeOf(context);
return MainBackgroundBouble(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Stack(children: [
padding: const EdgeInsets.symmetric(vertical: 16),
child: Stack(
children: [
Positioned(
left: 0,
top: 0,
@@ -38,8 +35,9 @@ class _PaymentBannerState extends State<PaymentBanner> {
child: Container(
width: 4,
decoration: BoxDecoration(
color: widget.successfulPayment ? appTheme.blueA400 : appTheme.strongRed,
borderRadius: const BorderRadius.only(topRight: Radius.circular(20), bottomRight: Radius.circular(20))),
color: widget.successfulPayment ? appTheme.blueA400 : appTheme.strongRed,
borderRadius: const BorderRadius.only(topRight: Radius.circular(20), bottomRight: Radius.circular(20)),
),
),
),
Padding(
@@ -50,78 +48,65 @@ class _PaymentBannerState extends State<PaymentBanner> {
? [
SizedBox(
width: size.width * 0.6,
child: Text(
s.cunsutation_was_paid_successfully,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
),
const SizedBox(
height: 8,
child: Text(s.cunsutation_was_paid_successfully, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
),
const SizedBox(height: 8),
SizedBox(
width: size.width * 0.6,
child: Text(
s.wait_for_start_day,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
),
),
const SizedBox(
height: 16,
child: Text(s.wait_for_start_day, style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black)),
),
const SizedBox(height: 16),
isLoading
? const Center(child: Loader())
: CustomOutlinedButton(
text: s.go_to_diary,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle:
ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () async {
GoRouter.of(context).pushReplacement('/diary-screen');
},
),
const SizedBox(height: 10),
isLoading
? const Center(child: Loader())
: CustomOutlinedButton(
text: s.back_to_chat,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle:
ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () {
GoRouter.of(context).pop();
},
)
),
]
: [
SizedBox(
width: size.width * 0.6,
child: Text(
s.smth_went_wrong_payment_failed,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
),
const SizedBox(
height: 8,
child: Text(s.smth_went_wrong_payment_failed, style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
),
const SizedBox(height: 8),
SizedBox(
width: size.width * 0.6,
child: Text(
s.try_again_2,
style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black),
),
),
const SizedBox(
height: 16,
child: Text(s.try_again_2, style: TextStyles.textMMediumAuto.copyWith(color: appTheme.black)),
),
const SizedBox(height: 16),
isLoading
? const Center(child: Loader())
: CustomOutlinedButton(
text: s.pay,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle:
ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
buttonStyle: ButtonStyle(
side: const WidgetStatePropertyAll(BorderSide.none),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
),
onPressed: () async {
final planId = context.read<OrderInfoCubit>().state.chosenPlan?.id;
setState(() {
@@ -133,16 +118,15 @@ class _PaymentBannerState extends State<PaymentBanner> {
});
if (paymentLink != null && planId != null) {
if (!context.mounted) return;
GoRouter.of(context).pushReplacement(
PaymentScreen.path,
extra: {'url': paymentLink, 'planId': planId},
);
GoRouter.of(context).pushReplacement(PaymentScreen.path, extra: {'url': paymentLink, 'planId': planId});
}
},
)
),
],
),
)
]));
),
],
),
);
}
}

View File

@@ -5,42 +5,65 @@ import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
class PlanState extends StatelessWidget {
PlanState({super.key, required this.state});
class PlanState extends StatefulWidget {
const PlanState({super.key, required this.state, required this.hasPersonalDiet});
final ChosenPlanStateEnum state;
final List<List<ChosenPlanStateEnum>> states = [
[
ChosenPlanStateEnum.stateInited,
ChosenPlanStateEnum.stateInCoordination,
],
[
ChosenPlanStateEnum.stateStartDateHasBeenSet,
ChosenPlanStateEnum.statePendingPayment,
],
[
ChosenPlanStateEnum.statePaid,
],
[
ChosenPlanStateEnum.stateStarted,
ChosenPlanStateEnum.stateInProgress,
ChosenPlanStateEnum.stateLastDayStarted,
ChosenPlanStateEnum.stateLastDayFinished,
],
[
ChosenPlanStateEnum.stateComplained,
ChosenPlanStateEnum.stateDone,
ChosenPlanStateEnum.stateChargeWasCollected,
]
];
final bool hasPersonalDiet;
@override
State<PlanState> createState() => _PlanStateState();
}
class _PlanStateState extends State<PlanState> {
late final List<List<ChosenPlanStateEnum>> states;
@override
void initState() {
super.initState();
if (widget.hasPersonalDiet) {
states = [
[ChosenPlanStateEnum.stateInited, ChosenPlanStateEnum.stateInCoordination],
[ChosenPlanStateEnum.stateStartDateHasBeenSet, ChosenPlanStateEnum.statePendingPayment],
[ChosenPlanStateEnum.statePaid],
[ChosenPlanStateEnum.personalDietSet],
[
ChosenPlanStateEnum.stateStarted,
ChosenPlanStateEnum.stateInProgress,
ChosenPlanStateEnum.stateLastDayStarted,
ChosenPlanStateEnum.stateLastDayFinished,
],
[ChosenPlanStateEnum.stateComplained, ChosenPlanStateEnum.stateDone, ChosenPlanStateEnum.stateChargeWasCollected],
];
} else {
states = [
[ChosenPlanStateEnum.stateInited, ChosenPlanStateEnum.stateInCoordination],
[ChosenPlanStateEnum.stateStartDateHasBeenSet, ChosenPlanStateEnum.statePendingPayment],
[ChosenPlanStateEnum.statePaid],
[
ChosenPlanStateEnum.stateStarted,
ChosenPlanStateEnum.stateInProgress,
ChosenPlanStateEnum.stateLastDayStarted,
ChosenPlanStateEnum.stateLastDayFinished,
],
[ChosenPlanStateEnum.stateComplained, ChosenPlanStateEnum.stateDone, ChosenPlanStateEnum.stateChargeWasCollected],
];
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
final titles = [s.order_created, s.startDateHasBeenSet, s.order_paid, s.in_progress, s.done];
final List<String> titles;
if (widget.hasPersonalDiet) {
titles = [s.order_created, s.startDateHasBeenSet, s.order_paid, s.personal_diet_set, s.in_progress, s.done];
} else {
titles = [s.order_created, s.startDateHasBeenSet, s.order_paid, s.in_progress, s.done];
}
return MainBackgroundBouble(
padding: const EdgeInsets.all(16),
child: Column(
children: List.generate(states.length, (index) {
final color = index <= states.indexWhere((element) => element.contains(state)) ? appTheme.greenA700 : appTheme.lightGray00;
final color = index <= states.indexWhere((element) => element.contains(widget.state)) ? appTheme.greenA700 : appTheme.lightGray00;
final text = titles[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -51,29 +74,16 @@ class PlanState extends StatelessWidget {
width: 24,
height: 24,
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
child: Center(
child: Icon(
Icons.check,
color: appTheme.whiteA700,
size: 14,
),
),
child: Center(child: Icon(Icons.check, color: appTheme.whiteA700, size: 14)),
),
const SizedBox(width: 12),
Text(
text,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
Text(text, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
],
),
Padding(
padding: const EdgeInsets.only(left: 12),
child: Container(
height: 12,
width: 1,
color: color,
),
)
child: Container(height: 12, width: 1, color: color),
),
],
);
}),

View File

@@ -0,0 +1,67 @@
import 'dart:developer';
import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_state.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/repository/personal_diet_suggestion_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PersonalDietSuggestionCubit extends Cubit<PersonalDietSuggestionState> {
final DietRepository dietRepository;
final PersonalDietSuggestionRepository personalDietSuggestionService;
late final int dietId;
PersonalDietSuggestionCubit({required this.dietRepository, required this.personalDietSuggestionService}) : super(PersonalDietSuggestionState());
Future<void> setDietName(int dietId) async {
try {
emit(state.copyWith(isLoading: true, error: null));
final diet = await dietRepository.getDiet(dietId);
emit(state.copyWith(isLoading: false, dietName: diet.name));
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
Future<void> getConsultationWithPersonalDiets(int dietId) async {
try {
this.dietId = dietId;
emit(state.copyWith(isLoading: true, error: null));
setDietName(dietId);
final consultations = await personalDietSuggestionService.getConsultationWithPersonalDiets();
final consultationWithDiet = consultations.where((consultation) => consultation.personalDietId != null).toList();
final consultationWithoutDiet = consultations.where((consultation) => consultation.personalDietId == null).toList();
emit(state.copyWith(
isLoading: false,
consultationsWithDiet: consultationWithDiet,
consultationsWithoutDiet: consultationWithoutDiet,
));
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
Future<bool> attachDietToConsultation({
required int consultationId,
}) async {
try {
await personalDietSuggestionService.attachPersonalDiet(consultationId: consultationId, dietId: dietId);
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
return false;
}
}
Future<bool> reattachDietToConsultation({
required int consultationId,
}) async {
try {
await personalDietSuggestionService.reattachPersonalDiet(consultationId: consultationId, dietId: dietId);
return true;
} catch (e, st) {
log(e.toString(), stackTrace: st);
return false;
}
}
}

View File

@@ -0,0 +1,16 @@
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'personal_diet_suggestion_state.freezed.dart';
@freezed
abstract class PersonalDietSuggestionState with _$PersonalDietSuggestionState {
factory PersonalDietSuggestionState({
@Default([]) List<ConsultationModel> consultationsWithDiet,
@Default([]) List<ConsultationModel> consultationsWithoutDiet,
@Default('') String dietName,
@Default(false) bool isLoading,
@Default(null) String? error,
}) = _PersonalDietSuggestionState;
}

View File

@@ -0,0 +1,358 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_with_blur.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class ConsultationWidget extends StatelessWidget {
const ConsultationWidget({super.key, required this.consultationModel, required this.dietName});
final ConsultationModel consultationModel;
final String dietName;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: appTheme.whiteA700,
),
child: Column(
children: [
if (consultationModel.personalDietId == null)
Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(sl<AppConfig>().baseUrl + consultationModel.userProfilePhotoUrl),
),
const SizedBox(width: 12),
Column(
mainAxisSize: MainAxisSize.min,
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
consultationModel.userName,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
Text(
consultationModel.planTitle,
style: TextStyles.textMMedium.copyWith(color: const Color(0xFF88949F)),
),
Text(
'Старт: ${consultationModel.userName}',
style: TextStyles.textMMedium.copyWith(color: const Color(0xFF88949F)),
),
],
)
],
)
else
Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(sl<AppConfig>().baseUrl + consultationModel.userProfilePhotoUrl),
),
const SizedBox(width: 12),
Column(
mainAxisSize: MainAxisSize.min,
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
consultationModel.userName,
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
),
Text(
consultationModel.planTitle,
style: TextStyles.textMMedium.copyWith(color: const Color(0xFF88949F)),
),
],
)
],
),
const SizedBox(
height: 12,
),
if (consultationModel.personalDietId != null)
CustomOutlinedButton(
text: s.changeDiet,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.gray),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.dividerColor)),
onPressed: () async {
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestDietBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
onTap: () async {
final result = await context.read<PersonalDietSuggestionCubit>().attachDietToConsultation(consultationId: consultationModel.id);
AppRouter.navigatorKey.currentContext?.pop();
if (result) {
if (!context.mounted) return;
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestionSuccessBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
),
);
return;
}
if (!context.mounted) return;
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestionFailureBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
),
);
},
),
);
},
)
else
CustomOutlinedButton(
text: s.suggestDiet,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestDietBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
onTap: () async {
AppRouter.navigatorKey.currentContext?.pop();
final result = await context.read<PersonalDietSuggestionCubit>().attachDietToConsultation(consultationId: consultationModel.id);
if (result) {
if (!context.mounted) return;
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestionSuccessBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
),
);
return;
}
if (!context.mounted) return;
showBottomSheetWithBlur(
userRootNavigator: true,
context: context,
height: null,
child: SuggestionFailureBottomSheet(
userName: consultationModel.userName,
dietName: dietName,
),
);
},
),
);
},
)
],
),
);
}
}
class SuggestDietBottomSheet extends StatefulWidget {
const SuggestDietBottomSheet({
super.key,
required this.userName,
required this.dietName,
required this.onTap,
});
final String userName;
final String dietName;
final Future<void> Function() onTap;
@override
State<SuggestDietBottomSheet> createState() => _SuggestDietBottomSheet();
}
class _SuggestDietBottomSheet extends State<SuggestDietBottomSheet> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 8,
),
const BottomSheetHandlerBar(),
const SizedBox(
height: 20,
),
Text('Предложение диеты', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(
height: 16,
),
Text(
'Предложить пользователю ${widget.userName} «${widget.dietName}»',
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
CustomOutlinedButton(
text: s.toOffer,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: widget.onTap,
),
const SizedBox(
height: 40,
),
],
),
);
}
}
class SuggestionSuccessBottomSheet extends StatefulWidget {
const SuggestionSuccessBottomSheet({
super.key,
required this.userName,
required this.dietName,
});
final String userName;
final String dietName;
@override
State<SuggestionSuccessBottomSheet> createState() => _SuggestionSuccessBottomSheet();
}
class _SuggestionSuccessBottomSheet extends State<SuggestionSuccessBottomSheet> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 8,
),
const BottomSheetHandlerBar(),
const SizedBox(
height: 20,
),
Text('Предложение диеты', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(
height: 16,
),
Text(
'«${widget.dietName}» предложена пользователю ${widget.userName}',
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
CustomOutlinedButton(
text: s.ok,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
AppRouter.navigatorKey.currentContext?.pop();
},
),
const SizedBox(
height: 40,
),
],
),
);
}
}
class SuggestionFailureBottomSheet extends StatefulWidget {
const SuggestionFailureBottomSheet({
super.key,
required this.userName,
required this.dietName,
});
final String userName;
final String dietName;
@override
State<SuggestionFailureBottomSheet> createState() => _SuggestionFailureBottomSheet();
}
class _SuggestionFailureBottomSheet extends State<SuggestionFailureBottomSheet> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 8,
),
const BottomSheetHandlerBar(),
const SizedBox(
height: 20,
),
Text('Предложение диеты', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
const SizedBox(
height: 16,
),
Text(
'Не удалось предложить диету ${widget.dietName} пользователю ${widget.userName}',
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
CustomOutlinedButton(
text: s.ok,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
AppRouter.navigatorKey.currentContext?.pop();
},
),
const SizedBox(
height: 40,
),
],
),
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_cubit.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/cubit/personal_diet_suggestion_state.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/presentation/consultation_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PersonalDietSuggestionScreen extends StatelessWidget {
final int dietId;
const PersonalDietSuggestionScreen({super.key, required this.dietId});
static const path = '/personal-diet-suggestion';
static const name = 'personalDietSuggestion';
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => sl<PersonalDietSuggestionCubit>()..getConsultationWithPersonalDiets(dietId),
child: const PersonalDietSuggestionScreenContent(),
);
}
}
class PersonalDietSuggestionScreenContent extends StatelessWidget {
const PersonalDietSuggestionScreenContent({super.key});
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<PersonalDietSuggestionCubit, PersonalDietSuggestionState>(
builder: (context, state) {
return Scaffold(
backgroundColor: appTheme.gray100,
appBar: AppBar(
centerTitle: true,
title: Text(
'${s.dietSuggestion}\n«${state.dietName}»',
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
body: state.isLoading
? const Center(child: Loader())
: SingleChildScrollView(
child: Column(
children: [
...List.generate(state.consultationsWithDiet.length, (index) {
return ConsultationWidget(
consultationModel: state.consultationsWithDiet[index],
dietName: state.dietName,
);
}),
const SizedBox(
height: 12,
),
...List.generate(state.consultationsWithoutDiet.length, (index) {
return ConsultationWidget(
consultationModel: state.consultationsWithoutDiet[index],
dietName: state.dietName,
);
})
],
),
),
);
},
);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/personal_diet_suggestion/services/personal_diet_suggestion_service.dart';
class PersonalDietSuggestionRepository {
final PersonalDietSuggestionService personalDietSuggestionService;
PersonalDietSuggestionRepository({required this.personalDietSuggestionService});
Future<List<ConsultationModel>> getConsultationWithPersonalDiets() async{
try {
return (await personalDietSuggestionService.getConsultationWithPersonalDiets()).data;
} catch (e) {
rethrow;
}
}
Future<void> attachPersonalDiet({required int consultationId, required int dietId}) {
try {
return personalDietSuggestionService.attachPersonalDiet(consultationId: consultationId, dietId: dietId);
} catch (e) {
rethrow;
}
}
Future<void> reattachPersonalDiet({required int consultationId, required int dietId}) {
try {
return personalDietSuggestionService.reattachPersonalDiet(consultationId: consultationId, dietId: dietId);
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,36 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
class PersonalDietSuggestionService {
final Dio dio;
PersonalDietSuggestionService(this.dio);
Future<ConsultationPaginationModel> getConsultationWithPersonalDiets() async {
try {
final response = await dio.get('/api/consultations', queryParameters: {
'filters[plan][has_personal_diet][\$personal_diet]': '1',
'filters[last_state][\$notIn][]': ['done', 'charge_was_collected', 'complained', 'last_day_finished', 'canceled_by_the_initiator', 'inited'],
'sort[]': 'start_date:desc',
});
return ConsultationPaginationModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<void> attachPersonalDiet({required int consultationId, required int dietId}) async {
try {
await dio.post('/api/chosen_plans/$consultationId/attach_diet', data: {'diet_id': dietId});
} catch (e) {
rethrow;
}
}
Future<void> reattachPersonalDiet({required int consultationId, required int dietId}) async {
try {
await dio.post('/api/chosen_plans/$consultationId/re_attach_diet', data: {'diet_id': dietId});
} catch (e) {
rethrow;
}
}
}

View File

@@ -27,6 +27,8 @@ enum ChosenPlanStateEnum {
stateComplained,
@JsonValue('done')
stateDone,
@JsonValue('personal_diet_setted')
personalDietSet,
@JsonValue('charge_was_collected')
stateChargeWasCollected;
@@ -40,6 +42,7 @@ enum ChosenPlanStateEnum {
ChosenPlanStateEnum.stateLastDayFinished,
ChosenPlanStateEnum.stateDone,
ChosenPlanStateEnum.stateInProgress,
ChosenPlanStateEnum.personalDietSet,
];
}
@@ -64,7 +67,8 @@ enum ChosenPlanStateEnum {
ChosenPlanStateEnum.stateLastDayStarted,
ChosenPlanStateEnum.stateLastDayFinished,
ChosenPlanStateEnum.stateComplained,
ChosenPlanStateEnum.stateDone
ChosenPlanStateEnum.stateDone,
ChosenPlanStateEnum.personalDietSet
];
}
}

View File

@@ -505,12 +505,15 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
switch (state.chosenOptionIndex) {
1 => OptionWidget(
option: state.planModel?.packet2Options,
duration: state.planModel?.duration ?? 0,
),
2 => OptionWidget(
option: state.planModel?.packet3Options,
duration: state.planModel?.duration ?? 0,
),
_ => OptionWidget(
option: state.planModel?.packet1Options,
duration: state.planModel?.duration ?? 0,
),
},
const SizedBox(

View File

@@ -16,17 +16,14 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:share_plus/share_plus.dart';
class ProfileBlogPostWidget extends StatefulWidget {
final int index;
final ProfileCubit profileCubit;
const ProfileBlogPostWidget({
super.key,
required this.index,
required this.profileCubit,
});
const ProfileBlogPostWidget({super.key, required this.index, required this.profileCubit});
@override
State<ProfileBlogPostWidget> createState() => _ProfileBlogPostWidgetState();
@@ -46,15 +43,9 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
totalLikes = state.blogPosts[widget.index].totalLikes;
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: state.blogPosts[widget.index].id,
),
onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.blogPosts[widget.index].id),
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -80,19 +71,12 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
children: [
Text(
state.blogPosts[widget.index].author.authorName,
style: const TextStyle(
height: 1,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
style: const TextStyle(height: 1, color: Colors.black87, fontWeight: FontWeight.w500),
),
const SizedBox(height: 4),
Text(
'${calculateDifference(state.blogPosts[widget.index].createdAt)} в ${state.blogPosts[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.blogPosts[widget.index].createdAt.minute.toString().padLeft(2, '0')}',
style: const TextStyle(
height: 1,
color: Colors.black45,
),
style: const TextStyle(height: 1, color: Colors.black45),
),
],
),
@@ -114,11 +98,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
state.blogPosts[widget.index].title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
color: Colors.black87,
),
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18, color: Colors.black87),
),
),
],
@@ -128,14 +108,36 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Html(
data: state.blogPosts[widget.index].description.length > 200
? state.blogPosts[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
: state.blogPosts[widget.index].description.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
state.blogPosts[widget.index].description.length > 200
? state.blogPosts[widget.index].description.substring(0, 200).replaceAll('\n', '<br>')
: state.blogPosts[widget.index].description.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
fontSize: FontSize(16),
color: Colors.black87,
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero, fontSize: FontSize(16), lineHeight: const LineHeight(1.5)),
"p": Style(margin: Margins.only(bottom: 12), color: Colors.black87),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: Colors.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: Colors.black87),
"strong, b": Style(fontWeight: FontWeight.bold, color: Colors.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
@@ -144,12 +146,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text(
'Показать ещё',
style: TextStyle(
height: 1,
color: Color(0xFF8B8B8B),
fontSize: 14,
fontWeight: FontWeight.w600,
),
style: TextStyle(height: 1, color: Color(0xFF8B8B8B), fontSize: 14, fontWeight: FontWeight.w600),
),
),
if (state.blogPosts[widget.index].title.isNotEmpty || state.blogPosts[widget.index].description.isNotEmpty) const SizedBox(height: 8),
@@ -157,10 +154,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
onTap: () {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: ['${sl<AppConfig>().baseUrl}/storage/${state.blogPosts[widget.index].image}'],
index: 0,
),
PhotoViewScreen(imageUrls: ['${sl<AppConfig>().baseUrl}/storage/${state.blogPosts[widget.index].image}'], index: 0),
);
},
child: Hero(
@@ -192,10 +186,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
isLiked
? await context.read<FeedCubit>().likePost(state.blogPosts[widget.index].id)
: await context.read<FeedCubit>().unlikedPost(state.blogPosts[widget.index].id);
widget.profileCubit.updateFeedBlogPostList(
state.blogPosts[widget.index],
isLiked ? false : true,
);
widget.profileCubit.updateFeedBlogPostList(state.blogPosts[widget.index], isLiked ? false : true);
},
),
const SizedBox(width: 8),
@@ -204,16 +195,10 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
'assets/icons/comment.svg',
height: 20,
width: 20,
colorFilter: ColorFilter.mode(
appTheme.gray600,
BlendMode.srcIn,
),
colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn),
),
text: state.blogPosts[widget.index].totalComments.toString(),
onTap: () => GoRouter.of(context).pushNamed(
'blogPostScreen',
extra: state.blogPosts[widget.index].id,
),
onTap: () => GoRouter.of(context).pushNamed('blogPostScreen', extra: state.blogPosts[widget.index].id),
),
const SizedBox(width: 8),
FeedIconButton(
@@ -234,19 +219,11 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
},
),
const Spacer(),
Icon(
Icons.visibility_rounded,
size: 16,
color: appTheme.gray500,
),
Icon(Icons.visibility_rounded, size: 16, color: appTheme.gray500),
const SizedBox(width: 4),
Text(
state.blogPosts[widget.index].totalViews.toString(),
style: TextStyle(
fontSize: 12,
color: appTheme.gray500,
fontWeight: FontWeight.w400,
),
style: TextStyle(fontSize: 12, color: appTheme.gray500, fontWeight: FontWeight.w400),
),
],
),

View File

@@ -15,15 +15,13 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:markdown/markdown.dart' as md;
import 'package:share_plus/share_plus.dart';
class MyRecipeWidget extends StatefulWidget {
final MyRecipeModel recipe;
const MyRecipeWidget({
super.key,
required this.recipe,
});
const MyRecipeWidget({super.key, required this.recipe});
@override
State<MyRecipeWidget> createState() => _MyRecipeWidgetState();
@@ -50,10 +48,7 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
Widget build(BuildContext context) {
final s = S.of(context);
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: Colors.white),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -61,13 +56,7 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
Stack(
children: [
InkWell(
onTap: () async => GoRouter.of(context).pushNamed(
'aboutRecipesScreen',
extra: {
'recipe': widget.recipe.slug,
'navigateToComments': false,
},
),
onTap: () async => GoRouter.of(context).pushNamed('aboutRecipesScreen', extra: {'recipe': widget.recipe.slug, 'navigateToComments': false}),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
@@ -99,28 +88,48 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.recipe.title!,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
child: Text(widget.recipe.title!, style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
),
if (widget.recipe.description.isNotEmpty) ...[
const SizedBox(
height: 12,
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.recipe.description.length > 200
? '${widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.recipe.description.replaceAll('\n', '<br>'),
data: md.markdownToHtml(
widget.recipe.description.length > 200
? '${widget.recipe.description.substring(0, 200).replaceAll('\n', '<br>')}...'
: widget.recipe.description.replaceAll('\n', '<br>'),
),
style: {
"*": Style(
"body": Style(
margin: Margins.zero,
color: appTheme.black,
padding: HtmlPaddings.zero,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
lineHeight: const LineHeight(1.5),
),
"p": Style(margin: Margins.only(bottom: 12), color: appTheme.black),
"h1, h2, h3, h4, h5, h6": Style(margin: Margins.only(top: 16, bottom: 8), color: appTheme.black, fontWeight: FontWeight.bold),
"h1": Style(fontSize: FontSize(24)),
"h2": Style(fontSize: FontSize(20)),
"h3": Style(fontSize: FontSize(18)),
"ul, ol": Style(margin: Margins.only(bottom: 12), padding: HtmlPaddings.only(left: 16)),
"li": Style(margin: Margins.only(bottom: 4), color: appTheme.black),
"strong, b": Style(fontWeight: FontWeight.bold, color: appTheme.black),
"em, i": Style(fontStyle: FontStyle.italic),
"code": Style(
backgroundColor: Colors.grey.shade100,
padding: HtmlPaddings.symmetric(horizontal: 4, vertical: 2),
fontFamily: 'monospace',
),
"pre": Style(backgroundColor: Colors.grey.shade100, padding: HtmlPaddings.all(12), margin: Margins.only(bottom: 12)),
"blockquote": Style(
padding: HtmlPaddings.only(left: 16),
margin: Margins.only(left: 8, bottom: 12),
fontStyle: FontStyle.italic,
color: Colors.grey.shade700,
),
"a": Style(color: Colors.blue, textDecoration: TextDecoration.underline),
"img": Style(width: Width(MediaQuery.of(context).size.width - 32)),
},
),
),
@@ -192,26 +201,17 @@ class _MyRecipeWidgetState extends State<MyRecipeWidget> {
Container _iconButton(String iconPath, [String? text]) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(16),
),
decoration: BoxDecoration(color: appTheme.whiteA700, borderRadius: BorderRadius.circular(16)),
height: 32,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
iconPath,
),
SvgPicture.asset(iconPath),
if (text != null) const SizedBox(width: 8),
if (text != null)
Text(
text,
style: const TextStyle(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w500,
),
style: const TextStyle(fontSize: 12, color: Colors.black, fontWeight: FontWeight.w500),
),
],
),

View File

@@ -3,6 +3,8 @@ import 'dart:developer';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/firebase_api.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/mixins/registration_date_cache_mixin.dart';
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
@@ -12,10 +14,9 @@ import 'package:eatvio_app/features/registration/services/registration_analytics
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class RegistrationCubit extends Cubit<RegistrationState> {
class RegistrationCubit extends Cubit<RegistrationState> with RegistrationDateCacheMixin {
final RegistrationRepository registrationRepository;
RegistrationCubit({required this.registrationRepository})
RegistrationCubit({required this.registrationRepository, required Prefs prefs})
: super(
const RegistrationState(
sheet: UserRegistrationSheet(
@@ -47,7 +48,9 @@ class RegistrationCubit extends Cubit<RegistrationState> {
phone: '',
),
),
);
) {
this.prefs = prefs;
}
void updateSheet(UserRegistrationSheet sheet) => emit(RegistrationState(sheet: sheet));
@@ -59,6 +62,7 @@ class RegistrationCubit extends Cubit<RegistrationState> {
await TokenStorage.saveTokenInStorage(token);
await sl<FirebaseApi>().registerPushToken();
RegistrationAnalyticsService.reportRegistrationComplete();
cacheRegistrationDate(DateTime.now());
AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen');
} catch (e, st) {
log(e.toString(), stackTrace: st);

View File

@@ -49,8 +49,7 @@ class _UserDetailsContentState extends State<UserDetailsContent> {
final List<String> chipsIntervalList = ['1', '3', '6', '12', 'all'];
List<WeightModel> weightPoints = [];
List<CircumferenceModel> circumferencePoints = [];
int _selectedChipIndex = 0;
List<CircumferenceModel> circumferencePoints = []; int _selectedChipIndex = 0;
int _selectedIntervalChipIndex = 0;
bool isMoreOptionsVisible = false;
@override

View File

@@ -138,6 +138,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы.",
),
"carbohydrates": MessageLookupByLibrary.simpleMessage("Углеводы"),
"changeDiet": MessageLookupByLibrary.simpleMessage("Заменить диету"),
"chargeApproved": MessageLookupByLibrary.simpleMessage("Выплата одобрена"),
"chargeCollectionFailed": MessageLookupByLibrary.simpleMessage(
"Не удалось оформить выплату",
@@ -158,6 +159,10 @@ class MessageLookup extends MessageLookupByLibrary {
"choose_plan_start_date": MessageLookupByLibrary.simpleMessage(
"Выберите дату старта плана питания",
),
"choose_plan_start_date_with_personal_diet":
MessageLookupByLibrary.simpleMessage(
"Выберите дату старта плана питания с учетом составления персональной диеты",
),
"choose_start_diet_date": MessageLookupByLibrary.simpleMessage(
"Выберите дату старты диеты. Она отобразиться у Вас в дневнике",
),
@@ -165,6 +170,9 @@ class MessageLookup extends MessageLookupByLibrary {
"chose_plan_start_date_2": MessageLookupByLibrary.simpleMessage(
"Выберите дату старты плана. Она отобразиться в дневнике пользователя",
),
"chosenPlanStartDateDescr": MessageLookupByLibrary.simpleMessage(
"Дату старта плана можно выбрать не ранее, чем через два. Такое ограничение дается на создание персональной диеты",
),
"circumference_empty": MessageLookupByLibrary.simpleMessage(
"Все замеры обязательны к заполнению",
),
@@ -199,6 +207,7 @@ class MessageLookup extends MessageLookupByLibrary {
"congratsYouHaveFinishedPlan": MessageLookupByLibrary.simpleMessage(
"Поздравляем, Вы закончили план",
),
"congratulations": MessageLookupByLibrary.simpleMessage("Поздравляем!"),
"consultantLeftComment": m0,
"consultations_count": MessageLookupByLibrary.simpleMessage(
"Кол-во проведенных консулдьтаций",
@@ -245,6 +254,9 @@ class MessageLookup extends MessageLookupByLibrary {
"Не удалось обновить диету",
),
"country": MessageLookupByLibrary.simpleMessage("Страна"),
"createPersonalDiet": MessageLookupByLibrary.simpleMessage(
"Составьте персональную диету",
),
"create_diet": MessageLookupByLibrary.simpleMessage("Создание диеты"),
"cunsutation_was_paid_successfully": MessageLookupByLibrary.simpleMessage(
"Поздравляем, консультация успешно оплачена",
@@ -273,6 +285,7 @@ class MessageLookup extends MessageLookupByLibrary {
"deleted": MessageLookupByLibrary.simpleMessage("Удалена"),
"diary": MessageLookupByLibrary.simpleMessage("Дневник"),
"diet": MessageLookupByLibrary.simpleMessage("Диета"),
"dietSuggestion": MessageLookupByLibrary.simpleMessage("Предложение диеты"),
"diet_author": MessageLookupByLibrary.simpleMessage("Автор диеты"),
"diet_has_been_sent_to_draft": MessageLookupByLibrary.simpleMessage(
"снята с публикации и перенесена в черновик",
@@ -306,6 +319,9 @@ class MessageLookup extends MessageLookupByLibrary {
"eating_behavior_type_desc": MessageLookupByLibrary.simpleMessage(
"Формирование здорового пищевого поведения — это процесс, который включает в себя изменение привычек и отношения к еде. Он направлен на то, чтобы помочь людям научиться правильно питаться и поддерживать здоровый образ жизни.Основные принципы формирования здорового пищевого поведения: \n● Разнообразие продуктов; \n● Сбалансированный рацион; \n● Питьевой режим; \n● Ограничение вредных продуктов; \n● Регулярные приёмы пищи; \n● Умеренность;Осознанное питание; \n● Физическая активность;",
),
"eatvioIsAHelthyHabbit": MessageLookupByLibrary.simpleMessage(
"Итвио — это полезная привычка",
),
"economy": MessageLookupByLibrary.simpleMessage("Эконом"),
"edit": MessageLookupByLibrary.simpleMessage("Редактировать"),
"email": MessageLookupByLibrary.simpleMessage("Электронная почта"),
@@ -379,6 +395,7 @@ class MessageLookup extends MessageLookupByLibrary {
"for_whole_period_of_time": MessageLookupByLibrary.simpleMessage(
"За всё время",
),
"fourteenDays": MessageLookupByLibrary.simpleMessage("14 дней"),
"fraud": MessageLookupByLibrary.simpleMessage("Мошенничество"),
"fraud_description": MessageLookupByLibrary.simpleMessage(
"Мошенничество с целью наживы. Если вас обманули, пожалуйста, сообщите об этом в правоохранительные органы.",
@@ -388,6 +405,9 @@ class MessageLookup extends MessageLookupByLibrary {
"from_z_to_a": MessageLookupByLibrary.simpleMessage("Название: Я-А"),
"gallery": MessageLookupByLibrary.simpleMessage("Галерея"),
"general_info": MessageLookupByLibrary.simpleMessage("Общая информация"),
"goToMyDiets": MessageLookupByLibrary.simpleMessage(
"Перейти к моим диетам",
),
"go_to_diary": MessageLookupByLibrary.simpleMessage("Перейти в дневник"),
"gr": MessageLookupByLibrary.simpleMessage("гр"),
"grams": MessageLookupByLibrary.simpleMessage("гр"),
@@ -419,6 +439,7 @@ class MessageLookup extends MessageLookupByLibrary {
"initial_measurements": MessageLookupByLibrary.simpleMessage(
"Первичные замеры",
),
"invite": MessageLookupByLibrary.simpleMessage("Пригласить"),
"invited_people_count": MessageLookupByLibrary.simpleMessage(
"Количество приглашенных",
),
@@ -441,6 +462,9 @@ class MessageLookup extends MessageLookupByLibrary {
"leaveComplainRequest": MessageLookupByLibrary.simpleMessage(
"Подать жалобу",
),
"letsInvite": MessageLookupByLibrary.simpleMessage(
"Давайте пригласим близких в итвио",
),
"light_type": MessageLookupByLibrary.simpleMessage("Легкая"),
"light_type_desc": MessageLookupByLibrary.simpleMessage(
"С некоторыми видами диет можно употреблять алкоголь в умеренных количествах. Рекомендуется совмещать вместе с низкокалорийными закусками.",
@@ -561,6 +585,12 @@ class MessageLookup extends MessageLookupByLibrary {
"pcs": MessageLookupByLibrary.simpleMessage("шт"),
"pending_payment": MessageLookupByLibrary.simpleMessage("Ожидается оплата"),
"personal": MessageLookupByLibrary.simpleMessage("Персональные"),
"personalDietCreating": MessageLookupByLibrary.simpleMessage(
"Составление индивидуальной диеты",
),
"personal_diet_set": MessageLookupByLibrary.simpleMessage(
"Персональная диета установлена",
),
"personal_diet_state": MessageLookupByLibrary.simpleMessage("Персональная"),
"photo": MessageLookupByLibrary.simpleMessage("Фото"),
"physical_fitness": MessageLookupByLibrary.simpleMessage(
@@ -690,6 +720,7 @@ class MessageLookup extends MessageLookupByLibrary {
"successfully_published": MessageLookupByLibrary.simpleMessage(
"успешно опубликован",
),
"suggestDiet": MessageLookupByLibrary.simpleMessage("Предложить диету"),
"suicide": MessageLookupByLibrary.simpleMessage("Склонение к самоубийству"),
"takes_pills": MessageLookupByLibrary.simpleMessage("Принимает лекарства"),
"tap_on_link_to_pay": MessageLookupByLibrary.simpleMessage(
@@ -774,6 +805,9 @@ class MessageLookup extends MessageLookupByLibrary {
"want_to_see_tomorrow": MessageLookupByLibrary.simpleMessage(
"Хотите заглянуть в будущее?",
),
"weAreLookingFor": MessageLookupByLibrary.simpleMessage(
"Мы собираем самое большое сообщество людей, желающих вести здоровый образ жизни!",
),
"weapon": MessageLookupByLibrary.simpleMessage("Оружие"),
"weight": MessageLookupByLibrary.simpleMessage("Вес"),
"weight_chart": MessageLookupByLibrary.simpleMessage("График веса"),
@@ -802,6 +836,7 @@ class MessageLookup extends MessageLookupByLibrary {
"youShureYouWantToDeleteAccount": MessageLookupByLibrary.simpleMessage(
"Вы уверены, что хотите удалить аккаунт?",
),
"youWithUs": MessageLookupByLibrary.simpleMessage("Вы с итвио уже\n"),
"you_can_not_open_this_day": MessageLookupByLibrary.simpleMessage(
"Вы не можете перейти в этот день",
),

View File

@@ -3434,6 +3434,86 @@ class S {
);
}
/// `Составление индивидуальной диеты`
String get personalDietCreating {
return Intl.message(
'Составление индивидуальной диеты',
name: 'personalDietCreating',
desc: '',
args: [],
);
}
/// `Предложение диеты`
String get dietSuggestion {
return Intl.message(
'Предложение диеты',
name: 'dietSuggestion',
desc: '',
args: [],
);
}
/// `Предложить диету`
String get suggestDiet {
return Intl.message(
'Предложить диету',
name: 'suggestDiet',
desc: '',
args: [],
);
}
/// `Заменить диету`
String get changeDiet {
return Intl.message(
'Заменить диету',
name: 'changeDiet',
desc: '',
args: [],
);
}
/// `Выберите дату старта плана питания с учетом составления персональной диеты`
String get choose_plan_start_date_with_personal_diet {
return Intl.message(
'Выберите дату старта плана питания с учетом составления персональной диеты',
name: 'choose_plan_start_date_with_personal_diet',
desc: '',
args: [],
);
}
/// `Дату старта плана можно выбрать не ранее, чем через два. Такое ограничение дается на создание персональной диеты`
String get chosenPlanStartDateDescr {
return Intl.message(
'Дату старта плана можно выбрать не ранее, чем через два. Такое ограничение дается на создание персональной диеты',
name: 'chosenPlanStartDateDescr',
desc: '',
args: [],
);
}
/// `Составьте персональную диету`
String get createPersonalDiet {
return Intl.message(
'Составьте персональную диету',
name: 'createPersonalDiet',
desc: '',
args: [],
);
}
/// `Перейти к моим диетам`
String get goToMyDiets {
return Intl.message(
'Перейти к моим диетам',
name: 'goToMyDiets',
desc: '',
args: [],
);
}
/// `Удаление аккаунта`
String get accountDeleting {
return Intl.message(
@@ -3503,6 +3583,76 @@ class S {
String get customer {
return Intl.message('Покупатель', name: 'customer', desc: '', args: []);
}
/// `Персональная диета установлена`
String get personal_diet_set {
return Intl.message(
'Персональная диета установлена',
name: 'personal_diet_set',
desc: '',
args: [],
);
}
/// `Поздравляем!`
String get congratulations {
return Intl.message(
'Поздравляем!',
name: 'congratulations',
desc: '',
args: [],
);
}
/// `Вы с итвио уже\n`
String get youWithUs {
return Intl.message(
'Вы с итвио уже\n',
name: 'youWithUs',
desc: '',
args: [],
);
}
/// `14 дней`
String get fourteenDays {
return Intl.message('14 дней', name: 'fourteenDays', desc: '', args: []);
}
/// `Мы собираем самое большое сообщество людей, желающих вести здоровый образ жизни!`
String get weAreLookingFor {
return Intl.message(
'Мы собираем самое большое сообщество людей, желающих вести здоровый образ жизни!',
name: 'weAreLookingFor',
desc: '',
args: [],
);
}
/// `Давайте пригласим близких в итвио`
String get letsInvite {
return Intl.message(
'Давайте пригласим близких в итвио',
name: 'letsInvite',
desc: '',
args: [],
);
}
/// `Итвио — это полезная привычка`
String get eatvioIsAHelthyHabbit {
return Intl.message(
'Итвио — это полезная привычка',
name: 'eatvioIsAHelthyHabbit',
desc: '',
args: [],
);
}
/// `Пригласить`
String get invite {
return Intl.message('Пригласить', name: 'invite', desc: '', args: []);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -408,6 +408,14 @@
"copy": "Скопировать",
"reply": "Ответить",
"deleteComment": "Удалить комментарий",
"personalDietCreating": "Составление индивидуальной диеты",
"dietSuggestion": "Предложение диеты",
"suggestDiet": "Предложить диету",
"changeDiet": "Заменить диету",
"choose_plan_start_date_with_personal_diet": "Выберите дату старта плана питания с учетом составления персональной диеты",
"chosenPlanStartDateDescr": "Дату старта плана можно выбрать не ранее, чем через два. Такое ограничение дается на создание персональной диеты",
"createPersonalDiet": "Составьте персональную диету",
"goToMyDiets": "Перейти к моим диетам",
"accountDeleting": "Удаление аккаунта",
"youShureYouWantToDeleteAccount": "Вы уверены, что хотите удалить аккаунт?",
"itCantBeRecovered": "Его нельзя будет восстановить",
@@ -415,6 +423,14 @@
"cannotDeleteAccount": "Не получилось удалить ваш аккаунт",
"resend": "Отправить повторно",
"notifications": "Уведомления",
"customer": "Покупатель"
"customer": "Покупатель",
"personal_diet_set": "Персональная диета установлена",
"congratulations": "Поздравляем!",
"youWithUs": "Вы с итвио уже\n",
"fourteenDays": "14 дней",
"weAreLookingFor": "Мы собираем самое большое сообщество людей, желающих вести здоровый образ жизни!",
"letsInvite": "Давайте пригласим близких в итвио",
"eatvioIsAHelthyHabbit": "Итвио — это полезная привычка",
"invite": "Пригласить"
}

View File

@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
version: 1.3.2+81
version: 1.3.3+82
publish_to: 'none'
environment:
@@ -76,6 +76,7 @@ dependencies:
appmetrica_plugin: ^3.3.0
vk_id: ^1.0.3
flutter_login_yandex: ^0.0.5
markdown: ^7.3.0
dev_dependencies:
build_runner: ^2.7.0

View File

@@ -1,5 +1,6 @@
// ignore_for_file: deprecated_member_use
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
@@ -10,7 +11,7 @@ import 'package:mockito/annotations.dart';
import 'basic_widget_test.mocks.dart';
@GenerateMocks([RegistrationRepository])
@GenerateMocks([RegistrationRepository, Prefs])
void main() {
group('Registration Basic Widget Tests', () {
late MockRegistrationRepository mockRegistrationRepository;
@@ -18,7 +19,7 @@ void main() {
setUp(() {
mockRegistrationRepository = MockRegistrationRepository();
registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository);
registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs());
});
tearDown(() {
@@ -37,7 +38,7 @@ void main() {
testWidgets('should create RegistrationCubit successfully', (WidgetTester tester) async {
// Arrange & Act
final cubit = RegistrationCubit(registrationRepository: mockRegistrationRepository);
final cubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs());
// Assert
expect(cubit, isA<RegistrationCubit>());

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/prefs.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
@@ -8,7 +9,7 @@ import 'package:mockito/annotations.dart';
import 'integration_widget_test.mocks.dart';
@GenerateMocks([RegistrationRepository])
@GenerateMocks([RegistrationRepository, Prefs])
void main() {
group('Registration Integration Widget Tests', () {
late MockRegistrationRepository mockRegistrationRepository;
@@ -16,7 +17,7 @@ void main() {
setUp(() {
mockRegistrationRepository = MockRegistrationRepository();
registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository);
registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs());
});
tearDown(() {