< Summary

Information
Class: Chronicis.Client.ViewModels.WorldSharingViewModel
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/ViewModels/WorldSharingViewModel.cs
Line coverage
100%
Covered lines: 47
Uncovered lines: 0
Coverable lines: 47
Total lines: 201
Line coverage: 100%
Branch coverage
100%
Covered branches: 26
Total branches: 26
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_IsPublic()100%11100%
set_IsPublic(...)100%11100%
get_PublicSlug()100%11100%
set_PublicSlug(...)100%11100%
get_IsCheckingSlug()100%11100%
set_IsCheckingSlug(...)100%11100%
get_SlugIsAvailable()100%11100%
set_SlugIsAvailable(...)100%11100%
get_SlugError()100%11100%
set_SlugError(...)100%11100%
get_SlugHelperText()100%11100%
set_SlugHelperText(...)100%11100%
InitializeFrom(...)100%22100%
OnPublicToggleChanged(...)100%1212100%
GetPublicUrlBase(...)100%11100%
GetFullPublicUrl(...)100%44100%
ShouldShowPublicPreview(...)100%44100%
GenerateSlugFromName(...)100%44100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/ViewModels/WorldSharingViewModel.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 public slug and sharing settings for a world.
 10/// </summary>
 11public sealed class WorldSharingViewModel : ViewModelBase
 12{
 13    private readonly IWorldApiService _worldApi;
 14    private readonly IAppNavigator _navigator;
 15    private readonly IUserNotifier _notifier;
 16    private readonly ILogger<WorldSharingViewModel> _logger;
 17
 18    private bool _isPublic;
 4219    private string _publicSlug = string.Empty;
 20    private bool _isCheckingSlug;
 21    private bool _slugIsAvailable;
 22    private string? _slugError;
 23    private string? _slugHelperText;
 24
 25    /// <summary>Raised when a change is made that requires the parent world to be saved.</summary>
 26    public event Action? UnsavedChangesOccurred;
 27
 4228    public WorldSharingViewModel(
 4229        IWorldApiService worldApi,
 4230        IAppNavigator navigator,
 4231        IUserNotifier notifier,
 4232        ILogger<WorldSharingViewModel> logger)
 33    {
 4234        _worldApi = worldApi;
 4235        _navigator = navigator;
 4236        _notifier = notifier;
 4237        _logger = logger;
 4238    }
 39
 40    public bool IsPublic
 41    {
 1442        get => _isPublic;
 1843        set => SetField(ref _isPublic, value);
 44    }
 45
 46    public string PublicSlug
 47    {
 2748        get => _publicSlug;
 2749        set => SetField(ref _publicSlug, value);
 50    }
 51
 52    public bool IsCheckingSlug
 53    {
 154        get => _isCheckingSlug;
 1455        private set => SetField(ref _isCheckingSlug, value);
 56    }
 57
 58    public bool SlugIsAvailable
 59    {
 960        get => _slugIsAvailable;
 2361        private set => SetField(ref _slugIsAvailable, value);
 62    }
 63
 64    public string? SlugError
 65    {
 766        get => _slugError;
 2767        private set => SetField(ref _slugError, value);
 68    }
 69
 70    public string? SlugHelperText
 71    {
 472        get => _slugHelperText;
 2873        private set => SetField(ref _slugHelperText, value);
 74    }
 75
 76    /// <summary>Initialises sharing state from a loaded world.</summary>
 77    public void InitializeFrom(WorldDetailDto world)
 78    {
 1379        IsPublic = world.IsPublic;
 1380        PublicSlug = world.PublicSlug ?? string.Empty;
 1381        SlugIsAvailable = world.IsPublic;
 1382        SlugError = null;
 1383        SlugHelperText = null;
 1384    }
 85
 86    public void OnPublicToggleChanged(WorldDetailDto? world)
 87    {
 588        UnsavedChangesOccurred?.Invoke();
 89
 590        if (IsPublic && string.IsNullOrEmpty(PublicSlug))
 91        {
 392            PublicSlug = GenerateSlugFromName(world?.Name ?? string.Empty);
 393            _ = CheckSlugAvailabilityAsync(world?.Id ?? Guid.Empty);
 94        }
 595    }
 96
 97    public async Task CheckSlugAvailabilityAsync(Guid worldId)
 98    {
 99        if (string.IsNullOrWhiteSpace(PublicSlug))
 100        {
 101            SlugIsAvailable = false;
 102            SlugError = null;
 103            SlugHelperText = null;
 104            return;
 105        }
 106
 107        IsCheckingSlug = true;
 108        SlugError = null;
 109        SlugHelperText = null;
 110
 111        try
 112        {
 113            var result = await _worldApi.CheckPublicSlugAsync(worldId, PublicSlug);
 114
 115            if (result == null)
 116            {
 117                SlugError = "Failed to check availability";
 118                SlugIsAvailable = false;
 119            }
 120            else if (!string.IsNullOrEmpty(result.ValidationError))
 121            {
 122                SlugError = result.ValidationError;
 123                SlugIsAvailable = false;
 124                if (!string.IsNullOrEmpty(result.SuggestedSlug))
 125                    SlugHelperText = $"Try: {result.SuggestedSlug}";
 126            }
 127            else if (!result.IsAvailable)
 128            {
 129                SlugError = "This slug is already taken";
 130                SlugIsAvailable = false;
 131                if (!string.IsNullOrEmpty(result.SuggestedSlug))
 132                    SlugHelperText = $"Try: {result.SuggestedSlug}";
 133            }
 134            else
 135            {
 136                SlugIsAvailable = true;
 137                SlugHelperText = "Available!";
 138            }
 139
 140            UnsavedChangesOccurred?.Invoke();
 141        }
 142        catch (Exception ex)
 143        {
 144            _logger.LogErrorSanitized(ex, "Error checking slug availability for world {WorldId}", worldId);
 145            SlugError = $"Error: {ex.Message}";
 146            SlugIsAvailable = false;
 147        }
 148        finally
 149        {
 150            IsCheckingSlug = false;
 151        }
 152    }
 153
 154    public async Task CopyPublicUrlAsync(string url)
 155    {
 156        if (string.IsNullOrEmpty(url))
 157            return;
 158
 159        try
 160        {
 161            // Navigation.BaseUri is passed in by the page to keep IJSRuntime out of the VM
 162            _navigator.NavigateTo($"javascript:navigator.clipboard.writeText('{url}')");
 163            _notifier.Success("Public URL copied to clipboard");
 164        }
 165        catch (Exception ex)
 166        {
 167            _logger.LogErrorSanitized(ex, "Error copying public URL");
 168            _notifier.Error("Failed to copy URL");
 169        }
 170
 171        await Task.CompletedTask;
 172    }
 173
 174    public string GetPublicUrlBase(string baseUri) =>
 2175        $"{baseUri.TrimEnd('/')}/w/";
 176
 177    public string GetFullPublicUrl(string baseUri, WorldDetailDto? world)
 178    {
 3179        if (world == null || string.IsNullOrEmpty(world.PublicSlug))
 2180            return string.Empty;
 1181        return $"{GetPublicUrlBase(baseUri)}{world.PublicSlug}";
 182    }
 183
 184    public bool ShouldShowPublicPreview(WorldDetailDto? world) =>
 5185        world is not null && world.IsPublic && !string.IsNullOrEmpty(world.PublicSlug);
 186
 187    /// <summary>Generates a URL-safe slug from a world name.</summary>
 188    public static string GenerateSlugFromName(string name)
 189    {
 9190        if (string.IsNullOrWhiteSpace(name))
 4191            return string.Empty;
 192
 5193        var slug = name.Trim().ToLowerInvariant();
 5194        slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[\s_]+", "-");
 5195        slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[^a-z0-9-]", "");
 5196        slug = System.Text.RegularExpressions.Regex.Replace(slug, @"-+", "-");
 5197        slug = slug.Trim('-');
 198
 5199        return slug.Length >= 3 ? slug : slug.PadRight(3, '0');
 200    }
 201}