104 lines
4 KiB
C#
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;
|
|
}
|
|
}
|