feat(ui): Refactor context menu with platform-specific styling

feat(navigation): migrate to super_drag_and_drop for folder drag and drop
feat(ui): Add context menu preview builders for chat and notes
refactor(ui): Remove preview builders and simplify note card rendering
This commit is contained in:
cogwheel
2025-12-20 18:26:03 +05:30
parent 622dcf9142
commit 97ace86b12
13 changed files with 1032 additions and 724 deletions

View File

@@ -709,39 +709,50 @@ class _NoteEditorPageState extends ConsumerState<NoteEditorPage> {
),
),
),
// Actions (more menu)
// Actions (more menu) - uses PopupMenuButton for tap interaction
Padding(
padding: const EdgeInsets.only(right: Spacing.inputPadding),
child: Center(
child: PopupMenuButton<String>(
tooltip: '',
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md),
),
onSelected: (value) {
switch (value) {
case 'generate_title':
case 'generate':
HapticFeedback.selectionClick();
_generateTitle();
case 'copy':
HapticFeedback.selectionClick();
_copyToClipboard();
case 'delete':
HapticFeedback.mediumImpact();
_deleteNote();
}
},
offset: const Offset(0, 44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
AppBorderRadius.card,
),
),
color: conduitTheme.surfaceContainer,
itemBuilder: (context) => [
PopupMenuItem(
value: 'generate_title',
value: 'generate',
child: Row(
children: [
Icon(
Platform.isIOS
? CupertinoIcons.sparkles
: Icons.auto_awesome_rounded,
color: conduitTheme.buttonPrimary,
size: IconSize.md,
size: IconSize.small,
color: conduitTheme.textPrimary,
),
const SizedBox(width: Spacing.sm),
Text(l10n.generateTitle),
Text(
l10n.generateTitle,
style: TextStyle(
color: conduitTheme.textPrimary,
),
),
],
),
),
@@ -753,11 +764,16 @@ class _NoteEditorPageState extends ConsumerState<NoteEditorPage> {
Platform.isIOS
? CupertinoIcons.doc_on_clipboard
: Icons.copy_rounded,
color: conduitTheme.iconPrimary,
size: IconSize.md,
size: IconSize.small,
color: conduitTheme.textPrimary,
),
const SizedBox(width: Spacing.sm),
Text(l10n.copy),
Text(
l10n.copy,
style: TextStyle(
color: conduitTheme.textPrimary,
),
),
],
),
),
@@ -769,13 +785,15 @@ class _NoteEditorPageState extends ConsumerState<NoteEditorPage> {
Platform.isIOS
? CupertinoIcons.delete
: Icons.delete_rounded,
size: IconSize.small,
color: conduitTheme.error,
size: IconSize.md,
),
const SizedBox(width: Spacing.sm),
Text(
l10n.delete,
style: TextStyle(color: conduitTheme.error),
style: TextStyle(
color: conduitTheme.error,
),
),
],
),
@@ -823,17 +841,13 @@ class _NoteEditorPageState extends ConsumerState<NoteEditorPage> {
}
Widget _buildFloatingMetadataBar(BuildContext context) {
final theme = Theme.of(context);
final conduitTheme = context.conduitTheme;
final l10n = AppLocalizations.of(context)!;
final isDark = theme.brightness == Brightness.dark;
final backgroundColor = isDark
? Color.lerp(conduitTheme.cardBackground, Colors.white, 0.08)!
: Color.lerp(conduitTheme.inputBackground, Colors.black, 0.06)!;
final borderColor = conduitTheme.cardBorder.withValues(
alpha: isDark ? 0.65 : 0.55,
// Use consistent colors with the floating app bar pills
final backgroundColor = conduitTheme.surfaceContainer.withValues(alpha: 0.9);
final borderColor = conduitTheme.surfaceContainerHighest.withValues(
alpha: 0.4,
);
final dateFormat = DateFormat.MMMd();
@@ -898,7 +912,7 @@ class _NoteEditorPageState extends ConsumerState<NoteEditorPage> {
child: Text(
'·',
style: AppTypography.tiny.copyWith(
color: theme.textTertiary.withValues(alpha: 0.5),
color: theme.textSecondary.withValues(alpha: 0.5),
),
),
);
@@ -921,14 +935,14 @@ class _NoteEditorPageState extends ConsumerState<NoteEditorPage> {
children: [
Icon(
icon,
color: theme.textTertiary.withValues(alpha: 0.7),
color: theme.textSecondary,
size: IconSize.xs,
),
const SizedBox(width: Spacing.xxs),
Text(
label,
style: AppTypography.tiny.copyWith(
color: theme.textTertiary.withValues(alpha: 0.7),
color: theme.textSecondary,
fontWeight: FontWeight.w500,
),
),

View File

@@ -414,33 +414,40 @@ class _NotesListPageState extends ConsumerState<NotesListPage> {
return Colors.transparent;
}
return Padding(
padding: const EdgeInsets.only(bottom: Spacing.sm),
child: Container(
decoration: BoxDecoration(
color: sidebarTheme.accent.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(AppBorderRadius.card),
border: Border.all(
color: sidebarTheme.border.withValues(alpha: 0.15),
width: BorderWidth.thin,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
BoxShadow(
color: Colors.black.withValues(alpha: 0.02),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
),
// Compute opaque background for proper context menu snapshot rendering
final cardBackground = Color.alphaBlend(
sidebarTheme.accent.withValues(alpha: 0.5),
sidebarTheme.background,
);
return ConduitContextMenu(
actions: _buildNoteActions(context, note),
child: Padding(
padding: const EdgeInsets.only(bottom: Spacing.sm),
child: Material(
color: Colors.transparent,
color: cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.card),
child: InkWell(
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppBorderRadius.card),
border: Border.all(
color: sidebarTheme.border.withValues(alpha: 0.15),
width: BorderWidth.thin,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
BoxShadow(
color: Colors.black.withValues(alpha: 0.02),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
),
child: InkWell(
borderRadius: BorderRadius.circular(AppBorderRadius.card),
overlayColor: WidgetStateProperty.resolveWith(overlayForStates),
onTap: () {
@@ -450,7 +457,7 @@ class _NotesListPageState extends ConsumerState<NotesListPage> {
pathParameters: {'id': note.id},
);
},
onLongPress: () => _showNoteContextMenu(context, note),
onLongPress: null, // Handled by ConduitContextMenu
child: Padding(
padding: const EdgeInsets.all(Spacing.md),
child: Row(
@@ -558,32 +565,13 @@ class _NotesListPageState extends ConsumerState<NotesListPage> {
],
),
),
// More button
Builder(
builder: (buttonContext) => IconButton(
icon: Icon(
Platform.isIOS
? CupertinoIcons.ellipsis
: Icons.more_vert_rounded,
color: sidebarTheme.foreground.withValues(alpha: 0.5),
size: IconSize.md,
),
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: TouchTarget.badge,
minHeight: TouchTarget.badge,
),
onPressed: () =>
_showNoteContextMenu(buttonContext, note),
),
),
],
),
),
),
),
),
),
);
}
@@ -594,51 +582,51 @@ class _NotesListPageState extends ConsumerState<NotesListPage> {
date.day == now.day;
}
void _showNoteContextMenu(BuildContext context, Note note) {
List<ConduitContextMenuAction> _buildNoteActions(
BuildContext context,
Note note,
) {
final l10n = AppLocalizations.of(context)!;
showConduitContextMenu(
context: context,
actions: [
ConduitContextMenuAction(
cupertinoIcon: CupertinoIcons.pencil,
materialIcon: Icons.edit_rounded,
label: l10n.edit,
onBeforeClose: () => HapticFeedback.selectionClick(),
onSelected: () async {
context.pushNamed(
RouteNames.noteEditor,
pathParameters: {'id': note.id},
);
},
),
ConduitContextMenuAction(
cupertinoIcon: CupertinoIcons.doc_on_clipboard,
materialIcon: Icons.copy_rounded,
label: l10n.copy,
onBeforeClose: () => HapticFeedback.selectionClick(),
onSelected: () async {
final messenger = ScaffoldMessenger.of(context);
await Clipboard.setData(ClipboardData(text: note.markdownContent));
if (!mounted) return;
messenger.showSnackBar(
SnackBar(
content: Text(l10n.noteCopiedToClipboard),
duration: const Duration(seconds: 2),
),
);
},
),
ConduitContextMenuAction(
cupertinoIcon: CupertinoIcons.delete,
materialIcon: Icons.delete_rounded,
label: l10n.delete,
destructive: true,
onBeforeClose: () => HapticFeedback.mediumImpact(),
onSelected: () async => _deleteNote(note),
),
],
);
return [
ConduitContextMenuAction(
cupertinoIcon: CupertinoIcons.pencil,
materialIcon: Icons.edit_rounded,
label: l10n.edit,
onBeforeClose: () => HapticFeedback.selectionClick(),
onSelected: () async {
context.pushNamed(
RouteNames.noteEditor,
pathParameters: {'id': note.id},
);
},
),
ConduitContextMenuAction(
cupertinoIcon: CupertinoIcons.doc_on_clipboard,
materialIcon: Icons.copy_rounded,
label: l10n.copy,
onBeforeClose: () => HapticFeedback.selectionClick(),
onSelected: () async {
final messenger = ScaffoldMessenger.of(context);
await Clipboard.setData(ClipboardData(text: note.markdownContent));
if (!mounted) return;
messenger.showSnackBar(
SnackBar(
content: Text(l10n.noteCopiedToClipboard),
duration: const Duration(seconds: 2),
),
);
},
),
ConduitContextMenuAction(
cupertinoIcon: CupertinoIcons.delete,
materialIcon: Icons.delete_rounded,
label: l10n.delete,
destructive: true,
onBeforeClose: () => HapticFeedback.mediumImpact(),
onSelected: () async => _deleteNote(note),
),
];
}
Widget _buildEmptyState(BuildContext context) {