278 lines
9.0 KiB
C#
278 lines
9.0 KiB
C#
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Tornado3_2026Election.Common;
|
|
using Tornado3_2026Election.Domain;
|
|
using Tornado3_2026Election.Services;
|
|
|
|
namespace Tornado3_2026Election.ViewModels;
|
|
|
|
public sealed class ChannelScheduleViewModel : ObservableObject
|
|
{
|
|
private readonly ChannelScheduleEngine _engine;
|
|
private readonly ITornado3Adapter _adapter;
|
|
private readonly LogService _logService;
|
|
private FormatTemplateDefinition? _selectedFormat;
|
|
private SelectionOption<EmptyScheduleBehavior>? _selectedEmptyBehaviorOption;
|
|
private bool _loopEnabled;
|
|
private EmptyScheduleBehavior _emptyScheduleBehavior = EmptyScheduleBehavior.ImmediateOut;
|
|
|
|
public ChannelScheduleViewModel(
|
|
BroadcastChannel channel,
|
|
string title,
|
|
IReadOnlyList<FormatTemplateDefinition> formats,
|
|
ITornado3Adapter adapter,
|
|
ChannelScheduleEngine engine,
|
|
LogService logService)
|
|
{
|
|
Channel = channel;
|
|
Title = title;
|
|
_adapter = adapter;
|
|
_engine = engine;
|
|
_logService = logService;
|
|
AvailableFormats = new ObservableCollection<FormatTemplateDefinition>(formats);
|
|
EmptyBehaviorOptions =
|
|
[
|
|
new SelectionOption<EmptyScheduleBehavior>(EmptyScheduleBehavior.ImmediateOut, "즉시 아웃"),
|
|
new SelectionOption<EmptyScheduleBehavior>(EmptyScheduleBehavior.HoldLastFrame, "마지막 프레임 유지")
|
|
];
|
|
Queue = engine.Queue;
|
|
|
|
StartCommand = new AsyncRelayCommand(StartAsync);
|
|
StopCommand = new AsyncRelayCommand(StopAsync);
|
|
ForceNextCommand = new AsyncRelayCommand(ForceNextAsync);
|
|
AddFormatCommand = new RelayCommand(AddFormat, () => SelectedFormat is not null);
|
|
ResetQueueCommand = new RelayCommand(ResetQueue);
|
|
RemoveItemCommand = new RelayCommand<ChannelScheduleItem>(RemoveItem);
|
|
MoveUpCommand = new RelayCommand<ChannelScheduleItem>(MoveUp);
|
|
MoveDownCommand = new RelayCommand<ChannelScheduleItem>(MoveDown);
|
|
PromoteToNextCommand = new RelayCommand<ChannelScheduleItem>(PromoteToNext);
|
|
SelectedFormat = AvailableFormats.FirstOrDefault();
|
|
SelectedEmptyBehaviorOption = FindEmptyBehaviorOption(_emptyScheduleBehavior);
|
|
|
|
_engine.QueueChanged += (_, _) => RefreshSummary();
|
|
_adapter.StateChanged += (_, _) => RefreshSummary();
|
|
RefreshSummary();
|
|
}
|
|
|
|
public BroadcastChannel Channel { get; }
|
|
|
|
public string Title { get; }
|
|
|
|
public ObservableCollection<FormatTemplateDefinition> AvailableFormats { get; }
|
|
|
|
public IReadOnlyList<SelectionOption<EmptyScheduleBehavior>> EmptyBehaviorOptions { get; }
|
|
|
|
public ObservableCollection<ChannelScheduleItem> Queue { get; }
|
|
|
|
public AsyncRelayCommand StartCommand { get; }
|
|
|
|
public AsyncRelayCommand StopCommand { get; }
|
|
|
|
public AsyncRelayCommand ForceNextCommand { get; }
|
|
|
|
public RelayCommand AddFormatCommand { get; }
|
|
|
|
public RelayCommand ResetQueueCommand { get; }
|
|
|
|
public RelayCommand<ChannelScheduleItem> RemoveItemCommand { get; }
|
|
|
|
public RelayCommand<ChannelScheduleItem> MoveUpCommand { get; }
|
|
|
|
public RelayCommand<ChannelScheduleItem> MoveDownCommand { get; }
|
|
|
|
public RelayCommand<ChannelScheduleItem> PromoteToNextCommand { get; }
|
|
|
|
public FormatTemplateDefinition? SelectedFormat
|
|
{
|
|
get => _selectedFormat;
|
|
set
|
|
{
|
|
if (SetProperty(ref _selectedFormat, value))
|
|
{
|
|
if (AddFormatCommand is not null)
|
|
{
|
|
AddFormatCommand.NotifyCanExecuteChanged();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool LoopEnabled
|
|
{
|
|
get => _loopEnabled;
|
|
set
|
|
{
|
|
if (SetProperty(ref _loopEnabled, value))
|
|
{
|
|
_engine.LoopEnabled = value;
|
|
RefreshSummary();
|
|
}
|
|
}
|
|
}
|
|
|
|
public EmptyScheduleBehavior EmptyScheduleBehavior
|
|
{
|
|
get => _emptyScheduleBehavior;
|
|
set
|
|
{
|
|
if (SetProperty(ref _emptyScheduleBehavior, value))
|
|
{
|
|
_engine.EmptyScheduleBehavior = value;
|
|
SelectedEmptyBehaviorOption = FindEmptyBehaviorOption(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
public SelectionOption<EmptyScheduleBehavior>? SelectedEmptyBehaviorOption
|
|
{
|
|
get => _selectedEmptyBehaviorOption;
|
|
set
|
|
{
|
|
if (value is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (SetProperty(ref _selectedEmptyBehaviorOption, value) && _emptyScheduleBehavior != value.Value)
|
|
{
|
|
EmptyScheduleBehavior = value.Value;
|
|
}
|
|
}
|
|
}
|
|
|
|
public TornadoConnectionState AdapterState => _adapter.State;
|
|
|
|
public string AdapterStateLabel => AdapterState switch
|
|
{
|
|
TornadoConnectionState.Idle => "대기",
|
|
TornadoConnectionState.Ready => "준비 완료",
|
|
TornadoConnectionState.Sending => "전송 중",
|
|
TornadoConnectionState.OnAir => "송출 중",
|
|
TornadoConnectionState.Error => "오류",
|
|
_ => "대기"
|
|
};
|
|
|
|
public string AdapterStateText => $"Tornado 상태: {AdapterStateLabel}";
|
|
|
|
public string TransmissionLabel => Queue.Any(item => item.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending)
|
|
? "송출 중"
|
|
: "대기";
|
|
|
|
public string CurrentItemName => Queue.FirstOrDefault(item => item.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending)?.FormatName ?? "대기 화면";
|
|
|
|
public string NextItemName => Queue.FirstOrDefault(item => item.State == ScheduleQueueItemState.Next)?.FormatName ?? "다음 포맷 없음";
|
|
|
|
public int QueuedItemCount => Queue.Count(item => item.State == ScheduleQueueItemState.Queued);
|
|
|
|
public string QueueFootnote => $"대기 {QueuedItemCount}건 · 프리셋 {AvailableFormats.Count}개";
|
|
|
|
public string QueueSummary
|
|
{
|
|
get
|
|
{
|
|
var current = Queue.FirstOrDefault(item => item.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending)?.FormatName ?? "-";
|
|
var next = Queue.FirstOrDefault(item => item.State == ScheduleQueueItemState.Next)?.FormatName ?? "-";
|
|
return $"현재 {current} / 다음 {next} / 대기 {Queue.Count(item => item.State == ScheduleQueueItemState.Queued)}";
|
|
}
|
|
}
|
|
|
|
public string LoopSummary => LoopEnabled ? "전체 반복 켜짐" : "한 번 재생";
|
|
|
|
public string EmptyBehaviorLabel => SelectedEmptyBehaviorOption?.Label ?? "즉시 아웃";
|
|
|
|
public string OperatorQuickSummary => $"{AdapterStateLabel} · {LoopSummary} · 빈 스케줄 {EmptyBehaviorLabel}";
|
|
|
|
private async Task StartAsync()
|
|
{
|
|
await _engine.StartAsync().ConfigureAwait(false);
|
|
RefreshSummary();
|
|
_logService.Info($"[{Title}] 스케줄 시작");
|
|
}
|
|
|
|
private async Task StopAsync()
|
|
{
|
|
await _engine.StopAsync().ConfigureAwait(false);
|
|
RefreshSummary();
|
|
_logService.Info($"[{Title}] 스케줄 종료");
|
|
}
|
|
|
|
private async Task ForceNextAsync()
|
|
{
|
|
await _engine.ForceNextAsync().ConfigureAwait(false);
|
|
RefreshSummary();
|
|
_logService.Info($"[{Title}] 현재 포맷 강제 중지 후 전환");
|
|
}
|
|
|
|
private void AddFormat()
|
|
{
|
|
if (SelectedFormat is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_engine.Enqueue(ChannelScheduleItem.FromTemplate(SelectedFormat));
|
|
RefreshSummary();
|
|
}
|
|
|
|
private void ResetQueue()
|
|
{
|
|
_engine.Reset();
|
|
RefreshSummary();
|
|
_logService.Info($"[{Title}] 스케줄을 첫 포맷부터 다시 시작하도록 초기화했습니다.");
|
|
}
|
|
|
|
private void RemoveItem(ChannelScheduleItem? item)
|
|
{
|
|
if (!_engine.Remove(item))
|
|
{
|
|
_logService.Warning($"[{Title}] 송출 중 포맷은 삭제할 수 없습니다.");
|
|
return;
|
|
}
|
|
|
|
RefreshSummary();
|
|
}
|
|
|
|
private void MoveUp(ChannelScheduleItem? item)
|
|
{
|
|
_engine.MoveUp(item);
|
|
RefreshSummary();
|
|
}
|
|
|
|
private void MoveDown(ChannelScheduleItem? item)
|
|
{
|
|
_engine.MoveDown(item);
|
|
RefreshSummary();
|
|
}
|
|
|
|
private void PromoteToNext(ChannelScheduleItem? item)
|
|
{
|
|
_engine.PromoteToNext(item);
|
|
RefreshSummary();
|
|
}
|
|
|
|
public void RefreshSummary()
|
|
{
|
|
_engine.RefreshQueueMarkers();
|
|
OnPropertyChanged(
|
|
nameof(AdapterState),
|
|
nameof(AdapterStateText),
|
|
nameof(AdapterStateLabel),
|
|
nameof(TransmissionLabel),
|
|
nameof(CurrentItemName),
|
|
nameof(NextItemName),
|
|
nameof(QueuedItemCount),
|
|
nameof(QueueFootnote),
|
|
nameof(QueueSummary),
|
|
nameof(LoopSummary),
|
|
nameof(EmptyBehaviorLabel),
|
|
nameof(OperatorQuickSummary));
|
|
}
|
|
|
|
private SelectionOption<EmptyScheduleBehavior>? FindEmptyBehaviorOption(EmptyScheduleBehavior behavior)
|
|
{
|
|
return EmptyBehaviorOptions.FirstOrDefault(option => option.Value == behavior);
|
|
}
|
|
}
|