Simplify schedule playback order
This commit is contained in:
@@ -192,7 +192,7 @@
|
|||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="18">
|
CornerRadius="18">
|
||||||
<StackPanel Spacing="6">
|
<StackPanel Spacing="6">
|
||||||
<TextBlock Style="{StaticResource ConsoleLabelTextStyle}" Text="대기열" />
|
<TextBlock Style="{StaticResource ConsoleLabelTextStyle}" Text="스케줄 목록" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
FontFamily="Consolas"
|
FontFamily="Consolas"
|
||||||
FontSize="30"
|
FontSize="30"
|
||||||
@@ -754,7 +754,7 @@
|
|||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Style="{StaticResource ConsoleSectionTitleTextStyle}"
|
Style="{StaticResource ConsoleSectionTitleTextStyle}"
|
||||||
Text="대기 중 목록" />
|
Text="스케줄 목록" />
|
||||||
<Button
|
<Button
|
||||||
Width="22"
|
Width="22"
|
||||||
Height="22"
|
Height="22"
|
||||||
@@ -769,7 +769,7 @@
|
|||||||
<Flyout>
|
<Flyout>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
MaxWidth="280"
|
MaxWidth="280"
|
||||||
Text="빨강은 현재 송출 중, 노랑은 다음 송출 예정입니다. 목록의 다음 버튼은 다음 예약만 바꾸고, '다음 컷 즉시 송출'은 노란 컷을 바로 송출합니다. '다음 목록 즉시 송출'은 대기열 순서상 다음 목록을 바로 송출합니다."
|
Text="빨강은 현재 송출 중, 노랑은 다음 송출 예정입니다. 스케줄은 항상 목록 위에서 아래로 진행하고, 마지막 컷 이후에는 반복이 켜져 있을 때만 맨 위로 이어집니다."
|
||||||
TextWrapping="WrapWholeWords" />
|
TextWrapping="WrapWholeWords" />
|
||||||
</Flyout>
|
</Flyout>
|
||||||
</Button.Flyout>
|
</Button.Flyout>
|
||||||
@@ -799,24 +799,11 @@
|
|||||||
Command="{x:Bind ViewModel.ForceNextCommand}"
|
Command="{x:Bind ViewModel.ForceNextCommand}"
|
||||||
Style="{StaticResource PanelCommandButtonStyle}">
|
Style="{StaticResource PanelCommandButtonStyle}">
|
||||||
<TextBlock TextAlignment="Center">
|
<TextBlock TextAlignment="Center">
|
||||||
<Run Text="다음 컷" />
|
<Run Text="다음" />
|
||||||
<LineBreak />
|
<LineBreak />
|
||||||
<Run Text="즉시 송출" />
|
<Run Text="즉시 송출" />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
Command="{x:Bind ViewModel.ForceQueueNextCommand}"
|
|
||||||
Style="{StaticResource PanelCommandButtonStyle}">
|
|
||||||
<TextBlock TextAlignment="Center">
|
|
||||||
<Run Text="다음 목록" />
|
|
||||||
<LineBreak />
|
|
||||||
<Run Text="즉시 송출" />
|
|
||||||
</TextBlock>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
Command="{x:Bind ViewModel.ResetQueueCommand}"
|
|
||||||
Content="큐 초기화"
|
|
||||||
Style="{StaticResource PanelCommandButtonStyle}" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -961,10 +948,6 @@
|
|||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
Spacing="8"
|
Spacing="8"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
<Button
|
|
||||||
Click="PromoteToNextButton_Click"
|
|
||||||
Content="다음"
|
|
||||||
Style="{StaticResource PanelCommandButtonStyle}" />
|
|
||||||
<Button
|
<Button
|
||||||
Click="MoveUpButton_Click"
|
Click="MoveUpButton_Click"
|
||||||
Content="위"
|
Content="위"
|
||||||
|
|||||||
@@ -91,7 +91,10 @@ public sealed class ChannelScheduleItem : ObservableObject
|
|||||||
get => _state;
|
get => _state;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (SetProperty(ref _state, value))
|
var normalized = value == ScheduleQueueItemState.Completed
|
||||||
|
? ScheduleQueueItemState.Queued
|
||||||
|
: value;
|
||||||
|
if (SetProperty(ref _state, normalized))
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(StateLabel));
|
OnPropertyChanged(nameof(StateLabel));
|
||||||
OnPropertyChanged(nameof(StateBrush));
|
OnPropertyChanged(nameof(StateBrush));
|
||||||
@@ -135,7 +138,7 @@ public sealed class ChannelScheduleItem : ObservableObject
|
|||||||
ScheduleQueueItemState.Next => "다음",
|
ScheduleQueueItemState.Next => "다음",
|
||||||
ScheduleQueueItemState.Sending => "준비",
|
ScheduleQueueItemState.Sending => "준비",
|
||||||
ScheduleQueueItemState.OnAir => "송출 중",
|
ScheduleQueueItemState.OnAir => "송출 중",
|
||||||
ScheduleQueueItemState.Completed => "완료",
|
ScheduleQueueItemState.Completed => "대기",
|
||||||
ScheduleQueueItemState.Error => "오류",
|
ScheduleQueueItemState.Error => "오류",
|
||||||
_ => "대기"
|
_ => "대기"
|
||||||
};
|
};
|
||||||
@@ -181,7 +184,7 @@ public sealed class ChannelScheduleItem : ObservableObject
|
|||||||
});
|
});
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public double CardOpacity => State == ScheduleQueueItemState.Completed ? 0.45 : 1.0;
|
public double CardOpacity => 1.0;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool CanDelete => State is not ScheduleQueueItemState.OnAir and not ScheduleQueueItemState.Sending;
|
public bool CanDelete => State is not ScheduleQueueItemState.OnAir and not ScheduleQueueItemState.Sending;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public sealed class ChannelScheduleEngine
|
|||||||
private readonly SemaphoreSlim _executionLock = new(1, 1);
|
private readonly SemaphoreSlim _executionLock = new(1, 1);
|
||||||
private CancellationTokenSource? _playbackCts;
|
private CancellationTokenSource? _playbackCts;
|
||||||
private TaskCompletionSource<bool>? _advanceSignal;
|
private TaskCompletionSource<bool>? _advanceSignal;
|
||||||
private Guid? _preferredNextItemId;
|
private Guid? _lastPlaybackItemId;
|
||||||
private Guid? _skipCurrentItemId;
|
private Guid? _skipCurrentItemId;
|
||||||
private ChannelScheduleItem? _directPlaybackItem;
|
private ChannelScheduleItem? _directPlaybackItem;
|
||||||
private PreparedCutFrame? _preparedCutFrame;
|
private PreparedCutFrame? _preparedCutFrame;
|
||||||
@@ -86,6 +86,7 @@ public sealed class ChannelScheduleEngine
|
|||||||
ClearPreparedFrame(resetState: true);
|
ClearPreparedFrame(resetState: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_lastPlaybackItemId = null;
|
||||||
_playbackCts = new CancellationTokenSource();
|
_playbackCts = new CancellationTokenSource();
|
||||||
IsRunning = true;
|
IsRunning = true;
|
||||||
RefreshQueueMarkers();
|
RefreshQueueMarkers();
|
||||||
@@ -108,7 +109,7 @@ public sealed class ChannelScheduleEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
ClearPreparedFrame(resetState: true);
|
ClearPreparedFrame(resetState: true);
|
||||||
_preferredNextItemId = null;
|
_lastPlaybackItemId = null;
|
||||||
_skipCurrentItemId = null;
|
_skipCurrentItemId = null;
|
||||||
RefreshQueueMarkers();
|
RefreshQueueMarkers();
|
||||||
QueueChanged?.Invoke(this, EventArgs.Empty);
|
QueueChanged?.Invoke(this, EventArgs.Empty);
|
||||||
@@ -130,7 +131,7 @@ public sealed class ChannelScheduleEngine
|
|||||||
item.ClearInternalNextPreview();
|
item.ClearInternalNextPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
_preferredNextItemId = null;
|
_lastPlaybackItemId = null;
|
||||||
_skipCurrentItemId = null;
|
_skipCurrentItemId = null;
|
||||||
ClearPreparedFrame(resetState: false);
|
ClearPreparedFrame(resetState: false);
|
||||||
IsRunning = false;
|
IsRunning = false;
|
||||||
@@ -148,6 +149,7 @@ public sealed class ChannelScheduleEngine
|
|||||||
await _executionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
await _executionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
_lastPlaybackItemId = null;
|
||||||
ClearPreparedFrame(resetState: true);
|
ClearPreparedFrame(resetState: true);
|
||||||
RefreshQueueMarkers();
|
RefreshQueueMarkers();
|
||||||
|
|
||||||
@@ -251,7 +253,7 @@ public sealed class ChannelScheduleEngine
|
|||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
_preferredNextItemId = null;
|
_lastPlaybackItemId = null;
|
||||||
ClearPreparedFrame(resetState: false);
|
ClearPreparedFrame(resetState: false);
|
||||||
foreach (var item in Queue)
|
foreach (var item in Queue)
|
||||||
{
|
{
|
||||||
@@ -272,18 +274,6 @@ public sealed class ChannelScheduleEngine
|
|||||||
|
|
||||||
public async Task ForceQueueNextAsync()
|
public async Task ForceQueueNextAsync()
|
||||||
{
|
{
|
||||||
if (!IsRunning)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var nextListItem = GetNextPendingQueueItem();
|
|
||||||
if (nextListItem is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_preferredNextItemId = nextListItem.Id;
|
|
||||||
await ForceNextAsync().ConfigureAwait(false);
|
await ForceNextAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,9 +288,11 @@ public sealed class ChannelScheduleEngine
|
|||||||
if (activeItem is not null)
|
if (activeItem is not null)
|
||||||
{
|
{
|
||||||
_skipCurrentItemId = activeItem.Id;
|
_skipCurrentItemId = activeItem.Id;
|
||||||
activeItem.State = ScheduleQueueItemState.Completed;
|
_lastPlaybackItemId = activeItem.Id;
|
||||||
|
activeItem.State = ScheduleQueueItemState.Queued;
|
||||||
activeItem.LastError = string.Empty;
|
activeItem.LastError = string.Empty;
|
||||||
activeItem.CurrentRegionLabel = string.Empty;
|
activeItem.CurrentRegionLabel = string.Empty;
|
||||||
|
activeItem.ClearInternalNextPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
RefreshQueueMarkers();
|
RefreshQueueMarkers();
|
||||||
@@ -319,9 +311,9 @@ public sealed class ChannelScheduleEngine
|
|||||||
var removed = Queue.Remove(item);
|
var removed = Queue.Remove(item);
|
||||||
if (removed)
|
if (removed)
|
||||||
{
|
{
|
||||||
if (_preferredNextItemId == item.Id)
|
if (_lastPlaybackItemId == item.Id)
|
||||||
{
|
{
|
||||||
_preferredNextItemId = null;
|
_lastPlaybackItemId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
RefreshQueueMarkers();
|
RefreshQueueMarkers();
|
||||||
@@ -343,17 +335,25 @@ public sealed class ChannelScheduleEngine
|
|||||||
|
|
||||||
public bool PromoteToNext(ChannelScheduleItem? item)
|
public bool PromoteToNext(ChannelScheduleItem? item)
|
||||||
{
|
{
|
||||||
if (item is null || item.State is not (ScheduleQueueItemState.Queued or ScheduleQueueItemState.Next))
|
if (item is null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_preferredNextItemId == item.Id && item.State == ScheduleQueueItemState.Next)
|
var anchorItem = ActivePlaybackItem ?? GetLastPlaybackItem();
|
||||||
|
var targetIndex = anchorItem is null ? 0 : Queue.IndexOf(anchorItem) + 1;
|
||||||
|
var currentIndex = Queue.IndexOf(item);
|
||||||
|
if (currentIndex < 0 || targetIndex < 0 || targetIndex > Queue.Count || currentIndex == targetIndex)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_preferredNextItemId = item.Id;
|
if (currentIndex < targetIndex)
|
||||||
|
{
|
||||||
|
targetIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
Queue.Move(currentIndex, targetIndex);
|
||||||
RefreshQueueMarkers();
|
RefreshQueueMarkers();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -387,12 +387,6 @@ public sealed class ChannelScheduleEngine
|
|||||||
var next = GetNextPlayableItem();
|
var next = GetNextPlayableItem();
|
||||||
if (next is null)
|
if (next is null)
|
||||||
{
|
{
|
||||||
if (LoopEnabled && Queue.Count > 0)
|
|
||||||
{
|
|
||||||
Reset();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (EmptyScheduleBehavior == EmptyScheduleBehavior.ImmediateOut)
|
if (EmptyScheduleBehavior == EmptyScheduleBehavior.ImmediateOut)
|
||||||
{
|
{
|
||||||
await _adapter.OutAsync(Channel, cancellationToken).ConfigureAwait(false);
|
await _adapter.OutAsync(Channel, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -411,6 +405,7 @@ public sealed class ChannelScheduleEngine
|
|||||||
{
|
{
|
||||||
next.State = ScheduleQueueItemState.Error;
|
next.State = ScheduleQueueItemState.Error;
|
||||||
next.LastError = "포맷을 찾을 수 없습니다.";
|
next.LastError = "포맷을 찾을 수 없습니다.";
|
||||||
|
_lastPlaybackItemId = next.Id;
|
||||||
_logService.Error($"[{Channel}] Missing template: {next.FormatId}");
|
_logService.Error($"[{Channel}] Missing template: {next.FormatId}");
|
||||||
RefreshQueueMarkers();
|
RefreshQueueMarkers();
|
||||||
continue;
|
continue;
|
||||||
@@ -528,6 +523,7 @@ public sealed class ChannelScheduleEngine
|
|||||||
queueItem.State = ScheduleQueueItemState.Error;
|
queueItem.State = ScheduleQueueItemState.Error;
|
||||||
queueItem.LastError = "송출 가능한 지역 데이터가 없습니다.";
|
queueItem.LastError = "송출 가능한 지역 데이터가 없습니다.";
|
||||||
queueItem.CurrentRegionLabel = string.Empty;
|
queueItem.CurrentRegionLabel = string.Empty;
|
||||||
|
_lastPlaybackItemId = queueItem.Id;
|
||||||
RefreshQueueMarkers();
|
RefreshQueueMarkers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -613,7 +609,8 @@ public sealed class ChannelScheduleEngine
|
|||||||
|
|
||||||
queueItem.CurrentRegionLabel = string.Empty;
|
queueItem.CurrentRegionLabel = string.Empty;
|
||||||
queueItem.ClearInternalNextPreview();
|
queueItem.ClearInternalNextPreview();
|
||||||
queueItem.State = playedAny ? ScheduleQueueItemState.Completed : ScheduleQueueItemState.Error;
|
_lastPlaybackItemId = queueItem.Id;
|
||||||
|
queueItem.State = playedAny ? ScheduleQueueItemState.Queued : ScheduleQueueItemState.Error;
|
||||||
queueItem.LastError = playedAny ? string.Empty : (string.IsNullOrWhiteSpace(lastFailure) ? "송출 가능한 지역 데이터가 없습니다." : lastFailure);
|
queueItem.LastError = playedAny ? string.Empty : (string.IsNullOrWhiteSpace(lastFailure) ? "송출 가능한 지역 데이터가 없습니다." : lastFailure);
|
||||||
ClearSkipCurrentItem(queueItem);
|
ClearSkipCurrentItem(queueItem);
|
||||||
RefreshQueueMarkers();
|
RefreshQueueMarkers();
|
||||||
@@ -697,7 +694,8 @@ public sealed class ChannelScheduleEngine
|
|||||||
|
|
||||||
queueItem.CurrentRegionLabel = string.Empty;
|
queueItem.CurrentRegionLabel = string.Empty;
|
||||||
queueItem.ClearInternalNextPreview();
|
queueItem.ClearInternalNextPreview();
|
||||||
queueItem.State = playedAny ? ScheduleQueueItemState.Completed : ScheduleQueueItemState.Error;
|
_lastPlaybackItemId = queueItem.Id;
|
||||||
|
queueItem.State = playedAny ? ScheduleQueueItemState.Queued : ScheduleQueueItemState.Error;
|
||||||
queueItem.LastError = playedAny ? string.Empty : (string.IsNullOrWhiteSpace(lastFailure) ? "송출 가능한 지역 데이터가 없습니다." : lastFailure);
|
queueItem.LastError = playedAny ? string.Empty : (string.IsNullOrWhiteSpace(lastFailure) ? "송출 가능한 지역 데이터가 없습니다." : lastFailure);
|
||||||
ClearSkipCurrentItem(queueItem);
|
ClearSkipCurrentItem(queueItem);
|
||||||
RefreshQueueMarkers();
|
RefreshQueueMarkers();
|
||||||
@@ -1112,8 +1110,7 @@ public sealed class ChannelScheduleEngine
|
|||||||
|
|
||||||
private ChannelScheduleItem? GetPreviewNextItem(ChannelScheduleItem activeItem)
|
private ChannelScheduleItem? GetPreviewNextItem(ChannelScheduleItem activeItem)
|
||||||
{
|
{
|
||||||
return Queue.FirstOrDefault(item => item != activeItem && item.State == ScheduleQueueItemState.Next)
|
return GetSequentialNextItem(activeItem, allowWrap: LoopEnabled);
|
||||||
?? Queue.FirstOrDefault(item => item != activeItem && item.State == ScheduleQueueItemState.Queued);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldSkipCurrentItem(ChannelScheduleItem queueItem)
|
private bool ShouldSkipCurrentItem(ChannelScheduleItem queueItem)
|
||||||
@@ -1615,32 +1612,19 @@ public sealed class ChannelScheduleEngine
|
|||||||
return preparedItem;
|
return preparedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Queue.FirstOrDefault(item => item.State == ScheduleQueueItemState.Next)
|
var anchorItem = ActivePlaybackItem ?? GetLastPlaybackItem();
|
||||||
?? Queue.FirstOrDefault(item => item.State == ScheduleQueueItemState.Queued);
|
return GetSequentialNextItem(anchorItem, allowWrap: LoopEnabled);
|
||||||
}
|
|
||||||
|
|
||||||
private ChannelScheduleItem? GetNextPendingQueueItem()
|
|
||||||
{
|
|
||||||
return Queue.FirstOrDefault(item => item.State is ScheduleQueueItemState.Next or ScheduleQueueItemState.Queued);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RefreshQueueMarkers()
|
public void RefreshQueueMarkers()
|
||||||
{
|
{
|
||||||
var activeItem = Queue.FirstOrDefault(item => item.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending);
|
var activeItem = Queue.FirstOrDefault(item => item.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending);
|
||||||
var pendingItems = Queue
|
var anchorItem = activeItem ?? GetLastPlaybackItem();
|
||||||
.Where(item => item != activeItem && item.State is ScheduleQueueItemState.Queued or ScheduleQueueItemState.Next)
|
var nextItem = GetSequentialNextItem(anchorItem, allowWrap: LoopEnabled);
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var nextItem = pendingItems.FirstOrDefault(item => _preferredNextItemId == item.Id);
|
|
||||||
if (nextItem is null)
|
|
||||||
{
|
|
||||||
_preferredNextItemId = null;
|
|
||||||
nextItem = pendingItems.FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var item in Queue)
|
foreach (var item in Queue)
|
||||||
{
|
{
|
||||||
if (item == activeItem || item.State == ScheduleQueueItemState.Completed || item.State == ScheduleQueueItemState.Error)
|
if (item == activeItem || item.State == ScheduleQueueItemState.Error)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1649,6 +1633,50 @@ public sealed class ChannelScheduleEngine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ChannelScheduleItem? GetLastPlaybackItem()
|
||||||
|
{
|
||||||
|
if (_lastPlaybackItemId is not { } itemId)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Queue.FirstOrDefault(item => item.Id == itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChannelScheduleItem? GetSequentialNextItem(ChannelScheduleItem? anchorItem, bool allowWrap)
|
||||||
|
{
|
||||||
|
if (Queue.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchorItem is null)
|
||||||
|
{
|
||||||
|
return Queue.FirstOrDefault(IsPlayableQueueItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
var anchorIndex = Queue.IndexOf(anchorItem);
|
||||||
|
if (anchorIndex < 0)
|
||||||
|
{
|
||||||
|
return Queue.FirstOrDefault(IsPlayableQueueItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextItem = Queue.Skip(anchorIndex + 1).FirstOrDefault(IsPlayableQueueItem);
|
||||||
|
if (nextItem is not null)
|
||||||
|
{
|
||||||
|
return nextItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowWrap
|
||||||
|
? Queue.Take(anchorIndex + 1).FirstOrDefault(IsPlayableQueueItem)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPlayableQueueItem(ChannelScheduleItem item)
|
||||||
|
{
|
||||||
|
return item.State is ScheduleQueueItemState.Queued or ScheduleQueueItemState.Next or ScheduleQueueItemState.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
private sealed record PreparedCutFrame(
|
private sealed record PreparedCutFrame(
|
||||||
ChannelScheduleItem Item,
|
ChannelScheduleItem Item,
|
||||||
FormatTemplateDefinition Template,
|
FormatTemplateDefinition Template,
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
|||||||
if (SetProperty(ref _loopEnabled, value))
|
if (SetProperty(ref _loopEnabled, value))
|
||||||
{
|
{
|
||||||
_engine.LoopEnabled = value;
|
_engine.LoopEnabled = value;
|
||||||
|
_engine.RefreshQueueMarkers();
|
||||||
RefreshSummary();
|
RefreshSummary();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,9 +375,9 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
|||||||
|
|
||||||
public double NextPreviewHeight => ResolvePlaybackPreviewMetrics(NextPlaybackItem).Height;
|
public double NextPreviewHeight => ResolvePlaybackPreviewMetrics(NextPlaybackItem).Height;
|
||||||
|
|
||||||
public int QueuedItemCount => Queue.Count(item => item.State == ScheduleQueueItemState.Queued);
|
public int QueuedItemCount => Queue.Count;
|
||||||
|
|
||||||
public string QueueFootnote => $"대기 {QueuedItemCount}건 / 컷 {AvailableFormats.Count}개";
|
public string QueueFootnote => $"목록 {QueuedItemCount}건 / 컷 {AvailableFormats.Count}개";
|
||||||
|
|
||||||
public string QueueSummary
|
public string QueueSummary
|
||||||
{
|
{
|
||||||
@@ -386,7 +387,7 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
|||||||
var next = InternalNextPlaybackItem?.InternalNextPreviewDisplayName
|
var next = InternalNextPlaybackItem?.InternalNextPreviewDisplayName
|
||||||
?? QueueNextPlaybackItem?.DisplayName
|
?? QueueNextPlaybackItem?.DisplayName
|
||||||
?? "-";
|
?? "-";
|
||||||
return $"현재 {current} / 다음 {next} / 대기 {Queue.Count(item => item.State == ScheduleQueueItemState.Queued)}";
|
return $"현재 {current} / 다음 {next} / 목록 {Queue.Count}건";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,7 +530,7 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
await _engine.StartAsync().ConfigureAwait(false);
|
await _engine.StartAsync().ConfigureAwait(false);
|
||||||
RefreshSummary();
|
RefreshSummary();
|
||||||
_logService.Info($"[{Title}] 큐를 시작");
|
_logService.Info($"[{Title}] 스케줄 시작");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PrepareScheduleAsync()
|
private async Task PrepareScheduleAsync()
|
||||||
@@ -551,7 +552,7 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
await _engine.StopAsync().ConfigureAwait(false);
|
await _engine.StopAsync().ConfigureAwait(false);
|
||||||
RefreshSummary();
|
RefreshSummary();
|
||||||
_logService.Info($"[{Title}] 큐를 종료");
|
_logService.Info($"[{Title}] 스케줄 정지");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DirectPrepareAsync()
|
private async Task DirectPrepareAsync()
|
||||||
@@ -718,7 +719,7 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
await _engine.ForceQueueNextAsync().ConfigureAwait(false);
|
await _engine.ForceQueueNextAsync().ConfigureAwait(false);
|
||||||
RefreshSummary();
|
RefreshSummary();
|
||||||
_logService.Info($"[{Title}] 대기열의 다음 목록을 즉시 송출");
|
_logService.Info($"[{Title}] 다음 순서를 즉시 송출");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddFormat()
|
private void AddFormat()
|
||||||
@@ -745,7 +746,7 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
_engine.Reset();
|
_engine.Reset();
|
||||||
RefreshSummary();
|
RefreshSummary();
|
||||||
_logService.Info($"[{Title}] 큐를 첫 컷부터 다시 시작하도록 초기화했습니다.");
|
_logService.Info($"[{Title}] 스케줄을 맨 위 컷부터 다시 시작하도록 되돌렸습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveItem(ChannelScheduleItem? item)
|
private void RemoveItem(ChannelScheduleItem? item)
|
||||||
|
|||||||
Reference in New Issue
Block a user