131 lines
5.3 KiB
C#
131 lines
5.3 KiB
C#
|
|
using System.Windows.Threading;
|
|||
|
|
using TeamsISO.App.Services;
|
|||
|
|
|
|||
|
|
namespace TeamsISO.App.ViewModels;
|
|||
|
|
|
|||
|
|
// Teams launch / in-call / join-by-URL command helpers — split out of
|
|||
|
|
// MainViewModel.cs so the body methods don't live alongside the
|
|||
|
|
// constructor wiring + reactive subscriptions. The four command
|
|||
|
|
// PROPERTIES are declared back in MainViewModel.cs (public API surface);
|
|||
|
|
// this file holds the helpers they invoke.
|
|||
|
|
public sealed partial class MainViewModel
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// Wraps a <see cref="TeamsControlBridge"/> invocation in a RelayCommand
|
|||
|
|
/// that translates the result to a user-visible toast. Centralizes the
|
|||
|
|
/// toast wording so the four control commands stay consistent.
|
|||
|
|
/// </summary>
|
|||
|
|
private RelayCommand MakeTeamsCommand(string label, Func<TeamsControlBridge.InvokeResult> invoke, string successMessage)
|
|||
|
|
{
|
|||
|
|
return new RelayCommand(() =>
|
|||
|
|
{
|
|||
|
|
switch (invoke())
|
|||
|
|
{
|
|||
|
|
case TeamsControlBridge.InvokeResult.Invoked:
|
|||
|
|
Toast.Show(successMessage);
|
|||
|
|
break;
|
|||
|
|
case TeamsControlBridge.InvokeResult.TeamsNotRunning:
|
|||
|
|
Toast.Warn("Teams isn't running.");
|
|||
|
|
break;
|
|||
|
|
case TeamsControlBridge.InvokeResult.ControlNotFound:
|
|||
|
|
Toast.Warn($"{label} control not found — are you in a call?");
|
|||
|
|
break;
|
|||
|
|
case TeamsControlBridge.InvokeResult.InvokeFailed:
|
|||
|
|
Toast.Warn($"{label} button found but disabled.");
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Body of <c>JoinMeetingCommand</c>. Trims the pasted URL, hands it to
|
|||
|
|
/// <see cref="TeamsLauncher.TryJoinMeeting"/>, and runs the auto-hide
|
|||
|
|
/// follow-up if the operator has that preference set.
|
|||
|
|
/// </summary>
|
|||
|
|
private void JoinPastedMeeting()
|
|||
|
|
{
|
|||
|
|
var url = (_joinMeetingUrl ?? string.Empty).Trim();
|
|||
|
|
if (string.IsNullOrEmpty(url)) return;
|
|||
|
|
if (TeamsLauncher.TryJoinMeeting(url, out var error))
|
|||
|
|
{
|
|||
|
|
Toast.Show("Joining Teams meeting…");
|
|||
|
|
JoinMeetingUrl = string.Empty;
|
|||
|
|
if (Settings.AutoHideTeamsWindows)
|
|||
|
|
_ = TeamsLauncher.AutoHideAfterLaunchAsync();
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
Toast.Warn($"Could not join: {error}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Pull the meaningful "meeting title" out of Teams' raw window title.
|
|||
|
|
/// Teams uses formats like:
|
|||
|
|
/// "Weekly Standup | Microsoft Teams"
|
|||
|
|
/// "Meeting with Alice | Microsoft Teams"
|
|||
|
|
/// "Microsoft Teams" (no meeting, just the app)
|
|||
|
|
/// Strip the trailing " | Microsoft Teams" so the IN-CALL pill stays
|
|||
|
|
/// short and readable. Truncate beyond 50 chars so a long meeting
|
|||
|
|
/// subject doesn't push the rest of the IN-CALL bar off screen.
|
|||
|
|
/// </summary>
|
|||
|
|
internal static string ExtractMeetingTitle(string windowTitle)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrWhiteSpace(windowTitle)) return string.Empty;
|
|||
|
|
var t = windowTitle.Trim();
|
|||
|
|
foreach (var sep in new[] { " | Microsoft Teams", " | Teams", " - Microsoft Teams", " - Teams" })
|
|||
|
|
{
|
|||
|
|
var idx = t.IndexOf(sep, StringComparison.Ordinal);
|
|||
|
|
if (idx > 0) { t = t.Substring(0, idx).Trim(); break; }
|
|||
|
|
}
|
|||
|
|
if (t.Equals("Microsoft Teams", StringComparison.OrdinalIgnoreCase)) return string.Empty;
|
|||
|
|
if (t.Length > 50) t = t.Substring(0, 47) + "…";
|
|||
|
|
return t;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Meeting-state probe — runs on every 1Hz stats tick. We fire the UIA
|
|||
|
|
/// traversal on a worker thread because it can take 50–200ms in a busy
|
|||
|
|
/// call; the result is marshalled back to the dispatcher to update the
|
|||
|
|
/// view-model properties. One-tick latency on the displayed state is
|
|||
|
|
/// preferable to a UI hiccup.
|
|||
|
|
/// </summary>
|
|||
|
|
private void PollTeamsMeetingState()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var teamsRunning = TeamsLauncher.IsRunning();
|
|||
|
|
if (!teamsRunning)
|
|||
|
|
{
|
|||
|
|
TeamsMeetingState = string.Empty;
|
|||
|
|
IsTeamsInCall = false;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_ = Task.Run(() =>
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// Single UIA traversal returns all three signals (in-call /
|
|||
|
|
// muted / camera-off) so we don't pay for three walks of
|
|||
|
|
// the same descendant tree at 1Hz.
|
|||
|
|
var snap = TeamsControlBridge.DetectCallState();
|
|||
|
|
var inCall = snap.IsInCall;
|
|||
|
|
var title = inCall ? ExtractMeetingTitle(TeamsLauncher.GetActiveWindowTitle()) : null;
|
|||
|
|
_dispatcher.InvokeAsync(() =>
|
|||
|
|
{
|
|||
|
|
IsTeamsInCall = inCall;
|
|||
|
|
TeamsMeetingState = inCall
|
|||
|
|
? (string.IsNullOrEmpty(title) ? "IN CALL" : $"IN CALL · {title}")
|
|||
|
|
: "READY";
|
|||
|
|
IsLocalMuted = inCall && (snap.IsMuted ?? false);
|
|||
|
|
IsLocalCameraOff = inCall && (snap.IsCameraOff ?? false);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
catch { /* UIA flakiness shouldn't crash the stats tick */ }
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
catch { /* defensive — probe failures must never break the tick */ }
|
|||
|
|
}
|
|||
|
|
}
|