feat: establish legacy parity migration baseline
This commit is contained in:
14
src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml
Normal file
14
src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Application
|
||||
x:Class="MBN_STOCK_WEBVIEW.LegacyParityApp.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
48
src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs
Normal file
48
src/MBN_STOCK_WEBVIEW.LegacyParityApp/App.xaml.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyParityApp;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private const string MainInstanceKey = "Wickedness.MBNStockWebView.LegacyParity.Main";
|
||||
|
||||
private Window? _window;
|
||||
private AppInstance? _mainInstance;
|
||||
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override async void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
var registeredInstance = AppInstance.FindOrRegisterForKey(MainInstanceKey);
|
||||
if (!registeredInstance.IsCurrent)
|
||||
{
|
||||
try
|
||||
{
|
||||
var activation = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
if (activation is not null)
|
||||
{
|
||||
await registeredInstance.RedirectActivationToAsync(activation);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Exit();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_mainInstance = registeredInstance;
|
||||
_mainInstance.Activated += OnMainInstanceActivated;
|
||||
_window = new MainWindow();
|
||||
_window.Activate();
|
||||
}
|
||||
|
||||
private void OnMainInstanceActivated(object? sender, AppActivationArguments args)
|
||||
{
|
||||
_window?.DispatcherQueue.TryEnqueue(_window.Activate);
|
||||
}
|
||||
}
|
||||
2
src/MBN_STOCK_WEBVIEW.LegacyParityApp/Imports.cs
Normal file
2
src/MBN_STOCK_WEBVIEW.LegacyParityApp/Imports.cs
Normal file
@@ -0,0 +1,2 @@
|
||||
global using Microsoft.UI.Xaml;
|
||||
global using Microsoft.UI.Xaml.Controls;
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>MBN_STOCK_WEBVIEW.LegacyParityApp</RootNamespace>
|
||||
<AssemblyName>MBN_STOCK_WEBVIEW.LegacyParityApp</AssemblyName>
|
||||
<UseRidGraph>true</UseRidGraph>
|
||||
<Platforms>x64</Platforms>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
<ApplicationDisplayVersion>0.1.0</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>1</ApplicationVersion>
|
||||
<Version>0.1.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\Assets\LockScreenLogo.scale-200.png" Link="Assets\LockScreenLogo.scale-200.png" />
|
||||
<Content Include="..\..\Assets\SplashScreen.scale-200.png" Link="Assets\SplashScreen.scale-200.png" />
|
||||
<Content Include="..\..\Assets\Square150x150Logo.scale-200.png" Link="Assets\Square150x150Logo.scale-200.png" />
|
||||
<Content Include="..\..\Assets\Square44x44Logo.scale-200.png" Link="Assets\Square44x44Logo.scale-200.png" />
|
||||
<Content Include="..\..\Assets\Square44x44Logo.targetsize-24_altform-unplated.png" Link="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
|
||||
<Content Include="..\..\Assets\StoreLogo.png" Link="Assets\StoreLogo.png" />
|
||||
<Content Include="..\..\Assets\Wide310x150Logo.scale-200.png" Link="Assets\Wide310x150Logo.scale-200.png" />
|
||||
<Content Include="Web\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260317003" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3967.48" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7705" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MBN_STOCK_WEBVIEW.LegacyApplication\MBN_STOCK_WEBVIEW.LegacyApplication.csproj" />
|
||||
<ProjectReference Include="..\MBN_STOCK_WEBVIEW.LegacyBridge\MBN_STOCK_WEBVIEW.LegacyBridge.csproj" />
|
||||
<ProjectReference Include="..\MBN_STOCK_WEBVIEW.Core\MBN_STOCK_WEBVIEW.Core.csproj" />
|
||||
<ProjectReference Include="..\MBN_STOCK_WEBVIEW.Infrastructure\MBN_STOCK_WEBVIEW.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\MBN_STOCK_WEBVIEW.Playout\MBN_STOCK_WEBVIEW.Playout.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
30
src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml
Normal file
30
src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Window
|
||||
x:Class="MBN_STOCK_WEBVIEW.LegacyParityApp.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="V-Stock 증권정보송출시스템 for 매일경제TV (26.03.26)">
|
||||
<Grid x:Name="Root" Background="#FFF0F0F0">
|
||||
<WebView2 x:Name="Browser" />
|
||||
|
||||
<Grid x:Name="LoadingOverlay" Background="#FFF0F0F0">
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="12">
|
||||
<ProgressRing Width="32" Height="32" IsActive="True" />
|
||||
<TextBlock Text="원본 호환 화면을 준비하고 있습니다." />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<InfoBar
|
||||
x:Name="ErrorBar"
|
||||
Margin="12"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
IsClosable="True"
|
||||
IsOpen="False"
|
||||
Severity="Error"
|
||||
Title="화면을 초기화하지 못했습니다." />
|
||||
</Grid>
|
||||
</Window>
|
||||
353
src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs
Normal file
353
src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,353 @@
|
||||
using MBN_STOCK_WEBVIEW.Infrastructure;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
using MBN_STOCK_WEBVIEW.LegacyBridge;
|
||||
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 readonly CancellationTokenSource _lifetimeCancellation = new();
|
||||
private readonly SemaphoreSlim _intentGate = new(1, 1);
|
||||
private readonly LegacyOperatorController _controller;
|
||||
private DatabaseRuntime? _databaseRuntime;
|
||||
private bool _closing;
|
||||
private bool _intentBusy;
|
||||
private bool _webViewReady;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
ILegacyStockLookup stockLookup;
|
||||
string? initializationError = null;
|
||||
try
|
||||
{
|
||||
_databaseRuntime = DatabaseRuntime.CreateDefault();
|
||||
DataQueryExecutor.Configure(_databaseRuntime.Executor);
|
||||
stockLookup = new LegacyParityStockSearchService(_databaseRuntime.Executor);
|
||||
}
|
||||
catch
|
||||
{
|
||||
initializationError =
|
||||
"데이터베이스가 설정되지 않았습니다. 로컬 설정을 확인하세요.";
|
||||
stockLookup = new UnavailableStockLookup(initializationError);
|
||||
DataQueryExecutor.Reset();
|
||||
}
|
||||
|
||||
_controller = new LegacyOperatorController(stockLookup);
|
||||
if (initializationError is not null)
|
||||
{
|
||||
_controller.ReportError(initializationError);
|
||||
}
|
||||
|
||||
Root.Loaded += OnRootLoaded;
|
||||
Closed += OnClosed;
|
||||
}
|
||||
|
||||
private async void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Root.Loaded -= OnRootLoaded;
|
||||
if (_closing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigureWindow();
|
||||
await InitializeWebViewAsync();
|
||||
}
|
||||
|
||||
private void ConfigureWindow()
|
||||
{
|
||||
var windowHandle = WindowNative.GetWindowHandle(this);
|
||||
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.AreDefaultContextMenusEnabled = false;
|
||||
coreWebView.Settings.IsStatusBarEnabled = 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;
|
||||
try
|
||||
{
|
||||
// MainForm performed database search synchronously. The busy snapshot
|
||||
// freezes the Web controls, while this zero-timeout gate prevents an
|
||||
// already-posted duplicate from growing an unbounded DB request queue.
|
||||
gateEntered = await _intentGate.WaitAsync(
|
||||
0,
|
||||
_lifetimeCancellation.Token);
|
||||
if (!gateEntered)
|
||||
{
|
||||
PostState(_controller.Current);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_closing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (intent is LegacySearchStocksIntent searchIntent &&
|
||||
!(searchIntent.Trigger == LegacySearchTrigger.Button &&
|
||||
searchIntent.Text.Length == 0))
|
||||
{
|
||||
_intentBusy = true;
|
||||
PostState(_controller.Current);
|
||||
}
|
||||
|
||||
var state = await HandleIntentAsync(intent, _lifetimeCancellation.Token);
|
||||
if (!_closing)
|
||||
{
|
||||
_intentBusy = false;
|
||||
PostState(state);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (_lifetimeCancellation.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch when (_closing)
|
||||
{
|
||||
}
|
||||
catch
|
||||
{
|
||||
_intentBusy = false;
|
||||
PostState(_controller.ReportError(
|
||||
"요청을 처리하지 못했습니다. 데이터베이스 연결 상태를 확인하세요."));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_closing)
|
||||
{
|
||||
_intentBusy = false;
|
||||
}
|
||||
|
||||
if (gateEntered)
|
||||
{
|
||||
_intentGate.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
LegacyDropSelectedCutsIntent => _controller.DropSelectedCutsOnPlaylist(),
|
||||
LegacySetPlaylistEnabledIntent enabled =>
|
||||
_controller.SetPlaylistRowEnabled(enabled.RowId, enabled.Enabled),
|
||||
LegacyDismissDialogIntent => _controller.DismissDialog(),
|
||||
_ => _controller.ReportError("지원하지 않는 화면 요청입니다.")
|
||||
};
|
||||
}
|
||||
|
||||
private void PostState(LegacyOperatorSnapshot snapshot)
|
||||
{
|
||||
if (!_closing && _webViewReady && Browser.CoreWebView2 is not null)
|
||||
{
|
||||
Browser.CoreWebView2.PostWebMessageAsJson(
|
||||
LegacyBridgeProtocol.SerializeState(snapshot, _intentBusy));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProcessFailed(object? sender, CoreWebView2ProcessFailedEventArgs args)
|
||||
{
|
||||
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;
|
||||
Root.Loaded -= OnRootLoaded;
|
||||
Closed -= OnClosed;
|
||||
_lifetimeCancellation.Cancel();
|
||||
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 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));
|
||||
}
|
||||
}
|
||||
43
src/MBN_STOCK_WEBVIEW.LegacyParityApp/Package.appxmanifest
Normal file
43
src/MBN_STOCK_WEBVIEW.LegacyParityApp/Package.appxmanifest
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
<Identity
|
||||
Name="Wickedness.MBNStockWebView.LegacyParity"
|
||||
Publisher="CN=Comtrophy"
|
||||
Version="0.1.0.0" />
|
||||
<mp:PhoneIdentity PhoneProductId="8f62f17b-a9a4-42cc-a696-a1987b5588f1" PhonePublisherId="00000000-0000-0000-0000-000000000000" />
|
||||
<Properties>
|
||||
<DisplayName>MBN Stock WebView (Legacy Parity)</DisplayName>
|
||||
<PublisherDisplayName>Wickedness</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
<Resources>
|
||||
<Resource Language="x-generate" />
|
||||
</Resources>
|
||||
<Applications>
|
||||
<Application Id="LegacyParityApp" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="MBN Stock WebView (Legacy Parity)"
|
||||
Description="매일경제TV 증권정보송출시스템 원본 호환 이관판"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
<Capabilities>
|
||||
<Capability Name="internetClient" />
|
||||
<Capability Name="privateNetworkClientServer" />
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"profiles": {
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp (Package)": {
|
||||
"commandName": "MsixPackage"
|
||||
}
|
||||
}
|
||||
}
|
||||
232
src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/app.js
Normal file
232
src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/app.js
Normal file
@@ -0,0 +1,232 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const webview = window.chrome && window.chrome.webview;
|
||||
const searchInput = document.getElementById("stock-search");
|
||||
const searchButton = document.getElementById("stock-search-button");
|
||||
const stockResults = document.getElementById("stock-results");
|
||||
const cutList = document.getElementById("cut-list");
|
||||
const playlistRows = document.getElementById("playlist-rows");
|
||||
const playlistDropZone = document.getElementById("playlist-drop-zone");
|
||||
const status = document.getElementById("operator-status");
|
||||
const dialog = document.getElementById("legacy-dialog");
|
||||
const dialogMessage = document.getElementById("legacy-dialog-message");
|
||||
const dialogOk = document.getElementById("legacy-dialog-ok");
|
||||
let uiBusy = false;
|
||||
|
||||
function send(type, payload) {
|
||||
if (webview && !uiBusy) webview.postMessage({ type: type, payload: payload || {} });
|
||||
}
|
||||
|
||||
function createStockResultElement(row) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.dataset.resultIndex = String(row.index);
|
||||
button.setAttribute("role", "option");
|
||||
button.addEventListener("click", function () {
|
||||
send("select-stock", { resultIndex: Number(button.dataset.resultIndex) });
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
function renderStockResults(state) {
|
||||
state.searchResults.forEach(function (row, position) {
|
||||
let button = stockResults.children.item(position);
|
||||
if (!button || button.dataset.resultIndex !== String(row.index)) {
|
||||
button = createStockResultElement(row);
|
||||
if (position < stockResults.children.length) {
|
||||
stockResults.replaceChild(button, stockResults.children.item(position));
|
||||
} else {
|
||||
stockResults.appendChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
button.className = "stock-result" +
|
||||
(state.selectedStockIndex === row.index ? " selected" : "");
|
||||
button.setAttribute("aria-disabled", state.isBusy === true ? "true" : "false");
|
||||
button.textContent = row.displayName;
|
||||
button.setAttribute("aria-selected", state.selectedStockIndex === row.index ? "true" : "false");
|
||||
});
|
||||
|
||||
while (stockResults.children.length > state.searchResults.length) {
|
||||
stockResults.lastElementChild.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function pointerPosition(event) {
|
||||
const bounds = cutList.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.max(0, Math.round(event.clientX - bounds.left)),
|
||||
y: Math.max(0, Math.round(event.clientY - bounds.top))
|
||||
};
|
||||
}
|
||||
|
||||
function createCutElement(row) {
|
||||
const element = document.createElement("div");
|
||||
element.setAttribute("role", "option");
|
||||
element.dataset.physicalIndex = String(row.physicalIndex);
|
||||
element.draggable = true;
|
||||
|
||||
const number = document.createElement("span");
|
||||
number.className = "cut-number";
|
||||
number.textContent = row.displayOrdinal === null ? "" : String(row.displayOrdinal);
|
||||
const label = document.createElement("span");
|
||||
label.textContent = row.rawLabel;
|
||||
element.append(number, label);
|
||||
|
||||
element.addEventListener("pointerdown", function (event) {
|
||||
const position = pointerPosition(event);
|
||||
send("cut-pointer-down", {
|
||||
physicalIndex: row.physicalIndex,
|
||||
control: event.ctrlKey,
|
||||
shift: event.shiftKey,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
timestampMilliseconds: Math.max(0, Math.round(event.timeStamp))
|
||||
});
|
||||
});
|
||||
element.addEventListener("dragstart", function (event) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("text/plain", "legacy-selected-cuts");
|
||||
}
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
function renderCuts(state) {
|
||||
state.cutRows.forEach(function (row, index) {
|
||||
let element = cutList.children.item(index);
|
||||
if (!element || element.dataset.physicalIndex !== String(row.physicalIndex)) {
|
||||
element = createCutElement(row);
|
||||
if (index < cutList.children.length) {
|
||||
cutList.replaceChild(element, cutList.children.item(index));
|
||||
} else {
|
||||
cutList.appendChild(element);
|
||||
}
|
||||
}
|
||||
|
||||
element.className = "cut-row" +
|
||||
(row.isSelected ? " selected" : "") +
|
||||
(row.isSeparator ? " separator" : "");
|
||||
element.setAttribute("aria-selected", row.isSelected ? "true" : "false");
|
||||
element.setAttribute("aria-disabled", state.isBusy === true ? "true" : "false");
|
||||
element.draggable = state.isBusy !== true;
|
||||
});
|
||||
|
||||
while (cutList.children.length > state.cutRows.length) {
|
||||
cutList.lastElementChild.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function createPlaylistElement(row) {
|
||||
const element = document.createElement("div");
|
||||
element.className = "playlist-row playlist-data-row";
|
||||
element.dataset.rowId = row.rowId;
|
||||
|
||||
const enabledCell = document.createElement("div");
|
||||
enabledCell.className = "enabled-cell";
|
||||
const enabled = document.createElement("input");
|
||||
enabled.type = "checkbox";
|
||||
enabled.addEventListener("change", function () {
|
||||
send("set-playlist-enabled", {
|
||||
rowId: element.dataset.rowId,
|
||||
enabled: enabled.checked
|
||||
});
|
||||
});
|
||||
enabledCell.appendChild(enabled);
|
||||
element.appendChild(enabledCell);
|
||||
|
||||
for (let index = 0; index < 5; index += 1) {
|
||||
element.appendChild(document.createElement("div"));
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function renderPlaylist(state) {
|
||||
state.playlist.forEach(function (row, position) {
|
||||
let element = playlistRows.children.item(position);
|
||||
if (!element || element.dataset.rowId !== row.rowId) {
|
||||
element = createPlaylistElement(row);
|
||||
if (position < playlistRows.children.length) {
|
||||
playlistRows.replaceChild(element, playlistRows.children.item(position));
|
||||
} else {
|
||||
playlistRows.appendChild(element);
|
||||
}
|
||||
}
|
||||
|
||||
const enabled = element.querySelector("input");
|
||||
enabled.checked = row.isEnabled;
|
||||
enabled.setAttribute("aria-disabled", state.isBusy === true ? "true" : "false");
|
||||
[row.marketText, row.stockName, row.graphicType, row.subtype, row.pageText]
|
||||
.forEach(function (text, cellIndex) {
|
||||
const cell = element.children.item(cellIndex + 1);
|
||||
cell.textContent = text;
|
||||
cell.title = text;
|
||||
});
|
||||
});
|
||||
|
||||
while (playlistRows.children.length > state.playlist.length) {
|
||||
playlistRows.lastElementChild.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function renderDialog(state) {
|
||||
if (state.dialog && state.dialog.message) {
|
||||
dialogMessage.textContent = state.dialog.message;
|
||||
dialog.hidden = false;
|
||||
dialogOk.focus();
|
||||
} else {
|
||||
dialog.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function render(state) {
|
||||
const isBusy = state.isBusy === true;
|
||||
uiBusy = isBusy;
|
||||
document.body.classList.toggle("busy", isBusy);
|
||||
document.body.setAttribute("aria-busy", isBusy ? "true" : "false");
|
||||
searchInput.readOnly = isBusy;
|
||||
searchButton.setAttribute("aria-disabled", isBusy ? "true" : "false");
|
||||
if (document.activeElement !== searchInput) searchInput.value = state.searchText || "";
|
||||
renderStockResults(state);
|
||||
renderCuts(state);
|
||||
renderPlaylist(state);
|
||||
renderDialog(state);
|
||||
status.textContent = isBusy ? "종목을 검색하고 있습니다." : (state.statusMessage || "");
|
||||
}
|
||||
|
||||
searchButton.addEventListener("click", function () {
|
||||
send("search-stocks", { text: searchInput.value, trigger: "button" });
|
||||
});
|
||||
|
||||
searchInput.addEventListener("keypress", function (event) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
send("search-stocks", { text: searchInput.value, trigger: "enter" });
|
||||
}
|
||||
});
|
||||
|
||||
playlistDropZone.addEventListener("dragover", function (event) {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
||||
});
|
||||
playlistDropZone.addEventListener("drop", function (event) {
|
||||
event.preventDefault();
|
||||
send("drop-selected-cuts", {});
|
||||
});
|
||||
dialogOk.addEventListener("click", function () {
|
||||
send("dismiss-dialog", {});
|
||||
});
|
||||
|
||||
if (webview) {
|
||||
webview.addEventListener("message", function (event) {
|
||||
if (event.data && event.data.type === "state" && event.data.payload) {
|
||||
render(event.data.payload);
|
||||
}
|
||||
});
|
||||
send("ready", {});
|
||||
} else {
|
||||
status.textContent = "WebView2 브리지를 사용할 수 없습니다.";
|
||||
}
|
||||
}());
|
||||
114
src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/index.html
Normal file
114
src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/index.html
Normal file
@@ -0,0 +1,114 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>V-Stock 증권정보송출시스템</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="operator-shell" aria-label="V-Stock 증권정보송출시스템">
|
||||
<section class="left-panel" aria-label="종목 및 컷 선택">
|
||||
<header class="brand-line">
|
||||
<div class="brand"><span>매일경제TV</span><strong>VRi</strong></div>
|
||||
<div class="connection-state" aria-label="Tornado2 연결 상태">미연결</div>
|
||||
</header>
|
||||
|
||||
<div class="search-line">
|
||||
<label for="stock-search">종목</label>
|
||||
<input id="stock-search" maxlength="32767" autocomplete="off" spellcheck="false">
|
||||
<button id="stock-search-button" type="button"><span aria-hidden="true">⌕</span> 검색</button>
|
||||
</div>
|
||||
|
||||
<div id="stock-results" class="stock-results" role="listbox" aria-label="종목명"></div>
|
||||
|
||||
<div class="manual-actions" aria-label="수동 입력">
|
||||
<button type="button" disabled>주요매출 구성</button>
|
||||
<button type="button" disabled>성장성 지표</button>
|
||||
<button type="button" disabled>매출액</button>
|
||||
<button type="button" disabled>영업이익</button>
|
||||
</div>
|
||||
|
||||
<div id="cut-list" class="cut-list" role="listbox" aria-label="종목 컷" aria-multiselectable="true"></div>
|
||||
<div id="operator-status" class="operator-status" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
<section class="catalog-panel" aria-label="분류별 그래픽">
|
||||
<nav class="market-tabs" aria-label="시장 분류">
|
||||
<button class="active" type="button" disabled>해외</button>
|
||||
<button type="button" disabled>환율</button>
|
||||
<button type="button" disabled>지수</button>
|
||||
<button type="button" disabled>코스피</button>
|
||||
<button type="button" disabled>코스닥</button>
|
||||
<button type="button" disabled>비교</button>
|
||||
<button type="button" disabled>테마</button>
|
||||
<button type="button" disabled>해외종목</button>
|
||||
<button type="button" disabled>전문가</button>
|
||||
<button type="button" disabled>정지</button>
|
||||
</nav>
|
||||
<label class="expand-line"><input type="checkbox" checked disabled> Expand</label>
|
||||
<div class="category-title">Category</div>
|
||||
<div class="category-tree" aria-label="원본 카테고리">
|
||||
<div class="tree-root">⊖ <strong>[1열판기본]</strong></div>
|
||||
<div class="tree-item">▸ 다우</div>
|
||||
<div class="tree-item">▸ 나스닥</div>
|
||||
<div class="tree-item">▸ S&P</div>
|
||||
<div class="tree-item">▸ 독일</div>
|
||||
<div class="tree-item">▸ 영국</div>
|
||||
<div class="tree-item">▸ 프랑스</div>
|
||||
<div class="tree-item">▸ 니케이</div>
|
||||
<div class="tree-item">▸ 중국 상해</div>
|
||||
<div class="tree-item">▸ 홍콩 항셍</div>
|
||||
<div class="tree-item">▸ 대만 가권</div>
|
||||
<div class="tree-item">▸ 싱가포르 지수</div>
|
||||
<div class="tree-item">▸ 필리핀 지수</div>
|
||||
<div class="tree-item">▸ 말레이시아 지수</div>
|
||||
<div class="tree-root second">⊖ <strong>[2열판-]</strong></div>
|
||||
<div class="tree-item">▸ 공산품 [원면, 목화(면화)]</div>
|
||||
<div class="tree-item">▸ 원자재 [국제 금, 국제 은]</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="playlist-panel" aria-label="플레이리스트">
|
||||
<div class="playlist-toolbar">
|
||||
<label><input type="checkbox" checked disabled> 전체</label>
|
||||
<label><input type="checkbox" checked disabled> 5일선</label>
|
||||
<label><input type="checkbox" checked disabled> 20일선</label>
|
||||
<button type="button" disabled>↑</button>
|
||||
<button type="button" disabled>↓</button>
|
||||
<span class="toolbar-spacer"></span>
|
||||
<label><input type="checkbox" disabled> 배경없음(F3)</label>
|
||||
<button type="button" disabled>배경파일(F2)</button>
|
||||
<input class="background-name" value="기본.vrv" readonly>
|
||||
<button type="button" disabled>DB 저장</button>
|
||||
<button type="button" disabled>DB 불러오기</button>
|
||||
<button type="button" disabled>선택삭제</button>
|
||||
<button type="button" disabled>전체삭제</button>
|
||||
</div>
|
||||
|
||||
<div id="playlist-drop-zone" class="playlist-grid">
|
||||
<div class="playlist-header playlist-row">
|
||||
<div>송출</div><div>종류</div><div>종목</div><div>컷파일</div><div>세부사항</div><div>Page</div>
|
||||
</div>
|
||||
<div id="playlist-rows" class="playlist-rows"></div>
|
||||
</div>
|
||||
|
||||
<div class="playout-actions">
|
||||
<button class="take-in" type="button" disabled>● TAKE IN [F8]</button>
|
||||
<button class="next" type="button" disabled>▶ NEXT</button>
|
||||
<button class="take-out" type="button" disabled>✖ TAKE OUT[Esc]</button>
|
||||
</div>
|
||||
<div class="playout-status">원본 호환 UI 기준선 · 송출 연결 안 함</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="legacy-dialog" class="dialog-backdrop" hidden>
|
||||
<div class="legacy-dialog" role="alertdialog" aria-modal="true" aria-labelledby="legacy-dialog-message">
|
||||
<div id="legacy-dialog-message"></div>
|
||||
<button id="legacy-dialog-ok" type="button">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
92
src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/styles.css
Normal file
92
src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/styles.css
Normal file
@@ -0,0 +1,92 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family: "NanumGothic", "맑은 고딕", sans-serif;
|
||||
font-size: 13px;
|
||||
background: #efefef;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
||||
body.busy { cursor: wait; }
|
||||
body.busy .operator-shell { pointer-events: none; }
|
||||
button, input { font: inherit; }
|
||||
button { border: 1px solid #a8a8a8; background: linear-gradient(#fff, #e9e9e9); color: #111; }
|
||||
button:not(:disabled) { cursor: pointer; }
|
||||
button:disabled { color: #555; opacity: 1; }
|
||||
|
||||
.operator-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 430px 500px minmax(760px, 1fr);
|
||||
width: 100%;
|
||||
min-width: 1690px;
|
||||
height: 100vh;
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
.left-panel, .catalog-panel, .playlist-panel { min-height: 0; border-right: 1px solid #bbb; }
|
||||
.left-panel { display: flex; flex-direction: column; padding: 6px 15px 4px 20px; background: #f5f5f5; }
|
||||
|
||||
.brand-line { height: 68px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.brand { display: flex; align-items: center; gap: 12px; white-space: nowrap; }
|
||||
.brand span { color: #ed671f; font-size: 28px; font-weight: 800; letter-spacing: -2px; }
|
||||
.brand strong { font: italic 900 38px Arial, sans-serif; letter-spacing: -5px; }
|
||||
.connection-state { padding: 10px 12px; background: #c40000; color: white; font: 700 18px Consolas, monospace; }
|
||||
|
||||
.search-line { display: grid; grid-template-columns: 40px 162px 1fr 117px; align-items: center; gap: 3px; height: 38px; }
|
||||
.search-line label { font-weight: 700; }
|
||||
.search-line input { grid-column: 2; height: 27px; padding: 2px 7px; border: 1px solid #888; font-size: 16px; }
|
||||
.search-line button { grid-column: 4; height: 31px; font-size: 14px; }
|
||||
.search-line button span { font-size: 21px; margin-right: 20px; }
|
||||
|
||||
.stock-results { height: 174px; border: 1px solid #999; background: #fff; overflow-y: auto; }
|
||||
.stock-result { display: block; width: 100%; height: 20px; padding: 1px 5px; border: 0; background: #fff; text-align: left; font-size: 16px; line-height: 18px; }
|
||||
.stock-result.selected { background: #696969; color: #fff; }
|
||||
|
||||
.manual-actions { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin: 9px 0; }
|
||||
.manual-actions button { height: 34px; font-size: 12px; font-weight: 700; }
|
||||
|
||||
.cut-list { flex: 1; min-height: 0; border: 1px solid #999; background: #fff; overflow-y: auto; user-select: none; }
|
||||
.cut-row { display: grid; grid-template-columns: 62px 1fr; min-height: 23px; border-bottom: 1px solid #ddd; background: #fff; font-size: 15px; }
|
||||
.cut-row > span { padding: 2px 7px; pointer-events: none; }
|
||||
.cut-row .cut-number { border-right: 1px solid #ddd; text-align: center; color: #145ab4; }
|
||||
.cut-row.selected { background: #dcdcdc; }
|
||||
.cut-row.separator { background: #fff; }
|
||||
.operator-status { min-height: 18px; padding-top: 2px; color: #a40000; font-size: 11px; }
|
||||
|
||||
.catalog-panel { display: flex; flex-direction: column; padding: 0 8px; background: #f3f3f3; }
|
||||
.market-tabs { display: flex; height: 29px; border-bottom: 1px solid #999; }
|
||||
.market-tabs button { min-width: 48px; padding: 3px 8px; border-width: 0 1px 0 0; background: #efefef; }
|
||||
.market-tabs button.active { background: #fff; border-top: 2px solid #e8781d; }
|
||||
.expand-line { height: 31px; padding: 7px 8px 0; }
|
||||
.category-title { height: 27px; border-bottom: 1px solid #777; font-size: 12px; }
|
||||
.category-tree { flex: 1; overflow: auto; padding: 7px 12px; background: #fff; border-left: 1px solid #aaa; border-right: 1px solid #aaa; }
|
||||
.tree-root { margin: 5px 0 2px; color: #168000; font-size: 16px; }
|
||||
.tree-root.second { margin-top: 15px; }
|
||||
.tree-item { padding: 3px 0 3px 29px; font-size: 15px; }
|
||||
|
||||
.playlist-panel { display: grid; grid-template-rows: 58px minmax(0, 1fr) 76px 42px; padding: 0 8px; background: #efefef; }
|
||||
.playlist-toolbar { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
|
||||
.playlist-toolbar button { height: 25px; padding: 2px 9px; }
|
||||
.playlist-toolbar .toolbar-spacer { flex: 1; }
|
||||
.background-name { width: 95px; height: 25px; border: 1px solid #888; background: #fff; }
|
||||
|
||||
.playlist-grid { min-height: 0; border: 1px solid #888; background: #d5d5d5; overflow: auto; }
|
||||
.playlist-row { display: grid; grid-template-columns: 40px 100px 260px 200px minmax(255px, 1fr) 52px; min-width: 907px; }
|
||||
.playlist-header { position: sticky; top: 0; z-index: 2; height: 29px; background: #efefef; border-bottom: 1px solid #999; }
|
||||
.playlist-header > div { display: flex; align-items: center; justify-content: center; border-right: 1px solid #aaa; }
|
||||
.playlist-data-row { min-height: 25px; background: #fff; border-bottom: 1px solid #ddd; }
|
||||
.playlist-data-row > div { padding: 3px 6px; border-right: 1px solid #ddd; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.playlist-data-row .enabled-cell { text-align: center; }
|
||||
|
||||
.playout-actions { display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
||||
.playout-actions button { height: 51px; background: #fff; font-size: 25px; font-weight: 700; }
|
||||
.take-in { width: 215px; }
|
||||
.next { width: 180px; }
|
||||
.take-out { width: 212px; }
|
||||
.playout-status { border-top: 1px solid #bbb; display: flex; align-items: center; justify-content: center; color: #666; }
|
||||
|
||||
.dialog-backdrop { position: fixed; inset: 0; z-index: 20; background: rgba(0, 0, 0, .25); }
|
||||
.dialog-backdrop[hidden] { display: none; }
|
||||
.legacy-dialog { position: absolute; left: 50%; top: 50%; width: 320px; padding: 24px; transform: translate(-50%, -50%); border: 1px solid #777; background: #fff; box-shadow: 0 8px 30px rgba(0, 0, 0, .3); text-align: center; }
|
||||
.legacy-dialog button { min-width: 80px; margin-top: 24px; padding: 6px 15px; }
|
||||
14
src/MBN_STOCK_WEBVIEW.LegacyParityApp/app.manifest
Normal file
14
src/MBN_STOCK_WEBVIEW.LegacyParityApp/app.manifest
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="MBN_STOCK_WEBVIEW.LegacyParityApp" />
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
Reference in New Issue
Block a user