diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml
index f1f63162..d41230f4 100644
--- a/.gitea/workflows/test.yml
+++ b/.gitea/workflows/test.yml
@@ -3,7 +3,7 @@ name: Eatvio App Android Test
on: [ pull_request ]
env:
- flutter_version: "3.35.1"
+ flutter_version: "3.38.1"
java_version: "21.x"
flutter_channel: "stable"
my_local_cache_dir: "/opt/hostedtoolcache/"
@@ -71,7 +71,7 @@ jobs:
- name: Install utils for Android SDK
run: |
apt-get update
- apt-get install -y openjdk-11-jdk wget jq clang cmake ninja-build libgtk-3-dev
+ apt-get install -y openjdk-17-jdk wget jq clang cmake ninja-build libgtk-3-dev
- name: Install Android SDK
if: steps.cache-sdk.outputs.cache-hit != 'true'
@@ -140,7 +140,69 @@ jobs:
run: flutter analyze
- name: Test
- run: flutter test
+ run: flutter test --coverage
+
+ - name: Test coverage
+ run: |
+ echo "📊 Analyzing test coverage..."
+ echo ""
+ awk -F: '
+ /^SF:/ {
+ file = $0
+ sub(/^SF:/, "", file)
+ sub(/^[[:space:]]+/, "", file)
+ is_generated = 0
+ if (file ~ /\.(g|freezed|gr|mocks)\.dart$/) {
+ is_generated = 1
+ generated_files++
+ } else if (file ~ /\/(build|\.dart_tool)\//) {
+ is_generated = 1
+ generated_files++
+ } else {
+ source_files++
+ }
+ }
+ /^LF:/ {
+ if (!is_generated) {
+ total += $2
+ } else {
+ generated_lines += $2
+ }
+ }
+ /^LH:/ {
+ if (!is_generated) {
+ covered += $2
+ } else {
+ generated_covered += $2
+ }
+ }
+ END {
+ print "📁 Files:"
+ printf " Source files: %d\n", source_files
+ printf " Generated files: %d (excluded)\n", generated_files
+ print ""
+ print "📝 Lines:"
+ printf " Total lines: %d\n", total
+ printf " Covered lines: %d\n", covered
+ printf " Uncovered lines: %d\n", total - covered
+ print ""
+ if (total == 0) {
+ print "⚠️ Coverage: 0%"
+ exit 1
+ } else {
+ percentage = (covered / total) * 100
+ printf "✅ Coverage: %.2f%%\n", percentage
+ print ""
+ if (percentage < 60) {
+ printf "⚠️ Warning: Coverage is below 60%%\n"
+ } else if (percentage < 80) {
+ printf "💛 Good: Coverage is above 60%%\n"
+ } else {
+ printf "💚 Excellent: Coverage is above 80%%\n"
+ }
+ }
+ }
+ ' coverage/lcov.info
- name: Build apk
run: |
diff --git a/.gitignore b/.gitignore
index afeb23f8..530ea713 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,4 +19,5 @@ ios/XCBuildData/
env/.env
.env
android/key.properties
-app/upload-keystore.jks
\ No newline at end of file
+app/upload-keystore.jks
+coverage/
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 3db711ab..593e663e 100644
--- a/Makefile
+++ b/Makefile
@@ -7,4 +7,7 @@ run-analyse:
run-checks: run-analyse run-tests
build-generated-files:
- dart run build_runner build --delete-conflicting-outputs
\ No newline at end of file
+ dart run build_runner build --delete-conflicting-outputs
+
+clean-coverage:
+ @./scripts/remove_generated_from_coverage.sh
\ No newline at end of file
diff --git a/analyze_output.txt b/analyze_output.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 8fc22d6d..79e6d71d 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -19,15 +19,15 @@ def yandexClientId = project.hasProperty('YANDEX_CLIENT_ID') ? project.property(
android {
namespace = "com.eatvioapp.app"
compileSdk = flutter.compileSdkVersion
- ndkVersion = "27.0.12077973"
+ ndkVersion = flutter.ndkVersion
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_11.toString()
+ jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html
index ce07f9e3..746fadde 100644
--- a/android/build/reports/problems/problems-report.html
+++ b/android/build/reports/problems/problems-report.html
@@ -650,7 +650,7 @@ code + .copy-button {
diff --git a/assets/icons/check.svg b/assets/icons/check.svg
new file mode 100644
index 00000000..d0d70964
--- /dev/null
+++ b/assets/icons/check.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/check_filled.svg b/assets/icons/check_filled.svg
new file mode 100644
index 00000000..2366f097
--- /dev/null
+++ b/assets/icons/check_filled.svg
@@ -0,0 +1,11 @@
+
diff --git a/assets/icons/close_mark.svg b/assets/icons/close_mark.svg
new file mode 100644
index 00000000..dad9d60c
--- /dev/null
+++ b/assets/icons/close_mark.svg
@@ -0,0 +1,12 @@
+
diff --git a/assets/icons/logo_2.svg b/assets/icons/logo_2.svg
new file mode 100644
index 00000000..e37bf02f
--- /dev/null
+++ b/assets/icons/logo_2.svg
@@ -0,0 +1,9 @@
+
diff --git a/assets/icons/sucess.svg b/assets/icons/sucess.svg
new file mode 100644
index 00000000..a4cf6d09
--- /dev/null
+++ b/assets/icons/sucess.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/logo_bg.png b/assets/images/logo_bg.png
new file mode 100644
index 00000000..e9d377c9
Binary files /dev/null and b/assets/images/logo_bg.png differ
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index f33870db..ab1714c7 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 81;
+ CURRENT_PROJECT_VERSION = 85;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -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";
@@ -539,7 +539,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 81;
+ CURRENT_PROJECT_VERSION = 85;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -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";
@@ -578,7 +578,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
- CURRENT_PROJECT_VERSION = 81;
+ CURRENT_PROJECT_VERSION = 85;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -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";
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 7a90873e..cc28e395 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -21,7 +21,7 @@
CFBundleSignature
????
CFBundleVersion
- 81
+ 85
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
diff --git a/lib/common/widgets/markdown_editor.dart b/lib/common/widgets/markdown_editor.dart
new file mode 100644
index 00000000..551f1449
--- /dev/null
+++ b/lib/common/widgets/markdown_editor.dart
@@ -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 createState() => MarkdownEditorState();
+}
+
+class MarkdownEditorState extends State {
+ late final TextEditingController _controller;
+ final FocusNode _focusNode = FocusNode();
+ bool _isPreview = false;
+ bool _enablePanel = false;
+ late final KeyboardVisibilityController _keyboardVisibilityController;
+ late final StreamSubscription _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 = [];
+ 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 = [];
+ 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)]),
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/core/app_config.dart b/lib/core/app_config.dart
index 7dc49aa0..afe814e0 100644
--- a/lib/core/app_config.dart
+++ b/lib/core/app_config.dart
@@ -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(create: (context) => sl()..getChatRoomsWithLoading()),
BlocProvider(create: (context) => sl()),
BlocProvider(lazy: false, create: (context) => sl()),
+ BlocProvider(create: (context) => sl()),
];
/// Делегаты локализации
diff --git a/lib/core/cubit/marketing/marketing_cubit.dart b/lib/core/cubit/marketing/marketing_cubit.dart
new file mode 100644
index 00000000..cf5b0c13
--- /dev/null
+++ b/lib/core/cubit/marketing/marketing_cubit.dart
@@ -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 {
+ final Prefs prefs;
+ MarketingCubit(this.prefs) : super(const MarketingCubitState());
+
+ Future 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;
+ }
+ }
+}
diff --git a/lib/core/cubit/marketing/marketing_cubit_state.dart b/lib/core/cubit/marketing/marketing_cubit_state.dart
new file mode 100644
index 00000000..164a0afe
--- /dev/null
+++ b/lib/core/cubit/marketing/marketing_cubit_state.dart
@@ -0,0 +1,3 @@
+class MarketingCubitState {
+ const MarketingCubitState();
+}
\ No newline at end of file
diff --git a/lib/core/cubit/state_utils.dart b/lib/core/cubit/state_utils.dart
index 9c6dfb78..a5cc6b34 100644
--- a/lib/core/cubit/state_utils.dart
+++ b/lib/core/cubit/state_utils.dart
@@ -20,7 +20,7 @@ class StateUtils {
if (isError(state)) {
// Попытка извлечь сообщение об ошибке из состояния
final stateString = state.toString();
- final errorMatch = RegExp(r'error[:\s]+([^,}]+)').firstMatch(stateString);
+ final errorMatch = RegExp(r'error[:\s]+([^,}]+)', caseSensitive: false).firstMatch(stateString);
return errorMatch?.group(1)?.trim();
}
return null;
diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart
index ca3ece97..e01596f8 100644
--- a/lib/core/di/cubit_registrations.dart
+++ b/lib/core/di/cubit_registrations.dart
@@ -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';
@@ -46,6 +48,7 @@ import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
+import 'package:eatvio_app/features/registration_v2/cubit/registration_cubit.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart';
@@ -74,10 +77,11 @@ class CubitRegistrations {
static void _registerFactoryCubits() {
// Auth related cubits
sl.registerFactory(() => AuthCubit(authRepository: sl()));
- sl.registerFactory(() => RegistrationCubit(registrationRepository: sl()));
+ sl.registerFactory(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl()));
sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl()));
- sl.registerFactory(() => OAuthCubit(authRepository: sl()));
+ sl.registerFactory(() => OAuthCubit(authRepository: sl(), prefs: sl()));
+ sl.registerFactory(() => RegistrationCubitV2(registrationRepository: sl()));
// Feed related cubits
sl.registerFactory(() => FeedCubit(feedRepository: sl()));
sl.registerFactory(() => RecipesCubit(feedRepository: sl(), recipeRepository: sl(), aboutRecipeRepository: sl()));
@@ -130,7 +134,7 @@ class CubitRegistrations {
sl.registerFactory(() => RegisterConsultantCubit(consultantRepository: sl()));
sl.registerFactory(() => ComplainingCubit(repository: sl()));
sl.registerFactory(() => UserCardsCubit(cardsRepository: sl()));
- sl.registerFactory(() => OrderInfoCubit(chatRepository: sl()));
+ sl.registerFactory(() => OrderInfoCubit(chatRepository: sl(), profileRepository: sl()));
sl.registerFactory(() => ConsultationsCubit(consultationsRepository: sl()));
sl.registerFactory(() => ConsultantCommentsCubit(diaryRepository: sl()));
sl.registerFactory(() => CancelPlanCubit(cancelPlanRepository: sl()));
@@ -143,5 +147,7 @@ class CubitRegistrations {
sl.registerFactory(() => ViewsCubit(viewsRepository: sl()));
sl.registerFactory(() => UserDetailsCubit(profileRepository: sl()));
sl.registerFactory(() => MenuCubit(sl()));
+ sl.registerFactory(() => MarketingCubit(sl()));
+ sl.registerFactory(() => PersonalDietSuggestionCubit(dietRepository: sl(), personalDietSuggestionService: sl()));
}
}
diff --git a/lib/core/di/repository_registrations.dart b/lib/core/di/repository_registrations.dart
index 6d467810..fbbb4498 100644
--- a/lib/core/di/repository_registrations.dart
+++ b/lib/core/di/repository_registrations.dart
@@ -23,12 +23,14 @@ 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';
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
+import 'package:eatvio_app/features/registration_v2/repositories/registration_repository_v2.dart';
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
import 'package:get_it/get_it.dart';
@@ -68,6 +70,8 @@ class RepositoryRegistrations {
sl.registerLazySingleton(() => BlockedUsersRepository(blockedUserService: sl()));
sl.registerLazySingleton(() => CommentsRepository(service: sl()));
sl.registerLazySingleton(() => NotificationSettingsRepository(sl()));
+ sl.registerLazySingleton(() => PersonalDietSuggestionRepository(personalDietSuggestionService: sl()));
+ sl.registerLazySingleton(() => RegistrationRepositoryV2(registrationService: sl()));
// Content repositories
sl.registerLazySingleton(() => BlogPostRepository(blogPostService: sl()));
diff --git a/lib/core/di/service_registrations.dart b/lib/core/di/service_registrations.dart
index 454d9064..f70e28ce 100644
--- a/lib/core/di/service_registrations.dart
+++ b/lib/core/di/service_registrations.dart
@@ -25,12 +25,14 @@ 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';
import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart';
+import 'package:eatvio_app/features/registration_v2/services/registration_service_v2.dart';
import 'package:eatvio_app/features/settings/services/settings_service.dart';
import 'package:get_it/get_it.dart';
@@ -76,6 +78,8 @@ class ServiceRegistrations {
sl.registerLazySingleton(() => ConsultantRatingService(sl()));
sl.registerLazySingleton(() => NotificationSettingsService(sl()));
sl.registerLazySingleton(() => ViewsService(sl()));
+ sl.registerLazySingleton(() => PersonalDietSuggestionService(sl()));
+ sl.registerLazySingleton(() => RegistrationServiceV2(sl()));
// Content services
sl.registerLazySingleton(() => CreateRecipeService(sl()));
diff --git a/lib/core/image_compressing/image_compress_utility.dart b/lib/core/image_compressing/image_compress_utility.dart
index e3c5fa65..b9cbeb18 100644
--- a/lib/core/image_compressing/image_compress_utility.dart
+++ b/lib/core/image_compressing/image_compress_utility.dart
@@ -1,4 +1,7 @@
import 'dart:io';
+import 'package:eatvio_app/core/app_initializer.dart';
+import 'package:eatvio_app/core/app_config.dart';
+import 'package:eatvio_app/core/locator_service.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:image/image.dart' as img;
import 'package:path/path.dart' as p;
@@ -27,6 +30,17 @@ abstract class ImageCompressUtility {
}
static Future compressFile(File file) async {
+ // Skip compression in test environment to avoid hangs
+ try {
+ final appConfig = sl();
+ if (appConfig.buildType == AppBuildType.tests) {
+ // In tests, return the original file without compression
+ return file;
+ }
+ } catch (_) {
+ // If AppConfig is not available, proceed with compression
+ }
+
final image = img.decodeImage(file.readAsBytesSync())!;
int width = image.width;
int height = image.height;
diff --git a/lib/core/mixins/registration_date_cache_mixin.dart b/lib/core/mixins/registration_date_cache_mixin.dart
new file mode 100644
index 00000000..6d5fb922
--- /dev/null
+++ b/lib/core/mixins/registration_date_cache_mixin.dart
@@ -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 cacheRegistrationDate(DateTime date) async {
+ await prefs.setInt(PrefsKeys.registrationDate, date.millisecondsSinceEpoch);
+ }
+}
diff --git a/lib/core/prefs_keys.dart b/lib/core/prefs_keys.dart
index 0ba552d5..ea23e1d4 100644
--- a/lib/core/prefs_keys.dart
+++ b/lib/core/prefs_keys.dart
@@ -1 +1 @@
-enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed }
+enum PrefsKeys { hasAnonymousTokenBeenSent, welcomeScreenWasShowed, registrationDate, marketingBottomSheetHasBeenShown }
diff --git a/lib/core/routing/auth_routes.dart b/lib/core/routing/auth_routes.dart
index 39e7b2a9..2ac62618 100644
--- a/lib/core/routing/auth_routes.dart
+++ b/lib/core/routing/auth_routes.dart
@@ -9,6 +9,7 @@ import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.
import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart';
+import 'package:eatvio_app/features/registration_v2/presentation/pages/registration_v2_page.dart';
import 'package:go_router/go_router.dart';
/// Маршруты для аутентификации
@@ -26,6 +27,7 @@ class AuthRoutes {
},
routes: [
GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
+ GoRoute(path: RouteConstants.registrationScreenV2, name: 'registrationScreenV2', builder: (context, state) => const RegistrationV2Page()),
GoRoute(
path: RouteConstants.forgetPasswordScreen,
name: 'forgetPasswordScreen',
diff --git a/lib/core/routing/other_routes.dart b/lib/core/routing/other_routes.dart
index 27c2e0cb..1110f88a 100644
--- a/lib/core/routing/other_routes.dart
+++ b/lib/core/routing/other_routes.dart
@@ -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';
@@ -156,7 +157,7 @@ class OtherRoutes {
},
),
GoRoute(
- path: RouteConstants.subsScreen,
+ path: '${RouteConstants.subsScreen}/:slug',
name: 'subsScreen',
builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType),
),
@@ -194,7 +195,11 @@ class OtherRoutes {
),
// Other routes
- GoRoute(path: RouteConstants.glossaryScreen, name: 'glossaryScreen', builder: (context, state) => GlossaryScreen(passKey: state.extra as String)),
+ GoRoute(
+ path: RouteConstants.glossaryScreen,
+ name: 'glossaryScreen',
+ builder: (context, state) => GlossaryScreen(passKey: state.extra as String),
+ ),
GoRoute(path: RouteConstants.blockedUsersScreen, name: 'blockedUsersScreen', builder: (context, state) => const BlockedUsersScreen()),
GoRoute(
path: RouteConstants.notificationsSettingsScreen,
@@ -215,7 +220,8 @@ class OtherRoutes {
final data = state.extra as Map;
final planId = data['planId'] as int;
final successfulPayment = data['successfulPayment'] as bool?;
- return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment);
+ final customerSlug = data['customerSlug'] as String?;
+ return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment, customerSlug: customerSlug);
},
),
GoRoute(
@@ -238,6 +244,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,),
+ ),
];
}
}
diff --git a/lib/core/routing/route_constants.dart b/lib/core/routing/route_constants.dart
index 1702b425..4ce78f4f 100644
--- a/lib/core/routing/route_constants.dart
+++ b/lib/core/routing/route_constants.dart
@@ -3,6 +3,7 @@ class RouteConstants {
// Auth routes
static const String authScreen = '/auth-screen';
static const String registrationScreen = 'registration-screen';
+ static const String registrationScreenV2 = 'registration-screen-v2';
static const String forgetPasswordScreen = 'forget-password-screen';
static const String scanQRCodeScreen = 'scan-qr-code-screen';
static const String verifyScreen = '/verify-screen';
diff --git a/lib/core/theme/custom_text_style.dart b/lib/core/theme/custom_text_style.dart
index 62544e86..f0d0661b 100644
--- a/lib/core/theme/custom_text_style.dart
+++ b/lib/core/theme/custom_text_style.dart
@@ -10,161 +10,76 @@ class CustomTextStyles {
static TextStyle get headlineSmallMulish => theme.textTheme.headlineSmall!.mulish;
// Title text style
- static TextStyle get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith(
- fontWeight: FontWeight.w600,
- );
+ static TextStyle get titleMediumSegoeUI => theme.textTheme.titleMedium!.segoeUI.copyWith(fontWeight: FontWeight.w600);
- static TextStyle get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith(
- color: appTheme.black900,
- );
+ static TextStyle get bodyMediumBlack900 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black900);
- static TextStyle get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith(
- color: appTheme.black900,
- fontSize: 13,
- );
+ static TextStyle get bodyMediumBlack90013 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black900, fontSize: 13);
- static TextStyle get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith(
- color: appTheme.black,
- );
+ static TextStyle get bodyMediumBluegray800 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
- static TextStyle get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith(
- color: appTheme.black,
- );
+ static TextStyle get bodyMediumBluegray800_1 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
- static TextStyle get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith(
- color: appTheme.black,
- );
+ static TextStyle get bodyMediumBluegray800_2 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.black);
- static TextStyle get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith(
- color: appTheme.whiteA700,
- );
+ static TextStyle get bodyMediumWhiteA700 => theme.textTheme.bodyMedium!.copyWith(color: appTheme.whiteA700);
- static TextStyle get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith(
- color: appTheme.black900,
- fontSize: 12,
- );
+ static TextStyle get bodySmallMulishBlack900 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black900, fontSize: 12);
- static TextStyle get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith(
- color: appTheme.black,
- fontSize: 12,
- );
+ static TextStyle get bodySmallMulishBluegray800 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
- static TextStyle get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith(
- color: appTheme.black,
- fontSize: 12,
- );
+ static TextStyle get bodySmallMulishBluegray80012 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
- static TextStyle get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith(
- color: appTheme.black,
- fontSize: 12,
- );
+ static TextStyle get bodySmallMulishBluegray80012_1 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.black, fontSize: 12);
- static TextStyle get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith(
- color: appTheme.blueGray900,
- fontSize: 12,
- );
+ static TextStyle get bodySmallMulishBluegray900 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.blueGray900, fontSize: 12);
- static TextStyle get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith(
- color: appTheme.gray600,
- );
+ static TextStyle get bodySmallMulishGray600 => theme.textTheme.bodySmall!.mulish.copyWith(color: appTheme.gray600);
- static TextStyle get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith(
- color: theme.colorScheme.primary,
- fontSize: 12,
- );
+ static TextStyle get bodySmallMulishPrimary => theme.textTheme.bodySmall!.mulish.copyWith(color: theme.colorScheme.primary, fontSize: 12);
- static TextStyle get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith(
- color: appTheme.black900,
- fontSize: 12,
- );
+ static TextStyle get bodySmallSegoeUIBlack900 => theme.textTheme.bodySmall!.segoeUI.copyWith(color: appTheme.black900, fontSize: 12);
// Label text style
- static TextStyle get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith(
- color: appTheme.gray60003,
- );
+ static TextStyle get labelLargeGray60003 => theme.textTheme.labelLarge!.copyWith(color: appTheme.gray60003);
- static TextStyle get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(
- color: appTheme.whiteA700,
- );
+ static TextStyle get labelLargeWhiteA700 => theme.textTheme.labelLarge!.copyWith(color: appTheme.whiteA700);
// Title text style
- static TextStyle get titleMedium16 => theme.textTheme.titleMedium!.copyWith(
- fontSize: 16,
- );
+ static TextStyle get titleMedium16 => theme.textTheme.titleMedium!.copyWith(fontSize: 16);
- static TextStyle get titleMediumBlack => theme.textTheme.titleMedium!.copyWith(
- fontSize: 17,
- fontWeight: FontWeight.w900,
- );
+ static TextStyle get titleMediumBlack => theme.textTheme.titleMedium!.copyWith(fontSize: 17, fontWeight: FontWeight.w900);
- static TextStyle get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(
- color: appTheme.black900,
- );
+ static TextStyle get titleMediumBlack900 => theme.textTheme.titleMedium!.copyWith(color: appTheme.black900);
- static TextStyle get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith(
- fontWeight: FontWeight.w600,
- );
+ static TextStyle get titleMediumSemiBold => theme.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600);
- static TextStyle get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(
- color: appTheme.whiteA700,
- fontWeight: FontWeight.w600,
- );
+ static TextStyle get titleMediumWhiteA700 => theme.textTheme.titleMedium!.copyWith(color: appTheme.whiteA700, fontWeight: FontWeight.w600);
- static TextStyle get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith(
- color: appTheme.blue800,
- fontWeight: FontWeight.w800,
- );
+ static TextStyle get titleSmallBlue800 => theme.textTheme.titleSmall!.copyWith(color: appTheme.blue800, fontWeight: FontWeight.w800);
- static TextStyle get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith(
- color: appTheme.black,
- );
+ static TextStyle get titleSmallBluegray800 => theme.textTheme.titleSmall!.copyWith(color: appTheme.black);
- static TextStyle get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith(
- color: appTheme.black,
- fontWeight: FontWeight.w700,
- );
+ static TextStyle get titleSmallBluegray800Bold => theme.textTheme.titleSmall!.copyWith(color: appTheme.black, fontWeight: FontWeight.w700);
- static TextStyle get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith(
- color: appTheme.black,
- fontWeight: FontWeight.w800,
- );
+ static TextStyle get titleSmallBluegray800ExtraBold => theme.textTheme.titleSmall!.copyWith(color: appTheme.black, fontWeight: FontWeight.w800);
- static TextStyle get titleSmallBold => theme.textTheme.titleSmall!.copyWith(
- fontSize: 15,
- fontWeight: FontWeight.w700,
- );
+ static TextStyle get titleSmallBold => theme.textTheme.titleSmall!.copyWith(fontSize: 15, fontWeight: FontWeight.w700);
- static TextStyle get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith(
- color: appTheme.greenA70001,
- );
+ static TextStyle get titleSmallGreenA70001 => theme.textTheme.titleSmall!.copyWith(color: appTheme.greenA70001);
- static TextStyle get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith(
- color: appTheme.greenA70001,
- fontWeight: FontWeight.w800,
- );
+ static TextStyle get titleSmallGreenA70001ExtraBold => theme.textTheme.titleSmall!.copyWith(color: appTheme.greenA70001, fontWeight: FontWeight.w800);
- static TextStyle get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith(
- color: appTheme.lightGreen300,
- fontWeight: FontWeight.w800,
- );
+ static TextStyle get titleSmallLightgreen300 => theme.textTheme.titleSmall!.copyWith(color: appTheme.lightGreen300, fontWeight: FontWeight.w800);
- static TextStyle get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith(
- color: appTheme.red200,
- fontWeight: FontWeight.w800,
- );
+ static TextStyle get titleSmallRed200 => theme.textTheme.titleSmall!.copyWith(color: appTheme.red200, fontWeight: FontWeight.w800);
- static TextStyle get titleSmallSecondaryContainer => theme.textTheme.titleSmall!.copyWith(
- color: theme.colorScheme.secondaryContainer,
- fontWeight: FontWeight.w800,
- );
+ static TextStyle get titleSmallSecondaryContainer =>
+ theme.textTheme.titleSmall!.copyWith(color: theme.colorScheme.secondaryContainer, fontWeight: FontWeight.w800);
- static TextStyle get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith(
- color: theme.colorScheme.secondaryContainer,
- );
+ static TextStyle get titleSmallSecondaryContainer_1 => theme.textTheme.titleSmall!.copyWith(color: theme.colorScheme.secondaryContainer);
- static TextStyle get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith(
- color: theme.colorScheme.onPrimary,
- );
+ static TextStyle get titleSmallSegoeUIOnPrimary => theme.textTheme.titleSmall!.segoeUI.copyWith(color: theme.colorScheme.onPrimary);
}
extension on TextStyle {
@@ -178,75 +93,32 @@ extension on TextStyle {
}
abstract class TextStyles {
- static const headingXLSemibold = TextStyle(
- fontSize: 20,
- fontWeight: FontWeight.w600,
- height: 1.255,
- );
+ static const headingXLSemibold = TextStyle(fontSize: 20, fontWeight: FontWeight.w600, height: 1.255);
- static const headingFS40FW600 = TextStyle(
- fontSize: 40,
- fontWeight: FontWeight.w600,
- height: 1.255,
- );
+ static const headingFS40FW600 = TextStyle(fontSize: 40, fontWeight: FontWeight.w600, height: 1.255);
- static const textLSemibold = TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.w600,
- height: 1.255,
- );
+ static const textLSemibold = TextStyle(fontSize: 18, fontWeight: FontWeight.w600, height: 1.255);
- static const textLMedium = TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.w500,
- height: 1.255,
- );
-
- static const textMSemibold = TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w600,
- height: 1.255,
- );
+ static const textLMedium = TextStyle(fontSize: 18, fontWeight: FontWeight.w500, height: 1.255);
- static const textMMedium = TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w500,
- height: 1,
- );
+ static const textMSemibold = TextStyle(fontSize: 16, fontWeight: FontWeight.w600, height: 1.255);
- static const textMMediumAuto = TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w500,
- height: 1.255,
- );
+ static const textMMedium = TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1);
- static const textSSemibold = TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.w600,
- height: 1.255,
- );
+ static const textMMediumAuto = TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1.255);
- static const textSMedium = TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.w500,
- height: 1.255,
- );
+ static const textSSemibold = TextStyle(fontSize: 14, fontWeight: FontWeight.w600, height: 1.255);
- static const textFS13FW400 = TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.w400,
- height: 1.255,
- );
+ static const textSMedium = TextStyle(fontSize: 14, fontWeight: FontWeight.w500, height: 1.255);
- static const textXSSemibold = TextStyle(
- fontSize: 12,
- fontWeight: FontWeight.w500,
- height: 1.255,
- );
+ static const textFS13FW400 = TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1.255);
+
+ static const textXSSemibold = TextStyle(fontSize: 12, fontWeight: FontWeight.w500, height: 1.255);
+
+ static const textFS10FW500 = TextStyle(fontSize: 10, fontWeight: FontWeight.w500, height: 1.255);
+
+ static const textFS24FW700 = TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.255);
+
+ static const textFS22FW600 = TextStyle(fontSize: 22, fontWeight: FontWeight.w600, height: 1.255);
- static const textFS10FW500 = TextStyle(
- fontSize: 10,
- fontWeight: FontWeight.w500,
- height: 1.255,
- );
}
diff --git a/lib/core/theme/theme_helper.dart b/lib/core/theme/theme_helper.dart
index c202129e..01f9a96b 100644
--- a/lib/core/theme/theme_helper.dart
+++ b/lib/core/theme/theme_helper.dart
@@ -7,23 +7,17 @@ class ThemeHelper {
// The current app theme
final _appTheme = 'primary';
-// A map of custom color themes supported by the app
- final Map _supportedCustomColor = {
- 'primary': PrimaryColors(),
- };
+ // A map of custom color themes supported by the app
+ final Map _supportedCustomColor = {'primary': PrimaryColors()};
-// A map of color schemes supported by the app
- final Map _supportedColorScheme = {
- 'primary': ColorSchemes.primaryColorScheme,
- };
+ // A map of color schemes supported by the app
+ final Map _supportedColorScheme = {'primary': ColorSchemes.primaryColorScheme};
/// Returns the primary colors for the current theme.
PrimaryColors _getThemeColors() {
//throw exception to notify given theme is not found or not generated by the generator
if (!_supportedCustomColor.containsKey(_appTheme)) {
- throw Exception(
- "$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner",
- );
+ throw Exception("$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner");
}
//return theme from map
@@ -34,9 +28,7 @@ class ThemeHelper {
ThemeData _getThemeData() {
//throw exception to notify given theme is not found or not generated by the generator
if (!_supportedColorScheme.containsKey(_appTheme)) {
- throw Exception(
- "$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner",
- );
+ throw Exception("$_appTheme is not found.Make sure you have added this theme class in JSON Try running flutter pub run build_runner");
}
//return theme from map
@@ -60,17 +52,9 @@ class ThemeHelper {
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
backgroundColor: Colors.transparent,
- side: BorderSide(
- color: colorScheme.primary,
- width: 1,
- ),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(20),
- ),
- visualDensity: const VisualDensity(
- vertical: -4,
- horizontal: -4,
- ),
+ side: BorderSide(color: colorScheme.primary, width: 1),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
+ visualDensity: const VisualDensity(vertical: -4, horizontal: -4),
padding: EdgeInsets.zero,
),
),
@@ -101,7 +85,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,12 +125,17 @@ class PrimaryColors {
Color get gray60003 => const Color(0XFF707070);
+ Color get gray60005 => const Color(0xFF6C7C93);
+
// Green
Color get greenA700 => const Color(0XFF019A3E);
Color get greenA70001 => const Color(0XFF00BE13);
Color get green => const Color(0XFF0BB450);
+ Color get darkGreen => const Color(0XFF16A249);
+
+ Color get transparentGreen => const Color.fromARGB(12, 1, 154, 63);
// LightGreen
Color get lightGreen300 => const Color(0XFFB2DF8A);
@@ -180,6 +169,8 @@ class PrimaryColors {
Color get lightGray0 => const Color(0xFFF6F6F6);
+ Color get darkGrey => const Color(0xFF5B656F);
+
Color get lightGray00 => const Color(0xFFE9E9EA);
Color get dottedBorder => const Color(0xFFE6E6E6);
@@ -206,6 +197,8 @@ class PrimaryColors {
Color get cardNotificationsBG => const Color(0xFFA4A4A4);
Color get grey7B => const Color(0xFF7B7B7B);
+
+ Color get greyD9 => const Color(0xFFD9D9D9);
}
PrimaryColors get appTheme => ThemeHelper().themeColor();
diff --git a/lib/core/utils/string_extentions.dart b/lib/core/utils/string_extentions.dart
index 1d366a54..ddfd2abd 100644
--- a/lib/core/utils/string_extentions.dart
+++ b/lib/core/utils/string_extentions.dart
@@ -1,11 +1,12 @@
extension Capitalization on String {
String capitalize() {
+ if (isEmpty) return '';
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
}
String parseGender() {
if (this == 'male') {
- return 'Мужской';
+ return 'Мужской';
} else if (this == 'female') {
return 'Женский';
}
diff --git a/lib/core/widgets/custom_drop_down_button.dart b/lib/core/widgets/custom_drop_down_button.dart
index ac8f3c2b..d4c0e571 100644
--- a/lib/core/widgets/custom_drop_down_button.dart
+++ b/lib/core/widgets/custom_drop_down_button.dart
@@ -3,16 +3,28 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class CustomDropDownButton extends StatelessWidget {
- const CustomDropDownButton({super.key, required this.items, required this.onChanged, this.value, this.hint, this.hasError = false});
+ const CustomDropDownButton({
+ super.key,
+ required this.items,
+ required this.onChanged,
+ this.value,
+ this.hint,
+ this.selectedItemBuilder,
+ this.hasError = false,
+ this.isExpanded = false,
+ });
final T? value;
final List> items;
final void Function(T?)? onChanged;
final Widget? hint;
final bool hasError;
+ final List Function(BuildContext)? selectedItemBuilder;
+ final bool isExpanded;
@override
Widget build(BuildContext context) {
return DropdownButtonFormField(
hint: hint,
+ isExpanded: isExpanded,
icon: SvgPicture.asset('assets/icons/arrow_down2.svg', width: 12, height: 8),
borderRadius: BorderRadius.circular(20),
dropdownColor: appTheme.lightGray0,
@@ -35,6 +47,7 @@ class CustomDropDownButton extends StatelessWidget {
initialValue: value,
items: items,
onChanged: onChanged,
+ selectedItemBuilder: selectedItemBuilder,
);
}
}
diff --git a/lib/core/widgets/custom_outlined_button.dart b/lib/core/widgets/custom_outlined_button.dart
index 2ac583ce..cb396f56 100644
--- a/lib/core/widgets/custom_outlined_button.dart
+++ b/lib/core/widgets/custom_outlined_button.dart
@@ -35,7 +35,7 @@ class CustomOutlinedButton extends BaseButton {
margin: margin,
decoration: decoration,
child: OutlinedButton(
- style: buttonStyle!,
+ style: buttonStyle ?? OutlinedButton.styleFrom(),
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -59,7 +59,7 @@ class CustomOutlinedButton extends BaseButton {
margin: margin,
decoration: decoration,
child: OutlinedButton(
- style: buttonStyle!,
+ style: buttonStyle ?? OutlinedButton.styleFrom(),
onPressed: isDisabled ?? false ? null : onPressed ?? () {},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
diff --git a/lib/core/widgets/marketing_bottom_sheet.dart b/lib/core/widgets/marketing_bottom_sheet.dart
new file mode 100644
index 00000000..53b833ae
--- /dev/null
+++ b/lib/core/widgets/marketing_bottom_sheet.dart
@@ -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),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/core/widgets/option_widget.dart b/lib/core/widgets/option_widget.dart
index 779f6c38..bed09c27 100644
--- a/lib/core/widgets/option_widget.dart
+++ b/lib/core/widgets/option_widget.dart
@@ -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,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/core/widgets/show_error_dialog.dart b/lib/core/widgets/show_error_dialog.dart
index 8ae4666e..c6acef34 100644
--- a/lib/core/widgets/show_error_dialog.dart
+++ b/lib/core/widgets/show_error_dialog.dart
@@ -1,9 +1,7 @@
-import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
-import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart';
Future showErrorDialog(BuildContext context, {String? title, String? description, String? buttonTitle, bool? needToOpenSettings}) async {
@@ -45,7 +43,7 @@ Future showErrorDialog(BuildContext context, {String? title, String? descr
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
- AppRouter.navigatorKey.currentContext?.pop();
+ Navigator.of(context).pop();
if (needToOpenSettings ?? false) {
openAppSettings();
}
diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart
index 52a0e73b..b4f78b4a 100644
--- a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart
+++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart
@@ -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 {
+class OAuthCubit extends Cubit 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 signInViaSocialMedia({required String provider, required String accessToken, required String refreshToken, required String clientId}) async {
try {
@@ -63,6 +67,7 @@ class OAuthCubit extends Cubit {
clientId: clientId,
);
TokenStorage.saveTokenInStorage(token);
+ cacheRegistrationDate(DateTime.now());
emit(OAuthState.loaded(token: token));
} on DioException catch (e) {
emit(OAuthState.error(errorMessage: e.toString()));
diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart
index f7484a87..0220c9fb 100644
--- a/lib/features/auth/presentation/pages/auth_screen.dart
+++ b/lib/features/auth/presentation/pages/auth_screen.dart
@@ -337,7 +337,7 @@ class _AuthScreenState extends State {
children: [
const Text('Нет аккаунта? '),
GestureDetector(
- onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
+ onTap: () => GoRouter.of(context).pushNamed('registrationScreenV2'),
child: Text(
'Зарегистрироваться',
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
@@ -411,7 +411,7 @@ class _AuthScreenState extends State {
_isLoading = false;
});
},
- label: state is AuthStateLoading
+ label: state is AuthStateLoading || _isLoading
? const CupertinoActivityIndicator()
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
),
@@ -426,8 +426,8 @@ class _AuthScreenState extends State {
elevation: 0,
focusElevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
- backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
- onPressed: state is AuthStateLoading
+ backgroundColor: state is AuthStateLoading || _isLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
+ onPressed: state is AuthStateLoading || _isLoading
? null
: () async {
await showDialog(
@@ -487,7 +487,7 @@ class _AuthScreenState extends State {
},
);
},
- label: state is AuthStateLoading
+ label: state is AuthStateLoading || _isLoading
? const CupertinoActivityIndicator()
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
),
diff --git a/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart b/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart
index 02196288..844c8043 100644
--- a/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart
+++ b/lib/features/blog_post/presentation/cubit/blog_post_cubit.dart
@@ -11,7 +11,9 @@ class BlogPostCubit extends Cubit {
Future 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);
diff --git a/lib/features/blog_post/presentation/pages/blog_post_screen.dart b/lib/features/blog_post/presentation/pages/blog_post_screen.dart
index 4b08b333..08312aeb 100644
--- a/lib/features/blog_post/presentation/pages/blog_post_screen.dart
+++ b/lib/features/blog_post/presentation/pages/blog_post_screen.dart
@@ -18,6 +18,8 @@ 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;
+import 'package:url_launcher/url_launcher.dart';
class BlogPostScreen extends StatefulWidget {
final int id;
@@ -231,10 +233,105 @@ class _BlogPostScreenState extends State {
),
if (blogPostState.blogPostModel?.description.isNotEmpty ?? false)
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8),
- child: Html(
- data: blogPostState.blogPostModel?.description.replaceAll('\n', '
'),
- style: {"*": Style(color: Colors.black87)},
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Builder(
+ builder: (context) {
+ // Автоматически преобразуем URL в markdown ссылки
+ String description = blogPostState.blogPostModel!.description;
+
+ // Сначала исправляем неправильный синтаксис markdown ссылок, где URL в тексте, а не в href
+ // Например: [http://example.com](url) -> [http://example.com](http://example.com)
+ final wrongLinkRegex = RegExp(r'\[(https?://[^\]]+|www\.[^\]]+)\]\(([^\)]+)\)');
+ description = description.replaceAllMapped(wrongLinkRegex, (match) {
+ final urlInText = match.group(1)!;
+ // Если текст ссылки является URL, используем его как href
+ if (!urlInText.startsWith('http://') && !urlInText.startsWith('https://') && !urlInText.startsWith('www.')) {
+ return match.group(0)!; // Оставляем как есть, если это не URL
+ }
+ final fullUrl = urlInText.startsWith('www.') ? 'https://$urlInText' : urlInText;
+ // Используем URL как href, а текст ссылки - это сам URL
+ return '[$urlInText]($fullUrl)';
+ });
+
+ // Теперь находим все существующие markdown ссылки и их позиции
+ final linkRegex = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
+ final existingLinks = <_LinkRange>[];
+
+ for (final linkMatch in linkRegex.allMatches(description)) {
+ existingLinks.add(_LinkRange(linkMatch.start, linkMatch.end));
+ }
+
+ // Регулярное выражение для поиска URL (http://, https://, www.)
+ final urlRegex = RegExp(r'(https?://[^\s\)]+|www\.[^\s\)]+)', caseSensitive: false);
+
+ // Заменяем URL на markdown ссылки, если они еще не являются ссылками
+ String result = description;
+ int offset = 0;
+
+ for (final urlMatch in urlRegex.allMatches(description)) {
+ // Проверяем, не находится ли URL внутри существующей markdown ссылки
+ bool isInsideLink = existingLinks.any((link) => urlMatch.start >= link.start && urlMatch.end <= link.end);
+
+ if (!isInsideLink) {
+ final url = urlMatch.group(0)!;
+ final fullUrl = url.startsWith('www.') ? 'https://$url' : url;
+ final replacement = '[$url]($fullUrl)';
+ result = result.replaceRange(urlMatch.start + offset, urlMatch.end + offset, replacement);
+ offset += replacement.length - url.length;
+ }
+ }
+
+ description = result;
+
+ final htmlData = md.markdownToHtml(description.replaceAll('\n', '
'), extensionSet: md.ExtensionSet.commonMark);
+ debugPrint('Markdown input: ${blogPostState.blogPostModel!.description}');
+ debugPrint('Processed markdown: $description');
+ debugPrint('HTML output: $htmlData');
+ return Html(
+ data: htmlData,
+ 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)),
+ },
+ onLinkTap: (url, attributes, element) async {
+ debugPrint('Link tapped: $url');
+ if (url != null) {
+ try {
+ final uri = Uri.parse(url);
+ if (await canLaunchUrl(uri)) {
+ await launchUrl(uri, mode: LaunchMode.externalApplication);
+ } else {
+ debugPrint('Cannot launch URL: $url');
+ }
+ } catch (e) {
+ debugPrint('Error launching URL: $e');
+ }
+ }
+ },
+ );
+ },
),
),
const SizedBox(height: 8),
@@ -337,3 +434,10 @@ class _BlogPostScreenState extends State {
);
}
}
+
+class _LinkRange {
+ final int start;
+ final int end;
+
+ _LinkRange(this.start, this.end);
+}
diff --git a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart
index 6e93944a..2617b664 100644
--- a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart
+++ b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart
@@ -16,7 +16,7 @@ class BlogPostCreateCubit extends BaseCubit {
return true;
} catch (error, stackTrace) {
handleError(error, stackTrace);
- emit(const BlogPostCreateState.loaded());
+ emit(BlogPostCreateState.error(errorMessage: error.toString()));
return false;
}
}
@@ -29,7 +29,7 @@ class BlogPostCreateCubit extends BaseCubit {
return true;
} catch (error, stackTrace) {
handleError(error, stackTrace);
- emit(const BlogPostCreateState.loaded());
+ emit(BlogPostCreateState.error(errorMessage: error.toString()));
return false;
}
}
diff --git a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart
index da1dfa24..0254d3c7 100644
--- a/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart
+++ b/lib/features/blog_post_create/presentation/pages/blog_post_create_screen.dart
@@ -5,6 +5,7 @@ import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
import 'package:eatvio_app/core/theme/app_decoration.dart';
+import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart';
@@ -25,10 +26,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 createState() => _BlogPostCreateScreenState();
@@ -43,6 +41,9 @@ class _BlogPostCreateScreenState extends State {
final descriptionController = TextEditingController();
final videoUrlController = TextEditingController();
bool isLoading = false;
+ bool enableMdPanel = false;
+ bool isPreviewMode = false;
+ final FocusNode descriptionFocusNode = FocusNode();
@override
void initState() {
@@ -51,6 +52,7 @@ class _BlogPostCreateScreenState extends State {
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
setState(() {
shrinkImage = visible;
+ enableMdPanel = visible;
});
});
if (widget.feedBlogPostModel != null) {
@@ -63,9 +65,326 @@ class _BlogPostCreateScreenState extends State {
@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 = [];
+
+ 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 = [];
+ 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(
@@ -103,13 +422,7 @@ class _BlogPostCreateScreenState extends State {
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 +440,102 @@ class _BlogPostCreateScreenState extends State {
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 +544,19 @@ class _BlogPostCreateScreenState extends State {
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 +565,79 @@ class _BlogPostCreateScreenState extends State {
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;
+ if (context.canPop()) {
+ 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;
+ if (context.canPop()) {
+ context.pop();
+ }
+ }
+ }
+ },
+ borderRadius: BorderRadius.circular(16),
+ child: isLoading
+ ? const Center(child: Loader())
+ : Ink(
+ width: MediaQuery.of(context).size.width - 32,
+ height: 52,
+ decoration: BoxDecoration(borderRadius: BorderRadius.circular(16), color: appTheme.blueA400),
+ child: const Center(
+ child: Text('Создать', style: TextStyle(color: Colors.white)),
+ ),
+ ),
+ );
+ }
+
GestureDetector _buildAddPhoto() {
return GestureDetector(
onTap: () {
@@ -270,22 +650,14 @@ class _BlogPostCreateScreenState extends State {
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 +666,7 @@ class _BlogPostCreateScreenState extends State {
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 +674,7 @@ class _BlogPostCreateScreenState extends State {
width: shrinkImage ? 52 : MediaQuery.of(context).size.width - 32,
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
- child: Image(
- image: NetworkImage('${sl().baseUrl}/storage/${widget.feedBlogPostModel!.image}'),
- ),
+ child: Image(image: NetworkImage('${sl().baseUrl}/storage/${widget.feedBlogPostModel!.image}')),
),
),
],
diff --git a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart
index 935eecf7..a0910fcb 100644
--- a/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart
+++ b/lib/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart
@@ -36,20 +36,24 @@ class ProfileAvatarCubit extends Cubit {
Future getMyProfile() async {
emit(const ProfileAvatarState.loading());
- final profile = await profileRepository.getMyProfile();
- final profileDetails = await profileRepository.getMyProfileDetails();
try {
+ final profile = await profileRepository.getMyProfile();
+ final profileDetails = await profileRepository.getMyProfileDetails();
emit(ProfileAvatarState.loaded(profile: profile));
- final notificationStatus = await Permission.notification.status;
- final isNotificationEnabled = notificationStatus.isGranted;
- AppMetricaUserProfile userProfile = AppMetricaUserProfile([
- AppMetricaNameAttribute.withValue(profile.name),
- AppMetricaGenderAttribute.withValue(profileDetails.gender == 'male' ? AppMetricaGender.male : AppMetricaGender.female),
- AppMetricaBirthDateAttribute.withAge(_calculateAge(profileDetails.birthdate)),
- AppMetricaNotificationEnabledAttribute.withValue(isNotificationEnabled),
- ]);
- AppMetrica.setUserProfileID(profile.slug);
- AppMetrica.reportUserProfile(userProfile);
+ try {
+ final notificationStatus = await Permission.notification.status;
+ final isNotificationEnabled = notificationStatus.isGranted;
+ AppMetricaUserProfile userProfile = AppMetricaUserProfile([
+ AppMetricaNameAttribute.withValue(profile.name),
+ AppMetricaGenderAttribute.withValue(profileDetails.gender == 'male' ? AppMetricaGender.male : AppMetricaGender.female),
+ AppMetricaBirthDateAttribute.withAge(_calculateAge(profileDetails.birthdate)),
+ AppMetricaNotificationEnabledAttribute.withValue(isNotificationEnabled),
+ ]);
+ AppMetrica.setUserProfileID(profile.slug);
+ AppMetrica.reportUserProfile(userProfile);
+ } catch (e) {
+ // Игнорируем ошибки плагинов, профиль уже загружен
+ }
} catch (e) {
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
emit(ProfileAvatarState.error(errorMessage: e.toString()));
diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart
index c22ddbd3..b42bf76e 100644
--- a/lib/features/chat/presentation/pages/chat_screen.dart
+++ b/lib/features/chat/presentation/pages/chat_screen.dart
@@ -114,13 +114,21 @@ class _ChatScreenState extends State {
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 {
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(
@@ -149,10 +151,7 @@ class _ChatScreenState extends State {
centerTitle: false,
title: GestureDetector(
onTap: () {
- GoRouter.of(context).pushNamed(
- 'profileScreen',
- extra: state.chatRoom.users.receiver.slug,
- );
+ GoRouter.of(context).pushNamed('profileScreen', extra: state.chatRoom.users.receiver.slug);
},
child: Row(
children: [
@@ -320,6 +319,7 @@ class _ChatScreenState extends State {
chosenPlanId: chosenPlan?.id,
planName: chosenPlan?.plan.name,
planPrice: chosenOption?.price.toDouble(),
+ hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,
),
),
],
@@ -447,7 +447,7 @@ class _ChatScreenState extends State {
final blockedUsersCubit = sl();
await blockedUsersCubit.getBlockedUsers();
final blockedState = blockedUsersCubit.state;
-
+
if (blockedState is BlockedUsersStateLoaded) {
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug);
if (isBlocked) {
@@ -462,7 +462,7 @@ class _ChatScreenState extends State {
return;
}
}
-
+
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
chatRoomCubit.sendMessage(
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),
@@ -497,6 +497,12 @@ class _ChatScreenState extends State {
left: 16,
child: AboutOrderButton(
onTap: () async {
+ if (amIConsultant) {
+ GoRouter.of(
+ context,
+ ).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id, 'customerSlug': state.chatRoom.users.receiver.slug});
+ return;
+ }
GoRouter.of(context).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id});
},
),
diff --git a/lib/features/chat/presentation/widgets/chose_plan_date_widget.dart b/lib/features/chat/presentation/widgets/chose_plan_date_widget.dart
index 5ec4e370..2744ba74 100644
--- a/lib/features/chat/presentation/widgets/chose_plan_date_widget.dart
+++ b/lib/features/chat/presentation/widgets/chose_plan_date_widget.dart
@@ -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 createState() => _ChosePlanDateWidget();
}
class _ChosePlanDateWidget extends State {
- final startDate = [DateTime.now()];
+ late final List 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 {
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),
+ ),
),
),
- ),
- ],
+ ],
+ ),
);
}
}
diff --git a/lib/features/chat/presentation/widgets/notification_banner.dart b/lib/features/chat/presentation/widgets/notification_banner.dart
index 8828863f..3a8203f9 100644
--- a/lib/features/chat/presentation/widgets/notification_banner.dart
+++ b/lib/features/chat/presentation/widgets/notification_banner.dart
@@ -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 createState() => _NotificationBannerState();
}
@@ -68,7 +69,8 @@ class _NotificationBannerState extends State {
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 {
context: context,
builder: (context) => ChosePlanDateWidget(
chatCubit: cubit,
+ hasPersonalDiet: widget.hasPersonalDiet,
),
);
},
@@ -155,6 +158,20 @@ class _NotificationBannerState extends State {
),
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 }
diff --git a/lib/features/consultant/become_consultant/models/consultant_cert_photo_model.dart b/lib/features/consultant/become_consultant/models/consultant_cert_photo_model.dart
new file mode 100644
index 00000000..6fdcc1e8
--- /dev/null
+++ b/lib/features/consultant/become_consultant/models/consultant_cert_photo_model.dart
@@ -0,0 +1,27 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'consultant_cert_photo_model.freezed.dart';
+part 'consultant_cert_photo_model.g.dart';
+
+@freezed
+abstract class ConsultantCertPhotoModel with _$ConsultantCertPhotoModel {
+ const factory ConsultantCertPhotoModel({
+ required int id,
+ @JsonKey(name: 'user_id') int? userId,
+ required String group,
+ @JsonKey(name: 'original_name') required String originalName,
+ required String path,
+ required int size,
+ Map? specs,
+ required String url,
+ }) = _ConsultantCertPhotoModel;
+
+ const ConsultantCertPhotoModel._();
+
+ factory ConsultantCertPhotoModel.fromJson(Map json) => _$ConsultantCertPhotoModelFromJson(json);
+
+ static const String visibleGroup = 'user_certs';
+ static const String hiddenGroup = 'hidden_user_certs';
+
+ bool get isHidden => group == hiddenGroup;
+}
diff --git a/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart b/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart
index 84961990..3c6f487c 100644
--- a/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart
+++ b/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart
@@ -1,59 +1,33 @@
import 'dart:io';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
+import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:http/http.dart' as http;
-import 'package:path_provider/path_provider.dart';
class RegisterConsultantCubit extends Cubit {
final ConsultantRepository consultantRepository;
bool _update = false;
- RegisterConsultantCubit({
- required this.consultantRepository,
- }) : super(
- const RegisterConsultantState(
- consultant: ConsultantProfileModel(
- socialLinks: SocialLinksModel(),
- hasMedicalEducation: true,
- ),
- loading: true,
- acceptOffer: false,
- ),
- );
-
- Future downloadImageAsFile(String imageUrl) async {
- final response = await http.get(Uri.parse(imageUrl));
-
- if (response.statusCode == 200) {
- final tempDir = await getTemporaryDirectory();
- final filePath = '${tempDir.path}/${imageUrl.split('/').last}';
- final file = File(filePath);
- return await file.writeAsBytes(response.bodyBytes);
- } else {
- throw Exception('Не удалось загрузить изображение');
- }
- }
+ RegisterConsultantCubit({required this.consultantRepository})
+ : super(
+ const RegisterConsultantState(
+ consultant: ConsultantProfileModel(socialLinks: SocialLinksModel(), hasMedicalEducation: true),
+ loading: true,
+ acceptOffer: false,
+ ),
+ );
Future getMyRequestStatus(String slug) async {
_update = true;
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
-
- final List photos = await Future.wait(
- myRequestStatus.photos.map(
- (e) async => (await downloadImageAsFile(e.url)).path,
- ),
- );
+ final certPhotos = await _fetchVisibleCertPhotos();
List? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
final course = courseMap.values.first;
- return CertificatesModel(
- title: course.title,
- date: course.date,
- );
+ return CertificatesModel(title: course.title, date: course.date);
}).toList();
String? sanitizeLink(String? link) {
@@ -63,7 +37,6 @@ class RegisterConsultantCubit extends Cubit {
emit(
RegisterConsultantState(
consultant: ConsultantProfileModel(
- photos: photos,
certificates: certificates,
socialLinks: SocialLinksModel(
vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink),
@@ -77,38 +50,44 @@ class RegisterConsultantCubit extends Cubit {
acceptOffer: false,
loading: false,
requestModel: myRequestStatus,
+ certPhotos: certPhotos,
),
);
}
- void disableLoading() {
- emit(state.copyWith(loading: false));
+ Future disableLoading() async {
+ final certPhotos = await _fetchVisibleCertPhotos();
+ emit(state.copyWith(loading: false, certPhotos: certPhotos));
}
void updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) {
- emit(
- state.copyWith(
- consultant: consultant,
- acceptOffer: acceptOffer,
- loading: false,
- ),
- );
+ emit(state.copyWith(consultant: consultant, acceptOffer: acceptOffer, loading: false));
}
Future registerConsultant() async {
- if (state.consultant.photos != null) {
- updateConsultantProfileModel(
- state.consultant.copyWith(
- photos: await uploadPhotos(
- state.consultant.photos!.map((e) => File(e)).toList(),
- ),
- ),
- );
- }
await consultantRepository.registerConsultant(consultant: state.consultant, update: _update);
}
- Future> uploadPhotos(List files) async {
- return await consultantRepository.uploadPhotos(files: await ImageCompressUtility.compressFilesList(files));
+ Future uploadCertPhotos(List files) async {
+ if (files.isEmpty) {
+ return;
+ }
+ final compressedFiles = await ImageCompressUtility.compressFilesList(files);
+ final uploaded = await consultantRepository.storeConsultantCertPhotos(files: compressedFiles);
+ emit(state.copyWith(certPhotos: _onlyVisible(uploaded)));
+ }
+
+ Future removeCertPhoto(int id) async {
+ await consultantRepository.hideConsultantCertPhoto(id);
+ emit(state.copyWith(certPhotos: state.certPhotos.where((photo) => photo.id != id).toList()));
+ }
+
+ Future> _fetchVisibleCertPhotos() async {
+ final photos = await consultantRepository.getConsultantCertPhotos();
+ return _onlyVisible(photos);
+ }
+
+ List _onlyVisible(List photos) {
+ return photos.where((photo) => !photo.isHidden).toList();
}
}
diff --git a/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart b/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart
index 5f613821..ee6644a9 100644
--- a/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart
+++ b/lib/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart
@@ -1,3 +1,4 @@
+import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -11,5 +12,6 @@ abstract class RegisterConsultantState with _$RegisterConsultantState {
required bool acceptOffer,
required bool loading,
ConsultationRequestModel? requestModel,
+ @Default([]) List certPhotos,
}) = _RegisterConsultantState;
}
diff --git a/lib/features/consultant/become_consultant/presentation/pages/photo_or_scans_screen.dart b/lib/features/consultant/become_consultant/presentation/pages/photo_or_scans_screen.dart
index 234e3d74..4ad6db76 100644
--- a/lib/features/consultant/become_consultant/presentation/pages/photo_or_scans_screen.dart
+++ b/lib/features/consultant/become_consultant/presentation/pages/photo_or_scans_screen.dart
@@ -1,9 +1,11 @@
import 'dart:io';
import 'package:dismissible_page/dismissible_page.dart';
-import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
-import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
+import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart';
+import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
+import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
+import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart';
@@ -16,14 +18,7 @@ class PhotoOrScansScreen extends StatefulWidget {
}
class _PhotoOrScansScreenState extends State {
- List imagesPaths = [];
-
- @override
- void initState() {
- super.initState();
- imagesPaths =
- context.read().state.consultant.photos != null ? [...context.read().state.consultant.photos!] : [];
- }
+ bool isUploading = false;
@override
Widget build(BuildContext context) {
@@ -34,125 +29,121 @@ class _PhotoOrScansScreenState extends State {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- const Text(
- 'Загрузите фото или сканы дипломов и сертификатов',
- style: TextStyle(
- fontSize: 24,
- fontWeight: FontWeight.w700,
- height: 1.1,
- ),
- ),
+ const Text('Загрузите фото или сканы дипломов и сертификатов', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)),
const SizedBox(height: 16),
- GridView.builder(
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: 3,
- childAspectRatio: 3 / 4,
- crossAxisSpacing: 8,
- mainAxisSpacing: 8,
- ),
- itemBuilder: (BuildContext context, int index) {
- if (index == imagesPaths.length) {
- return InkWell(
- borderRadius: BorderRadius.circular(20),
- onTap: () async {
- final picker = ImagePicker();
- List? newImages = await picker.pickMultiImage();
- List newImagesPaths = newImages.map((e) => e.path).toList();
- setState(() {
- imagesPaths = [...imagesPaths, ...newImagesPaths];
- });
- _saveState();
- },
- child: Ink(
- decoration: BoxDecoration(
- color: const Color(0xFFF6F6F6),
- borderRadius: BorderRadius.circular(20),
+ BlocBuilder(
+ builder: (context, state) {
+ final photos = state.certPhotos;
+ return Column(
+ children: [
+ if (isUploading) ...[const LinearProgressIndicator(), const SizedBox(height: 16)],
+ GridView.builder(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 3,
+ childAspectRatio: 3 / 4,
+ crossAxisSpacing: 8,
+ mainAxisSpacing: 8,
),
- child: Icon(
- Icons.add,
- color: appTheme.blueA400,
- ),
- ),
- );
- } else {
- return Hero(
- tag: imagesPaths[index],
- child: Material(
- borderRadius: BorderRadius.circular(20),
- child: InkWell(
- onTap: () {
- context.pushTransparentRoute(
- rootNavigator: true,
- PhotoViewScreen(
- imageUrls: imagesPaths,
- isLocal: true,
- index: index,
+ itemBuilder: (BuildContext context, int index) {
+ if (index == photos.length) {
+ return InkWell(
+ borderRadius: BorderRadius.circular(20),
+ onTap: () => _pickAndUploadPhotos(),
+ child: Ink(
+ decoration: BoxDecoration(color: const Color(0xFFF6F6F6), borderRadius: BorderRadius.circular(20)),
+ child: Icon(Icons.add, color: appTheme.blueA400),
),
);
- },
- borderRadius: BorderRadius.circular(20),
- child: Stack(
- clipBehavior: Clip.none,
- children: [
- Ink(
- decoration: BoxDecoration(
- borderRadius: const BorderRadius.all(
- Radius.circular(20),
- ),
- image: DecorationImage(
- fit: BoxFit.fitHeight,
- image: FileImage(
- File(imagesPaths[index]),
- ),
- ),
- ),
- ),
- Positioned(
- right: -12,
- top: -12,
- child: InkWell(
- onTap: () {
- setState(() {
- imagesPaths.removeAt(index);
- });
- _saveState();
- },
- customBorder: const CircleBorder(),
- child: Ink(
- padding: const EdgeInsets.all(4),
- decoration: const BoxDecoration(
- color: Color(0xFFF1F2F6),
- shape: BoxShape.circle,
- ),
- child: const Icon(
- size: 20,
- Icons.close,
- color: Colors.black,
- ),
- ),
- ),
- ),
- ],
- ),
- ),
+ } else {
+ return _CertPhotoCard(photo: photos[index], allPhotos: photos, onRemove: () => _removePhoto(photos[index].id));
+ }
+ },
+ itemCount: photos.length + 1,
),
- );
- }
+ ],
+ );
},
- itemCount: imagesPaths.length + 1,
),
],
),
);
}
- void _saveState() {
- context.read().updateConsultantProfileModel(
- context.read().state.consultant.copyWith(
- photos: imagesPaths,
- ),
- );
+ Future _pickAndUploadPhotos() async {
+ final picker = ImagePicker();
+ final newImages = await picker.pickMultiImage();
+ if (newImages.isEmpty) {
+ return;
+ }
+ setState(() {
+ isUploading = true;
+ });
+ try {
+ // ignore: use_build_context_synchronously
+ await context.read().uploadCertPhotos(newImages.map((file) => File(file.path)).toList());
+ } finally {
+ if (mounted) {
+ setState(() {
+ isUploading = false;
+ });
+ }
+ }
+ }
+
+ Future _removePhoto(int id) async {
+ await context.read().removeCertPhoto(id);
+ }
+}
+
+class _CertPhotoCard extends StatelessWidget {
+ final ConsultantCertPhotoModel photo;
+ final List allPhotos;
+ final VoidCallback onRemove;
+
+ const _CertPhotoCard({required this.photo, required this.allPhotos, required this.onRemove});
+
+ @override
+ Widget build(BuildContext context) {
+ return Hero(
+ tag: photo.id,
+ child: Material(
+ borderRadius: BorderRadius.circular(20),
+ child: InkWell(
+ onTap: () {
+ context.pushTransparentRoute(
+ rootNavigator: true,
+ PhotoViewScreen(imageUrls: allPhotos.map((e) => e.url).toList(), isLocal: false, index: allPhotos.indexOf(photo)),
+ );
+ },
+ borderRadius: BorderRadius.circular(20),
+ child: Stack(
+ clipBehavior: Clip.none,
+ children: [
+ Ink(
+ decoration: BoxDecoration(
+ borderRadius: const BorderRadius.all(Radius.circular(20)),
+ image: DecorationImage(fit: BoxFit.cover, image: NetworkImage(photo.url)),
+ ),
+ ),
+ Positioned(
+ right: -12,
+ top: -12,
+ child: InkWell(
+ onTap: onRemove,
+ customBorder: const CircleBorder(),
+ child: Ink(
+ padding: const EdgeInsets.all(4),
+ decoration: const BoxDecoration(color: Color(0xFFF1F2F6), shape: BoxShape.circle),
+ child: const Icon(size: 20, Icons.close, color: Colors.black),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
}
}
diff --git a/lib/features/consultant/become_consultant/repositories/consultant_repository.dart b/lib/features/consultant/become_consultant/repositories/consultant_repository.dart
index 89eef1c8..05f357c3 100644
--- a/lib/features/consultant/become_consultant/repositories/consultant_repository.dart
+++ b/lib/features/consultant/become_consultant/repositories/consultant_repository.dart
@@ -1,62 +1,28 @@
import 'dart:io';
+import 'package:dio/dio.dart';
+import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
import 'package:http_parser/http_parser.dart';
-import 'package:dio/dio.dart';
class ConsultantRepository {
final ConsultantService consultantService;
- const ConsultantRepository({
- required this.consultantService,
- });
+ const ConsultantRepository({required this.consultantService});
- Future registerConsultant({
- required ConsultantProfileModel consultant,
- bool update = false,
- }) async {
+ Future registerConsultant({required ConsultantProfileModel consultant, bool update = false}) async {
try {
if (update) {
- return await consultantService.consultantUpdate(
- consultantModel: consultant,
- );
+ return await consultantService.consultantUpdate(consultantModel: consultant);
} else {
- return await consultantService.consultantRegistration(
- consultantModel: consultant,
- );
+ return await consultantService.consultantRegistration(consultantModel: consultant);
}
} catch (e) {
rethrow;
}
}
- Future> uploadPhotos({
- required List files,
- }) async {
- try {
- final multipartFiles = [];
-
- for (final file in files) {
- final fileBytes = await file.readAsBytes();
- final multipartFile = MultipartFile.fromBytes(
- fileBytes,
- filename: file.path
- .split('/')
- .last,
- contentType: MediaType('application', 'octet-stream'),
- );
- multipartFiles.add(multipartFile);
- }
-
- return await consultantService.uploadPhotos(
- photos: multipartFiles.isEmpty ? null : multipartFiles,
- );
- } catch (e) {
- rethrow;
- }
- }
-
Future getConsultantRequestStatus(String slug) async {
try {
return await consultantService.getConsultantRequestStatus(slug: slug);
@@ -72,4 +38,48 @@ class ConsultantRepository {
rethrow;
}
}
+
+ Future> getConsultantCertPhotos() async {
+ try {
+ return await consultantService.getConsultantCertPhotos();
+ } catch (e) {
+ rethrow;
+ }
+ }
+
+ Future> storeConsultantCertPhotos({required List files}) async {
+ try {
+ final multipartFiles = [];
+
+ for (final file in files) {
+ final fileBytes = await file.readAsBytes();
+ final multipartFile = MultipartFile.fromBytes(fileBytes, filename: file.path.split('/').last, contentType: MediaType('application', 'octet-stream'));
+ multipartFiles.add(multipartFile);
+ }
+
+ if (multipartFiles.isEmpty) {
+ return [];
+ }
+
+ return await consultantService.storeConsultantCertPhotos(photos: multipartFiles);
+ } catch (e) {
+ rethrow;
+ }
+ }
+
+ Future hideConsultantCertPhoto(int id) async {
+ try {
+ await consultantService.hideConsultantCertPhoto(payload: {'cert_photo_id': id});
+ } catch (e) {
+ rethrow;
+ }
+ }
+
+ Future unHideConsultantCertPhoto(int id) async {
+ try {
+ await consultantService.unHideConsultantCertPhoto(payload: {'cert_photo_id': id});
+ } catch (e) {
+ rethrow;
+ }
+ }
}
diff --git a/lib/features/consultant/become_consultant/services/consultant_service.dart b/lib/features/consultant/become_consultant/services/consultant_service.dart
index bfde5a92..d7171b8e 100644
--- a/lib/features/consultant/become_consultant/services/consultant_service.dart
+++ b/lib/features/consultant/become_consultant/services/consultant_service.dart
@@ -1,4 +1,5 @@
import 'package:dio/dio.dart';
+import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:retrofit/retrofit.dart';
@@ -10,28 +11,33 @@ abstract class ConsultantService {
factory ConsultantService(Dio dio, {String baseUrl}) = _ConsultantService;
@POST('/api/consultant_request/registration')
- Future consultantRegistration({
- @Body() required ConsultantProfileModel consultantModel,
- });
+ Future consultantRegistration({@Body() required ConsultantProfileModel consultantModel});
@POST('/api/consultant_request/photos/upload')
@MultiPart()
- Future> uploadPhotos({
- @Part(name: 'photos[]', contentType: 'multipart/form-data') List? photos,
- });
+ Future> uploadPhotos({@Part(name: 'photos[]', contentType: 'multipart/form-data') List? photos});
@GET('/api/profile/consultant/{slug}/status')
- Future getConsultantRequestStatus({
- @Path() required String slug,
- });
+ Future getConsultantRequestStatus({@Path() required String slug});
@GET('/api/consultant_request/{slug}/status')
- Future getMyRequestStatus({
- @Path() required String slug,
- });
+ Future getMyRequestStatus({@Path() required String slug});
@PUT('/api/consultant_request/update')
- Future consultantUpdate({
- @Body() required ConsultantProfileModel consultantModel,
+ Future consultantUpdate({@Body() required ConsultantProfileModel consultantModel});
+
+ @GET('/api/v2/consultant_request/cert_photos')
+ Future> getConsultantCertPhotos();
+
+ @POST('/api/v2/consultant_request/cert_photos/store')
+ @MultiPart()
+ Future> storeConsultantCertPhotos({
+ @Part(name: 'photos[]', contentType: 'multipart/form-data') required List photos,
});
+
+ @POST('/api/v2/consultant_request/cert_photos/hide')
+ Future hideConsultantCertPhoto({@Body() required Map payload});
+
+ @POST('/api/v2/consultant_request/cert_photos/un_hide')
+ Future unHideConsultantCertPhoto({@Body() required Map payload});
}
diff --git a/lib/features/consultations/models/consultation_model.dart b/lib/features/consultations/models/consultation_model.dart
index 59c48f09..c73a8d37 100644
--- a/lib/features/consultations/models/consultation_model.dart
+++ b/lib/features/consultations/models/consultation_model.dart
@@ -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 diaries,
+ @JsonKey(name: 'personal_diet_id') required int? personalDietId,
}) = _ConsultationModel;
factory ConsultationModel.fromJson(Map json) => _$ConsultationModelFromJson(json);
diff --git a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart
index f9fa4143..b1529280 100644
--- a/lib/features/consultations/presentation/widgets/consultation_card_widget.dart
+++ b/lib/features/consultations/presentation/widgets/consultation_card_widget.dart
@@ -63,25 +63,28 @@ class _ConsultationCardWidgetState extends State {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Row(
- children: [
- CircleAvatar(radius: 30, backgroundImage: NetworkImage(widget.consultationsModel.userProfilePhotoUrl)),
- const SizedBox(width: 12),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- widget.consultationsModel.userName,
- style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600),
- ),
- const SizedBox(height: 4),
- Text(
- widget.consultationsModel.planTitle,
- style: const TextStyle(height: 1, color: Color(0xFF88949F), fontSize: 14, fontWeight: FontWeight.w500),
- ),
- ],
- ),
- ],
+ GestureDetector(
+ onTap: () => GoRouter.of(context).pushNamed('profileScreen', extra: widget.consultationsModel.userSlug),
+ child: Row(
+ children: [
+ CircleAvatar(radius: 30, backgroundImage: NetworkImage(widget.consultationsModel.userProfilePhotoUrl)),
+ const SizedBox(width: 12),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ widget.consultationsModel.userName,
+ style: const TextStyle(height: 1, color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ widget.consultationsModel.planTitle,
+ style: const TextStyle(height: 1, color: Color(0xFF88949F), fontSize: 14, fontWeight: FontWeight.w500),
+ ),
+ ],
+ ),
+ ],
+ ),
),
const SizedBox(height: 12),
GestureDetector(
diff --git a/lib/features/create_diet/presentation/create_diet_screen.dart b/lib/features/create_diet/presentation/create_diet_screen.dart
index f0f729d4..95775e86 100644
--- a/lib/features/create_diet/presentation/create_diet_screen.dart
+++ b/lib/features/create_diet/presentation/create_diet_screen.dart
@@ -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 {
final nameController = TextEditingController();
final descriptionController = TextEditingController();
final scaffoldKey = GlobalKey();
+ final GlobalKey _mdKey = GlobalKey();
+ bool _mdToolbarVisible = false;
@override
void initState() {
@@ -76,9 +79,7 @@ class _CreateDietScreenContentState extends State {
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 {
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 {
context.read().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().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().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 {
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 {
onPressed: () async {
context.read().setDietState(DietState.draft);
if (widget.diet != null) {
- final result = await context.read().updateDiet(
- widget.diet!.id,
- );
+ final result = await context.read().updateDiet(widget.diet!.id);
if (result == 'error') {
if (!context.mounted) return;
showErrorDialog(context, title: '${s.could_not_update_diet} ${state.dietData.name}');
diff --git a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart
index 757101ae..7d50d11c 100644
--- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart
+++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart
@@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/app_config.dart';
+import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
import 'package:eatvio_app/core/locator_service.dart';
@@ -47,6 +48,7 @@ class CreateDietCubit extends Cubit {
type['veg'] = diet.type.veg ?? false;
type['carb'] = diet.type.carb ?? false;
type['light'] = diet.type.light ?? false;
+ final appConfig = sl();
emit(
state.copyWith.dietData(
dailySchedule: dailySchedule,
@@ -55,7 +57,8 @@ class CreateDietCubit extends Cubit {
name: diet.name,
duration: diet.duration,
type: type,
- imageUrl: '${sl().baseUrl}/storage/${diet.image}',
+ // Avoid network image URLs during tests to prevent HTTP fetch attempts
+ imageUrl: appConfig.buildType == AppBuildType.tests ? null : '${appConfig.baseUrl}/storage/${diet.image}',
),
);
emit(state.copyWith(dietState: diet.state, durationIndex: durationValues.indexOf(diet.duration)));
@@ -76,11 +79,7 @@ class CreateDietCubit extends Cubit {
final dailySchedule = state.dietData.dailySchedule;
if (currentDaysValue != null && dailySchedule != null) {
- final deepCopyOfDailySchedule = dailySchedule
- .map(
- (day) => day.map((meal) => [...meal]).toList(),
- )
- .toList();
+ final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
if (currentDaysValue > selectedDaysValue) {
final difference = currentDaysValue - selectedDaysValue;
@@ -93,28 +92,16 @@ class CreateDietCubit extends Cubit {
deepCopyOfDailySchedule.add([[], [], [], [], []]);
}
}
- emit(
- state.copyWith(
- dietData: state.dietData.copyWith(
- dailySchedule: deepCopyOfDailySchedule,
- ),
- ),
- );
+ emit(state.copyWith(dietData: state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule)));
} else {
final List>> dailySchedule = List.generate(selectedDaysValue, (index) => [[], [], [], [], []]);
- emit(
- state.copyWith(
- dietData: state.dietData.copyWith(dailySchedule: dailySchedule),
- ),
- );
+ emit(state.copyWith(dietData: state.dietData.copyWith(dailySchedule: dailySchedule)));
}
emit(
state.copyWith(
durationIndex: index,
- dietData: state.dietData.copyWith(
- duration: durationValues[index],
- ),
+ dietData: state.dietData.copyWith(duration: durationValues[index]),
dietDurationError: null,
),
);
@@ -125,7 +112,12 @@ class CreateDietCubit extends Cubit {
}
void setImage(XFile? image, {String? imageUrl}) {
- emit(state.copyWith(dietData: state.dietData.copyWith(image: image, imageUrl: imageUrl), dietImageError: null));
+ emit(
+ state.copyWith(
+ dietData: state.dietData.copyWith(image: image, imageUrl: imageUrl),
+ dietImageError: null,
+ ),
+ );
}
void setDailySchedule(List>> dailySchedule) {
@@ -133,27 +125,25 @@ class CreateDietCubit extends Cubit {
}
void setDietState(DietState dietState) {
- emit(state.copyWith(dietData: state.dietData.copyWith(state: dietState), dietState: dietState, dietStateError: null));
+ emit(
+ state.copyWith(
+ dietData: state.dietData.copyWith(state: dietState),
+ dietState: dietState,
+ dietStateError: null,
+ ),
+ );
}
void setDietType({required String key, required bool value}) {
final type = Map.from(state.dietData.type);
type[key] = value;
- emit(
- state.copyWith(
- dietData: state.dietData.copyWith(type: type),
- ),
- );
+ emit(state.copyWith(dietData: state.dietData.copyWith(type: type)));
}
void addPortion({required int dayIndex, required int mealIndex, required DailySchedule portion}) {
final dailySchedule = state.dietData.dailySchedule;
if (dailySchedule != null) {
- final deepCopyOfDailySchedule = dailySchedule
- .map(
- (day) => day.map((meal) => [...meal]).toList(),
- )
- .toList();
+ final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
deepCopyOfDailySchedule[dayIndex][mealIndex].add(portion);
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
@@ -162,18 +152,10 @@ class CreateDietCubit extends Cubit {
}
}
- void removePortion({
- required int day,
- required int meal,
- required int portionIndex,
- }) {
+ void removePortion({required int day, required int meal, required int portionIndex}) {
final dailySchedule = state.dietData.dailySchedule;
if (dailySchedule != null) {
- final deepCopyOfDailySchedule = dailySchedule
- .map(
- (day) => day.map((meal) => [...meal]).toList(),
- )
- .toList();
+ final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
deepCopyOfDailySchedule[day][meal].removeAt(portionIndex);
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
@@ -184,11 +166,7 @@ class CreateDietCubit extends Cubit {
void updatePortion({required int dayIndex, required int mealIndex, required int portionIndex, required DailySchedule portion}) {
final dailySchedule = state.dietData.dailySchedule;
if (dailySchedule != null) {
- final deepCopyOfDailySchedule = dailySchedule
- .map(
- (day) => day.map((meal) => [...meal]).toList(),
- )
- .toList();
+ final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
deepCopyOfDailySchedule[dayIndex][mealIndex][portionIndex] = portion;
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
@@ -199,11 +177,7 @@ class CreateDietCubit extends Cubit {
void copyDay(int dayIndex) {
final dailySchedule = state.dietData.dailySchedule;
if (dailySchedule != null) {
- final deepCopyOfDailySchedule = dailySchedule
- .map(
- (day) => day.map((meal) => [...meal]).toList(),
- )
- .toList();
+ final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
final copiedDay = deepCopyOfDailySchedule[dayIndex];
emit(state.copyWith(copiedDay: copiedDay));
}
@@ -213,11 +187,7 @@ class CreateDietCubit extends Cubit {
final dailySchedule = state.dietData.dailySchedule;
final copiedDay = state.copiedDay;
if (dailySchedule != null && copiedDay != null) {
- final deepCopyOfDailySchedule = dailySchedule
- .map(
- (day) => day.map((meal) => [...meal]).toList(),
- )
- .toList();
+ final deepCopyOfDailySchedule = dailySchedule.map((day) => day.map((meal) => [...meal]).toList()).toList();
deepCopyOfDailySchedule[dayIndex] = copiedDay;
final newDietData = state.dietData.copyWith(dailySchedule: deepCopyOfDailySchedule);
emit(state.copyWith(dietData: newDietData));
@@ -268,14 +238,7 @@ class CreateDietCubit extends Cubit {
final dailyNutrition = [];
if (carbs != null && calories != null && fats != null && proteins != null) {
for (int i = 0; i < carbs.length; i++) {
- dailyNutrition.add(
- DailyNutrition(
- carbohydrates: carbs[i],
- energy: calories[i],
- fats: fats[i],
- proteins: proteins[i],
- ),
- );
+ dailyNutrition.add(DailyNutrition(carbohydrates: carbs[i], energy: calories[i], fats: fats[i], proteins: proteins[i]));
}
}
final dailySchedule = >>{};
@@ -339,7 +302,10 @@ class CreateDietCubit extends Cubit {
Future createDiet() async {
emit(state.copyWith(isDietPublishing: true));
- if (!_isFromValid()) return 'validation';
+ if (!_isFromValid()) {
+ emit(state.copyWith(isDietPublishing: false));
+ return 'validation';
+ }
try {
File? file;
if (state.dietData.imageUrl != null) {
diff --git a/lib/features/create_diet/presentation/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart
index 94b21709..23d946af 100644
--- a/lib/features/create_diet/presentation/diet_preview_screen.dart
+++ b/lib/features/create_diet/presentation/diet_preview_screen.dart
@@ -1,13 +1,8 @@
import 'dart:convert';
-import 'dart:io';
import 'dart:typed_data';
-import 'package:eatvio_app/core/enums/complained_content.dart';
import 'package:eatvio_app/core/enums/product_type.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
-import 'package:eatvio_app/core/utils/string_extentions.dart';
-import 'package:eatvio_app/core/widgets/bookmark_button.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
-import 'package:eatvio_app/core/widgets/custom_popup.dart';
import 'package:eatvio_app/core/widgets/info_row.dart';
import 'package:eatvio_app/features/diets/presentation/widgets/author_card.dart';
import 'package:eatvio_app/core/widgets/cpfc_container.dart';
@@ -36,20 +31,11 @@ class DietPreviewScreen extends StatefulWidget {
class _DietPreviewScreenState extends State {
final ScrollController scrollController = ScrollController();
- final textEditingController = TextEditingController();
- bool isMoreOptionsVisible = false;
late final Uint8List imageData;
@override
void initState() {
super.initState();
imageData = base64.decode(widget.diet.image);
- scrollController.addListener(() {
- if (isMoreOptionsVisible) {
- setState(() {
- isMoreOptionsVisible = false;
- });
- }
- });
}
@override
@@ -58,86 +44,6 @@ class _DietPreviewScreenState extends State {
return Scaffold(
backgroundColor: appTheme.gray100,
resizeToAvoidBottomInset: true,
- bottomSheet: Container(
- padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
- decoration: BoxDecoration(
- color: Colors.white,
- border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
- ),
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
- children: [
- Expanded(
- child: BookmarkButton(
- isFavorite: widget.diet.isFavorite,
- onTap: () {},
- width: 52,
- height: 52,
- size: 24,
- shape: BoxShape.circle,
- color: appTheme.gray50,
- ),
- ),
- ],
- ),
- const SizedBox(height: 8),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Expanded(
- child: SizedBox(
- child: TextFormField(
- textCapitalization: TextCapitalization.sentences,
- onChanged: (_) {
- setState(() {});
- },
- controller: textEditingController,
- maxLines: 5,
- minLines: 1,
- keyboardType: TextInputType.multiline,
- style: TextStyles.textMMedium.copyWith(color: appTheme.black),
- decoration: InputDecoration(
- isDense: true,
- contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- hintText: s.comment.capitalize(),
- hintStyle: TextStyles.textMMedium.copyWith(color: appTheme.lightGray600),
- filled: true,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(16),
- borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
- ),
- enabledBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(16),
- borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
- ),
- focusedBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(16),
- borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
- ),
- ),
- ),
- ),
- ),
- const SizedBox(width: 8),
- GestureDetector(
- onTap: () {},
- child: Container(
- decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, shape: BoxShape.circle),
- height: 32,
- width: 32,
- child: Icon(Icons.send_rounded, size: 18, color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black),
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
appBar: AppBar(
centerTitle: true,
title: Text(
@@ -148,15 +54,7 @@ class _DietPreviewScreenState extends State {
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
- actions: [
- CustomPopup(
- isAuthor: widget.diet.author.authorIsMe,
- path: 'diet/${widget.diet.slug}',
- complainedContentType: ComplainedContent.diet,
- userSlug: widget.diet.author.authorSlug,
- ),
- const SizedBox(width: 16),
- ],
+ actions: const [],
),
body: SingleChildScrollView(
controller: scrollController,
diff --git a/lib/features/create_plan/presentation/create_plan_screen.dart b/lib/features/create_plan/presentation/create_plan_screen.dart
index c9ac8302..1b0a3e5c 100644
--- a/lib/features/create_plan/presentation/create_plan_screen.dart
+++ b/lib/features/create_plan/presentation/create_plan_screen.dart
@@ -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(),
- child: const CreatePlanScreenContent(),
- );
+ return BlocProvider(create: (context) => sl(), child: const CreatePlanScreenContent());
}
}
-class CreatePlanScreenContent extends StatelessWidget {
+class CreatePlanScreenContent extends StatefulWidget {
const CreatePlanScreenContent({super.key});
+ @override
+ State createState() => _CreatePlanScreenContentState();
+}
+
+class _CreatePlanScreenContentState extends State {
+ final GlobalKey _mdKey = GlobalKey();
+ 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().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().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()),
);
},
),
diff --git a/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart b/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart
index 96385904..2289b31f 100644
--- a/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart
+++ b/lib/features/create_plan/presentation/cubit/create_plan_cubit.dart
@@ -67,6 +67,13 @@ class CreatePlanCubit extends Cubit {
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 {
'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 {
'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',
},
});
}
diff --git a/lib/features/create_plan/presentation/widgets/option_widget.dart b/lib/features/create_plan/presentation/widgets/option_widget.dart
index 42362e96..24cde52b 100644
--- a/lib/features/create_plan/presentation/widgets/option_widget.dart
+++ b/lib/features/create_plan/presentation/widgets/option_widget.dart
@@ -90,7 +90,7 @@ class _OptionWidgetState extends State {
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 {
),
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 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 states) {
+ return BorderSide(width: 1, color: appTheme.gray);
+ },
+ ),
+ onChanged: (bool? value) {
+ context.read().togglePersonalizedDietOptionState();
+ },
+ ),
+ Expanded(
+ child: Text(
+ s.option_personal_diet_desc,
+ style: TextStyles.textMMedium.copyWith(color: appTheme.black),
+ ),
+ ),
+ ],
+ ),
],
),
);
diff --git a/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart b/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart
index d230bd1a..932852ac 100644
--- a/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart
+++ b/lib/features/create_recipe/presentation/pages/recipe_create_screen.dart
@@ -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,7 +31,9 @@ class RecipeCreateScreen extends StatefulWidget {
class _RecipeCreateScreenState extends State {
late final CreateRecipeCubit createRecipeCubit;
-
+ final GlobalKey _mdKey = GlobalKey();
+ bool _mdToolbarVisible = false;
+ bool _isRecipeBeingPublished = false;
@override
void initState() {
super.initState();
@@ -54,6 +57,8 @@ class _RecipeCreateScreenState extends State {
} 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,51 +70,66 @@ class _RecipeCreateScreenState extends State {
child: Column(
spacing: 16,
children: [
- InfoAboutRecipeWidget(createRecipeCubit: createRecipeCubit),
+ InfoAboutRecipeWidget(
+ createRecipeCubit: createRecipeCubit,
+ markdownEditorKey: _mdKey,
+ onMarkdownToolbarVisibilityChanged: (v) => setState(() => _mdToolbarVisible = v),
+ ),
const IngredientsWidget(),
StepsWidget(createRecipeCubit: createRecipeCubit),
BlocBuilder(
builder: (context, state) {
return Builder(
builder: (context) {
- return InkWell(
- key: AppKeys.recipeCreateScreenPublishButton,
- onTap: () async {
- if (context.read().validateAll()) {
- final isSuccess = await context.read().publishRecipe(
- widget.recipe == null ? false : true,
- widget.recipe?.slug,
+ return _isRecipeBeingPublished
+ ? const Center(child: Loader())
+ : InkWell(
+ key: AppKeys.recipeCreateScreenPublishButton,
+ onTap: () async {
+ setState(() {
+ _isRecipeBeingPublished = true;
+ });
+ if (context.read().validateAll()) {
+ final isSuccess = await context.read().publishRecipe(
+ widget.recipe == null ? false : true,
+ widget.recipe?.slug,
+ );
+ setState(() {
+ _isRecipeBeingPublished = false;
+ });
+ if (isSuccess) {
+ Future.delayed(const Duration(milliseconds: 500), () {
+ if (!context.mounted) return;
+ context.pop();
+ showBottomSheetWithBlur(
+ userRootNavigator: true,
+ context: context,
+ child: SuccessPublishedRecipeBottomSheet(title: state.entity.title ?? 'Рецепт'),
+ );
+ });
+ } else {
+ if (!context.mounted) return;
+ showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}');
+ }
+ }
+ setState(() {
+ _isRecipeBeingPublished = false;
+ });
+ },
+ borderRadius: BorderRadius.circular(20),
+ child: Ink(
+ width: MediaQuery.of(context).size.width - 32,
+ decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
+ child: Container(
+ padding: const EdgeInsets.all(16),
+ alignment: Alignment.center,
+ child: Text(
+ widget.recipe != null ? 'Обновить' : 'Опубликовать',
+ style: const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
+ ),
+ ),
+ ),
);
- if (isSuccess) {
- Future.delayed(const Duration(milliseconds: 500), () {
- if (!context.mounted) return;
- context.pop();
- showBottomSheetWithBlur(
- userRootNavigator: true,
- context: context,
- child: SuccessPublishedRecipeBottomSheet(title: state.entity.title ?? 'Рецепт'),
- );
- });
- } else {
- if (!context.mounted) return;
- showErrorDialog(context, title: '${s.could_not_publish_recipe} ${state.entity.title}');
- }
- }
- },
- borderRadius: BorderRadius.circular(20),
- child: Ink(
- width: MediaQuery.of(context).size.width - 32,
- decoration: BoxDecoration(color: appTheme.blueA400, borderRadius: BorderRadius.circular(20)),
- child: Container(
- padding: const EdgeInsets.all(16),
- alignment: Alignment.center,
- child: Text(
- widget.recipe != null ? 'Обновить' : 'Опубликовать',
- style: const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w600),
- ),
- ),
- ),
- );
},
);
},
diff --git a/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart b/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart
index 7ce4d26d..0eb609e4 100644
--- a/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart
+++ b/lib/features/create_recipe/presentation/widgets/info_about_recipe_widget.dart
@@ -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? 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 createState() => _InfoAboutRecipeWidgetState();
@@ -105,19 +108,24 @@ class _InfoAboutRecipeWidgetState extends State {
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),
diff --git a/lib/features/diary/presentation/pages/barcode_scanner_screen.dart b/lib/features/diary/presentation/pages/barcode_scanner_screen.dart
new file mode 100644
index 00000000..19994568
--- /dev/null
+++ b/lib/features/diary/presentation/pages/barcode_scanner_screen.dart
@@ -0,0 +1,87 @@
+import 'package:flutter/material.dart';
+import 'package:mobile_scanner/mobile_scanner.dart';
+
+class BarcodeScannerScreen extends StatefulWidget {
+ const BarcodeScannerScreen({super.key});
+
+ @override
+ State createState() => _BarcodeScannerScreenState();
+}
+
+class _BarcodeScannerScreenState extends State {
+ final MobileScannerController _controller = MobileScannerController();
+ String? _detectedBarcode;
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Сканирование штрихкода'),
+ centerTitle: true,
+ ),
+ body: Column(
+ children: [
+ Expanded(
+ child: MobileScanner(
+ controller: _controller,
+ onDetect: (capture) {
+ final List barcodes = capture.barcodes;
+ if (barcodes.isEmpty) return;
+ final rawValue = barcodes.first.rawValue;
+ if (rawValue == null || rawValue == _detectedBarcode) return;
+ setState(() {
+ _detectedBarcode = rawValue;
+ });
+ },
+ ),
+ ),
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(16),
+ decoration: const BoxDecoration(
+ color: Color(0xFFF6F6F6),
+ borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Text(
+ 'Найденный штрихкод',
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ _detectedBarcode ?? 'Наведите камеру на штрихкод',
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ color: _detectedBarcode == null ? Colors.grey : Colors.black,
+ ),
+ ),
+ const SizedBox(height: 16),
+ if (_detectedBarcode != null)
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: () {
+ Navigator.of(context).pop(_detectedBarcode);
+ },
+ child: const Text('Использовать штрихкод'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
diff --git a/lib/features/diary/presentation/pages/diary_screen.dart b/lib/features/diary/presentation/pages/diary_screen.dart
index ae6f9e10..607e97d2 100644
--- a/lib/features/diary/presentation/pages/diary_screen.dart
+++ b/lib/features/diary/presentation/pages/diary_screen.dart
@@ -44,16 +44,24 @@ class UserDiaryScreen extends StatelessWidget {
}
}
-class DiaryScreenContent extends StatelessWidget {
+class DiaryScreenContent extends StatefulWidget {
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay});
final int? diaryId;
final int? currentPlanDay;
+
+ @override
+ State createState() => _DiaryScreenContentState();
+}
+
+class _DiaryScreenContentState extends State {
+ bool _isDiaryFinishing = false;
+
@override
Widget build(BuildContext context) {
final s = S.of(context);
return MultiBlocProvider(
providers: [
- BlocProvider