< Summary

Information
Class: Chronicis.Client.ViewModels.WorldLinksViewModel
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/ViewModels/WorldLinksViewModel.cs
Line coverage
100%
Covered lines: 64
Uncovered lines: 0
Coverable lines: 64
Total lines: 290
Line coverage: 100%
Branch coverage
100%
Covered branches: 6
Total branches: 6
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Links()100%11100%
set_Links(...)100%11100%
get_IsAddingLink()100%11100%
set_IsAddingLink(...)100%11100%
get_IsSavingLink()100%11100%
set_IsSavingLink(...)100%11100%
get_NewLinkTitle()100%11100%
set_NewLinkTitle(...)100%11100%
get_NewLinkUrl()100%11100%
set_NewLinkUrl(...)100%11100%
get_NewLinkDescription()100%11100%
set_NewLinkDescription(...)100%11100%
get_EditingLinkId()100%11100%
set_EditingLinkId(...)100%11100%
get_EditLinkTitle()100%11100%
set_EditLinkTitle(...)100%11100%
get_EditLinkUrl()100%11100%
set_EditLinkUrl(...)100%11100%
get_EditLinkDescription()100%11100%
set_EditLinkDescription(...)100%11100%
StartAddLink()100%11100%
CancelAddLink()100%11100%
StartEditLink(...)100%22100%
CancelEditLink()100%11100%
GetFaviconUrl(...)100%11100%
IsValidUrl(...)100%44100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/ViewModels/WorldLinksViewModel.cs

