Table of Contents

Class ServerModsViewModel

Namespace
McServerLauncher.ViewModels
Assembly
McServerLauncher.dll

The "you can clean up" panel: libraries nothing uses any more, and mods switched off for a month.

public sealed class ServerModsViewModel : ObservableObject, INotifyPropertyChanged, INotifyPropertyChanging, IDisposable
Inheritance
ServerModsViewModel
Implements
Inherited Members

Remarks

Only suggested, never done: the panel lists, the user ticks, the user presses the button. What is listed is decided by McServerLauncher.Services.CleanupAdvisor; this half finds the facts it needs (the jars, what the store knows about them, the ledger) and puts its answer on screen.

Nothing here runs until the tab has been shown. The panel is also built for servers whose tab is never opened, and by tests that have no business writing to the user's application data or asking Modrinth anything; EnsureLoaded() is what arms it.

Constructors

ServerModsViewModel(ServerConfig)

public ServerModsViewModel(ServerConfig config)

Parameters

config ServerConfig

Properties

ActiveFilters

Everything narrowing the results right now, as one row of chips: the server's own loader and Minecraft version (always on, not removable) followed by the categories the user picked. Putting them together answers "why am I seeing these results?" in one glance.

public ObservableCollection<ActiveFilterViewModel> ActiveFilters { get; }

Property Value

ObservableCollection<ActiveFilterViewModel>

BrowseTags

The category chips, built from the tag catalogue and filtered to what makes sense for this kind of server. Tags that map to a Modrinth facet filter exactly; the rest fall back to a search term.

public IReadOnlyList<StoreTagViewModel> BrowseTags { get; }

Property Value

IReadOnlyList<StoreTagViewModel>

Remarks

Rebuilt, not fixed at construction: converting a server between the two families changes which catalogue it browses, and a Paper server offering "Adventure & RPG" because it used to be Fabric would search for something Modrinth has no plugins under.

BrowseTitle

public string BrowseTitle { get; }

Property Value

string

CanIncludeExcludedAnyway

True while there is something to put back, which is what the button hangs off.

public bool CanIncludeExcludedAnyway { get; }

Property Value

bool

CanLoadMore

[ExcludeFromCodeCoverage]
public bool CanLoadMore { get; set; }

Property Value

bool

CanRemoveSelectedCleanup

public bool CanRemoveSelectedCleanup { get; }

Property Value

bool

Remarks

Not while "update all" runs: it walks the installed rows and swaps their files, and removing rebuilds those rows under it.

CheckUpdatesCommand

[ExcludeFromCodeCoverage]
public IAsyncRelayCommand CheckUpdatesCommand { get; }

Property Value

IAsyncRelayCommand

CleanupSuggestions

What the panel lists right now.

public ObservableCollection<CleanupSuggestionViewModel> CleanupSuggestions { get; }

Property Value

ObservableCollection<CleanupSuggestionViewModel>

ClearFiltersCommand

Gets an IRelayCommand instance wrapping McServerLauncher.ViewModels.ServerModsViewModel.ClearFilters().

[ExcludeFromCodeCoverage]
public IRelayCommand ClearFiltersCommand { get; }

Property Value

IRelayCommand

ClientPackMods

The client-only mods added for this server's players: Sodium, MaLiLib, a minimap. The server never loads them (see ClientPack); the export puts them in the pack.

public ObservableCollection<ModItem> ClientPackMods { get; }

Property Value

ObservableCollection<ModItem>

ClientPackTitleText

"Players' pack (3)".

public string ClientPackTitleText { get; }

Property Value

string

ContentTabTitle

public string ContentTabTitle { get; }

Property Value

string

DeleteModCommand

Gets an IAsyncRelayCommand<T> instance wrapping DeleteMod(ModItem).

[ExcludeFromCodeCoverage]
public IAsyncRelayCommand<ModItem?> DeleteModCommand { get; }

Property Value

IAsyncRelayCommand<ModItem>

Details

The open details page, or null while the browser list is showing. The list is deliberately left untouched underneath, so coming back is instant and keeps the scroll position.

[ExcludeFromCodeCoverage]
public ModDetailsViewModel? Details { get; set; }

Property Value

ModDetailsViewModel

DismissCleanupCommand

Gets an IRelayCommand instance wrapping McServerLauncher.ViewModels.ServerModsViewModel.DismissCleanup().

[ExcludeFromCodeCoverage]
public IRelayCommand DismissCleanupCommand { get; }

Property Value

IRelayCommand

DismissExportNoticeCommand

Gets an IRelayCommand instance wrapping McServerLauncher.ViewModels.ServerModsViewModel.DismissExportNotice().

[ExcludeFromCodeCoverage]
public IRelayCommand DismissExportNoticeCommand { get; }

Property Value

IRelayCommand

DismissInstallNoticeCommand

