Merge pull request #99 from cogwheel0/feat-strip-reasoning-copy

feat-strip-reasoning-copy
This commit is contained in:
cogwheel
2025-10-24 00:53:02 +05:30
committed by GitHub
7 changed files with 321 additions and 199 deletions

View File

@@ -969,7 +969,37 @@ class _ChatPageState extends ConsumerState<ChatPage> {
} }
void _copyMessage(String content) { void _copyMessage(String content) {
Clipboard.setData(ClipboardData(text: content)); // Strip reasoning details from the copied content
String cleanedContent = content;
// Remove <details type="reasoning"> blocks
cleanedContent = cleanedContent.replaceAll(
RegExp(
r'<details\s+type="reasoning"[^>]*>[\s\S]*?<\/details>',
multiLine: true,
dotAll: true,
),
'',
);
// Remove raw reasoning tags
cleanedContent = cleanedContent.replaceAll(
RegExp(r'<think>[\s\S]*?<\/think>', multiLine: true, dotAll: true),
'',
);
cleanedContent = cleanedContent.replaceAll(
RegExp(
r'<reasoning>[\s\S]*?<\/reasoning>',
multiLine: true,
dotAll: true,
),
'',
);
// Clean up any extra whitespace
cleanedContent = cleanedContent.trim();
Clipboard.setData(ClipboardData(text: cleanedContent));
} }
void _regenerateMessage(dynamic message) async { void _regenerateMessage(dynamic message) async {
@@ -1248,8 +1278,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
final maxFraction = isTablet ? 0.42 : 0.84; final maxFraction = isTablet ? 0.42 : 0.84;
final edgeFraction = isTablet ? 0.36 : 0.50; // large phone edge final edgeFraction = isTablet ? 0.36 : 0.50; // large phone edge
final scrim = Platform.isIOS final scrim = Platform.isIOS
? context.colorTokens.overlayMedium ? context.colorTokens.scrimMedium
: context.colorTokens.overlayStrong; : context.colorTokens.scrimStrong;
return ResponsiveDrawerLayout( return ResponsiveDrawerLayout(
maxFraction: maxFraction, maxFraction: maxFraction,

View File

@@ -435,10 +435,6 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
Widget _buildSegmentedContent() { Widget _buildSegmentedContent() {
final children = <Widget>[]; final children = <Widget>[];
// Determine if media (attachments or generated images) is rendered above.
final hasMediaAbove =
(widget.message.attachmentIds?.isNotEmpty ?? false) ||
(widget.message.files?.isNotEmpty ?? false);
bool firstToolSpacerAdded = false; bool firstToolSpacerAdded = false;
int idx = 0; int idx = 0;
for (final seg in _segments) { for (final seg in _segments) {
@@ -450,11 +446,6 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
} }
children.add(_buildToolCallTile(seg.toolCall!)); children.add(_buildToolCallTile(seg.toolCall!));
} else if (seg.isReasoning && seg.reasoning != null) { } else if (seg.isReasoning && seg.reasoning != null) {
// If a reasoning tile is the very first content and sits at the top,
// add a small spacer above it for breathing room.
if (children.isEmpty && !hasMediaAbove) {
children.add(const SizedBox(height: Spacing.sm));
}
children.add(_buildReasoningTile(seg.reasoning!, idx)); children.add(_buildReasoningTile(seg.reasoning!, idx));
} else if ((seg.text ?? '').trim().isNotEmpty) { } else if ((seg.text ?? '').trim().isNotEmpty) {
children.add(_buildEnhancedMarkdownContent(seg.text!)); children.add(_buildEnhancedMarkdownContent(seg.text!));

View File

@@ -210,7 +210,14 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
PlatformUtils.lightHaptic(); PlatformUtils.lightHaptic();
widget.onSendMessage(text); widget.onSendMessage(text);
_controller.clear(); _controller.clear();
// Keep focus and keyboard open; do not collapse automatically
// Dismiss keyboard after sending to recover screen space
_focusNode.unfocus();
try {
SystemChannels.textInput.invokeMethod('TextInput.hide');
} catch (_) {
// Silently handle if keyboard dismissal fails
}
} }
void _insertNewline() { void _insertNewline() {
@@ -786,7 +793,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
Spacing.sm, Spacing.sm,
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
_buildOverflowButton( _buildOverflowButton(
tooltip: AppLocalizations.of(context)!.more, tooltip: AppLocalizations.of(context)!.more,
@@ -796,42 +803,47 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
), ),
const SizedBox(width: Spacing.sm), const SizedBox(width: Spacing.sm),
Expanded( Expanded(
child: AnimatedContainer( child: ConstrainedBox(
duration: const Duration(milliseconds: 180), constraints: BoxConstraints(
curve: Curves.easeOutCubic, maxHeight: MediaQuery.of(context).size.height * 0.25,
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
constraints: const BoxConstraints(
minHeight: TouchTarget.input,
), ),
decoration: BoxDecoration( child: AnimatedContainer(
color: composerSurface.withValues( duration: const Duration(milliseconds: 180),
alpha: brightness == Brightness.dark ? 0.9 : 0.2, curve: Curves.easeOutCubic,
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
constraints: const BoxConstraints(
minHeight: TouchTarget.input,
), ),
borderRadius: BorderRadius.circular(AppBorderRadius.round), decoration: BoxDecoration(
border: Border.all( color: composerSurface.withValues(
color: outlineColor.withValues( alpha: brightness == Brightness.dark ? 0.9 : 0.2,
alpha: brightness == Brightness.dark ? 0.32 : 0.2,
), ),
width: BorderWidth.micro, borderRadius: BorderRadius.circular(_composerRadius),
), border: Border.all(
), color: outlineColor.withValues(
child: Row( alpha: brightness == Brightness.dark ? 0.32 : 0.2,
children: [
Expanded(
child: _buildComposerTextField(
brightness: brightness,
sendOnEnter: sendOnEnter,
placeholderBase: placeholderBase,
placeholderFocused: placeholderFocused,
contentPadding: const EdgeInsets.symmetric(
vertical: Spacing.xs,
),
isActive: isActive,
), ),
width: BorderWidth.micro,
), ),
if (!_hasText && voiceAvailable && !isGenerating) ),
_buildInlineMicIcon(voiceAvailable), child: Row(
], children: [
Expanded(
child: _buildComposerTextField(
brightness: brightness,
sendOnEnter: sendOnEnter,
placeholderBase: placeholderBase,
placeholderFocused: placeholderFocused,
contentPadding: const EdgeInsets.symmetric(
vertical: Spacing.xs,
),
isActive: isActive,
),
),
if (!_hasText && voiceAvailable && !isGenerating)
_buildInlineMicIcon(voiceAvailable),
],
),
), ),
), ),
), ),

View File

@@ -7,10 +7,11 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:io' show Platform; import 'dart:io' show Platform;
import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/l10n/app_localizations.dart';
import 'package:conduit/shared/widgets/chat_action_button.dart'; import 'package:flutter/services.dart';
import '../../../core/providers/app_providers.dart'; import '../../../core/providers/app_providers.dart';
import '../providers/chat_providers.dart'; import '../providers/chat_providers.dart';
import '../../../shared/services/tasks/task_queue.dart'; import '../../../shared/services/tasks/task_queue.dart';
import '../../../shared/utils/conversation_context_menu.dart';
import '../../tools/providers/tools_providers.dart'; import '../../tools/providers/tools_providers.dart';
class UserMessageBubble extends ConsumerStatefulWidget { class UserMessageBubble extends ConsumerStatefulWidget {
@@ -41,28 +42,15 @@ class UserMessageBubble extends ConsumerStatefulWidget {
ConsumerState<UserMessageBubble> createState() => _UserMessageBubbleState(); ConsumerState<UserMessageBubble> createState() => _UserMessageBubbleState();
} }
class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
with TickerProviderStateMixin {
bool _showActions = false;
late AnimationController _fadeController;
late AnimationController _slideController;
// press state handled by shared ChatActionButton
bool _isEditing = false; bool _isEditing = false;
late final TextEditingController _editController; late final TextEditingController _editController;
final FocusNode _editFocusNode = FocusNode(); final FocusNode _editFocusNode = FocusNode();
final GlobalKey _bubbleKey = GlobalKey();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fadeController = AnimationController(
duration: AnimationDuration.microInteraction,
vsync: this,
);
_slideController = AnimationController(
duration: AnimationDuration.messageSlide,
vsync: this,
);
_editController = TextEditingController( _editController = TextEditingController(
text: widget.message?.content ?? '', text: widget.message?.content ?? '',
); );
@@ -417,25 +405,56 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
@override @override
void dispose() { void dispose() {
_fadeController.dispose();
_slideController.dispose();
_editController.dispose(); _editController.dispose();
_editFocusNode.dispose(); _editFocusNode.dispose();
super.dispose(); super.dispose();
} }
void _toggleActions() { Future<void> _showMessageMenu(BuildContext context) async {
setState(() { // Don't show menu while editing - use the visible Save/Cancel buttons instead
_showActions = !_showActions; if (_isEditing) return;
});
if (_showActions) { final l10n = AppLocalizations.of(context)!;
_fadeController.forward(); HapticFeedback.selectionClick();
_slideController.forward();
} else { // Get the position of the bubble to show menu below it
_fadeController.reverse(); Offset? menuPosition;
_slideController.reverse(); final RenderBox? renderBox =
_bubbleKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox != null) {
final position = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
// Position menu at bottom-right of the bubble
menuPosition = Offset(
position.dx + size.width,
position.dy + size.height,
);
} }
await showConduitContextMenu(
context: context,
position: menuPosition,
actions: [
ConduitContextMenuAction(
cupertinoIcon: CupertinoIcons.pencil,
materialIcon: Icons.edit_outlined,
label: l10n.edit,
onBeforeClose: () => HapticFeedback.selectionClick(),
onSelected: () async => _startInlineEdit(),
),
ConduitContextMenuAction(
cupertinoIcon: CupertinoIcons.doc_on_clipboard,
materialIcon: Icons.content_copy,
label: l10n.copy,
onBeforeClose: () => HapticFeedback.selectionClick(),
onSelected: () async {
if (widget.onCopy != null) {
widget.onCopy!();
}
},
),
],
);
} }
@override @override
@@ -458,7 +477,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
); );
return GestureDetector( return GestureDetector(
onLongPress: () => _toggleActions(), onLongPress: () => _showMessageMenu(context),
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
child: Container( child: Container(
width: double.infinity, width: double.infinity,
@@ -490,6 +509,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
maxWidth: MediaQuery.of(context).size.width * 0.82, maxWidth: MediaQuery.of(context).size.width * 0.82,
), ),
child: Container( child: Container(
key: _bubbleKey,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: Spacing.chatBubblePadding, horizontal: Spacing.chatBubblePadding,
vertical: Spacing.sm, vertical: Spacing.sm,
@@ -584,12 +604,11 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
), ),
], ],
), ),
if (hasText) const SizedBox(height: Spacing.xs),
// Action buttons below the message // Edit action buttons - show Save/Cancel when editing
if (_showActions) ...[ if (_isEditing) ...[
const SizedBox(height: Spacing.sm), const SizedBox(height: Spacing.sm),
_buildUserActionButtons(), _buildEditActionButtons(),
], ],
], ],
), ),
@@ -597,59 +616,100 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
); );
} }
// Assistant-only message renderer removed. Widget _buildEditActionButtons() {
final l10n = AppLocalizations.of(context)!;
final theme = context.conduitTheme;
// Markdown rendering and typing indicator helpers removed. return Row(
mainAxisAlignment: MainAxisAlignment.end,
// Removed unused assistant action buttons builder.
Widget _buildActionButton({
required IconData icon,
required String label,
VoidCallback? onTap,
}) {
return ChatActionButton(icon: icon, label: label, onTap: onTap);
}
Widget _buildUserActionButtons() {
return Wrap(
spacing: Spacing.sm,
runSpacing: Spacing.sm,
children: [ children: [
if (_isEditing) ...[ // Cancel button
_buildActionButton( Material(
icon: Platform.isIOS ? CupertinoIcons.check_mark : Icons.check, color: Colors.transparent,
label: AppLocalizations.of(context)!.save, child: InkWell(
onTap: _saveInlineEdit,
),
_buildActionButton(
icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close,
label: AppLocalizations.of(context)!.cancel,
onTap: _cancelInlineEdit, onTap: _cancelInlineEdit,
borderRadius: BorderRadius.circular(AppBorderRadius.small),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.xs,
),
decoration: BoxDecoration(
color: theme.surfaceContainer,
borderRadius: BorderRadius.circular(AppBorderRadius.small),
border: Border.all(
color: theme.cardBorder,
width: BorderWidth.thin,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Platform.isIOS ? CupertinoIcons.xmark : Icons.close,
size: IconSize.xs,
color: theme.textSecondary,
),
const SizedBox(width: Spacing.xs),
Text(
l10n.cancel,
style: AppTypography.standard.copyWith(
color: theme.textSecondary,
fontWeight: FontWeight.w500,
),
),
],
),
),
), ),
] else ...[ ),
_buildActionButton( const SizedBox(width: Spacing.sm),
icon: Platform.isIOS ? CupertinoIcons.pencil : Icons.edit_outlined, // Save button
label: AppLocalizations.of(context)!.edit, Material(
onTap: widget.onEdit ?? _startInlineEdit, color: Colors.transparent,
child: InkWell(
onTap: _saveInlineEdit,
borderRadius: BorderRadius.circular(AppBorderRadius.small),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.xs,
),
decoration: BoxDecoration(
color: theme.buttonPrimary,
borderRadius: BorderRadius.circular(AppBorderRadius.small),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Platform.isIOS ? CupertinoIcons.check_mark : Icons.check,
size: IconSize.xs,
color: theme.buttonPrimaryText,
),
const SizedBox(width: Spacing.xs),
Text(
l10n.save,
style: AppTypography.standard.copyWith(
color: theme.buttonPrimaryText,
fontWeight: FontWeight.w600,
),
),
],
),
),
), ),
_buildActionButton( ),
icon: Platform.isIOS
? CupertinoIcons.doc_on_clipboard
: Icons.content_copy,
label: AppLocalizations.of(context)!.copy,
onTap: widget.onCopy,
),
],
], ],
); );
} }
// Assistant-only message renderer removed.
void _startInlineEdit() { void _startInlineEdit() {
if (_isEditing) return; if (_isEditing) return;
setState(() { setState(() {
_isEditing = true; _isEditing = true;
_showActions = true; // ensure actions visible for Save/Cancel
_editController.text = widget.message.content ?? ''; _editController.text = widget.message.content ?? '';
}); });
// Request focus after frame to show keyboard // Request focus after frame to show keyboard
@@ -664,7 +724,6 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
if (!_isEditing) return; if (!_isEditing) return;
setState(() { setState(() {
_isEditing = false; _isEditing = false;
// keep actions panel open; user can close with long-press
_editController.text = widget.message.content ?? ''; _editController.text = widget.message.content ?? '';
}); });
_editFocusNode.unfocus(); _editFocusNode.unfocus();

View File

@@ -1254,9 +1254,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
? null ? null
: () => _selectConversation(context, conv.id), : () => _selectConversation(context, conv.id),
onLongPress: null, onLongPress: null,
onMorePressed: () { onMorePressed: (buttonContext) {
showConversationContextMenu( showConversationContextMenu(
context: context, context: buttonContext,
ref: ref, ref: ref,
conversation: conv, conversation: conv,
); );
@@ -1609,7 +1609,7 @@ class _ConversationTileContent extends StatelessWidget {
final bool pinned; final bool pinned;
final bool selected; final bool selected;
final bool isLoading; final bool isLoading;
final VoidCallback? onMorePressed; final void Function(BuildContext)? onMorePressed;
final Widget? leading; final Widget? leading;
const _ConversationTileContent({ const _ConversationTileContent({
@@ -1664,22 +1664,29 @@ class _ConversationTileContent extends StatelessWidget {
} else if (onMorePressed != null) { } else if (onMorePressed != null) {
trailing.addAll([ trailing.addAll([
const SizedBox(width: Spacing.sm), const SizedBox(width: Spacing.sm),
IconButton( Builder(
iconSize: IconSize.sm, builder: (buttonContext) {
visualDensity: const VisualDensity(horizontal: -2, vertical: -2), return IconButton(
padding: EdgeInsets.zero, iconSize: IconSize.sm,
constraints: const BoxConstraints( visualDensity: const VisualDensity(
minWidth: TouchTarget.listItem, horizontal: -2,
minHeight: TouchTarget.listItem, vertical: -2,
), ),
icon: Icon( padding: EdgeInsets.zero,
Platform.isIOS constraints: const BoxConstraints(
? CupertinoIcons.ellipsis minWidth: TouchTarget.listItem,
: Icons.more_vert_rounded, minHeight: TouchTarget.listItem,
color: theme.iconSecondary, ),
), icon: Icon(
onPressed: onMorePressed, Platform.isIOS
tooltip: AppLocalizations.of(context)!.more, ? CupertinoIcons.ellipsis
: Icons.more_vert_rounded,
color: theme.iconSecondary,
),
onPressed: () => onMorePressed!(buttonContext),
tooltip: AppLocalizations.of(context)!.more,
);
},
), ),
]); ]);
} }
@@ -1720,7 +1727,7 @@ class _ConversationTile extends StatelessWidget {
final Widget? leading; final Widget? leading;
final VoidCallback? onTap; final VoidCallback? onTap;
final VoidCallback? onLongPress; final VoidCallback? onLongPress;
final VoidCallback? onMorePressed; final void Function(BuildContext)? onMorePressed;
const _ConversationTile({ const _ConversationTile({
required this.title, required this.title,

View File

@@ -39,6 +39,8 @@ class AppColorTokens extends ThemeExtension<AppColorTokens> {
required this.overlayWeak, required this.overlayWeak,
required this.overlayMedium, required this.overlayMedium,
required this.overlayStrong, required this.overlayStrong,
required this.scrimMedium,
required this.scrimStrong,
required this.codeBackground, required this.codeBackground,
required this.codeBorder, required this.codeBorder,
required this.codeText, required this.codeText,
@@ -84,6 +86,10 @@ class AppColorTokens extends ThemeExtension<AppColorTokens> {
final Color overlayMedium; final Color overlayMedium;
final Color overlayStrong; final Color overlayStrong;
// Scrim tokens (for drawer/modal overlays)
final Color scrimMedium;
final Color scrimStrong;
// Markdown/code tokens // Markdown/code tokens
final Color codeBackground; final Color codeBackground;
final Color codeBorder; final Color codeBorder;
@@ -194,6 +200,14 @@ class AppColorTokens extends ThemeExtension<AppColorTokens> {
alpha: isLight ? 0.32 : 0.36, alpha: isLight ? 0.32 : 0.36,
); );
// Scrim tokens use black to create darkening effect in both modes
final Color scrimMedium = Colors.black.withValues(
alpha: isLight ? 0.2 : 0.5,
);
final Color scrimStrong = Colors.black.withValues(
alpha: isLight ? 0.32 : 0.6,
);
final Color codeBackground = mix(variant.muted, neutralTone00, 0.5); final Color codeBackground = mix(variant.muted, neutralTone00, 0.5);
final Color codeBorder = mix(variant.border, neutralTone40, 0.6); final Color codeBorder = mix(variant.border, neutralTone40, 0.6);
final Color codeText = _ensureContrast( final Color codeText = _ensureContrast(
@@ -232,6 +246,8 @@ class AppColorTokens extends ThemeExtension<AppColorTokens> {
overlayWeak: overlayWeak, overlayWeak: overlayWeak,
overlayMedium: overlayMedium, overlayMedium: overlayMedium,
overlayStrong: overlayStrong, overlayStrong: overlayStrong,
scrimMedium: scrimMedium,
scrimStrong: scrimStrong,
codeBackground: codeBackground, codeBackground: codeBackground,
codeBorder: codeBorder, codeBorder: codeBorder,
codeText: codeText, codeText: codeText,
@@ -269,6 +285,8 @@ class AppColorTokens extends ThemeExtension<AppColorTokens> {
Color? overlayWeak, Color? overlayWeak,
Color? overlayMedium, Color? overlayMedium,
Color? overlayStrong, Color? overlayStrong,
Color? scrimMedium,
Color? scrimStrong,
Color? codeBackground, Color? codeBackground,
Color? codeBorder, Color? codeBorder,
Color? codeText, Color? codeText,
@@ -303,6 +321,8 @@ class AppColorTokens extends ThemeExtension<AppColorTokens> {
overlayWeak: overlayWeak ?? this.overlayWeak, overlayWeak: overlayWeak ?? this.overlayWeak,
overlayMedium: overlayMedium ?? this.overlayMedium, overlayMedium: overlayMedium ?? this.overlayMedium,
overlayStrong: overlayStrong ?? this.overlayStrong, overlayStrong: overlayStrong ?? this.overlayStrong,
scrimMedium: scrimMedium ?? this.scrimMedium,
scrimStrong: scrimStrong ?? this.scrimStrong,
codeBackground: codeBackground ?? this.codeBackground, codeBackground: codeBackground ?? this.codeBackground,
codeBorder: codeBorder ?? this.codeBorder, codeBorder: codeBorder ?? this.codeBorder,
codeText: codeText ?? this.codeText, codeText: codeText ?? this.codeText,
@@ -364,6 +384,8 @@ class AppColorTokens extends ThemeExtension<AppColorTokens> {
overlayWeak: Color.lerp(overlayWeak, other.overlayWeak, t)!, overlayWeak: Color.lerp(overlayWeak, other.overlayWeak, t)!,
overlayMedium: Color.lerp(overlayMedium, other.overlayMedium, t)!, overlayMedium: Color.lerp(overlayMedium, other.overlayMedium, t)!,
overlayStrong: Color.lerp(overlayStrong, other.overlayStrong, t)!, overlayStrong: Color.lerp(overlayStrong, other.overlayStrong, t)!,
scrimMedium: Color.lerp(scrimMedium, other.scrimMedium, t)!,
scrimStrong: Color.lerp(scrimStrong, other.scrimStrong, t)!,
codeBackground: Color.lerp(codeBackground, other.codeBackground, t)!, codeBackground: Color.lerp(codeBackground, other.codeBackground, t)!,
codeBorder: Color.lerp(codeBorder, other.codeBorder, t)!, codeBorder: Color.lerp(codeBorder, other.codeBorder, t)!,
codeText: Color.lerp(codeText, other.codeText, t)!, codeText: Color.lerp(codeText, other.codeText, t)!,

View File

@@ -3,9 +3,6 @@ import 'dart:io' show Platform;
import 'package:conduit/core/providers/app_providers.dart'; import 'package:conduit/core/providers/app_providers.dart';
import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/l10n/app_localizations.dart';
import 'package:conduit/shared/theme/theme_extensions.dart'; import 'package:conduit/shared/theme/theme_extensions.dart';
import 'package:conduit/shared/widgets/conduit_components.dart';
import 'package:conduit/shared/widgets/modal_safe_area.dart';
import 'package:conduit/shared/widgets/sheet_handle.dart';
import 'package:conduit/shared/widgets/themed_dialogs.dart'; import 'package:conduit/shared/widgets/themed_dialogs.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -35,74 +32,78 @@ class ConduitContextMenuAction {
Future<void> showConduitContextMenu({ Future<void> showConduitContextMenu({
required BuildContext context, required BuildContext context,
required List<ConduitContextMenuAction> actions, required List<ConduitContextMenuAction> actions,
Offset? position,
}) async { }) async {
if (actions.isEmpty) return; if (actions.isEmpty) return;
final theme = context.conduitTheme; final theme = context.conduitTheme;
final RenderBox? overlay =
Overlay.of(context).context.findRenderObject() as RenderBox?;
await showModalBottomSheet( if (overlay == null) return;
// Determine menu position
final Offset menuPosition = position ?? _getDefaultMenuPosition(context);
final result = await showMenu<ConduitContextMenuAction>(
context: context, context: context,
backgroundColor: Colors.transparent, position: RelativeRect.fromLTRB(
builder: (sheetContext) { menuPosition.dx,
Future<void> handleAction(ConduitContextMenuAction action) async { menuPosition.dy,
action.onBeforeClose?.call(); overlay.size.width - menuPosition.dx,
Navigator.of(sheetContext).pop(); overlay.size.height - menuPosition.dy,
await Future.microtask(action.onSelected); ),
} shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.small),
List<Widget> buildActionTiles() { ),
return actions color: theme.surfaceBackground,
.map( elevation: 4,
(action) => ConduitListItem( items: actions.map((action) {
isCompact: true, return PopupMenuItem<ConduitContextMenuAction>(
leading: Icon( value: action,
Platform.isIOS ? action.cupertinoIcon : action.materialIcon,
color: action.destructive ? Colors.red : theme.iconPrimary,
size: IconSize.modal,
),
title: Text(
action.label,
style: AppTypography.standard.copyWith(
color: action.destructive ? Colors.red : theme.textPrimary,
fontWeight: FontWeight.w500,
),
),
onTap: () => handleAction(action),
),
)
.toList();
}
final actionTiles = buildActionTiles();
return ModalSheetSafeArea(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: Spacing.screenPadding, horizontal: Spacing.sm,
vertical: Spacing.screenPadding, vertical: Spacing.xxs,
), ),
child: Container( height: 36,
decoration: BoxDecoration( child: Row(
color: theme.surfaceBackground, children: [
borderRadius: BorderRadius.circular(AppBorderRadius.lg), Icon(
boxShadow: ConduitShadows.modal(context), Platform.isIOS ? action.cupertinoIcon : action.materialIcon,
), color: action.destructive ? Colors.red : theme.iconPrimary,
child: Column( size: IconSize.xs,
mainAxisSize: MainAxisSize.min, ),
children: [ const SizedBox(width: Spacing.sm),
const SizedBox(height: Spacing.sm), Expanded(
const SheetHandle(), child: Text(
const SizedBox(height: Spacing.sm), action.label,
for (var i = 0; i < actionTiles.length; i++) ...[ style: AppTypography.standard.copyWith(
if (i != 0) const ConduitDivider(isCompact: true), color: action.destructive ? Colors.red : theme.textPrimary,
actionTiles[i], fontWeight: FontWeight.w500,
], fontSize: 14,
const SizedBox(height: Spacing.sm), ),
], ),
), ),
],
), ),
); );
}, }).toList(),
); );
if (result != null) {
result.onBeforeClose?.call();
await Future.microtask(result.onSelected);
}
}
Offset _getDefaultMenuPosition(BuildContext context) {
final RenderBox? renderBox = context.findRenderObject() as RenderBox?;
if (renderBox == null) {
return Offset.zero;
}
final position = renderBox.localToGlobal(Offset.zero);
final size = renderBox.size;
return Offset(position.dx + size.width, position.dy);
} }
Future<void> showConversationContextMenu({ Future<void> showConversationContextMenu({