| | | 1 | | namespace Chronicis.Client.Services; |
| | | 2 | | |
| | | 3 | | public class WikiLinkAutocompleteService : IWikiLinkAutocompleteService |
| | | 4 | | { |
| | | 5 | | private readonly ILinkApiService _linkApiService; |
| | | 6 | | private readonly IExternalLinkApiService _externalLinkApiService; |
| | | 7 | | private readonly ILogger<WikiLinkAutocompleteService> _logger; |
| | | 8 | | |
| | | 9 | | public event Action? OnShow; |
| | | 10 | | public event Action? OnHide; |
| | | 11 | | public event Action? OnSuggestionsUpdated; |
| | | 12 | | |
| | 0 | 13 | | public (double X, double Y) Position { get; private set; } |
| | 0 | 14 | | public bool IsVisible { get; private set; } |
| | 0 | 15 | | public string Query { get; private set; } = string.Empty; |
| | 0 | 16 | | public bool IsExternalQuery { get; private set; } |
| | 0 | 17 | | public string? ExternalSourceKey { get; private set; } |
| | 0 | 18 | | public List<WikiLinkAutocompleteItem> Suggestions { get; private set; } = new(); |
| | 0 | 19 | | public int SelectedIndex { get; private set; } |
| | 0 | 20 | | public bool IsLoading { get; private set; } |
| | | 21 | | |
| | 0 | 22 | | public WikiLinkAutocompleteService( |
| | 0 | 23 | | ILinkApiService linkApiService, |
| | 0 | 24 | | IExternalLinkApiService externalLinkApiService, |
| | 0 | 25 | | ILogger<WikiLinkAutocompleteService> logger) |
| | | 26 | | { |
| | 0 | 27 | | _linkApiService = linkApiService; |
| | 0 | 28 | | _externalLinkApiService = externalLinkApiService; |
| | 0 | 29 | | _logger = logger; |
| | 0 | 30 | | } |
| | | 31 | | |
| | | 32 | | public async Task ShowAsync(string query, double x, double y, Guid? worldId) |
| | | 33 | | { |
| | 0 | 34 | | Position = (x, y); |
| | 0 | 35 | | IsVisible = true; |
| | 0 | 36 | | SelectedIndex = 0; |
| | | 37 | | |
| | 0 | 38 | | IsExternalQuery = TryParseExternalQuery(query, out var sourceKey, out var remainder); |
| | 0 | 39 | | ExternalSourceKey = IsExternalQuery ? sourceKey : null; |
| | 0 | 40 | | Query = IsExternalQuery ? remainder : query; |
| | | 41 | | |
| | 0 | 42 | | OnShow?.Invoke(); |
| | | 43 | | |
| | | 44 | | // For external queries: no minimum length (show categories) |
| | | 45 | | // For internal queries: require 3 characters minimum |
| | 0 | 46 | | var minLength = IsExternalQuery ? 0 : 3; |
| | | 47 | | |
| | 0 | 48 | | if (Query.Length < minLength) |
| | | 49 | | { |
| | 0 | 50 | | Suggestions = new(); |
| | 0 | 51 | | OnSuggestionsUpdated?.Invoke(); |
| | 0 | 52 | | return; |
| | | 53 | | } |
| | | 54 | | |
| | 0 | 55 | | IsLoading = true; |
| | 0 | 56 | | OnSuggestionsUpdated?.Invoke(); |
| | | 57 | | |
| | | 58 | | try |
| | | 59 | | { |
| | 0 | 60 | | if (IsExternalQuery) |
| | | 61 | | { |
| | 0 | 62 | | var externalSuggestions = await _externalLinkApiService.GetSuggestionsAsync( |
| | 0 | 63 | | worldId ?? Guid.Empty, |
| | 0 | 64 | | ExternalSourceKey ?? string.Empty, |
| | 0 | 65 | | Query, |
| | 0 | 66 | | CancellationToken.None); |
| | | 67 | | |
| | 0 | 68 | | Suggestions = externalSuggestions |
| | 0 | 69 | | .Select(WikiLinkAutocompleteItem.FromExternal) |
| | 0 | 70 | | .ToList(); |
| | | 71 | | } |
| | | 72 | | else |
| | | 73 | | { |
| | 0 | 74 | | var internalSuggestions = await _linkApiService.GetSuggestionsAsync( |
| | 0 | 75 | | worldId ?? Guid.Empty, |
| | 0 | 76 | | Query); |
| | | 77 | | |
| | 0 | 78 | | Suggestions = internalSuggestions |
| | 0 | 79 | | .Select(WikiLinkAutocompleteItem.FromInternal) |
| | 0 | 80 | | .ToList(); |
| | | 81 | | } |
| | 0 | 82 | | } |
| | 0 | 83 | | catch (Exception ex) |
| | | 84 | | { |
| | 0 | 85 | | _logger.LogError(ex, "Error getting autocomplete suggestions for query: {Query}", query); |
| | 0 | 86 | | Suggestions = new(); |
| | 0 | 87 | | } |
| | | 88 | | finally |
| | | 89 | | { |
| | 0 | 90 | | IsLoading = false; |
| | 0 | 91 | | OnSuggestionsUpdated?.Invoke(); |
| | | 92 | | } |
| | 0 | 93 | | } |
| | | 94 | | |
| | | 95 | | public void Hide() |
| | | 96 | | { |
| | 0 | 97 | | IsVisible = false; |
| | 0 | 98 | | Suggestions = new(); |
| | 0 | 99 | | IsExternalQuery = false; |
| | 0 | 100 | | ExternalSourceKey = null; |
| | 0 | 101 | | Query = string.Empty; |
| | 0 | 102 | | SelectedIndex = 0; |
| | | 103 | | |
| | 0 | 104 | | OnHide?.Invoke(); |
| | 0 | 105 | | } |
| | | 106 | | |
| | | 107 | | public void SelectNext() |
| | | 108 | | { |
| | 0 | 109 | | if (Suggestions.Any()) |
| | | 110 | | { |
| | 0 | 111 | | SelectedIndex = (SelectedIndex + 1) % Suggestions.Count; |
| | 0 | 112 | | OnSuggestionsUpdated?.Invoke(); |
| | | 113 | | } |
| | 0 | 114 | | } |
| | | 115 | | |
| | | 116 | | public void SelectPrevious() |
| | | 117 | | { |
| | 0 | 118 | | if (Suggestions.Any()) |
| | | 119 | | { |
| | 0 | 120 | | SelectedIndex = (SelectedIndex - 1 + Suggestions.Count) % Suggestions.Count; |
| | 0 | 121 | | OnSuggestionsUpdated?.Invoke(); |
| | | 122 | | } |
| | 0 | 123 | | } |
| | | 124 | | |
| | | 125 | | public void SetSelectedIndex(int index) |
| | | 126 | | { |
| | 0 | 127 | | if (index >= 0 && index < Suggestions.Count) |
| | | 128 | | { |
| | 0 | 129 | | SelectedIndex = index; |
| | 0 | 130 | | OnSuggestionsUpdated?.Invoke(); |
| | | 131 | | } |
| | 0 | 132 | | } |
| | | 133 | | |
| | | 134 | | public WikiLinkAutocompleteItem? GetSelectedSuggestion() |
| | | 135 | | { |
| | 0 | 136 | | if (Suggestions.Any() && SelectedIndex >= 0 && SelectedIndex < Suggestions.Count) |
| | | 137 | | { |
| | 0 | 138 | | return Suggestions[SelectedIndex]; |
| | | 139 | | } |
| | 0 | 140 | | return null; |
| | | 141 | | } |
| | | 142 | | |
| | | 143 | | private static bool TryParseExternalQuery(string query, out string sourceKey, out string remainder) |
| | | 144 | | { |
| | 0 | 145 | | sourceKey = string.Empty; |
| | 0 | 146 | | remainder = string.Empty; |
| | | 147 | | |
| | 0 | 148 | | if (string.IsNullOrWhiteSpace(query)) |
| | 0 | 149 | | return false; |
| | | 150 | | |
| | 0 | 151 | | var slashIndex = query.IndexOf('/'); |
| | 0 | 152 | | if (slashIndex < 0) |
| | | 153 | | { |
| | | 154 | | // No slash found - check if it could be a source key prefix |
| | 0 | 155 | | var lowerQuery = query.ToLowerInvariant(); |
| | 0 | 156 | | if (lowerQuery.StartsWith("srd") || |
| | 0 | 157 | | lowerQuery.StartsWith("open5e") || |
| | 0 | 158 | | lowerQuery.StartsWith("ros")) |
| | | 159 | | { |
| | 0 | 160 | | sourceKey = lowerQuery; |
| | 0 | 161 | | remainder = string.Empty; |
| | 0 | 162 | | return true; |
| | | 163 | | } |
| | 0 | 164 | | return false; |
| | | 165 | | } |
| | | 166 | | |
| | 0 | 167 | | sourceKey = query.Substring(0, slashIndex).ToLowerInvariant(); |
| | 0 | 168 | | remainder = query.Substring(slashIndex + 1); |
| | 0 | 169 | | return true; |
| | | 170 | | } |
| | | 171 | | } |