< Summary

Information
Class: LOCKnet.App.ViewModels.CredentialListViewModel
Assembly: LOCKnet.App
File(s): /home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.App/ViewModels/CredentialListViewModel.cs
Line coverage
86%
Covered lines: 112
Uncovered lines: 18
Coverable lines: 130
Total lines: 229
Line coverage: 86.1%
Branch coverage
57%
Covered branches: 24
Total branches: 42
Branch coverage: 57.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
StartCountdownTimer()100%11100%
UpdateLockTimer()100%44100%
Refresh()100%11100%
OnSearchTextChanged(...)100%11100%
ApplyFilter()100%22100%
Add()50%22100%
Edit()50%22100%
Delete()100%22100%
CopyPassword()50%171687.5%
ShowTutorial()50%22100%
RetryStorageCleanupAsync()33.33%22623.52%
Lock()50%22100%
HasSelection()100%210%
CanRetryStorageCleanup()50%22100%
RefreshStorageCleanupState()100%11100%
SecureStringToString(...)50%22100%

File(s)

/home/runner/work/LOCKnet/LOCKnet/src/LOCKnet.App/ViewModels/CredentialListViewModel.cs

#LineLine coverage
 1using Avalonia.Controls;
 2using Avalonia.Threading;
 3using CommunityToolkit.Mvvm.ComponentModel;
 4using CommunityToolkit.Mvvm.Input;
 5using LOCKnet.Core.DataAbstractions;
 6using System.Collections.ObjectModel;
 7using System.Security;
 8
 9namespace LOCKnet.App.ViewModels;
 10
 11/// <summary>
 12/// ViewModel für die Credential-Liste (Hauptansicht nach dem Login).
 13/// </summary>
 14public partial class CredentialListViewModel : ViewModelBase
 15{
 16  public event EventHandler? LockRequested;
 17  public event EventHandler<CredentialRecord?>? AddEditRequested;
 18  public event EventHandler? TutorialRequested;
 19
 320  private IReadOnlyList<CredentialRecord> _allCredentials = [];
 21  private DispatcherTimer? _countdownTimer;
 22
 23  [ObservableProperty]
 324  private ObservableCollection<CredentialRecord> _credentials = [];
 25
 26  [ObservableProperty]
 27  [NotifyCanExecuteChangedFor(nameof(EditCommand))]
 28  [NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
 29  [NotifyCanExecuteChangedFor(nameof(CopyPasswordCommand))]
 30  private CredentialRecord? _selectedCredential;
 31
 32  [ObservableProperty]
 333  private string _searchText = string.Empty;
 34
 35  [ObservableProperty]
 336  private string _statusMessage = string.Empty;
 37
 38  [ObservableProperty]
 339  private string _storageCleanupMessage = string.Empty;
 40
 41  [ObservableProperty]
 42  private bool _isStorageCleanupPending;
 43
 44  [ObservableProperty]
 45  private bool _isRetryingStorageCleanup;
 46
 47  [ObservableProperty]
 348  private string _lockTimerText = string.Empty;
 49
 350  public CredentialListViewModel()
 351  {
 352    Refresh();
 353    RefreshStorageCleanupState();
 354    StartCountdownTimer();
 355  }
 56
 57  // ── Countdown ─────────────────────────────────────────────────────────────
 58
 59  private void StartCountdownTimer()
 360  {
 361    _countdownTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
 362    _countdownTimer.Tick += (_, _) => UpdateLockTimer();
 363    _countdownTimer.Start();
 364    UpdateLockTimer();
 365  }
 66
 67  private void UpdateLockTimer()
 668  {
 669    var monitor = AppServices.Current.ActivityMonitor;
 670    if (!monitor.IsRunning)
 471    {
 472      LockTimerText = string.Empty;
 473      return;
 74    }
 275    var elapsed = DateTimeOffset.UtcNow - monitor.LastActivity;
 276    var remaining = monitor.Timeout - elapsed;
 277    if (remaining <= TimeSpan.Zero)
 178    {
 179      LockTimerText = "⏱ 0:00";
 180      return;
 81    }
 182    LockTimerText = $"⏱ {(int)remaining.TotalMinutes}:{remaining.Seconds:D2}";
 683  }
 84
 85
 86  public void Refresh()
 587  {
 88    try
 589    {
 590      _allCredentials = AppServices.Current.CredentialService.GetAll();
 491      ApplyFilter();
 492    }
 193    catch (Exception ex)
 194    {
 195      StatusMessage = ex.Message;
 196    }
 97    finally
 598    {
 599      RefreshStorageCleanupState();
 5100    }
 5101  }
 102
 2103  partial void OnSearchTextChanged(string value) => ApplyFilter();
 104
 105  private void ApplyFilter()
 6106  {
 6107    var filtered = string.IsNullOrWhiteSpace(SearchText)
 6108      ? _allCredentials
 6109      : _allCredentials.Where(c =>
 9110        c.Title.Contains(SearchText, StringComparison.OrdinalIgnoreCase) ||
 9111        (c.Username?.Contains(SearchText, StringComparison.OrdinalIgnoreCase) ?? false) ||
 9112        (c.Url?.Contains(SearchText, StringComparison.OrdinalIgnoreCase) ?? false));
 113
 6114    Credentials = new ObservableCollection<CredentialRecord>(filtered);
 6115  }
 116
 117  // ── Commands ──────────────────────────────────────────────────────────────
 118
 119  [RelayCommand]
 1120  private void Add() => AddEditRequested?.Invoke(this, null);
 121
 122  [RelayCommand(CanExecute = nameof(HasSelection))]
 1123  private void Edit() => AddEditRequested?.Invoke(this, SelectedCredential);
 124
 125  [RelayCommand(CanExecute = nameof(HasSelection))]
 126  private void Delete()
 2127  {
 2128    if (SelectedCredential is null) return;
 129    try
 2130    {
 2131      AppServices.Current.CredentialService.Remove(SelectedCredential.Id);
 1132      StatusMessage = $"\"{SelectedCredential.Title}\" gelöscht.";
 1133      Refresh();
 1134    }
 1135    catch (Exception ex)
 1136    {
 1137      StatusMessage = ex.Message;
 1138    }
 2139  }
 140
 141  [RelayCommand(CanExecute = nameof(HasSelection))]
 142  private void CopyPassword()
 3143  {
 3144    if (SelectedCredential is null) return;
 3145    if (SelectedCredential.CredentialType == CredentialType.BackupCodes)
 0146    {
 0147      StatusMessage = "Backup-Codes bitte in der Bearbeitungsansicht kopieren.";
 0148      return;
 149    }
 150    try
 3151    {
 3152      var pw = AppServices.Current.CredentialService.GetPassword(SelectedCredential.Id);
 2153      if (pw is null)
 1154      {
 1155        StatusMessage = "Passwort nicht gefunden.";
 1156        return;
 157      }
 158
 1159      var text = SecureStringToString(pw);
 1160      var clipboard = Avalonia.Application.Current?.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClass
 1161      if (clipboard is not null) _ = clipboard.SetTextAsync(text);
 1162      StatusMessage = "Passwort kopiert.";
 163
 164      // Clipboard nach 30s leeren
 1165      _ = Task.Delay(TimeSpan.FromSeconds(30)).ContinueWith(_ =>
 1166        Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(async () => { if (clipboard is not null) await clipboard.Clea
 1167    }
 1168    catch (Exception ex)
 1169    {
 1170      StatusMessage = ex.Message;
 1171    }
 3172  }
 173
 174  [RelayCommand]
 1175  private void ShowTutorial() => TutorialRequested?.Invoke(this, EventArgs.Empty);
 176
 177  [RelayCommand(CanExecute = nameof(CanRetryStorageCleanup))]
 178  private async Task RetryStorageCleanupAsync()
 1179  {
 1180    if (!IsStorageCleanupPending || IsRetryingStorageCleanup)
 1181      return;
 182
 183    try
 0184    {
 0185      IsRetryingStorageCleanup = true;
 0186      var info = await Task.Run(() => AppServices.Current.MasterKeyManager.RetryPendingStorageCompaction());
 0187      RefreshStorageCleanupState();
 0188      StatusMessage = info.IsPending ? string.Empty : info.UserMessage;
 0189    }
 0190    catch (Exception ex)
 0191    {
 0192      StatusMessage = ex.Message;
 0193    }
 194    finally
 0195    {
 0196      IsRetryingStorageCleanup = false;
 0197      RetryStorageCleanupCommand.NotifyCanExecuteChanged();
 0198    }
 1199  }
 200
 201  [RelayCommand]
 202  private void Lock()
 1203  {
 1204    AppServices.Current.ActivityMonitor.Stop();
 1205    AppServices.Current.SessionManager.Lock();
 1206    LockRequested?.Invoke(this, EventArgs.Empty);
 1207  }
 208
 0209  private bool HasSelection() => SelectedCredential is not null;
 210
 1211  private bool CanRetryStorageCleanup() => IsStorageCleanupPending && !IsRetryingStorageCleanup;
 212
 213  // ── Helpers ───────────────────────────────────────────────────────────────
 214
 215  private void RefreshStorageCleanupState()
 8216  {
 8217    var info = AppServices.Current.MasterKeyManager.GetStorageCompactionInfo();
 8218    IsStorageCleanupPending = info.IsPending;
 8219    StorageCleanupMessage = info.UserMessage;
 8220    RetryStorageCleanupCommand.NotifyCanExecuteChanged();
 8221  }
 222
 223  private static string SecureStringToString(SecureString s)
 1224  {
 1225    var ptr = System.Runtime.InteropServices.Marshal.SecureStringToGlobalAllocUnicode(s);
 2226    try { return System.Runtime.InteropServices.Marshal.PtrToStringUni(ptr) ?? string.Empty; }
 3227    finally { System.Runtime.InteropServices.Marshal.ZeroFreeGlobalAllocUnicode(ptr); }
 1228  }
 229}