|
| 1 | +@using Microsoft.AspNetCore.Components.Routing |
| 2 | +@inject NavigationManager Navigation |
| 3 | +@inject IJSRuntime JS |
| 4 | +@implements IAsyncDisposable |
| 5 | + |
| 6 | +@if (_isVisible) |
| 7 | +{ |
| 8 | + <div class="search-overlay" @onclick="Close" @onkeydown="HandleKeyDown"> |
| 9 | + <div class="search-container" @onclick:stopPropagation> |
| 10 | + <div class="search-input-wrapper"> |
| 11 | + <svg class="search-input-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> |
| 12 | + <circle cx="9" cy="9" r="5.5" /> |
| 13 | + <path d="m13.5 13.5 3 3" /> |
| 14 | + </svg> |
| 15 | + <input @ref="_inputRef" |
| 16 | + type="text" |
| 17 | + class="search-input" |
| 18 | + placeholder="Search pages..." |
| 19 | + @bind-value="_query" |
| 20 | + @bind-value:event="oninput" |
| 21 | + @onkeydown="HandleKeyDown" |
| 22 | + autocomplete="off" |
| 23 | + spellcheck="false" /> |
| 24 | + <kbd class="search-kbd">ESC</kbd> |
| 25 | + </div> |
| 26 | + |
| 27 | + @if (FilteredResults.Any()) |
| 28 | + { |
| 29 | + <ul class="search-results"> |
| 30 | + @foreach (var (item, index) in FilteredResults.Select((v, i) => (v, i))) |
| 31 | + { |
| 32 | + <li class="search-result-item @(index == _selectedIndex ? "selected" : "")" |
| 33 | + @onclick="() => NavigateTo(item.Route)" |
| 34 | + @onmouseenter="() => _selectedIndex = index"> |
| 35 | + <div class="search-result-icon"> |
| 36 | + @((MarkupString)item.Icon) |
| 37 | + </div> |
| 38 | + <div class="search-result-text"> |
| 39 | + <span class="search-result-title">@item.Title</span> |
| 40 | + <span class="search-result-description">@item.Description</span> |
| 41 | + </div> |
| 42 | + <span class="search-result-category">@item.Category</span> |
| 43 | + </li> |
| 44 | + } |
| 45 | + </ul> |
| 46 | + } |
| 47 | + else if (!string.IsNullOrWhiteSpace(_query)) |
| 48 | + { |
| 49 | + <div class="search-empty"> |
| 50 | + <span>No results for "<strong>@_query</strong>"</span> |
| 51 | + </div> |
| 52 | + } |
| 53 | + </div> |
| 54 | + </div> |
| 55 | +} |
| 56 | + |
| 57 | +@code { |
| 58 | + private bool _isVisible; |
| 59 | + private string _query = ""; |
| 60 | + private int _selectedIndex; |
| 61 | + private ElementReference _inputRef; |
| 62 | + private DotNetObjectReference<GlobalSearch>? _dotNetRef; |
| 63 | + |
| 64 | + private record SearchEntry(string Title, string Description, string Route, string Category, string Icon, string[] Keywords); |
| 65 | + |
| 66 | + private static readonly List<SearchEntry> AllPages = new() |
| 67 | + { |
| 68 | + new("Home", "Dashboard, updates & recent activity", "/home", "Core", |
| 69 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 9.5 10 4l7 5.5\"/><path d=\"M5.5 8.5V16h9V8.5\"/><path d=\"M8.5 16v-3.5h3V16\"/></svg>", |
| 70 | + new[] { "home", "dashboard", "updates", "activity", "welcome" }), |
| 71 | + |
| 72 | + new("Server", "Connect and manage ARK servers", "/server", "Core", |
| 73 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"14\" height=\"5\" rx=\"1.4\"/><rect x=\"3\" y=\"11\" width=\"14\" height=\"5\" rx=\"1.4\"/><path d=\"M12.5 6.5h1\"/><path d=\"M15 6.5h.5\"/><path d=\"M12.5 13.5h1\"/><path d=\"M15 13.5h.5\"/></svg>", |
| 74 | + new[] { "server", "connect", "ip", "manage", "query" }), |
| 75 | + |
| 76 | + new("Game", "Control and monitor ARK: Survival Evolved", "/game", "Core", |
| 77 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2.5\" y=\"4\" width=\"15\" height=\"12\" rx=\"2\"/><path d=\"M7 8l3 2-3 2\"/><path d=\"M11.5 12h3\"/></svg>", |
| 78 | + new[] { "game", "launch", "start", "ark", "monitor", "status", "running" }), |
| 79 | + |
| 80 | + new("INI Changer", "Manage & optimize ARK configuration presets", "/ini-changer", "ARK Tweaks", |
| 81 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 5.5h12\"/><path d=\"M4 10h12\"/><path d=\"M4 14.5h12\"/><circle cx=\"7\" cy=\"5.5\" r=\"1.25\"/><circle cx=\"12.5\" cy=\"10\" r=\"1.25\"/><circle cx=\"9\" cy=\"14.5\" r=\"1.25\"/></svg>", |
| 82 | + new[] { "ini", "config", "configuration", "presets", "settings", "basedeviceprofiles", "optimize" }), |
| 83 | + |
| 84 | + new("Vision Tools", "TEK camera behavior and scope visibility", "/suitfov", "ARK Tweaks", |
| 85 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2.5 10s3-4.5 7.5-4.5S17.5 10 17.5 10s-3 4.5-7.5 4.5S2.5 10 2.5 10Z\"/><circle cx=\"10\" cy=\"10\" r=\"2.2\"/></svg>", |
| 86 | + new[] { "vision", "fov", "camera", "tek", "suit", "scope", "zoom" }), |
| 87 | + |
| 88 | + new("Launch Options", "Quick ARK startup flags with trade-offs", "/launch-options", "ARK Tweaks", |
| 89 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 16.5V3.5\"/><path d=\"M5 4h8l-1.6 2.6L13 9H5\"/></svg>", |
| 90 | + new[] { "launch", "options", "flags", "startup", "arguments", "steam", "parameters" }), |
| 91 | + |
| 92 | + new("Fonts", "Customize ARK font settings", "/fonts", "ARK Tweaks", |
| 93 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 15 10 5l4 10\"/><path d=\"M7.4 11.5h5.2\"/></svg>", |
| 94 | + new[] { "fonts", "text", "typography", "chinese", "global", "install" }), |
| 95 | + |
| 96 | + new("Pixel Glitch", "Manage ARK texture files for visual mods", "/pixel", "ARK Tweaks", |
| 97 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.5 2.5 4 10h5l-.6 7.5L16 10h-5l-.5-7.5Z\"/></svg>", |
| 98 | + new[] { "pixel", "glitch", "texture", "visual", "riot", "saddle", "armor" }), |
| 99 | + |
| 100 | + new("Paintings", "Manage your in-game paintings", "/paintings", "ARK Tweaks", |
| 101 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"14\" height=\"12\" rx=\"2\"/><path d=\"m6.2 12.5 2.5-2.8 2.4 2.4 2.7-3.3 1.9 3.7\"/><circle cx=\"7\" cy=\"7.5\" r=\"1\"/></svg>", |
| 102 | + new[] { "paintings", "mypaintings", "art", "images" }), |
| 103 | + |
| 104 | + new("Mutagen Prices", "Browse creature mutation values by name", "/dino-prices", "Mods & Intel", |
| 105 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M11 3H6.5a2 2 0 0 0-2 2v4.5L11 16l5-5-6-8Z\"/><circle cx=\"7.3\" cy=\"7.3\" r=\"1\"/></svg>", |
| 106 | + new[] { "mutagen", "prices", "dino", "creature", "gen2", "mutation", "values" }), |
| 107 | + |
| 108 | + new("OC BPs", "Genesis 2 mission rewards for overcapped blueprints", "/oc-bps", "Mods & Intel", |
| 109 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"4.5\" y=\"3.5\" width=\"11\" height=\"13\" rx=\"2\"/><path d=\"M8 6h4\"/><path d=\"M7 9.5h6\"/><path d=\"M7 12.5h6\"/></svg>", |
| 110 | + new[] { "oc", "blueprints", "bps", "genesis", "mission", "rewards", "overcapped" }), |
| 111 | + |
| 112 | + new("Bosses", "Boss tribute guide sorted by map", "/bosses", "Mods & Intel", |
| 113 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 3 5 5.8V10c0 3 1.7 5.3 5 6.8 3.3-1.5 5-3.8 5-6.8V5.8L10 3Z\"/></svg>", |
| 114 | + new[] { "bosses", "tribute", "guide", "map", "requirements", "mini-boss", "fight" }), |
| 115 | + |
| 116 | + new("Map Mods", "Server map mods with custom coordinates", "/server-specific", "Mods & Intel", |
| 117 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 16s4.8-4.8 4.8-8A4.8 4.8 0 1 0 5.2 8c0 3.2 4.8 8 4.8 8Z\"/><circle cx=\"10\" cy=\"8\" r=\"1.8\"/></svg>", |
| 118 | + new[] { "map", "mods", "coordinates", "landmarks", "spots", "mesa", "server-specific" }), |
| 119 | + |
| 120 | + new("Steam Mods", "Browse installed workshop mods", "/steam-mods", "Mods & Intel", |
| 121 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 3.5 4.5 6.7v6.6L10 16.5l5.5-3.2V6.7L10 3.5Z\"/><path d=\"M10 10V16\"/><path d=\"M4.5 6.7 10 10l5.5-3.3\"/></svg>", |
| 122 | + new[] { "steam", "workshop", "mods", "installed", "browse" }), |
| 123 | + |
| 124 | + new("Building", "Foundation raising and lowering tutorials", "/building", "Utilities", |
| 125 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"4\" y=\"4\" width=\"4\" height=\"4\" rx=\"1\"/><rect x=\"12\" y=\"4\" width=\"4\" height=\"4\" rx=\"1\"/><rect x=\"8\" y=\"12\" width=\"4\" height=\"4\" rx=\"1\"/></svg>", |
| 126 | + new[] { "building", "foundation", "raising", "lowering", "tutorials", "video" }), |
| 127 | + |
| 128 | + new("Auto Clicker", "Advanced mouse automation tool", "/autoclicker", "Utilities", |
| 129 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 3.5v3\"/><path d=\"M7 5.5h6\"/><path d=\"M10 8.5v8\"/><path d=\"M10 16.5c3 0 4.8-1.8 4.8-4.3 0-1.8-1.1-3.2-2.9-3.7\"/><path d=\"M10 16.5c-3 0-4.8-1.8-4.8-4.3 0-1.8 1.1-3.2 2.9-3.7\"/></svg>", |
| 130 | + new[] { "autoclicker", "auto", "clicker", "mouse", "automation", "click", "hotkey" }), |
| 131 | + |
| 132 | + new("Macro / AHK", "Video, refs, scripts for macros", "/crafting-scripts", "Utilities", |
| 133 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2.5\" y=\"6\" width=\"15\" height=\"8.5\" rx=\"2\"/><path d=\"M5.5 9.5h1\"/><path d=\"M8.5 9.5h1\"/><path d=\"M11.5 9.5h1\"/><path d=\"M14.5 9.5h0.5\"/><path d=\"M5.5 12h5\"/><path d=\"M12.5 12h2.5\"/></svg>", |
| 134 | + new[] { "macro", "ahk", "autohotkey", "crafting", "scripts", "jitbit" }), |
| 135 | + |
| 136 | + new("Troubleshoot", "Logging, diagnostics, and quick fixes", "/troubleshoot", "Help & About", |
| 137 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 3.5 16.5 15H3.5L10 3.5Z\"/><path d=\"M10 7.2v4.3\"/><path d=\"M10 13.6h.01\"/></svg>", |
| 138 | + new[] { "troubleshoot", "diagnostics", "logging", "error", "fix", "debug", "logs" }), |
| 139 | + |
| 140 | + new("Credits", "Developer information & contact", "/credits", "Help & About", |
| 141 | + "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"10\" cy=\"10\" r=\"6.5\"/><path d=\"M10 8v4\"/><path d=\"M10 6.3h.01\"/></svg>", |
| 142 | + new[] { "credits", "about", "developer", "contact", "info", "support" }), |
| 143 | + }; |
| 144 | + |
| 145 | + private List<SearchEntry> FilteredResults |
| 146 | + { |
| 147 | + get |
| 148 | + { |
| 149 | + if (string.IsNullOrWhiteSpace(_query)) |
| 150 | + return AllPages; |
| 151 | + |
| 152 | + var q = _query.Trim().ToLowerInvariant(); |
| 153 | + var scored = new List<(SearchEntry Entry, int Score)>(); |
| 154 | + |
| 155 | + foreach (var entry in AllPages) |
| 156 | + { |
| 157 | + int score = 0; |
| 158 | + |
| 159 | + // Title exact start match (highest priority) |
| 160 | + if (entry.Title.StartsWith(q, StringComparison.OrdinalIgnoreCase)) |
| 161 | + score += 100; |
| 162 | + // Title contains |
| 163 | + else if (entry.Title.Contains(q, StringComparison.OrdinalIgnoreCase)) |
| 164 | + score += 80; |
| 165 | + |
| 166 | + // Keyword exact match |
| 167 | + if (entry.Keywords.Any(k => k.Equals(q, StringComparison.OrdinalIgnoreCase))) |
| 168 | + score += 90; |
| 169 | + // Keyword starts with |
| 170 | + else if (entry.Keywords.Any(k => k.StartsWith(q, StringComparison.OrdinalIgnoreCase))) |
| 171 | + score += 60; |
| 172 | + // Keyword contains |
| 173 | + else if (entry.Keywords.Any(k => k.Contains(q, StringComparison.OrdinalIgnoreCase))) |
| 174 | + score += 40; |
| 175 | + |
| 176 | + // Description contains |
| 177 | + if (entry.Description.Contains(q, StringComparison.OrdinalIgnoreCase)) |
| 178 | + score += 30; |
| 179 | + |
| 180 | + // Category contains |
| 181 | + if (entry.Category.Contains(q, StringComparison.OrdinalIgnoreCase)) |
| 182 | + score += 20; |
| 183 | + |
| 184 | + if (score > 0) |
| 185 | + scored.Add((entry, score)); |
| 186 | + } |
| 187 | + |
| 188 | + _selectedIndex = 0; |
| 189 | + return scored.OrderByDescending(s => s.Score).Select(s => s.Entry).ToList(); |
| 190 | + } |
| 191 | + } |
| 192 | + |
| 193 | + protected override async Task OnAfterRenderAsync(bool firstRender) |
| 194 | + { |
| 195 | + if (firstRender) |
| 196 | + { |
| 197 | + _dotNetRef = DotNetObjectReference.Create(this); |
| 198 | + await JS.InvokeVoidAsync("registerGlobalSearch", _dotNetRef); |
| 199 | + } |
| 200 | + |
| 201 | + if (_isVisible) |
| 202 | + { |
| 203 | + try |
| 204 | + { |
| 205 | + await _inputRef.FocusAsync(); |
| 206 | + } |
| 207 | + catch |
| 208 | + { |
| 209 | + // Element may not be rendered yet |
| 210 | + } |
| 211 | + } |
| 212 | + } |
| 213 | + |
| 214 | + [JSInvokable] |
| 215 | + public void ToggleSearch() |
| 216 | + { |
| 217 | + _isVisible = !_isVisible; |
| 218 | + if (_isVisible) |
| 219 | + { |
| 220 | + _query = ""; |
| 221 | + _selectedIndex = 0; |
| 222 | + } |
| 223 | + _ = InvokeAsync(StateHasChanged); |
| 224 | + } |
| 225 | + |
| 226 | + private void Close() |
| 227 | + { |
| 228 | + _isVisible = false; |
| 229 | + _query = ""; |
| 230 | + _selectedIndex = 0; |
| 231 | + } |
| 232 | + |
| 233 | + private void HandleKeyDown(KeyboardEventArgs e) |
| 234 | + { |
| 235 | + var results = FilteredResults; |
| 236 | + |
| 237 | + switch (e.Key) |
| 238 | + { |
| 239 | + case "Escape": |
| 240 | + Close(); |
| 241 | + break; |
| 242 | + case "ArrowDown": |
| 243 | + _selectedIndex = (_selectedIndex + 1) % Math.Max(results.Count, 1); |
| 244 | + break; |
| 245 | + case "ArrowUp": |
| 246 | + _selectedIndex = (_selectedIndex - 1 + Math.Max(results.Count, 1)) % Math.Max(results.Count, 1); |
| 247 | + break; |
| 248 | + case "Enter": |
| 249 | + if (results.Count > 0 && _selectedIndex < results.Count) |
| 250 | + NavigateTo(results[_selectedIndex].Route); |
| 251 | + break; |
| 252 | + } |
| 253 | + } |
| 254 | + |
| 255 | + private void NavigateTo(string route) |
| 256 | + { |
| 257 | + Close(); |
| 258 | + Navigation.NavigateTo(route); |
| 259 | + } |
| 260 | + |
| 261 | + public async ValueTask DisposeAsync() |
| 262 | + { |
| 263 | + try |
| 264 | + { |
| 265 | + await JS.InvokeVoidAsync("unregisterGlobalSearch"); |
| 266 | + } |
| 267 | + catch |
| 268 | + { |
| 269 | + // JS interop may fail during disposal |
| 270 | + } |
| 271 | + _dotNetRef?.Dispose(); |
| 272 | + } |
| 273 | +} |
0 commit comments