Files
shd-desktop-app/Windows/SHD.Windows/MainWindow.xaml.cs
T

797 lines
33 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Task<CoreWebView2Environment>> SharedWebViewEnvironment = new(CreateWebViewEnvironmentAsync);
private readonly List<BrowserTab> _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<CoreWebView2Environment> CreateWebViewEnvironmentAsync()
{
var userDataFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"SHDDesktop",
WebViewProfileFolder);
Directory.CreateDirectory(userDataFolder);
return await CoreWebView2Environment.CreateAsync(userDataFolder: userDataFolder);
}
private async Task<BrowserTab> 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<ShdEnvironment>(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();
}
}