From dc7bc8a58de640898bf3940bff6f9cf41f05b025 Mon Sep 17 00:00:00 2001 From: Owen de Bree Date: Tue, 23 Jun 2026 09:17:22 +0200 Subject: [PATCH 1/2] feat(configs): add crossplay profile actions Add confirmed profile actions to clear all mods and clone a profile with EOS crossplay defaults. Lock the Mods tab with a tooltip while Cross Platform is enabled and refresh generated reference docs. --- Docs/Reference/Index.md | 2 +- Docs/Reference/Modules/Quasar.Components.md | 2 +- Docs/Reference/data/manifest.json | 4 +- Docs/Reference/data/module_index.json | 2 +- .../Quasar/Components/Pages/Configs.razor.md | 10 +- Quasar/Components/Pages/Configs.razor | 95 ++++++++++++++++++- 6 files changed, 105 insertions(+), 10 deletions(-) diff --git a/Docs/Reference/Index.md b/Docs/Reference/Index.md index dfae11d..076b73a 100644 --- a/Docs/Reference/Index.md +++ b/Docs/Reference/Index.md @@ -71,7 +71,7 @@ Every documented source file (210 total), alphabetical by path. See the [TOC](TO | [Quasar/Components/Pages/Chat.razor](files/Quasar/Components/Pages/Chat.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Routable page (`/chat`) that gives admins a full-width chat and command console for managed servers. It combines a server dropdown, live recent-chat feed from the selected agent snapshot, a chat/command input that sends text through `ServerCommandType.SendChat`, command-mode autocomplete sourced from registered PluginSdk commands, quick Refresh/Save/Restart actions, and recent command-result feedback. Server-authored chat (`IsServerMessage`, SteamId 0, `Good.bot`, or `Server`) is displayed as `Server`. | | [Quasar/Components/Pages/ConfigProfilePendingChangesDialog.razor](files/Quasar/Components/Pages/ConfigProfilePendingChangesDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Small confirmation dialog displayed by `Configs.razor` when the user tries to switch config templates while the current template has unsaved edits. Returns a `PendingChangesAction` discriminated union (Cancel, Discard, Save) to let the caller decide how to handle the pending state. | | [Quasar/Components/Pages/ConfigProfileQuickCreateDialog.razor](files/Quasar/Components/Pages/ConfigProfileQuickCreateDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Modal dialog for quickly creating a new `QuasarConfigProfile` (config template) from the Home dashboard setup wizard. Validates that a name is provided, persists the new profile via `QuasarConfigProfileCatalog`, and returns the created `QuasarConfigProfile` to the caller on success. | -| [Quasar/Components/Pages/Configs.razor](files/Quasar/Components/Pages/Configs.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.) | +| [Quasar/Components/Pages/Configs.razor](files/Quasar/Components/Pages/Configs.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, all-mod clearing, crossplay-safe profile cloning, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.) | | [Quasar/Components/Pages/Configs.razor.css](files/Quasar/Components/Pages/Configs.razor.css.md) | [Quasar.Components](Modules/Quasar.Components.md) | CSS | Scoped stylesheet for `Configs.razor`. Styles the two-column page shell (sticky template sidebar + scrollable editor), the template tiles, collapsible config/world panels, option cards (with search-highlight and focus states), workshop thumbnails, and plugin description lines. | | [Quasar/Components/Pages/ConfigsPageDialog.razor](files/Quasar/Components/Pages/ConfigsPageDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Thin full-screen dialog wrapper that hosts the `Configs` page component. Used by the Home dashboard setup wizard to surface the config-template editor as a modal overlay without navigating away from the dashboard. | | [Quasar/Components/Pages/Discord.razor](files/Quasar/Components/Pages/Discord.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Routable page (`/discord`) for configuring the Quasar Discord bot. Combines global bot settings (token, guild ID, enabled flag), per-server channel bindings (command, chat-relay, log, analytics, death-messages, simspeed-alert channels plus interval and feature toggles), configurable simspeed alert rules, editable death-message template text areas organised by death-type category with a copyable template-file path, and a primary `Console Logs` button that opens the dedicated Discord integration log dialog. | diff --git a/Docs/Reference/Modules/Quasar.Components.md b/Docs/Reference/Modules/Quasar.Components.md index c80dd79..26d301c 100644 --- a/Docs/Reference/Modules/Quasar.Components.md +++ b/Docs/Reference/Modules/Quasar.Components.md @@ -29,7 +29,7 @@ The Blazor Server user interface, built with MudBlazor. Routable pages cover the | [Quasar/Components/Pages/Chat.razor](../files/Quasar/Components/Pages/Chat.razor.md) | Blazor component | Routable page (`/chat`) that gives admins a full-width chat and command console for managed servers. It combines a server dropdown, live recent-chat feed from the selected agent snapshot, a chat/command input that sends text through `ServerCommandType.SendChat`, command-mode autocomplete sourced from registered PluginSdk commands, quick Refresh/Save/Restart actions, and recent command-result feedback. Server-authored chat (`IsServerMessage`, SteamId 0, `Good.bot`, or `Server`) is displayed as `Server`. | | [Quasar/Components/Pages/ConfigProfilePendingChangesDialog.razor](../files/Quasar/Components/Pages/ConfigProfilePendingChangesDialog.razor.md) | Blazor component | Small confirmation dialog displayed by `Configs.razor` when the user tries to switch config templates while the current template has unsaved edits. Returns a `PendingChangesAction` discriminated union (Cancel, Discard, Save) to let the caller decide how to handle the pending state. | | [Quasar/Components/Pages/ConfigProfileQuickCreateDialog.razor](../files/Quasar/Components/Pages/ConfigProfileQuickCreateDialog.razor.md) | Blazor component | Modal dialog for quickly creating a new `QuasarConfigProfile` (config template) from the Home dashboard setup wizard. Validates that a name is provided, persists the new profile via `QuasarConfigProfileCatalog`, and returns the created `QuasarConfigProfile` to the caller on success. | -| [Quasar/Components/Pages/Configs.razor](../files/Quasar/Components/Pages/Configs.razor.md) | Blazor component | The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.) | +| [Quasar/Components/Pages/Configs.razor](../files/Quasar/Components/Pages/Configs.razor.md) | Blazor component | The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, all-mod clearing, crossplay-safe profile cloning, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.) | | [Quasar/Components/Pages/Configs.razor.css](../files/Quasar/Components/Pages/Configs.razor.css.md) | CSS | Scoped stylesheet for `Configs.razor`. Styles the two-column page shell (sticky template sidebar + scrollable editor), the template tiles, collapsible config/world panels, option cards (with search-highlight and focus states), workshop thumbnails, and plugin description lines. | | [Quasar/Components/Pages/ConfigsPageDialog.razor](../files/Quasar/Components/Pages/ConfigsPageDialog.razor.md) | Blazor component | Thin full-screen dialog wrapper that hosts the `Configs` page component. Used by the Home dashboard setup wizard to surface the config-template editor as a modal overlay without navigating away from the dashboard. | | [Quasar/Components/Pages/Discord.razor](../files/Quasar/Components/Pages/Discord.razor.md) | Blazor component | Routable page (`/discord`) for configuring the Quasar Discord bot. Combines global bot settings (token, guild ID, enabled flag), per-server channel bindings (command, chat-relay, log, analytics, death-messages, simspeed-alert channels plus interval and feature toggles), configurable simspeed alert rules, editable death-message template text areas organised by death-type category with a copyable template-file path, and a primary `Console Logs` button that opens the dedicated Discord integration log dialog. | diff --git a/Docs/Reference/data/manifest.json b/Docs/Reference/data/manifest.json index 50040f2..c2a8adf 100644 --- a/Docs/Reference/data/manifest.json +++ b/Docs/Reference/data/manifest.json @@ -674,8 +674,8 @@ "path": "Quasar/Components/Pages/Configs.razor", "name": "Configs.razor", "ext": ".razor", - "size": 123364, - "sha256": "c999ded09f440b060b223af442160854670e195864310b2ae1ff4cf91353a952", + "size": 127286, + "sha256": "b3cac12e7199df4789422b204be50dd02ac3b40a36eab4e9d9dcef4d611d00ee", "module": "Quasar.Components", "tier": 2, "status": "pending" diff --git a/Docs/Reference/data/module_index.json b/Docs/Reference/data/module_index.json index 2db90df..90c5bc8 100644 --- a/Docs/Reference/data/module_index.json +++ b/Docs/Reference/data/module_index.json @@ -480,7 +480,7 @@ "name": "Configs.razor", "kind": "Blazor component", "tier": 2, - "summary": "The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.)" + "summary": "The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, all-mod clearing, crossplay-safe profile cloning, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.)" }, { "path": "Quasar/Components/Pages/Configs.razor.css", diff --git a/Docs/Reference/files/Quasar/Components/Pages/Configs.razor.md b/Docs/Reference/files/Quasar/Components/Pages/Configs.razor.md index f1ccb63..aeb26ab 100644 --- a/Docs/Reference/files/Quasar/Components/Pages/Configs.razor.md +++ b/Docs/Reference/files/Quasar/Components/Pages/Configs.razor.md @@ -3,19 +3,19 @@ **Module:** Quasar.Components **Kind:** Blazor component **Tier:** 2 ## Summary -The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.) +The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, all-mod clearing, crossplay-safe profile cloning, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.) ## Structure - `@page "/configs"`, `@implements IDisposable`. - **`[Inject]`ed services:** `QuasarConfigProfileCatalog ConfigProfiles`, `QuasarDevFolderCatalog DevFolderCatalog`, `QuasarPluginCatalogService PluginCatalog`, `QuasarWorkshopModResolver WorkshopMods`, `SteamWorkshopCredentialsCatalog WorkshopCredentials`, `DedicatedServerCatalog ServerCatalog`, `ISnackbar Snackbar`, `IJSRuntime JS`, `IDialogService DialogService`. - **`[Parameter]`s:** `InitialProfileId`; `RequestedProfileId` (`[SupplyParameterFromQuery(Name="profileId")]`) — selects the initial template. - **Sidebar:** new-template name field (Enter to create), template tiles (subtitle summary "N plugins, N mods, N servers"), clone/delete icon buttons; selection guarded by `ConfirmPendingChangesBeforeSwitchAsync`. -- **Header paper:** name/description fields, count chips (Plugins/Mods/Assigned Servers, "Catalog stale" warning), Save / Reset buttons. +- **Header paper:** name/description fields, count chips (Plugins/Mods/Assigned Servers, "Catalog stale" warning), Save / Reset, confirmed Clear Mods, and confirmed Clone for Crossplay buttons. The crossplay clone copies the current editor state into a new profile, forces EOS networking, enables Cross Platform, disables Experimental Mode, removes mods, persists the clone, and selects it. - **World tab:** search field + "Jump to First Match" + match-count chip; categories as `MudExpansionPanel`s (single-expansion, `KeepContentAlive=false`) driven by `QuasarConfigMetadata.Categories`/`Options`, including a dedicated **Survival** section for game mode, production, respawn, oxygen/radiation, hunger, and progression settings. Each option renders by `QuasarConfigOptionKind` only while its panel is open. Server password is masked in the editor and later emitted to DS config as hash/salt; block type world limits edit as `BlockSubtype=Limit` lines. A synthetic **Access** panel edits whitelist Group ID, Admin/Reserved/Banned ID lists (numeric-filtered, parsed via `ParseUnsignedLongList`/`SplitListTokens`) with matching explanatory tooltips. - **Plugins tab:** rendered only while active (`MudTabs KeepPanelsAlive=false`); contains "Plugins to load" table ordered as Name, Debug build, then a rightmost unlabeled action column (inline checkbox for selected local dev-folder plugins), "Plugin catalog" panel (lazy refresh on panel open, search, Refresh Catalog, selection checkboxes, hidden hub entries filtered out, auto-managed entries excluded from manual selection, full-Tooltip column with a shortened-Description fallback + `PluginCatalogDescriptionDialog` showing the full Description, and a rightmost unlabeled action column), and "Advanced/manual plugin setup" (add custom plugin ID). -- **Mods tab:** rendered only while active; contains "Mod list" table ordered as Name, Workshop ID, then a rightmost unlabeled action column ("Merge from World Template" → `MergeWorldTemplateModsDialog`, "Remove Dead Mods" → `WorkshopMods.CheckAvailabilityAsync` and a removed-mod summary), "Steam Workshop" panel (lazy popular-load on panel open when an API key exists, search/popular, API-key chip + `SteamWorkshopApiKeyDialog`, results table with selection/preview/tags/Workshop mod columns and rightmost unlabeled open action), and "Advanced/manual mod setup" (resolve URLs/IDs/collections via `WorkshopMods.ResolveAsync`, manual add). +- **Mods tab:** rendered only while active and disabled with a hover tooltip while `RootSettings.CrossPlatform` is true; contains "Mod list" table ordered as Name, Workshop ID, then a rightmost unlabeled action column ("Merge from World Template" → `MergeWorldTemplateModsDialog`, "Remove Dead Mods" → `WorkshopMods.CheckAvailabilityAsync` and a removed-mod summary), "Steam Workshop" panel (lazy popular-load on panel open when an API key exists, search/popular, API-key chip + `SteamWorkshopApiKeyDialog`, results table with selection/preview/tags/Workshop mod columns and rightmost unlabeled open action), and "Advanced/manual mod setup" (resolve URLs/IDs/collections via `WorkshopMods.ResolveAsync`, manual add). - **Developer panel:** collapsed by default with `KeepContentAlive=false`; opening it renders the dev-folder table with manifest, name, folder path, and rightmost unlabeled actions, plus "Add dev folder..." → `PluginManifestPickerDialog`; the picked XML manifest is validated/read via `PluginManifestReader` and registered in `DevFolderCatalog` with `DebugBuild=true` by default. -- **State helpers:** profile snapshot/`HasPendingChanges` (JSON diff ignoring `UpdatedAtUtc`), active-tab and panel expansion tracking, selected-plugin-to-dev-folder lookup for inline debug toggles, search matching (`MatchesSearchTerms`), option get/set via reflection through `QuasarConfigMetadata` with numeric conversion for non-`int` properties, `GetKeyValueTextOption`/`SetKeyValueTextOption` for dictionary-backed block limits, `JumpToFirstOptionAsync` uses JS interop `quasarConfigs.focusElement`. Config-profile catalog changes reload the selected profile from disk, while dev-folder catalog changes only re-render the page so toggling a local plugin's debug build does not discard pending profile edits. +- **State helpers:** profile snapshot/`HasPendingChanges` (JSON diff ignoring `UpdatedAtUtc`), active-tab and panel expansion tracking including crossplay mod-tab lockout, selected-plugin-to-dev-folder lookup for inline debug toggles, search matching (`MatchesSearchTerms`), option get/set via reflection through `QuasarConfigMetadata` with numeric conversion for non-`int` properties, `GetKeyValueTextOption`/`SetKeyValueTextOption` for dictionary-backed block limits, `JumpToFirstOptionAsync` uses JS interop `quasarConfigs.focusElement`. Config-profile catalog changes reload the selected profile from disk, while dev-folder catalog changes only re-render the page so toggling a local plugin's debug build does not discard pending profile edits. ## Dependencies - `Quasar/Services/QuasarConfigProfileCatalog.cs`, `Quasar/Services/QuasarDevFolderCatalog.cs`, `Quasar/Services/QuasarPluginCatalogService.cs`, `Quasar/Services/QuasarWorkshopModResolver.cs`, `Quasar/Services/SteamWorkshopCredentialsCatalog.cs`, `Quasar/Services/DedicatedServerCatalog.cs` @@ -27,6 +27,8 @@ The `/configs` page: a full editor for reusable Magnetar config templates (`Quas ## Notes - Switching/resetting templates is guarded against unsaved edits via a snapshot JSON comparison and the pending-changes dialog. +- Clearing all mods is a confirmed in-memory edit; Save Profile is still required to persist it to the profile catalog. +- Clone for Crossplay is a confirmed profile creation action that leaves the source profile unchanged and selects the persisted clone. - Plugin selection respects MagnetarHub rules: auto-managed plugins (`IsManualSelectionAllowed`) cannot be manually selected, and hidden hub entries are omitted from the manual catalog list even when searching. - Heavy sections are lazy: inactive tabs and collapsed expansion panels do not keep hidden content alive, plugin catalog refresh is deferred until its panel is opened, and popular Workshop results load only when the Workshop panel is opened with an API key available. - "Remove Dead Mods" checks selected Workshop IDs through Steam published-file details, removes unavailable entries from the in-memory profile editor, and requires Save to persist the cleaned list. diff --git a/Quasar/Components/Pages/Configs.razor b/Quasar/Components/Pages/Configs.razor index f379896..d6fa1b3 100644 --- a/Quasar/Components/Pages/Configs.razor +++ b/Quasar/Components/Pages/Configs.razor @@ -130,6 +130,21 @@ Save Profile Reset + + Clear Mods + + + Clone for Crossplay + @@ -568,7 +583,7 @@ } - + @if (IsConfigTabActive(ModsTabIndex)) { @@ -852,6 +867,7 @@ private const int ModsTabIndex = 2; private const int PluginDescriptionFallbackMaxLength = 50; private const string ConfigOptionTooltipStyle = "white-space: pre-line; max-width: 34rem; text-align: left; line-height: 1.35; font-size: 0.78rem;"; + private const string CrossplayModsDisabledWarning = "Mods are disabled while Cross Platform is enabled. Turn off Cross Platform to add or edit mods."; private int _activeConfigTabIndex = WorldTabIndex; private string _selectedProfileId = string.Empty; private QuasarConfigProfile? _editor; @@ -939,6 +955,12 @@ private bool HasHiddenCatalogEntries => CatalogEntries.Any(entry => entry.Hidden); + private bool IsModsTabLocked => _editor?.RootSettings.CrossPlatform == true; + + private string? ModsTabTooltip => IsModsTabLocked + ? CrossplayModsDisabledWarning + : null; + private static bool IsCatalogEntryVisible(QuasarPluginCatalogEntry entry) => !entry.Hidden; private List FilteredPlugins => (_editor?.Plugins ?? []) @@ -1041,6 +1063,55 @@ Snackbar.Add("Config profile cloned.", Severity.Success); } + private async Task ClearCurrentProfileModsAsync() + { + if (_editor is null) + return; + + if (_editor.Mods.Count == 0) + { + Snackbar.Add("No mods selected.", Severity.Info); + return; + } + + var confirmed = await DialogService.ShowMessageBoxAsync( + "Clear all mods?", + $"{_editor.Mods.Count} mod(s) will be removed from the pending config profile changes. Save the profile to persist this change.", + yesText: "Clear Mods", + cancelText: "Cancel"); + + if (confirmed != true) + return; + + _editor.Mods = []; + _deadModCleanupSummary = string.Empty; + _deadModCleanupRemoved = []; + Snackbar.Add("All mods cleared. Save profile to persist.", Severity.Success); + } + + private async Task CloneCurrentProfileForCrossplayAsync() + { + if (_editor is null) + return; + + var sourceName = string.IsNullOrWhiteSpace(_editor.Name) + ? "current profile" + : _editor.Name.Trim(); + var confirmed = await DialogService.ShowMessageBoxAsync( + "Clone profile for crossplay?", + $"A new profile will be created from '{sourceName}' with EOS networking, Cross Platform enabled, Experimental Mode disabled, and all mods removed. The current profile will not be changed.", + yesText: "Clone", + cancelText: "Cancel"); + + if (confirmed != true) + return; + + var clone = CreateCrossplayClone(_editor); + await ConfigProfiles.UpsertAsync(clone); + SelectProfile(clone.ConfigProfileId); + Snackbar.Add("Crossplay config profile cloned.", Severity.Success); + } + private async Task DeleteProfileAsync(string configProfileId) { var assignedServers = GetAssignedServers(configProfileId); @@ -1128,6 +1199,7 @@ _deadModCleanupRemoved = []; _hasExplicitCategoryExpansion = true; _expandedCategoryKey = null; + EnsureActiveConfigTabAllowed(); } private string GetInitialProfileId() @@ -1158,6 +1230,20 @@ return JsonSerializer.Serialize(clone); } + private static QuasarConfigProfile CreateCrossplayClone(QuasarConfigProfile source) + { + var clone = JsonSerializer.Deserialize(JsonSerializer.Serialize(source)) ?? new QuasarConfigProfile(); + clone.ConfigProfileId = string.Empty; + clone.Name = $"{(string.IsNullOrWhiteSpace(source.Name) ? "Profile" : source.Name.Trim())} (Crossplay)"; + clone.RootSettings ??= new QuasarWorldRootSettings(); + clone.SessionSettings ??= new QuasarSessionSettings(); + clone.RootSettings.NetworkType = QuasarNetworkType.EOS; + clone.RootSettings.CrossPlatform = true; + clone.SessionSettings.ExperimentalMode = false; + clone.Mods = []; + return clone; + } + private async Task RefreshPluginCatalogAsync(bool showSuccessMessage = true) { if (_catalogRefreshing) @@ -1270,6 +1356,12 @@ private bool IsConfigTabActive(int tabIndex) => _activeConfigTabIndex == tabIndex; + private void EnsureActiveConfigTabAllowed() + { + if (IsModsTabLocked && _activeConfigTabIndex == ModsTabIndex) + _activeConfigTabIndex = WorldTabIndex; + } + private string? GetExpandedCategoryKey() { if (_hasExplicitCategoryExpansion) @@ -1472,6 +1564,7 @@ converted = Convert.ChangeType(value, property.PropertyType, System.Globalization.CultureInfo.InvariantCulture); property.SetValue(target, converted); + EnsureActiveConfigTabAllowed(); } private object? GetOptionTarget(QuasarConfigOptionDefinition option) From 5c7cf24adc627478a9e4db48f3cb2b49ae77b770 Mon Sep 17 00:00:00 2001 From: Owen de Bree Date: Tue, 23 Jun 2026 10:02:16 +0200 Subject: [PATCH 2/2] feat(ui): add bulk admin actions - Add Ctrl/Meta toggle and Shift range selection to the entity browser. - Reuse the table hover palette for selected entity rows and keep the row order tied to the current MudTable filtered view. - Add Select shown, Clear, and Delete selected actions for entity cleanup, with row-action clicks kept out of selection handling. - Add Restore Default Profiles to the config profiles sidebar, backed by catalog logic that recreates or resets the built-in Survival and Creative profiles without deleting custom profiles. - Refresh generated reference docs, manifest hashes, and module indexes for the changed UI and service files. Verified: - dotnet build Quasar.sln --no-restore - Docs/Reference/data/verify_links.py --- Docs/Reference/Index.md | 6 +- Docs/Reference/Modules/Quasar.Components.md | 4 +- .../Reference/Modules/Quasar.Services.Core.md | 2 +- Docs/Reference/data/manifest.json | 16 +- Docs/Reference/data/module_index.json | 6 +- .../Quasar/Components/Pages/Configs.razor.md | 7 +- .../Quasar/Components/Pages/Entities.razor.md | 16 +- .../Services/QuasarConfigProfileCatalog.cs.md | 5 +- .../Reference/files/Quasar/wwwroot/app.css.md | 3 +- Quasar/Components/Pages/Configs.razor | 33 +++ Quasar/Components/Pages/Entities.razor | 269 +++++++++++++++++- Quasar/Services/QuasarConfigProfileCatalog.cs | 27 ++ Quasar/wwwroot/app.css | 86 +++--- 13 files changed, 402 insertions(+), 78 deletions(-) diff --git a/Docs/Reference/Index.md b/Docs/Reference/Index.md index 076b73a..7c7e819 100644 --- a/Docs/Reference/Index.md +++ b/Docs/Reference/Index.md @@ -71,12 +71,12 @@ Every documented source file (210 total), alphabetical by path. See the [TOC](TO | [Quasar/Components/Pages/Chat.razor](files/Quasar/Components/Pages/Chat.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Routable page (`/chat`) that gives admins a full-width chat and command console for managed servers. It combines a server dropdown, live recent-chat feed from the selected agent snapshot, a chat/command input that sends text through `ServerCommandType.SendChat`, command-mode autocomplete sourced from registered PluginSdk commands, quick Refresh/Save/Restart actions, and recent command-result feedback. Server-authored chat (`IsServerMessage`, SteamId 0, `Good.bot`, or `Server`) is displayed as `Server`. | | [Quasar/Components/Pages/ConfigProfilePendingChangesDialog.razor](files/Quasar/Components/Pages/ConfigProfilePendingChangesDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Small confirmation dialog displayed by `Configs.razor` when the user tries to switch config templates while the current template has unsaved edits. Returns a `PendingChangesAction` discriminated union (Cancel, Discard, Save) to let the caller decide how to handle the pending state. | | [Quasar/Components/Pages/ConfigProfileQuickCreateDialog.razor](files/Quasar/Components/Pages/ConfigProfileQuickCreateDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Modal dialog for quickly creating a new `QuasarConfigProfile` (config template) from the Home dashboard setup wizard. Validates that a name is provided, persists the new profile via `QuasarConfigProfileCatalog`, and returns the created `QuasarConfigProfile` to the caller on success. | -| [Quasar/Components/Pages/Configs.razor](files/Quasar/Components/Pages/Configs.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, all-mod clearing, crossplay-safe profile cloning, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.) | +| [Quasar/Components/Pages/Configs.razor](files/Quasar/Components/Pages/Configs.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/restores/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, all-mod clearing, crossplay-safe profile cloning, default-profile restoration, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.) | | [Quasar/Components/Pages/Configs.razor.css](files/Quasar/Components/Pages/Configs.razor.css.md) | [Quasar.Components](Modules/Quasar.Components.md) | CSS | Scoped stylesheet for `Configs.razor`. Styles the two-column page shell (sticky template sidebar + scrollable editor), the template tiles, collapsible config/world panels, option cards (with search-highlight and focus states), workshop thumbnails, and plugin description lines. | | [Quasar/Components/Pages/ConfigsPageDialog.razor](files/Quasar/Components/Pages/ConfigsPageDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Thin full-screen dialog wrapper that hosts the `Configs` page component. Used by the Home dashboard setup wizard to surface the config-template editor as a modal overlay without navigating away from the dashboard. | | [Quasar/Components/Pages/Discord.razor](files/Quasar/Components/Pages/Discord.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Routable page (`/discord`) for configuring the Quasar Discord bot. Combines global bot settings (token, guild ID, enabled flag), per-server channel bindings (command, chat-relay, log, analytics, death-messages, simspeed-alert channels plus interval and feature toggles), configurable simspeed alert rules, editable death-message template text areas organised by death-type category with a copyable template-file path, and a primary `Console Logs` button that opens the dedicated Discord integration log dialog. | | [Quasar/Components/Pages/DiscordConsoleDialog.razor](files/Quasar/Components/Pages/DiscordConsoleDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | MudBlazor dialog that displays the dedicated Discord integration log (`discord.log`). It mirrors the server console log UX with a copyable file path, full-log download button, Refresh action, exception-excerpt mode, and a scrollable monospace log pane. | -| [Quasar/Components/Pages/Entities.razor](files/Quasar/Components/Pages/Entities.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Routable page (`/entities`) providing a live entity browser for connected Space Engineers server agents. The user selects a connected agent and entity type filter, presses Refresh to fetch up to 500 entities, then can search across the result set and delete individual entities with confirmation. Requires a live Quasar.Agent connection; shows an informational alert otherwise. | +| [Quasar/Components/Pages/Entities.razor](files/Quasar/Components/Pages/Entities.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Routable page (`/entities`) providing a live entity browser for connected Space Engineers server agents. The user selects a connected agent and entity type filter, presses Refresh to fetch up to 500 entities, then can search across the result set and delete individual entities or a multi-selected batch with confirmation. Entity rows support normal click selection, Ctrl/Meta toggle selection, and Shift range selection. Requires a live Quasar.Agent connection; shows an informational alert otherwise. | | [Quasar/Components/Pages/Error.razor](files/Quasar/Components/Pages/Error.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Standard ASP.NET Core error page rendered at `/Error`. Displays a generic error message and, when a request or activity ID is available, shows it for diagnostics. Advises enabling the Development environment for detailed exceptions and warns against doing so in production. | | [Quasar/Components/Pages/FolderPickerDialog.razor](files/Quasar/Components/Pages/FolderPickerDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | General-purpose server-side folder browser dialog. Renders a navigable directory tree with breadcrumb navigation, bookmark shortcut chips, caller-supplied shortcut chips, hidden-folder toggle, quick filtering for the current folder list, and an optional world-folder validation mode. Returns the selected absolute path on confirmation. Used from `Configs.razor` (dev-folder picker), server instance editors (world-folder picker), and world-template import flows. | | [Quasar/Components/Pages/FolderPickerDialog.razor.css](files/Quasar/Components/Pages/FolderPickerDialog.razor.css.md) | [Quasar.Components](Modules/Quasar.Components.md) | CSS | Minimal scoped stylesheet for `FolderPickerDialog.razor`. Contains a single rule that removes the default uppercase text transform from the breadcrumb navigation buttons so directory names render in their natural casing. | @@ -187,7 +187,7 @@ Every documented source file (210 total), alphabetical by path. See the [TOC](TO | [Quasar/Services/PluginSdk/PluginLogEntry.cs](files/Quasar/Services/PluginSdk/PluginLogEntry.cs.md) | [Quasar.Services.PluginSdk](Modules/Quasar.Services.PluginSdk.md) | class | Immutable record-style class representing one structured log entry produced by a plugin through the PluginSdk `QuasarLogSink`. The sink writes compact JSON lines to the dedicated server's stdout; `PluginLogStream.TryParseSinkLine` parses those lines into this type. Field shape mirrors the sink JSON: `{ timestamp, level, plugin, thread, message, data?, exception? }`. | | [Quasar/Services/PluginSdk/PluginLogStream.cs](files/Quasar/Services/PluginSdk/PluginLogStream.cs.md) | [Quasar.Services.PluginSdk](Modules/Quasar.Services.PluginSdk.md) | class | In-memory ring buffer of recent plugin log entries keyed by server unique name, plus a static parser for the PluginSdk `QuasarLogSink` JSON stdout format. The supervisor feeds entries parsed from each dedicated server's standard output; Blazor components subscribe to `Changed` and read entries via `GetEntries`, `GetRecent`, or `Query`. Follows the same lock-guarded, event-raising shape as other Quasar runtime services. | | [Quasar/Services/QuasarConfigMetadata.cs](files/Quasar/Services/QuasarConfigMetadata.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | Defines the full compile-time schema for every Quasar configuration option that the UI can present and edit. It declares enums for option scope (`Root`/`Session`) and kind (Boolean, Integer, Decimal, Text, LongText, Password, KeyValueText, SelectInteger, SelectText), typed record helpers for categories and select options, and a rich `QuasarConfigOptionDefinition` type with validation and full-text search. The static `QuasarConfigMetadata` class holds two read-only lists — `Categories` (12 groupings, including Survival) and `Options` (covering Quasar root settings plus DS-visible session settings, including block type limits) — and provides reflection-based property lookup and value formatting helpers used by the config editor UI. | -| [Quasar/Services/QuasarConfigProfileCatalog.cs](files/Quasar/Services/QuasarConfigProfileCatalog.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | Manages the persistent catalog of named Quasar configuration profiles (reusable bundles of root/session settings, plugin selections, and mod lists). Profiles are stored as individual `profile.json` files under `/ConfigProfiles//`. The catalog watches the directory for external edits, debounces reload events, keeps versioned history on every save, and fires a `Changed` event when the in-memory state diverges from disk. | +| [Quasar/Services/QuasarConfigProfileCatalog.cs](files/Quasar/Services/QuasarConfigProfileCatalog.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | Manages the persistent catalog of named Quasar configuration profiles (reusable bundles of root/session settings, plugin selections, and mod lists). Profiles are stored as individual `profile.json` files under `/ConfigProfiles//`. The catalog watches the directory for external edits, debounces reload events, keeps versioned history on every save, can recreate/reset Quasar's built-in starter profiles, and fires a `Changed` event when the in-memory state diverges from disk. | | [Quasar/Services/QuasarDevFolderCatalog.cs](files/Quasar/Services/QuasarDevFolderCatalog.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | Manages a persisted list of local developer plugin folders used during plugin development. Each entry (`QuasarDevFolderSelection`) maps a folder path and plugin manifest data file to an optional plugin-id override and debug-build flag, which defaults to `true` for new or legacy entries without an explicit value. The catalog loads from and saves to a single `dev-folders.json` file in the Quasar data directory, and fires a `Changed` event after every mutation. | | [Quasar/Services/QuasarLoggingConfigurator.cs](files/Quasar/Services/QuasarLoggingConfigurator.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | Static configurator that wires NLog into the ASP.NET Core host at startup. It builds an NLog `LoggingConfiguration` with the main Quasar file target, a dedicated Discord integration file target, and an optional console target (activated when `QUASAR_CONSOLE_LOGGING=true`), then replaces the default Microsoft logging providers with NLog via `UseNLog()`. | | [Quasar/Services/QuasarPluginCatalogService.cs](files/Quasar/Services/QuasarPluginCatalogService.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | Maintains the in-memory catalog of available Quasar plugins, sourced from the MagnetarHub GitHub repository (downloaded as a ZIP archive, parsed from XML manifests) and supplemented at runtime by local developer-folder entries. The catalog is cached to disk with a schema-versioned JSON file and can be refreshed on demand. It also exposes helper utilities for URL construction and plugin-id resolution. | diff --git a/Docs/Reference/Modules/Quasar.Components.md b/Docs/Reference/Modules/Quasar.Components.md index 26d301c..b11e237 100644 --- a/Docs/Reference/Modules/Quasar.Components.md +++ b/Docs/Reference/Modules/Quasar.Components.md @@ -29,12 +29,12 @@ The Blazor Server user interface, built with MudBlazor. Routable pages cover the | [Quasar/Components/Pages/Chat.razor](../files/Quasar/Components/Pages/Chat.razor.md) | Blazor component | Routable page (`/chat`) that gives admins a full-width chat and command console for managed servers. It combines a server dropdown, live recent-chat feed from the selected agent snapshot, a chat/command input that sends text through `ServerCommandType.SendChat`, command-mode autocomplete sourced from registered PluginSdk commands, quick Refresh/Save/Restart actions, and recent command-result feedback. Server-authored chat (`IsServerMessage`, SteamId 0, `Good.bot`, or `Server`) is displayed as `Server`. | | [Quasar/Components/Pages/ConfigProfilePendingChangesDialog.razor](../files/Quasar/Components/Pages/ConfigProfilePendingChangesDialog.razor.md) | Blazor component | Small confirmation dialog displayed by `Configs.razor` when the user tries to switch config templates while the current template has unsaved edits. Returns a `PendingChangesAction` discriminated union (Cancel, Discard, Save) to let the caller decide how to handle the pending state. | | [Quasar/Components/Pages/ConfigProfileQuickCreateDialog.razor](../files/Quasar/Components/Pages/ConfigProfileQuickCreateDialog.razor.md) | Blazor component | Modal dialog for quickly creating a new `QuasarConfigProfile` (config template) from the Home dashboard setup wizard. Validates that a name is provided, persists the new profile via `QuasarConfigProfileCatalog`, and returns the created `QuasarConfigProfile` to the caller on success. | -| [Quasar/Components/Pages/Configs.razor](../files/Quasar/Components/Pages/Configs.razor.md) | Blazor component | The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, all-mod clearing, crossplay-safe profile cloning, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.) | +| [Quasar/Components/Pages/Configs.razor](../files/Quasar/Components/Pages/Configs.razor.md) | Blazor component | The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/restores/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, all-mod clearing, crossplay-safe profile cloning, default-profile restoration, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.) | | [Quasar/Components/Pages/Configs.razor.css](../files/Quasar/Components/Pages/Configs.razor.css.md) | CSS | Scoped stylesheet for `Configs.razor`. Styles the two-column page shell (sticky template sidebar + scrollable editor), the template tiles, collapsible config/world panels, option cards (with search-highlight and focus states), workshop thumbnails, and plugin description lines. | | [Quasar/Components/Pages/ConfigsPageDialog.razor](../files/Quasar/Components/Pages/ConfigsPageDialog.razor.md) | Blazor component | Thin full-screen dialog wrapper that hosts the `Configs` page component. Used by the Home dashboard setup wizard to surface the config-template editor as a modal overlay without navigating away from the dashboard. | | [Quasar/Components/Pages/Discord.razor](../files/Quasar/Components/Pages/Discord.razor.md) | Blazor component | Routable page (`/discord`) for configuring the Quasar Discord bot. Combines global bot settings (token, guild ID, enabled flag), per-server channel bindings (command, chat-relay, log, analytics, death-messages, simspeed-alert channels plus interval and feature toggles), configurable simspeed alert rules, editable death-message template text areas organised by death-type category with a copyable template-file path, and a primary `Console Logs` button that opens the dedicated Discord integration log dialog. | | [Quasar/Components/Pages/DiscordConsoleDialog.razor](../files/Quasar/Components/Pages/DiscordConsoleDialog.razor.md) | Blazor component | MudBlazor dialog that displays the dedicated Discord integration log (`discord.log`). It mirrors the server console log UX with a copyable file path, full-log download button, Refresh action, exception-excerpt mode, and a scrollable monospace log pane. | -| [Quasar/Components/Pages/Entities.razor](../files/Quasar/Components/Pages/Entities.razor.md) | Blazor component | Routable page (`/entities`) providing a live entity browser for connected Space Engineers server agents. The user selects a connected agent and entity type filter, presses Refresh to fetch up to 500 entities, then can search across the result set and delete individual entities with confirmation. Requires a live Quasar.Agent connection; shows an informational alert otherwise. | +| [Quasar/Components/Pages/Entities.razor](../files/Quasar/Components/Pages/Entities.razor.md) | Blazor component | Routable page (`/entities`) providing a live entity browser for connected Space Engineers server agents. The user selects a connected agent and entity type filter, presses Refresh to fetch up to 500 entities, then can search across the result set and delete individual entities or a multi-selected batch with confirmation. Entity rows support normal click selection, Ctrl/Meta toggle selection, and Shift range selection. Requires a live Quasar.Agent connection; shows an informational alert otherwise. | | [Quasar/Components/Pages/Error.razor](../files/Quasar/Components/Pages/Error.razor.md) | Blazor component | Standard ASP.NET Core error page rendered at `/Error`. Displays a generic error message and, when a request or activity ID is available, shows it for diagnostics. Advises enabling the Development environment for detailed exceptions and warns against doing so in production. | | [Quasar/Components/Pages/FolderPickerDialog.razor](../files/Quasar/Components/Pages/FolderPickerDialog.razor.md) | Blazor component | General-purpose server-side folder browser dialog. Renders a navigable directory tree with breadcrumb navigation, bookmark shortcut chips, caller-supplied shortcut chips, hidden-folder toggle, quick filtering for the current folder list, and an optional world-folder validation mode. Returns the selected absolute path on confirmation. Used from `Configs.razor` (dev-folder picker), server instance editors (world-folder picker), and world-template import flows. | | [Quasar/Components/Pages/FolderPickerDialog.razor.css](../files/Quasar/Components/Pages/FolderPickerDialog.razor.css.md) | CSS | Minimal scoped stylesheet for `FolderPickerDialog.razor`. Contains a single rule that removes the default uppercase text transform from the breadcrumb navigation buttons so directory names render in their natural casing. | diff --git a/Docs/Reference/Modules/Quasar.Services.Core.md b/Docs/Reference/Modules/Quasar.Services.Core.md index 2c00151..1dddd6e 100644 --- a/Docs/Reference/Modules/Quasar.Services.Core.md +++ b/Docs/Reference/Modules/Quasar.Services.Core.md @@ -33,7 +33,7 @@ The heart of the supervisor and its supporting services. `DedicatedServerSupervi | [Quasar/Services/PluginCatalogRefreshService.cs](../files/Quasar/Services/PluginCatalogRefreshService.cs.md) | class | Background hosted service that periodically refreshes the Quasar plugin catalog so its cached plugin/manifest data stays current without user action. After a short startup delay it refreshes once, then refreshes on a fixed interval. Refresh failures are logged and the previously cached catalog is kept as a fallback. | | [Quasar/Services/PluginManifestReader.cs](../files/Quasar/Services/PluginManifestReader.cs.md) | class | `PluginManifestReader` is a static utility for validating and reading metadata from a Magnetar plugin's manifest XML file when an admin registers a dev folder. It checks file existence and XML well-formedness, and extracts display fields (`FriendlyName`, `Author`, `Description`, `Tooltip`, `Runtimes`). | | [Quasar/Services/QuasarConfigMetadata.cs](../files/Quasar/Services/QuasarConfigMetadata.cs.md) | class | Defines the full compile-time schema for every Quasar configuration option that the UI can present and edit. It declares enums for option scope (`Root`/`Session`) and kind (Boolean, Integer, Decimal, Text, LongText, Password, KeyValueText, SelectInteger, SelectText), typed record helpers for categories and select options, and a rich `QuasarConfigOptionDefinition` type with validation and full-text search. The static `QuasarConfigMetadata` class holds two read-only lists — `Categories` (12 groupings, including Survival) and `Options` (covering Quasar root settings plus DS-visible session settings, including block type limits) — and provides reflection-based property lookup and value formatting helpers used by the config editor UI. | -| [Quasar/Services/QuasarConfigProfileCatalog.cs](../files/Quasar/Services/QuasarConfigProfileCatalog.cs.md) | class | Manages the persistent catalog of named Quasar configuration profiles (reusable bundles of root/session settings, plugin selections, and mod lists). Profiles are stored as individual `profile.json` files under `/ConfigProfiles//`. The catalog watches the directory for external edits, debounces reload events, keeps versioned history on every save, and fires a `Changed` event when the in-memory state diverges from disk. | +| [Quasar/Services/QuasarConfigProfileCatalog.cs](../files/Quasar/Services/QuasarConfigProfileCatalog.cs.md) | class | Manages the persistent catalog of named Quasar configuration profiles (reusable bundles of root/session settings, plugin selections, and mod lists). Profiles are stored as individual `profile.json` files under `/ConfigProfiles//`. The catalog watches the directory for external edits, debounces reload events, keeps versioned history on every save, can recreate/reset Quasar's built-in starter profiles, and fires a `Changed` event when the in-memory state diverges from disk. | | [Quasar/Services/QuasarDevFolderCatalog.cs](../files/Quasar/Services/QuasarDevFolderCatalog.cs.md) | class | Manages a persisted list of local developer plugin folders used during plugin development. Each entry (`QuasarDevFolderSelection`) maps a folder path and plugin manifest data file to an optional plugin-id override and debug-build flag, which defaults to `true` for new or legacy entries without an explicit value. The catalog loads from and saves to a single `dev-folders.json` file in the Quasar data directory, and fires a `Changed` event after every mutation. | | [Quasar/Services/QuasarLoggingConfigurator.cs](../files/Quasar/Services/QuasarLoggingConfigurator.cs.md) | class | Static configurator that wires NLog into the ASP.NET Core host at startup. It builds an NLog `LoggingConfiguration` with the main Quasar file target, a dedicated Discord integration file target, and an optional console target (activated when `QUASAR_CONSOLE_LOGGING=true`), then replaces the default Microsoft logging providers with NLog via `UseNLog()`. | | [Quasar/Services/QuasarPluginCatalogService.cs](../files/Quasar/Services/QuasarPluginCatalogService.cs.md) | class | Maintains the in-memory catalog of available Quasar plugins, sourced from the MagnetarHub GitHub repository (downloaded as a ZIP archive, parsed from XML manifests) and supplemented at runtime by local developer-folder entries. The catalog is cached to disk with a schema-versioned JSON file and can be refreshed on demand. It also exposes helper utilities for URL construction and plugin-id resolution. | diff --git a/Docs/Reference/data/manifest.json b/Docs/Reference/data/manifest.json index c2a8adf..7d7b6ea 100644 --- a/Docs/Reference/data/manifest.json +++ b/Docs/Reference/data/manifest.json @@ -674,8 +674,8 @@ "path": "Quasar/Components/Pages/Configs.razor", "name": "Configs.razor", "ext": ".razor", - "size": 127286, - "sha256": "b3cac12e7199df4789422b204be50dd02ac3b40a36eab4e9d9dcef4d611d00ee", + "size": 128650, + "sha256": "f302a98a0a7fdcfb576bb14845567a53b1f8a9f39076c69eaa3cf0a61d99211e", "module": "Quasar.Components", "tier": 2, "status": "pending" @@ -724,8 +724,8 @@ "path": "Quasar/Components/Pages/Entities.razor", "name": "Entities.razor", "ext": ".razor", - "size": 12843, - "sha256": "d21dce911dc713522ff51557c566781f411830d1bb1ba68e11c416e499b4a455", + "size": 21325, + "sha256": "78517f00784cadb6773673410480e973202357d01d4e2e348aea4c32f0e4a08b", "module": "Quasar.Components", "tier": 2, "status": "pending" @@ -1834,8 +1834,8 @@ "path": "Quasar/Services/QuasarConfigProfileCatalog.cs", "name": "QuasarConfigProfileCatalog.cs", "ext": ".cs", - "size": 15956, - "sha256": "d550eaf06107ec2bc87ecdecde126b0de5900d32238322b31469a13a577b1245", + "size": 16846, + "sha256": "1c5a61f5bb0532e68c95e06bfc8f6b665b7ca033b72f073283d8e2c0b3028503", "module": "Quasar.Services.Core", "tier": 1, "status": "pending" @@ -2094,8 +2094,8 @@ "path": "Quasar/wwwroot/app.css", "name": "app.css", "ext": ".css", - "size": 22703, - "sha256": "52ae73f6181895673afcc1767bcc1af9b43743324af858dae6001bda599a69e4", + "size": 23859, + "sha256": "4581fe4d0fa46a97dd6e48a979e08ec2ffd1b560b691000408196fbf2071e929", "module": "Quasar.Host", "tier": 3, "status": "pending" diff --git a/Docs/Reference/data/module_index.json b/Docs/Reference/data/module_index.json index 90c5bc8..a6431c3 100644 --- a/Docs/Reference/data/module_index.json +++ b/Docs/Reference/data/module_index.json @@ -480,7 +480,7 @@ "name": "Configs.razor", "kind": "Blazor component", "tier": 2, - "summary": "The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, all-mod clearing, crossplay-safe profile cloning, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.)" + "summary": "The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/restores/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, all-mod clearing, crossplay-safe profile cloning, default-profile restoration, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.)" }, { "path": "Quasar/Components/Pages/Configs.razor.css", @@ -515,7 +515,7 @@ "name": "Entities.razor", "kind": "Blazor component", "tier": 2, - "summary": "Routable page (`/entities`) providing a live entity browser for connected Space Engineers server agents. The user selects a connected agent and entity type filter, presses Refresh to fetch up to 500 entities, then can search across the result set and delete individual entities with confirmation. Requires a live Quasar.Agent connection; shows an informational alert otherwise." + "summary": "Routable page (`/entities`) providing a live entity browser for connected Space Engineers server agents. The user selects a connected agent and entity type filter, presses Refresh to fetch up to 500 entities, then can search across the result set and delete individual entities or a multi-selected batch with confirmation. Entity rows support normal click selection, Ctrl/Meta toggle selection, and Shift range selection. Requires a live Quasar.Agent connection; shows an informational alert otherwise." }, { "path": "Quasar/Components/Pages/Error.razor", @@ -1095,7 +1095,7 @@ "name": "QuasarConfigProfileCatalog.cs", "kind": "class", "tier": 1, - "summary": "Manages the persistent catalog of named Quasar configuration profiles (reusable bundles of root/session settings, plugin selections, and mod lists). Profiles are stored as individual `profile.json` files under `/ConfigProfiles//`. The catalog watches the directory for external edits, debounces reload events, keeps versioned history on every save, and fires a `Changed` event when the in-memory state diverges from disk." + "summary": "Manages the persistent catalog of named Quasar configuration profiles (reusable bundles of root/session settings, plugin selections, and mod lists). Profiles are stored as individual `profile.json` files under `/ConfigProfiles//`. The catalog watches the directory for external edits, debounces reload events, keeps versioned history on every save, can recreate/reset Quasar's built-in starter profiles, and fires a `Changed` event when the in-memory state diverges from disk." }, { "path": "Quasar/Services/QuasarDevFolderCatalog.cs", diff --git a/Docs/Reference/files/Quasar/Components/Pages/Configs.razor.md b/Docs/Reference/files/Quasar/Components/Pages/Configs.razor.md index aeb26ab..7edca68 100644 --- a/Docs/Reference/files/Quasar/Components/Pages/Configs.razor.md +++ b/Docs/Reference/files/Quasar/Components/Pages/Configs.razor.md @@ -3,19 +3,19 @@ **Module:** Quasar.Components **Kind:** Blazor component **Tier:** 2 ## Summary -The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, all-mod clearing, crossplay-safe profile cloning, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.) +The `/configs` page: a full editor for reusable Magnetar config templates (`QuasarConfigProfile`) that are applied to assigned dedicated servers at startup. A sidebar lists/creates/restores/clones/deletes templates; the main column edits World settings, Plugins, Mods, and Developer dev-folders across tabbed and collapsible panels, generating only the active tab and opened panel bodies. QoL features include searchable/jump-to world options, a refreshable plugin catalog, Steam Workshop search, world-template mod merge, all-mod clearing, crossplay-safe profile cloning, default-profile restoration, dead Workshop mod cleanup, unsaved-change guarding, and integration with the plugin-manifest picker dialog for registering local dev folders. (This page has no charts; the analytics charting work lives in `Analytics.razor`.) ## Structure - `@page "/configs"`, `@implements IDisposable`. - **`[Inject]`ed services:** `QuasarConfigProfileCatalog ConfigProfiles`, `QuasarDevFolderCatalog DevFolderCatalog`, `QuasarPluginCatalogService PluginCatalog`, `QuasarWorkshopModResolver WorkshopMods`, `SteamWorkshopCredentialsCatalog WorkshopCredentials`, `DedicatedServerCatalog ServerCatalog`, `ISnackbar Snackbar`, `IJSRuntime JS`, `IDialogService DialogService`. - **`[Parameter]`s:** `InitialProfileId`; `RequestedProfileId` (`[SupplyParameterFromQuery(Name="profileId")]`) — selects the initial template. -- **Sidebar:** new-template name field (Enter to create), template tiles (subtitle summary "N plugins, N mods, N servers"), clone/delete icon buttons; selection guarded by `ConfirmPendingChangesBeforeSwitchAsync`. +- **Sidebar:** new-template name field (Enter to create), Create Profile button, Restore Default Profiles button (recreates/resets built-in Survival/Creative profiles without changing custom profiles), template tiles (subtitle summary "N plugins, N mods, N servers"), clone/delete icon buttons; selection and restore are guarded by `ConfirmPendingChangesBeforeSwitchAsync`. - **Header paper:** name/description fields, count chips (Plugins/Mods/Assigned Servers, "Catalog stale" warning), Save / Reset, confirmed Clear Mods, and confirmed Clone for Crossplay buttons. The crossplay clone copies the current editor state into a new profile, forces EOS networking, enables Cross Platform, disables Experimental Mode, removes mods, persists the clone, and selects it. - **World tab:** search field + "Jump to First Match" + match-count chip; categories as `MudExpansionPanel`s (single-expansion, `KeepContentAlive=false`) driven by `QuasarConfigMetadata.Categories`/`Options`, including a dedicated **Survival** section for game mode, production, respawn, oxygen/radiation, hunger, and progression settings. Each option renders by `QuasarConfigOptionKind` only while its panel is open. Server password is masked in the editor and later emitted to DS config as hash/salt; block type world limits edit as `BlockSubtype=Limit` lines. A synthetic **Access** panel edits whitelist Group ID, Admin/Reserved/Banned ID lists (numeric-filtered, parsed via `ParseUnsignedLongList`/`SplitListTokens`) with matching explanatory tooltips. - **Plugins tab:** rendered only while active (`MudTabs KeepPanelsAlive=false`); contains "Plugins to load" table ordered as Name, Debug build, then a rightmost unlabeled action column (inline checkbox for selected local dev-folder plugins), "Plugin catalog" panel (lazy refresh on panel open, search, Refresh Catalog, selection checkboxes, hidden hub entries filtered out, auto-managed entries excluded from manual selection, full-Tooltip column with a shortened-Description fallback + `PluginCatalogDescriptionDialog` showing the full Description, and a rightmost unlabeled action column), and "Advanced/manual plugin setup" (add custom plugin ID). - **Mods tab:** rendered only while active and disabled with a hover tooltip while `RootSettings.CrossPlatform` is true; contains "Mod list" table ordered as Name, Workshop ID, then a rightmost unlabeled action column ("Merge from World Template" → `MergeWorldTemplateModsDialog`, "Remove Dead Mods" → `WorkshopMods.CheckAvailabilityAsync` and a removed-mod summary), "Steam Workshop" panel (lazy popular-load on panel open when an API key exists, search/popular, API-key chip + `SteamWorkshopApiKeyDialog`, results table with selection/preview/tags/Workshop mod columns and rightmost unlabeled open action), and "Advanced/manual mod setup" (resolve URLs/IDs/collections via `WorkshopMods.ResolveAsync`, manual add). - **Developer panel:** collapsed by default with `KeepContentAlive=false`; opening it renders the dev-folder table with manifest, name, folder path, and rightmost unlabeled actions, plus "Add dev folder..." → `PluginManifestPickerDialog`; the picked XML manifest is validated/read via `PluginManifestReader` and registered in `DevFolderCatalog` with `DebugBuild=true` by default. -- **State helpers:** profile snapshot/`HasPendingChanges` (JSON diff ignoring `UpdatedAtUtc`), active-tab and panel expansion tracking including crossplay mod-tab lockout, selected-plugin-to-dev-folder lookup for inline debug toggles, search matching (`MatchesSearchTerms`), option get/set via reflection through `QuasarConfigMetadata` with numeric conversion for non-`int` properties, `GetKeyValueTextOption`/`SetKeyValueTextOption` for dictionary-backed block limits, `JumpToFirstOptionAsync` uses JS interop `quasarConfigs.focusElement`. Config-profile catalog changes reload the selected profile from disk, while dev-folder catalog changes only re-render the page so toggling a local plugin's debug build does not discard pending profile edits. +- **State helpers:** profile snapshot/`HasPendingChanges` (JSON diff ignoring `UpdatedAtUtc`), active-tab and panel expansion tracking including crossplay mod-tab lockout, selected-plugin-to-dev-folder lookup for inline debug toggles, search matching (`MatchesSearchTerms`), option get/set via reflection through `QuasarConfigMetadata` with numeric conversion for non-`int` properties, `GetKeyValueTextOption`/`SetKeyValueTextOption` for dictionary-backed block limits, `JumpToFirstOptionAsync` uses JS interop `quasarConfigs.focusElement`, and `RestoreDefaultProfilesAsync` confirms then calls `QuasarConfigProfileCatalog.RestoreDefaultProfilesAsync`. Config-profile catalog changes reload the selected profile from disk, while dev-folder catalog changes only re-render the page so toggling a local plugin's debug build does not discard pending profile edits. ## Dependencies - `Quasar/Services/QuasarConfigProfileCatalog.cs`, `Quasar/Services/QuasarDevFolderCatalog.cs`, `Quasar/Services/QuasarPluginCatalogService.cs`, `Quasar/Services/QuasarWorkshopModResolver.cs`, `Quasar/Services/SteamWorkshopCredentialsCatalog.cs`, `Quasar/Services/DedicatedServerCatalog.cs` @@ -27,6 +27,7 @@ The `/configs` page: a full editor for reusable Magnetar config templates (`Quas ## Notes - Switching/resetting templates is guarded against unsaved edits via a snapshot JSON comparison and the pending-changes dialog. +- Restoring default profiles resets or recreates only the built-in Survival/Creative profiles; custom profiles are left in place. - Clearing all mods is a confirmed in-memory edit; Save Profile is still required to persist it to the profile catalog. - Clone for Crossplay is a confirmed profile creation action that leaves the source profile unchanged and selects the persisted clone. - Plugin selection respects MagnetarHub rules: auto-managed plugins (`IsManualSelectionAllowed`) cannot be manually selected, and hidden hub entries are omitted from the manual catalog list even when searching. diff --git a/Docs/Reference/files/Quasar/Components/Pages/Entities.razor.md b/Docs/Reference/files/Quasar/Components/Pages/Entities.razor.md index 0a21759..77681b5 100644 --- a/Docs/Reference/files/Quasar/Components/Pages/Entities.razor.md +++ b/Docs/Reference/files/Quasar/Components/Pages/Entities.razor.md @@ -3,7 +3,7 @@ **Module:** Quasar.Components **Kind:** Blazor component **Tier:** 2 ## Summary -Routable page (`/entities`) providing a live entity browser for connected Space Engineers server agents. The user selects a connected agent and entity type filter, presses Refresh to fetch up to 500 entities, then can search across the result set and delete individual entities with confirmation. Requires a live Quasar.Agent connection; shows an informational alert otherwise. +Routable page (`/entities`) providing a live entity browser for connected Space Engineers server agents. The user selects a connected agent and entity type filter, presses Refresh to fetch up to 500 entities, then can search across the result set and delete individual entities or a multi-selected batch with confirmation. Entity rows support normal click selection, Ctrl/Meta toggle selection, and Shift range selection. Requires a live Quasar.Agent connection; shows an informational alert otherwise. ## Structure - **Route:** `@page "/entities"` @@ -11,13 +11,20 @@ Routable page (`/entities`) providing a live entity browser for connected Space - **Injected services:** `AgentRegistry`, `DedicatedServerCatalog`, `EntityService`, `IDialogService`, `ISnackbar` - **Key UI sections:** - Toolbar: server selector `MudSelect` (connected agents only, labelled via `ResolveServerName`), type filter `MudSelect` (All/Grid/Character/Float/Voxel), search `MudTextField`, Refresh button (disabled while loading or no agent selected). - - Status row: chips for matching count, shown count, total entity count; last-updated timestamp; loading spinner. + - Status/action row: chips for matching count, shown count, total entity count, selected count; last-updated timestamp; loading spinner; multi-selection actions for Select shown, Clear, and Delete selected. - Conditional alerts for no connected servers, stale agent selection, errors, no results. - - `MudTable` — columns show Type, Entity ID, Sub-type, Blocks, PCU, Size (m), Owner, Position, Name, and a rightmost unlabeled Delete action column; sortable by Name, Blocks, PCU, Size; pager (25/50/100/250 options); fixed header at 60 vh. -- **Key state:** `_selectedAgentId`, `_typeFilter`, `_searchText`, `_entities`, `_lastResult`, `_lastUpdated`, `_loading`, `_error`. + - `MudTable` — columns show Type, Entity ID, Sub-type, Blocks, PCU, Size (m), Owner, Position, Name, and a rightmost unlabeled Delete action column; sortable by Name, Blocks, PCU, Size; row click selection via `OnRowClick`; selected rows get the shared `quasar-row-selected` hover-palette highlight; pager (25/50/100/250 options); fixed header at 60 vh. +- **Key state:** `_selectedAgentId`, `_typeFilter`, `_searchText`, `_entities`, `_selectedEntityIds`, `_entityTable`, `_lastResult`, `_lastUpdated`, `_selectionAnchorEntityId`, `_loading`, `_deletingSelected`, `_error`. - **Key methods:** - `LoadAsync()` — calls `EntityService.GetEntitiesAsync(agent, filter)` with `Limit=500`; client-side `FilteredEntities` then applies the search text. - `DeleteEntityAsync(EntitySummary)` — shows `ShowMessageBoxAsync` confirmation, then calls `EntityService.DeleteEntityAsync`; reloads on success. + - `DeleteSelectedEntitiesAsync()` — confirms a selected batch, deletes each entity through `EntityService.DeleteEntityAsync`, reports success/failure counts, clears selection on successful deletes, then reloads. + - `HandleRowClick(TableRowClickEventArgs)` — applies desktop-style selection semantics: plain click selects one row, Ctrl/Meta toggles a row, Shift selects a visible filtered range from `_selectionAnchorEntityId`. + - `SelectFilteredEntities()` / `ClearSelection()` — toolbar actions for bulk-selecting all currently filtered rows or clearing the current selection. + - `GetSelectionOrder()` — prefers the MudTable `FilteredItems` order for Shift ranges, so range selection tracks the table's current sorted/filtered view when available. + - `SelectRange(long, long, bool)`, `ToggleSelection(long)`, `PruneSelectionToLoadedEntities()`, `PruneSelectionToFilteredEntities()` — helpers that keep selection keyed by `EntityId` and remove hidden/stale selected IDs when the loaded result set or search text changes. + - `OnSearchChanged(string)` — updates the client-side search text and prunes selection to the visible filtered list. + - `GetEntityRowClass(EntitySummary, int)` — marks all rows selectable and selected rows with `quasar-row-selected`. - `MatchesSearch(EntitySummary)` — matches against `DisplayName`, `SubType`, `OwnerName`, `EntityId`, `OwnerSteamId`. - `HandleChanged()` — re-renders on `AgentRegistry.Changed` / `DedicatedServerCatalog.Changed`; also re-selects a default agent if the current selection was disconnected. - `ResolveServerName(AgentRuntimeState agent)` — prefers the server's configured `DedicatedServerDefinition.DisplayName` (looked up by `agent.UniqueNameKey`) over the agent's in-game `ServerDisplayName` (which is `ConfigDedicated.ServerName`, often blank and falling back to "Space Engineers {pid}"). @@ -36,3 +43,4 @@ Routable page (`/entities`) providing a live entity browser for connected Space ## Notes - Entity data is fetched on-demand only (user presses Refresh or the page first renders with a connected agent). There is no auto-refresh to avoid excessive agent load. - Search filtering is entirely client-side against the fetched batch; the `TypeTag` filter and `Limit=500` are sent to the agent. +- Selection is intentionally scoped to the current loaded batch. It is cleared on server/type changes and refreshes, and pruned when the search text hides selected rows, so bulk delete does not target invisible stale rows. diff --git a/Docs/Reference/files/Quasar/Services/QuasarConfigProfileCatalog.cs.md b/Docs/Reference/files/Quasar/Services/QuasarConfigProfileCatalog.cs.md index da91fad..d8de98c 100644 --- a/Docs/Reference/files/Quasar/Services/QuasarConfigProfileCatalog.cs.md +++ b/Docs/Reference/files/Quasar/Services/QuasarConfigProfileCatalog.cs.md @@ -3,7 +3,7 @@ **Module:** Quasar.Services.Core **Kind:** class **Tier:** 1 ## Summary -Manages the persistent catalog of named Quasar configuration profiles (reusable bundles of root/session settings, plugin selections, and mod lists). Profiles are stored as individual `profile.json` files under `/ConfigProfiles//`. The catalog watches the directory for external edits, debounces reload events, keeps versioned history on every save, and fires a `Changed` event when the in-memory state diverges from disk. +Manages the persistent catalog of named Quasar configuration profiles (reusable bundles of root/session settings, plugin selections, and mod lists). Profiles are stored as individual `profile.json` files under `/ConfigProfiles//`. The catalog watches the directory for external edits, debounces reload events, keeps versioned history on every save, can recreate/reset Quasar's built-in starter profiles, and fires a `Changed` event when the in-memory state diverges from disk. ## Structure **Namespace:** `Quasar.Services` @@ -18,11 +18,12 @@ Notable members: | `GetProfile(configProfileId)` | Returns a defensive clone by id (OrdinalIgnoreCase), or null. | | `UpsertAsync(profile, ct)` | Normalizes, updates in-memory list, saves atomically, appends timestamped history. | | `DeleteAsync(configProfileId, ct)` | Removes from memory, archives current file to history, deletes main file. | +| `RestoreDefaultProfilesAsync(ct)` | Recreates or resets the built-in Survival and Creative default profiles without deleting custom profiles; saves each through normal profile history. | | `Dispose()` | Stops the file-system watcher and cancels debounce. | Private helpers: - `LoadProfiles()` / `LoadProfile(path)` — deserializes from disk; returns default profiles if directory missing -- `CreateDefaultProfiles()` — seeds two starter profiles (Survival, Creative) on first run +- `CreateDefaultProfiles()` / `CreateDefaultProfile()` / `CreateDefaultPlugins()` — builds the two starter profiles (Survival, Creative) and their default manual plugin selections - `SaveProfileAsync()` — atomic write + history append using `AtomicFileWriter` - `ArchiveAndDeleteCurrentProfileAsync()` — archive-then-delete on removal - `Normalize(profile)` — trims strings, deduplicates admins/reserved/banned/plugins/mods, ensures non-null sub-objects diff --git a/Docs/Reference/files/Quasar/wwwroot/app.css.md b/Docs/Reference/files/Quasar/wwwroot/app.css.md index 5b09978..9f1704b 100644 --- a/Docs/Reference/files/Quasar/wwwroot/app.css.md +++ b/Docs/Reference/files/Quasar/wwwroot/app.css.md @@ -30,13 +30,14 @@ Global stylesheet for the Quasar Blazor Server UI. Overrides MudBlazor's elevati **MudBlazor overrides:** - `.mud-expansion-panels > .mud-expand-panel` — flat bordered panels with `0.5rem` gap, hover/focus header highlight, expanded-header bottom border - `.mud-checkbox` — rounded hit area with hover/focus background highlight -- `.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover` — paints hovered table rows with Quasar's `--quasar-hover-list-background` / `--quasar-hover-list-text` variables (falling back to active theme secondary colours), forces row descendants and button roots to the hover text colour, switches standalone SVG icons and outlined borders to the same contrast colour, keeps success/warning/error buttons and chips on semantic mixed hover colours, lets button-owned icons inherit from MudBlazor's button root colour, and skips expanded server detail rows +- `.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected)` — paints hovered rows and rows explicitly marked selected with Quasar's `--quasar-hover-list-background` / `--quasar-hover-list-text` variables (falling back to active theme secondary colours), forces row descendants and button roots to the hover text colour, switches standalone SVG icons and outlined borders to the same contrast colour, keeps success/warning/error buttons and chips on semantic mixed hover colours, lets button-owned icons inherit from MudBlazor's button root colour, and skips expanded server detail rows - `.mud-main-content`, `.mud-drawer` / `.magnetar-drawer` — background and right-border styling **Utility / feature classes:** - `.mono` — JetBrains Mono / Cascadia Code monospace font; `.mud-typography-caption.mono` renders monospace ID captions at 50% opacity (`opacity: 0.5`) - `.quasar-visually-hidden` — accessible visually-hidden text helper used by unlabeled table action headers - `.quasar-actions-column` — shared MudTable action-column rule that shrinks the column to action width, right-aligns its contents, and lets data columns consume the remaining table width +- `.entity-row-selectable` / `.quasar-row-selected` — selectable table-row affordance and selected-row marker used by the entity browser; selected rows reuse the same contrast-safe colour shift as hovered MudTable rows - `.copyable-path`, `.copyable-path-inline`, `.copyable-path-text`, `.copyable-path-button` — shared layout and monospace wrapping for `CopyablePath` path labels and clipboard icon buttons - `.appearance-color-field` — colour-backed wrapper behind each Appearance page `MudColorPicker`, using sanitized background/foreground CSS custom properties over a checkerboard layer so opaque and alpha colours are previewable while picker text/icons switch to readable dark or light contrast - `.chat-list` / `.chat-row` — scrollable chat log column with row separators diff --git a/Quasar/Components/Pages/Configs.razor b/Quasar/Components/Pages/Configs.razor index d6fa1b3..5a1c5f5 100644 --- a/Quasar/Components/Pages/Configs.razor +++ b/Quasar/Components/Pages/Configs.razor @@ -37,6 +37,14 @@ Create Profile + + Restore Default Profiles + + @if (Templates.Count == 0) @@ -1063,6 +1071,31 @@ Snackbar.Add("Config profile cloned.", Severity.Success); } + private async Task RestoreDefaultProfilesAsync() + { + if (!await ConfirmPendingChangesBeforeSwitchAsync()) + return; + + var confirmed = await DialogService.ShowMessageBoxAsync( + "Restore default profiles?", + "Survival (default) and Creative (default) will be recreated or reset to Quasar defaults. Custom profiles will not be changed.", + yesText: "Restore Defaults", + cancelText: "Cancel"); + + if (confirmed != true) + return; + + var selectedProfileId = FirstExistingProfileId(_selectedProfileId) ?? "survival-default"; + _selectedProfileId = selectedProfileId; + await ConfigProfiles.RestoreDefaultProfilesAsync(); + + SelectProfile(FirstExistingProfileId(selectedProfileId) + ?? FirstExistingProfileId("survival-default") + ?? Templates.FirstOrDefault()?.ConfigProfileId + ?? string.Empty); + Snackbar.Add("Default config profiles restored.", Severity.Success); + } + private async Task ClearCurrentProfileModsAsync() { if (_editor is null) diff --git a/Quasar/Components/Pages/Entities.razor b/Quasar/Components/Pages/Entities.razor index f688427..8eccd41 100644 --- a/Quasar/Components/Pages/Entities.razor +++ b/Quasar/Components/Pages/Entities.razor @@ -18,7 +18,7 @@ ValueChanged="OnServerChangedAsync" Variant="Variant.Outlined" Dense="true" - Disabled="@(ConnectedAgents.Count == 0)" + Disabled="@(ConnectedAgents.Count == 0 || _deletingSelected)" Style="min-width: 16rem;"> @foreach (var agent in ConnectedAgents) { @@ -32,6 +32,7 @@ ValueChanged="OnTypeChangedAsync" Variant="Variant.Outlined" Dense="true" + Disabled="@_deletingSelected" Style="min-width: 12rem;"> @foreach (var option in TypeOptions) { @@ -39,20 +40,23 @@ } - + Disabled="@(_loading || _deletingSelected || GetSelectedAgent() is null)"> Refresh @@ -68,6 +72,34 @@ { Last updated: @_lastUpdated.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") } + @if (_lastResult is not null) + { + + Selected @_selectedEntityIds.Count + + + + Select shown + + + + Clear + + + Delete selected + + } @if (_loading) { @@ -104,7 +136,16 @@ else if (FilteredEntities.Count == 0) } else { - + Type Entity ID @@ -130,12 +171,15 @@ else @FormatPosition(context) @context.DisplayName - - - + + + + + @@ -159,9 +203,13 @@ else private string _typeFilter = "All"; private string _searchText = string.Empty; private IReadOnlyList _entities = Array.Empty(); + private readonly HashSet _selectedEntityIds = new(); + private MudTable? _entityTable; private EntityListResult? _lastResult; private DateTimeOffset? _lastUpdated; + private long? _selectionAnchorEntityId; private bool _loading; + private bool _deletingSelected; private string? _error; private List ConnectedAgents => Registry.GetAgents() @@ -203,15 +251,23 @@ else private async Task OnServerChangedAsync(string agentId) { _selectedAgentId = agentId; + ClearSelection(); await LoadAsync(); } private async Task OnTypeChangedAsync(string typeTag) { _typeFilter = typeTag; + ClearSelection(); await LoadAsync(); } + private void OnSearchChanged(string searchText) + { + _searchText = searchText ?? string.Empty; + PruneSelectionToFilteredEntities(); + } + private async Task LoadAsync() { var agent = GetSelectedAgent(); @@ -219,11 +275,13 @@ else { _entities = Array.Empty(); _lastResult = null; + ClearSelection(); return; } _loading = true; _error = null; + ClearSelection(); StateHasChanged(); try @@ -240,12 +298,14 @@ else _lastResult = result; _entities = result.Entities; _lastUpdated = result.CapturedAtUtc; + PruneSelectionToLoadedEntities(); } catch (Exception exception) { _error = exception.Message; _entities = Array.Empty(); _lastResult = null; + ClearSelection(); } finally { @@ -254,6 +314,89 @@ else } } + private bool CanDeleteSelected => GetSelectedAgent() is not null + && !_loading + && !_deletingSelected + && _selectedEntityIds.Count > 0; + + private async Task DeleteSelectedEntitiesAsync() + { + var agent = GetSelectedAgent(); + if (agent is null) + { + Snackbar.Add("The selected server is not connected.", Severity.Error); + return; + } + + var selectedEntities = _entities + .Where(entity => _selectedEntityIds.Contains(entity.EntityId)) + .ToList(); + + if (selectedEntities.Count == 0) + { + ClearSelection(); + return; + } + + var confirmed = await DialogService.ShowMessageBoxAsync( + "Delete selected entities", + $"Permanently delete {selectedEntities.Count} selected entities? This cannot be undone.", + yesText: "Delete selected", + cancelText: "Cancel"); + + if (confirmed != true) + return; + + _deletingSelected = true; + StateHasChanged(); + + var deletedCount = 0; + var failedMessages = new List(); + + try + { + foreach (var entity in selectedEntities) + { + try + { + var result = await EntityService.DeleteEntityAsync(agent, entity.EntityId); + if (result.Success) + { + deletedCount++; + } + else + { + failedMessages.Add(string.IsNullOrWhiteSpace(result.Message) + ? $"#{entity.EntityId}: Delete failed." + : $"#{entity.EntityId}: {result.Message}"); + } + } + catch (Exception exception) + { + failedMessages.Add($"#{entity.EntityId}: {exception.Message}"); + } + } + } + finally + { + _deletingSelected = false; + } + + if (deletedCount > 0) + { + Snackbar.Add($"Deleted {deletedCount} selected entities.", Severity.Success); + ClearSelection(); + await LoadAsync(); + } + + if (failedMessages.Count > 0) + { + Snackbar.Add($"Failed to delete {failedMessages.Count} selected entities. First error: {failedMessages[0]}", Severity.Error); + } + + StateHasChanged(); + } + private async Task DeleteEntityAsync(EntitySummary entity) { var agent = GetSelectedAgent(); @@ -278,6 +421,9 @@ else if (result.Success) { Snackbar.Add(string.IsNullOrWhiteSpace(result.Message) ? "Entity deleted." : result.Message, Severity.Success); + _selectedEntityIds.Remove(entity.EntityId); + if (_selectionAnchorEntityId == entity.EntityId) + _selectionAnchorEntityId = null; await LoadAsync(); } else @@ -291,6 +437,109 @@ else } } + private void HandleRowClick(TableRowClickEventArgs args) + { + var entity = args.Item; + if (entity is null) + return; + + var additive = args.MouseEventArgs.CtrlKey || args.MouseEventArgs.MetaKey; + var range = args.MouseEventArgs.ShiftKey; + + if (range && _selectionAnchorEntityId is { } anchorId) + { + SelectRange(anchorId, entity.EntityId, additive); + } + else if (additive) + { + ToggleSelection(entity.EntityId); + } + else + { + _selectedEntityIds.Clear(); + _selectedEntityIds.Add(entity.EntityId); + } + + _selectionAnchorEntityId = entity.EntityId; + } + + private void SelectRange(long anchorEntityId, long targetEntityId, bool additive) + { + var visibleEntities = GetSelectionOrder(); + var anchorIndex = visibleEntities.FindIndex(entity => entity.EntityId == anchorEntityId); + var targetIndex = visibleEntities.FindIndex(entity => entity.EntityId == targetEntityId); + + if (anchorIndex < 0 || targetIndex < 0) + { + if (!additive) + _selectedEntityIds.Clear(); + + _selectedEntityIds.Add(targetEntityId); + return; + } + + if (!additive) + _selectedEntityIds.Clear(); + + var start = Math.Min(anchorIndex, targetIndex); + var end = Math.Max(anchorIndex, targetIndex); + for (var i = start; i <= end; i++) + _selectedEntityIds.Add(visibleEntities[i].EntityId); + } + + private void ToggleSelection(long entityId) + { + if (!_selectedEntityIds.Add(entityId)) + _selectedEntityIds.Remove(entityId); + } + + private void SelectFilteredEntities() + { + var visibleEntities = FilteredEntities; + _selectedEntityIds.Clear(); + + foreach (var entity in visibleEntities) + _selectedEntityIds.Add(entity.EntityId); + + _selectionAnchorEntityId = visibleEntities.LastOrDefault()?.EntityId; + } + + private List GetSelectionOrder() + { + return (_entityTable?.FilteredItems ?? FilteredEntities).ToList(); + } + + private void ClearSelection() + { + _selectedEntityIds.Clear(); + _selectionAnchorEntityId = null; + } + + private void PruneSelectionToLoadedEntities() + { + var loadedIds = _entities.Select(entity => entity.EntityId).ToHashSet(); + _selectedEntityIds.RemoveWhere(entityId => !loadedIds.Contains(entityId)); + + if (_selectionAnchorEntityId is { } anchorId && !loadedIds.Contains(anchorId)) + _selectionAnchorEntityId = null; + } + + private void PruneSelectionToFilteredEntities() + { + var visibleIds = FilteredEntities.Select(entity => entity.EntityId).ToHashSet(); + _selectedEntityIds.RemoveWhere(entityId => !visibleIds.Contains(entityId)); + + if (_selectionAnchorEntityId is { } anchorId && !visibleIds.Contains(anchorId)) + _selectionAnchorEntityId = null; + } + + private string GetEntityRowClass(EntitySummary entity, int rowNumber) + { + return _selectedEntityIds.Contains(entity.EntityId) + ? "entity-row-selectable quasar-row-selected" + : "entity-row-selectable"; + } + private AgentRuntimeState? GetSelectedAgent() { if (string.IsNullOrEmpty(_selectedAgentId)) diff --git a/Quasar/Services/QuasarConfigProfileCatalog.cs b/Quasar/Services/QuasarConfigProfileCatalog.cs index 99f5328..dbdea18 100644 --- a/Quasar/Services/QuasarConfigProfileCatalog.cs +++ b/Quasar/Services/QuasarConfigProfileCatalog.cs @@ -115,6 +115,33 @@ public async Task DeleteAsync(string configProfileId, CancellationToken cancella Changed?.Invoke(); } + public async Task RestoreDefaultProfilesAsync(CancellationToken cancellationToken = default) + { + var defaults = CreateDefaultProfiles(saveToDisk: false); + + foreach (var profile in defaults) + await SaveProfileAsync(profile, cancellationToken); + + lock (_sync) + { + foreach (var profile in defaults) + { + var normalized = Normalize(Clone(profile)); + var index = _profiles.FindIndex(existing => + string.Equals(existing.ConfigProfileId, normalized.ConfigProfileId, StringComparison.OrdinalIgnoreCase)); + + if (index >= 0) + _profiles[index] = Clone(normalized); + else + _profiles.Add(Clone(normalized)); + } + + _snapshot = CreateSnapshot(_profiles); + } + + Changed?.Invoke(); + } + private List LoadProfiles() { try diff --git a/Quasar/wwwroot/app.css b/Quasar/wwwroot/app.css index 2cbe2ab..c0185c1 100644 --- a/Quasar/wwwroot/app.css +++ b/Quasar/wwwroot/app.css @@ -154,9 +154,9 @@ h1:focus { background: var(--mud-palette-action-default-hover); } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover > .mud-table-cell, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover > td { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected), +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) > .mud-table-cell, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) > td { --quasar-table-hover-background: var(--quasar-hover-list-background, var(--mud-palette-secondary)); --quasar-table-hover-text: var(--quasar-hover-list-text, var(--mud-palette-secondary-text)); --quasar-table-hover-success: color-mix(in srgb, var(--mud-palette-success) 72%, var(--quasar-table-hover-text)); @@ -166,102 +166,106 @@ h1:focus { color: var(--quasar-table-hover-text) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover *:not(.mud-button-root *) { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) *:not(.mud-button-root *) { color: var(--quasar-table-hover-text) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-outlined, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-outlined { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-outlined, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-outlined { border-color: var(--quasar-table-hover-text) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-button-text-success, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-button-outlined-success, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-success-text { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-button-text-success, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-button-outlined-success, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-success-text { color: var(--quasar-table-hover-success) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-button-filled-success { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-button-filled-success { color: var(--mud-palette-success-text) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-button-text-warning, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-button-outlined-warning, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-warning-text { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-button-text-warning, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-button-outlined-warning, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-warning-text { color: var(--quasar-table-hover-warning) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-button-filled-warning { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-button-filled-warning { color: var(--mud-palette-warning-text) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-button-text-error, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-button-outlined-error, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-error-text { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-button-text-error, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-button-outlined-error, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-error-text { color: var(--quasar-table-hover-error) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-button-filled-error { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-button-filled-error { color: var(--mud-palette-error-text) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-button-outlined-success { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-button-outlined-success { border-color: var(--quasar-table-hover-success) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-button-outlined-warning { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-button-outlined-warning { border-color: var(--quasar-table-hover-warning) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-button-root.mud-button-outlined-error { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-button-root.mud-button-outlined-error { border-color: var(--quasar-table-hover-error) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-outlined.mud-chip-color-success, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-text.mud-chip-color-success, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-outlined.mud-chip-color-success *, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-text.mud-chip-color-success * { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-outlined.mud-chip-color-success, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-text.mud-chip-color-success, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-outlined.mud-chip-color-success *, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-text.mud-chip-color-success * { color: var(--quasar-table-hover-success) !important; border-color: var(--quasar-table-hover-success) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-outlined.mud-chip-color-warning, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-text.mud-chip-color-warning, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-outlined.mud-chip-color-warning *, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-text.mud-chip-color-warning * { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-outlined.mud-chip-color-warning, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-text.mud-chip-color-warning, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-outlined.mud-chip-color-warning *, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-text.mud-chip-color-warning * { color: var(--quasar-table-hover-warning) !important; border-color: var(--quasar-table-hover-warning) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-outlined.mud-chip-color-error, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-text.mud-chip-color-error, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-outlined.mud-chip-color-error *, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-text.mud-chip-color-error * { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-outlined.mud-chip-color-error, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-text.mud-chip-color-error, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-outlined.mud-chip-color-error *, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-text.mud-chip-color-error * { color: var(--quasar-table-hover-error) !important; border-color: var(--quasar-table-hover-error) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-filled.mud-chip-color-success, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-filled.mud-chip-color-success * { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-filled.mud-chip-color-success, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-filled.mud-chip-color-success * { color: var(--mud-palette-success-text) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-filled.mud-chip-color-warning, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-filled.mud-chip-color-warning * { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-filled.mud-chip-color-warning, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-filled.mud-chip-color-warning * { color: var(--mud-palette-warning-text) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-filled.mud-chip-color-error, -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover .mud-chip-filled.mud-chip-color-error * { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-filled.mud-chip-color-error, +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) .mud-chip-filled.mud-chip-color-error * { color: var(--mud-palette-error-text) !important; } -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover svg:not(.mud-button-root svg), -.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):hover svg:not(.mud-button-root svg) path { +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) svg:not(.mud-button-root svg), +.mud-table-hover .mud-table-body .mud-table-row:not(.servers-list-detail-row):is(:hover, .quasar-row-selected) svg:not(.mud-button-root svg) path { fill: currentColor !important; stroke: currentColor !important; } +.entity-row-selectable { + cursor: pointer; +} + .world-template-browse-button { margin-top: 1rem; }