using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using JJServiceManager.Remote;
namespace JJServiceManager
{
/// 대상 PC: 원격 화면 연결 승인 창 (30초 안에 답하지 않으면 거부)
public sealed class ApprovalWindow : Window
{
private readonly TaskCompletionSource _result = new TaskCompletionSource();
private readonly DispatcherTimer _timer;
private readonly TextBlock _countdown;
private int _left = 30;
private ApprovalWindow(string who, bool control)
{
Title = "원격관리 연결 요청" + AppInstance.TitleTag;
Width = 460;
SizeToContent = SizeToContent.Height;
ResizeMode = ResizeMode.NoResize;
WindowStartupLocation = WindowStartupLocation.CenterScreen;
Topmost = true;
FontFamily = new FontFamily("Malgun Gothic");
FontSize = 13;
Background = Brushes2.Make("#FAFAFA");
AppIcon.Apply(this);
var root = new StackPanel { Margin = new Thickness(20, 18, 20, 16) };
root.Children.Add(new TextBlock
{
Text = control ? "원격 화면 보기 + 키보드·마우스 제어 요청" : "원격 화면 보기 요청",
FontSize = 15,
FontWeight = FontWeights.Bold,
Foreground = Brushes2.Make(control ? "#B71C1C" : "#155E75"),
Margin = new Thickness(0, 0, 0, 10)
});
root.Children.Add(new TextBlock
{
Text = $"관리 PC [{who}] 가 이 PC 화면을 {(control ? "보고 조작" : "보")}려고 합니다.\n허용할까요?",
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 10)
});
_countdown = new TextBlock { Foreground = Brushes.Gray };
root.Children.Add(_countdown);
var buttons = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 12, 0, 0) };
var allow = new Button { Content = "허용", MinWidth = 90, FontWeight = FontWeights.Bold };
var deny = new Button { Content = "거부", MinWidth = 90, IsCancel = true };
allow.Click += (s, e) => Finish(true);
deny.Click += (s, e) => Finish(false);
buttons.Children.Add(allow);
buttons.Children.Add(deny);
root.Children.Add(buttons);
Content = root;
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_timer.Tick += (s, e) =>
{
_left--;
UpdateCountdown();
if (_left <= 0) Finish(false);
};
UpdateCountdown();
Closed += (s, e) => { _timer.Stop(); _result.TrySetResult(false); };
Loaded += (s, e) => { _timer.Start(); Activate(); };
}
private void UpdateCountdown() => _countdown.Text = $"{_left}초 뒤 자동으로 거부됩니다.";
private void Finish(bool ok)
{
_result.TrySetResult(ok);
Close();
}
/// UI 스레드에서 호출
public static Task AskAsync(string who, bool control)
{
var w = new ApprovalWindow(who, control);
w.Show();
return w._result.Task;
}
}
/// 대상 PC: 원격 화면 연결 중 화면 위쪽 표시줄 (항상 표시)
public sealed class RemoteIndicatorWindow : Window
{
private readonly TextBlock _text;
private readonly Border _bar;
public event Action DisconnectRequested;
public RemoteIndicatorWindow()
{
WindowStyle = WindowStyle.None;
ResizeMode = ResizeMode.NoResize;
AllowsTransparency = true;
Background = Brushes.Transparent;
Topmost = true;
ShowInTaskbar = false;
ShowActivated = false;
SizeToContent = SizeToContent.WidthAndHeight;
FontFamily = new FontFamily("Malgun Gothic");
FontSize = 13;
_bar = new Border
{
CornerRadius = new CornerRadius(0, 0, 8, 8),
Padding = new Thickness(14, 6, 8, 6),
Background = Brushes2.Make("#E65100")
};
var panel = new StackPanel { Orientation = Orientation.Horizontal };
_text = new TextBlock { Foreground = Brushes.White, FontWeight = FontWeights.Bold, VerticalAlignment = VerticalAlignment.Center };
var stop = new Button
{
Content = "연결 끊기",
Margin = new Thickness(12, 0, 0, 0),
MinWidth = 0,
MinHeight = 0,
Padding = new Thickness(8, 2, 8, 2),
Background = Brushes.White
};
stop.Click += (s, e) => DisconnectRequested?.Invoke();
panel.Children.Add(_text);
panel.Children.Add(stop);
_bar.Child = panel;
Content = _bar;
SizeChanged += (s, e) => Reposition();
}
public void Update(IReadOnlyList viewers)
{
if (viewers.Count == 0)
{
Hide();
return;
}
bool control = viewers.Any(v => v.Control);
_bar.Background = Brushes2.Make(control ? "#C62828" : "#E65100");
_text.Text = $"● 원격 {(control ? "제어" : "보기")} 중: {string.Join(", ", viewers.Select(v => v.Who).Distinct())}";
if (!IsVisible) Show();
Reposition();
}
private void Reposition()
{
var wa = SystemParameters.WorkArea;
Left = wa.Left + (wa.Width - ActualWidth) / 2;
Top = wa.Top;
}
}
/// 관리 PC: 원격 화면 창 (보기 / 제어)
public sealed class RemoteViewerWindow : Window
{
private readonly RemoteClient _client;
private readonly string _label;
private readonly Image _image;
private readonly TextBlock _status, _overlay;
private readonly StackPanel _monitorButtons;
private readonly HashSet _pressedKeys = new HashSet();
private int _sid;
private bool _control;
private bool _ended;
private int _monitor = 1;
private int _frames;
private DateTime _fpsStart = DateTime.UtcNow;
private int _decoding;
private DateTime _lastMove = DateTime.MinValue;
public string ViewerKey { get; }
public RemoteViewerWindow(string key, RemoteClient client, string label)
{
ViewerKey = key;
_client = client;
_label = label;
Title = $"원격관리 - {label}" + AppInstance.TitleTag;
Width = 1280;
Height = 800;
WindowStartupLocation = WindowStartupLocation.CenterScreen;
FontFamily = new FontFamily("Malgun Gothic");
FontSize = 13;
Background = Brushes2.Make("#263238");
AppIcon.Apply(this);
// 한글 입력기는 대상 PC에서 처리하도록 이 창에서는 끔
InputMethod.SetIsInputMethodEnabled(this, false);
var root = new DockPanel();
// ── 도구 모음
var bar = new DockPanel { Background = Brushes2.Make("#ECEFF1") };
DockPanel.SetDock(bar, Dock.Top);
var close = new Button { Content = "연결 끊기" };
close.Click += (s, e) => Close();
DockPanel.SetDock(close, Dock.Right);
bar.Children.Add(close);
_status = new TextBlock { VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(12, 0, 12, 0), Foreground = Brushes2.Make("#37474F") };
DockPanel.SetDock(_status, Dock.Right);
bar.Children.Add(_status);
_monitorButtons = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(4) };
bar.Children.Add(_monitorButtons);
root.Children.Add(bar);
// ── 화면
var stage = new Grid { Background = Brushes.Black, ClipToBounds = true };
_image = new Image
{
Stretch = Stretch.Uniform,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Focusable = true,
Cursor = Cursors.Arrow
};
RenderOptions.SetBitmapScalingMode(_image, BitmapScalingMode.HighQuality);
_overlay = new TextBlock
{
Text = "연결 중…",
Foreground = Brushes.White,
FontSize = 16,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Center,
TextWrapping = TextWrapping.Wrap
};
stage.Children.Add(_image);
stage.Children.Add(_overlay);
root.Children.Add(stage);
Content = root;
_image.MouseMove += OnMouseMove;
_image.MouseDown += OnMouseDown;
_image.MouseUp += OnMouseUp;
_image.MouseWheel += OnMouseWheel;
PreviewKeyDown += OnKey;
PreviewKeyUp += OnKey;
Deactivated += (s, e) => ReleaseKeys();
_client.ScreenEvent += OnScreenEvent;
Loaded += async (s, e) => await StartAsync();
Closed += (s, e) => Stop();
}
// ───────────────────────── 시작 / 끝 ─────────────────────────
private async Task StartAsync()
{
try
{
_overlay.Text = "대상 PC 정보를 확인하는 중…";
var info = await _client.RequestAsync("screenInfo", null, TimeSpan.FromSeconds(15));
if (!Json.Bool(info, "allowView"))
{
End("대상 PC에서 화면 보기를 허용하지 않았습니다.\n(대상 PC 설정 → 원격 접속 허용 → 화면 보기 허용)");
return;
}
var monitors = (info["monitors"] as JsonArray)?.OfType().ToList() ?? new List();
var primary = monitors.FirstOrDefault(m => Json.Bool(m, "primary"));
_monitor = primary != null ? Json.Int(primary, "index", 1) : 1;
foreach (var m in monitors)
{
int idx = Json.Int(m, "index", 1);
AddMonitorButton($"모니터 {idx}" + (Json.Bool(m, "primary") ? " (주)" : ""), idx,
$"{Json.Int(m, "width")} × {Json.Int(m, "height")}");
}
if (monitors.Count > 1) AddMonitorButton("전체", 0, "모든 모니터를 한 화면에");
HighlightMonitor();
_overlay.Text = "대상 PC의 승인을 기다리는 중…";
var r = await _client.RequestAsync("screenStart", new JsonObject { ["monitor"] = _monitor }, TimeSpan.FromSeconds(45));
_sid = Json.Int(r, "sid");
_control = Json.Bool(r, "control");
_overlay.Text = "";
UpdateStatus();
_image.Cursor = _control ? Cursors.Cross : Cursors.Arrow;
_image.Focus();
}
catch (Exception ex)
{
End(RemoteClient.Friendly(ex));
}
}
private void AddMonitorButton(string text, int index, string tip)
{
var b = new Button { Content = text, Tag = index, ToolTip = tip, MinWidth = 70 };
b.Click += async (s, e) =>
{
_monitor = index;
HighlightMonitor();
try { await _client.RequestAsync("screenMonitor", new JsonObject { ["monitor"] = index }, TimeSpan.FromSeconds(10)); }
catch { }
_image.Focus();
};
_monitorButtons.Children.Add(b);
}
private void HighlightMonitor()
{
foreach (Button b in _monitorButtons.Children)
{
bool on = (int)b.Tag == _monitor;
b.FontWeight = on ? FontWeights.Bold : FontWeights.Normal;
b.Background = on ? Brushes2.Make("#BBDEFB") : Brushes.White;
}
}
private void End(string reason)
{
_ended = true;
_overlay.Text = reason;
_status.Text = "연결 끝";
_image.Cursor = Cursors.Arrow;
}
private void Stop()
{
_client.ScreenEvent -= OnScreenEvent;
ReleaseKeys();
if (_sid > 0 && !_ended)
_ = _client.RequestAsync("screenStop", null, TimeSpan.FromSeconds(5)).ContinueWith(t => { _ = t.Exception; });
}
private void UpdateStatus()
{
var secs = (DateTime.UtcNow - _fpsStart).TotalSeconds;
double fps = secs > 0 ? _frames / secs : 0;
if (secs > 3) { _frames = 0; _fpsStart = DateTime.UtcNow; }
_status.Text = $"{(_control ? "● 제어 중" : "보기 전용")} · {fps:0.0}장/초";
_status.Foreground = _control ? Brushes2.Stopped : Brushes2.Make("#37474F");
}
// ───────────────────────── 화면 받기 ─────────────────────────
private void OnScreenEvent(JsonObject msg)
{
var ev = Json.Str(msg, "event");
int sid = Json.Int(msg, "sid");
if (ev == "screenEnd")
{
if (sid == 0 || sid == _sid)
{
var reason = Json.Str(msg, "reason");
Dispatcher.BeginInvoke(new Action(() => End("원격 화면 연결이 끝났습니다.\n" + reason)));
}
return;
}
if (ev != "frame" || sid != _sid || _ended) return;
// 화면 그리기가 밀리면 이전 장면은 건너뜀
if (Interlocked.Exchange(ref _decoding, 1) == 1) return;
try
{
var bytes = Convert.FromBase64String(Json.Str(msg, "data"));
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.StreamSource = new MemoryStream(bytes);
bmp.EndInit();
bmp.Freeze();
Dispatcher.BeginInvoke(new Action(() =>
{
_image.Source = bmp;
_frames++;
UpdateStatus();
Interlocked.Exchange(ref _decoding, 0);
}), DispatcherPriority.Render);
}
catch
{
Interlocked.Exchange(ref _decoding, 0);
}
}
// ───────────────────────── 입력 보내기 ─────────────────────────
private bool CanControl => _control && !_ended && _sid > 0;
private (double x, double y) Norm(MouseEventArgs e)
{
var p = e.GetPosition(_image);
double w = Math.Max(1, _image.ActualWidth), h = Math.Max(1, _image.ActualHeight);
return (Math.Clamp(p.X / w, 0, 1), Math.Clamp(p.Y / h, 0, 1));
}
private void SendMouse(MouseEventArgs e, string action, string button = "", int delta = 0)
{
var (x, y) = Norm(e);
_client.SendNoReply(new JsonObject
{
["op"] = "input",
["kind"] = "mouse",
["action"] = action,
["button"] = button,
["x"] = Math.Round(x, 5),
["y"] = Math.Round(y, 5),
["delta"] = delta
});
}
private static string ButtonName(MouseButton b) => b switch
{
MouseButton.Right => "right",
MouseButton.Middle => "middle",
_ => "left"
};
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (!CanControl) return;
// 너무 자주 보내지 않도록 (약 30회/초)
if ((DateTime.UtcNow - _lastMove).TotalMilliseconds < 33) return;
_lastMove = DateTime.UtcNow;
SendMouse(e, "move");
}
private void OnMouseDown(object sender, MouseButtonEventArgs e)
{
_image.Focus();
if (!CanControl) return;
_image.CaptureMouse();
SendMouse(e, "down", ButtonName(e.ChangedButton));
e.Handled = true;
}
private void OnMouseUp(object sender, MouseButtonEventArgs e)
{
if (!CanControl) return;
SendMouse(e, "up", ButtonName(e.ChangedButton));
if (Mouse.LeftButton == MouseButtonState.Released && Mouse.RightButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released)
_image.ReleaseMouseCapture();
e.Handled = true;
}
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
{
if (!CanControl) return;
SendMouse(e, "wheel", "", e.Delta);
e.Handled = true;
}
private void OnKey(object sender, KeyEventArgs e)
{
if (!CanControl) return;
var key = e.Key switch
{
Key.System => e.SystemKey,
Key.ImeProcessed => e.ImeProcessedKey,
Key.DeadCharProcessed => e.DeadCharProcessedKey,
_ => e.Key
};
int vk = KeyInterop.VirtualKeyFromKey(key);
if (vk <= 0) return;
bool down = e.RoutedEvent == Keyboard.PreviewKeyDownEvent;
if (down) _pressedKeys.Add(vk); else _pressedKeys.Remove(vk);
_client.SendNoReply(new JsonObject { ["op"] = "input", ["kind"] = "key", ["vk"] = vk, ["down"] = down });
e.Handled = true;
}
/// 창을 벗어나면 눌린 키가 대상 PC에 남지 않도록 모두 뗌
private void ReleaseKeys()
{
if (_pressedKeys.Count == 0) return;
foreach (var vk in _pressedKeys.ToList())
_client.SendNoReply(new JsonObject { ["op"] = "input", ["kind"] = "key", ["vk"] = vk, ["down"] = false });
_pressedKeys.Clear();
}
}
}