improve Frontend
This commit is contained in:
@@ -272,6 +272,18 @@
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="Button.flat:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
</Style>
|
||||
<Style Selector="Button.flat:pressed /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
</Style>
|
||||
<Style Selector="Button.flat:focus /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
</Style>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- TASK ROW -->
|
||||
|
||||
@@ -21,6 +21,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
[ObservableProperty] private DateTime? _scheduledFor;
|
||||
[ObservableProperty] private int _diffAdditions;
|
||||
[ObservableProperty] private int _diffDeletions;
|
||||
[ObservableProperty] private bool _dropHintAbove;
|
||||
[ObservableProperty] private bool _dropHintBelow;
|
||||
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public string CreatedAtFormatted => CreatedAt == default ? "—" : $"Created {CreatedAt:MMM d}";
|
||||
|
||||
@@ -75,6 +75,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
var all = await db.Tasks
|
||||
.Include(t => t.List)
|
||||
.Include(t => t.Worktree)
|
||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
@@ -149,23 +150,83 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(NewTaskTitle) || _currentList?.Kind != ListKind.User) return;
|
||||
var listId = _currentList.Id["user:".Length..];
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var maxSort = await db.Tasks
|
||||
.Where(t => t.ListId == listId)
|
||||
.Select(t => (int?)t.SortOrder)
|
||||
.MaxAsync();
|
||||
var entity = new TaskEntity
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
ListId = listId,
|
||||
Title = NewTaskTitle.Trim(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
SortOrder = (maxSort ?? -1) + 1,
|
||||
};
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
db.Tasks.Add(entity);
|
||||
await db.SaveChangesAsync();
|
||||
var row = TaskRowViewModel.FromEntity(entity);
|
||||
Items.Insert(0, row);
|
||||
Items.Add(row);
|
||||
Regroup();
|
||||
NewTaskTitle = "";
|
||||
UpdateSubtitle();
|
||||
}
|
||||
|
||||
public bool CanReorder => _currentList?.Kind == ListKind.User;
|
||||
|
||||
public void ClearDropHints()
|
||||
{
|
||||
foreach (var r in Items)
|
||||
{
|
||||
r.DropHintAbove = false;
|
||||
r.DropHintBelow = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetDropHint(TaskRowViewModel target, bool placeBelow)
|
||||
{
|
||||
foreach (var r in Items)
|
||||
{
|
||||
var isTarget = ReferenceEquals(r, target);
|
||||
r.DropHintAbove = isTarget && !placeBelow;
|
||||
r.DropHintBelow = isTarget && placeBelow;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ReorderAsync(TaskRowViewModel source, TaskRowViewModel target, bool placeBelow)
|
||||
{
|
||||
if (!CanReorder || _currentList is null) return;
|
||||
if (source.IsRunning || target.IsRunning) return;
|
||||
if (ReferenceEquals(source, target)) return;
|
||||
|
||||
var srcIdx = Items.IndexOf(source);
|
||||
var tgtIdx = Items.IndexOf(target);
|
||||
if (srcIdx < 0 || tgtIdx < 0) return;
|
||||
|
||||
Items.RemoveAt(srcIdx);
|
||||
var newTgtIdx = Items.IndexOf(target);
|
||||
var insertIdx = placeBelow ? newTgtIdx + 1 : newTgtIdx;
|
||||
if (insertIdx < 0 || insertIdx > Items.Count) insertIdx = Items.Count;
|
||||
Items.Insert(insertIdx, source);
|
||||
|
||||
var listId = _currentList.Id["user:".Length..];
|
||||
var orderedIds = Items.Select(i => i.Id).ToList();
|
||||
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var idSet = orderedIds.ToHashSet();
|
||||
var entities = await db.Tasks
|
||||
.Where(t => t.ListId == listId && idSet.Contains(t.Id))
|
||||
.ToListAsync();
|
||||
for (int i = 0; i < orderedIds.Count; i++)
|
||||
{
|
||||
var e = entities.FirstOrDefault(x => x.Id == orderedIds[i]);
|
||||
if (e is not null) e.SortOrder = i;
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
Regroup();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ToggleDoneAsync(TaskRowViewModel row)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user