Parcourir la source

fix: release/0.1.1 known issues. (#1984)

* fix: #1976 Adding a cover image via upload doesn't work

* fix: #1973 Using the mouse to highlight text very easy to miss the first letter

* fix: #1962 Disable but still show the AI assistants icon in the toolbar menu when an OpenAI key is not provided

* fix: #1964 Text on the UI

* fix: #1966 the loading icon too close to the edge

* fix: #1967  the summarize feature generates duplicate answers

* fix: flutter analyze
Lucas.Xu il y a 2 ans
Parent
commit
3686eabcb3

+ 4 - 2
frontend/appflowy_flutter/assets/translations/en.json

@@ -344,16 +344,18 @@
       "referencedGrid": "Referenced Grid",
       "referencedGrid": "Referenced Grid",
       "autoCompletionMenuItemName": "Auto Completion",
       "autoCompletionMenuItemName": "Auto Completion",
       "autoGeneratorMenuItemName": "Auto Generator",
       "autoGeneratorMenuItemName": "Auto Generator",
-      "autoGeneratorTitleName": "Open AI: Auto Generator",
+      "autoGeneratorTitleName": "OpenAI: Ask AI to write anything...",
       "autoGeneratorLearnMore": "Learn more",
       "autoGeneratorLearnMore": "Learn more",
       "autoGeneratorGenerate": "Generate",
       "autoGeneratorGenerate": "Generate",
       "autoGeneratorHintText": "Tell us what you want to generate by OpenAI ...",
       "autoGeneratorHintText": "Tell us what you want to generate by OpenAI ...",
       "autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key",
       "autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key",
-      "smartEditTitleName": "Open AI: Smart Edit",
+      "smartEdit": "Smart Edit",
+      "smartEditTitleName": "OpenAI: Smart Edit",
       "smartEditFixSpelling": "Fix spelling",
       "smartEditFixSpelling": "Fix spelling",
       "smartEditSummarize": "Summarize",
       "smartEditSummarize": "Summarize",
       "smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
       "smartEditCouldNotFetchResult": "Could not fetch result from OpenAI",
       "smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
       "smartEditCouldNotFetchKey": "Could not fetch OpenAI key",
+      "smartEditDisabled": "Connect OpenAI in Settings",
       "cover": {
       "cover": {
         "changeCover": "Change Cover",
         "changeCover": "Change Cover",
         "colors": "Colors",
         "colors": "Colors",

+ 1 - 3
frontend/appflowy_flutter/lib/plugins/document/document_page.dart

@@ -183,9 +183,7 @@ class _AppFlowyEditorPageState extends State<_AppFlowyEditorPage> {
         ],
         ],
       ],
       ],
       toolbarItems: [
       toolbarItems: [
-        if (openAIKey != null && openAIKey!.isNotEmpty) ...[
-          smartEditItem,
-        ]
+        smartEditItem,
       ],
       ],
       themeData: theme.copyWith(extensions: [
       themeData: theme.copyWith(extensions: [
         ...theme.extensions.values,
         ...theme.extensions.values,

+ 3 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/cover/change_cover_popover.dart

@@ -16,6 +16,7 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:shared_preferences/shared_preferences.dart';
+import 'package:path/path.dart' as path;
 
 
 const String kLocalImagesKey = 'local_images';
 const String kLocalImagesKey = 'local_images';
 
 
@@ -263,7 +264,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
       if (path != null) {
       if (path != null) {
         final directory = await _coverPath();
         final directory = await _coverPath();
         final newPath = await File(path).copy(
         final newPath = await File(path).copy(
-          '$directory/${path.split('/').last}',
+          '$directory/${path.split(path).last}}',
         );
         );
         imageNames.add(newPath.path);
         imageNames.add(newPath.path);
       }
       }
@@ -274,7 +275,7 @@ class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
 
 
   Future<String> _coverPath() async {
   Future<String> _coverPath() async {
     final directory = await getIt<SettingsLocationCubit>().fetchLocation();
     final directory = await getIt<SettingsLocationCubit>().fetchLocation();
-    return Directory('$directory/covers')
+    return Directory(path.join(directory, 'covers'))
         .create(recursive: true)
         .create(recursive: true)
         .then((value) => value.path);
         .then((value) => value.path);
   }
   }

+ 2 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart

@@ -10,9 +10,9 @@ enum SmartEditAction {
   String get toInstruction {
   String get toInstruction {
     switch (this) {
     switch (this) {
       case SmartEditAction.summarize:
       case SmartEditAction.summarize:
-        return 'Make it shorter';
+        return 'Make this shorter and more concise:';
       case SmartEditAction.fixSpelling:
       case SmartEditAction.fixSpelling:
-        return 'Fix all the spelling mistakes';
+        return 'Correct this to standard English:';
     }
     }
   }
   }
 }
 }

+ 7 - 5
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart

@@ -140,9 +140,12 @@ class _SmartEditInputState extends State<_SmartEditInput> {
   }
   }
 
 
   Widget _buildResultWidget(BuildContext context) {
   Widget _buildResultWidget(BuildContext context) {
-    final loading = SizedBox.fromSize(
-      size: const Size.square(14),
-      child: const CircularProgressIndicator(),
+    final loading = Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 4.0),
+      child: SizedBox.fromSize(
+        size: const Size.square(14),
+        child: const CircularProgressIndicator(),
+      ),
     );
     );
     if (result == null) {
     if (result == null) {
       return loading;
       return loading;
@@ -222,7 +225,6 @@ class _SmartEditInputState extends State<_SmartEditInput> {
 
 
     final texts = result!.asRight().choices.first.text.split('\n')
     final texts = result!.asRight().choices.first.text.split('\n')
       ..removeWhere((element) => element.isEmpty);
       ..removeWhere((element) => element.isEmpty);
-    assert(texts.length == selectedNodes.length);
     final transaction = widget.editorState.transaction;
     final transaction = widget.editorState.transaction;
     transaction.replaceTexts(
     transaction.replaceTexts(
       selectedNodes.toList(growable: false),
       selectedNodes.toList(growable: false),
@@ -254,7 +256,7 @@ class _SmartEditInputState extends State<_SmartEditInput> {
       final edits = await openAIRepository.getEdits(
       final edits = await openAIRepository.getEdits(
         input: input,
         input: input,
         instruction: instruction,
         instruction: instruction,
-        n: input.split('\n').length,
+        n: 1,
       );
       );
       return edits.fold((error) async {
       return edits.fold((error) async {
         return dartz.Left(
         return dartz.Left(

+ 40 - 2
frontend/appflowy_flutter/lib/plugins/document/presentation/plugins/openai/widgets/smart_edit_toolbar_item.dart

@@ -1,10 +1,14 @@
 import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_action.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart';
 import 'package:appflowy/plugins/document/presentation/plugins/openai/widgets/smart_edit_node_widget.dart';
+import 'package:appflowy/user/application/user_service.dart';
 import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
 
 
 ToolbarItem smartEditItem = ToolbarItem(
 ToolbarItem smartEditItem = ToolbarItem(
   id: 'appflowy.toolbar.smart_edit',
   id: 'appflowy.toolbar.smart_edit',
@@ -33,6 +37,20 @@ class _SmartEditWidget extends StatefulWidget {
 }
 }
 
 
 class _SmartEditWidgetState extends State<_SmartEditWidget> {
 class _SmartEditWidgetState extends State<_SmartEditWidget> {
+  bool isOpenAIEnabled = false;
+
+  @override
+  void initState() {
+    super.initState();
+
+    UserBackendService.getCurrentUserProfile().then((value) {
+      setState(() {
+        isOpenAIEnabled =
+            value.fold((l) => l.openaiKey.isNotEmpty, (r) => false);
+      });
+    });
+  }
+
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return PopoverActionList<SmartEditActionWrapper>(
     return PopoverActionList<SmartEditActionWrapper>(
@@ -43,7 +61,9 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
       buildChild: (controller) {
       buildChild: (controller) {
         return FlowyIconButton(
         return FlowyIconButton(
           hoverColor: Colors.transparent,
           hoverColor: Colors.transparent,
-          tooltipText: 'Smart Edit',
+          tooltipText: isOpenAIEnabled
+              ? LocaleKeys.document_plugins_smartEdit.tr()
+              : LocaleKeys.document_plugins_smartEditDisabled.tr(),
           preferBelow: false,
           preferBelow: false,
           icon: const Icon(
           icon: const Icon(
             Icons.lightbulb_outline,
             Icons.lightbulb_outline,
@@ -51,7 +71,11 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
             color: Colors.white,
             color: Colors.white,
           ),
           ),
           onPressed: () {
           onPressed: () {
-            controller.show();
+            if (isOpenAIEnabled) {
+              controller.show();
+            } else {
+              _showError(LocaleKeys.document_plugins_smartEditDisabled.tr());
+            }
           },
           },
         );
         );
       },
       },
@@ -97,4 +121,18 @@ class _SmartEditWidgetState extends State<_SmartEditWidget> {
       withUpdateCursor: false,
       withUpdateCursor: false,
     );
     );
   }
   }
+
+  Future<void> _showError(String message) async {
+    ScaffoldMessenger.of(context).showSnackBar(
+      SnackBar(
+        action: SnackBarAction(
+          label: LocaleKeys.button_Cancel.tr(),
+          onPressed: () {
+            ScaffoldMessenger.of(context).hideCurrentSnackBar();
+          },
+        ),
+        content: FlowyText(message),
+      ),
+    );
+  }
 }
 }

+ 4 - 3
frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart

@@ -2,6 +2,7 @@ import 'dart:io';
 
 
 import 'package:appflowy_backend/appflowy_backend.dart';
 import 'package:appflowy_backend/appflowy_backend.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:path_provider/path_provider.dart';
+import 'package:path/path.dart' as path;
 
 
 import '../startup.dart';
 import '../startup.dart';
 
 
@@ -35,11 +36,11 @@ Future<Directory> appFlowyDocumentDirectory() async {
   switch (integrationEnv()) {
   switch (integrationEnv()) {
     case IntegrationMode.develop:
     case IntegrationMode.develop:
       Directory documentsDir = await getApplicationDocumentsDirectory();
       Directory documentsDir = await getApplicationDocumentsDirectory();
-      return Directory('${documentsDir.path}/flowy_dev').create();
+      return Directory(path.join(documentsDir.path, 'flowy_dev')).create();
     case IntegrationMode.release:
     case IntegrationMode.release:
       Directory documentsDir = await getApplicationDocumentsDirectory();
       Directory documentsDir = await getApplicationDocumentsDirectory();
-      return Directory('${documentsDir.path}/flowy').create();
+      return Directory(path.join(documentsDir.path, 'flowy')).create();
     case IntegrationMode.test:
     case IntegrationMode.test:
-      return Directory("${Directory.current.path}/.sandbox");
+      return Directory(path.join(Directory.current.path, '.sandbox'));
   }
   }
 }
 }

+ 4 - 1
frontend/appflowy_flutter/packages/appflowy_editor/lib/src/commands/command_extension.dart

@@ -57,7 +57,10 @@ extension CommandExtension on EditorState {
     Selection selection,
     Selection selection,
   ) {
   ) {
     List<String> res = [];
     List<String> res = [];
-    if (!selection.isCollapsed) {
+    if (selection.isSingle) {
+      final plainText = textNodes.first.toPlainText();
+      res.add(plainText.substring(selection.startIndex, selection.endIndex));
+    } else if (!selection.isCollapsed) {
       for (var i = 0; i < textNodes.length; i++) {
       for (var i = 0; i < textNodes.length; i++) {
         final plainText = textNodes[i].toPlainText();
         final plainText = textNodes[i].toPlainText();
         if (i == 0) {
         if (i == 0) {

+ 93 - 14
frontend/appflowy_flutter/packages/appflowy_editor/lib/src/core/transform/transaction.dart

@@ -291,46 +291,125 @@ extension TextTransaction on Transaction {
     Selection selection,
     Selection selection,
     List<String> texts,
     List<String> texts,
   ) {
   ) {
-    if (textNodes.isEmpty) {
+    if (textNodes.isEmpty || texts.isEmpty) {
       return;
       return;
     }
     }
 
 
-    if (selection.isSingle) {
-      assert(textNodes.length == 1 && texts.length == 1);
-      replaceText(
-        textNodes.first,
-        selection.startIndex,
-        selection.length,
-        texts.first,
-      );
-    } else {
+    if (textNodes.length == texts.length) {
       final length = textNodes.length;
       final length = textNodes.length;
-      for (var i = 0; i < length; i++) {
+
+      if (length == 1) {
+        replaceText(
+          textNodes.first,
+          selection.startIndex,
+          selection.endIndex - selection.startIndex,
+          texts.first,
+        );
+        return;
+      }
+
+      for (var i = 0; i < textNodes.length; i++) {
         final textNode = textNodes[i];
         final textNode = textNodes[i];
-        final text = texts[i];
         if (i == 0) {
         if (i == 0) {
           replaceText(
           replaceText(
             textNode,
             textNode,
             selection.startIndex,
             selection.startIndex,
             textNode.toPlainText().length,
             textNode.toPlainText().length,
-            text,
+            texts.first,
           );
           );
         } else if (i == length - 1) {
         } else if (i == length - 1) {
           replaceText(
           replaceText(
             textNode,
             textNode,
             0,
             0,
             selection.endIndex,
             selection.endIndex,
-            text,
+            texts.last,
           );
           );
         } else {
         } else {
           replaceText(
           replaceText(
             textNode,
             textNode,
             0,
             0,
             textNode.toPlainText().length,
             textNode.toPlainText().length,
+            texts[i],
+          );
+        }
+      }
+      return;
+    }
+
+    if (textNodes.length > texts.length) {
+      final length = textNodes.length;
+      for (var i = 0; i < textNodes.length; i++) {
+        final textNode = textNodes[i];
+        if (i == 0) {
+          replaceText(
+            textNode,
+            selection.startIndex,
+            textNode.toPlainText().length,
+            texts.first,
+          );
+        } else if (i == length - 1) {
+          replaceText(
+            textNode,
+            0,
+            selection.endIndex,
+            texts.last,
+          );
+        } else {
+          if (i < texts.length - 1) {
+            replaceText(
+              textNode,
+              0,
+              textNode.toPlainText().length,
+              texts[i],
+            );
+          } else {
+            deleteNode(textNode);
+          }
+        }
+      }
+      afterSelection = null;
+      return;
+    }
+
+    if (textNodes.length < texts.length) {
+      final length = texts.length;
+      for (var i = 0; i < texts.length; i++) {
+        final text = texts[i];
+        if (i == 0) {
+          replaceText(
+            textNodes.first,
+            selection.startIndex,
+            textNodes.first.toPlainText().length,
             text,
             text,
           );
           );
+        } else if (i == length - 1) {
+          replaceText(
+            textNodes.last,
+            0,
+            selection.endIndex,
+            text,
+          );
+        } else {
+          if (i < textNodes.length - 1) {
+            replaceText(
+              textNodes[i],
+              0,
+              textNodes[i].toPlainText().length,
+              text,
+            );
+          } else {
+            var path = textNodes.first.path;
+            var j = i - textNodes.length + length - 1;
+            while (j > 0) {
+              path = path.next;
+              j--;
+            }
+            insertNode(path, TextNode(delta: Delta()..insert(text)));
+          }
         }
         }
       }
       }
+      afterSelection = null;
+      return;
     }
     }
   }
   }
 }
 }

+ 1 - 0
frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/keyboard_service.dart

@@ -112,6 +112,7 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
     isFocus = false;
     isFocus = false;
     this.showCursor = showCursor;
     this.showCursor = showCursor;
     _focusNode.unfocus(disposition: disposition);
     _focusNode.unfocus(disposition: disposition);
+    _onFocusChange(false);
   }
   }
 
 
   @override
   @override

+ 2 - 1
frontend/appflowy_flutter/packages/appflowy_editor/lib/src/service/selection_service.dart

@@ -347,8 +347,9 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
 
 
   void _onPanStart(DragStartDetails details) {
   void _onPanStart(DragStartDetails details) {
     clearSelection();
     clearSelection();
+    _clearToolbar();
 
 
-    _panStartOffset = details.globalPosition;
+    _panStartOffset = details.globalPosition.translate(-3.0, 0);
     _panStartScrollDy = editorState.service.scrollService?.dy;
     _panStartScrollDy = editorState.service.scrollService?.dy;
 
 
     _enableInteraction();
     _enableInteraction();

+ 132 - 0
frontend/appflowy_flutter/packages/appflowy_editor/test/core/transform/transaction_test.dart

@@ -0,0 +1,132 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter_test/flutter_test.dart';
+import '../../infra/test_editor.dart';
+
+Document createEmptyDocument() {
+  return Document(
+    root: Node(
+      type: 'editor',
+    ),
+  );
+}
+
+void main() async {
+  group('transaction.dart', () {
+    testWidgets('test replaceTexts, textNodes.length == texts.length',
+        (tester) async {
+      TestWidgetsFlutterBinding.ensureInitialized();
+
+      final editor = tester.editor
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789');
+      await editor.startTesting();
+      await tester.pumpAndSettle();
+
+      expect(editor.documentLength, 4);
+
+      final selection = Selection(
+        start: Position(path: [0], offset: 4),
+        end: Position(path: [3], offset: 4),
+      );
+      final transaction = editor.editorState.transaction;
+      var textNodes = [0, 1, 2, 3]
+          .map((e) => editor.nodeAtPath([e])!)
+          .whereType<TextNode>()
+          .toList(growable: false);
+      final texts = ['ABC', 'ABC', 'ABC', 'ABC'];
+      transaction.replaceTexts(textNodes, selection, texts);
+      editor.editorState.apply(transaction);
+      await tester.pumpAndSettle();
+
+      expect(editor.documentLength, 4);
+      textNodes = [0, 1, 2, 3]
+          .map((e) => editor.nodeAtPath([e])!)
+          .whereType<TextNode>()
+          .toList(growable: false);
+      expect(textNodes[0].toPlainText(), '0123ABC');
+      expect(textNodes[1].toPlainText(), 'ABC');
+      expect(textNodes[2].toPlainText(), 'ABC');
+      expect(textNodes[3].toPlainText(), 'ABC456789');
+    });
+
+    testWidgets('test replaceTexts, textNodes.length >  texts.length',
+        (tester) async {
+      TestWidgetsFlutterBinding.ensureInitialized();
+
+      final editor = tester.editor
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789');
+      await editor.startTesting();
+      await tester.pumpAndSettle();
+
+      expect(editor.documentLength, 5);
+
+      final selection = Selection(
+        start: Position(path: [0], offset: 4),
+        end: Position(path: [4], offset: 4),
+      );
+      final transaction = editor.editorState.transaction;
+      var textNodes = [0, 1, 2, 3, 4]
+          .map((e) => editor.nodeAtPath([e])!)
+          .whereType<TextNode>()
+          .toList(growable: false);
+      final texts = ['ABC', 'ABC', 'ABC', 'ABC'];
+      transaction.replaceTexts(textNodes, selection, texts);
+      editor.editorState.apply(transaction);
+      await tester.pumpAndSettle();
+
+      expect(editor.documentLength, 4);
+      textNodes = [0, 1, 2, 3]
+          .map((e) => editor.nodeAtPath([e])!)
+          .whereType<TextNode>()
+          .toList(growable: false);
+      expect(textNodes[0].toPlainText(), '0123ABC');
+      expect(textNodes[1].toPlainText(), 'ABC');
+      expect(textNodes[2].toPlainText(), 'ABC');
+      expect(textNodes[3].toPlainText(), 'ABC456789');
+    });
+
+    testWidgets('test replaceTexts, textNodes.length < texts.length',
+        (tester) async {
+      TestWidgetsFlutterBinding.ensureInitialized();
+
+      final editor = tester.editor
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789')
+        ..insertTextNode('0123456789');
+      await editor.startTesting();
+      await tester.pumpAndSettle();
+
+      expect(editor.documentLength, 3);
+
+      final selection = Selection(
+        start: Position(path: [0], offset: 4),
+        end: Position(path: [2], offset: 4),
+      );
+      final transaction = editor.editorState.transaction;
+      var textNodes = [0, 1, 2]
+          .map((e) => editor.nodeAtPath([e])!)
+          .whereType<TextNode>()
+          .toList(growable: false);
+      final texts = ['ABC', 'ABC', 'ABC', 'ABC'];
+      transaction.replaceTexts(textNodes, selection, texts);
+      editor.editorState.apply(transaction);
+      await tester.pumpAndSettle();
+
+      expect(editor.documentLength, 4);
+      textNodes = [0, 1, 2, 3]
+          .map((e) => editor.nodeAtPath([e])!)
+          .whereType<TextNode>()
+          .toList(growable: false);
+      expect(textNodes[0].toPlainText(), '0123ABC');
+      expect(textNodes[1].toPlainText(), 'ABC');
+      expect(textNodes[2].toPlainText(), 'ABC');
+      expect(textNodes[3].toPlainText(), 'ABC456789');
+    });
+  });
+}

+ 1 - 1
frontend/appflowy_flutter/pubspec.lock

@@ -830,7 +830,7 @@ packages:
     source: hosted
     source: hosted
     version: "1.0.5"
     version: "1.0.5"
   path:
   path:
-    dependency: transitive
+    dependency: "direct main"
     description:
     description:
       name: path
       name: path
       url: "https://pub.dartlang.org"
       url: "https://pub.dartlang.org"

+ 1 - 0
frontend/appflowy_flutter/pubspec.yaml

@@ -95,6 +95,7 @@ dependencies:
   window_manager: ^0.3.0
   window_manager: ^0.3.0
   http: ^0.13.5
   http: ^0.13.5
   json_annotation: ^4.7.0
   json_annotation: ^4.7.0
+  path: ^1.8.2
 
 
 dev_dependencies:
 dev_dependencies:
   flutter_lints: ^2.0.1
   flutter_lints: ^2.0.1