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 _dbFactory; private readonly IWorkerClient _workerClient; private readonly HashSet _existingDirs = new(StringComparer.OrdinalIgnoreCase); private readonly List _folders = new(); public OperationStatus ScanOp { get; } = new(); public ObservableCollection Repos { get; } = new(); public Action? CloseAction { get; set; } public event Action? 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 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 folders) { var added = new List(); 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 folders) { using var op = ScanOp.Begin(Loc.T("ops.repoImport.scanning")); var current = new HashSet( 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 BuildCandidates( IEnumerable found, IReadOnlySet currentPaths, IReadOnlySet existingDirs) { var items = new List(); 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(); }