dragon-iso/src/TeamsISO.App/Services/PresetApplier.cs

104 lines
4 KiB
C#

using System.Windows.Threading;
using TeamsISO.App.ViewModels;
using TeamsISO.Engine.Controller;
namespace TeamsISO.App.Services;
/// <summary>
/// Shared preset-application logic. Originally lived inline in
/// <c>PresetsDialog.OnApply</c>; lifted out so the REST control surface
/// (<see cref="ControlSurfaceServer"/>) and the auto-apply-on-launch path
/// (<see cref="MainViewModel.TryAutoApplyPendingPreset"/>) can call the same
/// implementation. Single source of truth for "what does Apply mean."
///
/// Application proceeds participant-by-participant, matching by display name
/// (the only stable join key across meetings since Ids regen each session).
/// For each match, the custom output name is updated and IsEnabled is
/// reconciled with the preset's value via <see cref="IIsoController.EnableIsoAsync"/>
/// / <see cref="IIsoController.DisableIsoAsync"/>. Per-participant failures are
/// caught and counted; one bad row never aborts applying the rest.
/// </summary>
public static class PresetApplier
{
/// <summary>Result counts from an apply pass.</summary>
public sealed record ApplyResult(int Matched, int Changed, int Skipped);
/// <summary>
/// Apply <paramref name="preset"/> to the live <paramref name="participants"/>
/// list. <paramref name="dispatcher"/>, when supplied, is used to marshal
/// IsEnabled / CustomName property writes onto the UI thread; pass null in
/// contexts that already run on the UI thread (e.g. the dialog's button click).
/// </summary>
public static async Task<ApplyResult> ApplyAsync(
Services.OperatorPresetStore.Preset preset,
IReadOnlyList<ParticipantViewModel> participants,
IIsoController controller,
Dispatcher? dispatcher = null,
CancellationToken cancellationToken = default)
{
// Build the lookup once, case-insensitive — Teams display names are
// human-typed, so "Jane" and "jane" should match the same row.
var byName = preset.Assignments.ToDictionary(
a => a.DisplayName,
StringComparer.OrdinalIgnoreCase);
var matched = 0;
var changed = 0;
foreach (var p in participants)
{
cancellationToken.ThrowIfCancellationRequested();
if (!byName.TryGetValue(p.DisplayName, out var assignment)) continue;
matched++;
await SetOnUiAsync(dispatcher, () => p.CustomName = assignment.CustomOutputName ?? string.Empty);
if (assignment.Enabled && !p.IsEnabled)
{
try
{
await controller.EnableIsoAsync(
p.Id,
string.IsNullOrWhiteSpace(p.CustomName) ? null : p.CustomName,
cancellationToken);
await SetOnUiAsync(dispatcher, () => p.IsEnabled = true);
changed++;
}
catch
{
// Per-participant best-effort: the rest still get applied.
}
}
else if (!assignment.Enabled && p.IsEnabled)
{
try
{
await controller.DisableIsoAsync(p.Id, cancellationToken);
await SetOnUiAsync(dispatcher, () => p.IsEnabled = false);
changed++;
}
catch
{
/* defensive */
}
}
}
// Mark applied so auto-apply-on-launch picks the right preset next time.
try { Services.OperatorPresetStore.MarkApplied(preset.Name); }
catch { /* preference write is best-effort */ }
var skipped = preset.Assignments.Count - matched;
return new ApplyResult(matched, changed, skipped);
}
private static Task SetOnUiAsync(Dispatcher? dispatcher, Action action)
{
if (dispatcher is null || dispatcher.CheckAccess())
{
action();
return Task.CompletedTask;
}
return dispatcher.InvokeAsync(action).Task;
}
}