From f4a952f5cf5ceffa70c5364c1cdf3f0669220803 Mon Sep 17 00:00:00 2001 From: miakh <2659269+miakh@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:27:45 +0200 Subject: [PATCH] convert test --- lib/pages/HtmlEditorPage.dart | 295 ++++++++++++++++++++++++++++++--- lib/widgets/ButtonsHelper.dart | 2 +- 2 files changed, 272 insertions(+), 25 deletions(-) diff --git a/lib/pages/HtmlEditorPage.dart b/lib/pages/HtmlEditorPage.dart index 0eabf35c..41cf17a1 100644 --- a/lib/pages/HtmlEditorPage.dart +++ b/lib/pages/HtmlEditorPage.dart @@ -1,11 +1,18 @@ +import 'dart:convert'; +import 'dart:typed_data'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:fstapp/RouterService.dart'; import 'package:fstapp/dataServices/DataExtensions.dart'; import 'package:fstapp/styles/Styles.dart'; import 'package:fstapp/widgets/ButtonsHelper.dart'; import 'package:fstapp/widgets/HtmlEditorWidget.dart'; import 'package:quill_html_editor/quill_html_editor.dart'; -import 'package:flutter/material.dart'; +import 'package:html/parser.dart' as html_parser; +import 'package:image/image.dart' as img; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:http/http.dart' as http; + class HtmlEditorPage extends StatefulWidget { static const String parContent = "content"; @@ -23,6 +30,8 @@ class _HtmlEditorPageState extends State { String? _originalHtml; bool _isTextSet = false; bool _isContentLoading = false; + bool _isSaving = false; + double _progress = 0.0; // Progress indicator for image processing Map? parameters; late QuillEditorController controller; @@ -34,12 +43,14 @@ class _HtmlEditorPageState extends State { _originalHtml = widget.content?[HtmlEditorPage.parContent]; } - controller = QuillEditorController(); var firstLoad = (t) async { - //if function before content loaded, then it will be set via function inside _loadHtmlContent - if(_isContentLoading) {_isTextSet = true;} - if(_isTextSet){return;} + if (_isContentLoading) { + _isTextSet = true; + } + if (_isTextSet) { + return; + } await setHtmlText(_originalHtml ?? _html); _isTextSet = true; }; @@ -80,18 +91,18 @@ class _HtmlEditorPageState extends State { resizeToAvoidBottomInset: true, body: Stack( children: [ - Align( - alignment: Alignment.topCenter, - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: appMaxWidth), - child: HtmlEditorWidget( - initialContent: _html, - controller: controller, - onTextChanged: (text) => debugPrint('listening to $text'), + if (!_isSaving) // Display the editor only if not saving + Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: appMaxWidth), + child: HtmlEditorWidget( + initialContent: _html, + controller: controller, + ), ), ), - ), - if (_isContentLoading) + if (_isContentLoading) // Display loading indicator when loading Container( color: Colors.black54, child: Center( @@ -100,7 +111,8 @@ class _HtmlEditorPageState extends State { ), ], ), - bottomNavigationBar: Container( + bottomNavigationBar: !_isSaving // Hide the buttons if saving + ? Container( width: double.maxFinite, color: Colors.grey.shade200, child: Wrap( @@ -108,29 +120,264 @@ class _HtmlEditorPageState extends State { children: [ ButtonsHelper.bottomBarButton( text: "Reset".tr(), - onPressed: () async { + onPressed: _isSaving + ? null + : () async { await setHtmlText(_originalHtml ?? _html); }, ), ButtonsHelper.bottomBarButton( text: "Storno".tr(), - onPressed: cancelPressed, + onPressed: _isSaving ? null : cancelPressed, ), ButtonsHelper.bottomBarButton( text: "Save".tr(), - onPressed: savePressed, + onPressed: _isSaving ? null : savePressed, ), ], ), - ), + ) + : null, ), ); } - void savePressed() async { - String? htmlTextEdited = await controller.getText(); - var htmlText = htmlTextEdited.removeBackgroundColor(); - RouterService.goBack(context, htmlText); + List detectImagesToProcess(String htmlText) { + final document = html_parser.parse(htmlText); + final images = document.getElementsByTagName('img'); + List imagesToProcess = []; + + for (var image in images) { + String? src = image.attributes['src']; + if (src != null && (src.startsWith('data:image/jpeg;base64,') || src.startsWith('data:image/png;base64,'))) { + String base64Data = src.contains('data:image/jpeg;base64,') + ? src.replaceFirst('data:image/jpeg;base64,', '') + : src.replaceFirst('data:image/png;base64,', ''); + + Uint8List imageData = base64.decode(base64Data); + + if (src.startsWith('data:image/jpeg;base64,')) { + if (imageData.length > 1000000) { // Size > 1MB + imagesToProcess.add(src); + } + } else if (src.startsWith('data:image/png;base64,')) { + img.Image? originalImage = img.decodeImage(imageData); + if (originalImage != null && originalImage.width > 1200) { + imagesToProcess.add(src); + } + } + } + } + return imagesToProcess; + } + + Future compressImages(String htmlText, List imagesToProcess) async { + final document = html_parser.parse(htmlText); + final images = document.getElementsByTagName('img'); + + int processedCount = 0; + + for (var image in images) { + String? src = image.attributes['src']; + if (src != null && imagesToProcess.contains(src)) { + String base64Data = src.contains('data:image/jpeg;base64,') + ? src.replaceFirst('data:image/jpeg;base64,', '') + : src.replaceFirst('data:image/png;base64,', ''); + + Uint8List imageData = base64.decode(base64Data); + + Uint8List compressedData; + + if (src.startsWith('data:image/jpeg;base64,')) { + if (imageData.length > 1000000) { + compressedData = compressJpeg(imageData, 1200); + String compressedBase64 = base64.encode(compressedData); + image.attributes['src'] = 'data:image/jpeg;base64,$compressedBase64'; + } + } else if (src.startsWith('data:image/png;base64,')) { + img.Image? originalImage = img.decodeImage(imageData); + if (originalImage != null && originalImage.width > 1200) { + compressedData = compressPng(imageData, 1200); + String compressedBase64 = base64.encode(compressedData); + image.attributes['src'] = 'data:image/png;base64,$compressedBase64'; + } + } + + // Update progress + processedCount++; + setState(() { + _progress = processedCount / imagesToProcess.length; + }); + + // Delay to show progress (simulated delay, remove in production) + await Future.delayed(Duration(milliseconds: 200)); + } + } + + return document.outerHtml; + } + + Uint8List compressJpeg(Uint8List imageData, int width, {int quality = 85}) { + final img.Image? originalImage = img.decodeImage(imageData); + if (originalImage == null) { + throw Exception("Failed to decode JPEG image"); + } + final img.Image resizedImage = img.copyResize(originalImage, width: width); + return Uint8List.fromList(img.encodeJpg(resizedImage, quality: quality)); + } + + Uint8List compressPng(Uint8List imageData, int width) { + final img.Image? originalImage = img.decodeImage(imageData); + if (originalImage == null) { + throw Exception("Failed to decode PNG image"); + } + final img.Image resizedImage = img.copyResize(originalImage, width: width); + return Uint8List.fromList(img.encodePng(resizedImage)); + } + + // Future compressJpegWithSupabase(Uint8List imageData, int width, {int quality = 85}) async { + // final supabase = Supabase.instance.client; + // final bucketName = 'your-bucket-name'; + // final path = 'uploads/${DateTime.now().millisecondsSinceEpoch}.jpg'; + // + // // Upload the image to Supabase Storage + // final uploadResponse = await supabase.storage.from(bucketName).uploadBinary(path, imageData); + // if (uploadResponse.isNotEmpty) { + // throw Exception('Error uploading image: ${uploadResponse.toString()}'); + // } + // + // // Get the URL with the transformation parameters + // final transformedUrl = supabase.storage + // .from(bucketName) + // .getPublicUrl(path) + // .replaceAll('your-bucket-name', 'your-bucket-name/image/resize') + // .replaceAll('storage/v1', 'storage/v1/render/image') + + // '?width=$width&quality=$quality'; + // + // // Download the transformed image + // final response = await http.get(Uri.parse(transformedUrl)); + // if (response.statusCode != 200) { + // throw Exception('Failed to fetch transformed image'); + // } + // + // // Delete the original image from Supabase Storage + // final deleteResponse = await supabase.storage.from(bucketName).remove([path]); + // if (deleteResponse.isNotEmpty) { + // throw Exception('Error deleting image: ${deleteResponse.toString()}'); + // } + // + // return response.bodyBytes; + // } + // + // Future compressPngWithSupabase(Uint8List imageData, int width) async { + // final supabase = Supabase.instance.client; + // final bucketName = 'your-bucket-name'; + // final path = 'uploads/${DateTime.now().millisecondsSinceEpoch}.png'; + // + // // Upload the image to Supabase Storage + // final uploadResponse = await supabase.storage.from(bucketName).uploadBinary(path, imageData); + // if (uploadResponse.isNotEmpty) { + // throw Exception('Error uploading image: ${uploadResponse.toString()}'); + // } + // + // // Get the URL with the transformation parameters + // final transformedUrl = supabase.storage + // .from(bucketName) + // .getPublicUrl(path) + // .replaceAll('your-bucket-name', 'your-bucket-name/image/resize') + // .replaceAll('storage/v1', 'storage/v1/render/image') + + // '?width=$width&format=png'; + // + // // Download the transformed image + // final response = await http.get(Uri.parse(transformedUrl)); + // if (response.statusCode != 200) { + // throw Exception('Failed to fetch transformed image'); + // } + // + // // Delete the original image from Supabase Storage + // final deleteResponse = await supabase.storage.from(bucketName).remove([path]); + // if (deleteResponse.isNotEmpty) { + // throw Exception('Error deleting image: ${deleteResponse.toString()}'); + // } + // + // return response.bodyBytes; + // } + + Future savePressed() async { + setState(() { + _isSaving = true; // Set saving state to true, remove editor and show loading + }); + + try { + String? htmlTextEdited = await controller.getText(); + var htmlText = htmlTextEdited.removeBackgroundColor(); + + // Detect images that need processing + List imagesToProcess = detectImagesToProcess(htmlText); + bool hasLargeImages = imagesToProcess.isNotEmpty; + + if (hasLargeImages) { + bool compress = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text("Large Images Detected"), + content: Text("Some images are large and may slow down the app. Do you want to compress them?"), + actions: [ + TextButton( + child: Text("No"), + onPressed: () => Navigator.of(context).pop(false), + ), + TextButton( + child: Text("Yes"), + onPressed: () => Navigator.of(context).pop(true), + ), + ], + ), + ); + + if (compress) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + Future.delayed(Duration(milliseconds: 200), () async { + htmlText = await compressImages(htmlText, imagesToProcess); + Navigator.of(context).pop(); // Close the progress dialog + RouterService.goBack(context, htmlText); + }); + + return PopScope( + canPop: false, // Disable back button + child: Dialog( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Processing Images...'), + SizedBox(height: 20), + LinearProgressIndicator(value: _progress), + SizedBox(height: 10), + Text( + '${(_progress * imagesToProcess.length).toInt()} / ${imagesToProcess.length}', + style: TextStyle(fontSize: 16), + ), + ], + ), + ), + ), + ); + }, + ); + } + } else { + RouterService.goBack(context, htmlText); + } + } finally { + setState(() { + _isSaving = false; // Set saving state to false after completing save + }); + } } void cancelPressed() async { diff --git a/lib/widgets/ButtonsHelper.dart b/lib/widgets/ButtonsHelper.dart index 71680325..5fb91832 100644 --- a/lib/widgets/ButtonsHelper.dart +++ b/lib/widgets/ButtonsHelper.dart @@ -65,7 +65,7 @@ class ButtonsHelper { ); } - static Widget bottomBarButton({required String text, required VoidCallback onPressed}) { + static Widget bottomBarButton({required String text, required VoidCallback? onPressed}) { return Padding( padding: const EdgeInsets.all(8.0), child: MaterialButton(