Files
ClaudeDo/src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs
T
Mika Kuns ec4cbcbaf9 chore(claude-do): [A3] Settings + Update-Check: OperationStatus für Skill-Inst
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
2026-08-12 09:55:04 +02:00

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();
}