Vorgaben: `docs/superpowers/plans/2026-08-11-operation-feedback.md`, Gruppe A, Entwurf A3. P0-1 ist gemergt: `src/ClaudeDo.Ui/Services/OperationStatus.cs` + `src/ClaudeDo.Ui/Views/Controls/OperationIndicator.axaml`. SCOPE-ÄNDERUNG (Nutzer, 2026-08-12): OnlineInbox Sign-In/Sign-Out ist AUS DEM SCOPE GENOMMEN — vorerst unwichtig. `ViewModels/Modals/Settings/OnlineInboxSettingsViewModel.cs` und die ClaudeDo-Task: 2356806f-f16e-4652-b615-4562e95c3a65
227 lines
7.1 KiB
C#
227 lines
7.1 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using ClaudeDo.Data;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Data.Repositories;
|
|
using ClaudeDo.Ui.Localization;
|
|
using ClaudeDo.Ui.Services;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ClaudeDo.Ui.ViewModels.Modals;
|
|
|
|
public sealed partial class RepoImportItemViewModel : ViewModelBase
|
|
{
|
|
public string Name { get; init; } = "";
|
|
public string FullPath { get; init; } = "";
|
|
|
|
// True when a list already points at this path. Such rows are shown ticked + disabled.
|
|
public bool AlreadyAdded { get; init; }
|
|
public bool CanToggle => !AlreadyAdded;
|
|
|
|
[ObservableProperty] private bool _isChecked;
|
|
|
|
// Driven by the search filter; the row collapses when it doesn't match.
|
|
[ObservableProperty] private bool _isVisible = true;
|
|
}
|
|
|
|
public sealed partial class RepoImportModalViewModel : ViewModelBase
|
|
{
|
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
|
private readonly IWorkerClient _workerClient;
|
|
private readonly HashSet<string> _existingDirs = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly List<string> _folders = new();
|
|
|
|
public OperationStatus ScanOp { get; } = new();
|
|
|
|
public ObservableCollection<RepoImportItemViewModel> Repos { get; } = new();
|
|
|
|
public Action? CloseAction { get; set; }
|
|
public event Action<string>? ErrorReported;
|
|
|
|
[ObservableProperty] private string _searchText = "";
|
|
|
|
public int CreateCount => Repos.Count(r => r.IsChecked && !r.AlreadyAdded);
|
|
public bool CanCreate => CreateCount > 0;
|
|
public string CreateButtonText => $"Create {CreateCount} list(s)";
|
|
public bool HasFolders => _folders.Count > 0;
|
|
|
|
public RepoImportModalViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient workerClient)
|
|
{
|
|
_dbFactory = dbFactory;
|
|
_workerClient = workerClient;
|
|
}
|
|
|
|
public async Task LoadAsync(CancellationToken ct = default)
|
|
{
|
|
ClearRepos();
|
|
_existingDirs.Clear();
|
|
_folders.Clear();
|
|
|
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
|
var lists = new ListRepository(ctx);
|
|
foreach (var l in await lists.GetAllAsync(ct))
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(l.WorkingDir))
|
|
_existingDirs.Add(l.WorkingDir!);
|
|
}
|
|
|
|
try
|
|
{
|
|
foreach (var f in await _workerClient.GetRepoImportFoldersAsync())
|
|
AddFolderToSet(f);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ErrorReported?.Invoke(Loc.T("vm.repoImport.loadFailed", ex.Message));
|
|
}
|
|
|
|
await ScanAndAddAsync(_folders);
|
|
OnPropertyChanged(nameof(HasFolders));
|
|
NotifyCreateState();
|
|
}
|
|
|
|
public async Task AddFoldersAsync(IEnumerable<string> folders)
|
|
{
|
|
var added = new List<string>();
|
|
foreach (var f in folders)
|
|
if (AddFolderToSet(f)) added.Add(f);
|
|
|
|
if (added.Count == 0) return;
|
|
|
|
await ScanAndAddAsync(added);
|
|
OnPropertyChanged(nameof(HasFolders));
|
|
NotifyCreateState();
|
|
await SaveFoldersAsync();
|
|
}
|
|
|
|
private async Task ScanAndAddAsync(IEnumerable<string> folders)
|
|
{
|
|
using var op = ScanOp.Begin(Loc.T("ops.repoImport.scanning"));
|
|
var current = new HashSet<string>(
|
|
Repos.Select(r => r.FullPath), StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var folder in folders)
|
|
{
|
|
var found = await Task.Run(() => RepoScanner.Scan(folder));
|
|
foreach (var item in BuildCandidates(found, current, _existingDirs))
|
|
{
|
|
item.PropertyChanged += OnItemChanged;
|
|
Repos.Add(item);
|
|
current.Add(item.FullPath);
|
|
}
|
|
}
|
|
ApplyFilter();
|
|
}
|
|
|
|
public static List<RepoImportItemViewModel> BuildCandidates(
|
|
IEnumerable<RepoCandidate> found,
|
|
IReadOnlySet<string> currentPaths,
|
|
IReadOnlySet<string> existingDirs)
|
|
{
|
|
var items = new List<RepoImportItemViewModel>();
|
|
foreach (var c in found)
|
|
{
|
|
if (currentPaths.Contains(c.FullPath)) continue;
|
|
var alreadyAdded = existingDirs.Contains(c.FullPath);
|
|
items.Add(new RepoImportItemViewModel
|
|
{
|
|
Name = c.Name,
|
|
FullPath = c.FullPath,
|
|
AlreadyAdded = alreadyAdded,
|
|
// New repos start unchecked (no auto-select); already-added rows show ticked + disabled.
|
|
IsChecked = alreadyAdded,
|
|
});
|
|
}
|
|
return items;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task ForgetFoldersAsync()
|
|
{
|
|
_folders.Clear();
|
|
ClearRepos();
|
|
OnPropertyChanged(nameof(HasFolders));
|
|
NotifyCreateState();
|
|
await SaveFoldersAsync();
|
|
}
|
|
|
|
partial void OnSearchTextChanged(string value) => ApplyFilter();
|
|
|
|
private void ApplyFilter()
|
|
{
|
|
var q = SearchText?.Trim() ?? "";
|
|
foreach (var r in Repos)
|
|
r.IsVisible = q.Length == 0
|
|
|| r.Name.Contains(q, StringComparison.OrdinalIgnoreCase)
|
|
|| r.FullPath.Contains(q, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private bool AddFolderToSet(string folder)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(folder)) return false;
|
|
if (_folders.Any(f => string.Equals(f, folder, StringComparison.OrdinalIgnoreCase)))
|
|
return false;
|
|
_folders.Add(folder);
|
|
return true;
|
|
}
|
|
|
|
private async Task SaveFoldersAsync()
|
|
{
|
|
try
|
|
{
|
|
await _workerClient.SetRepoImportFoldersAsync(_folders);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ErrorReported?.Invoke(Loc.T("vm.repoImport.saveFailed", ex.Message));
|
|
}
|
|
}
|
|
|
|
private void ClearRepos()
|
|
{
|
|
foreach (var r in Repos) r.PropertyChanged -= OnItemChanged;
|
|
Repos.Clear();
|
|
}
|
|
|
|
private void OnItemChanged(object? sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName == nameof(RepoImportItemViewModel.IsChecked))
|
|
NotifyCreateState();
|
|
}
|
|
|
|
private void NotifyCreateState()
|
|
{
|
|
OnPropertyChanged(nameof(CreateCount));
|
|
OnPropertyChanged(nameof(CanCreate));
|
|
OnPropertyChanged(nameof(CreateButtonText));
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task CreateAsync()
|
|
{
|
|
var toCreate = Repos.Where(r => r.IsChecked && !r.AlreadyAdded).ToList();
|
|
if (toCreate.Count > 0)
|
|
{
|
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
|
var lists = new ListRepository(ctx);
|
|
foreach (var r in toCreate)
|
|
{
|
|
await lists.AddAsync(new ListEntity
|
|
{
|
|
Id = Guid.NewGuid().ToString("N"),
|
|
Name = r.Name,
|
|
WorkingDir = r.FullPath,
|
|
DefaultCommitType = CommitTypeRegistry.DefaultType,
|
|
CreatedAt = DateTime.UtcNow,
|
|
});
|
|
}
|
|
}
|
|
CloseAction?.Invoke();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void Cancel() => CloseAction?.Invoke();
|
|
}
|