Collapses the ~10 per-modal Show*Modal Func callbacks (wired separately on the shell and the lists island) into one IDialogService + WindowDialogService impl. Removes the RepoImport/WorktreesOverview dialog construction duplicated across MainWindow and ListsIslandView, plus the Confirm/Error dialogs duplicated in both code-behinds. Shell/lists Open* commands now route through an injected Dialogs handle (propagated shell->lists); the per-list worktrees overview also wires conflict resolution now, matching the global one. No VM ctor changes (Dialogs is a settable seam), so no test-fake impact.
161 lines
5.2 KiB
C#
161 lines
5.2 KiB
C#
using System.Linq;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Media;
|
|
using Avalonia.VisualTree;
|
|
using ClaudeDo.Ui.ViewModels;
|
|
using ClaudeDo.Ui.ViewModels.Islands;
|
|
using ClaudeDo.Ui.ViewModels.Modals;
|
|
using ClaudeDo.Ui.Views.Modals;
|
|
|
|
namespace ClaudeDo.Ui.Views.Islands;
|
|
|
|
public partial class ListsIslandView : UserControl
|
|
{
|
|
private static readonly DataFormat<string> ListRowFormat =
|
|
DataFormat.CreateStringApplicationFormat("claudedo-list-row");
|
|
|
|
public ListsIslandView()
|
|
{
|
|
InitializeComponent();
|
|
AddHandler(PointerPressedEvent, OnTunnelPointerPressed, RoutingStrategies.Tunnel);
|
|
DataContextChanged += (_, _) =>
|
|
{
|
|
if (DataContext is ListsIslandViewModel vm)
|
|
vm.FocusSearchRequested += (_, _) => SearchBox.Focus();
|
|
};
|
|
}
|
|
|
|
private void OnItemTapped(object? sender, RoutedEventArgs e)
|
|
{
|
|
if (sender is Border { DataContext: ListNavItemViewModel item }
|
|
&& DataContext is ListsIslandViewModel vm)
|
|
vm.SelectCommand.Execute(item);
|
|
}
|
|
|
|
private async void OnTunnelPointerPressed(object? sender, PointerPressedEventArgs e)
|
|
{
|
|
if (DataContext is not ListsIslandViewModel vm) return;
|
|
if (e.Source is not Visual src) return;
|
|
|
|
var border = FindListItemBorder(src);
|
|
if (border?.DataContext is not ListNavItemViewModel row || row.Kind != ListKind.User) return;
|
|
if (!e.GetCurrentPoint(border).Properties.IsLeftButtonPressed) return;
|
|
|
|
// Double-click opens the list's settings instead of starting a drag. Handled here
|
|
// because DoDragDropAsync captures the pointer and would swallow a DoubleTapped event.
|
|
if (e.ClickCount == 2)
|
|
{
|
|
vm.OpenListSettingsCommand.Execute(row);
|
|
return;
|
|
}
|
|
|
|
// Select now so the right pane updates whether the gesture becomes a click or a drag
|
|
// (the Tapped handler doesn't fire once DoDragDropAsync captures the pointer).
|
|
vm.SelectCommand.Execute(row);
|
|
|
|
var data = new DataTransfer();
|
|
data.Add(DataTransferItem.Create(ListRowFormat, row.Id));
|
|
try
|
|
{
|
|
await DragDrop.DoDragDropAsync(e, data, DragDropEffects.Move);
|
|
}
|
|
finally
|
|
{
|
|
vm.ClearDropHints();
|
|
}
|
|
}
|
|
|
|
private void OnListDragOver(object? sender, DragEventArgs e)
|
|
{
|
|
if (DataContext is not ListsIslandViewModel vm) { e.DragEffects = DragDropEffects.None; return; }
|
|
if (!e.DataTransfer?.Contains(ListRowFormat) ?? true)
|
|
{
|
|
e.DragEffects = DragDropEffects.None;
|
|
vm.ClearDropHints();
|
|
return;
|
|
}
|
|
if (sender is not Border b || b.DataContext is not ListNavItemViewModel target || target.Kind != ListKind.User)
|
|
{
|
|
e.DragEffects = DragDropEffects.None;
|
|
vm.ClearDropHints();
|
|
return;
|
|
}
|
|
|
|
var sourceId = e.DataTransfer?.TryGetValue(ListRowFormat);
|
|
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". Only the last row shows a below-line.
|
|
ListNavItemViewModel hintRow = target;
|
|
bool hintBelow = false;
|
|
if (placeBelow)
|
|
{
|
|
var next = FindNextUserList(vm, target);
|
|
if (next is not null) { hintRow = next; hintBelow = false; }
|
|
else { hintRow = target; hintBelow = true; }
|
|
}
|
|
|
|
if (hintRow.Id == sourceId)
|
|
{
|
|
e.DragEffects = DragDropEffects.None;
|
|
vm.ClearDropHints();
|
|
return;
|
|
}
|
|
|
|
vm.SetDropHint(hintRow, hintBelow);
|
|
e.DragEffects = DragDropEffects.Move;
|
|
}
|
|
|
|
private async void OnListDrop(object? sender, DragEventArgs e)
|
|
{
|
|
if (DataContext is not ListsIslandViewModel vm) return;
|
|
try
|
|
{
|
|
if (sender is not Border b || b.DataContext is not ListNavItemViewModel target || target.Kind != ListKind.User) return;
|
|
|
|
var sourceId = e.DataTransfer?.TryGetValue(ListRowFormat);
|
|
if (string.IsNullOrEmpty(sourceId) || sourceId == target.Id) return;
|
|
|
|
var source = vm.UserLists.FirstOrDefault(r => r.Id == sourceId);
|
|
if (source is null) return;
|
|
|
|
var placeBelow = e.GetPosition(b).Y > b.Bounds.Height / 2;
|
|
vm.ClearDropHints();
|
|
await vm.ReorderAsync(source, target, placeBelow);
|
|
}
|
|
catch
|
|
{
|
|
vm.ClearDropHints();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private static Border? FindListItemBorder(Visual? v)
|
|
{
|
|
while (v is not null)
|
|
{
|
|
if (v is Border b && b.Classes.Contains("list-item")) return b;
|
|
v = v.GetVisualParent();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static ListNavItemViewModel? FindNextUserList(ListsIslandViewModel vm, ListNavItemViewModel row)
|
|
{
|
|
var idx = vm.UserLists.IndexOf(row);
|
|
if (idx < 0) return null;
|
|
return idx + 1 < vm.UserLists.Count ? vm.UserLists[idx + 1] : null;
|
|
}
|
|
|
|
}
|