Gets an IRelayCommand instance wrapping McServerLauncher.ViewModels.ServerModsViewModel.DismissInstallNotice().

[ExcludeFromCodeCoverage]
public IRelayCommand DismissInstallNoticeCommand { get; }

Property Value

IRelayCommand

ExportModpackCommand

Gets an IAsyncRelayCommand instance wrapping McServerLauncher.ViewModels.ServerModsViewModel.ExportModpack().

[ExcludeFromCodeCoverage]
public IAsyncRelayCommand ExportModpackCommand { get; }

Property Value

IAsyncRelayCommand

ExportNotice

What the last export did, shown under the installed list until dismissed.

[ExcludeFromCodeCoverage]
public string? ExportNotice { get; set; }

Property Value

string

Remarks

Deliberately after the fact and not a dialog beforehand. A dialog charges a click to the normal case — the one where the detection is right — in order to serve the rare one, and per-mod checkboxes are precision nobody needs when including one jar too many costs a few megabytes. Naming the excluded files is what makes this actionable instead of unsettling.

FacetTags

Categories Modrinth can filter on exactly. These stack: each adds its own facet group, and Modrinth ANDs the groups.

public IReadOnlyList<StoreTagViewModel> FacetTags { get; }

Property Value

IReadOnlyList<StoreTagViewModel>

FilterTip

public string FilterTip { get; }

Property Value

string

FilterTypeBrush

public IBrush FilterTypeBrush { get; }

Property Value

IBrush

FilterTypeText

public string FilterTypeText { get; }

Property Value

string

FilterVersionText

public string FilterVersionText { get; }

Property Value

string

HasCleanupSuggestions

public bool HasCleanupSuggestions { get; }

Property Value

bool

HasClientPack

public bool HasClientPack { get; }

Property Value

bool

HasExportNotice

public bool HasExportNotice { get; }

Property Value

bool

HasInstallNotice

public bool HasInstallNotice { get; }

Property Value

bool

HasMissingDependencies

public bool HasMissingDependencies { get; }

Property Value

bool

HasNoMatches

Something is installed, but the search leaves none of it.

public bool HasNoMatches { get; }

Property Value

bool

HasPendingUpdates

public bool HasPendingUpdates { get; }

Property Value

bool

HasSelectedTags

public bool HasSelectedTags { get; }

Property Value

bool

HowToPlaySteps

Instructions to play: install the loader client (mods) or just join (plugins).

public string HowToPlaySteps { get; }

Property Value

string

HowToPlayTitle

public string HowToPlayTitle { get; }

Property Value

string

IncludeExcludedAnywayCommand

Gets an IAsyncRelayCommand instance wrapping McServerLauncher.ViewModels.ServerModsViewModel.IncludeExcludedAnyway().

[ExcludeFromCodeCoverage]
public IAsyncRelayCommand IncludeExcludedAnywayCommand { get; }

Property Value

IAsyncRelayCommand

InstallFailed

True when the notice reports a failure, which draws it in red rather than green.

[ExcludeFromCodeCoverage]
public bool InstallFailed { get; set; }

Property Value

bool

InstallMissingDependenciesCommand

[ExcludeFromCodeCoverage]
public IAsyncRelayCommand InstallMissingDependenciesCommand { get; }

Property Value

IAsyncRelayCommand

InstallNotice

What the last install said, shown as a banner over the store and the details page.

[ExcludeFromCodeCoverage]
public string? InstallNotice { get; set; }

Property Value

string

InstalledCountText

"12", or "3 of 12" while searching.

public string InstalledCountText { get; }

Property Value

string

InstalledFilter

The words typed in the installed list's search box.

[ExcludeFromCodeCoverage]
public string InstalledFilter { get; set; }

Property Value

string

InstalledFilterPlaceholder

public string InstalledFilterPlaceholder { get; }

Property Value

string

InstalledMods

public ObservableCollection<ModItem> InstalledMods { get; }

Property Value

ObservableCollection<ModItem>

InstalledTitle

public string InstalledTitle { get; }

Property Value

string

IsBrowsing

public bool IsBrowsing { get; }

Property Value

bool

IsCheckingUpdates

[ExcludeFromCodeCoverage]
public bool IsCheckingUpdates { get; set; }

Property Value

bool

IsFiltersOpen

Whether the filters popup is open (its toggle button binds both ways).

[ExcludeFromCodeCoverage]
public bool IsFiltersOpen { get; set; }

Property Value

bool

IsHowToPlayOpen

Whether the "how to play" popup is open.

[ExcludeFromCodeCoverage]
public bool IsHowToPlayOpen { get; set; }

Property Value

bool

IsInstalledFiltered

public bool IsInstalledFiltered { get; }

Property Value

bool

IsInstalledPanelOpen

