using System.Globalization;
namespace OpenAC.MosswartMassacre.Metas;
///
/// The binding object of the Metas window (mosswartmassacre-metas.xml),
/// 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.
///
internal sealed class MetasViewModel
{
private readonly Dictionary _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));
}
/// A button was pressed; the argument is its binding name.
internal event Action? ButtonPressed;
/// The search text changed or was cleared.
internal event Action? SearchRequested;
///
/// 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.
///
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 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 IgnoreRow { get; }
public IReadOnlyList FileTypes { get; private set; } = [];
public IReadOnlyList FilePaths { get; private set; } = [];
public IReadOnlyList FileStatus { get; private set; } = [];
public IReadOnlyList FileStatusColors { get; private set; } = [];
public IReadOnlyList SelectionItems { get; private set; } = [];
public string SelectedLabel { get; set; } = string.Empty;
public Action SelectionChanged { get; }
/// The repository path of the file picked in the list, or empty.
public string SelectedRelativePath =>
_pathByLabel.TryGetValue(SelectedLabel, out string? path) ? path : string.Empty;
///
/// 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.
///
public void ShowFiles(IReadOnlyList all)
{
IReadOnlyList files = ApplySearch(all);
List 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();
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}";
/// The status text colour: green current, orange outdated, blue missing, red failed.
public static uint StatusColor(MetaFileSyncStatus status) => status switch
{
MetaFileSyncStatus.UpToDate => 0x008000u,
MetaFileSyncStatus.UpdateAvailable => 0xB46E00u,
MetaFileSyncStatus.NotDownloaded => 0x0046A0u,
MetaFileSyncStatus.Error => 0xB40000u,
_ => 0xFFFFFFu,
};
private IReadOnlyList ApplySearch(IReadOnlyList 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))];
}
}