Meta sync: pull the club's metas and navs into the shared profiles
The original kept VTank's profile folder in step with the club's metas repository; the port does the same against host.VtankProfiles, dropping .met files under mosstank/metas and .nav files under mosstank/navs where MossTank reads dropped profiles, flattened to their bare names as the original flattened them. The Gitea tree and raw endpoints are the original's; the repository address is the metas_repository_url setting, defaulting to the original repository. An overwritten file keeps its old copy as .bak (MossTank's pickers skip .bak side files). Storage is text-only, so the SHA-256 compare hashes the text a stored read gives back (UTF-8, BOM dropped); for the plain-text files in the repo that equals the original's byte compare. Network work runs on the thread pool, one operation after another; chat lines and window updates are applied on the tick. The four /mm verbs keep their chat lines, and the Metas tab is its own markup window with IsVisible for the main window to open. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
714fbfa2e8
commit
162d242c06
8 changed files with 1380 additions and 1 deletions
172
src/OpenAC.MosswartMassacre/Metas/MetasViewModel.cs
Normal file
172
src/OpenAC.MosswartMassacre/Metas/MetasViewModel.cs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
using System.Globalization;
|
||||
|
||||
namespace OpenAC.MosswartMassacre.Metas;
|
||||
|
||||
/// <summary>
|
||||
/// The binding object of the Metas window (<c>mosswartmassacre-metas.xml</c>),
|
||||
/// the original's Metas tab: the repository's files with their status, a
|
||||
/// live search, a pick list for one file, and the refresh, check and
|
||||
/// download buttons. Assigned on the tick thread only.
|
||||
/// </summary>
|
||||
internal sealed class MetasViewModel
|
||||
{
|
||||
private readonly Dictionary<string, string> _pathByLabel = new(StringComparer.Ordinal);
|
||||
|
||||
public MetasViewModel()
|
||||
{
|
||||
IgnoreRow = _ => { };
|
||||
SearchChanged = text =>
|
||||
{
|
||||
SearchText = text ?? string.Empty;
|
||||
SearchRequested?.Invoke();
|
||||
};
|
||||
ClearSearch = () =>
|
||||
{
|
||||
SearchText = string.Empty;
|
||||
SearchRequested?.Invoke();
|
||||
};
|
||||
SelectionChanged = label => SelectedLabel = label ?? string.Empty;
|
||||
RefreshList = () => ButtonPressed?.Invoke(nameof(RefreshList));
|
||||
CheckUpdates = () => ButtonPressed?.Invoke(nameof(CheckUpdates));
|
||||
DownloadOutdated = () => ButtonPressed?.Invoke(nameof(DownloadOutdated));
|
||||
DownloadSelected = () => ButtonPressed?.Invoke(nameof(DownloadSelected));
|
||||
}
|
||||
|
||||
/// <summary>A button was pressed; the argument is its binding name.</summary>
|
||||
internal event Action<string>? ButtonPressed;
|
||||
|
||||
/// <summary>The search text changed or was cleared.</summary>
|
||||
internal event Action? SearchRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the window is open. The root panel's visibility is bound to it,
|
||||
/// so the main window opens this one by setting it; it starts closed.
|
||||
/// </summary>
|
||||
public bool IsVisible { get; set; }
|
||||
|
||||
public string PathText { get; set; } = "VTank Path: unresolved";
|
||||
|
||||
public string LastCheckText { get; set; } = "Last Check: never";
|
||||
|
||||
public string StatusText { get; set; } = "Status: idle";
|
||||
|
||||
public string SearchText { get; set; } = string.Empty;
|
||||
|
||||
public Action<string> SearchChanged { get; }
|
||||
|
||||
public Action ClearSearch { get; }
|
||||
|
||||
public Action RefreshList { get; }
|
||||
|
||||
public Action CheckUpdates { get; }
|
||||
|
||||
public Action DownloadOutdated { get; }
|
||||
|
||||
public Action DownloadSelected { get; }
|
||||
|
||||
public int NoRow => -1;
|
||||
|
||||
public Action<int> IgnoreRow { get; }
|
||||
|
||||
public IReadOnlyList<string> FileTypes { get; private set; } = [];
|
||||
|
||||
public IReadOnlyList<string> FilePaths { get; private set; } = [];
|
||||
|
||||
public IReadOnlyList<string> FileStatus { get; private set; } = [];
|
||||
|
||||
public IReadOnlyList<uint> FileStatusColors { get; private set; } = [];
|
||||
|
||||
public IReadOnlyList<string> SelectionItems { get; private set; } = [];
|
||||
|
||||
public string SelectedLabel { get; set; } = string.Empty;
|
||||
|
||||
public Action<string> SelectionChanged { get; }
|
||||
|
||||
/// <summary>The repository path of the file picked in the list, or empty.</summary>
|
||||
public string SelectedRelativePath =>
|
||||
_pathByLabel.TryGetValue(SelectedLabel, out string? path) ? path : string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Redraws the list and the pick list from the manager's files, filtered
|
||||
/// by the search text on path or name. The list puts the files that need
|
||||
/// attention first: missing, then outdated, then failed, then current.
|
||||
/// </summary>
|
||||
public void ShowFiles(IReadOnlyList<MetaFileEntry> all)
|
||||
{
|
||||
IReadOnlyList<MetaFileEntry> files = ApplySearch(all);
|
||||
|
||||
List<MetaFileEntry> ordered = [.. files
|
||||
.OrderBy(static file => file.Status switch
|
||||
{
|
||||
MetaFileSyncStatus.NotDownloaded => 0,
|
||||
MetaFileSyncStatus.UpdateAvailable => 1,
|
||||
MetaFileSyncStatus.Error => 2,
|
||||
_ => 3,
|
||||
})
|
||||
.ThenBy(static file => file.RelativePath, StringComparer.OrdinalIgnoreCase)];
|
||||
|
||||
if (ordered.Count == 0)
|
||||
{
|
||||
bool searching = !string.IsNullOrWhiteSpace(SearchText);
|
||||
FileTypes = ["-"];
|
||||
FilePaths = [searching ? "No matches" : "No files loaded"];
|
||||
FileStatus = [searching ? "Try a different filter" : "Click Refresh List"];
|
||||
FileStatusColors = [0xFFFFFFu];
|
||||
}
|
||||
else
|
||||
{
|
||||
FileTypes = [.. ordered.Select(static file => file.Extension.ToUpperInvariant())];
|
||||
FilePaths = [.. ordered.Select(static file => file.RelativePath)];
|
||||
FileStatus = [.. ordered.Select(static file => file.StatusText)];
|
||||
FileStatusColors = [.. ordered.Select(static file => StatusColor(file.Status))];
|
||||
}
|
||||
|
||||
string previous = SelectedRelativePath;
|
||||
_pathByLabel.Clear();
|
||||
var labels = new List<string>();
|
||||
foreach (MetaFileEntry file in files.OrderBy(static file => file.RelativePath, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
string label = $"[{file.Extension}] {file.RelativePath}";
|
||||
labels.Add(label);
|
||||
_pathByLabel[label] = file.RelativePath;
|
||||
}
|
||||
SelectionItems = labels;
|
||||
SelectedLabel = labels.FirstOrDefault(label =>
|
||||
previous.Length > 0 && _pathByLabel[label].Equals(previous, StringComparison.OrdinalIgnoreCase))
|
||||
?? labels.FirstOrDefault()
|
||||
?? string.Empty;
|
||||
}
|
||||
|
||||
public void ShowLastCheck(DateTime lastStatusCheckUtc, DateTime lastCatalogRefreshUtc)
|
||||
{
|
||||
DateTime stamp = lastStatusCheckUtc != DateTime.MinValue ? lastStatusCheckUtc : lastCatalogRefreshUtc;
|
||||
LastCheckText = stamp == DateTime.MinValue
|
||||
? "Last Check: never"
|
||||
: string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"Last Check: {DateTime.SpecifyKind(stamp, DateTimeKind.Utc).ToLocalTime():yyyy-MM-dd HH:mm:ss}");
|
||||
}
|
||||
|
||||
public void ShowPath(string? storageRoot) =>
|
||||
PathText = storageRoot is null ? "VTank Path: unresolved" : $"VTank Path: {storageRoot}";
|
||||
|
||||
/// <summary>The status text colour: green current, orange outdated, blue missing, red failed.</summary>
|
||||
public static uint StatusColor(MetaFileSyncStatus status) => status switch
|
||||
{
|
||||
MetaFileSyncStatus.UpToDate => 0x008000u,
|
||||
MetaFileSyncStatus.UpdateAvailable => 0xB46E00u,
|
||||
MetaFileSyncStatus.NotDownloaded => 0x0046A0u,
|
||||
MetaFileSyncStatus.Error => 0xB40000u,
|
||||
_ => 0xFFFFFFu,
|
||||
};
|
||||
|
||||
private IReadOnlyList<MetaFileEntry> ApplySearch(IReadOnlyList<MetaFileEntry> files)
|
||||
{
|
||||
string filter = SearchText.Trim();
|
||||
if (filter.Length == 0)
|
||||
return files;
|
||||
return [.. files.Where(file =>
|
||||
file.RelativePath.Contains(filter, StringComparison.OrdinalIgnoreCase)
|
||||
|| file.FileName.Contains(filter, StringComparison.OrdinalIgnoreCase))];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue