mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-06 14:23:19 +08:00
feat: 布局优化
This commit is contained in:
@@ -27,46 +27,8 @@ class _AgentComposer extends ConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller,
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: InputDecoration(
|
||||
hintText: l.tr('agent.inputHint'),
|
||||
filled: true,
|
||||
fillColor: context.colors.chip,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AgentUiTokens.radiusMedium,
|
||||
),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
suffixIcon: state.conversation.running
|
||||
? IconButton(
|
||||
tooltip: l.tr('agent.stop'),
|
||||
onPressed: ref
|
||||
.read(agentControllerProvider.notifier)
|
||||
.stop,
|
||||
icon: Icon(
|
||||
Icons.stop_circle,
|
||||
color: context.colors.danger,
|
||||
),
|
||||
)
|
||||
: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: controller,
|
||||
builder: (context, value, _) => IconButton(
|
||||
tooltip: l.tr('agent.send'),
|
||||
onPressed: state.canSendDraft(value.text)
|
||||
? onSend
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_upward_rounded),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
key: const Key('agent-composer-options'),
|
||||
children: [
|
||||
Expanded(
|
||||
child: _ComposerPickerButton(
|
||||
@@ -105,6 +67,46 @@ class _AgentComposer extends ConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
key: const Key('agent-composer-input'),
|
||||
controller: controller,
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
textInputAction: TextInputAction.newline,
|
||||
decoration: InputDecoration(
|
||||
hintText: l.tr('agent.inputHint'),
|
||||
filled: true,
|
||||
fillColor: context.colors.chip,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AgentUiTokens.radiusMedium,
|
||||
),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
suffixIcon: state.conversation.running
|
||||
? IconButton(
|
||||
tooltip: l.tr('agent.stop'),
|
||||
onPressed: ref
|
||||
.read(agentControllerProvider.notifier)
|
||||
.stop,
|
||||
icon: Icon(
|
||||
Icons.stop_circle,
|
||||
color: context.colors.danger,
|
||||
),
|
||||
)
|
||||
: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: controller,
|
||||
builder: (context, value, _) => IconButton(
|
||||
tooltip: l.tr('agent.send'),
|
||||
onPressed: state.canSendDraft(value.text)
|
||||
? onSend
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_upward_rounded),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -91,27 +91,36 @@ class _AgentGlobalOverlayState extends ConsumerState<AgentGlobalOverlay> {
|
||||
final bottomInset = media.viewInsets.bottom > 0
|
||||
? media.viewInsets.bottom + AgentUiTokens.edgeGap
|
||||
: media.padding.bottom + 84;
|
||||
final maxX = constraints.maxWidth - size - AgentUiTokens.edgeGap;
|
||||
const minX = AgentUiTokens.entryHorizontalInset;
|
||||
final maxX =
|
||||
(constraints.maxWidth -
|
||||
size -
|
||||
AgentUiTokens.entryHorizontalInset)
|
||||
.clamp(minX, double.infinity)
|
||||
.toDouble();
|
||||
final maxY = (constraints.maxHeight - size - bottomInset)
|
||||
.clamp(topInset, double.infinity)
|
||||
.toDouble();
|
||||
final range = (maxY - topInset)
|
||||
.clamp(1.0, double.infinity)
|
||||
.toDouble();
|
||||
_lastX =
|
||||
(_dragX ?? (_side == 'left' ? AgentUiTokens.edgeGap : maxX))
|
||||
.clamp(AgentUiTokens.edgeGap, maxX)
|
||||
.toDouble();
|
||||
_lastX = (_dragX ?? (_side == 'left' ? minX : maxX))
|
||||
.clamp(minX, maxX)
|
||||
.toDouble();
|
||||
_lastY = (_dragY ?? topInset + range * _yFraction)
|
||||
.clamp(topInset, maxY)
|
||||
.toDouble();
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
AnimatedPositioned(
|
||||
left: _lastX,
|
||||
top: _lastY,
|
||||
width: size,
|
||||
height: size,
|
||||
duration: _dragX == null
|
||||
? AgentUiTokens.entrySnapDuration
|
||||
: Duration.zero,
|
||||
curve: Curves.easeOutCubic,
|
||||
child: FocusableActionDetector(
|
||||
onShowFocusHighlight: (value) {
|
||||
if (mounted) setState(() => _entryFocused = value);
|
||||
@@ -148,7 +157,7 @@ class _AgentGlobalOverlayState extends ConsumerState<AgentGlobalOverlay> {
|
||||
onPanUpdate: (details) {
|
||||
setState(() {
|
||||
_dragX = (_dragX! + details.delta.dx)
|
||||
.clamp(AgentUiTokens.edgeGap, maxX)
|
||||
.clamp(minX, maxX)
|
||||
.toDouble();
|
||||
_dragY = (_dragY! + details.delta.dy)
|
||||
.clamp(topInset, maxY)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
abstract final class AgentUiTokens {
|
||||
static const compactBreakpoint = 720.0;
|
||||
static const entrySize = 56.0;
|
||||
static const entryHorizontalInset = 0.0;
|
||||
static const entrySnapDuration = Duration(milliseconds: 320);
|
||||
static const edgeGap = 12.0;
|
||||
static const wideWidth = 480.0;
|
||||
static const wideHeight = 720.0;
|
||||
|
||||
@@ -182,11 +182,42 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
final entry = find.byKey(const Key('agent-global-entry'));
|
||||
final logicalWidth =
|
||||
tester.view.physicalSize.width / tester.view.devicePixelRatio;
|
||||
expect(tester.getTopRight(entry).dx, closeTo(logicalWidth, 0.01));
|
||||
|
||||
await tester.drag(entry, const Offset(-700, -120));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(prefs.agentEntrySide, 'left');
|
||||
expect(prefs.agentEntryY, inInclusiveRange(0.05, 0.95));
|
||||
expect(tester.getTopLeft(entry).dx, lessThan(20));
|
||||
expect(tester.getTopLeft(entry).dx, closeTo(0, 0.01));
|
||||
});
|
||||
|
||||
testWidgets('animates to the nearest edge after release', (tester) async {
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
final prefs = await storage();
|
||||
final observer = AgentNavigationObserver();
|
||||
addTearDown(observer.dispose);
|
||||
await tester.pumpWidget(app(storage: prefs, observer: observer));
|
||||
await tester.pump();
|
||||
|
||||
final entry = find.byKey(const Key('agent-global-entry'));
|
||||
await tester.drag(entry, const Offset(-200, 0));
|
||||
await tester.pump();
|
||||
final releaseX = tester.getTopLeft(entry).dx;
|
||||
expect(releaseX, greaterThan(0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 80));
|
||||
|
||||
final transitioningX = tester.getTopLeft(entry).dx;
|
||||
expect(transitioningX, greaterThan(0));
|
||||
expect(transitioningX, lessThan(releaseX));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getTopLeft(entry).dx, closeTo(0, 0.01));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user