256 lines
8.1 KiB
C#
256 lines
8.1 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.UI;
|
|
using Microsoft.UI.Xaml.Media;
|
|
using Tornado3_2026Election.Common;
|
|
using Tornado3_2026Election.Services;
|
|
|
|
namespace Tornado3_2026Election.Domain;
|
|
|
|
public sealed class ChannelScheduleItem : ObservableObject
|
|
{
|
|
private ScheduleQueueItemState _state = ScheduleQueueItemState.Queued;
|
|
private string _lastError = string.Empty;
|
|
private DateTimeOffset? _lastPlayedAt;
|
|
private string _currentRegionLabel = string.Empty;
|
|
private double _defaultCutDurationSeconds;
|
|
private double _draftCutDurationSeconds;
|
|
private int _totalCuts;
|
|
private double _thumbnailWidth = 160;
|
|
private double _thumbnailHeight = 90;
|
|
private ImageSource? _thumbnailSource;
|
|
|
|
public Guid Id { get; set; } = Guid.NewGuid();
|
|
|
|
public required string FormatId { get; init; }
|
|
|
|
public required string FormatName { get; init; }
|
|
|
|
public required string Description { get; init; }
|
|
|
|
public required BroadcastChannel Channel { get; init; }
|
|
|
|
public required bool RequiresImage { get; init; }
|
|
|
|
public required double DefaultCutDurationSeconds
|
|
{
|
|
get => _defaultCutDurationSeconds;
|
|
set
|
|
{
|
|
var hadPendingDurationChange = HasPendingDurationChange;
|
|
var normalized = ScheduleTemplatePolicy.NormalizeCutDurationSeconds(value, Channel, FormatName);
|
|
if (SetProperty(ref _defaultCutDurationSeconds, normalized))
|
|
{
|
|
if (!hadPendingDurationChange || _draftCutDurationSeconds <= 0)
|
|
{
|
|
SetProperty(ref _draftCutDurationSeconds, normalized, nameof(DraftCutDurationSeconds));
|
|
}
|
|
|
|
OnDurationStateChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
public double DraftCutDurationSeconds
|
|
{
|
|
get => _draftCutDurationSeconds <= 0 ? DefaultCutDurationSeconds : _draftCutDurationSeconds;
|
|
set
|
|
{
|
|
var normalized = ScheduleTemplatePolicy.NormalizeCutDurationSeconds(value, Channel, FormatName);
|
|
if (SetProperty(ref _draftCutDurationSeconds, normalized))
|
|
{
|
|
OnDurationStateChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
public required int TotalCuts
|
|
{
|
|
get => _totalCuts;
|
|
set => SetProperty(ref _totalCuts, value);
|
|
}
|
|
|
|
public ScheduleRegionScope RegionScope { get; set; } = ScheduleRegionScope.All;
|
|
|
|
public string ScheduleElectionType { get; set; } = string.Empty;
|
|
|
|
public string RegionLabel { get; set; } = string.Empty;
|
|
|
|
public string RegionCode { get; set; } = string.Empty;
|
|
|
|
public ScheduleQueueItemState State
|
|
{
|
|
get => _state;
|
|
set
|
|
{
|
|
if (SetProperty(ref _state, value))
|
|
{
|
|
OnPropertyChanged(nameof(StateLabel));
|
|
OnPropertyChanged(nameof(StateBrush));
|
|
OnPropertyChanged(nameof(CardOpacity));
|
|
OnPropertyChanged(nameof(CanDelete));
|
|
}
|
|
}
|
|
}
|
|
|
|
public string LastError
|
|
{
|
|
get => _lastError;
|
|
set => SetProperty(ref _lastError, value);
|
|
}
|
|
|
|
public DateTimeOffset? LastPlayedAt
|
|
{
|
|
get => _lastPlayedAt;
|
|
set => SetProperty(ref _lastPlayedAt, value);
|
|
}
|
|
|
|
public string CurrentRegionLabel
|
|
{
|
|
get => _currentRegionLabel;
|
|
set
|
|
{
|
|
if (SetProperty(ref _currentRegionLabel, value))
|
|
{
|
|
OnPropertyChanged(nameof(DisplayRegionLabel));
|
|
OnPropertyChanged(nameof(DisplayName));
|
|
}
|
|
}
|
|
}
|
|
|
|
[JsonIgnore]
|
|
public string StateLabel => State switch
|
|
{
|
|
ScheduleQueueItemState.Next => "다음",
|
|
ScheduleQueueItemState.Sending => "준비",
|
|
ScheduleQueueItemState.OnAir => "송출 중",
|
|
ScheduleQueueItemState.Completed => "완료",
|
|
ScheduleQueueItemState.Error => "오류",
|
|
_ => "대기"
|
|
};
|
|
|
|
[JsonIgnore]
|
|
public SolidColorBrush StateBrush => new(State switch
|
|
{
|
|
ScheduleQueueItemState.Next => ColorHelper.FromArgb(255, 245, 158, 11),
|
|
ScheduleQueueItemState.OnAir => ColorHelper.FromArgb(255, 239, 68, 68),
|
|
_ => ColorHelper.FromArgb(255, 100, 116, 139)
|
|
});
|
|
|
|
[JsonIgnore]
|
|
public double CardOpacity => State == ScheduleQueueItemState.Completed ? 0.45 : 1.0;
|
|
|
|
[JsonIgnore]
|
|
public bool CanDelete => State is not ScheduleQueueItemState.OnAir and not ScheduleQueueItemState.Sending;
|
|
|
|
[JsonIgnore]
|
|
public double MinimumDurationSeconds => ScheduleTemplatePolicy.GetMinimumCutDurationSeconds(Channel, FormatName);
|
|
|
|
[JsonIgnore]
|
|
public bool HasPendingDurationChange => Math.Abs(DraftCutDurationSeconds - DefaultCutDurationSeconds) >= 0.001d;
|
|
|
|
[JsonIgnore]
|
|
public string DurationApplyStatusLabel => HasPendingDurationChange ? "미적용" : "적용됨";
|
|
|
|
[JsonIgnore]
|
|
public string LastPlayedLabel => LastPlayedAt?.ToString("HH:mm:ss") ?? "아직 송출 전";
|
|
|
|
[JsonIgnore]
|
|
public string SelectionRegionLabel => RegionScope switch
|
|
{
|
|
ScheduleRegionScope.All => "전체",
|
|
ScheduleRegionScope.StationRegions => "선택권역",
|
|
ScheduleRegionScope.RegionGroup => string.IsNullOrWhiteSpace(RegionLabel) ? "시도" : RegionLabel,
|
|
_ => string.IsNullOrWhiteSpace(RegionLabel) ? "개별 지역" : RegionLabel
|
|
};
|
|
|
|
[JsonIgnore]
|
|
public string DisplayRegionLabel => string.IsNullOrWhiteSpace(CurrentRegionLabel)
|
|
? SelectionRegionLabel
|
|
: CurrentRegionLabel;
|
|
|
|
[JsonIgnore]
|
|
public string DisplayName => $"{FormatName} / {DisplayRegionLabel}";
|
|
|
|
[JsonIgnore]
|
|
public ImageSource? ThumbnailSource => _thumbnailSource ??= CutThumbnailAssetCatalog.CreateImageSource(FormatId);
|
|
|
|
[JsonIgnore]
|
|
public bool HasThumbnail => CutThumbnailAssetCatalog.HasThumbnail(FormatId);
|
|
|
|
[JsonIgnore]
|
|
public double ThumbnailWidth
|
|
{
|
|
get => _thumbnailWidth;
|
|
private set => SetProperty(ref _thumbnailWidth, value);
|
|
}
|
|
|
|
[JsonIgnore]
|
|
public double ThumbnailHeight
|
|
{
|
|
get => _thumbnailHeight;
|
|
private set => SetProperty(ref _thumbnailHeight, value);
|
|
}
|
|
|
|
[JsonIgnore]
|
|
public string ThumbnailStatusLabel => HasThumbnail ? "등록 썸네일" : "기본 아이콘";
|
|
|
|
public void RefreshThumbnail()
|
|
{
|
|
_thumbnailSource = CutThumbnailAssetCatalog.CreateImageSource(FormatId);
|
|
OnPropertyChanged(nameof(ThumbnailSource));
|
|
OnPropertyChanged(nameof(HasThumbnail));
|
|
OnPropertyChanged(nameof(ThumbnailStatusLabel));
|
|
}
|
|
|
|
public void UpdateThumbnailLayout(ThumbnailDisplayMetrics metrics)
|
|
{
|
|
ThumbnailWidth = metrics.Width;
|
|
ThumbnailHeight = metrics.Height;
|
|
}
|
|
|
|
public void StepDraftDuration(double deltaSeconds)
|
|
{
|
|
DraftCutDurationSeconds = DraftCutDurationSeconds + deltaSeconds;
|
|
}
|
|
|
|
public void ApplyDraftDuration()
|
|
{
|
|
DefaultCutDurationSeconds = DraftCutDurationSeconds;
|
|
OnDurationStateChanged();
|
|
}
|
|
|
|
private void OnDurationStateChanged()
|
|
{
|
|
OnPropertyChanged(nameof(HasPendingDurationChange));
|
|
OnPropertyChanged(nameof(DurationApplyStatusLabel));
|
|
}
|
|
|
|
public static ChannelScheduleItem FromTemplate(FormatTemplateDefinition template, ScheduleRegionOption? regionOption = null)
|
|
{
|
|
var selectedRegion = regionOption ?? new ScheduleRegionOption
|
|
{
|
|
Scope = ScheduleRegionScope.All,
|
|
Label = "전체"
|
|
};
|
|
|
|
return new ChannelScheduleItem
|
|
{
|
|
FormatId = template.Id,
|
|
FormatName = template.Name,
|
|
Description = template.Description,
|
|
Channel = template.RecommendedChannel,
|
|
RequiresImage = template.RequiresImage,
|
|
DefaultCutDurationSeconds = template.Cuts.First().DurationSeconds,
|
|
TotalCuts = template.Cuts.Count,
|
|
RegionScope = selectedRegion.Scope,
|
|
ScheduleElectionType = selectedRegion.ElectionType,
|
|
RegionLabel = selectedRegion.Scope is ScheduleRegionScope.Single or ScheduleRegionScope.RegionGroup
|
|
? selectedRegion.Label
|
|
: string.Empty,
|
|
RegionCode = selectedRegion.DistrictCode
|
|
};
|
|
}
|
|
}
|