From 3520d2d3d2e79575b7acbde4ca1c587f2fcbc31c Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Fri, 6 Mar 2026 15:23:10 +0100 Subject: [PATCH] Geometry update, update quill editor, fix save in multiple new features + MISC --- lib/Components/map_geometry_picker.dart | 20 +- .../multi_string_input_html_modal.dart | 212 ++++---- ...nslation_input_and_resource_container.dart | 378 +++++++------- .../translation_input_container.dart | 482 +++++++++--------- .../SubSection/Agenda/agenda_config.dart | 150 ++++-- .../Agenda/showNewOrUpdateEventAgenda.dart | 5 +- .../SubSection/Event/event_config.dart | 113 ++-- .../Event/showNewOrUpdateProgrammeBlock.dart | 18 +- .../SubSection/Parcours/parcours_config.dart | 66 ++- .../Parcours/showNewOrUpdateGuidedPath.dart | 30 +- .../Parcours/showNewOrUpdateGuidedStep.dart | 24 +- .../Parcours/showNewOrUpdateQuizQuestion.dart | 3 + .../Section/section_detail_screen.dart | 33 +- lib/main.dart | 18 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 309 ++++++++--- pubspec.yaml | 7 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 19 files changed, 1137 insertions(+), 737 deletions(-) diff --git a/lib/Components/map_geometry_picker.dart b/lib/Components/map_geometry_picker.dart index 6d681cf..a0fc204 100644 --- a/lib/Components/map_geometry_picker.dart +++ b/lib/Components/map_geometry_picker.dart @@ -57,12 +57,24 @@ class _MapGeometryPickerState extends State { if (currentType == "Point") { var coords = widget.initialGeometry!.coordinates as List; points = [LatLng(coords[0].toDouble(), coords[1].toDouble())]; - } else if (currentType == "LineString" || currentType == "Polygon") { + } else if (currentType == "LineString") { var list = widget.initialGeometry!.coordinates as List; points = list.map((e) { var pair = e as List; return LatLng(pair[0].toDouble(), pair[1].toDouble()); }).toList(); + } else if (currentType == "Polygon") { + // Polygon coordinates: [[[lat,lng],...]] — first element is exterior ring + var rings = widget.initialGeometry!.coordinates as List; + var ring = rings[0] as List; + points = ring.map((e) { + var pair = e as List; + return LatLng(pair[0].toDouble(), pair[1].toDouble()); + }).toList(); + // Remove closing point if it duplicates the first + if (points.length > 1 && points.first == points.last) { + points.removeLast(); + } } } catch (e) { print("Error parsing geometry: $e"); @@ -87,6 +99,12 @@ class _MapGeometryPickerState extends State { ? [points[0].latitude, points[0].longitude] : null, ); + } else if (currentType == "Polygon") { + // Polygon: [[[lat,lng],...]] — wrap ring in outer array + return GeometryDTO( + type: "Polygon", + coordinates: [points.map((e) => [e.latitude, e.longitude]).toList()], + ); } else { return GeometryDTO( type: currentType, diff --git a/lib/Components/multi_string_input_html_modal.dart b/lib/Components/multi_string_input_html_modal.dart index 77fb744..331e590 100644 --- a/lib/Components/multi_string_input_html_modal.dart +++ b/lib/Components/multi_string_input_html_modal.dart @@ -11,59 +11,71 @@ showMultiStringInputHTML (String label, String modalLabel, bool isTitle, List[ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Container( - width: 180, - height: 70, - child: RoundedButton( - text: "Annuler", - icon: Icons.undo, - color: kSecond, - press: () { - onGetResult(values); - Navigator.of(context).pop(); - }, - fontSize: 20, + insetPadding: const EdgeInsets.symmetric(horizontal: 60, vertical: 24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 20, 24, 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center(child: Text(modalLabel, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500))), + const SizedBox(height: 16), + SingleChildScrollView( + child: TranslationInputContainer(isTitle: isTitle, values: values, newValues: newValues, onGetResult: onGetResult, maxLines: maxLines, resourceTypes: resourceTypes), ), - ), - Container( - width: 180, - height: 70, - child: RoundedButton( - text: "Valider", - icon: Icons.check, - color: kPrimaryColor, - textColor: kWhite, - press: () { - Function deepEq = const DeepCollectionEquality().equals; - if (!deepEq(values, newValues)) { - if(isMandatory && newValues.any((label) => label.value == null || label.value!.trim() == "")) { - showNotification(kPrimaryColor, kWhite, "La traduction n'est pas complète", context, null); - } else { - onGetResult(newValues); - Navigator.of(context).pop(); - } - } else { - Navigator.of(context).pop(); - } - }, - fontSize: 20, + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox( + width: 180, + height: 70, + child: RoundedButton( + text: "Annuler", + icon: Icons.undo, + color: kSecond, + press: () { + onGetResult(values); + Navigator.of(context).pop(); + }, + fontSize: 20, + ), + ), + SizedBox( + width: 180, + height: 70, + child: RoundedButton( + text: "Valider", + icon: Icons.check, + color: kPrimaryColor, + textColor: kWhite, + press: () { + Function deepEq = const DeepCollectionEquality().equals; + if (!deepEq(values, newValues)) { + if(isMandatory && newValues.any((label) => label.value == null || label.value!.trim() == "")) { + showNotification(kPrimaryColor, kWhite, "La traduction n'est pas complète", context, null); + } else { + onGetResult(newValues); + Navigator.of(context).pop(); + } + } else { + Navigator.of(context).pop(); + } + }, + fontSize: 20, + ), + ), + ], ), - ), - ], + ], + ), ), - ], + ), ); }, context: context ); @@ -73,59 +85,71 @@ showMultiStringInputAndResourceHTML (String label, String modalLabel, bool isTit showDialog( useRootNavigator: false, builder: (BuildContext context) { - return AlertDialog( + return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(20.0)) ), - title: Center(child: Text(modalLabel)), - content: SingleChildScrollView( - child: TranslationInputAndResourceContainer(isTitle: isTitle, values: values, newValues: newValues, onGetResult: onGetResult, maxLines: maxLines, resourceTypes: resourceTypes) - ), - actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Container( - width: 180, - height: 70, - child: RoundedButton( - text: "Annuler", - icon: Icons.undo, - color: kSecond, - press: () { - onGetResult(values); - Navigator.of(context).pop(); - }, - fontSize: 20, + insetPadding: const EdgeInsets.symmetric(horizontal: 60, vertical: 24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 20, 24, 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center(child: Text(modalLabel, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500))), + const SizedBox(height: 16), + SingleChildScrollView( + child: TranslationInputAndResourceContainer(isTitle: isTitle, values: values, newValues: newValues, onGetResult: onGetResult, maxLines: maxLines, resourceTypes: resourceTypes), ), - ), - Container( - width: 180, - height: 70, - child: RoundedButton( - text: "Valider", - icon: Icons.check, - color: kPrimaryColor, - textColor: kWhite, - press: () { - Function deepEq = const DeepCollectionEquality().equals; - if (!deepEq(values, newValues)) { - if(newValues.any((label) => label.value == null || label.value!.trim() == "")) { - showNotification(kPrimaryColor, kWhite, "La traduction n'est pas complète", context, null); - } else { - onGetResult(newValues); - Navigator.of(context).pop(); - } - } else { - Navigator.of(context).pop(); - } - }, - fontSize: 20, + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox( + width: 180, + height: 70, + child: RoundedButton( + text: "Annuler", + icon: Icons.undo, + color: kSecond, + press: () { + onGetResult(values); + Navigator.of(context).pop(); + }, + fontSize: 20, + ), + ), + SizedBox( + width: 180, + height: 70, + child: RoundedButton( + text: "Valider", + icon: Icons.check, + color: kPrimaryColor, + textColor: kWhite, + press: () { + Function deepEq = const DeepCollectionEquality().equals; + if (!deepEq(values, newValues)) { + if(newValues.any((label) => label.value == null || label.value!.trim() == "")) { + showNotification(kPrimaryColor, kWhite, "La traduction n'est pas complète", context, null); + } else { + onGetResult(newValues); + Navigator.of(context).pop(); + } + } else { + Navigator.of(context).pop(); + } + }, + fontSize: 20, + ), + ), + ], ), - ), - ], + ], + ), ), - ], + ), ); }, context: context ); diff --git a/lib/Components/translation_input_and_resource_container.dart b/lib/Components/translation_input_and_resource_container.dart index c3a3498..5e2bf17 100644 --- a/lib/Components/translation_input_and_resource_container.dart +++ b/lib/Components/translation_input_and_resource_container.dart @@ -1,15 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart'; +import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart'; import 'package:manager_api_new/api.dart'; -import 'package:manager_app/Components/audio_input_container.dart'; import 'package:manager_app/Components/resource_input_container.dart'; -import 'package:manager_app/app_context.dart'; import 'package:manager_app/constants.dart'; -import 'package:provider/provider.dart'; -import 'package:quill_html_editor/quill_html_editor.dart'; import 'flag_decoration.dart'; -class TranslationInputAndResourceContainer extends StatefulWidget { +class TranslationInputAndResourceContainer extends StatefulWidget { TranslationInputAndResourceContainer({ Key? key, required this.isTitle, @@ -28,225 +27,224 @@ class TranslationInputAndResourceContainer extends StatefulWidget { List? resourceTypes; @override - State createState() => _TranslationInputAndResourceContainerState(); + State createState() => + _TranslationInputAndResourceContainerState(); } -class _TranslationInputAndResourceContainerState extends State with TickerProviderStateMixin { - TabController? _tabController; - QuillEditorController controllerQuill = QuillEditorController(); - ValueNotifier? currentLanguage; - bool isInit = false; +class _TranslationInputAndResourceContainerState + extends State { + late Map _controllers; + bool _isEnforcingLimit = false; @override void initState() { super.initState(); + _controllers = _buildControllers(); + } - _tabController = new TabController(length: widget.newValues.length, vsync: this); - currentLanguage = ValueNotifier(widget.newValues.first.language!); + static const _emptyDelta = [{'insert': '\n'}]; - controllerQuill.onEditorLoaded(() { - isInit = true; - }); + List _htmlToDeltaJson(String html) { + if (html.trim().isEmpty) return _emptyDelta; + final ops = HtmlToDelta().convert(html).toJson(); + return ops.isEmpty ? _emptyDelta : ops; + } - Future.delayed(Duration(milliseconds: 500), () { - controllerQuill.clear(); - controllerQuill.insertText(widget.newValues[_tabController!.index].value!); - isInit = true; - }); + Map _buildControllers() { + final map = {}; + for (final translation in widget.newValues) { + final html = translation.value ?? ''; + final controller = QuillController( + document: Document.fromJson(_htmlToDeltaJson(html)), + selection: const TextSelection.collapsed(offset: 0), + ); + _setupListener(translation.language!, controller); + map[translation.language!] = controller; + } + return map; + } - controllerQuill.onTextChanged((p0) async { - var plainText = await controllerQuill.getPlainText(); - if(widget.isTitle) { - if(plainText.length > kTitleMaxLength) { - controllerQuill.undo(); - } - } else { - if(plainText.length > kDescriptionMaxLength) { - print("to much text description au dessus"); - controllerQuill.undo(); - } - } - }); - - _tabController!.addListener(() { - if (!_tabController!.indexIsChanging) { - setState(() { - currentLanguage!.value = widget.newValues[_tabController!.index].language; - //if(widget.resourceTypes == null) { - print("insert try without ress"); - print(widget.newValues[_tabController!.index].value!); - controllerQuill.clear(); - controllerQuill.insertText(widget.newValues[_tabController!.index].value!); - //} + void _setupListener(String lang, QuillController controller) { + controller.document.changes.listen((_) { + if (!mounted || _isEnforcingLimit) return; + final limit = widget.isTitle ? kTitleMaxLength : kDescriptionMaxLength; + final plain = controller.document.toPlainText().trimRight(); + if (plain.length > limit) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && !_isEnforcingLimit) { + _isEnforcingLimit = true; + controller.undo(); + _isEnforcingLimit = false; + } }); + return; } + widget.newValues + .firstWhere((e) => e.language == lang) + .value = _controllerToHtml(controller); }); } + String _controllerToHtml(QuillController controller) { + final ops = controller.document.toDelta().toJson(); + return QuillDeltaToHtmlConverter( + List>.from(ops), + ).convert(); + } + @override void dispose() { + for (final c in _controllers.values) { + c.dispose(); + } super.dispose(); - _tabController!.dispose(); } @override Widget build(BuildContext context) { - final customToolBarList = widget.isTitle ? [ - ToolBarStyle.bold, - ToolBarStyle.italic, - ToolBarStyle.color, - ToolBarStyle.background, - ToolBarStyle.clean - ] : [ - ToolBarStyle.bold, - ToolBarStyle.italic, - ToolBarStyle.color, - ToolBarStyle.background, - ToolBarStyle.listBullet, - ToolBarStyle.listOrdered, - ToolBarStyle.clean - ]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: widget.newValues.map((t) => _buildLanguageSection(t)).toList(), + ); + } - return Container( - height: widget.isTitle ? MediaQuery.of(context).size.height *0.45 : MediaQuery.of(context).size.height *0.5, - //color: Colors.orange, - width: MediaQuery.of(context).size.width *0.7, - constraints: BoxConstraints( - minHeight: 300, - minWidth: 300 - ), - child: DefaultTabController( - length: widget.newValues.length, + Widget _buildLanguageSection(TranslationAndResourceDTO translation) { + final lang = translation.language!; + + // Resource-only mode (no text editor) + if (widget.resourceTypes != null && _controllers[lang] == null) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - RotatedBox( - quarterTurns: 0, // Can be used to test vertical tab in case of smaller screen - child: TabBar( - indicatorColor: kPrimaryColor, - //overlayColor: MaterialStateProperty().c, - labelColor: kPrimaryColor, - unselectedLabelColor: Colors.black, - controller: _tabController, - tabs: widget.newValues.map((v) => Tab(icon: FlagDecoration(language: v.language!))).toList(), // text: v.language!.toUpperCase(), + _buildLangHeader(lang), + const SizedBox(height: 4), + SizedBox( + width: 250, + height: 120, + child: ResourceInputContainer( + label: "", + initialValue: translation.resourceId, + inResourceTypes: widget.resourceTypes!, + onChanged: (ResourceDTO resource) { + setState(() { + translation.resourceId = + resource.id == null ? null : resource.id; + translation.resource = + resource.id == null ? null : resource; + }); + }, ), ), - getTranslation(context, Provider.of(context), controllerQuill, customToolBarList, widget.isTitle, widget.resourceTypes, widget.newValues, currentLanguage!) ], ), + ); + } + + final controller = _controllers[lang]!; + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLangHeader(lang), + const SizedBox(height: 4), + QuillSimpleToolbar( + controller: controller, + config: _buildToolbarConfig(), + ), + Container( + height: widget.isTitle ? 100 : 200, + decoration: BoxDecoration( + color: kBackgroundColor, + border: Border.all(color: Colors.grey.shade300), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(4), + bottomRight: Radius.circular(4), + ), + ), + child: QuillEditor.basic( + controller: controller, + config: const QuillEditorConfig( + scrollable: true, + expands: false, + padding: EdgeInsets.all(8), + autoFocus: false, + ), + ), + ), + if (widget.resourceTypes != null) ...[ + const SizedBox(height: 8), + ResourceInputContainer( + label: "Ressource à afficher :", + initialValue: translation.resourceId, + color: kPrimaryColor, + inResourceTypes: widget.resourceTypes!, + isLanguageTab: true, + onChanged: (ResourceDTO resource) { + setState(() { + translation.resourceId = + resource.id == null ? null : resource.id; + translation.resource = + resource.id == null ? null : resource; + }); + }, + isSmall: true, + ), + ], + ], ), ); } - getTranslation(BuildContext context, AppContext appContext, QuillEditorController controllerQuill, List customToolBarList, bool isTitle, List? resourceTypes, List newValues, ValueNotifier currentLanguage) { - return Padding( - padding: const EdgeInsets.all(6.0), - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Container( - width: MediaQuery.of(context).size.width *0.7, - //color: Colors.blueAccent, - height: widget.isTitle ? MediaQuery.of(context).size.height *0.34 : MediaQuery.of(context).size.height *0.37, - child: resourceTypes != null ? - Column( - children: [ - ToolBar( - toolBarColor: kSecond, - activeIconColor: kPrimaryColor, - padding: const EdgeInsets.all(8), - iconSize: 20, - toolBarConfig: customToolBarList, - controller: controllerQuill, - customButtons: [], - ), - SingleChildScrollView( - child: Column( - children: [ - Container( - height: widget.isTitle ? MediaQuery.of(context).size.height *0.13 : MediaQuery.of(context).size.height *0.2, - child: QuillHtmlEditor( - text: newValues.where((element) => element.language! == currentLanguage.value).first.value!, - hintText: '', - controller: controllerQuill, - minHeight: widget.isTitle ? 80 : 240, - /*textStyle: _editorTextStyle, - hintTextStyle: _hintTextStyle,*/ - hintTextAlign: TextAlign.start, - padding: const EdgeInsets.only(left: 10, right: 10, top: 5), - hintTextPadding: EdgeInsets.zero, - backgroundColor: kBackgroundColor, - ensureVisible: true, - inputAction: widget.isTitle ? InputAction.send : InputAction.newline, // don't accept enter if title - //onFocusChanged: (hasFocus) => debugPrint('has focus $hasFocus'), - //onTextChanged: (text) => debugPrint('widget text change $text'), - onTextChanged: (value) { - if(isInit) { - newValues.where((element) => element.language! == currentLanguage.value).first.value = value; - } - }, - onEditorCreated: () => debugPrint('Editor has been loaded'), - onEditorResized: (height) => - debugPrint('Editor resized $height'), - onSelectionChanged: (sel) => - debugPrint('${sel.index},${sel.length}'), - ), - ), - ValueListenableBuilder( - valueListenable: currentLanguage, - builder: (context, value, _) { - return ResourceInputContainer( - label: "Ressource à afficher :", - initialValue: newValues.where((element) => element.language! == value).first.resourceId, - color: kPrimaryColor, - inResourceTypes: resourceTypes, - isLanguageTab: true, - onChanged: (ResourceDTO resource) { - setState(() { - if(resource.id == null) { - newValues.where((element) => element.language! == value).first.resourceId = null; - newValues.where((element) => element.language! == value).first.resource = null; - } else { - newValues.where((element) => element.language! == value).first.resourceId = resource.id; - newValues.where((element) => element.language! == value).first.resource = resource; - /*newValues.where((element) => element.language! == value).first.resourceUrl = resource.url; - newValues.where((element) => element.language! == value).first.resourceType = resource.type;*/ - } - }); - }, - isSmall: true - ); - } - ), - ], - ), - ), - ], - ) : - Container( - width: 250, - height: 120, - child: ValueListenableBuilder( - valueListenable: currentLanguage, - builder: (context, value, _) { - return ResourceInputContainer( - label: "", - initialValue: newValues.where((element) => element.language! == value).first.resourceId, - inResourceTypes: widget.resourceTypes!, - onChanged: (ResourceDTO resource) { - newValues.where((element) => element.language! == value).first.value = resource.id; - } - ); - return AudioInputContainer( - initialValue: newValues.where((element) => element.language! == value).first.value, - color: kPrimaryColor, - onChanged: (ResourceDTO resource) { - newValues.where((element) => element.language! == value).first.value = resource.id; - }, - ); - } - ), - ), + Widget _buildLangHeader(String lang) { + return Row( + children: [ + FlagDecoration(language: lang), + const SizedBox(width: 8), + Text( + lang.toUpperCase(), + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14), ), - ), + ], + ); + } + + QuillSimpleToolbarConfig _buildToolbarConfig() { + return QuillSimpleToolbarConfig( + showBoldButton: true, + showItalicButton: true, + showColorButton: true, + showBackgroundColorButton: true, + showListBullets: !widget.isTitle, + showListNumbers: !widget.isTitle, + showListCheck: false, + showClearFormat: true, + showUnderLineButton: false, + showStrikeThrough: false, + showInlineCode: false, + showSubscript: false, + showSuperscript: false, + showSmallButton: false, + showLineHeightButton: false, + showHeaderStyle: false, + showLink: false, + showSearchButton: false, + showQuote: false, + showCodeBlock: false, + showIndent: false, + showAlignmentButtons: false, + showLeftAlignment: false, + showCenterAlignment: false, + showRightAlignment: false, + showJustifyAlignment: false, + showDirection: false, + showUndo: false, + showRedo: false, + showClipboardCut: false, + showClipboardCopy: false, + showClipboardPaste: false, ); } } diff --git a/lib/Components/translation_input_container.dart b/lib/Components/translation_input_container.dart index 2ab65bc..8f687e1 100644 --- a/lib/Components/translation_input_container.dart +++ b/lib/Components/translation_input_container.dart @@ -1,258 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart'; +import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart'; import 'package:manager_api_new/api.dart'; -import 'package:manager_app/Components/audio_input_container.dart'; import 'package:manager_app/Components/resource_input_container.dart'; import 'package:manager_app/Components/rounded_button.dart'; -import 'package:manager_app/app_context.dart'; import 'package:manager_app/constants.dart'; -import 'package:provider/provider.dart'; -import 'package:quill_html_editor/quill_html_editor.dart'; import 'flag_decoration.dart'; import 'message_notification.dart'; -class _TranslationInputContainerState extends State with TickerProviderStateMixin { - TabController? _tabController; - QuillEditorController controllerQuill = QuillEditorController(); - ValueNotifier? currentLanguage; - bool isInit = false; - - @override - void initState() { - super.initState(); - _tabController = new TabController(length: widget.newValues.length, vsync: this); - currentLanguage = ValueNotifier(widget.newValues.first.language!); - - controllerQuill.onEditorLoaded(() { - isInit = true; - }); - - Future.delayed(Duration(milliseconds: 500), () { - controllerQuill.clear(); - controllerQuill.insertText(widget.newValues[_tabController!.index].value!); - isInit = true; - }); - - controllerQuill.onTextChanged((p0) async { - var plainText = await controllerQuill.getPlainText(); - if(widget.isTitle) { - if(plainText.length > kTitleMaxLength) { - controllerQuill.undo(); - } - } else { - if(plainText.length > kDescriptionMaxLength) { - controllerQuill.undo(); - } - } - }); - - _tabController!.addListener(() { - if (!_tabController!.indexIsChanging) { - setState(() { - currentLanguage!.value = widget.newValues[_tabController!.index].language; - if(widget.resourceTypes == null) { - print("insert try without ress"); - print(widget.newValues[_tabController!.index].value!); - controllerQuill.clear(); - controllerQuill.insertText(widget.newValues[_tabController!.index].value!); - } - }); - } - }); - } - - @override - void dispose() { - super.dispose(); - _tabController!.dispose(); - } - - getTranslation(BuildContext context, AppContext appContext, QuillEditorController controllerQuill, List customToolBarList, bool isTitle, List? resourceTypes, List newValues, ValueNotifier currentLanguage) { - return Padding( - padding: const EdgeInsets.all(6.0), - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Container( - width: MediaQuery.of(context).size.width *0.7, - height: widget.isTitle ? MediaQuery.of(context).size.height *0.25 : MediaQuery.of(context).size.height *0.4, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - resourceTypes == null ? - Column( - children: [ - ToolBar( - toolBarColor: kSecond, - activeIconColor: kPrimaryColor, - padding: const EdgeInsets.all(8), - iconSize: 20, - toolBarConfig: customToolBarList, - controller: controllerQuill, - customButtons: [], - ), - SingleChildScrollView( - child: Container( - height: widget.isTitle ? MediaQuery.of(context).size.height *0.13 : MediaQuery.of(context).size.height *0.35, - child: QuillHtmlEditor( - //text: newValues.where((element) => element.language! == currentLanguage.value).first.value!, - hintText: '', - controller: controllerQuill, - minHeight: widget.isTitle ? 80 : 240, - /*textStyle: _editorTextStyle, - hintTextStyle: _hintTextStyle,*/ - hintTextAlign: TextAlign.start, - padding: const EdgeInsets.only(left: 10, right: 10, top: 5), - hintTextPadding: EdgeInsets.zero, - backgroundColor: kBackgroundColor, - ensureVisible: true, - inputAction: widget.isTitle ? InputAction.send : InputAction.newline, // don't accept enter if title - //onFocusChanged: (hasFocus) => debugPrint('has focus $hasFocus'), - //onTextChanged: (text) => debugPrint('widget text change $text'), - onTextChanged: (value) { - if(isInit) { - newValues.where((element) => element.language! == currentLanguage.value).first.value = value; - } - }, - onEditorCreated: () => debugPrint('Editor has been loaded'), - onEditorResized: (height) => - debugPrint('Editor resized $height'), - onSelectionChanged: (sel) => - debugPrint('${sel.index},${sel.length}'), - ), - ), - ), - ], - ) - /*HtmlEditor( - controller: controller, - htmlEditorOptions: HtmlEditorOptions( - hint: "Your text here...", - initialText: newValues.where((element) => element.language == language).first.value!, - shouldEnsureVisible: true, - ), - htmlToolbarOptions: HtmlToolbarOptions( - toolbarPosition: ToolbarPosition.aboveEditor, //required to place toolbar anywhere! - //other options - ), - otherOptions: OtherOptions( - height: 400, - ), - )*/ - /*TextFormInputContainer( - label: label, - color: kWhite, - isTitle: isTitle, - initialValue: newValues.where((element) => element.language == language).first.value!, - onChanged: (value) { - newValues.where((element) => element.language == language).first.value = value; - }, - )*/ : - Container( - width: 250, - height: 120, - child: ValueListenableBuilder( - valueListenable: currentLanguage, - builder: (context, value, _) { - return ResourceInputContainer( - label: "", - initialValue: newValues.where((element) => element.language! == value).first.value, - inResourceTypes: widget.resourceTypes!, - onChanged: (ResourceDTO resource) { - newValues.where((element) => element.language! == value).first.value = resource.id; - } - ); - /*return AudioInputContainer( - //label: "Audio :", - initialValue: newValues.where((element) => element.language! == value).first.value, - color: kPrimaryColor, - onChanged: (ResourceDTO resource) { - newValues.where((element) => element.language! == value).first.value = resource.id; - }, - );*/ - }, - ), - ), - ], - ), - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - final customToolBarList = widget.isTitle ? [ - ToolBarStyle.bold, - ToolBarStyle.italic, - ToolBarStyle.color, - ToolBarStyle.background, - ToolBarStyle.clean - ] : [ - ToolBarStyle.bold, - ToolBarStyle.italic, - ToolBarStyle.color, - ToolBarStyle.background, - ToolBarStyle.listBullet, - ToolBarStyle.listOrdered, - ToolBarStyle.clean - ]; - - return Container( - height: widget.isTitle ? MediaQuery.of(context).size.height *0.4 : MediaQuery.of(context).size.height *0.53, - width: MediaQuery.of(context).size.width *0.7, - constraints: BoxConstraints( - minHeight: 200, - minWidth: 300 - ), - child: Column( - children: [ - DefaultTabController( - length: widget.newValues.length, - child: Column( - children: [ - RotatedBox( - quarterTurns: 0, // Can be used to test vertical tab in case of smaller screen - child: TabBar( - indicatorColor: kPrimaryColor, - //overlayColor: MaterialStateProperty().c, - labelColor: kPrimaryColor, - unselectedLabelColor: Colors.black, - controller: _tabController, - tabs: widget.newValues.map((v) => Tab(icon: FlagDecoration(language: v.language!))).toList(), // text: v.language!.toUpperCase(), - ), - ), - getTranslation(context, Provider.of(context), controllerQuill, customToolBarList, widget.isTitle, widget.resourceTypes, widget.newValues, currentLanguage!) - ], - ), - ), - Center( - child: Container( - width: 370, - height: 70, - child: RoundedButton( - text: "Appliquer à toutes les langues", - icon: Icons.copy, - color: kSecond, - press: () async { - var plainText = await controllerQuill.getText(); - widget.newValues.forEach((language) { - language.value = plainText; - } - ); - showNotification(kSuccess, kWhite, 'Le texte a été appliqué à toutes les langues', context, null); - }, - fontSize: 20, - ), - ), - ), - ], - ), - ); - } -} - -class TranslationInputContainer extends StatefulWidget { +class TranslationInputContainer extends StatefulWidget { TranslationInputContainer({ Key? key, required this.isTitle, @@ -273,3 +31,235 @@ class TranslationInputContainer extends StatefulWidget { @override State createState() => _TranslationInputContainerState(); } + +class _TranslationInputContainerState extends State { + late Map _controllers; + bool _isEnforcingLimit = false; + + @override + void initState() { + super.initState(); + _controllers = _buildControllers(); + } + + static const _emptyDelta = [{'insert': '\n'}]; + + List _htmlToDeltaJson(String html) { + if (html.trim().isEmpty) return _emptyDelta; + final ops = HtmlToDelta().convert(html).toJson(); + return ops.isEmpty ? _emptyDelta : ops; + } + + Map _buildControllers() { + final map = {}; + for (final translation in widget.newValues) { + final html = translation.value ?? ''; + final controller = QuillController( + document: Document.fromJson(_htmlToDeltaJson(html)), + selection: const TextSelection.collapsed(offset: 0), + ); + _setupListener(translation.language!, controller); + map[translation.language!] = controller; + } + return map; + } + + void _setupListener(String lang, QuillController controller) { + controller.document.changes.listen((_) { + if (!mounted || _isEnforcingLimit) return; + final limit = widget.isTitle ? kTitleMaxLength : kDescriptionMaxLength; + final plain = controller.document.toPlainText().trimRight(); + if (plain.length > limit) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && !_isEnforcingLimit) { + _isEnforcingLimit = true; + controller.undo(); + _isEnforcingLimit = false; + } + }); + return; + } + widget.newValues + .firstWhere((e) => e.language == lang) + .value = _controllerToHtml(controller); + }); + } + + String _controllerToHtml(QuillController controller) { + final ops = controller.document.toDelta().toJson(); + return QuillDeltaToHtmlConverter( + List>.from(ops), + ).convert(); + } + + void _applyToAllLanguages() { + if (_controllers.isEmpty) return; + final firstLang = widget.newValues.first.language!; + final html = _controllerToHtml(_controllers[firstLang]!); + + setState(() { + for (final translation in widget.newValues) { + translation.value = html; + if (translation.language != firstLang) { + _controllers[translation.language!]?.dispose(); + final controller = QuillController( + document: Document.fromJson(_htmlToDeltaJson(html)), + selection: const TextSelection.collapsed(offset: 0), + ); + _setupListener(translation.language!, controller); + _controllers[translation.language!] = controller; + } + } + }); + + showNotification(kSuccess, kWhite, 'Le texte a été appliqué à toutes les langues', context, null); + } + + @override + void dispose() { + for (final c in _controllers.values) { + c.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...widget.newValues.map((t) => _buildLanguageSection(t)), + const SizedBox(height: 8), + if (widget.resourceTypes == null) + Center( + child: SizedBox( + width: 370, + height: 70, + child: RoundedButton( + text: "Appliquer à toutes les langues", + icon: Icons.copy, + color: kSecond, + press: _applyToAllLanguages, + fontSize: 20, + ), + ), + ), + ], + ); + } + + Widget _buildLanguageSection(TranslationDTO translation) { + final lang = translation.language!; + + if (widget.resourceTypes != null) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLangHeader(lang), + const SizedBox(height: 4), + SizedBox( + width: 250, + height: 120, + child: ResourceInputContainer( + label: "", + initialValue: translation.value, + inResourceTypes: widget.resourceTypes!, + onChanged: (ResourceDTO resource) { + translation.value = resource.id; + }, + ), + ), + ], + ), + ); + } + + final controller = _controllers[lang]!; + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildLangHeader(lang), + const SizedBox(height: 4), + QuillSimpleToolbar( + controller: controller, + config: _buildToolbarConfig(), + ), + Container( + height: widget.isTitle ? 100 : 200, + decoration: BoxDecoration( + color: kBackgroundColor, + border: Border.all(color: Colors.grey.shade300), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(4), + bottomRight: Radius.circular(4), + ), + ), + child: QuillEditor.basic( + controller: controller, + config: const QuillEditorConfig( + scrollable: true, + expands: false, + padding: EdgeInsets.all(8), + autoFocus: false, + ), + ), + ), + ], + ), + ); + } + + Widget _buildLangHeader(String lang) { + return Row( + children: [ + FlagDecoration(language: lang), + const SizedBox(width: 8), + Text( + lang.toUpperCase(), + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14), + ), + ], + ); + } + + QuillSimpleToolbarConfig _buildToolbarConfig() { + return QuillSimpleToolbarConfig( + showBoldButton: true, + showItalicButton: true, + showColorButton: true, + showBackgroundColorButton: true, + showListBullets: !widget.isTitle, + showListNumbers: !widget.isTitle, + showListCheck: false, + showClearFormat: true, + showUnderLineButton: false, + showStrikeThrough: false, + showInlineCode: false, + showSubscript: false, + showSuperscript: false, + showSmallButton: false, + showLineHeightButton: false, + showHeaderStyle: false, + showLink: false, + showSearchButton: false, + showQuote: false, + showCodeBlock: false, + showIndent: false, + showAlignmentButtons: false, + showLeftAlignment: false, + showCenterAlignment: false, + showRightAlignment: false, + showJustifyAlignment: false, + showDirection: false, + showUndo: false, + showRedo: false, + showClipboardCut: false, + showClipboardCopy: false, + showClipboardPaste: false, + ); + } +} diff --git a/lib/Screens/Configurations/Section/SubSection/Agenda/agenda_config.dart b/lib/Screens/Configurations/Section/SubSection/Agenda/agenda_config.dart index 40d6ac4..27e92c2 100644 --- a/lib/Screens/Configurations/Section/SubSection/Agenda/agenda_config.dart +++ b/lib/Screens/Configurations/Section/SubSection/Agenda/agenda_config.dart @@ -4,6 +4,11 @@ import 'package:manager_app/constants.dart'; import 'package:manager_app/Components/check_input_container.dart'; import 'package:manager_app/Components/multi_string_input_container.dart'; import 'package:manager_app/Components/single_select_container.dart'; +import 'package:manager_app/Components/message_notification.dart'; +import 'package:provider/provider.dart'; +import 'package:manager_app/app_context.dart'; +import 'package:manager_app/Models/managerContext.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'showNewOrUpdateEventAgenda.dart'; class AgendaConfig extends StatefulWidget { @@ -26,11 +31,38 @@ class AgendaConfig extends StatefulWidget { class _AgendaConfigState extends State { late AgendaDTO agendaDTO; + List events = []; @override void initState() { super.initState(); agendaDTO = widget.initialValue; + WidgetsBinding.instance.addPostFrameCallback((_) => _loadFromApi()); + } + + Future _loadFromApi() async { + if (agendaDTO.id == null || !mounted) return; + final appContext = Provider.of(context, listen: false); + final api = (appContext.getContext() as ManagerAppContext) + .clientAPI! + .sectionAgendaApi!; + try { + final fetched = + await api.sectionAgendaGetAllEventAgendaFromSection(agendaDTO.id!); + if (fetched == null || !mounted) return; + setState(() { + events = List.from(fetched); + }); + } catch (e) { + // Silently keep empty on error + } + } + + SectionAgendaApi _api(BuildContext ctx) { + final appContext = Provider.of(ctx, listen: false); + return (appContext.getContext() as ManagerAppContext) + .clientAPI! + .sectionAgendaApi!; } @override @@ -51,6 +83,7 @@ class _AgendaConfigState extends State { } return Column( + mainAxisSize: MainAxisSize.min, children: [ _buildAgendaHeader(size, mapProviderIn), if (agendaDTO.isOnlineAgenda == false) ...[ @@ -67,22 +100,35 @@ class _AgendaConfigState extends State { ElevatedButton.icon( icon: const Icon(Icons.add), label: const Text("Ajouter un évènement"), - onPressed: () { - showNewOrUpdateEventAgenda( - context, - null, - agendaDTO.id ?? "temp", - (newEvent) { - setState(() { - agendaDTO.events = [ - ...(agendaDTO.events ?? []), - newEvent - ]; - widget.onChanged(agendaDTO); - }); - }, - ); - }, + onPressed: agendaDTO.id == null + ? null + : () { + showNewOrUpdateEventAgenda( + context, + null, + agendaDTO.id!, + (newEvent) async { + try { + final created = await _api(context) + .sectionAgendaCreateEventAgenda( + agendaDTO.id!, newEvent); + if (created != null && mounted) { + setState(() => events.add(created)); + showNotification(kSuccess, kWhite, + 'Évènement créé avec succès', context, null); + } + } catch (e) { + showNotification( + kError, + kWhite, + 'Erreur lors de la création de l\'évènement', + context, + null); + rethrow; + } + }, + ); + }, style: ElevatedButton.styleFrom( backgroundColor: kSuccess, foregroundColor: kWhite, @@ -98,14 +144,14 @@ class _AgendaConfigState extends State { Container( height: 600, padding: const EdgeInsets.symmetric(vertical: 8), - child: (agendaDTO.events == null || agendaDTO.events!.isEmpty) + child: events.isEmpty ? const Center( child: Text("Aucun évènement manuel", style: TextStyle(fontStyle: FontStyle.italic))) : ListView.builder( - itemCount: agendaDTO.events!.length, + itemCount: events.length, itemBuilder: (context, index) { - final event = agendaDTO.events![index]; + final event = events[index]; return Card( elevation: 2, margin: const EdgeInsets.symmetric( @@ -120,13 +166,13 @@ class _AgendaConfigState extends State { child: const Icon(Icons.event, color: kPrimaryColor), ), - title: Text( + title: HtmlWidget( (event.label != null && event.label!.isNotEmpty) ? (event.label!.firstWhere( (t) => t.language == 'FR', - orElse: () => event.label![0])).value! + orElse: () => event.label![0])).value ?? "Évènement $index" : "Évènement $index", - style: const TextStyle(fontWeight: FontWeight.bold), + textStyle: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Padding( padding: const EdgeInsets.only(top: 4), @@ -143,23 +189,61 @@ class _AgendaConfigState extends State { showNewOrUpdateEventAgenda( context, event, - agendaDTO.id ?? "temp", - (updatedEvent) { - setState(() { - agendaDTO.events![index] = updatedEvent; - widget.onChanged(agendaDTO); - }); + agendaDTO.id ?? "", + (updatedEvent) async { + try { + final result = await _api(context) + .sectionAgendaUpdateEventAgenda( + updatedEvent); + if (result != null && mounted) { + setState( + () => events[index] = result); + showNotification( + kSuccess, + kWhite, + 'Évènement mis à jour avec succès', + context, + null); + } + } catch (e) { + showNotification( + kError, + kWhite, + 'Erreur lors de la mise à jour de l\'évènement', + context, + null); + rethrow; + } }, ); }, ), IconButton( icon: const Icon(Icons.delete, color: kError), - onPressed: () { - setState(() { - agendaDTO.events!.removeAt(index); - widget.onChanged(agendaDTO); - }); + onPressed: () async { + try { + if (event.id != null) { + await _api(context) + .sectionAgendaDeleteEventAgenda( + event.id!); + } + if (mounted) { + setState(() => events.removeAt(index)); + showNotification( + kSuccess, + kWhite, + 'Évènement supprimé avec succès', + context, + null); + } + } catch (e) { + showNotification( + kError, + kWhite, + 'Erreur lors de la suppression de l\'évènement', + context, + null); + } }, ), ], diff --git a/lib/Screens/Configurations/Section/SubSection/Agenda/showNewOrUpdateEventAgenda.dart b/lib/Screens/Configurations/Section/SubSection/Agenda/showNewOrUpdateEventAgenda.dart index 32656f6..27488b7 100644 --- a/lib/Screens/Configurations/Section/SubSection/Agenda/showNewOrUpdateEventAgenda.dart +++ b/lib/Screens/Configurations/Section/SubSection/Agenda/showNewOrUpdateEventAgenda.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:manager_api_new/api.dart'; import 'package:manager_app/constants.dart'; @@ -13,7 +14,7 @@ void showNewOrUpdateEventAgenda( Function(EventAgendaDTO) onSave, ) { EventAgendaDTO workingEvent = event != null - ? EventAgendaDTO.fromJson(event.toJson())! + ? EventAgendaDTO.fromJson(jsonDecode(jsonEncode(event)))! : EventAgendaDTO( label: [], description: [], @@ -74,6 +75,7 @@ void showNewOrUpdateEventAgenda( setState(() => workingEvent.label = val), maxLines: 1, isTitle: true, + isHTML: true, ), ), SizedBox(width: 20), @@ -87,6 +89,7 @@ void showNewOrUpdateEventAgenda( () => workingEvent.description = val), maxLines: 5, isTitle: false, + isHTML: true, ), ), ], diff --git a/lib/Screens/Configurations/Section/SubSection/Event/event_config.dart b/lib/Screens/Configurations/Section/SubSection/Event/event_config.dart index fcacb8a..40d9de2 100644 --- a/lib/Screens/Configurations/Section/SubSection/Event/event_config.dart +++ b/lib/Screens/Configurations/Section/SubSection/Event/event_config.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:manager_api_new/api.dart'; import 'package:manager_app/constants.dart'; import 'package:intl/intl.dart'; @@ -50,6 +51,7 @@ class _EventConfigState extends State { @override Widget build(BuildContext context) { return Column( + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( @@ -198,11 +200,13 @@ class _EventConfigState extends State { null, (newBlock) async { try { - // The API expects ProgrammeBlockDTO, while eventDTO.programme is List - // Usually they are structural equivalents in these generated APIs, but let's be safe. - final programmeBlockDTO = - ProgrammeBlockDTO.fromJson(newBlock.toJson()); - if (programmeBlockDTO == null) return; + final programmeBlockDTO = ProgrammeBlockDTO( + id: newBlock.id, + title: newBlock.title, + description: newBlock.description, + startTime: newBlock.startTime, + endTime: newBlock.endTime, + ); final createdBlockDTO = await (appContext.getContext() as ManagerAppContext) @@ -212,24 +216,26 @@ class _EventConfigState extends State { eventDTO.id!, programmeBlockDTO); if (createdBlockDTO != null) { - // Convert back if necessary - final createdBlock = - ProgrammeBlock.fromJson(createdBlockDTO.toJson()); - if (createdBlock != null) { - setState(() { - eventDTO.programme = [ - ...(eventDTO.programme ?? []), - createdBlock - ]; - widget.onChanged(eventDTO); - }); - showNotification( - kSuccess, - kWhite, - 'Bloc de programme créé avec succès', - context, - null); - } + final createdBlock = ProgrammeBlock( + id: createdBlockDTO.id, + title: createdBlockDTO.title, + description: createdBlockDTO.description, + startTime: createdBlockDTO.startTime, + endTime: createdBlockDTO.endTime, + ); + setState(() { + eventDTO.programme = [ + ...(eventDTO.programme ?? []), + createdBlock + ]; + widget.onChanged(eventDTO); + }); + showNotification( + kSuccess, + kWhite, + 'Bloc de programme créé avec succès', + context, + null); } } catch (e) { showNotification( @@ -248,7 +254,8 @@ class _EventConfigState extends State { ], ), ), - Expanded( + Container( + height: 600, child: (eventDTO.programme == null || eventDTO.programme!.isEmpty) ? Center( child: Text("Aucun bloc de programme défini", @@ -260,16 +267,18 @@ class _EventConfigState extends State { return Card( margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ListTile( - title: Text( - block.title != null && block.title!.isNotEmpty - ? block.title! - .firstWhere((t) => t.language == 'FR', + title: block.title != null && block.title!.isNotEmpty + ? HtmlWidget( + block.title! + .firstWhere( + (t) => t.language == 'FR', orElse: () => block.title![0]) .value ?? - "Bloc ${index + 1}" - : "Bloc ${index + 1}"), + "Bloc ${index + 1}", + ) + : Text("Bloc ${index + 1}"), subtitle: Text( - "${block.startTime != null ? DateFormat('HH:mm').format(block.startTime!) : '??'} - ${block.endTime != null ? DateFormat('HH:mm').format(block.endTime!) : '??'}"), + "${block.startTime != null ? DateFormat('HH:mm').format(block.startTime!.toLocal()) : '??'} - ${block.endTime != null ? DateFormat('HH:mm').format(block.endTime!.toLocal()) : '??'}"), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -284,10 +293,13 @@ class _EventConfigState extends State { block, (updatedBlock) async { try { - final programmeBlockDTO = - ProgrammeBlockDTO.fromJson( - updatedBlock.toJson()); - if (programmeBlockDTO == null) return; + final programmeBlockDTO = ProgrammeBlockDTO( + id: updatedBlock.id, + title: updatedBlock.title, + description: updatedBlock.description, + startTime: updatedBlock.startTime, + endTime: updatedBlock.endTime, + ); final resultDTO = await (appContext.getContext() @@ -298,20 +310,23 @@ class _EventConfigState extends State { programmeBlockDTO); if (resultDTO != null) { - final result = ProgrammeBlock.fromJson( - resultDTO.toJson()); - if (result != null) { - setState(() { - eventDTO.programme![index] = result; - widget.onChanged(eventDTO); - }); - showNotification( - kSuccess, - kWhite, - 'Bloc mis à jour avec succès', - context, - null); - } + final result = ProgrammeBlock( + id: resultDTO.id, + title: resultDTO.title, + description: resultDTO.description, + startTime: resultDTO.startTime, + endTime: resultDTO.endTime, + ); + setState(() { + eventDTO.programme![index] = result; + widget.onChanged(eventDTO); + }); + showNotification( + kSuccess, + kWhite, + 'Bloc mis à jour avec succès', + context, + null); } } catch (e) { showNotification( diff --git a/lib/Screens/Configurations/Section/SubSection/Event/showNewOrUpdateProgrammeBlock.dart b/lib/Screens/Configurations/Section/SubSection/Event/showNewOrUpdateProgrammeBlock.dart index af11f66..3fef5fa 100644 --- a/lib/Screens/Configurations/Section/SubSection/Event/showNewOrUpdateProgrammeBlock.dart +++ b/lib/Screens/Configurations/Section/SubSection/Event/showNewOrUpdateProgrammeBlock.dart @@ -11,7 +11,13 @@ void showNewOrUpdateProgrammeBlock( Function(ProgrammeBlock) onSave, ) { ProgrammeBlock workingBlock = block != null - ? ProgrammeBlock.fromJson(block.toJson())! + ? ProgrammeBlock( + id: block.id, + title: List.from(block.title ?? []), + description: List.from(block.description ?? []), + startTime: block.startTime, + endTime: block.endTime, + ) : ProgrammeBlock( title: [], description: [], @@ -69,6 +75,7 @@ void showNewOrUpdateProgrammeBlock( setState(() => workingBlock.title = val), maxLines: 1, isTitle: true, + isHTML: true, ), ), SizedBox(width: 20), @@ -82,6 +89,7 @@ void showNewOrUpdateProgrammeBlock( () => workingBlock.description = val), maxLines: 1, isTitle: false, + isHTML: true, ), ), ], @@ -99,7 +107,7 @@ void showNewOrUpdateProgrammeBlock( subtitle: Text( workingBlock.startTime != null ? DateFormat('HH:mm') - .format(workingBlock.startTime!) + .format(workingBlock.startTime!.toLocal()) : "Non définie", style: TextStyle( color: kPrimaryColor, @@ -109,7 +117,7 @@ void showNewOrUpdateProgrammeBlock( TimeOfDay? time = await showTimePicker( context: context, initialTime: TimeOfDay.fromDateTime( - workingBlock.startTime ?? + workingBlock.startTime?.toLocal() ?? DateTime.now()), builder: (context, child) { return Theme( @@ -148,7 +156,7 @@ void showNewOrUpdateProgrammeBlock( subtitle: Text( workingBlock.endTime != null ? DateFormat('HH:mm') - .format(workingBlock.endTime!) + .format(workingBlock.endTime!.toLocal()) : "Non définie", style: TextStyle( color: kPrimaryColor, @@ -158,7 +166,7 @@ void showNewOrUpdateProgrammeBlock( TimeOfDay? time = await showTimePicker( context: context, initialTime: TimeOfDay.fromDateTime( - workingBlock.endTime ?? + workingBlock.endTime?.toLocal() ?? DateTime.now() .add(Duration(hours: 1))), builder: (context, child) { diff --git a/lib/Screens/Configurations/Section/SubSection/Parcours/parcours_config.dart b/lib/Screens/Configurations/Section/SubSection/Parcours/parcours_config.dart index bfeee7a..6fe1129 100644 --- a/lib/Screens/Configurations/Section/SubSection/Parcours/parcours_config.dart +++ b/lib/Screens/Configurations/Section/SubSection/Parcours/parcours_config.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:manager_api_new/api.dart'; import 'package:manager_app/constants.dart'; import 'package:manager_app/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedPath.dart'; @@ -40,15 +41,18 @@ class _ParcoursConfigState extends State { Future _loadFromApi() async { final appContext = Provider.of(context, listen: false); - final api = (appContext.getContext() as ManagerAppContext).clientAPI!.sectionMapApi!; + final api = (appContext.getContext() as ManagerAppContext) + .clientAPI! + .sectionMapApi!; try { // Backend already includes steps + quiz questions via .Include() - final fetchedPaths = await api.sectionMapGetAllGuidedPathFromSection(widget.parentId); + final fetchedPaths = + await api.sectionMapGetAllGuidedPathFromSection(widget.parentId); if (fetchedPaths == null || !mounted) return; fetchedPaths.sort((a, b) => (a.order ?? 0).compareTo(b.order ?? 0)); setState(() { - paths = fetchedPaths; + paths = List.from(fetchedPaths); }); } catch (e) { // Silently keep initial value on error @@ -81,7 +85,9 @@ class _ParcoursConfigState extends State { (newPath) async { try { newPath.order = paths.length; - newPath.instanceId = (appContext.getContext() as ManagerAppContext).instanceId; + newPath.instanceId = + (appContext.getContext() as ManagerAppContext) + .instanceId; if (widget.isEscapeMode) { newPath.sectionGameId = widget.parentId; } else if (widget.isEvent) { @@ -89,6 +95,7 @@ class _ParcoursConfigState extends State { } else { newPath.sectionMapId = widget.parentId; } + final createdPath = await (appContext.getContext() as ManagerAppContext) .clientAPI! @@ -97,10 +104,12 @@ class _ParcoursConfigState extends State { widget.parentId, newPath); if (createdPath != null) { - setState(() { - paths.add(createdPath); - widget.onChanged(paths); - }); + if (mounted) { + setState(() { + paths.add(createdPath); + widget.onChanged(paths); + }); + } showNotification(kSuccess, kWhite, 'Parcours créé avec succès', context, null); } @@ -111,6 +120,7 @@ class _ParcoursConfigState extends State { 'Erreur lors de la création du parcours', context, null); + rethrow; // Important so showNewOrUpdateGuidedPath knows it failed } }, ); @@ -169,13 +179,15 @@ class _ParcoursConfigState extends State { child: Text("${index + 1}"), backgroundColor: kPrimaryColor, foregroundColor: kWhite), - title: Text(path.title != null && path.title!.isNotEmpty - ? path.title! - .firstWhere((t) => t.language == 'FR', - orElse: () => path.title![0]) - .value ?? - "Parcours sans titre" - : "Parcours sans titre"), + title: path.title != null && path.title!.isNotEmpty + ? HtmlWidget( + path.title! + .firstWhere((t) => t.language == 'FR', + orElse: () => path.title![0]) + .value ?? + "Parcours sans titre", + ) + : Text("Parcours sans titre"), subtitle: Text("${path.steps?.length ?? 0} étapes"), trailing: Row( mainAxisSize: MainAxisSize.min, @@ -194,19 +206,20 @@ class _ParcoursConfigState extends State { widget.isEscapeMode, (updatedPath) async { try { + final api = (appContext.getContext() + as ManagerAppContext) + .clientAPI! + .sectionMapApi!; final result = - await (appContext.getContext() - as ManagerAppContext) - .clientAPI! - .sectionMapApi! - .sectionMapUpdateGuidedPath( - updatedPath); - + await api.sectionMapUpdateGuidedPath( + updatedPath); if (result != null) { - setState(() { - paths[index] = result; - widget.onChanged(paths); - }); + if (mounted) { + setState(() { + paths[index] = result; + widget.onChanged(paths); + }); + } showNotification( kSuccess, kWhite, @@ -221,6 +234,7 @@ class _ParcoursConfigState extends State { 'Erreur lors de la mise à jour du parcours', context, null); + rethrow; } }, ); diff --git a/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedPath.dart b/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedPath.dart index 1f0645f..fe2a41b 100644 --- a/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedPath.dart +++ b/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedPath.dart @@ -1,5 +1,7 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:manager_api_new/api.dart'; import 'package:manager_app/constants.dart'; import 'package:manager_app/Components/rounded_button.dart'; @@ -13,7 +15,7 @@ void showNewOrUpdateGuidedPath( String parentId, bool isEvent, bool isEscapeMode, - Function(GuidedPathDTO) onSave, + FutureOr Function(GuidedPathDTO) onSave, ) { GuidedPathDTO workingPath = path != null ? GuidedPathDTO.fromJson(jsonDecode(jsonEncode(path)))! @@ -24,8 +26,11 @@ void showNewOrUpdateGuidedPath( order: 0, ); + bool isSaving = false; + showDialog( context: context, + barrierDismissible: false, builder: (BuildContext context) { return StatefulBuilder( builder: (context, setState) { @@ -76,6 +81,7 @@ void showNewOrUpdateGuidedPath( setState(() => workingPath.title = val), maxLines: 1, isTitle: true, + isHTML: true, ), ), SizedBox(width: 20), @@ -89,6 +95,7 @@ void showNewOrUpdateGuidedPath( () => workingPath.description = val), maxLines: 1, isTitle: false, + isHTML: true, ), ), ], @@ -150,7 +157,7 @@ void showNewOrUpdateGuidedPath( null, workingPath.id ?? "temp", isEscapeMode, - (newStep) { + (newStep) async { setState(() { workingPath.steps = [ ...(workingPath.steps ?? []), @@ -183,7 +190,7 @@ void showNewOrUpdateGuidedPath( return ListTile( leading: CircleAvatar(child: Text("${index + 1}")), - title: Text( + title: HtmlWidget( step.title != null && step.title!.isNotEmpty ? step.title! .firstWhere( @@ -210,7 +217,7 @@ void showNewOrUpdateGuidedPath( step, workingPath.id ?? "temp", isEscapeMode, - (updatedStep) { + (updatedStep) async { setState(() { workingPath.steps![index] = updatedStep; @@ -255,8 +262,11 @@ void showNewOrUpdateGuidedPath( SizedBox( height: 46, child: RoundedButton( - text: "Sauvegarder", - press: () { + text: isSaving ? "Sauvegarde..." : "Sauvegarder", + icon: isSaving ? Icons.hourglass_empty : null, + press: () async { + if (isSaving) return; + setState(() => isSaving = true); // Initialise les booleans null → false workingPath.isLinear ??= false; workingPath.requireSuccessToAdvance ??= false; @@ -267,8 +277,12 @@ void showNewOrUpdateGuidedPath( s.isStepTimer ??= false; s.isStepLocked ??= false; } - onSave(workingPath); - Navigator.pop(context); + try { + await onSave(workingPath); + if (context.mounted) Navigator.pop(context); + } catch (e) { + setState(() => isSaving = false); + } }, color: kPrimaryColor, fontSize: 15, diff --git a/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedStep.dart b/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedStep.dart index 52e60d1..2568988 100644 --- a/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedStep.dart +++ b/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateGuidedStep.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:manager_api_new/api.dart'; @@ -13,7 +14,7 @@ void showNewOrUpdateGuidedStep( GuidedStepDTO? step, String pathId, bool isEscapeMode, - Function(GuidedStepDTO) onSave, + FutureOr Function(GuidedStepDTO) onSave, ) { // Use jsonEncode/jsonDecode for a robust deep copy that handles nested DTOs correctly GuidedStepDTO workingStep = step != null @@ -25,8 +26,11 @@ void showNewOrUpdateGuidedStep( order: 0, ); + bool isSaving = false; + showDialog( context: context, + barrierDismissible: false, builder: (BuildContext context) { return StatefulBuilder( builder: (context, setState) { @@ -73,6 +77,7 @@ void showNewOrUpdateGuidedStep( setState(() => workingStep.title = val), maxLines: 1, isTitle: true, + isHTML: true, ), ), SizedBox(width: 20), @@ -86,6 +91,7 @@ void showNewOrUpdateGuidedStep( () => workingStep.description = val), maxLines: 1, isTitle: false, + isHTML: true, ), ), ], @@ -231,15 +237,21 @@ void showNewOrUpdateGuidedStep( SizedBox( height: 46, child: RoundedButton( - text: "Sauvegarder", - press: () { + text: isSaving ? "Sauvegarde..." : "Sauvegarder", + icon: isSaving ? Icons.hourglass_empty : null, + press: () async { + if (isSaving) return; + setState(() => isSaving = true); // Initialise les booleans null → false - // pour éviter l'erreur backend "Error converting null to Boolean" workingStep.isHiddenInitially ??= false; workingStep.isStepTimer ??= false; workingStep.isStepLocked ??= false; - onSave(workingStep); - Navigator.pop(context); + try { + await onSave(workingStep); + if (context.mounted) Navigator.pop(context); + } catch (e) { + setState(() => isSaving = false); + } }, color: kPrimaryColor, fontSize: 15, diff --git a/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateQuizQuestion.dart b/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateQuizQuestion.dart index 729c4bc..69adcba 100644 --- a/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateQuizQuestion.dart +++ b/lib/Screens/Configurations/Section/SubSection/Parcours/showNewOrUpdateQuizQuestion.dart @@ -113,6 +113,7 @@ void showNewOrUpdateQuizQuestion( }), maxLines: 3, isTitle: false, + isHTML: true, ), SizedBox(height: 16), @@ -164,6 +165,7 @@ void showNewOrUpdateQuizQuestion( }), maxLines: 1, isTitle: true, + isHTML: true, ); }), SizedBox(height: 4), @@ -252,6 +254,7 @@ void showNewOrUpdateQuizQuestion( val)), maxLines: 1, isTitle: true, + isHTML: true, ), ), // Supprimer diff --git a/lib/Screens/Configurations/Section/section_detail_screen.dart b/lib/Screens/Configurations/Section/section_detail_screen.dart index bf5bde7..0f676a1 100644 --- a/lib/Screens/Configurations/Section/section_detail_screen.dart +++ b/lib/Screens/Configurations/Section/section_detail_screen.dart @@ -53,18 +53,40 @@ class _SectionDetailScreenState extends State { late SectionDTO sectionDTO; Object? sectionDetailDTO; String? lastLoadedSectionId; + final GlobalKey globalKey = GlobalKey(); + late Future _sectionFuture; + + Future _loadSection() { + final appContext = Provider.of(context, listen: false); + return getSection( + widget.id, (appContext.getContext() as ManagerAppContext).clientAPI!); + } + + @override + void initState() { + super.initState(); + _sectionFuture = _loadSection(); + } + + @override + void didUpdateWidget(SectionDetailScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.id != widget.id) { + sectionDetailDTO = null; + lastLoadedSectionId = null; + _sectionFuture = _loadSection(); + } + } @override Widget build(BuildContext context) { final appContext = Provider.of(context); Size size = MediaQuery.of(context).size; - GlobalKey globalKey = new GlobalKey(); Object? rawSectionData; return FutureBuilder( - future: getSection(widget.id, - (appContext.getContext() as ManagerAppContext).clientAPI!), + future: _sectionFuture, builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { rawSectionData = snapshot.data; @@ -82,9 +104,10 @@ class _SectionDetailScreenState extends State { } return Stack( + fit: StackFit.expand, children: [ bodySection( - rawSectionData, size, appContext, context, globalKey), + rawSectionData, size, appContext, context), Align( alignment: AlignmentDirectional.bottomCenter, child: Container( @@ -110,7 +133,7 @@ class _SectionDetailScreenState extends State { } Widget bodySection(Object? rawSectionDTO, Size size, AppContext appContext, - BuildContext context, GlobalKey globalKey) { + BuildContext context) { ManagerAppContext managerAppContext = appContext.getContext(); //SectionDTO? sectionDTO = SectionDTO.fromJson(rawSectionDTO); diff --git a/lib/main.dart b/lib/main.dart index 5c94295..33cd679 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,7 @@ import 'dart:ui'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_quill/flutter_quill.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:go_router/go_router.dart'; import 'package:manager_api_new/api.dart'; @@ -50,6 +52,14 @@ Future main() async { ChangeNotifierProvider( create: (_) => AppContext(managerAppContext), child: MaterialApp( + locale: const Locale('fr'), + supportedLocales: const [Locale('fr')], + localizationsDelegates: const [ + FlutterQuillLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], home: FutureBuilder( future: getInstanceInfo(managerAppContext), builder: (context, asyncSnapshot) { @@ -235,6 +245,14 @@ class _MyAppState extends State { const Breakpoint(start: 1921, end: double.infinity, name: '4K'), ], ), + locale: const Locale('fr'), + supportedLocales: const [Locale('fr')], + localizationsDelegates: const [ + FlutterQuillLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], scrollBehavior: MyCustomScrollBehavior(), debugShowCheckedModeBanner: false, title: 'MyInfoMate - Manager', diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 4d7b966..67a7172 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,6 +13,7 @@ import just_audio import package_info_plus import pasteboard import path_provider_foundation +import quill_native_bridge_macos import sqflite import url_launcher_macos import video_player_avfoundation @@ -27,6 +28,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) diff --git a/pubspec.lock b/pubspec.lock index bd8ab9d..6cf6a75 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "0816f12bbbd9e21f72ea8592b11bce4a628d4e5cb7a81ff9f1eee4f3dc23206e" + sha256: "50e24b769bd1e725732f0aff18b806b8731c1fbcf4e8018ab98e7c4805a2a52f" url: "https://pub.dev" source: hosted - version: "1.3.37" + version: "1.3.57" analyzer: dependency: transitive description: @@ -193,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -205,10 +213,10 @@ packages: dependency: transitive description: name: chewie - sha256: "1fc84d88d3b1dc26b1fe799500e2ebcc8916af30ce62595ad802cfd965b60bc3" + sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.7.5" cli_launcher: dependency: transitive description: @@ -249,6 +257,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" crypto: dependency: transitive description: @@ -273,6 +289,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_quill_delta: + dependency: transitive + description: + name: dart_quill_delta + sha256: bddb0b2948bd5b5a328f1651764486d162c59a8ccffd4c63e8b2c5e44be1dac4 + url: "https://pub.dev" + source: hosted + version: "10.8.3" dart_style: dependency: transitive description: @@ -297,6 +321,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.5" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" dio: dependency: transitive description: @@ -353,30 +385,46 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.1" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: fae4ab4317c2a7afb13d44ef1e3f9f28a630e10016bc5cfe761e8e6a0ed7816a + sha256: "5bba5924139e91d26446fd2601c18a6aa62c1161c768a989bb5e245dcdc20644" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.15.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "1003a5a03a61fc9a22ef49f37cbcb9e46c86313a7b2e7029b9390cf8c6fc32cb" + sha256: "8bcfad6d7033f5ea951d15b867622a824b13812178bfec0c779b9d81de011bbb" url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.4.2" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "6643fe3dbd021e6ccfb751f7882b39df355708afbdeb4130fc50f9305a9d1a3d" + sha256: eb3afccfc452b2b2075acbe0c4b27de62dd596802b4e5e19869c1e926cbb20b3 url: "https://pub.dev" source: hosted - version: "2.17.2" + version: "2.24.0" firebase_storage: dependency: "direct main" description: @@ -389,18 +437,18 @@ packages: dependency: transitive description: name: firebase_storage_platform_interface - sha256: "505dbaa4bceab07216c4e26cb1480bdb69ca03ab59bb7cb63f4681e2f7d5b23e" + sha256: ef0fc49be3dc96ebaf6965ec591c124256a1d9f252db31a06ceaa580a72e07a6 url: "https://pub.dev" source: hosted - version: "5.1.24" + version: "5.2.8" firebase_storage_web: dependency: transitive description: name: firebase_storage_web - sha256: d3e6f27be96c7ebdf38a43520f19584e55dc45c71f616aa86f135dba97d63a8e + sha256: "0b263674a6b4e9a6eeea95873e63cd96d8e7c2bd5cc7aaa57f46d906f61214cb" url: "https://pub.dev" source: hosted - version: "3.9.9" + version: "3.10.15" fixnum: dependency: transitive description: @@ -430,6 +478,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.2" + flutter_colorpicker: + dependency: transitive + description: + name: flutter_colorpicker + sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter_compass: dependency: transitive description: @@ -446,6 +502,51 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_temp_fork: + dependency: transitive + description: + name: flutter_keyboard_visibility_temp_fork + sha256: e3d02900640fbc1129245540db16944a0898b8be81694f4bf04b6c985bed9048 + url: "https://pub.dev" + source: hosted + version: "0.1.5" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_map: dependency: "direct main" description: @@ -478,6 +579,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.20" + flutter_quill: + dependency: "direct main" + description: + name: flutter_quill + sha256: b96bb8525afdeaaea52f5d02f525e05cc34acd176467ab6d6f35d434cf14fde2 + url: "https://pub.dev" + source: hosted + version: "11.5.0" + flutter_quill_delta_from_html: + dependency: "direct main" + description: + name: flutter_quill_delta_from_html + sha256: "0eb801ea8dd498cadc057507af5da794d4c9599ce58b2569cb3d4bb53ba8bed2" + url: "https://pub.dev" + source: hosted + version: "1.5.3" flutter_svg: dependency: "direct main" description: @@ -644,10 +761,18 @@ packages: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.6" + html_unescape: + dependency: transitive + description: + name: html_unescape + sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" + url: "https://pub.dev" + source: hosted + version: "2.0.0" http: dependency: transitive description: @@ -681,13 +806,13 @@ packages: source: hosted version: "4.2.0" intl: - dependency: transitive + dependency: "direct overridden" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -807,6 +932,14 @@ packages: relative: true source: path version: "1.0.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + url: "https://pub.dev" + source: hosted + version: "7.3.0" matcher: dependency: transitive description: @@ -1047,38 +1180,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointer_interceptor: - dependency: transitive - description: - name: pointer_interceptor - sha256: d0a8e660d1204eaec5bd34b34cc92174690e076d2e4f893d9d68c486a13b07c4 - url: "https://pub.dev" - source: hosted - version: "0.10.1+1" - pointer_interceptor_ios: - dependency: transitive - description: - name: pointer_interceptor_ios - sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917 - url: "https://pub.dev" - source: hosted - version: "0.10.1" - pointer_interceptor_platform_interface: - dependency: transitive - description: - name: pointer_interceptor_platform_interface - sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" - url: "https://pub.dev" - source: hosted - version: "0.10.0+1" - pointer_interceptor_web: - dependency: transitive - description: - name: pointer_interceptor_web - sha256: a6237528b46c411d8d55cdfad8fcb3269fc4cbb26060b14bff94879165887d1e - url: "https://pub.dev" - source: hosted - version: "0.10.2" pointycastle: dependency: transitive description: @@ -1151,14 +1252,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" - quill_html_editor: - dependency: "direct main" + quill_native_bridge: + dependency: transitive description: - name: quill_html_editor - sha256: "7f46ec06a41b8ec30910afc219f1a447cd2e872614cc3a34ef16600043163f19" + name: quill_native_bridge + sha256: "76a16512e398e84216f3f659f7cb18a89ec1e141ea908e954652b4ce6cf15b18" url: "https://pub.dev" source: hosted - version: "2.2.8" + version: "11.1.0" + quill_native_bridge_android: + dependency: transitive + description: + name: quill_native_bridge_android + sha256: b75c7e6ede362a7007f545118e756b1f19053994144ec9eda932ce5e54a57569 + url: "https://pub.dev" + source: hosted + version: "0.0.1+2" + quill_native_bridge_ios: + dependency: transitive + description: + name: quill_native_bridge_ios + sha256: d23de3cd7724d482fe2b514617f8eedc8f296e120fb297368917ac3b59d8099f + url: "https://pub.dev" + source: hosted + version: "0.0.1" + quill_native_bridge_macos: + dependency: transitive + description: + name: quill_native_bridge_macos + sha256: "1c0631bd1e2eee765a8b06017c5286a4e829778f4585736e048eb67c97af8a77" + url: "https://pub.dev" + source: hosted + version: "0.0.1" + quill_native_bridge_platform_interface: + dependency: transitive + description: + name: quill_native_bridge_platform_interface + sha256: "8264a2bdb8a294c31377a27b46c0f8717fa9f968cf113f7dc52d332ed9c84526" + url: "https://pub.dev" + source: hosted + version: "0.0.2+1" + quill_native_bridge_web: + dependency: transitive + description: + name: quill_native_bridge_web + sha256: "7c723f6824b0250d7f33e8b6c23f2f8eb0103fe48ee7ebf47ab6786b64d5c05d" + url: "https://pub.dev" + source: hosted + version: "0.0.2" + quill_native_bridge_windows: + dependency: transitive + description: + name: quill_native_bridge_windows + sha256: "3f96ced19e3206ddf4f6f7dde3eb16bdd05e10294964009ea3a806d995aa7caa" + url: "https://pub.dev" + source: hosted + version: "0.0.2" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" responsive_framework: dependency: "direct main" description: @@ -1376,10 +1533,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" url_launcher_windows: dependency: transitive description: @@ -1464,10 +1621,10 @@ packages: dependency: transitive description: name: video_player_web - sha256: ff4d69a6614b03f055397c27a71c9d3ddea2b2a23d71b2ba0164f59ca32b8fe2 + sha256: "881b375a934d8ebf868c7fb1423b2bfaa393a0a265fa3f733079a86536064a10" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.3" vm_service: dependency: transitive description: @@ -1476,14 +1633,22 @@ packages: url: "https://pub.dev" source: hosted version: "14.3.0" + vsc_quill_delta_to_html: + dependency: "direct main" + description: + name: vsc_quill_delta_to_html + sha256: "9aca60d53ed1b700e922dabff8cd8b3490daecbf99c258f19eb45820b794fa45" + url: "https://pub.dev" + source: hosted + version: "1.0.5" wakelock_plus: dependency: transitive description: name: wakelock_plus - sha256: "104d94837bb28c735894dcd592877e990149c380e6358b00c04398ca1426eed4" + sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.1.4" wakelock_plus_platform_interface: dependency: transitive description: @@ -1504,26 +1669,26 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.1" web_socket: dependency: transitive description: name: web_socket - sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276 + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.3" webview_flutter: dependency: transitive description: @@ -1608,10 +1773,10 @@ packages: dependency: transitive description: name: youtube_player_iframe_web - sha256: "743e8ce2ee77b8ea6e36ae986f0ef111b38c3119ae55ae813223ecddeb56b770" + sha256: "05222a228937932e7ee7a6171e8020fee4cd23d1c7bf6b4128c569484338c593" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.1.1" sdks: - dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.29.0" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.32.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7427c36..e2fa815 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,7 +69,9 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 - quill_html_editor: ^2.2.8 + flutter_quill: ^11.5.0 + vsc_quill_delta_to_html: ^1.0.5 + flutter_quill_delta_from_html: ^1.5.3 responsive_framework: ^1.4.0 tab_container: ^2.0.0 flutter_widget_from_html: ^0.15.3 @@ -77,6 +79,9 @@ dependencies: firebase_core: ^3.1.0 #another_flushbar: ^1.12.30 +dependency_overrides: + intl: ^0.20.2 + dev_dependencies: flutter_test: sdk: flutter diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d94640c..31c2687 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); FirebaseStoragePluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index a7d12b1..8622dad 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows firebase_core firebase_storage geolocator_windows