Whether the installed panel is showing. Collapsing it is the cheapest way to give the results room: it is 300px that mostly holds a short list, and on a 1150px window it is the difference between one column of results and two.

[ExcludeFromCodeCoverage]
public bool IsInstalledPanelOpen { get; set; }

Property Value

bool

IsInstallingDependencies

[ExcludeFromCodeCoverage]
public bool IsInstallingDependencies { get; set; }

Property Value

bool

IsPluginBased

public bool IsPluginBased { get; }

Property Value

bool

IsSearching

[ExcludeFromCodeCoverage]
public bool IsSearching { get; set; }

Property Value

bool

IsUpdatingAll

[ExcludeFromCodeCoverage]
public bool IsUpdatingAll { get; set; }

Property Value

bool

LoadMoreCommand

[ExcludeFromCodeCoverage]
public IAsyncRelayCommand LoadMoreCommand { get; }

Property Value

IAsyncRelayCommand

MissingDependencyText

[ExcludeFromCodeCoverage]
public string? MissingDependencyText { get; set; }

Property Value

string

NoInstalledText

public string NoInstalledText { get; }

Property Value

string

PendingUpdateCount

How many rows are offering an update right now.

public int PendingUpdateCount { get; }

Property Value

int

QueryTags

Categories Modrinth has no facet for, which can only contribute words to the query. Picking a second one would blur the search instead of narrowing it, so they behave as a radio group.

public IReadOnlyList<StoreTagViewModel> QueryTags { get; }

Property Value

IReadOnlyList<StoreTagViewModel>

RefreshInstalledModsCommand

Gets an IRelayCommand instance wrapping McServerLauncher.ViewModels.ServerModsViewModel.RefreshInstalledMods().

[ExcludeFromCodeCoverage]
public IRelayCommand RefreshInstalledModsCommand { get; }

Property Value

IRelayCommand

RemoveFromClientPackCommand

[ExcludeFromCodeCoverage]
public IRelayCommand<ModItem?> RemoveFromClientPackCommand { get; }

Property Value

IRelayCommand<ModItem>

RemoveSelectedCleanupCommand

Gets an IRelayCommand instance wrapping McServerLauncher.ViewModels.ServerModsViewModel.RemoveSelectedCleanup().

[ExcludeFromCodeCoverage]
public IRelayCommand RemoveSelectedCleanupCommand { get; }

Property Value

IRelayCommand

RemoveSelectedText

"Quitar 1 seleccionado" / "Quitar 3 seleccionados".

public string RemoveSelectedText { get; }

Property Value

string

RemoveTagCommand

[ExcludeFromCodeCoverage]
public IRelayCommand<StoreTagViewModel?> RemoveTagCommand { get; }

Property Value

IRelayCommand<StoreTagViewModel>

SearchCommand

Gets an IAsyncRelayCommand instance wrapping Search(CancellationToken).

[ExcludeFromCodeCoverage]
public IAsyncRelayCommand SearchCommand { get; }

Property Value

IAsyncRelayCommand

SearchPlaceholder

public string SearchPlaceholder { get; }

Property Value

string

SearchQuery

[ExcludeFromCodeCoverage]
public string SearchQuery { get; set; }

Property Value

string

SearchResults

The results, flat. Replaced in one go rather than emptied and refilled: a list that sits empty across a network round trip makes the whole panel blink out and back.

public BulkObservableCollection<ModrinthProjectViewModel> SearchResults { get; }

Property Value

BulkObservableCollection<ModrinthProjectViewModel>

SearchStatus

[ExcludeFromCodeCoverage]
public string SearchStatus { get; set; }

Property Value

string

SelectedCleanupCount

How many rows are ticked.

public int SelectedCleanupCount { get; }

Property Value

int

SelectedSideIndex

Index into SideOptions. Everything by default: nothing is hidden.

[ExcludeFromCodeCoverage]
public int SelectedSideIndex { get; set; }

Property Value

int

SelectedSortIndex

[ExcludeFromCodeCoverage]
public int SelectedSortIndex { get; set; }

Property Value

int

SelectedTags

The categories currently filtering the browse, in the order the user picked them. Shown as removable chips, so a filter can never be selected but scrolled out of sight.

public ObservableCollection<StoreTagViewModel> SelectedTags { get; }

Property Value

ObservableCollection<StoreTagViewModel>

ShowsSideFilter

Plugins have no client side, so the choice does not exist on a Paper server and the control is not drawn.

public bool ShowsSideFilter { get; }

Property Value

bool

SideOptions

The three ways to cut the browser by where a project runs: everything, what can run on the server, and the client-only mods that go in the players' pack.

public IReadOnlyList<string> SideOptions { get; }

Property Value

IReadOnlyList<string>

SortOptions

Sort options shown in the UI. Index 0 = relevance, 1 = downloads.

public IReadOnlyList<string> SortOptions { get; }

Property Value

IReadOnlyList<string>