#LineLine coverage
 1using Chronicis.Client.Abstractions;
 2using Chronicis.Client.Services;
 3using Chronicis.Shared.DTOs;
 4using Chronicis.Shared.Extensions;
 5
 6namespace Chronicis.Client.ViewModels;
 7
 8/// <summary>
 9/// ViewModel managing external link CRUD for a world's Resources tab.
 10/// </summary>
 11public sealed class WorldLinksViewModel : ViewModelBase
 12{
 13    private readonly IWorldApiService _worldApi;
 14    private readonly ITreeStateService _treeState;
 15    private readonly IUserNotifier _notifier;
 16    private readonly ILogger<WorldLinksViewModel> _logger;
 17
 18    private Guid _worldId;
 3519    private List<WorldLinkDto> _links = new();
 20    private bool _isAddingLink;
 21    private bool _isSavingLink;
 3522    private string _newLinkTitle = string.Empty;
 3523    private string _newLinkUrl = string.Empty;
 3524    private string _newLinkDescription = string.Empty;
 25    private Guid? _editingLinkId;
 3526    private string _editLinkTitle = string.Empty;
 3527    private string _editLinkUrl = string.Empty;
 3528    private string _editLinkDescription = string.Empty;
 29
 3530    public WorldLinksViewModel(
 3531        IWorldApiService worldApi,
 3532        ITreeStateService treeState,
 3533        IUserNotifier notifier,
 3534        ILogger<WorldLinksViewModel> logger)
 35    {
 3536        _worldApi = worldApi;
 3537        _treeState = treeState;
 3538        _notifier = notifier;
 3539        _logger = logger;
 3540    }
 41
 42    public List<WorldLinkDto> Links
 43    {
 1044        get => _links;
 2345        private set => SetField(ref _links, value);
 46    }
 47
 48    public bool IsAddingLink
 49    {
 950        get => _isAddingLink;
 451        private set => SetField(ref _isAddingLink, value);
 52    }
 53
 54    public bool IsSavingLink
 55    {
 156        get => _isSavingLink;
 857        private set => SetField(ref _isSavingLink, value);
 58    }
 59
 60    public string NewLinkTitle
 61    {
 962        get => _newLinkTitle;
 1063        set => SetField(ref _newLinkTitle, value);
 64    }
 65
 66    public string NewLinkUrl
 67    {
 1168        get => _newLinkUrl;
 969        set => SetField(ref _newLinkUrl, value);
 70    }
 71
 72    public string NewLinkDescription
 73    {
 374        get => _newLinkDescription;
 475        set => SetField(ref _newLinkDescription, value);
 76    }
 77
 78    public Guid? EditingLinkId
 79    {
 780        get => _editingLinkId;
 681        private set => SetField(ref _editingLinkId, value);
 82    }
 83
 84    public string EditLinkTitle
 85    {
 586        get => _editLinkTitle;
 787        set => SetField(ref _editLinkTitle, value);
 88    }
 89
 90    public string EditLinkUrl
 91    {
 492        get => _editLinkUrl;
 693        set => SetField(ref _editLinkUrl, value);
 94    }
 95
 96    public string EditLinkDescription
 97    {
 298        get => _editLinkDescription;
 699        set => SetField(ref _editLinkDescription, value);
 100    }
 101
 102    /// <summary>Loads links for the specified world.</summary>
 103    public async Task LoadAsync(Guid worldId)
 104    {
 105        _worldId = worldId;
 106        Links = await _worldApi.GetWorldLinksAsync(worldId);
 107    }
 108
 109    public void StartAddLink()
 110    {
 2111        IsAddingLink = true;
 2112        NewLinkTitle = string.Empty;
 2113        NewLinkUrl = string.Empty;
 2114        NewLinkDescription = string.Empty;
 2115    }
 116
 117    public void CancelAddLink()
 118    {
 1119        IsAddingLink = false;
 1120        NewLinkTitle = string.Empty;
 1121        NewLinkUrl = string.Empty;
 1122        NewLinkDescription = string.Empty;
 1123    }
 124
 125    public async Task SaveNewLinkAsync()
 126    {
 127        if (string.IsNullOrWhiteSpace(NewLinkTitle) || string.IsNullOrWhiteSpace(NewLinkUrl))
 128        {
 129            _notifier.Warning("Title and URL are required");
 130            return;
 131        }
 132
 133        if (!IsValidUrl(NewLinkUrl))
 134        {
 135            _notifier.Warning("Please enter a valid URL (starting with http:// or https://)");
 136            return;
 137        }
 138
 139        IsSavingLink = true;
 140
 141        try
 142        {
 143            var dto = new WorldLinkCreateDto
 144            {
 145                Title = NewLinkTitle.Trim(),
 146                Url = NewLinkUrl.Trim(),
 147                Description = string.IsNullOrWhiteSpace(NewLinkDescription) ? null : NewLinkDescription.Trim()
 148            };
 149
 150            var created = await _worldApi.CreateWorldLinkAsync(_worldId, dto);
 151            if (created != null)
 152            {
 153                Links = await _worldApi.GetWorldLinksAsync(_worldId);
 154                await _treeState.RefreshAsync();
 155                IsAddingLink = false;
 156                NewLinkTitle = string.Empty;
 157                NewLinkUrl = string.Empty;
 158                NewLinkDescription = string.Empty;
 159                _notifier.Success("Link added");
 160            }
 161            else
 162            {
 163                _notifier.Error("Failed to add link");
 164            }
 165        }
 166        catch (Exception ex)
 167        {
 168            _logger.LogErrorSanitized(ex, "Error adding link to world {WorldId}", _worldId);
 169            _notifier.Error($"Failed to add link: {ex.Message}");
 170        }
 171        finally
 172        {
 173            IsSavingLink = false;
 174        }
 175    }
 176
 177    public void StartEditLink(WorldLinkDto link)
 178    {
 4179        EditingLinkId = link.Id;
 4180        EditLinkTitle = link.Title;
 4181        EditLinkUrl = link.Url;
 4182        EditLinkDescription = link.Description ?? string.Empty;
 4183    }
 184
 185    public void CancelEditLink()
 186    {
 1187        EditingLinkId = null;
 1188        EditLinkTitle = string.Empty;
 1189        EditLinkUrl = string.Empty;
 1190        EditLinkDescription = string.Empty;
 1191    }
 192
 193    public async Task SaveEditLinkAsync()
 194    {
 195        if (EditingLinkId == null)
 196            return;
 197
 198        if (string.IsNullOrWhiteSpace(EditLinkTitle) || string.IsNullOrWhiteSpace(EditLinkUrl))
 199        {
 200            _notifier.Warning("Title and URL are required");
 201            return;
 202        }
 203
 204        if (!IsValidUrl(EditLinkUrl))
 205        {
 206            _notifier.Warning("Please enter a valid URL (starting with http:// or https://)");
 207            return;
 208        }
 209
 210        IsSavingLink = true;
 211
 212        try
 213        {
 214            var dto = new WorldLinkUpdateDto
 215            {
 216                Title = EditLinkTitle.Trim(),
 217                Url = EditLinkUrl.Trim(),
 218                Description = string.IsNullOrWhiteSpace(EditLinkDescription) ? null : EditLinkDescription.Trim()
 219            };
 220
 221            var updated = await _worldApi.UpdateWorldLinkAsync(_worldId, EditingLinkId.Value, dto);
 222            if (updated != null)
 223            {
 224                Links = await _worldApi.GetWorldLinksAsync(_worldId);
 225                await _treeState.RefreshAsync();
 226                EditingLinkId = null;
 227                EditLinkTitle = string.Empty;
 228                EditLinkUrl = string.Empty;
 229                EditLinkDescription = string.Empty;
 230                _notifier.Success("Link updated");
 231            }
 232            else
 233            {
 234                _notifier.Error("Failed to update link");
 235            }
 236        }
 237        catch (Exception ex)
 238        {
 239            _logger.LogErrorSanitized(ex, "Error updating link {LinkId}", EditingLinkId!.Value);
 240            _notifier.Error($"Failed to update link: {ex.Message}");
 241        }
 242        finally
 243        {
 244            IsSavingLink = false;
 245        }
 246    }
 247
 248    public async Task DeleteLinkAsync(WorldLinkDto link)
 249    {
 250        try
 251        {
 252            var deleted = await _worldApi.DeleteWorldLinkAsync(_worldId, link.Id);
 253            if (deleted)
 254            {
 255                var updated = new List<WorldLinkDto>(_links);
 256                updated.Remove(link);
 257                Links = updated;
 258                await _treeState.RefreshAsync();
 259                _notifier.Success("Link deleted");
 260            }
 261            else
 262            {
 263                _notifier.Error("Failed to delete link");
 264            }
 265        }
 266        catch (Exception ex)
 267        {
 268            _logger.LogErrorSanitized(ex, "Error deleting link {LinkId}", link.Id);
 269            _notifier.Error($"Failed to delete link: {ex.Message}");
 270        }
 271    }
 272
 273    /// <summary>Returns a Google favicon URL for the given link URL.</summary>
 274    public static string GetFaviconUrl(string url)
 275    {
 276        try
 277        {
 2278            var uri = new Uri(url);
 1279            return $"https://www.google.com/s2/favicons?domain={uri.Host}&sz=32";
 280        }
 1281        catch
 282        {
 1283            return string.Empty;
 284        }
 2285    }
 286
 287    internal static bool IsValidUrl(string url) =>
 10288        Uri.TryCreate(url, UriKind.Absolute, out var uri)
 10289        && (uri.Scheme == "http" || uri.Scheme == "https");
 290}