From 1cda7879089ace2af20ed5115e4fb601820b1360 Mon Sep 17 00:00:00 2001 From: realth000 Date: Tue, 6 Aug 2024 05:00:49 +0800 Subject: [PATCH] feat(editor): Autofill image size Fill inserted image bbcode size with image real size. --- CHANGELOG.md | 1 + lib/features/editor/widgets/image_dialog.dart | 227 ++++++++++++------ lib/features/editor/widgets/rich_editor.dart | 3 + lib/features/editor/widgets/toolbar.dart | 3 + lib/i18n/strings.i18n.json | 4 +- lib/i18n/strings_zh-CN.i18n.json | 4 +- lib/i18n/strings_zh-TW.i18n.json | 4 +- packages/flutter_bbcode_editor | 2 +- 8 files changed, 173 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fddc737a..cab6a645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - 展开状态下显示所有支持的bbcode格式,输入框不限制最大外观高度。 - 增加将编辑器变回默认状态的按钮。 - 在图片上显示设置的图片宽高。 + - 插入图片时自动填写图片宽高。 - 用户:用户页面向下滚动到用户名隐藏时,在顶部显示用户名。 - 分区:支持显示在帖子中最后回复的用户。 - 设置:支持设置帖子卡片的外观,包括对齐方式和是否显示最后回复的用户。 diff --git a/lib/features/editor/widgets/image_dialog.dart b/lib/features/editor/widgets/image_dialog.dart index 156cd0b9..a7a8e8cb 100644 --- a/lib/features/editor/widgets/image_dialog.dart +++ b/lib/features/editor/widgets/image_dialog.dart @@ -1,32 +1,45 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bbcode_editor/flutter_bbcode_editor.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:tsdm_client/constants/layout.dart'; import 'package:tsdm_client/extensions/list.dart'; +import 'package:tsdm_client/features/cache/repository/image_cache_repository.dart'; import 'package:tsdm_client/generated/i18n/strings.g.dart'; - -const _maxAllowedWidth = 160; -const _maxAllowedHeight = 90; -// const _ratio = _maxAllowedWidth / _maxAllowedHeight; -const _defaultWidth = _maxAllowedWidth; -const _defaultHeight = _maxAllowedHeight; +import 'package:tsdm_client/instance.dart'; +import 'package:tsdm_client/shared/providers/image_cache_provider/image_cache_provider.dart'; +import 'package:tsdm_client/utils/debug.dart'; /// Show a picture dialog to add picture into editor. -Future showImageDialog( - BuildContext context, - BBCodeEditorController controller, -) async => - showDialog( +Future showImagePicker( + BuildContext context, { + String? url, + int? width, + int? height, +}) async => + showDialog( context: context, - builder: (context) => _ImageDialog(controller), + builder: (context) => _ImageDialog( + url: url, + width: width, + height: height, + ), ); /// Show a dialog to insert picture and description. class _ImageDialog extends StatefulWidget { - const _ImageDialog(this.bbCodeEditorController); + const _ImageDialog({ + required this.url, + required this.width, + required this.height, + }); - final BBCodeEditorController bbCodeEditorController; + final String? url; + final int? width; + final int? height; @override State<_ImageDialog> createState() => _ImageDialogState(); @@ -34,9 +47,76 @@ class _ImageDialog extends StatefulWidget { class _ImageDialogState extends State<_ImageDialog> { final formKey = GlobalKey(); - final imageUrlController = TextEditingController(); - final widthController = TextEditingController(text: '$_defaultWidth'); - final heightController = TextEditingController(text: '$_defaultHeight'); + final urlFieldKey = GlobalKey>(); + + late final TextEditingController urlController; + late final TextEditingController widthController; + late final TextEditingController heightController; + + /// Flag indicating automatically fill image size. + /// + /// This is achieved by downloading the image cache and calculate its size. + /// Only fill the image size in bbcode (if size enabled): + /// + /// ```console + /// [img=$WIDTH,$HEIGHT}$IMAGE_URL[/img] + /// ``` + /// + /// Usually this width/height equals to original image size, which means size + /// overflow is not considered here, because the server only render images in + /// acceptable width: + /// + /// Now the max image width is 550. + /// + /// * If image width is no larger than max image width, both original width + /// and height are used. + /// * If image width is larger than the max image width, set rendered width to + /// max image width and adjust height to MAX_IMAGE_WIDTH / ORIGINAL_WIDTH * + /// ORIGINAL HEIGHT, this keeps the same image width/height ratio and limit + /// image width to max image width. + /// + /// So fill with original image size is fine. + bool autoFillSize = true; + + /// Flag indicating in auto-fill-size progress. + bool fillingSize = false; + + Future _fillImageSize(BuildContext context, String url) async { + try { + final cacheInfo = getIt.get().getCacheInfo(url); + if (cacheInfo == null) { + // Not cached + // FIXME: SO CONFUSING + await context.read().updateImageCache(url); + } + final imageData = await getIt.get().getCache(url); + final uiImage = await decodeImageFromList(imageData); + setState(() { + widthController.text = '${uiImage.width}'; + heightController.text = '${uiImage.height}'; + }); + } catch (e, _) { + // Directly cache Future.error. + debug('[editor] failed to fill image size: invalid image: $e'); + return; + } + } + + @override + void initState() { + super.initState(); + urlController = TextEditingController(text: widget.url); + widthController = TextEditingController(text: '${widget.width ?? ""}'); + heightController = TextEditingController(text: '${widget.height ?? ""}'); + } + + @override + void dispose() { + urlController.dispose(); + widthController.dispose(); + heightController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -50,13 +130,32 @@ class _ImageDialogState extends State<_ImageDialog> { mainAxisSize: MainAxisSize.min, children: [ TextFormField( - controller: imageUrlController, + key: urlFieldKey, + controller: urlController, autofocus: true, decoration: InputDecoration( prefixIcon: const Icon(Icons.image_outlined), labelText: tr.link, ), validator: (v) => v!.trim().isNotEmpty ? null : tr.errorEmpty, + onChanged: (v) async { + // Try fill image size from image file. + if (!autoFillSize) { + return; + } + final cs = urlFieldKey.currentState; + if (cs == null || !cs.validate()) { + return; + } + // Try get image size when image url changes. + setState(() { + fillingSize = true; + }); + await _fillImageSize(context, v); + setState(() { + fillingSize = false; + }); + }, ), TextFormField( controller: widthController, @@ -90,7 +189,7 @@ class _ImageDialogState extends State<_ImageDialog> { ), ], decoration: InputDecoration( - prefixIcon: const Icon(Icons.vertical_distribute_outlined), + prefixIcon: const Icon(Icons.add), labelText: tr.height, ), validator: (v) { @@ -104,62 +203,48 @@ class _ImageDialogState extends State<_ImageDialog> { return null; }, ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - child: Text(context.t.general.cancel), - onPressed: () => context.pop(), - ), - TextButton( - child: Text(context.t.general.ok), - onPressed: () async { - // if (formKey.currentState == null || - // !(formKey.currentState!).validate()) { - // return; - // } - // final String url; - // if (imageUrlController.text.startsWith('http://') || - // imageUrlController.text.startsWith('https://')) { - // url = imageUrlController.text; - // } else { - // url = 'https://${imageUrlController.text}'; - // } - // final width = int.parse(widthController.text); - // final height = int.parse(heightController.text); - // assert(height != 0, 'image height should not be zero'); - // final actualRatio = width / height; - - // final double displayWidth; - // final double displayHeight; - // if (actualRatio <= _ratio) { - // // Image size is more in height. - // displayWidth = width * height / _maxAllowedHeight; - // displayHeight = _maxAllowedHeight.toDouble(); - // } else { - // // Image size is more in width. - // displayWidth = _maxAllowedWidth.toDouble(); - // displayHeight = height * width / _maxAllowedWidth; - // } - // // TODO: Implement - // // await widget.bbCodeEditorController.insertImage( - // // url: url, - // // width: width, - // // height: height, - // // displayWith: displayWidth, - // // displayHeight: displayHeight, - // // ); - // if (!context.mounted) { - // return; - // } - // context.pop(); - }, - ), - ], + SwitchListTile( + title: Text(tr.autoFillSize), + subtitle: Text(tr.autoFillSizeDetail), + value: autoFillSize, + onChanged: (v) { + setState(() { + autoFillSize = v; + }); + }, ), ].insertBetween(sizedBoxW10H10), ), ), + actions: [ + if (fillingSize) sizedCircularProgressIndicator, + TextButton( + child: Text(context.t.general.cancel), + onPressed: () => context.pop(), + ), + TextButton( + child: Text(context.t.general.ok), + onPressed: () async { + if (formKey.currentState == null || + !(formKey.currentState!).validate()) { + return; + } + + final width = int.parse(widthController.text); + final height = int.parse(heightController.text); + assert(width != 0, 'image width should >= zero'); + assert(height != 0, 'image height should >= zero'); + + context.pop( + BBCodeImageInfo( + urlController.text, + width: width, + height: height, + ), + ); + }, + ), + ], ); } } diff --git a/lib/features/editor/widgets/rich_editor.dart b/lib/features/editor/widgets/rich_editor.dart index 8683ee58..b65710c3 100644 --- a/lib/features/editor/widgets/rich_editor.dart +++ b/lib/features/editor/widgets/rich_editor.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bbcode_editor/flutter_bbcode_editor.dart'; import 'package:tsdm_client/constants/url.dart'; import 'package:tsdm_client/extensions/build_context.dart'; +import 'package:tsdm_client/features/editor/widgets/image_dialog.dart'; import 'package:tsdm_client/instance.dart'; import 'package:tsdm_client/shared/providers/image_cache_provider/image_cache_provider.dart'; import 'package:tsdm_client/widgets/cached_image/cached_image.dart'; @@ -67,6 +68,8 @@ class RichEditor extends StatelessWidget { '$usernameProfilePage$username', ), imageConstraints: const BoxConstraints(maxWidth: 100, maxHeight: 100), + imagePicker: (context, url, width, height) => + showImagePicker(context, url: url, width: width, height: height), ); } } diff --git a/lib/features/editor/widgets/toolbar.dart b/lib/features/editor/widgets/toolbar.dart index d51d1046..4b99a87f 100644 --- a/lib/features/editor/widgets/toolbar.dart +++ b/lib/features/editor/widgets/toolbar.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bbcode_editor/flutter_bbcode_editor.dart'; import 'package:tsdm_client/features/editor/widgets/color_bottom_sheet.dart'; import 'package:tsdm_client/features/editor/widgets/emoji_bottom_sheet.dart'; +import 'package:tsdm_client/features/editor/widgets/image_dialog.dart'; import 'package:tsdm_client/features/editor/widgets/url_dialog.dart'; /// Representing all features types. @@ -337,6 +338,8 @@ class _EditorToolbarState extends State { urlPicker: (context, url, description) async => showUrlPicker(context, url: url, description: description), backgroundColorPicker: (context) async => showColorPicker(context), + imagePicker: (context, url, width, height) => + showImagePicker(context, url: url, width: width, height: height), // Features. showUndo: hasFeature(EditorFeatures.undo), showRedo: hasFeature(EditorFeatures.redo), diff --git a/lib/i18n/strings.i18n.json b/lib/i18n/strings.i18n.json index 3fd9201f..fcd0d6a1 100644 --- a/lib/i18n/strings.i18n.json +++ b/lib/i18n/strings.i18n.json @@ -537,7 +537,9 @@ "errorEmpty": "Should not be empty", "width": "Width", "height": "Height", - "errorInvalidNumber": "Invalid number" + "errorInvalidNumber": "Invalid number", + "autoFillSize": "Auto fill size", + "autoFillSizeDetail": "Fill size with image real size" }, "mentionUser": { "title": "Mention User", diff --git a/lib/i18n/strings_zh-CN.i18n.json b/lib/i18n/strings_zh-CN.i18n.json index 0e33c8cb..970d20e9 100644 --- a/lib/i18n/strings_zh-CN.i18n.json +++ b/lib/i18n/strings_zh-CN.i18n.json @@ -537,7 +537,9 @@ "errorEmpty": "不能为空", "width": "宽度", "height": "高度", - "errorInvalidNumber": "无效的数字" + "errorInvalidNumber": "无效的数字", + "autoFillSize": "自动填写大小", + "autoFillSizeDetail": "填写图片的实际大小" }, "mentionUser": { "title": "提醒用户", diff --git a/lib/i18n/strings_zh-TW.i18n.json b/lib/i18n/strings_zh-TW.i18n.json index 64255a37..ae1e15ee 100644 --- a/lib/i18n/strings_zh-TW.i18n.json +++ b/lib/i18n/strings_zh-TW.i18n.json @@ -537,7 +537,9 @@ "errorEmpty": "不能為空", "width": "寬度", "height": "高度", - "errorInvalidNumber": "無效的數字" + "errorInvalidNumber": "無效的數字", + "autoFillSize": "自動填入大小", + "autoFillSizeDetail": "填寫圖片的實際大小" }, "mentionUser": { "title": "提醒使用者", diff --git a/packages/flutter_bbcode_editor b/packages/flutter_bbcode_editor index 48dbf119..73785eff 160000 --- a/packages/flutter_bbcode_editor +++ b/packages/flutter_bbcode_editor @@ -1 +1 @@ -Subproject commit 48dbf11960f8f12f0878de7e0f4d82ae0e068aa6 +Subproject commit 73785eff7a5bda190a0c32d00b389c3c99841717