feat(webcam): app-default gate slice — per-scene offer, Add places default directly, identity survives removal
Chat's camera no longer greys Web Cam in Live (per-scene max, not app-wide); TASK 26 superseded by creator directive (app-level resource model). Add Webcam now places the existing app default without the picker; the picker runs only for the initial selection. Removing the last placement keeps the identity (_webcam never nulled) so Add stays offered. Startup pass adopts a solo camera as the app default, so a clean layout offers the layer in Live/Chat at once. Dynamic WebcamAddToolTip names the why (incl. the 'graduated to OBS' line). Good Dog: WebcamMenuGateTests rewritten (real app + temp DB + camera seams) — identity in Chat does not gray Live; Add in Live places same wc-1 no picker; scene-with-placement stays gray; identity survives both removals (DB row 1). WebcamStartupResourceTests single-lock fact asserts CanAddWebcam after adoption. Docs same commit (ai.md supersession, TASKS.md, HANDOFF.md). 304/304, 0 warnings.
This commit is contained in:
@@ -49,6 +49,7 @@ public partial class MainViewModel
|
||||
OnPropertyChanged(nameof(CanAddWebcam));
|
||||
OnPropertyChanged(nameof(CanAddYouTubeChat));
|
||||
OnPropertyChanged(nameof(CanShowWebcamInStagedScene));
|
||||
OnPropertyChanged(nameof(WebcamAddToolTip));
|
||||
OnPropertyChanged(nameof(IsCountdownAvailable));
|
||||
UpdateActiveBackground();
|
||||
UpdateBackgroundImage();
|
||||
|
||||
@@ -217,9 +217,9 @@ public partial class MainViewModel
|
||||
|
||||
// Removes an element from the active scene. For a webcam config this drops the
|
||||
// scene's usage and releases one camera reference (CameraManager stops the
|
||||
// session when the last using scene lets go). When the last config anywhere is
|
||||
// removed, the webcam identity is cleared too — re-adding opens the picker
|
||||
// again instead of silently resurrecting the old camera.
|
||||
// session when the last using scene lets go). The webcam identity survives —
|
||||
// it's the app-level default, so removing a scene's placement re-enables Add
|
||||
// for the same camera instead of dropping it.
|
||||
private void RemoveElement(SceneElement? element)
|
||||
{
|
||||
var scene = StagedScene;
|
||||
@@ -235,13 +235,11 @@ public partial class MainViewModel
|
||||
_webView2Manager?.Unregister(webSource.Id);
|
||||
_sceneGraph.RemoveElement(scene, element);
|
||||
|
||||
if (element is WebcamSceneConfig && _webcam != null
|
||||
&& !Scenes.Any(s => s.Elements.OfType<WebcamSceneConfig>().Any()))
|
||||
if (element is WebcamSceneConfig)
|
||||
{
|
||||
_webcam = null;
|
||||
OnPropertyChanged(nameof(CanChangeWebcam));
|
||||
OnPropertyChanged(nameof(CanAddWebcam));
|
||||
OnPropertyChanged(nameof(CanShowWebcamInStagedScene));
|
||||
OnPropertyChanged(nameof(WebcamAddToolTip));
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(ShowEmptySceneHint));
|
||||
|
||||
@@ -22,9 +22,30 @@ public partial class MainViewModel
|
||||
private string? _webcamLockAlert;
|
||||
private DispatcherTimer? _webcamLockPollTimer;
|
||||
|
||||
/// <summary>The Add → Webcam menu item: enabled only while no webcam exists anywhere —
|
||||
/// one camera identity app-wide; re-adding always opens the picker.</summary>
|
||||
public bool CanAddWebcam => StagedScene != null && _webcam == null;
|
||||
/// <summary>
|
||||
/// Add → Webcam menu item: the webcam is an app-level resource (one camera —
|
||||
/// the app default). The row is offered in any scene that doesn't already
|
||||
/// host it, wherever a camera is selected; it is NOT blocked by other scenes
|
||||
/// holding their own placement (per-scene max, not app-wide).
|
||||
/// </summary>
|
||||
public bool CanAddWebcam => StagedScene is { WebcamConfig: null } && _webcam != null;
|
||||
|
||||
/// <summary>
|
||||
/// Why the Webcam row is greyed, when it is — never a silent bool. The max-1
|
||||
/// reason gets the gentle "graduated to OBS" line; no camera at all gets its
|
||||
/// own wording. Shown as the row's tooltip.
|
||||
/// </summary>
|
||||
public string WebcamAddToolTip
|
||||
{
|
||||
get
|
||||
{
|
||||
if (StagedScene?.WebcamConfig != null)
|
||||
return "Already in this scene — one webcam per stream. A second camera means you've graduated to OBS.";
|
||||
if (_webcam == null)
|
||||
return "No webcam detected — plug one in, or allow camera access in Windows.";
|
||||
return "Adds the app default webcam to this scene.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Right-click-on-preview → "Show Webcam": offered when the active scene has no
|
||||
@@ -229,6 +250,15 @@ public partial class MainViewModel
|
||||
var started = await _cameraManager.AcquireAsync(device.Id);
|
||||
if (started)
|
||||
{
|
||||
// The single camera becomes the app default so the Web Cam layer is
|
||||
// offered in the capabled scenes (Live/Chat) even on a clean layout.
|
||||
if (_webcam == null)
|
||||
{
|
||||
_webcam = new Webcam { DeviceId = device.Id, Name = device.DisplayName };
|
||||
OnPropertyChanged(nameof(CanChangeWebcam));
|
||||
OnPropertyChanged(nameof(CanAddWebcam));
|
||||
OnPropertyChanged(nameof(WebcamAddToolTip));
|
||||
}
|
||||
WebcamLockAlert = null;
|
||||
return;
|
||||
}
|
||||
@@ -237,33 +267,28 @@ public partial class MainViewModel
|
||||
WebcamLockAlert = $"Web Cam unavailable: {reason}";
|
||||
}
|
||||
|
||||
// Adds the webcam to the active scene. The creator ALWAYS picks from the
|
||||
// cameras Windows has registered — never silently resurrects the previous
|
||||
// camera (which is what happened after deleting one scene's webcam while
|
||||
// another scene still used it). Picking a different camera than the current
|
||||
// app-wide one swaps it everywhere, so the single-identity model stays honest;
|
||||
// each scene's placement config is independent (webcam.{scene}.config).
|
||||
// Adds the webcam to the active scene. The webcam is an app-level resource:
|
||||
// once a camera is the app default, adding the layer just places it — the
|
||||
// picker runs only for the initial selection. Each scene's placement config
|
||||
// is independent (webcam.{scene}.config); the camera device is Windows' own
|
||||
// singleton and stays where we put it.
|
||||
private async Task AddWebcamToStagedSceneAsync()
|
||||
{
|
||||
var scene = StagedScene;
|
||||
if (scene == null || scene.WebcamConfig != null) return;
|
||||
|
||||
var dialog = new CameraPickerDialog(new CameraPickerViewModel(_cameraEnumerator))
|
||||
{
|
||||
Owner = Application.Current.MainWindow
|
||||
};
|
||||
if (dialog.ShowDialog() != true || dialog.PickedDevice == null) return;
|
||||
var device = dialog.PickedDevice;
|
||||
|
||||
if (_webcam == null)
|
||||
{
|
||||
var dialog = new CameraPickerDialog(new CameraPickerViewModel(_cameraEnumerator))
|
||||
{
|
||||
Owner = Application.Current.MainWindow
|
||||
};
|
||||
if (dialog.ShowDialog() != true || dialog.PickedDevice == null) return;
|
||||
var device = dialog.PickedDevice;
|
||||
_webcam = new Webcam { DeviceId = device.Id, Name = device.DisplayName };
|
||||
OnPropertyChanged(nameof(CanChangeWebcam));
|
||||
OnPropertyChanged(nameof(CanAddWebcam));
|
||||
}
|
||||
else if (_webcam.DeviceId != device.Id)
|
||||
{
|
||||
await SwapWebcamIdentityAsync(device);
|
||||
OnPropertyChanged(nameof(WebcamAddToolTip));
|
||||
}
|
||||
|
||||
var config = new WebcamSceneConfig
|
||||
|
||||
Reference in New Issue
Block a user