ToggleModCommand

Gets an IRelayCommand<T> instance wrapping ToggleMod(ModItem).

[ExcludeFromCodeCoverage]
public IRelayCommand<ModItem?> ToggleModCommand { get; }

Property Value

IRelayCommand<ModItem>

ToggleTagCommand

[ExcludeFromCodeCoverage]
public IRelayCommand<StoreTagViewModel?> ToggleTagCommand { get; }

Property Value

IRelayCommand<StoreTagViewModel>

UpdateAllCommand

Gets an IAsyncRelayCommand instance wrapping McServerLauncher.ViewModels.ServerModsViewModel.UpdateAll().

[ExcludeFromCodeCoverage]
public IAsyncRelayCommand UpdateAllCommand { get; }

Property Value

IAsyncRelayCommand

UpdateAllText

The label of the button that updates them all, with how many there are.

public string UpdateAllText { get; }

Property Value

string

UpdateModCommand

Gets an IAsyncRelayCommand<T> instance wrapping UpdateMod(ModItem).

[ExcludeFromCodeCoverage]
public IAsyncRelayCommand<ModItem?> UpdateModCommand { get; }

Property Value

IAsyncRelayCommand<ModItem>

UpdateStatus

[ExcludeFromCodeCoverage]
public string UpdateStatus { get; set; }

Property Value

string

VisibleInstalledMods

What the installed list shows: InstalledMods through InstalledFilter.

public BulkObservableCollection<ModItem> VisibleInstalledMods { get; }

Property Value

BulkObservableCollection<ModItem>

Remarks

A copy rather than a filter on InstalledMods itself, because that one is also what counts pending updates and what "update all" walks. Filtering it in place would make a search box quietly hide updates from both.

Methods

Dispose()

Shuts down for good and lets go of the tokens, for a server that is being removed. Shutdown() is the one for an app that may carry on.

public void Dispose()

EnsureLoaded()

Loads the first page of mods the first time the Mods tab is shown.

public void EnsureLoaded()

GoBack()

Goes back to the previous details page, or to the list when there isn't one.

public void GoBack()

InstallMissingAsync(IEnumerable<string>, IProgress<string>?, CancellationToken)

Fetches the mods or plugins named by ids, for the check before a start.

public Task InstallMissingAsync(IEnumerable<string> ids, IProgress<string>? log = null, CancellationToken ct = default)

Parameters

ids IEnumerable<string>
log IProgress<string>
ct CancellationToken

Returns

Task

Remarks

The ids come from the jars, and a mod id is not a store id: cristallib is published on Modrinth as cristel-lib, and there is no rule that maps one to the other. So anything the store does not recognise is skipped rather than reported as an error — the caller checks again afterwards and says what is still missing, which is the honest answer.

Progress goes to the caller's log rather than to this tab's status line: the user pressed Start and is looking at the server's console, not at the Mods tab.

InstallModAsync(string, VersionResult?, bool)

Installs a project. chosen installs that exact version (the details page offers the last few); without it the newest compatible one is resolved, as before. This is the only install path in the app — the details page routes through it too.

public Task<bool> InstallModAsync(string projectId, VersionResult? chosen = null, bool toPlayerPack = false)

Parameters

projectId string

The project to install.

chosen VersionResult

One exact version, or null for the newest compatible one.

toPlayerPack bool

Put it in the players' pack instead of the server's folder: for the client-only mods, which do nothing on a server. Everything else about the install is the same.

Returns

Task<bool>

True when the mod ended up installed.

RefreshFromConfig()

Re-reads everything in this panel that comes from the server's type and Minecraft version.

public void RefreshFromConfig()

Remarks

The panel reads both straight off the ServerConfig it was handed, and that object is mutated in place when a server is converted or moved to another version. None of the properties derived from it announce anything on their own, so without this call the tab kept the old family's name, the old version's filter chip and the old folder's jars until the app was restarted — while every search it ran used the new values. The two disagreeing is what made an install fail later: the results on screen had been chosen for a version the server no longer runs.

Everything here is idempotent, so callers that are not sure whether anything moved can just call it.

ReloadInstalled()

Re-reads the content folder, for jars that arrived without the user putting them there.

public void ReloadInstalled()

Remarks

The panel takes its snapshot when it is built, which is the same moment the server is created — before crossplay has downloaded Geyser and Floodgate into it. Without this the two jars are on disk and loaded by the server while the Mods tab still shows nothing, and the only way to see them is the refresh button, which nobody thinks to press for mods they did not install.

ShowDetails(StoreItem)

Opens the details page for a store item, remembering the current one.

public void ShowDetails(StoreItem item)

Parameters

item StoreItem

Shutdown()

Drops the open details page, cancelling whatever it is still fetching. Called when the app closes so pending requests don't outlive the window they were going to fill.

public void Shutdown()