Files
Tornado3_2026Election/Tornado3_2026Election/ViewModels/ChannelScheduleViewModel.cs
2026-03-25 17:26:16 +09:00

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);
}
}