using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Wpf; using System.Diagnostics; using System.IO; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using DrawingColor = System.Drawing.Color; namespace SHD.Windows; public partial class MainWindow : Window { private const string WebViewProfileFolder = "WebView2"; private static readonly Uri WebViewRuntimePage = new("https://developer.microsoft.com/microsoft-edge/webview2/"); private static readonly Lazy> SharedWebViewEnvironment = new(CreateWebViewEnvironmentAsync); private readonly List _tabs = []; private readonly ShdEnvironment _initialEnvironment; private CoreWebView2Environment? _webViewEnvironment; private ShdEnvironment _selectedEnvironment; private PendingDesktopAuth? _authAttempt; private MemoryStream? _authPostData; private WebView2? _authPostTarget; private ulong? _authNavigationId; private bool _suppressEnvironmentChange; private bool _initialized; private bool _closing; private bool _isFullScreen; private WindowStyle _previousWindowStyle; private WindowState _previousWindowState; private Rect _previousBounds; public MainWindow(ShdEnvironment? initialEnvironment = null) { InitializeComponent(); _initialEnvironment = initialEnvironment ?? SettingsStore.LoadEnvironment(); _selectedEnvironment = _initialEnvironment; EnvironmentPicker.IsEnabled = false; Tabs.IsEnabled = false; } private BrowserTab? ActiveTab => Tabs.SelectedItem is TabItem item ? _tabs.FirstOrDefault(tab => ReferenceEquals(tab.Item, item)) : null; private async void Window_Loaded(object sender, RoutedEventArgs e) { if (_initialized) return; _initialized = true; try { _webViewEnvironment = await SharedWebViewEnvironment.Value; SetEnvironmentPicker(_initialEnvironment); EnvironmentPicker.IsEnabled = true; Tabs.IsEnabled = true; await AddTabAsync(_initialEnvironment); StatusText.Text = "Готово"; } catch (Exception) { var result = MessageBox.Show( this, "Не удалось запустить встроенный браузер SHD. Установите Microsoft Edge WebView2 Runtime и запустите приложение ещё раз.\n\nОткрыть страницу загрузки Runtime?", "SHD — встроенный браузер", MessageBoxButton.YesNo, MessageBoxImage.Warning); if (result == MessageBoxResult.Yes) OpenExternalUri(WebViewRuntimePage); Close(); } } private static async Task CreateWebViewEnvironmentAsync() { var userDataFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SHDDesktop", WebViewProfileFolder); Directory.CreateDirectory(userDataFolder); return await CoreWebView2Environment.CreateAsync(userDataFolder: userDataFolder); } private async Task AddTabAsync(ShdEnvironment environment, Uri? initialUri = null) { if (_webViewEnvironment is null) throw new InvalidOperationException("WebView2 environment is not ready."); var view = new WebView2 { HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch, DefaultBackgroundColor = DrawingColor.FromArgb(18, 18, 18) }; var progress = new ProgressBar { Height = 2, IsIndeterminate = true, Visibility = Visibility.Collapsed, VerticalAlignment = VerticalAlignment.Top, HorizontalAlignment = HorizontalAlignment.Stretch, IsHitTestVisible = false, Foreground = new SolidColorBrush(Color.FromRgb(84, 156, 255)), Background = Brushes.Transparent }; var content = new Grid { Background = new SolidColorBrush(Color.FromRgb(18, 18, 18)) }; content.Children.Add(view); Panel.SetZIndex(progress, 1); content.Children.Add(progress); var item = new TabItem { Background = new SolidColorBrush(Color.FromRgb(34, 34, 34)), Foreground = Brushes.White, Content = content }; var headerTitle = new TextBlock { Text = "SHD", VerticalAlignment = VerticalAlignment.Center, TextTrimming = TextTrimming.CharacterEllipsis, MaxWidth = 210, Margin = new Thickness(2, 0, 7, 0) }; var closeButton = new Button { Content = "×", Width = 22, Height = 22, Padding = new Thickness(0), Margin = new Thickness(3, 0, 0, 0), FontSize = 15, ToolTip = "Закрыть вкладку", VerticalAlignment = VerticalAlignment.Center }; var header = new DockPanel { LastChildFill = true, Margin = new Thickness(2, 0, 2, 0) }; DockPanel.SetDock(closeButton, Dock.Right); header.Children.Add(closeButton); header.Children.Add(headerTitle); item.Header = header; var tab = new BrowserTab(view, item, progress, headerTitle, environment); closeButton.Click += (_, _) => CloseTab(tab); _tabs.Add(tab); Tabs.Items.Add(item); Tabs.SelectedItem = item; await view.EnsureCoreWebView2Async(_webViewEnvironment); await ConfigureBrowserAsync(tab); view.CoreWebView2.Navigate((initialUri ?? new Uri(environment.BaseUrl())).AbsoluteUri); UpdateWindowChrome(); return tab; } private async Task ConfigureBrowserAsync(BrowserTab tab) { var core = tab.View.CoreWebView2; core.DefaultBackgroundColor = DrawingColor.FromArgb(18, 18, 18); core.Settings.IsStatusBarEnabled = false; core.Settings.AreDevToolsEnabled = true; core.Settings.IsZoomControlEnabled = true; core.Settings.IsPasswordAutosaveEnabled = true; core.Settings.IsGeneralAutofillEnabled = true; core.Settings.AreDefaultScriptDialogsEnabled = false; core.NavigationStarting += Browser_NavigationStarting; core.NavigationCompleted += Browser_NavigationCompleted; core.DocumentTitleChanged += Browser_DocumentTitleChanged; core.WebMessageReceived += Browser_WebMessageReceived; core.NewWindowRequested += Browser_NewWindowRequested; core.PermissionRequested += Browser_PermissionRequested; core.DownloadStarting += Browser_DownloadStarting; core.ScriptDialogOpening += Browser_ScriptDialogOpening; await core.AddScriptToExecuteOnDocumentCreatedAsync(""" (() => { const darkStyle = document.createElement('style'); darkStyle.id = '__shd_desktop_dark__'; darkStyle.textContent = 'html, body { background-color: #121212 !important; color: #e0e0e0 !important; }'; const parent = document.head || document.documentElement; if (parent) parent.appendChild(darkStyle); window.print = function () { if (window.chrome && window.chrome.webview) window.chrome.webview.postMessage('shd-print'); }; })(); """); await core.AddScriptToExecuteOnDocumentCreatedAsync(""" (() => { if (window.top !== window) return; document.addEventListener('click', event => { let path = window.location.pathname; while (path.length > 1 && path.endsWith('/')) path = path.slice(0, -1); if (path !== '/login' && path !== '/login/two-factor') return; const target = event.target instanceof Element ? event.target : (event.target && event.target.parentElement); if (!target || !target.closest('#btnPasskeyLogin, #twoFactorPasskey')) return; event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); if (window.chrome && window.chrome.webview) window.chrome.webview.postMessage('shd-passkey'); }, true); })(); """); } private void Browser_NavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs e) { if (sender is not CoreWebView2 core) return; var tab = _tabs.FirstOrDefault(candidate => ReferenceEquals(candidate.View.CoreWebView2, core)); if (tab is not null) tab.Progress.Visibility = Visibility.Visible; if (!Uri.TryCreate(e.Uri, UriKind.Absolute, out var uri)) return; if (tab is not null && ReferenceEquals(tab.View, _authPostTarget) && uri.AbsolutePath.Equals("/desktop-auth/complete", StringComparison.Ordinal)) _authNavigationId = e.NavigationId; if (uri.Scheme is not ("https" or "http")) { e.Cancel = true; if (e.IsUserInitiated) OpenExternalUri(uri); return; } if (tab is not null) { if (ShdEnvironmentExtensions.IsDevelopmentHost(uri.Host)) tab.Environment = ShdEnvironment.Dev; else if (ShdEnvironmentExtensions.IsProductionHost(uri.Host)) tab.Environment = ShdEnvironment.Production; if (ReferenceEquals(tab, ActiveTab)) { _selectedEnvironment = tab.Environment; SetEnvironmentPicker(tab.Environment); SettingsStore.SaveEnvironment(tab.Environment); StatusEnvironment.Text = tab.Environment.DisplayName(); } } if (e.IsUserInitiated && !e.IsRedirected && !ShdEnvironmentExtensions.IsInternalHost(uri.Host)) { e.Cancel = true; OpenExternalUri(uri); } } private void Browser_NavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e) { if (sender is not CoreWebView2 core) return; var tab = _tabs.FirstOrDefault(candidate => ReferenceEquals(candidate.View.CoreWebView2, core)); if (tab is not null) tab.Progress.Visibility = Visibility.Collapsed; UpdateWindowChrome(); if (tab is not null && ReferenceEquals(_authPostTarget, tab.View) && _authNavigationId == e.NavigationId) { _authPostData?.Dispose(); _authPostData = null; _authPostTarget = null; _authNavigationId = null; StatusText.Text = e.IsSuccess ? "Вход завершён" : "Не удалось завершить вход"; } } private void Browser_DocumentTitleChanged(object? sender, object e) { if (sender is not CoreWebView2 core) return; var tab = _tabs.FirstOrDefault(candidate => ReferenceEquals(candidate.View.CoreWebView2, core)); if (tab is null) return; var title = CleanTitle(core.DocumentTitle); tab.Title.Text = title; if (ReferenceEquals(tab, ActiveTab)) { PageTitle.Text = title; Title = $"{title} — SHD"; } } private async void Browser_WebMessageReceived(object? sender, CoreWebView2WebMessageReceivedEventArgs e) { if (sender is not WebView2 view || !Uri.TryCreate(e.Source, UriKind.Absolute, out var source) || source.Scheme != Uri.UriSchemeHttps || !ShdEnvironmentExtensions.IsInternalHost(source.Host)) return; string message; try { message = e.TryGetWebMessageAsString(); } catch (InvalidOperationException) { return; } if (message == "shd-print") { view.CoreWebView2.ShowPrintUI(CoreWebView2PrintDialogKind.Browser); return; } if (message != "shd-passkey") return; if (!(ShdEnvironmentExtensions.IsDevelopmentHost(source.Host) || ShdEnvironmentExtensions.IsProductionHost(source.Host))) return; var tab = _tabs.FirstOrDefault(candidate => ReferenceEquals(candidate.View, view)); if (tab is null) return; var environment = ShdEnvironmentExtensions.IsDevelopmentHost(source.Host) ? ShdEnvironment.Dev : ShdEnvironment.Production; await BeginPasskeyLoginAsync(tab, environment); } private async void Browser_NewWindowRequested(object? sender, CoreWebView2NewWindowRequestedEventArgs e) { e.Handled = true; if (!Uri.TryCreate(e.Uri, UriKind.Absolute, out var uri)) return; if (uri.Scheme is "http" or "https" && ShdEnvironmentExtensions.IsInternalHost(uri.Host)) { var environment = ShdEnvironmentExtensions.IsDevelopmentHost(uri.Host) ? ShdEnvironment.Dev : ShdEnvironmentExtensions.IsProductionHost(uri.Host) ? ShdEnvironment.Production : _selectedEnvironment; await AddTabAsync(environment, uri); } else { OpenExternalUri(uri); } } private void Browser_PermissionRequested(object? sender, CoreWebView2PermissionRequestedEventArgs e) { if (e.PermissionKind is not (CoreWebView2PermissionKind.Camera or CoreWebView2PermissionKind.Microphone)) return; var trusted = Uri.TryCreate(e.Uri, UriKind.Absolute, out var origin) && origin.Scheme == Uri.UriSchemeHttps && ShdEnvironmentExtensions.IsInternalHost(origin.Host); if (trusted) { e.State = CoreWebView2PermissionState.Allow; } else { var label = e.PermissionKind == CoreWebView2PermissionKind.Camera ? "камеру" : "микрофон"; var allow = MessageBox.Show( this, $"Разрешить сайту {e.Uri} доступ к устройству ({label})?", "Разрешение SHD", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes; e.State = allow ? CoreWebView2PermissionState.Allow : CoreWebView2PermissionState.Deny; } e.Handled = true; } private void Browser_DownloadStarting(object? sender, CoreWebView2DownloadStartingEventArgs e) { var downloadsFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); downloadsFolder = Path.Combine(downloadsFolder, "Downloads"); Directory.CreateDirectory(downloadsFolder); var suggestedName = Path.GetFileName(e.ResultFilePath); if (string.IsNullOrWhiteSpace(suggestedName)) suggestedName = "download"; e.ResultFilePath = GetUniquePath(Path.Combine(downloadsFolder, suggestedName)); e.Handled = true; StatusText.Text = $"Загрузка: {suggestedName}"; e.DownloadOperation.StateChanged += (_, _) => { Dispatcher.InvokeAsync(() => { if (e.DownloadOperation.State == CoreWebView2DownloadState.Completed) StatusText.Text = $"Загружено: {Path.GetFileName(e.ResultFilePath)}"; else if (e.DownloadOperation.State == CoreWebView2DownloadState.Interrupted) StatusText.Text = "Загрузка прервана"; }); }; } private void Browser_ScriptDialogOpening(object? sender, CoreWebView2ScriptDialogOpeningEventArgs e) { switch (e.Kind) { case CoreWebView2ScriptDialogKind.Alert: MessageBox.Show(this, e.Message, "SHD", MessageBoxButton.OK, MessageBoxImage.Information); e.Accept(); break; case CoreWebView2ScriptDialogKind.Confirm: case CoreWebView2ScriptDialogKind.BeforeUnload: if (MessageBox.Show(this, e.Message, "SHD", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes) e.Accept(); break; case CoreWebView2ScriptDialogKind.Prompt: var answer = ShowPrompt(e.Message, e.DefaultText); if (answer is not null) { e.ResultText = answer; e.Accept(); } break; } } private async Task BeginPasskeyLoginAsync(BrowserTab tab, ShdEnvironment environment) { if (_authAttempt is not null) { MessageBox.Show(this, "Вход уже выполняется. Завершите его в браузере или закройте окно входа.", "Вход в SHD", MessageBoxButton.OK, MessageBoxImage.Information); return; } if (environment != ShdEnvironment.Dev) { MessageBox.Show(this, "Вход по passkey в приложении пока подключён только для DEV.", "Вход в SHD", MessageBoxButton.OK, MessageBoxImage.Information); return; } try { var attempt = DesktopAuthFlow.Create(environment, tab); _authAttempt = attempt; Process.Start(new ProcessStartInfo(attempt.AuthorizationUri.AbsoluteUri) { UseShellExecute = true }); StatusText.Text = "Продолжите вход в системном браузере"; } catch (Exception) { _authAttempt = null; MessageBox.Show(this, "Не удалось открыть системный браузер для входа. Попробуйте ещё раз.", "Вход в SHD", MessageBoxButton.OK, MessageBoxImage.Warning); } await Task.CompletedTask; } public bool CanHandleAuthCallback(string callbackValue) { if (_authAttempt is null) return false; if (!DesktopAuthFlow.TryParseCallback(callbackValue, out var callback, out _)) return false; return callback is not null && callback.State == _authAttempt.State; } public async void HandleAuthCallback(string callbackValue) { if (!DesktopAuthFlow.TryParseCallback(callbackValue, out var callback, out var serverError)) { MessageBox.Show(this, "Сервер вернул некорректный ответ входа.", "Вход в SHD", MessageBoxButton.OK, MessageBoxImage.Warning); return; } var attempt = _authAttempt; if (attempt is null) { MessageBox.Show(this, "Этот ответ входа уже нельзя использовать. Нажмите кнопку passkey на странице и начните заново.", "Вход в SHD", MessageBoxButton.OK, MessageBoxImage.Information); return; } if (callback is null || callback.State != attempt.State) { MessageBox.Show(this, "Не удалось подтвердить ответ сервера. Начните вход заново.", "Вход в SHD", MessageBoxButton.OK, MessageBoxImage.Warning); return; } if (serverError) { _authAttempt = null; MessageBox.Show(this, "Сервер не завершил вход. Попробуйте ещё раз.", "Вход в SHD", MessageBoxButton.OK, MessageBoxImage.Warning); return; } if (!_tabs.Contains(attempt.TargetTab) || attempt.TargetTab.View.CoreWebView2 is null) { _authAttempt = null; MessageBox.Show(this, "Вкладка, из которой начался вход, уже закрыта. Начните вход заново.", "Вход в SHD", MessageBoxButton.OK, MessageBoxImage.Warning); return; } try { _authAttempt = null; var formBody = $"code={DesktopAuthFlow.FormEncode(callback.Code)}&code_verifier={DesktopAuthFlow.FormEncode(attempt.Verifier)}"; _authPostData?.Dispose(); _authPostData = new MemoryStream(Encoding.UTF8.GetBytes(formBody)); _authPostTarget = attempt.TargetTab.View; _authNavigationId = null; var request = _webViewEnvironment!.CreateWebResourceRequest( DesktopAuthFlow.CompleteUri(attempt.Environment).AbsoluteUri, "POST", _authPostData, "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\nCache-Control: no-store\r\n"); attempt.TargetTab.Environment = attempt.Environment; Tabs.SelectedItem = attempt.TargetTab.Item; SetEnvironmentPicker(attempt.Environment); SettingsStore.SaveEnvironment(attempt.Environment); attempt.TargetTab.View.CoreWebView2.NavigateWithWebResourceRequest(request); StatusText.Text = "Завершаем вход…"; } catch (Exception) { _authPostData?.Dispose(); _authPostData = null; _authPostTarget = null; _authNavigationId = null; MessageBox.Show(this, "Не удалось завершить вход в выбранном контуре. Начните вход заново.", "Вход в SHD", MessageBoxButton.OK, MessageBoxImage.Warning); } await Task.CompletedTask; } private void SetEnvironment(ShdEnvironment environment) { _selectedEnvironment = environment; SettingsStore.SaveEnvironment(environment); SetEnvironmentPicker(environment); StatusEnvironment.Text = environment.DisplayName(); var tab = ActiveTab; if (tab is null || tab.View.CoreWebView2 is null) return; tab.Environment = environment; tab.View.CoreWebView2.Navigate(environment.BaseUrl()); } private void SetEnvironmentPicker(ShdEnvironment environment) { _suppressEnvironmentChange = true; EnvironmentPicker.SelectedValue = environment.ToString(); _suppressEnvironmentChange = false; } private async void NewTab_Click(object sender, RoutedEventArgs e) { if (_webViewEnvironment is null) return; await AddTabAsync(_selectedEnvironment); } private void NewWindow_Click(object sender, RoutedEventArgs e) { var window = new MainWindow(_selectedEnvironment); window.Show(); } private void CloseTab_Click(object sender, RoutedEventArgs e) { if (ActiveTab is { } tab) CloseTab(tab); } private void CloseTab(BrowserTab tab) { if (_authAttempt?.TargetTab == tab) _authAttempt = null; if (ReferenceEquals(_authPostTarget, tab.View)) { _authPostData?.Dispose(); _authPostData = null; _authPostTarget = null; _authNavigationId = null; } tab.View.Dispose(); _tabs.Remove(tab); Tabs.Items.Remove(tab.Item); if (_tabs.Count == 0 && !_closing) _ = AddTabAsync(_selectedEnvironment); UpdateWindowChrome(); } private void Tabs_SelectionChanged(object sender, SelectionChangedEventArgs e) { var tab = ActiveTab; if (tab is null) return; _selectedEnvironment = tab.Environment; SetEnvironmentPicker(tab.Environment); StatusEnvironment.Text = tab.Environment.DisplayName(); PageTitle.Text = tab.Title.Text; Title = $"{CleanTitle(tab.View.CoreWebView2?.DocumentTitle ?? "SHD")} — SHD"; UpdateNavigationButtons(); } private void EnvironmentPicker_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_suppressEnvironmentChange || !Enum.TryParse(EnvironmentPicker.SelectedValue as string, out var environment)) return; if (environment == _selectedEnvironment && ActiveTab?.Environment == environment) return; SetEnvironment(environment); } private void Window_PreviewKeyDown(object sender, KeyEventArgs e) { var modifiers = Keyboard.Modifiers; if (modifiers == ModifierKeys.Control) { switch (e.Key) { case Key.T: NewTab_Click(sender, e); e.Handled = true; break; case Key.N: NewWindow_Click(sender, e); e.Handled = true; break; case Key.W: CloseTab_Click(sender, e); e.Handled = true; break; case Key.R: Reload_Click(sender, e); e.Handled = true; break; case Key.P: Print_Click(sender, e); e.Handled = true; break; case Key.D0: ZoomReset_Click(sender, e); e.Handled = true; break; case Key.Add: case Key.OemPlus: ZoomIn_Click(sender, e); e.Handled = true; break; case Key.Subtract: case Key.OemMinus: ZoomOut_Click(sender, e); e.Handled = true; break; } } else if (modifiers == (ModifierKeys.Control | ModifierKeys.Shift) && e.Key == Key.R) { HardReload_Click(sender, e); e.Handled = true; } else if (e.Key == Key.F12) { DevTools_Click(sender, e); e.Handled = true; } else if (e.Key == Key.F11) { FullScreen_Click(sender, e); e.Handled = true; } else if (modifiers == ModifierKeys.Alt && e.Key == Key.Left) { Back_Click(sender, e); e.Handled = true; } else if (modifiers == ModifierKeys.Alt && e.Key == Key.Right) { Forward_Click(sender, e); e.Handled = true; } } private void Back_Click(object sender, RoutedEventArgs e) { if (ActiveTab?.View.CoreWebView2?.CanGoBack == true) ActiveTab.View.CoreWebView2.GoBack(); } private void Forward_Click(object sender, RoutedEventArgs e) { if (ActiveTab?.View.CoreWebView2?.CanGoForward == true) ActiveTab.View.CoreWebView2.GoForward(); } private void Reload_Click(object sender, RoutedEventArgs e) => ActiveTab?.View.CoreWebView2?.Reload(); private async void HardReload_Click(object sender, RoutedEventArgs e) { var core = ActiveTab?.View.CoreWebView2; if (core is null) return; try { await core.CallDevToolsProtocolMethodAsync("Page.reload", "{\"ignoreCache\":true}"); } catch (InvalidOperationException) { core.Reload(); } } private void ZoomIn_Click(object sender, RoutedEventArgs e) { if (ActiveTab is { } tab) tab.View.ZoomFactor = Math.Min(tab.View.ZoomFactor + 0.1, 3.0); } private void ZoomOut_Click(object sender, RoutedEventArgs e) { if (ActiveTab is { } tab) tab.View.ZoomFactor = Math.Max(tab.View.ZoomFactor - 0.1, 0.4); } private void ZoomReset_Click(object sender, RoutedEventArgs e) { if (ActiveTab is { } tab) tab.View.ZoomFactor = 1.0; } private void Print_Click(object sender, RoutedEventArgs e) => ActiveTab?.View.CoreWebView2?.ShowPrintUI(CoreWebView2PrintDialogKind.Browser); private void DevTools_Click(object sender, RoutedEventArgs e) => ActiveTab?.View.CoreWebView2?.OpenDevToolsWindow(); private void FullScreen_Click(object sender, RoutedEventArgs e) { if (!_isFullScreen) { _previousBounds = RestoreBounds; _previousWindowState = WindowState; _previousWindowStyle = WindowStyle; WindowState = WindowState.Normal; WindowStyle = WindowStyle.None; WindowState = WindowState.Maximized; _isFullScreen = true; return; } WindowStyle = _previousWindowStyle; WindowState = WindowState.Normal; Left = _previousBounds.Left; Top = _previousBounds.Top; Width = _previousBounds.Width; Height = _previousBounds.Height; WindowState = _previousWindowState; _isFullScreen = false; } private void Downloads_Click(object sender, RoutedEventArgs e) { var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"); Directory.CreateDirectory(path); OpenExternalUri(new Uri(path)); } private void SelectProduction_Click(object sender, RoutedEventArgs e) => SetEnvironment(ShdEnvironment.Production); private void SelectDev_Click(object sender, RoutedEventArgs e) => SetEnvironment(ShdEnvironment.Dev); private void Exit_Click(object sender, RoutedEventArgs e) => Application.Current.Shutdown(); private void Undo_Click(object sender, RoutedEventArgs e) => ExecuteEditCommand("undo"); private void Redo_Click(object sender, RoutedEventArgs e) => ExecuteEditCommand("redo"); private void Cut_Click(object sender, RoutedEventArgs e) => ExecuteEditCommand("cut"); private void Copy_Click(object sender, RoutedEventArgs e) => ExecuteEditCommand("copy"); private void Paste_Click(object sender, RoutedEventArgs e) => ExecuteEditCommand("paste"); private void SelectAll_Click(object sender, RoutedEventArgs e) => ExecuteEditCommand("selectAll"); private void ExecuteEditCommand(string command) { var escaped = command.Replace("'", "\\'", StringComparison.Ordinal); _ = ActiveTab?.View.CoreWebView2?.ExecuteScriptAsync($"document.execCommand('{escaped}')"); } private void UpdateWindowChrome() { if (ActiveTab is { } tab) { PageTitle.Text = tab.Title.Text; StatusEnvironment.Text = tab.Environment.DisplayName(); } UpdateNavigationButtons(); } private void UpdateNavigationButtons() { BackButton.IsEnabled = ActiveTab?.View.CoreWebView2?.CanGoBack == true; ForwardButton.IsEnabled = ActiveTab?.View.CoreWebView2?.CanGoForward == true; } private static string CleanTitle(string? raw) { var title = raw ?? string.Empty; foreach (var suffix in new[] { " — SHD Project", " - SHD Project", " | SHD" }) { var index = title.IndexOf(suffix, StringComparison.Ordinal); if (index >= 0) title = title[..index]; } return string.IsNullOrWhiteSpace(title) ? "SHD" : title.Trim(); } private static string GetUniquePath(string path) { if (!File.Exists(path)) return path; var directory = Path.GetDirectoryName(path)!; var name = Path.GetFileNameWithoutExtension(path); var extension = Path.GetExtension(path); for (var counter = 1; ; counter++) { var candidate = Path.Combine(directory, $"{name} ({counter}){extension}"); if (!File.Exists(candidate)) return candidate; } } private string? ShowPrompt(string message, string defaultValue) { var dialog = new Window { Owner = this, Title = "SHD", Width = 430, SizeToContent = SizeToContent.Height, WindowStartupLocation = WindowStartupLocation.CenterOwner, ResizeMode = ResizeMode.NoResize, Background = new SolidColorBrush(Color.FromRgb(34, 34, 34)), Foreground = Brushes.White, ShowInTaskbar = false }; var input = new TextBox { Text = defaultValue, MinWidth = 380, Margin = new Thickness(0, 9, 0, 14), Padding = new Thickness(6) }; var accept = new Button { Content = "OK", IsDefault = true, MinWidth = 80 }; var cancel = new Button { Content = "Отмена", IsCancel = true, MinWidth = 80 }; var buttons = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right }; buttons.Children.Add(accept); buttons.Children.Add(cancel); var panel = new StackPanel { Margin = new Thickness(18) }; panel.Children.Add(new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap, MaxWidth = 380 }); panel.Children.Add(input); panel.Children.Add(buttons); dialog.Content = panel; string? result = null; accept.Click += (_, _) => { result = input.Text; dialog.DialogResult = true; }; dialog.Loaded += (_, _) => { input.Focus(); input.SelectAll(); }; dialog.ShowDialog(); return result; } private void OpenExternalUri(Uri uri) { try { if (uri.IsFile) { Process.Start(new ProcessStartInfo(uri.LocalPath) { UseShellExecute = true }); return; } if (uri.Scheme is "http" or "https" or "mailto") Process.Start(new ProcessStartInfo(uri.AbsoluteUri) { UseShellExecute = true }); } catch (Exception) { StatusText.Text = "Не удалось открыть ссылку"; } } private void Window_Closing(object? sender, System.ComponentModel.CancelEventArgs e) { _closing = true; _authAttempt = null; _authPostData?.Dispose(); _authPostData = null; foreach (var tab in _tabs.ToArray()) tab.View.Dispose(); _tabs.Clear(); } }