fix: remove clipboard paste menu item, fix timeline line overlap, dim input placeholder

- Remove redundant "从剪贴板粘贴" option from attachment menu (long-press to paste natively)
- Remove super_clipboard dependency (no longer needed)
- Fix timeline vertical line overlapping icon nodes by using dynamic dotRadius
- Dim input field placeholder color to AppColors.textMuted

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-28 05:05:27 -08:00
parent cfc0a97da7
commit 1f1bf18a75
3 changed files with 16 additions and 68 deletions

View File

@ -1,10 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:super_clipboard/super_clipboard.dart';
import 'package:file_picker/file_picker.dart';
import '../../../../core/theme/app_colors.dart';
import '../../domain/entities/chat_message.dart';
@ -93,11 +91,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
title: const Text('拍照'),
onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); },
),
ListTile(
leading: const Icon(Icons.content_paste),
title: const Text('从剪贴板粘贴'),
onTap: () { Navigator.pop(ctx); _pasteFromClipboard(); },
),
ListTile(
leading: const Icon(Icons.attach_file),
title: const Text('选择文件'),
@ -193,58 +186,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
}
}
Future<void> _pasteFromClipboard() async {
final clipboard = SystemClipboard.instance;
if (clipboard == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('当前设备不支持剪贴板读取')),
);
}
return;
}
try {
final reader = await clipboard.read();
for (final item in reader.items) {
// Try PNG first, then JPEG
for (final format in [Formats.png, Formats.jpeg]) {
if (item.canProvide(format)) {
final completer = Completer<List<int>>();
item.getFile(format, (file) async {
try {
final bytes = await file.readAll();
completer.complete(bytes);
} catch (e) {
completer.completeError(e);
}
}, onError: (e) {
if (!completer.isCompleted) completer.completeError(e);
});
final bytes = await completer.future;
final mediaType = format == Formats.png ? 'image/png' : 'image/jpeg';
setState(() {
_pendingAttachments.add(ChatAttachment(
base64Data: base64Encode(bytes),
mediaType: mediaType,
fileName: 'clipboard.${format == Formats.png ? "png" : "jpg"}',
));
});
return;
}
}
}
} catch (e) {
// Clipboard read failed
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('剪贴板中没有图片')),
);
}
}
Future<void> _pickFile() async {
final remaining = _maxAttachments - _pendingAttachments.length;
if (remaining <= 0) {
@ -673,6 +614,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
controller: _messageController,
decoration: InputDecoration(
hintText: isStreaming ? '追加指令...' : '输入指令...',
hintStyle: TextStyle(color: AppColors.textMuted),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(24)),
),

View File

@ -40,12 +40,16 @@ class TimelineEventNode extends StatelessWidget {
Widget build(BuildContext context) {
// Use CustomPaint for the timeline line to avoid IntrinsicHeight which
// causes bottom-overflow on long content.
// Icon nodes are 16px tall, plain dots are 10px. The gap in the
// vertical line must be large enough to avoid overlapping either.
final dotRadius = icon != null ? 10.0 : 6.0;
return CustomPaint(
painter: _TimelineLinePainter(
color: AppColors.surfaceLight,
isFirst: isFirst,
isLast: isLast,
dotOffset: 14, // vertical center of the dot area
dotCenter: 14, // vertical center of the dot area
dotRadius: dotRadius,
lineX: 14, // horizontal center of the 28px left column
),
child: Row(
@ -109,14 +113,16 @@ class _TimelineLinePainter extends CustomPainter {
final Color color;
final bool isFirst;
final bool isLast;
final double dotOffset;
final double dotCenter;
final double dotRadius;
final double lineX;
_TimelineLinePainter({
required this.color,
required this.isFirst,
required this.isLast,
required this.dotOffset,
required this.dotCenter,
required this.dotRadius,
required this.lineX,
});
@ -126,15 +132,15 @@ class _TimelineLinePainter extends CustomPainter {
..color = color
..strokeWidth = 1.5;
// Line above the dot
// Line above the dot/icon
if (!isFirst) {
canvas.drawLine(Offset(lineX, 0), Offset(lineX, dotOffset), paint);
canvas.drawLine(Offset(lineX, 0), Offset(lineX, dotCenter - dotRadius), paint);
}
// Line below the dot
// Line below the dot/icon
if (!isLast) {
canvas.drawLine(
Offset(lineX, dotOffset + 10),
Offset(lineX, dotCenter + dotRadius),
Offset(lineX, size.height),
paint,
);
@ -145,7 +151,8 @@ class _TimelineLinePainter extends CustomPainter {
bool shouldRepaint(_TimelineLinePainter oldDelegate) =>
color != oldDelegate.color ||
isFirst != oldDelegate.isFirst ||
isLast != oldDelegate.isLast;
isLast != oldDelegate.isLast ||
dotRadius != oldDelegate.dotRadius;
}
/// Static colored dot with optional icon for completed/error/idle states.

View File

@ -37,7 +37,6 @@ dependencies:
gpt_markdown: ^1.1.5
flutter_svg: ^2.0.10+1
image_picker: ^1.1.2
super_clipboard: ^0.8.1
file_picker: ^8.0.0
# Voice