Skip to content

Commit

Permalink
feat(editor): Autofill image size
Browse files Browse the repository at this point in the history
Fill inserted image bbcode size with image real size.
  • Loading branch information
realth000 committed Aug 5, 2024
1 parent 55f6f19 commit 1cda787
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 75 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- 展开状态下显示所有支持的bbcode格式,输入框不限制最大外观高度。
- 增加将编辑器变回默认状态的按钮。
- 在图片上显示设置的图片宽高。
- 插入图片时自动填写图片宽高。
- 用户:用户页面向下滚动到用户名隐藏时,在顶部显示用户名。
- 分区:支持显示在帖子中最后回复的用户。
- 设置:支持设置帖子卡片的外观,包括对齐方式和是否显示最后回复的用户。
Expand Down
227 changes: 156 additions & 71 deletions lib/features/editor/widgets/image_dialog.dart
Original file line number Diff line number Diff line change
@@ -1,42 +1,122 @@
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<void> showImageDialog(
BuildContext context,
BBCodeEditorController controller,
) async =>
showDialog<void>(
Future<BBCodeImageInfo?> showImagePicker(
BuildContext context, {
String? url,
int? width,
int? height,
}) async =>
showDialog<BBCodeImageInfo>(
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();
}

class _ImageDialogState extends State<_ImageDialog> {
final formKey = GlobalKey<FormState>();
final imageUrlController = TextEditingController();
final widthController = TextEditingController(text: '$_defaultWidth');
final heightController = TextEditingController(text: '$_defaultHeight');
final urlFieldKey = GlobalKey<FormFieldState<String>>();

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<void> _fillImageSize(BuildContext context, String url) async {
try {
final cacheInfo = getIt.get<ImageCacheProvider>().getCacheInfo(url);
if (cacheInfo == null) {
// Not cached
// FIXME: SO CONFUSING
await context.read<ImageCacheRepository>().updateImageCache(url);
}
final imageData = await getIt.get<ImageCacheProvider>().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) {
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
),
);
},
),
],
);
}
}
3 changes: 3 additions & 0 deletions lib/features/editor/widgets/rich_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
);
}
}
3 changes: 3 additions & 0 deletions lib/features/editor/widgets/toolbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -337,6 +338,8 @@ class _EditorToolbarState extends State<EditorToolbar> {
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),
Expand Down
4 changes: 3 additions & 1 deletion lib/i18n/strings.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion lib/i18n/strings_zh-CN.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,9 @@
"errorEmpty": "不能为空",
"width": "宽度",
"height": "高度",
"errorInvalidNumber": "无效的数字"
"errorInvalidNumber": "无效的数字",
"autoFillSize": "自动填写大小",
"autoFillSizeDetail": "填写图片的实际大小"
},
"mentionUser": {
"title": "提醒用户",
Expand Down
4 changes: 3 additions & 1 deletion lib/i18n/strings_zh-TW.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,9 @@
"errorEmpty": "不能為空",
"width": "寬度",
"height": "高度",
"errorInvalidNumber": "無效的數字"
"errorInvalidNumber": "無效的數字",
"autoFillSize": "自動填入大小",
"autoFillSizeDetail": "填寫圖片的實際大小"
},
"mentionUser": {
"title": "提醒使用者",
Expand Down

0 comments on commit 1cda787

Please sign in to comment.