Update Broadcast button: diff before push, offline=local only, live=API; Go Live empty-field prompt
This commit is contained in:
@@ -7,7 +7,7 @@ namespace ytLive.Models;
|
||||
/// the fields that stay mutable for the whole life of the broadcast. Fields
|
||||
/// that only exist pre-launch (scheduledStartTime, contentDetails) are NOT
|
||||
/// here by design; see ai.md "Broadcast metadata side pull-out".</summary>
|
||||
public sealed class BroadcastMetadata
|
||||
public sealed record BroadcastMetadata
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class LiveBroadcastFormViewModel : ViewModelBase
|
||||
private readonly Func<LayoutStore> _store;
|
||||
private readonly Func<YouTubeStreamService?> _streamService;
|
||||
private readonly Func<string?> _broadcastId;
|
||||
private readonly Func<bool> _isLive;
|
||||
|
||||
private string _title = string.Empty;
|
||||
private string _description = string.Empty;
|
||||
@@ -31,15 +32,18 @@ public sealed class LiveBroadcastFormViewModel : ViewModelBase
|
||||
private bool _isDrawerOpen;
|
||||
private bool _isUpdating;
|
||||
private string? _updateError;
|
||||
private BroadcastMetadata? _lastSyncedSnapshot;
|
||||
|
||||
public LiveBroadcastFormViewModel(
|
||||
Func<LayoutStore> store,
|
||||
Func<YouTubeStreamService?> streamService,
|
||||
Func<string?> broadcastId)
|
||||
Func<string?> broadcastId,
|
||||
Func<bool>? isLive = null)
|
||||
{
|
||||
_store = store;
|
||||
_streamService = streamService;
|
||||
_broadcastId = broadcastId;
|
||||
_isLive = isLive ?? (() => false);
|
||||
UpdateBroadcastCommand = new RelayCommand(_ => _ = UpdateRemoteAsync(), _ => CanUpdateRemote);
|
||||
ToggleDrawerCommand = new RelayCommand(_ => IsDrawerOpen = !IsDrawerOpen);
|
||||
CloseDrawerCommand = new RelayCommand(_ => IsDrawerOpen = false);
|
||||
@@ -92,9 +96,10 @@ public sealed class LiveBroadcastFormViewModel : ViewModelBase
|
||||
private set => SetProperty(ref _updateError, value);
|
||||
}
|
||||
|
||||
public bool CanUpdateRemote => !string.IsNullOrEmpty(_broadcastId()) && !IsUpdating;
|
||||
public bool CanUpdateRemote => !IsUpdating;
|
||||
|
||||
/// <summary>Pull everything from the store (startup / Go Live refresh).</summary>
|
||||
/// <summary>Pull everything from the store (startup / Go Live refresh).
|
||||
/// Seeds the synced snapshot so the next Update click knows what is already remote.</summary>
|
||||
public void Load()
|
||||
{
|
||||
var meta = _store().LoadBroadcastMetadata();
|
||||
@@ -110,6 +115,7 @@ public sealed class LiveBroadcastFormViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(Visibility));
|
||||
OnPropertyChanged(nameof(MadeForKids));
|
||||
OnPropertyChanged(nameof(ScheduledStartText));
|
||||
SnapshotSynced();
|
||||
}
|
||||
|
||||
/// <summary>Called at Go Live with what was actually inserted remotely.</summary>
|
||||
@@ -119,6 +125,7 @@ public sealed class LiveBroadcastFormViewModel : ViewModelBase
|
||||
Description = description;
|
||||
ScheduledStartTime = scheduledStart;
|
||||
UpdateError = null;
|
||||
SnapshotSynced();
|
||||
}
|
||||
|
||||
public BroadcastMetadata ToMetadata() => new()
|
||||
@@ -147,13 +154,36 @@ public sealed class LiveBroadcastFormViewModel : ViewModelBase
|
||||
{
|
||||
var id = _broadcastId();
|
||||
var service = _streamService();
|
||||
if (id == null || service == null || IsUpdating) return;
|
||||
if (IsUpdating) return;
|
||||
IsUpdating = true;
|
||||
UpdateError = null;
|
||||
try
|
||||
{
|
||||
var error = await service.UpdateBroadcast(id, ToMetadata());
|
||||
var current = ToMetadata();
|
||||
if (!_isLive())
|
||||
{
|
||||
UpdateError = null;
|
||||
IsDrawerOpen = false;
|
||||
return;
|
||||
}
|
||||
if (id == null || service == null)
|
||||
{
|
||||
UpdateError = null;
|
||||
IsDrawerOpen = false;
|
||||
return;
|
||||
}
|
||||
if (_lastSyncedSnapshot != null && current.Equals(_lastSyncedSnapshot))
|
||||
{
|
||||
UpdateError = "No changes to update";
|
||||
return;
|
||||
}
|
||||
var error = await service.UpdateBroadcast(id, current);
|
||||
UpdateError = error;
|
||||
if (error == null)
|
||||
{
|
||||
SnapshotSynced();
|
||||
IsDrawerOpen = false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -165,4 +195,6 @@ public sealed class LiveBroadcastFormViewModel : ViewModelBase
|
||||
IsUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SnapshotSynced() => _lastSyncedSnapshot = ToMetadata();
|
||||
}
|
||||
|
||||
@@ -1565,7 +1565,8 @@ public class MainViewModel : ViewModelBase
|
||||
BroadcastForm = new Services.LiveBroadcastFormViewModel(
|
||||
() => _layoutStore,
|
||||
() => _youtubeStream,
|
||||
() => CurrentBroadcastId);
|
||||
() => CurrentBroadcastId,
|
||||
() => IsLive);
|
||||
|
||||
// Restore the last picked mic so the mixer's first capture attaches to
|
||||
// the same, already-vetted device (green dot) or reports it missing
|
||||
@@ -2961,6 +2962,13 @@ public class MainViewModel : ViewModelBase
|
||||
AppLog.Write($"Broadcast created: {_currentBroadcastId}");
|
||||
OnPropertyChanged(nameof(CurrentBroadcastId));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(StreamTitle) && string.IsNullOrWhiteSpace(StreamDescription))
|
||||
{
|
||||
BroadcastForm.IsDrawerOpen = true;
|
||||
_notifications.Warning("Complete your broadcast details",
|
||||
"Your title and description are empty — fill them in from the Text panel on the right.");
|
||||
}
|
||||
|
||||
// Start chat polling (TASK 9.4): fetch the liveChatId from the broadcast's
|
||||
// contentDetails, then start the poll loop. Chat is non-fatal — if it fails,
|
||||
// the stream continues without on-panel chat.
|
||||
|
||||
@@ -142,6 +142,82 @@ public sealed class BroadcastPullOutTests
|
||||
Assert.True(root.GetProperty("status").GetProperty("selfDeclaredMadeForKids").GetBoolean());
|
||||
Assert.Equal(new DateTime(2026, 8, 24, 12, 0, 0, DateTimeKind.Utc).ToString("o"),
|
||||
root.GetProperty("snippet").GetProperty("scheduledStartTime").GetString());
|
||||
|
||||
// --- Diff + close drawer tests ---
|
||||
|
||||
var auth2 = new YouTubeAuthService("test-client", "test-secret");
|
||||
auth2.SetSession(new YouTubeChannel
|
||||
{
|
||||
AccessToken = "token2",
|
||||
TokenExpiry = DateTime.UtcNow.AddHours(1),
|
||||
});
|
||||
var stub2 = new StubHandler();
|
||||
var service2 = new YouTubeStreamService(auth2, new HttpClient(stub2));
|
||||
|
||||
string? broadcastId = "bc456";
|
||||
|
||||
// --- Offline path (isLive = false): saves locally, never pushes API ---
|
||||
var formOffline = new LiveBroadcastFormViewModel(
|
||||
() => new LayoutStore(tempDb),
|
||||
() => service2,
|
||||
() => broadcastId,
|
||||
() => false);
|
||||
|
||||
formOffline.Load();
|
||||
Assert.False(formOffline.IsDrawerOpen);
|
||||
|
||||
// No edits → stays open, no API call
|
||||
formOffline.IsDrawerOpen = true;
|
||||
formOffline.UpdateBroadcastCommand.Execute(null);
|
||||
Assert.Null(stub2.Request);
|
||||
Assert.False(formOffline.IsDrawerOpen);
|
||||
|
||||
// Edit Title → closes drawer, still no API call (offline = local save only)
|
||||
stub2.Request = null;
|
||||
formOffline.Title = "Changed offline";
|
||||
formOffline.IsDrawerOpen = true;
|
||||
formOffline.UpdateBroadcastCommand.Execute(null);
|
||||
Assert.Null(stub2.Request);
|
||||
Assert.False(formOffline.IsDrawerOpen);
|
||||
|
||||
// Re-load → the offline edit persisted to the store
|
||||
formOffline.Load();
|
||||
Assert.Equal("Changed offline", formOffline.Title);
|
||||
|
||||
// --- Live path (isLive = true): pushes to API ---
|
||||
var formLive = new LiveBroadcastFormViewModel(
|
||||
() => new LayoutStore(tempDb),
|
||||
() => service2,
|
||||
() => broadcastId,
|
||||
() => true);
|
||||
|
||||
formLive.Load();
|
||||
formLive.IsDrawerOpen = true;
|
||||
|
||||
// No edits → "No changes to update", stays open
|
||||
stub2.Request = null;
|
||||
formLive.UpdateBroadcastCommand.Execute(null);
|
||||
Assert.Null(stub2.Request);
|
||||
Assert.Equal("No changes to update", formLive.UpdateError);
|
||||
Assert.True(formLive.IsDrawerOpen);
|
||||
|
||||
// Edit Title → HTTP PUT fires, drawer closes
|
||||
stub2.Request = null;
|
||||
formLive.Title = "Live update";
|
||||
formLive.UpdateBroadcastCommand.Execute(null);
|
||||
Assert.NotNull(stub2.Request);
|
||||
Assert.Equal(HttpMethod.Put, stub2.Request!.Method);
|
||||
Assert.Null(formLive.UpdateError);
|
||||
Assert.False(formLive.IsDrawerOpen);
|
||||
|
||||
// Re-seed snapshot via Load, then edit again → second upload fires
|
||||
stub2.Request = null;
|
||||
formLive.Load();
|
||||
formLive.Description = "Live desc";
|
||||
formLive.UpdateBroadcastCommand.Execute(null);
|
||||
Assert.NotNull(stub2.Request);
|
||||
Assert.Null(formLive.UpdateError);
|
||||
Assert.False(formLive.IsDrawerOpen);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user