Skip to content

Commit

Permalink
Merge pull request #143 from ZhuJHua/dev
Browse files Browse the repository at this point in the history
feat(markdown): support markdown editing and embed rendering
  • Loading branch information
ZhuJHua authored Jan 26, 2025
2 parents 042da7d + 686bd4f commit c6022db
Show file tree
Hide file tree
Showing 17 changed files with 469 additions and 103 deletions.
9 changes: 7 additions & 2 deletions lib/components/base/button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,19 @@ class MultiFabLayoutDelegate extends MultiChildLayoutDelegate {
}

class PageBackButton extends StatelessWidget {
const PageBackButton({super.key});
final Function()? onBack;

const PageBackButton({
super.key,
this.onBack,
});

@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Center(
child: IconButton(
onPressed: Get.back,
onPressed: onBack ?? Get.back,
icon: const Icon(Icons.arrow_back_rounded),
color: colorScheme.onSurface,
tooltip: l10n.back,
Expand Down
14 changes: 12 additions & 2 deletions lib/components/markdown_bar/markdown_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class MarkdownToolbar extends StatefulWidget {
this.checkboxTooltip = 'Checkbox',
this.quoteTooltip = 'Quote',
this.horizontalRuleTooltip = 'Horizontal rule',
required this.beforeImagePressed,
});

/// It is recommended that you use your own TextField outside this widget for more customization.
Expand Down Expand Up @@ -243,6 +244,8 @@ class MarkdownToolbar extends StatefulWidget {
/// Set a custom tooltip [String] for the horizontal rule button. Leave blank `''` to disable tooltip.
final String horizontalRuleTooltip;

final Future<String?> Function() beforeImagePressed;

@override
State<MarkdownToolbar> createState() => MarkdownToolbarState();
}
Expand Down Expand Up @@ -602,8 +605,13 @@ class MarkdownToolbarState extends State<MarkdownToolbar> {
void onLinkPressed() =>
onToolbarItemPressed(markdownToolbarOption: MarkdownToolbarOption.link);

void onImagePressed() =>
onToolbarItemPressed(markdownToolbarOption: MarkdownToolbarOption.image);
void onImagePressed() async {
final path = await widget.beforeImagePressed();
onToolbarItemPressed(
markdownToolbarOption: MarkdownToolbarOption.image,
mediaPath: path,
);
}

void onCodePressed() =>
onToolbarItemPressed(markdownToolbarOption: MarkdownToolbarOption.code);
Expand All @@ -626,6 +634,7 @@ class MarkdownToolbarState extends State<MarkdownToolbar> {
void onToolbarItemPressed({
required MarkdownToolbarOption markdownToolbarOption,
int? option,
String? mediaPath,
}) {
widget.useIncludedTextField
? _includedFocusNode.requestFocus()
Expand All @@ -644,6 +653,7 @@ class MarkdownToolbarState extends State<MarkdownToolbar> {
customCodeCharacter: widget.codeCharacter,
customBulletedListCharacter: widget.bulletedListCharacter,
customHorizontalRuleCharacter: widget.horizontalRuleCharacter,
mediaPath: mediaPath,
);
}

Expand Down
16 changes: 11 additions & 5 deletions lib/components/markdown_bar/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class Format {
String? customCodeCharacter,
String? customBulletedListCharacter,
String? customHorizontalRuleCharacter,
String? mediaPath,
}) {
switch (markdownToolbarOption) {
case MarkdownToolbarOption.bold:
Expand Down Expand Up @@ -174,6 +175,7 @@ class Format {
formatImage(
controller: controller,
selection: selection,
imgPath: mediaPath,
);
break;
case MarkdownToolbarOption.horizontalRule:
Expand Down Expand Up @@ -588,17 +590,21 @@ void formatTextLink({
void formatImage({
required TextEditingController controller,
required TextSelection selection,
required String? imgPath,
}) {
const String altPlaceholder = 'Alt text';
const String linkPlaceholder = '/link/to/picture.jpg';
const String altPlaceholder = '';
const String linkPlaceholder = '';

final beforeText = controller.text.substring(0, selection.start);
final afterText =
controller.text.substring(selection.end, controller.text.length);
controller.text = '$beforeText![$altPlaceholder]($linkPlaceholder)$afterText';
controller.text =
'$beforeText![$altPlaceholder](${imgPath ?? linkPlaceholder})$afterText';

controller.selection = TextSelection(
baseOffset: selection.start + 4 + altPlaceholder.length,
extentOffset:
selection.start + altPlaceholder.length + 4 + linkPlaceholder.length);
extentOffset: selection.start +
altPlaceholder.length +
4 +
(imgPath ?? linkPlaceholder).length);
}
23 changes: 23 additions & 0 deletions lib/components/markdown_embed/audio_embed.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:flutter/widgets.dart';
import 'package:moodiary/utils/file_util.dart';

import '../audio_player/audio_player_view.dart';

class MarkdownAudioEmbed extends StatelessWidget {
final String audioName;
final bool isEdit;

const MarkdownAudioEmbed(
{super.key, required this.audioName, required this.isEdit});

@override
Widget build(
BuildContext context,
) {
final path = isEdit
? FileUtil.getCachePath(audioName)
: FileUtil.getRealPath('audio', audioName);

return AudioPlayerComponent(path: path); // 使用音频播放器组件渲染
}
}
43 changes: 43 additions & 0 deletions lib/components/markdown_embed/image_embed.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:moodiary/router/app_routes.dart';
import 'package:moodiary/utils/file_util.dart';
import 'package:refreshed/refreshed.dart';

class MarkdownImageEmbed extends StatelessWidget {
final bool isEdit;
final String imageName;

const MarkdownImageEmbed(
{super.key, required this.isEdit, required this.imageName});

@override
Widget build(
BuildContext context,
) {
final imagePath =
isEdit ? imageName : FileUtil.getRealPath('image', imageName);

return Center(
child: GestureDetector(
onTap: () {
if (!isEdit) {
Get.toNamed(AppRoutes.photoPage, arguments: [
[imagePath],
0,
]);
}
},
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: Card.outlined(
clipBehavior: Clip.hardEdge,
color: Colors.transparent,
child: Image.file(File(imagePath)),
),
),
),
);
}
}
32 changes: 32 additions & 0 deletions lib/components/markdown_embed/video_embed.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:moodiary/components/video_player/video_player_view.dart';
import 'package:moodiary/utils/file_util.dart';

class MarkdownVideoEmbed extends StatelessWidget {
final bool isEdit;
final String videoName;

const MarkdownVideoEmbed(
{super.key, required this.isEdit, required this.videoName});

@override
Widget build(
BuildContext context,
) {
final videoPath =
isEdit ? videoName : FileUtil.getRealPath('video', videoName);
final colorScheme = Theme.of(context).colorScheme;
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: Card.outlined(
clipBehavior: Clip.hardEdge,
color: colorScheme.surfaceContainerLowest,
child: VideoPlayerComponent(
videoPath: videoPath,
),
),
),
); // 使用音频播放器组件渲染
}
}
3 changes: 2 additions & 1 deletion lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -258,5 +258,6 @@
"diaryEdit": "Edit",
"diaryShare": "Share",
"diaryCount": "{count} Words",
"dataSync": "Data sync"
"dataSync": "Data sync",
"diaryType": "Type"
}
3 changes: 2 additions & 1 deletion lib/l10n/intl_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -258,5 +258,6 @@
"diaryEdit": "编辑",
"diaryShare": "分享",
"diaryCount": "{count} 字",
"dataSync": "数据同步"
"dataSync": "数据同步",
"diaryType": "类型"
}
21 changes: 15 additions & 6 deletions lib/pages/diary_details/diary_details_logic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:moodiary/common/models/isar/diary.dart';
import 'package:moodiary/common/values/diary_type.dart';
import 'package:moodiary/presentation/isar.dart';
import 'package:moodiary/router/app_routes.dart';
import 'package:refreshed/refreshed.dart';
Expand All @@ -15,18 +16,26 @@ class DiaryDetailsLogic extends GetxController {
final DiaryDetailsState state = DiaryDetailsState();

// 编辑器控制器
late QuillController quillController = QuillController(
document: Document.fromJson(jsonDecode(state.diary.content)),
readOnly: true,
selection: const TextSelection.collapsed(offset: 0),
);
QuillController? quillController;

// 图片预览
late final PageController pageController = PageController();

@override
void onInit() {
if (state.diary.type != DiaryType.markdown.value) {
quillController = QuillController(
document: Document.fromJson(jsonDecode(state.diary.content)),
readOnly: true,
selection: const TextSelection.collapsed(offset: 0),
);
}
super.onInit();
}

@override
void onClose() {
quillController.dispose();
quillController?.dispose();
pageController.dispose();
super.onClose();
}
Expand Down
61 changes: 44 additions & 17 deletions lib/pages/diary_details/diary_details_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import 'dart:io';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_highlight/themes/a11y-dark.dart';
import 'package:flutter_highlight/themes/a11y-light.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:intl/intl.dart';
import 'package:markdown_widget/markdown_widget.dart';
import 'package:moodiary/common/models/isar/diary.dart';
import 'package:moodiary/common/values/diary_type.dart';
import 'package:moodiary/common/values/icons.dart';
import 'package:moodiary/components/base/button.dart';
import 'package:moodiary/components/markdown_embed/image_embed.dart';
import 'package:moodiary/components/mood_icon/mood_icon_view.dart';
import 'package:moodiary/components/quill_embed/audio_embed.dart';
import 'package:moodiary/components/quill_embed/image_embed.dart';
Expand All @@ -26,6 +31,23 @@ class DiaryDetailsPage extends StatelessWidget {

final tag = (Get.arguments[0] as Diary).id;

Widget _buildMarkdownWidget(
{required Brightness brightness, required String data}) {
final config = brightness == Brightness.dark
? MarkdownConfig.darkConfig
: MarkdownConfig.defaultConfig;
return MarkdownBlock(
data: data,
config: config.copy(configs: [
ImgConfig(builder: (src, _) {
return MarkdownImageEmbed(isEdit: false, imageName: src);
}),
brightness == Brightness.dark
? PreConfig.darkConfig.copy(theme: a11yDarkTheme)
: const PreConfig().copy(theme: a11yLightTheme)
]));
}

@override
Widget build(BuildContext context) {
final logic = Bind.find<DiaryDetailsLogic>(tag: tag);
Expand Down Expand Up @@ -402,23 +424,28 @@ class DiaryDetailsPage extends StatelessWidget {
),
Card.filled(
color: customColorScheme.surfaceContainerLow,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: QuillEditor.basic(
controller: logic.quillController,
config: QuillEditorConfig(
showCursor: false,
customStyles: ThemeUtil.getInstance(context,
customColorScheme: customColorScheme),
embedBuilders: [
ImageEmbedBuilder(isEdit: false),
VideoEmbedBuilder(isEdit: false),
AudioEmbedBuilder(isEdit: false),
TextIndentEmbedBuilder(isEdit: false),
],
),
),
),
child: state.diary.type == DiaryType.markdown.value
? Padding(
padding: const EdgeInsets.all(8.0),
child: _buildMarkdownWidget(
brightness: customColorScheme.brightness,
data: state.diary.content),
)
: QuillEditor.basic(
controller: logic.quillController!,
config: QuillEditorConfig(
showCursor: false,
padding: const EdgeInsets.all(8.0),
customStyles: ThemeUtil.getInstance(context,
customColorScheme: customColorScheme),
embedBuilders: [
ImageEmbedBuilder(isEdit: false),
VideoEmbedBuilder(isEdit: false),
AudioEmbedBuilder(isEdit: false),
TextIndentEmbedBuilder(isEdit: false),
],
),
),
),
],
),
Expand Down
6 changes: 3 additions & 3 deletions lib/pages/draw/draw_logic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ class DrawLogic extends GetxController {
super.onClose();
}

Future<void> getImageData() async {
Future<void> getImageData(BuildContext context) async {
final data = await drawingController.getImageData();
final image = data!.buffer.asUint8List();
Get.back();
editLogic.pickDraw(image);
if (context.mounted) Navigator.pop(context);
if (context.mounted) await editLogic.pickDraw(image, context);
}

void pickColor(Color color) {
Expand Down
4 changes: 2 additions & 2 deletions lib/pages/draw/draw_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ class DrawPage extends StatelessWidget {
title: l10n.hint,
message: l10n.sureToSave,
style: AdaptiveStyle.material);
if (res == OkCancelResult.ok) {
await logic.getImageData();
if (res == OkCancelResult.ok && context.mounted) {
await logic.getImageData(context);
}
},
child: Text(l10n.save),
Expand Down
Loading

0 comments on commit c6022db

Please sign in to comment.