improve Frontend

This commit is contained in:
Mika Kuns
2026-04-22 17:09:00 +02:00
parent 7de5510735
commit a4e313dbad
12 changed files with 437 additions and 12 deletions

View File

@@ -3,10 +3,23 @@
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
x:Class="ClaudeDo.Ui.Views.Islands.TaskRowView"
x:DataType="vm:TaskRowViewModel">
<Border Classes="task-row"
Classes.selected="{Binding IsSelected}"
Classes.done="{Binding Done}">
<Grid ColumnDefinitions="4,32,*,32" Margin="6,8,10,8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="6"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Above-row indicator: lives in the 6px gap between cards -->
<Border Grid.Row="0" Height="2" VerticalAlignment="Center" Margin="4,0"
Background="{DynamicResource MossBrush}" CornerRadius="1"
IsVisible="{Binding DropHintAbove}"/>
<Border Grid.Row="1" Classes="task-row"
Margin="0"
Classes.selected="{Binding IsSelected}"
Classes.done="{Binding Done}">
<Grid ColumnDefinitions="4,32,*,32" Margin="6,8,10,8">
<!-- Left accent bar (visible when selected) -->
<Border Grid.Column="0" Classes="task-row-accent"
@@ -108,6 +121,13 @@
CommandParameter="{Binding}">
<PathIcon Width="14" Height="14" Data="{StaticResource Icon.Star}"/>
</Button>
</Grid>
</Border>
<!-- Below-row indicator: only expands when visible (used for the last row of a section) -->
<Grid Grid.Row="2" Height="6" IsVisible="{Binding DropHintBelow}">
<Border Height="2" VerticalAlignment="Center" Margin="4,0"
Background="{DynamicResource MossBrush}" CornerRadius="1"/>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@@ -80,7 +80,13 @@
<Button Classes="flat" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).SelectCommand}"
CommandParameter="{Binding}">
CommandParameter="{Binding}"
PointerPressed="OnRowPointerPressed"
PointerMoved="OnRowPointerMoved"
PointerReleased="OnRowPointerReleased"
DragDrop.AllowDrop="True"
DragDrop.DragOver="OnRowDragOver"
DragDrop.Drop="OnRowDrop">
<islands:TaskRowView/>
</Button>
</DataTemplate>
@@ -99,7 +105,13 @@
<Button Classes="flat" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).SelectCommand}"
CommandParameter="{Binding}">
CommandParameter="{Binding}"
PointerPressed="OnRowPointerPressed"
PointerMoved="OnRowPointerMoved"
PointerReleased="OnRowPointerReleased"
DragDrop.AllowDrop="True"
DragDrop.DragOver="OnRowDragOver"
DragDrop.Drop="OnRowDrop">
<islands:TaskRowView/>
</Button>
</DataTemplate>
@@ -123,7 +135,13 @@
<Button Classes="flat" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).SelectCommand}"
CommandParameter="{Binding}">
CommandParameter="{Binding}"
PointerPressed="OnRowPointerPressed"
PointerMoved="OnRowPointerMoved"
PointerReleased="OnRowPointerReleased"
DragDrop.AllowDrop="True"
DragDrop.DragOver="OnRowDragOver"
DragDrop.Drop="OnRowDrop">
<islands:TaskRowView/>
</Button>
</DataTemplate>

View File

