1794 lines
75 KiB
C#
1794 lines
75 KiB
C#
using System.Runtime.InteropServices;
|
|
using MBN_STOCK_WEBVIEW.Infrastructure;
|
|
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
|
using MBN_STOCK_WEBVIEW.LegacyBridge;
|
|
using MBN_STOCK_WEBVIEW.Playout.Safety;
|
|
using Microsoft.UI.Windowing;
|
|
using Microsoft.Web.WebView2.Core;
|
|
using MMoneyCoderSharp.Data;
|
|
using Windows.Graphics;
|
|
using WinRT.Interop;
|
|
|
|
namespace MBN_STOCK_WEBVIEW.LegacyParityApp;
|
|
|
|
public sealed partial class MainWindow : Window
|
|
{
|
|
private const int SmCxDrag = 68;
|
|
private const int SmCyDrag = 69;
|
|
private const uint WmSettingChange = 0x001A;
|
|
private const uint WmDisplayChange = 0x007E;
|
|
private const uint WmDpiChanged = 0x02E0;
|
|
private const nuint InteractionMetricsSubclassId = 0x4D424E49;
|
|
|
|
private readonly CancellationTokenSource _lifetimeCancellation = new();
|
|
private readonly SemaphoreSlim _intentGate = new(1, 1);
|
|
private readonly SemaphoreSlim _orderedPlayoutIntentGate = new(1, 1);
|
|
private readonly PlayoutLaunchAuthorization _playoutLaunchAuthorization;
|
|
private readonly WindowSubclassProcedure _windowSubclassProcedure;
|
|
private readonly LegacyOperatorController _controller;
|
|
private readonly LegacyIndustrySelectionWorkflow _industryWorkflow;
|
|
private DatabaseRuntime? _databaseRuntime;
|
|
private bool _closing;
|
|
private bool _interactionMetricsRefreshQueued;
|
|
private bool _intentBusy;
|
|
private bool _windowSubclassAttached;
|
|
private bool _webViewReady;
|
|
private nint _windowHandle;
|
|
private LegacyInteractionMetrics _interactionMetrics =
|
|
LegacyInteractionMetrics.Default;
|
|
|
|
public MainWindow()
|
|
{
|
|
// Capture and remove Gate A bearer material before XAML can create WebView2.
|
|
// Only the native process keeps the capability digest for this launch.
|
|
_playoutLaunchAuthorization =
|
|
PlayoutLaunchAuthorization.CaptureFromEnvironment();
|
|
_windowSubclassProcedure = OnWindowSubclassMessage;
|
|
InitializeComponent();
|
|
EnsureCloseConfirmationAttached();
|
|
var localApplicationData = Environment.GetFolderPath(
|
|
Environment.SpecialFolder.LocalApplicationData);
|
|
var runtimeCutMenuOverridePath = string.IsNullOrWhiteSpace(localApplicationData)
|
|
? null
|
|
: Path.Combine(
|
|
localApplicationData,
|
|
"MBN_STOCK_WEBVIEW",
|
|
"Res",
|
|
"종목.ini");
|
|
var cutMenuComposition = LegacyRuntimeStockCutMenuLoader.ResolveFromCandidates(
|
|
runtimeCutMenuOverridePath,
|
|
AppContext.BaseDirectory);
|
|
var runtimeUiCatalogs = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
|
|
AppContext.BaseDirectory);
|
|
|
|
ILegacyStockLookup stockLookup;
|
|
IIndustrySelectionService industrySelectionService;
|
|
IThemeSelectionService themeSelectionService;
|
|
IExpertSelectionService expertSelectionService;
|
|
ITradingHaltSelectionService tradingHaltSelectionService;
|
|
IOverseasIndustryIndexSearchService overseasIndustrySearchService;
|
|
IOverseasStockSearchService overseasStockSearchService;
|
|
IWorldStockSearchService worldStockSearchService;
|
|
IManualFinancialScreenService manualFinancialService;
|
|
IStockSearchService manualFinancialStockSearchService;
|
|
INamedPlaylistPersistenceService namedPlaylistPersistenceService;
|
|
IThemeCatalogPersistenceService themeCatalogPersistenceService;
|
|
IExpertCatalogPersistenceService expertCatalogPersistenceService;
|
|
IStockMasterIdentityValidationService stockMasterIdentityValidationService;
|
|
IOperatorCatalogSchemaValidationService operatorCatalogSchemaValidationService;
|
|
string? initializationError = null;
|
|
try
|
|
{
|
|
_databaseRuntime = DatabaseRuntime.CreateLegacyExecutableDefault(
|
|
AppContext.BaseDirectory);
|
|
DataQueryExecutor.Configure(_databaseRuntime.Executor);
|
|
var databaseMutationAuthorization =
|
|
new LaunchDatabaseMutationAuthorization(_playoutLaunchAuthorization);
|
|
stockLookup = new LegacyParityStockSearchService(_databaseRuntime.Executor);
|
|
industrySelectionService = new LegacyIndustrySelectionService(
|
|
_databaseRuntime.Executor);
|
|
themeSelectionService = new LegacyThemeSelectionService(
|
|
_databaseRuntime.Executor);
|
|
expertSelectionService = new LegacyExpertSelectionService(
|
|
_databaseRuntime.Executor);
|
|
tradingHaltSelectionService = new LegacyTradingHaltSelectionService(
|
|
_databaseRuntime.Executor);
|
|
overseasIndustrySearchService = new LegacyOverseasIndustryIndexSearchService(
|
|
_databaseRuntime.Executor);
|
|
overseasStockSearchService = new LegacyOverseasStockSearchService(
|
|
_databaseRuntime.Executor);
|
|
worldStockSearchService = new LegacyWorldStockSearchService(
|
|
_databaseRuntime.Executor);
|
|
manualFinancialService = new LegacyManualFinancialScreenService(
|
|
_databaseRuntime.Executor,
|
|
new OracleManualFinancialMutationExecutor(
|
|
_databaseRuntime.ConnectionFactory,
|
|
_databaseRuntime.Options.Resilience,
|
|
errorDetector: null,
|
|
mutationAuthorization: databaseMutationAuthorization));
|
|
manualFinancialStockSearchService = new LegacyStockSearchService(
|
|
_databaseRuntime.Executor);
|
|
namedPlaylistPersistenceService = new LegacyNamedPlaylistPersistenceService(
|
|
_databaseRuntime.Executor,
|
|
new OracleNamedPlaylistMutationExecutor(
|
|
_databaseRuntime.ConnectionFactory,
|
|
_databaseRuntime.Options.Resilience,
|
|
errorDetector: null,
|
|
mutationAuthorization: databaseMutationAuthorization));
|
|
var operatorCatalogMutationExecutor = new OperatorCatalogMutationExecutor(
|
|
_databaseRuntime.ConnectionFactory,
|
|
_databaseRuntime.Options.Resilience,
|
|
errorDetector: null,
|
|
mutationAuthorization: databaseMutationAuthorization);
|
|
themeCatalogPersistenceService = new LegacyThemeCatalogPersistenceService(
|
|
_databaseRuntime.Executor,
|
|
operatorCatalogMutationExecutor);
|
|
expertCatalogPersistenceService = new LegacyExpertCatalogPersistenceService(
|
|
_databaseRuntime.Executor,
|
|
operatorCatalogMutationExecutor);
|
|
stockMasterIdentityValidationService =
|
|
new LegacyStockMasterIdentityValidationService(_databaseRuntime.Executor);
|
|
operatorCatalogSchemaValidationService =
|
|
new LegacyOperatorCatalogSchemaValidationService(_databaseRuntime.Executor);
|
|
}
|
|
catch
|
|
{
|
|
initializationError =
|
|
"데이터베이스가 설정되지 않았습니다. 로컬 설정을 확인하세요.";
|
|
stockLookup = new UnavailableStockLookup(initializationError);
|
|
industrySelectionService = new UnavailableIndustrySelectionService(
|
|
initializationError);
|
|
themeSelectionService = new UnavailableThemeSelectionService(
|
|
initializationError);
|
|
expertSelectionService = new UnavailableExpertSelectionService(
|
|
initializationError);
|
|
tradingHaltSelectionService = new UnavailableTradingHaltSelectionService(
|
|
initializationError);
|
|
overseasIndustrySearchService = new UnavailableOverseasIndustryIndexSearchService(
|
|
initializationError);
|
|
overseasStockSearchService = new UnavailableOverseasStockSearchService(
|
|
initializationError);
|
|
worldStockSearchService = new UnavailableWorldStockSearchService(
|
|
initializationError);
|
|
manualFinancialService = new UnavailableManualFinancialScreenService(
|
|
initializationError);
|
|
manualFinancialStockSearchService = new UnavailableStockSearchService(
|
|
initializationError);
|
|
namedPlaylistPersistenceService = new UnavailableNamedPlaylistPersistenceService(
|
|
initializationError);
|
|
themeCatalogPersistenceService = new UnavailableThemeCatalogPersistenceService(
|
|
initializationError);
|
|
expertCatalogPersistenceService = new UnavailableExpertCatalogPersistenceService(
|
|
initializationError);
|
|
stockMasterIdentityValidationService =
|
|
new UnavailableStockMasterIdentityValidationService(initializationError);
|
|
operatorCatalogSchemaValidationService =
|
|
new UnavailableOperatorCatalogSchemaValidationService(initializationError);
|
|
DataQueryExecutor.Reset();
|
|
}
|
|
|
|
_industryWorkflow = new LegacyIndustrySelectionWorkflow(
|
|
industrySelectionService,
|
|
actionLayout: runtimeUiCatalogs.IndustryLayout);
|
|
var overseasWorkflow = new LegacyOverseasSelectionWorkflow(
|
|
overseasIndustrySearchService,
|
|
overseasStockSearchService);
|
|
var comparisonStore = new LegacyComparisonPairFileStore(
|
|
Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
"MBN_STOCK_WEBVIEW",
|
|
"Data",
|
|
"종목비교.dat"),
|
|
Path.Combine(AppContext.BaseDirectory, "Data", "종목비교.dat"));
|
|
var manualListsWorkflow = CreateManualListsWorkflow(stockLookup);
|
|
_controller = new LegacyOperatorController(
|
|
stockLookup,
|
|
fixedCatalog: runtimeUiCatalogs.FixedCatalog,
|
|
industryWorkflow: _industryWorkflow,
|
|
themeWorkflow: new LegacyThemeWorkflow(themeSelectionService),
|
|
expertWorkflow: new LegacyExpertWorkflowController(expertSelectionService),
|
|
tradingHaltWorkflow: new LegacyTradingHaltWorkflowController(
|
|
tradingHaltSelectionService),
|
|
comparisonWorkflow: new LegacyComparisonWorkflowController(
|
|
new LegacyComparisonDataService(stockLookup, worldStockSearchService),
|
|
comparisonStore,
|
|
runtimeUiCatalogs.ComparisonLayout),
|
|
manualFinancialWorkflow: new LegacyManualFinancialWorkflow(
|
|
manualFinancialService,
|
|
manualFinancialStockSearchService),
|
|
namedPlaylistWorkflow: new LegacyNamedPlaylistWorkflowController(
|
|
namedPlaylistPersistenceService),
|
|
overseasWorkflow: overseasWorkflow,
|
|
manualListsWorkflow: manualListsWorkflow,
|
|
operatorCatalogWorkflow: new LegacyOperatorCatalogWorkflowController(
|
|
themeSelectionService,
|
|
expertSelectionService,
|
|
themeCatalogPersistenceService,
|
|
expertCatalogPersistenceService,
|
|
manualFinancialStockSearchService,
|
|
stockMasterIdentityValidationService,
|
|
operatorCatalogSchemaValidationService),
|
|
cutRows: cutMenuComposition.Rows);
|
|
if (initializationError is not null)
|
|
{
|
|
_controller.ReportError(initializationError);
|
|
}
|
|
else if (new[]
|
|
{
|
|
cutMenuComposition.WarningMessage,
|
|
runtimeUiCatalogs.WarningMessage
|
|
}
|
|
.Where(message => message is not null)
|
|
.ToArray() is { Length: > 0 } catalogWarnings)
|
|
{
|
|
_controller.ReportWarning(string.Join(" ", catalogWarnings));
|
|
}
|
|
|
|
InitializePlayoutRuntime();
|
|
|
|
Root.Loaded += OnRootLoaded;
|
|
Closed += OnClosed;
|
|
}
|
|
|
|
private async void OnRootLoaded(object sender, RoutedEventArgs e)
|
|
{
|
|
Root.Loaded -= OnRootLoaded;
|
|
if (_closing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ConfigureWindow();
|
|
await InitializeWebViewAsync();
|
|
if (!_closing)
|
|
{
|
|
StartDatabaseHealthMonitor();
|
|
await ConnectPlayoutAsync(_lifetimeCancellation.Token);
|
|
}
|
|
}
|
|
|
|
private void ConfigureWindow()
|
|
{
|
|
EnsureCloseConfirmationAttached();
|
|
var windowHandle = WindowNative.GetWindowHandle(this);
|
|
_windowHandle = windowHandle;
|
|
AttachInteractionMetricsRefresh();
|
|
RefreshInteractionMetrics();
|
|
var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(windowHandle);
|
|
var appWindow = AppWindow.GetFromWindowId(windowId);
|
|
appWindow.Title = "V-Stock 증권정보송출시스템 for 매일경제TV (26.03.26)";
|
|
|
|
if (appWindow.Presenter is OverlappedPresenter presenter)
|
|
{
|
|
presenter.IsMaximizable = false;
|
|
presenter.IsResizable = false;
|
|
}
|
|
|
|
// WinForms stored 1905 x 1015 as ClientSize. AppWindow.Resize would include
|
|
// the caption and borders and silently make the WebView viewport smaller.
|
|
appWindow.ResizeClient(new SizeInt32(1905, 1015));
|
|
var displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Primary);
|
|
appWindow.Move(new PointInt32(displayArea.WorkArea.X, displayArea.WorkArea.Y));
|
|
}
|
|
|
|
private async Task InitializeWebViewAsync()
|
|
{
|
|
try
|
|
{
|
|
var webRoot = Path.Combine(AppContext.BaseDirectory, "Web");
|
|
var startPage = Path.Combine(webRoot, "index.html");
|
|
if (!File.Exists(startPage))
|
|
{
|
|
throw new FileNotFoundException("The parity Web UI was not found.", startPage);
|
|
}
|
|
|
|
await Browser.EnsureCoreWebView2Async();
|
|
if (_closing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var coreWebView = Browser.CoreWebView2 ??
|
|
throw new InvalidOperationException("WebView2 was not initialized.");
|
|
coreWebView.SetVirtualHostNameToFolderMapping(
|
|
LegacyBridgeProtocol.TrustedHost,
|
|
webRoot,
|
|
CoreWebView2HostResourceAccessKind.DenyCors);
|
|
coreWebView.Settings.AreDevToolsEnabled =
|
|
System.Diagnostics.Debugger.IsAttached;
|
|
coreWebView.Settings.AreBrowserAcceleratorKeysEnabled = false;
|
|
coreWebView.Settings.AreDefaultContextMenusEnabled = false;
|
|
coreWebView.Settings.IsStatusBarEnabled = false;
|
|
coreWebView.Settings.IsZoomControlEnabled = false;
|
|
coreWebView.Settings.IsPinchZoomEnabled = false;
|
|
coreWebView.NavigationStarting += OnNavigationStarting;
|
|
coreWebView.NavigationCompleted += OnNavigationCompleted;
|
|
coreWebView.NewWindowRequested += OnNewWindowRequested;
|
|
coreWebView.WebMessageReceived += OnWebMessageReceived;
|
|
coreWebView.ProcessFailed += OnProcessFailed;
|
|
|
|
_webViewReady = true;
|
|
Browser.Source = new Uri(
|
|
$"https://{LegacyBridgeProtocol.TrustedHost}/index.html");
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
if (!_closing)
|
|
{
|
|
ShowError("WebView2 초기화에 실패했습니다.", exception.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnNavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs args)
|
|
{
|
|
if (!LegacyBridgeProtocol.IsTrustedSource(args.Uri) &&
|
|
!string.Equals(args.Uri, "about:blank", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
args.Cancel = true;
|
|
}
|
|
}
|
|
|
|
private void OnNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs args)
|
|
{
|
|
if (_closing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LoadingOverlay.Visibility = Visibility.Collapsed;
|
|
if (!args.IsSuccess)
|
|
{
|
|
ShowError("원본 호환 화면을 불러오지 못했습니다.", args.WebErrorStatus.ToString());
|
|
}
|
|
}
|
|
|
|
private static void OnNewWindowRequested(
|
|
object? sender,
|
|
CoreWebView2NewWindowRequestedEventArgs args)
|
|
{
|
|
args.Handled = true;
|
|
}
|
|
|
|
private void OnWebMessageReceived(
|
|
object? sender,
|
|
CoreWebView2WebMessageReceivedEventArgs args)
|
|
{
|
|
if (_closing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LegacyUiIntent? intent = null;
|
|
string? parseError = null;
|
|
if (!LegacyBridgeProtocol.IsTrustedSource(args.Source) ||
|
|
!LegacyBridgeProtocol.TryParseIntent(
|
|
args.WebMessageAsJson,
|
|
out intent,
|
|
out parseError) ||
|
|
intent is null)
|
|
{
|
|
if (!string.IsNullOrEmpty(parseError))
|
|
{
|
|
PostState(_controller.ReportError(parseError));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
_ = ProcessIntentAsync(intent);
|
|
}
|
|
|
|
private async Task ProcessIntentAsync(LegacyUiIntent intent)
|
|
{
|
|
var gateEntered = false;
|
|
var orderedPlayoutIntentEntered = false;
|
|
var databaseGateEntered = false;
|
|
var dispatchStarted = false;
|
|
try
|
|
{
|
|
// MainForm performed database search synchronously. The busy snapshot
|
|
// freezes the Web controls. Playout commands must retain their order behind
|
|
// a preceding local row-selection intent, but only one may wait or execute
|
|
// at a time so a double post can never become a queued vendor command.
|
|
if (IsOrderedPlayoutIntent(intent))
|
|
{
|
|
orderedPlayoutIntentEntered = await _orderedPlayoutIntentGate.WaitAsync(
|
|
0,
|
|
_lifetimeCancellation.Token);
|
|
if (orderedPlayoutIntentEntered)
|
|
{
|
|
await _intentGate.WaitAsync(_lifetimeCancellation.Token);
|
|
gateEntered = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Keep the zero-timeout admission used by database and edit intents;
|
|
// those requests must not build an unbounded queue behind slow I/O.
|
|
gateEntered = await _intentGate.WaitAsync(
|
|
0,
|
|
_lifetimeCancellation.Token);
|
|
}
|
|
|
|
if (!gateEntered)
|
|
{
|
|
PostState(IsNamedPlaylistModalIntent(intent)
|
|
? ReportIntentFailure(
|
|
intent,
|
|
"다른 요청을 처리 중이어서 DB 재생목록 요청을 시작하지 않았습니다.")
|
|
: _controller.Current);
|
|
return;
|
|
}
|
|
|
|
if (_closing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_playoutLaunchAuthorization.IsGateA &&
|
|
IsPersistentWriteIntent(intent))
|
|
{
|
|
PostState(ReportIntentFailure(
|
|
intent,
|
|
"Gate A 검증 회차에서는 DB 및 로컬 영구 저장 변경이 허용되지 않습니다."));
|
|
return;
|
|
}
|
|
|
|
if (_controller.Current.Dialog?.IsConfirmation == true &&
|
|
intent is not LegacyReadyIntent and
|
|
not LegacyConfirmNativeDialogIntent and
|
|
not LegacyCancelNativeDialogIntent)
|
|
{
|
|
// A native MessageBox is modal in the original application. Ignore
|
|
// every forged or already-posted background intent until the
|
|
// operator answers the currently visible Yes/No question.
|
|
PostState(_controller.Current);
|
|
return;
|
|
}
|
|
|
|
if (IsOperatorMutationIntent(intent) && IsOperatorMutationQuarantined())
|
|
{
|
|
PostState(ReportIntentFailure(
|
|
intent,
|
|
"이전 저장 결과가 불명확하여 이번 실행의 추가 변경이 차단되었습니다. DB 또는 로컬 파일을 읽기 전용으로 대조한 뒤 앱을 다시 시작하세요."));
|
|
return;
|
|
}
|
|
|
|
if (_controller.Current.FixedSectionBatch is not null &&
|
|
IsOperatorMutationIntent(intent) &&
|
|
!IsFixedSectionBatchMutationIntent(intent))
|
|
{
|
|
PostState(ReportIntentFailure(
|
|
intent,
|
|
"고정 컷 섹션의 수동 입력 중에는 현재 입력 단계만 변경할 수 있습니다. 완료하거나 취소한 뒤 다른 편성 작업을 실행하세요."));
|
|
return;
|
|
}
|
|
|
|
if (IsOperatorMutationIntent(intent) && !CanAcceptOperatorMutation())
|
|
{
|
|
PostState(ReportIntentFailure(
|
|
intent,
|
|
"PREPARE 또는 송출 중에는 플레이리스트와 운영 데이터를 변경할 수 없습니다. TAKE OUT 후 다시 시도하세요."));
|
|
return;
|
|
}
|
|
|
|
if ((intent is LegacySearchStocksIntent searchIntent &&
|
|
!(searchIntent.Trigger == LegacySearchTrigger.Button &&
|
|
searchIntent.Text.Length == 0)) ||
|
|
intent is LegacySelectTabIntent or
|
|
LegacySearchThemesIntent or LegacySelectThemeIntent or
|
|
LegacyActivateThemeResultIntent or
|
|
LegacySearchExpertsIntent or LegacySelectExpertIntent or
|
|
LegacySearchTradingHaltsIntent or
|
|
LegacySearchComparisonDomesticIntent or
|
|
LegacySearchComparisonWorldIntent or
|
|
LegacySearchOverseasIndustriesIntent or
|
|
LegacySearchOverseasStocksIntent or
|
|
LegacyOpenOperatorCatalogIntent or
|
|
LegacyBeginUc4ThemeCatalogIntent or
|
|
LegacyEditUc4SelectedThemeIntent or
|
|
LegacyBeginUc6ExpertCatalogIntent or
|
|
LegacyEditUc6SelectedExpertIntent or
|
|
LegacySearchOperatorCatalogIntent or
|
|
LegacySelectOperatorCatalogResultIntent or
|
|
LegacyBeginCreateThemeCatalogIntent or
|
|
LegacyBeginCreateExpertCatalogIntent or
|
|
LegacySearchOperatorCatalogStocksIntent or
|
|
LegacyActivateOperatorCatalogStockIntent or
|
|
LegacySetOperatorCatalogDraftBuyAmountIntent or
|
|
LegacyCheckOperatorCatalogThemeNameIntent or
|
|
LegacySaveOperatorCatalogIntent or
|
|
LegacyConfirmNativeDialogIntent or
|
|
LegacyAddComparisonPairIntent or
|
|
LegacyMoveComparisonPairIntent or
|
|
LegacyDeleteSelectedComparisonPairIntent or
|
|
LegacyDeleteAllComparisonPairsIntent or
|
|
LegacyOpenManualFinancialIntent or
|
|
LegacySearchManualFinancialIntent or
|
|
LegacyFindManualFinancialIntent or
|
|
LegacyLoadManualFinancialIntent or
|
|
LegacyActivateManualFinancialResultIntent or
|
|
LegacySearchManualFinancialStocksIntent or
|
|
LegacySaveManualFinancialIntent or
|
|
LegacyDeleteManualFinancialIntent or
|
|
LegacyDeleteAllManualFinancialIntent or
|
|
LegacyOpenManualListIntent or
|
|
LegacyRefreshManualListIntent or
|
|
LegacySaveManualNetSellIntent or
|
|
LegacySearchManualViIntent or
|
|
LegacySaveManualViIntent or
|
|
LegacyImportManualListsIntent or
|
|
LegacyRefreshNamedPlaylistsIntent or
|
|
LegacyLoadSelectedNamedPlaylistIntent or
|
|
LegacyLoadNamedPlaylistByIdIntent or
|
|
LegacyCreateNamedPlaylistIntent or
|
|
LegacySaveCurrentNamedPlaylistIntent or
|
|
LegacySaveCurrentNamedPlaylistToIntent or
|
|
LegacyDeleteSelectedNamedPlaylistIntent or
|
|
LegacyExecutePlayoutIntent or
|
|
LegacyGateAPrepareIntent or
|
|
LegacySetFadeDurationIntent or
|
|
LegacyToggleBackgroundIntent or
|
|
LegacyChooseBackgroundIntent)
|
|
{
|
|
_intentBusy = true;
|
|
PostState(_controller.Current);
|
|
}
|
|
|
|
await AcquirePriorityDatabaseActivityAsync(_lifetimeCancellation.Token);
|
|
databaseGateEntered = true;
|
|
if (_closing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
dispatchStarted = true;
|
|
var state = await HandleIntentAsync(intent, _lifetimeCancellation.Token);
|
|
ObserveOperatorMutationQuarantine(state);
|
|
if (!_closing)
|
|
{
|
|
_intentBusy = false;
|
|
PostState(state);
|
|
}
|
|
}
|
|
catch (OperationCanceledException) when (_lifetimeCancellation.IsCancellationRequested)
|
|
{
|
|
}
|
|
catch when (_closing)
|
|
{
|
|
}
|
|
catch
|
|
{
|
|
_intentBusy = false;
|
|
var failureState = ReportIntentFailure(
|
|
intent,
|
|
"요청을 처리하지 못했습니다. 데이터베이스 연결 상태를 확인하세요.",
|
|
writeOutcomeMayBeUnknown: dispatchStarted);
|
|
ObserveOperatorMutationQuarantine(failureState);
|
|
PostState(failureState);
|
|
}
|
|
finally
|
|
{
|
|
if (_closing)
|
|
{
|
|
_intentBusy = false;
|
|
}
|
|
|
|
if (databaseGateEntered)
|
|
{
|
|
_databaseActivityGate.Release();
|
|
}
|
|
|
|
if (gateEntered)
|
|
{
|
|
_intentGate.Release();
|
|
}
|
|
|
|
if (orderedPlayoutIntentEntered)
|
|
{
|
|
_orderedPlayoutIntentGate.Release();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool IsOrderedPlayoutIntent(LegacyUiIntent intent) => intent is
|
|
LegacyExecutePlayoutIntent or LegacyGateAPrepareIntent;
|
|
|
|
private static bool IsNamedPlaylistModalIntent(LegacyUiIntent intent) =>
|
|
intent is LegacyRefreshNamedPlaylistsIntent or
|
|
LegacySelectNamedPlaylistIntent or
|
|
LegacyLoadSelectedNamedPlaylistIntent or
|
|
LegacyLoadNamedPlaylistByIdIntent or
|
|
LegacyCreateNamedPlaylistIntent or
|
|
LegacySaveCurrentNamedPlaylistIntent or
|
|
LegacySaveCurrentNamedPlaylistToIntent or
|
|
LegacyDeleteSelectedNamedPlaylistIntent;
|
|
|
|
private LegacyOperatorSnapshot ReportIntentFailure(
|
|
LegacyUiIntent intent,
|
|
string message,
|
|
bool writeOutcomeMayBeUnknown = false) =>
|
|
intent switch
|
|
{
|
|
LegacyRefreshNamedPlaylistsIntent refresh
|
|
when !string.IsNullOrWhiteSpace(refresh.RequestId) =>
|
|
_controller.ReportNamedPlaylistCommandFailure(
|
|
"refresh-named-playlists",
|
|
refresh.RequestId,
|
|
message),
|
|
LegacySelectNamedPlaylistIntent selection
|
|
when !string.IsNullOrWhiteSpace(selection.RequestId) =>
|
|
_controller.ReportNamedPlaylistCommandFailure(
|
|
"select-named-playlist",
|
|
selection.RequestId,
|
|
message),
|
|
LegacyCreateNamedPlaylistIntent create
|
|
when writeOutcomeMayBeUnknown &&
|
|
!string.IsNullOrWhiteSpace(create.RequestId) =>
|
|
_controller.ReportNamedPlaylistWriteOutcomeUnknown(
|
|
"create-named-playlist",
|
|
create.RequestId,
|
|
message),
|
|
LegacyCreateNamedPlaylistIntent create
|
|
when !string.IsNullOrWhiteSpace(create.RequestId) =>
|
|
_controller.ReportNamedPlaylistCommandFailure(
|
|
"create-named-playlist",
|
|
create.RequestId,
|
|
message),
|
|
LegacyDeleteSelectedNamedPlaylistIntent delete
|
|
when writeOutcomeMayBeUnknown &&
|
|
!string.IsNullOrWhiteSpace(delete.RequestId) =>
|
|
_controller.ReportNamedPlaylistWriteOutcomeUnknown(
|
|
"delete-selected-named-playlist",
|
|
delete.RequestId,
|
|
message),
|
|
LegacyDeleteSelectedNamedPlaylistIntent delete
|
|
when !string.IsNullOrWhiteSpace(delete.RequestId) =>
|
|
_controller.ReportNamedPlaylistCommandFailure(
|
|
"delete-selected-named-playlist",
|
|
delete.RequestId,
|
|
message),
|
|
LegacyLoadNamedPlaylistByIdIntent load
|
|
when !string.IsNullOrWhiteSpace(load.DefinitionId) =>
|
|
_controller.ReportNamedPlaylistCommandFailure(
|
|
"load-named-playlist-by-id",
|
|
load.DefinitionId,
|
|
message),
|
|
LegacySaveCurrentNamedPlaylistToIntent save
|
|
when writeOutcomeMayBeUnknown &&
|
|
!string.IsNullOrWhiteSpace(save.DefinitionId) =>
|
|
_controller.ReportNamedPlaylistWriteOutcomeUnknown(
|
|
"save-current-named-playlist-to",
|
|
save.DefinitionId,
|
|
message),
|
|
LegacySaveCurrentNamedPlaylistToIntent save
|
|
when !string.IsNullOrWhiteSpace(save.DefinitionId) =>
|
|
_controller.ReportNamedPlaylistCommandFailure(
|
|
"save-current-named-playlist-to",
|
|
save.DefinitionId,
|
|
message),
|
|
_ => _controller.ReportError(message)
|
|
};
|
|
|
|
private async Task<LegacyOperatorSnapshot> HandleIntentAsync(
|
|
LegacyUiIntent intent,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return intent switch
|
|
{
|
|
LegacyReadyIntent => _controller.Current,
|
|
LegacySearchStocksIntent search => await _controller.SearchStocksAsync(
|
|
search.Text,
|
|
search.Trigger,
|
|
cancellationToken),
|
|
LegacySelectStockIntent selection =>
|
|
_controller.SelectStock(selection.ResultIndex),
|
|
LegacyCutPointerDownIntent pointer => _controller.CutPointerDown(
|
|
pointer.PhysicalIndex,
|
|
pointer.Control,
|
|
pointer.Shift,
|
|
pointer.X,
|
|
pointer.Y,
|
|
pointer.TimestampMilliseconds,
|
|
pointer.AllowAppend),
|
|
LegacyCutKeyDownIntent key => _controller.CutKeyDown(
|
|
key.Key,
|
|
key.Control,
|
|
key.Shift),
|
|
LegacyClearCutSelectionIntent => _controller.ClearCutSelection(),
|
|
LegacyDropSelectedCutsIntent => _controller.DropSelectedCutsOnPlaylist(),
|
|
LegacySetPlaylistEnabledIntent enabled =>
|
|
_controller.SetPlaylistRowEnabled(enabled.RowId, enabled.Enabled),
|
|
LegacySetAllPlaylistEnabledIntent enabled =>
|
|
_controller.SetAllPlaylistRowsEnabled(enabled.Enabled),
|
|
LegacySelectPlaylistRowIntent selection =>
|
|
_controller.SelectPlaylistRow(
|
|
selection.RowId,
|
|
selection.Control,
|
|
selection.Shift),
|
|
LegacyActivatePlaylistRowIntent activation =>
|
|
_controller.ActivatePlaylistRow(activation.RowId),
|
|
LegacyMoveSelectedPlaylistRowsIntent move =>
|
|
_controller.MoveSelectedPlaylistRows(move.Direction),
|
|
LegacyReorderPlaylistRowsIntent reorder =>
|
|
_controller.ReorderPlaylistRows(
|
|
reorder.SourceRowId,
|
|
reorder.TargetRowId,
|
|
reorder.Position),
|
|
LegacyDeleteSelectedPlaylistRowsIntent =>
|
|
_controller.DeleteSelectedPlaylistRows(),
|
|
LegacyDeleteAllPlaylistRowsIntent =>
|
|
_controller.DeleteAllPlaylistRows(),
|
|
LegacyToggleActivePlaylistEnabledIntent =>
|
|
_controller.ToggleActivePlaylistRowEnabled(),
|
|
LegacySelectPlaylistBoundaryIntent boundary =>
|
|
SelectPlaylistBoundaryAndStopRefresh(boundary.Last),
|
|
LegacySelectTabIntent tab =>
|
|
await _controller.SelectTabAsync(tab.TabId, cancellationToken),
|
|
LegacySwapTabsIntent tabs =>
|
|
await _controller.SwapTabsAsync(
|
|
tabs.Source,
|
|
tabs.Target,
|
|
cancellationToken),
|
|
LegacyHoverSwapTabsIntent tabs =>
|
|
await _controller.SwapTabsAtExpectedPositionsAsync(
|
|
tabs.Source,
|
|
tabs.Target,
|
|
tabs.ExpectedSourceIndex,
|
|
tabs.ExpectedTargetIndex,
|
|
cancellationToken),
|
|
LegacySetMovingAveragesIntent movingAverages =>
|
|
_controller.SetMovingAverages(movingAverages.Ma5, movingAverages.Ma20),
|
|
LegacyActivateFixedActionIntent action =>
|
|
await _controller.ActivateFixedActionAsync(
|
|
action.ActionId,
|
|
cancellationToken),
|
|
LegacyActivateFixedSectionIntent section =>
|
|
await _controller.ActivateFixedSection(
|
|
section.SectionIndex,
|
|
cancellationToken),
|
|
LegacySelectIndustryIntent industry =>
|
|
_controller.SelectIndustry(industry.Index, industry.DoubleClick),
|
|
LegacySetIndustryComparisonTextIntent edit =>
|
|
_controller.SetIndustryComparisonText(edit.Slot, edit.Value),
|
|
LegacySwapIndustriesIntent =>
|
|
_controller.SwapIndustries(),
|
|
LegacyActivateIndustryActionIntent action =>
|
|
_controller.ActivateIndustryAction(action.ActionId),
|
|
LegacyActivateIndustrySectionIntent section =>
|
|
_controller.ActivateIndustrySection(section.SectionIndex),
|
|
LegacySelectOverseasFixedIndexIntent selection =>
|
|
_controller.SelectOverseasFixedIndex(selection.TargetId),
|
|
LegacySearchOverseasIndustriesIntent search =>
|
|
await _controller.SearchOverseasIndustriesAsync(
|
|
search.Query,
|
|
cancellationToken),
|
|
LegacySearchOverseasStocksIntent search =>
|
|
await _controller.SearchOverseasStocksAsync(
|
|
search.Query,
|
|
cancellationToken),
|
|
LegacySelectOverseasIndustryIntent selection =>
|
|
_controller.SelectOverseasIndustry(selection.SelectionId),
|
|
LegacySelectOverseasStockIntent selection =>
|
|
_controller.SelectOverseasStock(selection.SelectionId),
|
|
LegacyActivateOverseasActionIntent action =>
|
|
_controller.ActivateOverseasAction(action.ActionId),
|
|
LegacySearchComparisonDomesticIntent search =>
|
|
await _controller.SearchComparisonDomesticAsync(
|
|
search.Query,
|
|
cancellationToken),
|
|
LegacySearchComparisonWorldIntent search =>
|
|
await _controller.SearchComparisonWorldAsync(
|
|
search.Query,
|
|
cancellationToken),
|
|
LegacySelectComparisonDomesticIntent selection =>
|
|
_controller.SelectComparisonDomesticResult(selection.SelectionId),
|
|
LegacySelectComparisonWorldIntent selection =>
|
|
_controller.SelectComparisonWorldResult(selection.SelectionId),
|
|
LegacySelectComparisonFixedIntent selection =>
|
|
_controller.SelectComparisonFixedTarget(selection.TargetId),
|
|
LegacyChooseComparisonDomesticIntent selection =>
|
|
_controller.ChooseComparisonDomesticResult(selection.SelectionId),
|
|
LegacyChooseComparisonWorldIntent selection =>
|
|
_controller.ChooseComparisonWorldResult(selection.SelectionId),
|
|
LegacyChooseComparisonFixedIntent selection =>
|
|
_controller.ChooseComparisonFixedTarget(selection.TargetId),
|
|
LegacySwapComparisonTargetsIntent =>
|
|
_controller.SwapComparisonTargets(),
|
|
LegacyClearComparisonTargetsIntent =>
|
|
_controller.ClearComparisonTargets(),
|
|
LegacyAddComparisonPairIntent =>
|
|
await _controller.AddComparisonPairAsync(cancellationToken),
|
|
LegacySelectComparisonPairIntent selection =>
|
|
_controller.SelectComparisonPair(selection.RowId),
|
|
LegacyMoveComparisonPairIntent move =>
|
|
await _controller.MoveComparisonPairAsync(
|
|
move.Direction,
|
|
cancellationToken),
|
|
LegacyDeleteSelectedComparisonPairIntent =>
|
|
await _controller.DeleteComparisonPairAsync(
|
|
all: false,
|
|
cancellationToken),
|
|
LegacyDeleteAllComparisonPairsIntent =>
|
|
await _controller.DeleteComparisonPairAsync(
|
|
all: true,
|
|
cancellationToken),
|
|
LegacyActivateComparisonActionIntent action =>
|
|
_controller.ActivateComparisonAction(action.ActionId),
|
|
LegacyActivateComparisonSectionIntent section =>
|
|
_controller.ActivateComparisonSection(section.SectionIndex),
|
|
LegacySearchThemesIntent search =>
|
|
await _controller.SearchThemesAsync(
|
|
search.Query,
|
|
cancellationToken),
|
|
LegacySelectThemeIntent selection =>
|
|
await _controller.SelectThemeAsync(
|
|
selection.ResultId,
|
|
cancellationToken),
|
|
LegacyActivateThemeResultIntent activation =>
|
|
await _controller.ActivateThemeResultAsync(
|
|
activation.ResultId,
|
|
cancellationToken),
|
|
LegacySetThemeSortIntent sort =>
|
|
_controller.SetThemeSort(sort.SortId),
|
|
LegacyActivateThemeActionIntent action =>
|
|
_controller.ActivateThemeAction(action.ActionId),
|
|
LegacySearchExpertsIntent search =>
|
|
await _controller.SearchExpertsAsync(search.Query, cancellationToken),
|
|
LegacySelectExpertIntent selection =>
|
|
await _controller.SelectExpertAsync(
|
|
selection.SelectionId,
|
|
cancellationToken),
|
|
LegacyActivateExpertActionIntent action =>
|
|
_controller.ActivateExpertAction(action.ActionId),
|
|
LegacySearchTradingHaltsIntent search =>
|
|
await _controller.SearchTradingHaltsAsync(
|
|
search.Query,
|
|
cancellationToken),
|
|
LegacySelectTradingHaltIntent selection =>
|
|
_controller.SelectTradingHalt(selection.SelectionId),
|
|
LegacyToggleTradingHaltSortIntent =>
|
|
_controller.ToggleTradingHaltSort(),
|
|
LegacyActivateTradingHaltActionIntent action =>
|
|
_controller.ActivateTradingHaltAction(action.ActionId),
|
|
LegacyOpenOperatorCatalogIntent catalog =>
|
|
await _controller.OpenOperatorCatalogAsync(
|
|
catalog.Entity,
|
|
cancellationToken),
|
|
LegacyBeginUc4ThemeCatalogIntent =>
|
|
await _controller.BeginThemeCatalogFromUc4Async(cancellationToken),
|
|
LegacyEditUc4SelectedThemeIntent =>
|
|
await _controller.EditSelectedThemeCatalogAsync(cancellationToken),
|
|
LegacyDeleteUc4SelectedThemeIntent =>
|
|
await _controller.DeleteSelectedThemeCatalogAsync(cancellationToken),
|
|
LegacyBeginUc6ExpertCatalogIntent =>
|
|
await _controller.BeginExpertCatalogFromUc6Async(cancellationToken),
|
|
LegacyEditUc6SelectedExpertIntent =>
|
|
await _controller.EditSelectedExpertCatalogAsync(cancellationToken),
|
|
LegacyDeleteUc6SelectedExpertIntent =>
|
|
await _controller.DeleteSelectedExpertCatalogAsync(cancellationToken),
|
|
LegacyCloseOperatorCatalogIntent =>
|
|
await _controller.CloseOperatorCatalogAsync(cancellationToken),
|
|
LegacySearchOperatorCatalogIntent search =>
|
|
await _controller.SearchOperatorCatalogAsync(
|
|
search.Query,
|
|
search.NxtSession,
|
|
cancellationToken),
|
|
LegacySelectOperatorCatalogResultIntent selection =>
|
|
await _controller.SelectOperatorCatalogResultAsync(
|
|
selection.ResultId,
|
|
cancellationToken),
|
|
LegacyBeginCreateThemeCatalogIntent create =>
|
|
await _controller.BeginCreateThemeCatalogAsync(
|
|
create.Market,
|
|
cancellationToken),
|
|
LegacyBeginCreateExpertCatalogIntent =>
|
|
await _controller.BeginCreateExpertCatalogAsync(cancellationToken),
|
|
LegacySearchOperatorCatalogStocksIntent search =>
|
|
await _controller.SearchOperatorCatalogStocksAsync(
|
|
search.Query,
|
|
cancellationToken),
|
|
LegacySelectOperatorCatalogStockIntent selection =>
|
|
_controller.SelectOperatorCatalogStock(selection.ResultId),
|
|
LegacyAddOperatorCatalogStockIntent add =>
|
|
_controller.AddOperatorCatalogStock(add.BuyAmount),
|
|
LegacyActivateOperatorCatalogStockIntent activation =>
|
|
_controller.ActivateOperatorCatalogStock(
|
|
activation.ResultId,
|
|
activation.BuyAmount),
|
|
LegacyMoveOperatorCatalogDraftRowIntent move =>
|
|
_controller.MoveOperatorCatalogDraftRow(move.RowId, move.Direction),
|
|
LegacyReorderOperatorCatalogDraftRowIntent reorder =>
|
|
_controller.ReorderOperatorCatalogDraftRow(
|
|
reorder.SourceRowId,
|
|
reorder.TargetRowId,
|
|
reorder.Position),
|
|
LegacyRemoveOperatorCatalogDraftRowIntent remove =>
|
|
_controller.RemoveOperatorCatalogDraftRow(remove.RowId),
|
|
LegacySelectOperatorCatalogDraftRowIntent selection =>
|
|
_controller.SelectOperatorCatalogDraftRow(selection.RowId),
|
|
LegacyRemoveActiveOperatorCatalogDraftRowIntent =>
|
|
_controller.RemoveActiveOperatorCatalogDraftRow(),
|
|
LegacyClearOperatorCatalogDraftRowsIntent =>
|
|
_controller.ClearOperatorCatalogDraftRows(),
|
|
LegacySetOperatorCatalogDraftBuyAmountIntent amount =>
|
|
_controller.SetOperatorCatalogDraftBuyAmount(
|
|
amount.RowId,
|
|
amount.BuyAmount),
|
|
LegacyCheckOperatorCatalogThemeNameIntent check =>
|
|
await _controller.CheckOperatorCatalogThemeNameAsync(
|
|
check.Name,
|
|
cancellationToken),
|
|
LegacySaveOperatorCatalogIntent save =>
|
|
await _controller.SaveOperatorCatalogAsync(
|
|
save.Name,
|
|
cancellationToken),
|
|
LegacyConfirmNativeDialogIntent =>
|
|
await _controller.ConfirmDialogAsync(cancellationToken),
|
|
LegacyCancelNativeDialogIntent =>
|
|
_controller.CancelDialog(),
|
|
LegacyRefreshNamedPlaylistsIntent refresh =>
|
|
await RefreshNamedPlaylistsForDispatchAsync(
|
|
refresh.RequestId,
|
|
cancellationToken),
|
|
LegacySelectNamedPlaylistIntent selection =>
|
|
SelectNamedPlaylistForDispatch(
|
|
selection.DefinitionId,
|
|
selection.RequestId),
|
|
LegacyLoadSelectedNamedPlaylistIntent =>
|
|
await _controller.LoadSelectedNamedPlaylistAsync(cancellationToken),
|
|
LegacyLoadNamedPlaylistByIdIntent load =>
|
|
await _controller.LoadNamedPlaylistByIdAsync(
|
|
load.DefinitionId,
|
|
cancellationToken),
|
|
LegacyCreateNamedPlaylistIntent create =>
|
|
await CreateNamedPlaylistForDispatchAsync(
|
|
create.Title,
|
|
create.RequestId,
|
|
cancellationToken),
|
|
LegacySaveCurrentNamedPlaylistIntent =>
|
|
await _controller.SaveCurrentNamedPlaylistAsync(cancellationToken),
|
|
LegacySaveCurrentNamedPlaylistToIntent save =>
|
|
await _controller.SaveCurrentNamedPlaylistToAsync(
|
|
save.DefinitionId,
|
|
cancellationToken),
|
|
LegacyDeleteSelectedNamedPlaylistIntent delete =>
|
|
await DeleteNamedPlaylistForDispatchAsync(
|
|
delete.RequestId,
|
|
cancellationToken),
|
|
LegacyOpenManualFinancialIntent manual =>
|
|
await _controller.OpenManualFinancialAsync(
|
|
manual.Screen,
|
|
cancellationToken),
|
|
LegacyCloseManualFinancialIntent =>
|
|
_controller.CloseManualFinancial(),
|
|
LegacySearchManualFinancialIntent search =>
|
|
await _controller.SearchManualFinancialAsync(
|
|
search.Query,
|
|
cancellationToken),
|
|
LegacyFindManualFinancialIntent find =>
|
|
await _controller.FindManualFinancialAsync(
|
|
find.Query,
|
|
find.Direction,
|
|
find.Generation,
|
|
cancellationToken),
|
|
LegacyToggleManualFinancialNameSortIntent sort =>
|
|
_controller.ToggleManualFinancialNameSort(sort.Generation),
|
|
LegacyLoadManualFinancialIntent load =>
|
|
await _controller.LoadManualFinancialAsync(
|
|
load.ResultId,
|
|
cancellationToken),
|
|
LegacyActivateManualFinancialResultIntent activation =>
|
|
await _controller.ActivateManualFinancialResultAsync(
|
|
activation.ResultId,
|
|
cancellationToken),
|
|
LegacyBeginManualFinancialCreateIntent =>
|
|
_controller.BeginManualFinancialCreate(),
|
|
LegacySearchManualFinancialStocksIntent search =>
|
|
await _controller.SearchManualFinancialStocksAsync(
|
|
search.StockName,
|
|
cancellationToken),
|
|
LegacySelectManualFinancialStockIntent selection =>
|
|
_controller.SelectManualFinancialStock(selection.ResultId),
|
|
LegacySaveManualFinancialIntent save =>
|
|
await _controller.SaveManualFinancialAsync(
|
|
save.Record,
|
|
cancellationToken),
|
|
LegacyDeleteManualFinancialIntent =>
|
|
await _controller.DeleteManualFinancialAsync(
|
|
all: false,
|
|
cancellationToken),
|
|
LegacyDeleteAllManualFinancialIntent =>
|
|
await _controller.DeleteManualFinancialAsync(
|
|
all: true,
|
|
cancellationToken),
|
|
LegacyActivateManualFinancialActionIntent action =>
|
|
_controller.ActivateManualFinancialAction(action.ActionId),
|
|
LegacyOpenManualListIntent manual =>
|
|
manual.Screen == LegacyManualListScreen.NetSell
|
|
? await _controller.OpenManualNetSellAsync(
|
|
manual.Audience,
|
|
cancellationToken)
|
|
: await _controller.OpenManualViAsync(cancellationToken),
|
|
LegacyCloseManualListIntent =>
|
|
_controller.CloseManualLists(),
|
|
LegacyRefreshManualListIntent =>
|
|
await _controller.RefreshManualListsAsync(cancellationToken),
|
|
LegacySetManualNetSellCellIntent cell =>
|
|
_controller.SetManualNetSellCell(cell.RowId, cell.Field, cell.Value),
|
|
LegacySaveManualNetSellIntent =>
|
|
await _controller.SaveManualNetSellAsync(cancellationToken),
|
|
LegacyAddManualNetSellCutIntent =>
|
|
_controller.AddManualNetSellPlaylistRow(),
|
|
LegacySearchManualViIntent search =>
|
|
await _controller.SearchManualViAsync(search.Query, cancellationToken),
|
|
LegacySelectManualViResultIntent result =>
|
|
_controller.SelectManualViSearchResult(result.ResultId),
|
|
LegacyAddManualViResultIntent result =>
|
|
_controller.AddManualViSearchResult(result.ResultId),
|
|
LegacyMoveManualViItemIntent move =>
|
|
_controller.MoveManualViItem(move.ItemId, move.Direction),
|
|
LegacyDeleteManualViItemIntent item =>
|
|
_controller.DeleteManualViItem(item.ItemId),
|
|
LegacyDeleteAllManualViItemsIntent =>
|
|
_controller.DeleteAllManualViItems(),
|
|
LegacySaveManualViIntent =>
|
|
await _controller.SaveManualViAsync(cancellationToken),
|
|
LegacyAddManualViCutIntent =>
|
|
_controller.AddManualViPlaylistRow(),
|
|
LegacyConfirmManualListIntent =>
|
|
await _controller.ConfirmManualListsAsync(cancellationToken),
|
|
LegacyImportManualListsIntent =>
|
|
await _controller.ImportLegacyManualListsAsync(cancellationToken),
|
|
LegacyGateAPrepareIntent prepare =>
|
|
await ExecuteGateAPrepareAsync(
|
|
prepare.Capability,
|
|
cancellationToken),
|
|
LegacyExecutePlayoutIntent playout =>
|
|
await ExecuteOperatorPlayoutAsync(
|
|
playout.Command,
|
|
cancellationToken),
|
|
LegacySetFadeDurationIntent fade =>
|
|
await SetFadeDurationAsync(fade.Duration, cancellationToken),
|
|
LegacyToggleBackgroundIntent =>
|
|
await ToggleBackgroundAsync(cancellationToken),
|
|
LegacyChooseBackgroundIntent =>
|
|
await ChooseBackgroundAsync(cancellationToken),
|
|
LegacyDismissDialogIntent => _controller.DismissDialog(),
|
|
_ => _controller.ReportError("지원하지 않는 화면 요청입니다.")
|
|
};
|
|
}
|
|
|
|
private async Task<LegacyOperatorSnapshot> RefreshNamedPlaylistsForDispatchAsync(
|
|
string requestId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var state = await _controller.RefreshNamedPlaylistsAsync(cancellationToken);
|
|
var succeeded = state.NamedPlaylist?.ReadStatus ==
|
|
LegacyNamedPlaylistReadStatus.Ready;
|
|
return _controller.CompleteNamedPlaylistDispatch(
|
|
"refresh-named-playlists",
|
|
requestId,
|
|
succeeded);
|
|
}
|
|
|
|
private LegacyOperatorSnapshot SelectNamedPlaylistForDispatch(
|
|
string definitionId,
|
|
string requestId)
|
|
{
|
|
var state = _controller.SelectNamedPlaylist(definitionId);
|
|
var succeeded = string.Equals(
|
|
state.NamedPlaylist?.SelectedDefinitionId,
|
|
definitionId,
|
|
StringComparison.Ordinal);
|
|
return _controller.CompleteNamedPlaylistDispatch(
|
|
"select-named-playlist",
|
|
requestId,
|
|
succeeded);
|
|
}
|
|
|
|
private async Task<LegacyOperatorSnapshot> CreateNamedPlaylistForDispatchAsync(
|
|
string title,
|
|
string requestId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var previousRevision = _controller.Current.NamedPlaylist?.Revision ?? -1;
|
|
var state = await _controller.CreateNamedPlaylistAsync(title, cancellationToken);
|
|
var succeeded = (state.NamedPlaylist?.Revision ?? -1) > previousRevision &&
|
|
IsCommittedNamedPlaylistMutation(state.NamedPlaylist?.LastMutationOutcome);
|
|
return _controller.CompleteNamedPlaylistDispatch(
|
|
"create-named-playlist",
|
|
requestId,
|
|
succeeded);
|
|
}
|
|
|
|
private async Task<LegacyOperatorSnapshot> DeleteNamedPlaylistForDispatchAsync(
|
|
string requestId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var previousRevision = _controller.Current.NamedPlaylist?.Revision ?? -1;
|
|
var state = await _controller.DeleteSelectedNamedPlaylistAsync(cancellationToken);
|
|
var succeeded = (state.NamedPlaylist?.Revision ?? -1) > previousRevision &&
|
|
IsCommittedNamedPlaylistMutation(state.NamedPlaylist?.LastMutationOutcome);
|
|
return _controller.CompleteNamedPlaylistDispatch(
|
|
"delete-selected-named-playlist",
|
|
requestId,
|
|
succeeded);
|
|
}
|
|
|
|
private static bool IsCommittedNamedPlaylistMutation(
|
|
LegacyNamedPlaylistMutationOutcome? outcome) =>
|
|
outcome is LegacyNamedPlaylistMutationOutcome.CommittedFresh or
|
|
LegacyNamedPlaylistMutationOutcome.CommittedOptimistic;
|
|
|
|
private sealed class LaunchDatabaseMutationAuthorization
|
|
: IDatabaseMutationAuthorization
|
|
{
|
|
private readonly PlayoutLaunchAuthorization _authorization;
|
|
|
|
public LaunchDatabaseMutationAuthorization(
|
|
PlayoutLaunchAuthorization authorization)
|
|
{
|
|
_authorization = authorization ??
|
|
throw new ArgumentNullException(nameof(authorization));
|
|
}
|
|
|
|
public bool IsAuthorized(DatabaseMutationAuthorizationRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
return _authorization.AllowsDatabaseWrites;
|
|
}
|
|
}
|
|
|
|
private void PostState(LegacyOperatorSnapshot snapshot)
|
|
{
|
|
if (!_closing && _webViewReady && Browser.CoreWebView2 is not null)
|
|
{
|
|
// SystemInformation.DragSize was evaluated on every original
|
|
// ListView.MouseDown. Refreshing at every native-to-web state boundary,
|
|
// in addition to the settings/DPI message hook, prevents a startup-only
|
|
// value from surviving a runtime Windows preference change.
|
|
RefreshInteractionMetrics();
|
|
Browser.CoreWebView2.PostWebMessageAsJson(
|
|
LegacyBridgeProtocol.SerializeState(
|
|
snapshot,
|
|
_intentBusy,
|
|
CreatePlayoutSnapshot(),
|
|
interactionMetrics: _interactionMetrics));
|
|
}
|
|
}
|
|
|
|
private void AttachInteractionMetricsRefresh()
|
|
{
|
|
if (_windowSubclassAttached || _windowHandle == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_windowSubclassAttached = SetWindowSubclass(
|
|
_windowHandle,
|
|
_windowSubclassProcedure,
|
|
InteractionMetricsSubclassId,
|
|
0);
|
|
}
|
|
catch (DllNotFoundException)
|
|
{
|
|
}
|
|
catch (EntryPointNotFoundException)
|
|
{
|
|
}
|
|
catch (BadImageFormatException)
|
|
{
|
|
}
|
|
catch (PlatformNotSupportedException)
|
|
{
|
|
}
|
|
}
|
|
|
|
private void DetachInteractionMetricsRefresh()
|
|
{
|
|
if (!_windowSubclassAttached || _windowHandle == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
RemoveWindowSubclass(
|
|
_windowHandle,
|
|
_windowSubclassProcedure,
|
|
InteractionMetricsSubclassId);
|
|
}
|
|
catch (DllNotFoundException)
|
|
{
|
|
}
|
|
catch (EntryPointNotFoundException)
|
|
{
|
|
}
|
|
catch (BadImageFormatException)
|
|
{
|
|
}
|
|
catch (PlatformNotSupportedException)
|
|
{
|
|
}
|
|
finally
|
|
{
|
|
_windowSubclassAttached = false;
|
|
}
|
|
}
|
|
|
|
private nint OnWindowSubclassMessage(
|
|
nint windowHandle,
|
|
uint message,
|
|
nuint wParam,
|
|
nint lParam,
|
|
nuint subclassId,
|
|
nuint referenceData)
|
|
{
|
|
if (message is WmSettingChange or WmDisplayChange or WmDpiChanged)
|
|
{
|
|
QueueInteractionMetricsRefresh();
|
|
}
|
|
|
|
return DefSubclassProc(windowHandle, message, wParam, lParam);
|
|
}
|
|
|
|
private void QueueInteractionMetricsRefresh()
|
|
{
|
|
if (_closing || _interactionMetricsRefreshQueued)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_interactionMetricsRefreshQueued = true;
|
|
if (!DispatcherQueue.TryEnqueue(
|
|
Microsoft.UI.Dispatching.DispatcherQueuePriority.High,
|
|
() =>
|
|
{
|
|
_interactionMetricsRefreshQueued = false;
|
|
if (_closing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RefreshInteractionMetrics();
|
|
if (_webViewReady)
|
|
{
|
|
PostState(_controller.Current);
|
|
}
|
|
}))
|
|
{
|
|
_interactionMetricsRefreshQueued = false;
|
|
}
|
|
}
|
|
|
|
private void RefreshInteractionMetrics()
|
|
{
|
|
_interactionMetrics = ReadSystemInteractionMetrics();
|
|
}
|
|
|
|
private LegacyInteractionMetrics ReadSystemInteractionMetrics()
|
|
{
|
|
try
|
|
{
|
|
// GetSystemMetrics returns physical pixels and is not DPI-aware.
|
|
// Asking the DPI-aware API for its 96-DPI values produces the same
|
|
// logical DIP coordinate space used by XAML. The bridge also carries
|
|
// XamlRoot.RasterizationScale so JavaScript can account for a zoom
|
|
// value retained by an older WebView profile without private API use.
|
|
return LegacyInteractionMetrics.FromPixelsAtDpi(
|
|
GetSystemMetricsForDpi(
|
|
SmCxDrag,
|
|
LegacyInteractionMetrics.CssPixelsPerInch),
|
|
GetSystemMetricsForDpi(
|
|
SmCyDrag,
|
|
LegacyInteractionMetrics.CssPixelsPerInch),
|
|
LegacyInteractionMetrics.CssPixelsPerInch,
|
|
ReadHostRasterizationScale());
|
|
}
|
|
catch (DllNotFoundException)
|
|
{
|
|
return CreateFallbackInteractionMetrics();
|
|
}
|
|
catch (EntryPointNotFoundException)
|
|
{
|
|
return CreateFallbackInteractionMetrics();
|
|
}
|
|
catch (BadImageFormatException)
|
|
{
|
|
return CreateFallbackInteractionMetrics();
|
|
}
|
|
catch (PlatformNotSupportedException)
|
|
{
|
|
return CreateFallbackInteractionMetrics();
|
|
}
|
|
}
|
|
|
|
private LegacyInteractionMetrics CreateFallbackInteractionMetrics() =>
|
|
LegacyInteractionMetrics.Default with
|
|
{
|
|
HostRasterizationScale = ReadHostRasterizationScale()
|
|
};
|
|
|
|
private double ReadHostRasterizationScale()
|
|
{
|
|
var scale = Root.XamlRoot?.RasterizationScale ?? 1d;
|
|
return double.IsFinite(scale) && scale > 0d ? scale : 1d;
|
|
}
|
|
|
|
[DllImport("user32.dll", ExactSpelling = true)]
|
|
private static extern int GetSystemMetricsForDpi(int nIndex, uint dpi);
|
|
|
|
[DllImport("comctl32.dll", ExactSpelling = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool SetWindowSubclass(
|
|
nint windowHandle,
|
|
WindowSubclassProcedure subclassProcedure,
|
|
nuint subclassId,
|
|
nuint referenceData);
|
|
|
|
[DllImport("comctl32.dll", ExactSpelling = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool RemoveWindowSubclass(
|
|
nint windowHandle,
|
|
WindowSubclassProcedure subclassProcedure,
|
|
nuint subclassId);
|
|
|
|
[DllImport("comctl32.dll", ExactSpelling = true)]
|
|
private static extern nint DefSubclassProc(
|
|
nint windowHandle,
|
|
uint message,
|
|
nuint wParam,
|
|
nint lParam);
|
|
|
|
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
|
private delegate nint WindowSubclassProcedure(
|
|
nint windowHandle,
|
|
uint message,
|
|
nuint wParam,
|
|
nint lParam,
|
|
nuint subclassId,
|
|
nuint referenceData);
|
|
|
|
private void OnProcessFailed(object? sender, CoreWebView2ProcessFailedEventArgs args)
|
|
{
|
|
ObserveOperatorMutationQuarantine(
|
|
_controller.InvalidateOperatorCatalogBrowserCorrelation());
|
|
if (!_closing)
|
|
{
|
|
ShowError("WebView2 프로세스가 중단되었습니다.", args.ProcessFailedKind.ToString());
|
|
}
|
|
}
|
|
|
|
private void ShowError(string title, string message)
|
|
{
|
|
if (_closing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ErrorBar.Title = title;
|
|
ErrorBar.Message = message;
|
|
ErrorBar.IsOpen = true;
|
|
}
|
|
|
|
private void OnClosed(object sender, WindowEventArgs args)
|
|
{
|
|
if (_closing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_closing = true;
|
|
ObserveOperatorMutationQuarantine(
|
|
_controller.InvalidateOperatorCatalogBrowserCorrelation());
|
|
Root.Loaded -= OnRootLoaded;
|
|
Closed -= OnClosed;
|
|
DetachCloseConfirmation();
|
|
DetachInteractionMetricsRefresh();
|
|
_lifetimeCancellation.Cancel();
|
|
CancelActiveDatabaseHealthCheck();
|
|
ShutdownPlayoutRuntime();
|
|
ShutdownManualListsRuntime();
|
|
DataQueryExecutor.Reset();
|
|
|
|
if (Browser.CoreWebView2 is not null)
|
|
{
|
|
Browser.CoreWebView2.NavigationStarting -= OnNavigationStarting;
|
|
Browser.CoreWebView2.NavigationCompleted -= OnNavigationCompleted;
|
|
Browser.CoreWebView2.NewWindowRequested -= OnNewWindowRequested;
|
|
Browser.CoreWebView2.WebMessageReceived -= OnWebMessageReceived;
|
|
Browser.CoreWebView2.ProcessFailed -= OnProcessFailed;
|
|
}
|
|
|
|
_webViewReady = false;
|
|
|
|
// These process-lifetime guards deliberately remain undisposed. An in-flight
|
|
// provider call may resume after Closed and must still be able to observe
|
|
// cancellation and release the gate without racing disposed objects.
|
|
}
|
|
|
|
private sealed class UnavailableThemeCatalogPersistenceService
|
|
: IThemeCatalogPersistenceService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableThemeCatalogPersistenceService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public bool CanMutate => false;
|
|
|
|
public Task<bool> ThemeNameExistsAsync(
|
|
ThemeMarket market,
|
|
string themeTitle,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<bool>(new InvalidOperationException(_message));
|
|
|
|
public Task<string> SuggestNextCodeAsync(
|
|
ThemeMarket market,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<string>(new InvalidOperationException(_message));
|
|
|
|
public Task<OperatorCatalogMutationReceipt> CreateAsync(
|
|
ThemeCatalogDefinition definition,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<OperatorCatalogMutationReceipt>(
|
|
new InvalidOperationException(_message));
|
|
|
|
public Task<OperatorCatalogMutationReceipt> ReplaceAsync(
|
|
ThemeSelectionIdentity expectedIdentity,
|
|
string newTitle,
|
|
IReadOnlyList<ThemeCatalogItem> items,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<OperatorCatalogMutationReceipt>(
|
|
new InvalidOperationException(_message));
|
|
|
|
public Task<OperatorCatalogMutationReceipt> DeleteAsync(
|
|
ThemeSelectionIdentity expectedIdentity,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<OperatorCatalogMutationReceipt>(
|
|
new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableExpertCatalogPersistenceService
|
|
: IExpertCatalogPersistenceService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableExpertCatalogPersistenceService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public bool CanMutate => false;
|
|
|
|
public Task<string> SuggestNextCodeAsync(
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<string>(new InvalidOperationException(_message));
|
|
|
|
public Task<OperatorCatalogMutationReceipt> CreateAsync(
|
|
ExpertCatalogDefinition definition,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<OperatorCatalogMutationReceipt>(
|
|
new InvalidOperationException(_message));
|
|
|
|
public Task<OperatorCatalogMutationReceipt> ReplaceAsync(
|
|
ExpertSelectionIdentity expectedIdentity,
|
|
string newName,
|
|
IReadOnlyList<ExpertCatalogRecommendation> recommendations,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<OperatorCatalogMutationReceipt>(
|
|
new InvalidOperationException(_message));
|
|
|
|
public Task<OperatorCatalogMutationReceipt> DeleteAsync(
|
|
ExpertSelectionIdentity expectedIdentity,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<OperatorCatalogMutationReceipt>(
|
|
new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableStockMasterIdentityValidationService
|
|
: IStockMasterIdentityValidationService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableStockMasterIdentityValidationService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public Task<IReadOnlyList<StockMasterIdentity>> ValidateThemeItemsAsync(
|
|
IReadOnlyList<ThemeCatalogItem> items,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<IReadOnlyList<StockMasterIdentity>>(
|
|
new InvalidOperationException(_message));
|
|
|
|
public Task<IReadOnlyList<StockMasterIdentity>> ValidateExpertRecommendationsAsync(
|
|
IReadOnlyList<ExpertCatalogRecommendation> recommendations,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<IReadOnlyList<StockMasterIdentity>>(
|
|
new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableOperatorCatalogSchemaValidationService
|
|
: IOperatorCatalogSchemaValidationService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableOperatorCatalogSchemaValidationService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public Task<OperatorCatalogSchemaValidationResult> ValidateAsync(
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<OperatorCatalogSchemaValidationResult>(
|
|
new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableNamedPlaylistPersistenceService
|
|
: INamedPlaylistPersistenceService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableNamedPlaylistPersistenceService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public bool CanMutate => false;
|
|
|
|
public Task<NamedPlaylistListResult> ListAsync(
|
|
int maximumResults = LegacyNamedPlaylistPersistenceService.DefaultMaximumDefinitions,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<NamedPlaylistListResult>(
|
|
new InvalidOperationException(_message));
|
|
|
|
public Task<string> SuggestNextProgramCodeAsync(
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<string>(new InvalidOperationException(_message));
|
|
|
|
public Task<NamedPlaylistDocument> LoadAsync(
|
|
string programCode,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<NamedPlaylistDocument>(
|
|
new InvalidOperationException(_message));
|
|
|
|
public Task CreateDefinitionAsync(
|
|
string programCode,
|
|
string title,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException(new InvalidOperationException(_message));
|
|
|
|
public Task ReplaceItemsAsync(
|
|
string programCode,
|
|
IReadOnlyList<NamedPlaylistStoredItem> items,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException(new InvalidOperationException(_message));
|
|
|
|
public Task DeleteAsync(
|
|
string programCode,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException(new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableStockLookup : ILegacyStockLookup
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableStockLookup(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public Task<LegacyStockSearchData> SearchAsync(
|
|
string query,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<LegacyStockSearchData>(
|
|
new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableIndustrySelectionService : IIndustrySelectionService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableIndustrySelectionService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public Task<IReadOnlyList<IndustrySelection>> GetAsync(
|
|
IndustryMarket market,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<IReadOnlyList<IndustrySelection>>(
|
|
new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableThemeSelectionService : IThemeSelectionService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableThemeSelectionService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public Task<ThemeSearchResult> SearchAsync(
|
|
string query,
|
|
ThemeSession nxtSession,
|
|
int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<ThemeSearchResult>(new InvalidOperationException(_message));
|
|
|
|
public Task<ThemePreviewResult> PreviewAsync(
|
|
ThemeSelectionIdentity identity,
|
|
int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<ThemePreviewResult>(new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableExpertSelectionService : IExpertSelectionService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableExpertSelectionService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public Task<ExpertSearchResult> SearchAsync(
|
|
string query = "",
|
|
int maximumResults = LegacyExpertSelectionService.DefaultMaximumResults,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<ExpertSearchResult>(new InvalidOperationException(_message));
|
|
|
|
public Task<ExpertPreviewResult> PreviewAsync(
|
|
ExpertSelectionIdentity identity,
|
|
int maximumItems = LegacyExpertSelectionService.DefaultMaximumPreviewItems,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<ExpertPreviewResult>(new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableTradingHaltSelectionService : ITradingHaltSelectionService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableTradingHaltSelectionService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public Task<TradingHaltSelectionResult> SearchAsync(
|
|
string query = "",
|
|
int maximumResults = LegacyTradingHaltSelectionService.DefaultMaximumResults,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<TradingHaltSelectionResult>(
|
|
new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableManualFinancialScreenService
|
|
: IManualFinancialScreenService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableManualFinancialScreenService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public bool CanMutate => false;
|
|
|
|
public Task<ManualFinancialSearchResult> SearchAsync(
|
|
ManualFinancialScreenKind screen,
|
|
string query = "",
|
|
int maximumResults = LegacyManualFinancialScreenService.DefaultMaximumResults,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<ManualFinancialSearchResult>(
|
|
new InvalidOperationException(_message));
|
|
|
|
public Task<ManualFinancialSnapshot> GetAsync(
|
|
ManualFinancialIdentity identity,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<ManualFinancialSnapshot>(
|
|
new InvalidOperationException(_message));
|
|
|
|
public Task<ManualFinancialMutationReceipt> CreateAsync(
|
|
ManualFinancialRecord record,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<ManualFinancialMutationReceipt>(
|
|
new InvalidOperationException(_message));
|
|
|
|
public Task<ManualFinancialMutationReceipt> UpdateAsync(
|
|
ManualFinancialSnapshot expected,
|
|
ManualFinancialRecord replacement,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<ManualFinancialMutationReceipt>(
|
|
new InvalidOperationException(_message));
|
|
|
|
public Task<ManualFinancialMutationReceipt> DeleteAsync(
|
|
ManualFinancialSnapshot expected,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<ManualFinancialMutationReceipt>(
|
|
new InvalidOperationException(_message));
|
|
|
|
public Task<ManualFinancialMutationReceipt> DeleteAllAsync(
|
|
ManualFinancialScreenKind screen,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<ManualFinancialMutationReceipt>(
|
|
new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableStockSearchService : IStockSearchService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableStockSearchService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public Task<StockSearchResult> SearchAsync(
|
|
string query,
|
|
int maximumResults = LegacyStockSearchService.DefaultMaximumResults,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<StockSearchResult>(new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableOverseasStockSearchService : IOverseasStockSearchService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableOverseasStockSearchService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public Task<WorldStockSearchResult> SearchAsync(
|
|
string query,
|
|
int maximumResults = LegacyOverseasStockSearchService.DefaultMaximumResults,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<WorldStockSearchResult>(
|
|
new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableWorldStockSearchService : IWorldStockSearchService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableWorldStockSearchService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public Task<WorldStockSearchResult> SearchAsync(
|
|
string query,
|
|
int maximumResults = LegacyWorldStockSearchService.DefaultMaximumResults,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<WorldStockSearchResult>(
|
|
new InvalidOperationException(_message));
|
|
}
|
|
|
|
private sealed class UnavailableOverseasIndustryIndexSearchService
|
|
: IOverseasIndustryIndexSearchService
|
|
{
|
|
private readonly string _message;
|
|
|
|
public UnavailableOverseasIndustryIndexSearchService(string message)
|
|
{
|
|
_message = message;
|
|
}
|
|
|
|
public Task<OverseasIndustryIndexSearchResult> SearchAsync(
|
|
string query,
|
|
int maximumResults = LegacyOverseasIndustryIndexSearchService.DefaultMaximumResults,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromException<OverseasIndustryIndexSearchResult>(
|
|
new InvalidOperationException(_message));
|
|
}
|
|
}
|