feat(attachments): drag-and-drop file attachments on the detail pane

Drop a file anywhere on the detail pane to attach it: pane-wide drop target
with a 'Drop to attach' hover overlay (Copy cursor, gated on an idle selected
task), an explicit lingering confirmation/error line, plus an Attachments list
with size, remove, and an Add file… picker in the DETAILS card. ComposedPreview
now shows the reference files too. en/de keys added.
This commit is contained in:
Mika Kuns
2026-06-22 17:42:52 +02:00
parent f7e946e472
commit d8ff8cc110
7 changed files with 330 additions and 4 deletions

View File

@@ -1,8 +1,10 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Reactive;
using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.Views.Modals;
@@ -27,6 +29,78 @@ public partial class DetailsIslandView : UserControl
// row Min/Max during a drag, so the console stops shrinking at 1/3.
DetailBodyGrid.GetObservable(BoundsProperty)
.Subscribe(new AnonymousObserver<Rect>(_ => UpdateRowLimits()));
AddHandler(DragDrop.DragEnterEvent, OnDragEnter);
AddHandler(DragDrop.DragOverEvent, OnDragOver);
AddHandler(DragDrop.DragLeaveEvent, OnDragLeave);
AddHandler(DragDrop.DropEvent, OnDrop);
}
private static bool IsFilesDrop(DragEventArgs e)
=> e.DataTransfer?.Contains(DataFormat.File) == true;
private void OnDragEnter(object? sender, DragEventArgs e)
{
if (_vm is { CanAcceptDrop: true } && IsFilesDrop(e))
{
e.DragEffects = DragDropEffects.Copy;
_vm.IsDragOver = true;
}
else
{
e.DragEffects = DragDropEffects.None;
}
e.Handled = true;
}
private void OnDragOver(object? sender, DragEventArgs e)
{
if (_vm is { CanAcceptDrop: true } && IsFilesDrop(e))
{
e.DragEffects = DragDropEffects.Copy;
_vm.IsDragOver = true;
}
else
{
e.DragEffects = DragDropEffects.None;
}
e.Handled = true;
}
private void OnDragLeave(object? sender, RoutedEventArgs e)
{
if (_vm != null) _vm.IsDragOver = false;
}
private async void OnDrop(object? sender, DragEventArgs e)
{
if (_vm != null) _vm.IsDragOver = false;
if (_vm is not { CanAcceptDrop: true } || !IsFilesDrop(e)) return;
e.Handled = true;
var items = e.DataTransfer.TryGetFiles();
if (items is null) return;
var files = new List<(string FileName, System.IO.Stream Content)>();
foreach (var item in items)
{
if (item is IStorageFile sf)
{
var stream = await sf.OpenReadAsync();
files.Add((sf.Name, stream));
}
}
if (files.Count == 0) return;
try
{
await _vm.AddFilesAsync(files);
}
finally
{
foreach (var (_, s) in files) await s.DisposeAsync();
}
}
private void UpdateRowLimits()