diff --git a/app/lib/frontend/widgets/chat_message.dart b/app/lib/frontend/widgets/chat_message.dart index 2d6dfe6..163fce0 100644 --- a/app/lib/frontend/widgets/chat_message.dart +++ b/app/lib/frontend/widgets/chat_message.dart @@ -20,9 +20,11 @@ import 'package:unicons/unicons.dart'; class ChatMessageWidget extends StatefulWidget { final ChatMessageWrapper message; + final ScrollController scrollController; const ChatMessageWidget( - this.message, { + this.message, + this.scrollController, { super.key, }); @@ -238,6 +240,7 @@ class _ChatMessageWidgetState extends State { if (!_showEditWidget && widget.message.text.isNotEmpty) MarkdownBodyWidget( widget.message.text, + widget.scrollController, ), if (context.watch().isChatShowStatistics && widget.message.text.isNotEmpty && diff --git a/app/lib/frontend/widgets/chat_message_list.dart b/app/lib/frontend/widgets/chat_message_list.dart index 76768eb..bab7a6d 100644 --- a/app/lib/frontend/widgets/chat_message_list.dart +++ b/app/lib/frontend/widgets/chat_message_list.dart @@ -112,6 +112,7 @@ class _ChatMessageListState extends State { return ChatMessageWidget( key: Key(message.uuid), message, + _scrollController, ).animate().fadeIn(duration: 300.ms).move( begin: const Offset(-16, 0), curve: Curves.easeOutQuad, diff --git a/app/lib/frontend/widgets/markdown_body.dart b/app/lib/frontend/widgets/markdown_body.dart index 6327c8d..a3845bb 100644 --- a/app/lib/frontend/widgets/markdown_body.dart +++ b/app/lib/frontend/widgets/markdown_body.dart @@ -14,9 +14,11 @@ import 'package:url_launcher/url_launcher.dart'; class MarkdownBodyWidget extends StatelessWidget { final String message; + final ScrollController scrollController; const MarkdownBodyWidget( - this.message, { + this.message, + this.scrollController, { super.key, }); @@ -37,6 +39,7 @@ class MarkdownBodyWidget extends StatelessWidget { child, code, language, + scrollController, ), ) : const PreConfig().copy( @@ -46,6 +49,7 @@ class MarkdownBodyWidget extends StatelessWidget { child, code, language, + scrollController, ), ), LinkConfig( diff --git a/app/lib/frontend/widgets/markdown_code_wrapper.dart b/app/lib/frontend/widgets/markdown_code_wrapper.dart index 15698ac..952a541 100644 --- a/app/lib/frontend/widgets/markdown_code_wrapper.dart +++ b/app/lib/frontend/widgets/markdown_code_wrapper.dart @@ -11,6 +11,7 @@ import 'package:gap/gap.dart'; import 'package:open_local_ui/core/asset.dart'; import 'package:open_local_ui/core/snackbar.dart'; import 'package:unicons/unicons.dart'; +import 'package:flutter_sticky_widgets/flutter_sticky_widgets.dart'; Map languageToAsset = { 'apache': 'assets/graphics/logos/apache.svg', @@ -70,11 +71,13 @@ class MarkdownCodeWrapperWidget extends StatefulWidget { final Widget child; final String text; final String language; + final ScrollController scrollController; const MarkdownCodeWrapperWidget( this.child, this.text, - this.language, { + this.language, + this.scrollController, { super.key, }); @@ -108,7 +111,8 @@ class _CodeWrapperState extends State { Future _saveFile() async { setState(() => _isSaved = true); - final String? selectedDirectory = await FilePicker.platform.getDirectoryPath(); + final String? selectedDirectory = + await FilePicker.platform.getDirectoryPath(); if (selectedDirectory != null) { const String fileName = 'code_snippet.txt'; @@ -136,86 +140,66 @@ class _CodeWrapperState extends State { }); } + Size _getMarkdownBodySize(BuildContext context) { + final RenderBox renderBox = + context.findAncestorRenderObjectOfType() as RenderBox; + + return renderBox.size; + } + @override Widget build(BuildContext context) { return Stack( children: [ widget.child, - Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.only( - top: 16, - right: 8, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.language.isNotEmpty) - if (languageToAsset.containsKey(widget.language)) - Tooltip( - message: widget.language.toUpperCase(), - child: SvgPicture.memory( - AssetManager.getAsset( - languageToAsset[widget.language]!, - type: AssetType.binary, - ), - width: 20, - height: 20, - // ignore: deprecated_member_use - color: AdaptiveTheme.of(context).mode.isDark - ? Colors.white - : Colors.black, - ), - ), - if (widget.language.isNotEmpty) - if (!languageToAsset.containsKey(widget.language)) - SelectionContainer.disabled( - child: Text( - widget.language.toUpperCase(), - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + StickyWidget( + initialPosition: StickyPosition(top: 16, right: 16), + finalPosition: StickyPosition( + top: _getMarkdownBodySize(context).height - 56, + right: 16, + ), + controller: widget.scrollController, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.language.isNotEmpty) + if (languageToAsset.containsKey(widget.language)) + Tooltip( + message: widget.language.toUpperCase(), + child: SvgPicture.memory( + AssetManager.getAsset( + languageToAsset[widget.language]!, + type: AssetType.binary, ), - ), - const Gap(16.0), - InkWell( - onTap: () => _copyMessage(), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: ( - Widget child, - Animation animation, - ) { - return ScaleTransition(scale: animation, child: child); - }, - child: Icon( - _isCopied ? UniconsLine.check : UniconsLine.copy, - key: ValueKey(_isCopied), - size: 24, + width: 20, + height: 20, + // ignore: deprecated_member_use + color: AdaptiveTheme.of(context).mode.isDark + ? Colors.white + : Colors.black, ), ), - ), - const Gap(16.0), - InkWell( - onTap: () => _saveFile(), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - transitionBuilder: ( - Widget child, - Animation animation, - ) { - return ScaleTransition(scale: animation, child: child); - }, - child: Icon( - _isSaved ? UniconsLine.check : UniconsLine.save, - key: ValueKey(_isSaved), - size: 24, + if (widget.language.isNotEmpty) + if (!languageToAsset.containsKey(widget.language)) + SelectionContainer.disabled( + child: Text( + widget.language.toUpperCase(), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), ), ), - ), - ], - ), + const Gap(24.0), + IconButton( + onPressed: () => _copyMessage(), + icon: Icon(_isCopied ? UniconsLine.check : UniconsLine.copy), + ), + const Gap(16.0), + IconButton( + onPressed: () => _saveFile(), + icon: Icon(_isSaved ? UniconsLine.check : UniconsLine.save), + ), + ], ), ), ], diff --git a/app/pubspec.lock b/app/pubspec.lock index 37ddabc..48dacaa 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -667,6 +667,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.2.1" + flutter_sticky_widgets: + dependency: "direct main" + description: + name: flutter_sticky_widgets + sha256: d0624d3aec3f6acc25993515073c82233efcd658fa5a94818a212da97ee02c6f + url: "https://pub.dev" + source: hosted + version: "0.0.3" flutter_svg: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index c335f47..fcbdf80 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -111,6 +111,7 @@ dependencies: # Dependency Injection get_it: ^7.7.0 + flutter_sticky_widgets: ^0.0.3 dev_dependencies: flutter_test: