From ffbe399614c40c4516c056f2f9adfd1241d626f3 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 23 Jun 2025 23:40:29 +0800 Subject: [PATCH] :bug: Fixes on post compose --- lib/route.dart | 8 +- lib/screens/posts/compose.dart | 36 +----- lib/screens/posts/compose_article.dart | 121 +++++++++---------- lib/widgets/content/markdown.dart | 28 +++-- lib/widgets/content/markdown_latex.dart | 80 ++++++++++++ lib/widgets/post/compose_settings_sheet.dart | 92 +++++++------- lib/widgets/post/post_item.dart | 6 +- pubspec.lock | 22 ++-- pubspec.yaml | 1 + 9 files changed, 222 insertions(+), 172 deletions(-) create mode 100644 lib/widgets/content/markdown_latex.dart diff --git a/lib/route.dart b/lib/route.dart index fdfdd9c..559ee06 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -8,6 +8,10 @@ class AppRouter extends RootStackRouter { @override List get routes => [ + AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'), + AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'), + AutoRoute(page: CallRoute.page, path: '/chat/:id/call'), + AutoRoute(page: EventCalanderRoute.page, path: '/account/:name/calendar'), AutoRoute( page: TabsRoute.page, path: '/', @@ -52,10 +56,6 @@ class AppRouter extends RootStackRouter { ), ], ), - AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'), - AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'), - AutoRoute(page: CallRoute.page, path: '/chat/:id/call'), - AutoRoute(page: EventCalanderRoute.page, path: '/account/:name/calendar'), AutoRoute( page: CreatorHubShellRoute.page, path: '/creators', diff --git a/lib/screens/posts/compose.dart b/lib/screens/posts/compose.dart index cd99a4e..87278c8 100644 --- a/lib/screens/posts/compose.dart +++ b/lib/screens/posts/compose.dart @@ -32,11 +32,13 @@ class PostEditScreen extends HookConsumerWidget { data: (post) => PostComposeScreen(originalPost: post), loading: () => AppScaffold( + noBackground: false, appBar: AppBar(leading: const PageBackButton()), body: const Center(child: CircularProgressIndicator()), ), error: (e, _) => AppScaffold( + noBackground: false, appBar: AppBar(leading: const PageBackButton()), body: Text('Error: $e', textAlign: TextAlign.center), ), @@ -117,32 +119,6 @@ class PostComposeScreen extends HookConsumerWidget { ); } - void showKeyboardShortcutsDialog() { - showDialog( - context: context, - builder: - (context) => AlertDialog( - title: Text('keyboardShortcuts'.tr()), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Ctrl/Cmd + Enter: ${'submit'.tr()}'), - Text('Ctrl/Cmd + V: ${'paste'.tr()}'), - Text('Ctrl/Cmd + I: ${'add_image'.tr()}'), - Text('Ctrl/Cmd + Shift + V: ${'add_video'.tr()}'), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('close'.tr()), - ), - ], - ), - ); - } - Widget buildWideAttachmentGrid() { return GridView.builder( shrinkWrap: true, @@ -219,14 +195,6 @@ class PostComposeScreen extends HookConsumerWidget { onPressed: showSettingsSheet, tooltip: 'postSettings'.tr(), ), - if (isWideScreen(context)) - Tooltip( - message: 'keyboardShortcuts'.tr(), - child: IconButton( - icon: const Icon(Symbols.keyboard), - onPressed: showKeyboardShortcutsDialog, - ), - ), ValueListenableBuilder( valueListenable: state.submitting, builder: (context, submitting, _) { diff --git a/lib/screens/posts/compose_article.dart b/lib/screens/posts/compose_article.dart index e4c4ae8..984e5f3 100644 --- a/lib/screens/posts/compose_article.dart +++ b/lib/screens/posts/compose_article.dart @@ -1,19 +1,17 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; - import 'package:island/models/file.dart'; import 'package:island/models/post.dart'; import 'package:island/screens/creators/publishers.dart'; import 'package:island/services/responsive.dart'; - import 'package:island/widgets/app_scaffold.dart'; import 'package:island/screens/posts/detail.dart'; import 'package:island/widgets/content/attachment_preview.dart'; +import 'package:island/widgets/content/markdown.dart'; import 'package:island/widgets/post/compose_shared.dart'; import 'package:island/widgets/post/publishers_modal.dart'; import 'package:island/widgets/content/cloud_files.dart'; @@ -94,33 +92,6 @@ class ArticleComposeScreen extends HookConsumerWidget { ); } - void showKeyboardShortcutsDialog() { - showDialog( - context: context, - builder: - (context) => AlertDialog( - title: Text('keyboardShortcuts'.tr()), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Ctrl/Cmd + Enter: ${'submit'.tr()}'), - Text('Ctrl/Cmd + V: ${'paste'.tr()}'), - Text('Ctrl/Cmd + I: ${'add_image'.tr()}'), - Text('Ctrl/Cmd + Shift + V: ${'add_video'.tr()}'), - Text('Ctrl/Cmd + P: ${'toggle_preview'.tr()}'), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('close'.tr()), - ), - ], - ), - ); - } - Widget buildPreviewPane() { return Container( decoration: BoxDecoration( @@ -150,33 +121,49 @@ class ArticleComposeScreen extends HookConsumerWidget { Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (state.titleController.text.isNotEmpty) ...[ - Text( - state.titleController.text, - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const Gap(16), - ], - if (state.descriptionController.text.isNotEmpty) ...[ - Text( - state.descriptionController.text, - style: theme.textTheme.bodyLarge?.copyWith( - color: colorScheme.onSurface.withOpacity(0.7), - ), - ), - const Gap(16), - ], - if (state.contentController.text.isNotEmpty) - Text( - state.contentController.text, - style: theme.textTheme.bodyMedium, - ), - ], + child: ValueListenableBuilder( + valueListenable: state.titleController, + builder: (context, titleValue, _) { + return ValueListenableBuilder( + valueListenable: state.descriptionController, + builder: (context, descriptionValue, _) { + return ValueListenableBuilder( + valueListenable: state.contentController, + builder: (context, contentValue, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (titleValue.text.isNotEmpty) ...[ + Text( + titleValue.text, + style: theme.textTheme.headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), + ), + const Gap(16), + ], + if (descriptionValue.text.isNotEmpty) ...[ + Text( + descriptionValue.text, + style: theme.textTheme.bodyLarge?.copyWith( + color: colorScheme.onSurface.withOpacity( + 0.7, + ), + ), + ), + const Gap(16), + ], + if (contentValue.text.isNotEmpty) + MarkdownTextContent( + content: contentValue.text, + textStyle: theme.textTheme.bodyMedium, + ), + ], + ); + }, + ); + }, + ); + }, ), ), ), @@ -191,6 +178,7 @@ class ArticleComposeScreen extends HookConsumerWidget { children: [ // Publisher row Card( + margin: EdgeInsets.only(bottom: 8), elevation: 1, child: Padding( padding: const EdgeInsets.all(12), @@ -316,8 +304,17 @@ class ArticleComposeScreen extends HookConsumerWidget { } return AppScaffold( + noBackground: false, appBar: AppBar( leading: const PageBackButton(), + title: ValueListenableBuilder( + valueListenable: state.titleController, + builder: (context, titleValue, _) { + return Text( + titleValue.text.isEmpty ? 'postTitle'.tr() : titleValue.text, + ); + }, + ), actions: [ // Info banner for article compose const SizedBox.shrink(), @@ -333,14 +330,6 @@ class ArticleComposeScreen extends HookConsumerWidget { onPressed: () => showPreview.value = !showPreview.value, ), ), - if (isWideScreen(context)) - Tooltip( - message: 'keyboardShortcuts'.tr(), - child: IconButton( - icon: const Icon(Symbols.keyboard), - onPressed: showKeyboardShortcutsDialog, - ), - ), ValueListenableBuilder( valueListenable: state.submitting, builder: (context, submitting, _) { @@ -378,7 +367,7 @@ class ArticleComposeScreen extends HookConsumerWidget { children: [ Expanded( child: Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.only(left: 16, right: 16), child: isWideScreen(context) ? Row( diff --git a/lib/widgets/content/markdown.dart b/lib/widgets/content/markdown.dart index 47ab0c1..fd050fb 100644 --- a/lib/widgets/content/markdown.dart +++ b/lib/widgets/content/markdown.dart @@ -2,10 +2,13 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_highlight/themes/a11y-dark.dart'; +import 'package:flutter_highlight/themes/a11y-light.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/pods/config.dart'; import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/content/markdown_latex.dart'; import 'package:markdown/markdown.dart' as markdown; import 'package:markdown_widget/markdown_widget.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -18,6 +21,7 @@ class MarkdownTextContent extends HookConsumerWidget { final TextScaler? textScaler; final TextStyle? textStyle; final TextStyle? linkStyle; + final EdgeInsets? linesMargin; final bool isSelectable; const MarkdownTextContent({ @@ -28,6 +32,7 @@ class MarkdownTextContent extends HookConsumerWidget { this.textStyle, this.linkStyle, this.isSelectable = false, + this.linesMargin, }); @override @@ -54,19 +59,13 @@ class MarkdownTextContent extends HookConsumerWidget { config: config.copy( configs: [ isDark - ? PreConfig.darkConfig.copy( - textStyle: textStyle, - padding: EdgeInsets.zero, - margin: EdgeInsets.zero, - ) - : PreConfig().copy( - textStyle: textStyle, - padding: EdgeInsets.zero, - margin: EdgeInsets.zero, - ), + ? PreConfig.darkConfig.copy(textStyle: textStyle) + : PreConfig().copy(textStyle: textStyle), PConfig( textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!, ), + HrConfig(height: 1, color: Theme.of(context).dividerColor), + PreConfig(theme: isDark ? a11yDarkTheme : a11yLightTheme), LinkConfig( style: linkStyle ?? @@ -146,8 +145,13 @@ class MarkdownTextContent extends HookConsumerWidget { ], ), generator: MarkdownGenerator( - inlineSyntaxList: [_UserNameCardInlineSyntax(), _StickerInlineSyntax()], - linesMargin: EdgeInsets.zero, + generators: [latexGenerator], + inlineSyntaxList: [ + _UserNameCardInlineSyntax(), + _StickerInlineSyntax(), + LatexSyntax(isDark), + ], + linesMargin: linesMargin ?? EdgeInsets.symmetric(vertical: 4), ), ); } diff --git a/lib/widgets/content/markdown_latex.dart b/lib/widgets/content/markdown_latex.dart new file mode 100644 index 0000000..951986a --- /dev/null +++ b/lib/widgets/content/markdown_latex.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:markdown_widget/markdown_widget.dart'; +import 'package:flutter_math_fork/flutter_math.dart'; +import 'package:markdown/markdown.dart' as m; + +SpanNodeGeneratorWithTag latexGenerator = SpanNodeGeneratorWithTag( + tag: _latexTag, + generator: + (e, config, visitor) => LatexNode(e.attributes, e.textContent, config), +); + +const _latexTag = 'latex'; + +class LatexSyntax extends m.InlineSyntax { + final bool isDark; + LatexSyntax(this.isDark) : super(r'(\$\$[\s\S]+\$\$)|(\$.+?\$)'); + + @override + bool onMatch(m.InlineParser parser, Match match) { + final input = match.input; + final matchValue = input.substring(match.start, match.end); + String content = ''; + bool isInline = true; + const blockSyntax = '\$\$'; + const inlineSyntax = '\$'; + if (matchValue.startsWith(blockSyntax) && + matchValue.endsWith(blockSyntax) && + (matchValue != blockSyntax)) { + content = matchValue.substring(2, matchValue.length - 2); + isInline = false; + } else if (matchValue.startsWith(inlineSyntax) && + matchValue.endsWith(inlineSyntax) && + matchValue != inlineSyntax) { + content = matchValue.substring(1, matchValue.length - 1); + } + m.Element el = m.Element.text(_latexTag, matchValue); + el.attributes['content'] = content; + el.attributes['isInline'] = '$isInline'; + el.attributes['isDark'] = isDark.toString(); + parser.addNode(el); + return true; + } +} + +class LatexNode extends SpanNode { + final Map attributes; + final String textContent; + final MarkdownConfig config; + + LatexNode(this.attributes, this.textContent, this.config); + + @override + InlineSpan build() { + final content = attributes['content'] ?? ''; + final isInline = attributes['isInline'] == 'true'; + final isDark = attributes['isDark'] == 'true'; + final style = parentStyle ?? config.p.textStyle; + if (content.isEmpty) return TextSpan(style: style, text: textContent); + final latex = Math.tex( + content, + mathStyle: MathStyle.text, + textStyle: style.copyWith(color: isDark ? Colors.white : Colors.black), + textScaleFactor: 1, + onErrorFallback: (error) { + return Text(textContent, style: style.copyWith(color: Colors.red)); + }, + ); + return WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: + !isInline + ? Container( + width: double.infinity, + margin: EdgeInsets.symmetric(vertical: 16), + child: Center(child: latex), + ) + : latex, + ); + } +} diff --git a/lib/widgets/post/compose_settings_sheet.dart b/lib/widgets/post/compose_settings_sheet.dart index 09b4308..a467ccb 100644 --- a/lib/widgets/post/compose_settings_sheet.dart +++ b/lib/widgets/post/compose_settings_sheet.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -23,6 +24,9 @@ class ComposeSettingsSheet extends HookWidget { final theme = Theme.of(context); final colorScheme = theme.colorScheme; + // Listen to visibility changes to trigger rebuilds + final currentVisibility = useValueListenable(visibility); + IconData getVisibilityIcon(int visibilityValue) { switch (visibilityValue) { case 1: @@ -71,38 +75,39 @@ class ComposeSettingsSheet extends HookWidget { void showVisibilitySheet() { showModalBottomSheet( context: context, - builder: (context) => SheetScaffold( - titleText: 'postVisibility'.tr(), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - buildVisibilityOption( - context, - 0, - Symbols.public, - 'postVisibilityPublic', + builder: + (context) => SheetScaffold( + titleText: 'postVisibility'.tr(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + buildVisibilityOption( + context, + 0, + Symbols.public, + 'postVisibilityPublic', + ), + buildVisibilityOption( + context, + 1, + Symbols.group, + 'postVisibilityFriends', + ), + buildVisibilityOption( + context, + 2, + Symbols.link_off, + 'postVisibilityUnlisted', + ), + buildVisibilityOption( + context, + 3, + Symbols.lock, + 'postVisibilityPrivate', + ), + ], ), - buildVisibilityOption( - context, - 1, - Symbols.group, - 'postVisibilityFriends', - ), - buildVisibilityOption( - context, - 2, - Symbols.link_off, - 'postVisibilityUnlisted', - ), - buildVisibilityOption( - context, - 3, - Symbols.lock, - 'postVisibilityPrivate', - ), - ], - ), - ), + ), ); } @@ -124,10 +129,11 @@ class ComposeSettingsSheet extends HookWidget { ), contentPadding: const EdgeInsets.all(16), ), - style: theme.textTheme.titleLarge, - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + style: theme.textTheme.titleMedium, + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), ), - const SizedBox(height: 16), + const Gap(16), // Description field TextField( @@ -140,25 +146,23 @@ class ComposeSettingsSheet extends HookWidget { ), contentPadding: const EdgeInsets.all(16), ), - style: theme.textTheme.bodyLarge, + style: theme.textTheme.bodyMedium, maxLines: 3, - onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: + (_) => FocusManager.instance.primaryFocus?.unfocus(), ), - const SizedBox(height: 24), + const Gap(16), // Visibility setting Container( decoration: BoxDecoration( - border: Border.all( - color: colorScheme.outline, - width: 1, - ), + border: Border.all(color: colorScheme.outline, width: 1), borderRadius: BorderRadius.circular(12), ), child: ListTile( - leading: Icon(getVisibilityIcon(visibility.value)), + leading: Icon(getVisibilityIcon(currentVisibility)), title: Text('postVisibility'.tr()), - subtitle: Text(getVisibilityText(visibility.value).tr()), + subtitle: Text(getVisibilityText(currentVisibility).tr()), trailing: const Icon(Symbols.chevron_right), onTap: showVisibilitySheet, shape: RoundedRectangleBorder( @@ -175,4 +179,4 @@ class ComposeSettingsSheet extends HookWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 85efaf1..23ae78b 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -209,7 +209,11 @@ class PostItem extends HookConsumerWidget { ), ).padding(bottom: 8), if (item.content?.isNotEmpty ?? false) - MarkdownTextContent(content: item.content!), + MarkdownTextContent( + content: item.content!, + linesMargin: + item.type == 0 ? EdgeInsets.zero : null, + ), // Show truncation hint if post is truncated if (item.isTruncated && !isFullPost) _PostTruncateHint(), diff --git a/pubspec.lock b/pubspec.lock index 1b2b2dc..e35b420 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: build - sha256: "74273591bd8b7f82eeb1f191c1b65a6576535bbfd5ca3722778b07d5702d33cc" + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" build_config: dependency: transitive description: @@ -173,26 +173,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: badce70566085f2e87434531c4a6bc8e833672f755fc51146d612245947e91c9 + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: b9070a4127033777c0e63195f6f117ed16a351ed676f6313b095cf4f328c0b82 + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "1cdfece3eeb3f1263f7dbf5bcc0cba697bd0c22d2c866cb4b578c954dbb09bcf" + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.1.2" built_collection: dependency: transitive description: @@ -836,7 +836,7 @@ packages: source: hosted version: "0.3.4" flutter_math_fork: - dependency: transitive + dependency: "direct main" description: name: flutter_math_fork sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407" @@ -1745,10 +1745,10 @@ packages: dependency: transitive description: name: record_platform_interface - sha256: "8a575828733d4c3cb5983c914696f40db8667eab3538d4c41c50cbb79e722ef4" + sha256: c1ad38f51e4af88a085b3e792a22c685cb3e7c23fc37aa7ce44c4cf18f25fe89 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" record_web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e2946ca..c6f1e67 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -118,6 +118,7 @@ dependencies: native_exif: ^0.6.2 local_auth: ^2.3.0 flutter_secure_storage: ^4.2.1 + flutter_math_fork: ^0.7.4 dev_dependencies: flutter_test: