초기 커밋.
This commit is contained in:
277
Tornado3_2026Election/ViewModels/ChannelScheduleViewModel.cs
Normal file
277
Tornado3_2026Election/ViewModels/ChannelScheduleViewModel.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user