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:
parent
cfc0a97da7
commit
1f1bf18a75
|
|
@ -1,10 +1,8 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:super_clipboard/super_clipboard.dart';
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import '../../../../core/theme/app_colors.dart';
|
import '../../../../core/theme/app_colors.dart';
|
||||||
import '../../domain/entities/chat_message.dart';
|
import '../../domain/entities/chat_message.dart';
|
||||||
|
|
@ -93,11 +91,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
title: const Text('拍照'),
|
title: const Text('拍照'),
|
||||||
onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); },
|
onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); },
|
||||||
),
|
),
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.content_paste),
|
|
||||||
title: const Text('从剪贴板粘贴'),
|
|
||||||
onTap: () { Navigator.pop(ctx); _pasteFromClipboard(); },
|
|
||||||
),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.attach_file),
|
leading: const Icon(Icons.attach_file),
|
||||||
title: const Text('选择文件'),
|
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 {
|
Future<void> _pickFile() async {
|
||||||
final remaining = _maxAttachments - _pendingAttachments.length;
|
final remaining = _maxAttachments - _pendingAttachments.length;
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
|
|
@ -673,6 +614,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||||
controller: _messageController,
|
controller: _messageController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: isStreaming ? '追加指令...' : '输入指令...',
|
hintText: isStreaming ? '追加指令...' : '输入指令...',
|
||||||
|
hintStyle: TextStyle(color: AppColors.textMuted),
|
||||||
border: const OutlineInputBorder(
|
border: const OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(24)),
|
borderRadius: BorderRadius.all(Radius.circular(24)),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,16 @@ class TimelineEventNode extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Use CustomPaint for the timeline line to avoid IntrinsicHeight which
|
// Use CustomPaint for the timeline line to avoid IntrinsicHeight which
|
||||||
// causes bottom-overflow on long content.
|
// 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(
|
return CustomPaint(
|
||||||
painter: _TimelineLinePainter(
|
painter: _TimelineLinePainter(
|
||||||
color: AppColors.surfaceLight,
|
color: AppColors.surfaceLight,
|
||||||
isFirst: isFirst,
|
isFirst: isFirst,
|
||||||
isLast: isLast,
|
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
|
lineX: 14, // horizontal center of the 28px left column
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -109,14 +113,16 @@ class _TimelineLinePainter extends CustomPainter {
|
||||||
final Color color;
|
final Color color;
|
||||||
final bool isFirst;
|
final bool isFirst;
|
||||||
final bool isLast;
|
final bool isLast;
|
||||||
final double dotOffset;
|
final double dotCenter;
|
||||||
|
final double dotRadius;
|
||||||
final double lineX;
|
final double lineX;
|
||||||
|
|
||||||
_TimelineLinePainter({
|
_TimelineLinePainter({
|
||||||
required this.color,
|
required this.color,
|
||||||
required this.isFirst,
|
required this.isFirst,
|
||||||
required this.isLast,
|
required this.isLast,
|
||||||
required this.dotOffset,
|
required this.dotCenter,
|
||||||
|
required this.dotRadius,
|
||||||
required this.lineX,
|
required this.lineX,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -126,15 +132,15 @@ class _TimelineLinePainter extends CustomPainter {
|
||||||
..color = color
|
..color = color
|
||||||
..strokeWidth = 1.5;
|
..strokeWidth = 1.5;
|
||||||
|
|
||||||
// Line above the dot
|
// Line above the dot/icon
|
||||||
if (!isFirst) {
|
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) {
|
if (!isLast) {
|
||||||
canvas.drawLine(
|
canvas.drawLine(
|
||||||
Offset(lineX, dotOffset + 10),
|
Offset(lineX, dotCenter + dotRadius),
|
||||||
Offset(lineX, size.height),
|
Offset(lineX, size.height),
|
||||||
paint,
|
paint,
|
||||||
);
|
);
|
||||||
|
|
@ -145,7 +151,8 @@ class _TimelineLinePainter extends CustomPainter {
|
||||||
bool shouldRepaint(_TimelineLinePainter oldDelegate) =>
|
bool shouldRepaint(_TimelineLinePainter oldDelegate) =>
|
||||||
color != oldDelegate.color ||
|
color != oldDelegate.color ||
|
||||||
isFirst != oldDelegate.isFirst ||
|
isFirst != oldDelegate.isFirst ||
|
||||||
isLast != oldDelegate.isLast;
|
isLast != oldDelegate.isLast ||
|
||||||
|
dotRadius != oldDelegate.dotRadius;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Static colored dot with optional icon for completed/error/idle states.
|
/// Static colored dot with optional icon for completed/error/idle states.
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ dependencies:
|
||||||
gpt_markdown: ^1.1.5
|
gpt_markdown: ^1.1.5
|
||||||
flutter_svg: ^2.0.10+1
|
flutter_svg: ^2.0.10+1
|
||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
super_clipboard: ^0.8.1
|
|
||||||
file_picker: ^8.0.0
|
file_picker: ^8.0.0
|
||||||
|
|
||||||
# Voice
|
# Voice
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue