diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml index cfe66b7..b00a708 100644 --- a/src/TeamsISO.App/MainWindow.xaml +++ b/src/TeamsISO.App/MainWindow.xaml @@ -776,6 +776,26 @@ VerticalAlignment="Center"/> + + + + + + Join a Teams meeting from a pasted URL — see . public RelayCommand JoinMeetingCommand { get; } + /// Save a PNG snapshot of every enabled participant's current frame. + public RelayCommand SnapshotAllCommand { get; } + /// /// Two-way bound to the quick-join input. Whatever the operator pastes /// gets handed to when the @@ -407,6 +410,8 @@ public sealed class MainViewModel : ObservableObject, IDisposable notes.Show(); // non-modal so operators can stamp + read alongside the show }); + SnapshotAllCommand = new RelayCommand(SnapshotAll, () => Participants.Any(p => p.IsEnabled)); + JoinMeetingCommand = new RelayCommand(() => { // Trim + handle the operator pasting whitespace around the URL. @@ -587,6 +592,72 @@ public sealed class MainViewModel : ObservableObject, IDisposable Toast.Show($"Stopped {enabled.Length} ISO(s)"); } + /// + /// Save a PNG of every currently-enabled participant's latest processed + /// frame to a timestamped subdirectory under + /// %USERPROFILE%\Pictures\TeamsISO\. One folder per Snapshot All click + /// so back-to-back clicks don't comingle. Useful for end-of-meeting + /// archives, recapping who showed up, etc. + /// + private void SnapshotAll() + { + var enabled = Participants.Where(p => p.IsEnabled).ToArray(); + if (enabled.Length == 0) + { + Toast.Warn("No enabled participants to snapshot"); + return; + } + + var rootDir = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), + "TeamsISO", + $"snapshots-{DateTimeOffset.Now:yyyyMMdd_HHmmss}"); + + try + { + System.IO.Directory.CreateDirectory(rootDir); + } + catch (Exception ex) + { + Toast.Warn($"Couldn't create snapshot dir: {ex.Message}"); + return; + } + + var saved = 0; + var failed = 0; + foreach (var p in enabled) + { + try + { + var frame = _controller.GetLatestProcessedFrame(p.Id); + if (frame is null || frame.Pixels.IsEmpty) { failed++; continue; } + + var safeName = string.Join("_", p.DisplayName.Split(System.IO.Path.GetInvalidFileNameChars())); + if (string.IsNullOrWhiteSpace(safeName)) safeName = "participant"; + var path = System.IO.Path.Combine(rootDir, $"{safeName}.png"); + + var stride = frame.Width * 4; + var bmp = new System.Windows.Media.Imaging.WriteableBitmap( + frame.Width, frame.Height, 96, 96, + System.Windows.Media.PixelFormats.Bgra32, null); + bmp.WritePixels( + new System.Windows.Int32Rect(0, 0, frame.Width, frame.Height), + frame.Pixels.ToArray(), stride, 0); + + using var fs = new System.IO.FileStream(path, System.IO.FileMode.Create); + var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder(); + encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(bmp)); + encoder.Save(fs); + saved++; + } + catch { failed++; } + } + + Toast.Show(failed > 0 + ? $"Saved {saved} snapshot(s) ({failed} failed) to Pictures\\TeamsISO\\{System.IO.Path.GetFileName(rootDir)}" + : $"Saved {saved} snapshot(s) to Pictures\\TeamsISO\\{System.IO.Path.GetFileName(rootDir)}"); + } + /// /// Format a byte count as a human-readable string with 0-1 decimal /// places — e.g. "8.4 GB", "245 GB", "1.2 TB". Optimized for footer