@@ -1,17 +1,150 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels.Islands;
namespace ClaudeDo.Ui.Views.Islands;
public partial class TasksIslandView : UserControl
{
private static readonly DataFormat<string> TaskRowFormat =
DataFormat.CreateStringApplicationFormat("claudedo-task-row");
public TasksIslandView()
{
InitializeComponent();
// Tunnel handler runs BEFORE Button's class handler so we can start a drag
// without the Button first marking the event as handled.
AddHandler(PointerPressedEvent, OnTunnelPointerPressed, RoutingStrategies.Tunnel);
DataContextChanged += (_, _) =>
{
if (DataContext is TasksIslandViewModel vm)
vm.FocusAddTaskRequested += (_, _) => AddTaskBox.Focus();
};
}
private async void OnTunnelPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (DataContext is not TasksIslandViewModel vm || !vm.CanReorder) return;
if (e.Source is not Visual src) return;
var button = src as Button ?? src.FindAncestorOfType<Button>();
if (button?.DataContext is not TaskRowViewModel row) return;
if (row.IsRunning) return;
if (!e.GetCurrentPoint(button).Properties.IsLeftButtonPressed) return;
var data = new DataTransfer();
data.Add(DataTransferItem.Create(TaskRowFormat, row.Id));
try
{
await DragDrop.DoDragDropAsync(e, data, DragDropEffects.Move);
}
finally
{
vm.ClearDropHints();
}
}
private void OnRowPointerPressed(object? sender, PointerPressedEventArgs e) { }
private void OnRowPointerMoved(object? sender, PointerEventArgs e) { }
private void OnRowPointerReleased(object? sender, PointerReleasedEventArgs e) { }
private void OnRowDragOver(object? sender, DragEventArgs e)
{
if (DataContext is not TasksIslandViewModel vm) { e.DragEffects = DragDropEffects.None; return; }
if (!e.DataTransfer?.Contains(TaskRowFormat) ?? true)
{
e.DragEffects = DragDropEffects.None;
vm.ClearDropHints();
return;
}
if (sender is not Button b || b.DataContext is not TaskRowViewModel target || target.IsRunning)
{
e.DragEffects = DragDropEffects.None;
vm.ClearDropHints();
return;
}
var sourceId = e.DataTransfer?.TryGetValue(TaskRowFormat);
if (string.IsNullOrEmpty(sourceId) || sourceId == target.Id)
{
e.DragEffects = DragDropEffects.None;
vm.ClearDropHints();
return;
}
var placeBelow = e.GetPosition(b).Y > b.Bounds.Height / 2;
// Canonicalize: "drop below X" == "drop above X+1". Render the indicator
// above X+1 when there is one; only the last row in a section shows a below-line.
TaskRowViewModel hintRow = target;
bool hintBelow = false;
if (placeBelow)
{
var next = FindNextInSameSection(vm, target);
if (next is not null && !next.IsRunning)
{
hintRow = next;
hintBelow = false;
}
else
{
hintRow = target;
hintBelow = true;
}
}
// A hint that lands right where the dragged row already sits is a no-op.
if (hintRow.Id == sourceId)
{
e.DragEffects = DragDropEffects.None;
vm.ClearDropHints();
return;
}
vm.SetDropHint(hintRow, hintBelow);
e.DragEffects = DragDropEffects.Move;
}
private static TaskRowViewModel? FindNextInSameSection(TasksIslandViewModel vm, TaskRowViewModel row)
{
foreach (var section in new[] { vm.OverdueItems, vm.OpenItems, vm.CompletedItems })
{
var idx = section.IndexOf(row);
if (idx >= 0) return idx + 1 < section.Count ? section[idx + 1] : null;
}
return null;
}
private async void OnRowDrop(object? sender, DragEventArgs e)
{
if (DataContext is not TasksIslandViewModel vm) return;
try
{
if (sender is not Button b || b.DataContext is not TaskRowViewModel target) return;
if (target.IsRunning) return;
var sourceId = e.DataTransfer?.TryGetValue(TaskRowFormat);
if (string.IsNullOrEmpty(sourceId) || sourceId == target.Id) return;
var source = FindRowById(vm, sourceId);
if (source is null || source.IsRunning) return;
var placeBelow = e.GetPosition(b).Y > b.Bounds.Height / 2;
await vm.ReorderAsync(source, target, placeBelow);
}
finally
{
vm.ClearDropHints();
}
}
private static TaskRowViewModel? FindRowById(TasksIslandViewModel vm, string id)
{
foreach (var r in vm.Items)
if (r.Id == id) return r;
return null;
}
}