New startup window.
This commit is contained in:
@@ -13,6 +13,7 @@ using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Messages;
|
||||
using Wino.Mail.WinUI;
|
||||
using Wino.Mail.WinUI.Interfaces;
|
||||
using Wino.Mail.WinUI.Models;
|
||||
using Wino.Mail.WinUI.Services;
|
||||
using Wino.Mail.WinUI.Views.Calendar;
|
||||
using Wino.Messaging.Client.Mails;
|
||||
@@ -29,6 +30,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
{
|
||||
private readonly IStatePersistanceService _statePersistanceService;
|
||||
private readonly IDispatcher _dispatcher;
|
||||
private readonly IWinoWindowManager _windowManager;
|
||||
|
||||
private WinoPage[] _renderingPageTypes = new WinoPage[]
|
||||
{
|
||||
@@ -42,7 +44,8 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
WinoPage.MailRenderingPage,
|
||||
WinoPage.ComposePage,
|
||||
WinoPage.IdlePage,
|
||||
WinoPage.WelcomePage
|
||||
WinoPage.WelcomePage,
|
||||
WinoPage.WelcomePageV2
|
||||
];
|
||||
|
||||
private static readonly WinoPage[] CalendarOnlyPages =
|
||||
@@ -51,10 +54,11 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
WinoPage.EventDetailsPage
|
||||
];
|
||||
|
||||
public NavigationService(IStatePersistanceService statePersistanceService, IDispatcher dispatcher)
|
||||
public NavigationService(IStatePersistanceService statePersistanceService, IDispatcher dispatcher, IWinoWindowManager windowManager)
|
||||
{
|
||||
_statePersistanceService = statePersistanceService;
|
||||
_dispatcher = dispatcher;
|
||||
_windowManager = windowManager;
|
||||
}
|
||||
|
||||
private bool IsOnNavigationThread()
|
||||
@@ -101,6 +105,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
WinoPage.MailListPage => typeof(MailListPage),
|
||||
WinoPage.SettingsPage => typeof(SettingsPage),
|
||||
WinoPage.WelcomePage => typeof(WelcomePage),
|
||||
WinoPage.WelcomePageV2 => typeof(WelcomePageV2),
|
||||
WinoPage.SettingOptionsPage => typeof(SettingOptionsPage),
|
||||
WinoPage.AppPreferencesPage => typeof(AppPreferencesPage),
|
||||
WinoPage.AliasManagementPage => typeof(AliasManagementPage),
|
||||
@@ -120,19 +125,45 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
}
|
||||
|
||||
public Frame GetCoreFrame(NavigationReferenceFrame frameType)
|
||||
=> ExecuteOnNavigationThread(() => GetCoreFrameInternal(frameType));
|
||||
=> ExecuteOnNavigationThread(() => GetCoreFrameInternal(frameType) ?? throw new ArgumentException($"Frame '{frameType}' cannot be resolved."));
|
||||
|
||||
private Frame GetCoreFrameInternal(NavigationReferenceFrame frameType)
|
||||
private Frame? GetCoreFrameInternal(NavigationReferenceFrame frameType, WinoWindowKind? requestedWindowKind = null)
|
||||
{
|
||||
if (WinoApplication.MainWindow is not IWinoShellWindow shellWindow) throw new ArgumentException("MainWindow must implement IWinoShellWindow");
|
||||
if (shellWindow.GetMainFrame() is not Frame mainFrame) throw new ArgumentException("MainFrame cannot be null.");
|
||||
if (frameType == NavigationReferenceFrame.ShellFrame)
|
||||
{
|
||||
if (requestedWindowKind.HasValue)
|
||||
return _windowManager.GetPrimaryNavigationFrame(requestedWindowKind.Value);
|
||||
|
||||
if (frameType == NavigationReferenceFrame.ShellFrame) return shellWindow.GetMainFrame();
|
||||
var activeWindow = _windowManager.ActiveWindow;
|
||||
if (activeWindow != null)
|
||||
{
|
||||
var activeShellWindow = _windowManager.GetWindow(WinoWindowKind.Shell);
|
||||
if (ReferenceEquals(activeWindow, activeShellWindow))
|
||||
return _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Shell);
|
||||
|
||||
var contentRoot = mainFrame.Content as UIElement;
|
||||
if (contentRoot == null) return mainFrame;
|
||||
var activeWelcomeWindow = _windowManager.GetWindow(WinoWindowKind.Welcome);
|
||||
if (ReferenceEquals(activeWindow, activeWelcomeWindow))
|
||||
return _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Welcome);
|
||||
}
|
||||
|
||||
return WinoVisualTreeHelper.GetChildObject<Frame>(contentRoot, frameType.ToString()) ?? mainFrame;
|
||||
return _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Shell)
|
||||
?? _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Welcome);
|
||||
}
|
||||
|
||||
var mainFrame = _windowManager.GetPrimaryNavigationFrame(WinoWindowKind.Shell);
|
||||
if (mainFrame == null)
|
||||
return null;
|
||||
|
||||
var contentRoot = mainFrame.Content as FrameworkElement;
|
||||
if (contentRoot == null) return null;
|
||||
|
||||
// Use FindName first — it works immediately after InitializeComponent(),
|
||||
// before the visual tree is built by the layout pass.
|
||||
if (contentRoot.FindName(frameType.ToString()) is Frame namedFrame)
|
||||
return namedFrame;
|
||||
|
||||
// Fall back to visual tree search for deeply nested frames (e.g. RenderingFrame).
|
||||
return WinoVisualTreeHelper.GetChildObject<Frame>(contentRoot, frameType.ToString());
|
||||
}
|
||||
|
||||
public bool ChangeApplicationMode(WinoApplicationMode mode)
|
||||
@@ -211,14 +242,22 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
_statePersistanceService.IsReadingMail = _renderingPageTypes.Contains(page);
|
||||
_statePersistanceService.IsEventDetailsVisible = page == WinoPage.EventDetailsPage;
|
||||
|
||||
Frame innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame);
|
||||
Frame? innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame);
|
||||
if (innerShellFrame == null && frame == NavigationReferenceFrame.ShellFrame)
|
||||
{
|
||||
var requestedFrame = GetCoreFrameInternal(NavigationReferenceFrame.ShellFrame, WinoWindowKind.Welcome);
|
||||
if (requestedFrame == null)
|
||||
return false;
|
||||
|
||||
return requestedFrame.Navigate(pageType, parameter, GetNavigationTransitionInfo(transition));
|
||||
}
|
||||
|
||||
if (innerShellFrame != null)
|
||||
{
|
||||
// Calendar navigations.
|
||||
if (currentApplicationMode == WinoApplicationMode.Calendar)
|
||||
{
|
||||
var currentFrameType = GetCurrentFrameType(ref innerShellFrame);
|
||||
var currentFrameType = GetCurrentFrameType(innerShellFrame);
|
||||
|
||||
if (page == WinoPage.CalendarPage &&
|
||||
parameter is CalendarPageNavigationArgs calendarNavigationArgs)
|
||||
@@ -247,7 +286,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
||||
else
|
||||
{
|
||||
// Mail navigations.
|
||||
var currentFrameType = GetCurrentFrameType(ref innerShellFrame);
|
||||
var currentFrameType = GetCurrentFrameType(innerShellFrame);
|
||||
bool isMailListingPageActive = currentFrameType != null && currentFrameType == typeof(MailListPage);
|
||||
|
||||
// Active page is mail list page and we are refreshing the folder.
|
||||
|
||||
@@ -17,10 +17,10 @@ public class NavigationServiceBase
|
||||
};
|
||||
}
|
||||
|
||||
public Type? GetCurrentFrameType(ref Frame _frame)
|
||||
public Type? GetCurrentFrameType(Frame frame)
|
||||
{
|
||||
if (_frame != null && _frame.Content != null)
|
||||
return _frame.Content.GetType();
|
||||
if (frame != null && frame.Content != null)
|
||||
return frame.Content.GetType();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ public class NewThemeService : INewThemeService
|
||||
private readonly IConfigurationService _configurationService;
|
||||
private readonly IUnderlyingThemeService _underlyingThemeService;
|
||||
private readonly IApplicationResourceManager<ResourceDictionary> _applicationResourceManager;
|
||||
private readonly IWinoWindowManager _windowManager;
|
||||
|
||||
private List<AppThemeBase> preDefinedThemes { get; set; } = new List<AppThemeBase>()
|
||||
{
|
||||
@@ -75,11 +76,13 @@ public class NewThemeService : INewThemeService
|
||||
|
||||
public NewThemeService(IConfigurationService configurationService,
|
||||
IUnderlyingThemeService underlyingThemeService,
|
||||
IApplicationResourceManager<ResourceDictionary> applicationResourceManager)
|
||||
IApplicationResourceManager<ResourceDictionary> applicationResourceManager,
|
||||
IWinoWindowManager windowManager)
|
||||
{
|
||||
_configurationService = configurationService;
|
||||
_underlyingThemeService = underlyingThemeService;
|
||||
_applicationResourceManager = applicationResourceManager;
|
||||
_windowManager = windowManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -89,11 +92,17 @@ public class NewThemeService : INewThemeService
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetShellRootContent().RequestedTheme.ToWinoElementTheme();
|
||||
var rootContent = TryGetShellRootContent();
|
||||
if (rootContent == null)
|
||||
return _configurationService.Get(UnderlyingThemeService.SelectedAppThemeKey, ApplicationElementTheme.Default);
|
||||
|
||||
return rootContent.RequestedTheme.ToWinoElementTheme();
|
||||
}
|
||||
set
|
||||
{
|
||||
GetShellRootContent().RequestedTheme = value.ToWindowsElementTheme();
|
||||
var rootContent = TryGetShellRootContent();
|
||||
if (rootContent != null)
|
||||
rootContent.RequestedTheme = value.ToWindowsElementTheme();
|
||||
|
||||
_configurationService.Set(UnderlyingThemeService.SelectedAppThemeKey, value);
|
||||
|
||||
@@ -115,9 +124,10 @@ public class NewThemeService : INewThemeService
|
||||
|
||||
_configurationService.Set(CurrentApplicationThemeKey, value);
|
||||
|
||||
if (WinoApplication.MainWindow != null)
|
||||
var window = GetThemeWindow();
|
||||
if (window != null)
|
||||
{
|
||||
WinoApplication.MainWindow.DispatcherQueue.TryEnqueue(async () =>
|
||||
window.DispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
await ApplyCustomThemeAsync(false);
|
||||
});
|
||||
@@ -154,9 +164,10 @@ public class NewThemeService : INewThemeService
|
||||
currentBackdropType = value;
|
||||
_configurationService.Set(WindowBackdropTypeKey, (int)value);
|
||||
|
||||
if (WinoApplication.MainWindow != null)
|
||||
var window = GetThemeWindow();
|
||||
if (window != null)
|
||||
{
|
||||
WinoApplication.MainWindow.DispatcherQueue.TryEnqueue(() =>
|
||||
window.DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
ApplyBackdrop(value);
|
||||
});
|
||||
@@ -176,7 +187,17 @@ public class NewThemeService : INewThemeService
|
||||
}
|
||||
}
|
||||
|
||||
public FrameworkElement GetShellRootContent() => (WinoApplication.MainWindow as IWinoShellWindow)?.GetRootContent() ?? throw new Exception("No root content found");
|
||||
public FrameworkElement GetShellRootContent()
|
||||
{
|
||||
var window = GetThemeWindow();
|
||||
if (window is IWinoShellWindow shellWindow)
|
||||
return shellWindow.GetRootContent();
|
||||
|
||||
if (window?.Content is FrameworkElement frameworkElement)
|
||||
return frameworkElement;
|
||||
|
||||
throw new Exception("No root content found");
|
||||
}
|
||||
|
||||
private bool isInitialized = false;
|
||||
|
||||
@@ -210,9 +231,9 @@ public class NewThemeService : INewThemeService
|
||||
|
||||
public void ApplyBackdrop(WindowBackdropType backdropType)
|
||||
{
|
||||
if (WinoApplication.MainWindow is not WindowEx windowEx)
|
||||
if (GetThemeWindow() is not WindowEx windowEx)
|
||||
{
|
||||
Debug.WriteLine("MainWindow is not WindowEx, cannot apply backdrop");
|
||||
Debug.WriteLine("No active WindowEx found, cannot apply backdrop");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -267,7 +288,7 @@ public class NewThemeService : INewThemeService
|
||||
|
||||
private void NotifyThemeUpdate()
|
||||
{
|
||||
if (GetShellRootContent() is not UIElement rootContent) return;
|
||||
if (TryGetShellRootContent() is not UIElement rootContent) return;
|
||||
|
||||
_ = rootContent.DispatcherQueue.EnqueueAsync(() =>
|
||||
{
|
||||
@@ -283,9 +304,12 @@ public class NewThemeService : INewThemeService
|
||||
|
||||
public void UpdateSystemCaptionButtonColors()
|
||||
{
|
||||
GetShellRootContent().DispatcherQueue.TryEnqueue(() =>
|
||||
var rootContent = TryGetShellRootContent();
|
||||
if (rootContent == null) return;
|
||||
|
||||
rootContent.DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (WinoApplication.MainWindow is not WindowEx mainWindow) return;
|
||||
if (GetThemeWindow() is not WindowEx mainWindow) return;
|
||||
|
||||
var titleBar = mainWindow.AppWindow.TitleBar;
|
||||
if (titleBar == null) return;
|
||||
@@ -353,8 +377,7 @@ public class NewThemeService : INewThemeService
|
||||
|
||||
private void RefreshThemeResource()
|
||||
{
|
||||
var mainApplicationFrame = GetShellRootContent();
|
||||
|
||||
var mainApplicationFrame = TryGetShellRootContent();
|
||||
if (mainApplicationFrame == null) return;
|
||||
|
||||
if (mainApplicationFrame.RequestedTheme == ElementTheme.Dark)
|
||||
@@ -648,4 +671,26 @@ public class NewThemeService : INewThemeService
|
||||
new BackdropTypeWrapper(WindowBackdropType.AcrylicThin, "Acrylic Thin")
|
||||
};
|
||||
}
|
||||
|
||||
private WindowEx? GetThemeWindow() => _windowManager.ActiveWindow ?? WinoApplication.MainWindow;
|
||||
|
||||
private FrameworkElement? TryGetShellRootContent()
|
||||
{
|
||||
var window = GetThemeWindow();
|
||||
if (window == null)
|
||||
return null;
|
||||
|
||||
if (window is IWinoShellWindow shellWindow)
|
||||
return shellWindow.GetRootContent();
|
||||
|
||||
return window.Content as FrameworkElement;
|
||||
}
|
||||
|
||||
public async Task ApplyThemeToActiveWindowAsync()
|
||||
{
|
||||
ApplyBackdrop(currentBackdropType);
|
||||
RootTheme = _configurationService.Get(UnderlyingThemeService.SelectedAppThemeKey, ApplicationElementTheme.Default);
|
||||
await ApplyCustomThemeAsync(false);
|
||||
UpdateSystemCaptionButtonColors();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using WinUIEx;
|
||||
using Wino.Mail.WinUI.Interfaces;
|
||||
using Wino.Mail.WinUI.Models;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
public class WinoWindowManager : IWinoWindowManager
|
||||
{
|
||||
public event EventHandler<WindowEx?>? ActiveWindowChanged;
|
||||
public event EventHandler<WindowEx>? WindowRemoved;
|
||||
|
||||
private readonly object _syncLock = new();
|
||||
private readonly Dictionary<(WinoWindowKind Kind, string Name), WindowEx> _windows = [];
|
||||
private readonly Dictionary<WindowEx, (WinoWindowKind Kind, string Name)> _windowKeys = [];
|
||||
private readonly Dictionary<(WinoWindowKind Kind, string Name), Frame> _primaryNavigationFrames = [];
|
||||
|
||||
public WindowEx? ActiveWindow { get; private set; }
|
||||
|
||||
public WindowEx CreateWindow(WinoWindowKind kind, Func<WindowEx> factory, string? name = null)
|
||||
{
|
||||
var key = CreateKey(kind, name);
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_windows.TryGetValue(key, out var existingWindow))
|
||||
{
|
||||
ActiveWindow = existingWindow;
|
||||
ActiveWindowChanged?.Invoke(this, existingWindow);
|
||||
return existingWindow;
|
||||
}
|
||||
}
|
||||
|
||||
var newWindow = factory();
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_windows.TryGetValue(key, out var existingWindow))
|
||||
{
|
||||
ActiveWindow = existingWindow;
|
||||
ActiveWindowChanged?.Invoke(this, existingWindow);
|
||||
return existingWindow;
|
||||
}
|
||||
|
||||
TrackWindow(key, newWindow);
|
||||
ActiveWindow = newWindow;
|
||||
ActiveWindowChanged?.Invoke(this, newWindow);
|
||||
return newWindow;
|
||||
}
|
||||
}
|
||||
|
||||
public WindowEx? GetWindow(WinoWindowKind kind, string? name = null)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
_windows.TryGetValue(CreateKey(kind, name), out var window);
|
||||
return window;
|
||||
}
|
||||
}
|
||||
|
||||
public WindowEx? GetWindow(string name)
|
||||
{
|
||||
var normalizedName = NormalizeName(name);
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
return _windows
|
||||
.Where(x => x.Key.Name.Equals(normalizedName, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(x => x.Value)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public void ActivateWindow(WindowEx window)
|
||||
{
|
||||
window.Show();
|
||||
window.BringToFront();
|
||||
window.Activate();
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
ActiveWindow = window;
|
||||
}
|
||||
|
||||
ActiveWindowChanged?.Invoke(this, window);
|
||||
}
|
||||
|
||||
public bool ActivateWindow(WinoWindowKind kind, string? name = null)
|
||||
{
|
||||
var window = GetWindow(kind, name);
|
||||
if (window == null)
|
||||
return false;
|
||||
|
||||
ActivateWindow(window);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void HideWindow(WindowEx window)
|
||||
{
|
||||
window.Hide();
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (ReferenceEquals(ActiveWindow, window))
|
||||
{
|
||||
ActiveWindow = null;
|
||||
ActiveWindowChanged?.Invoke(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HideWindow(WinoWindowKind kind, string? name = null)
|
||||
{
|
||||
var window = GetWindow(kind, name);
|
||||
if (window == null)
|
||||
return false;
|
||||
|
||||
HideWindow(window);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetPrimaryNavigationFrame(WinoWindowKind kind, Frame frame, string? name = null)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
_primaryNavigationFrames[CreateKey(kind, name)] = frame;
|
||||
}
|
||||
}
|
||||
|
||||
public Frame? GetPrimaryNavigationFrame(WinoWindowKind kind, string? name = null)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
_primaryNavigationFrames.TryGetValue(CreateKey(kind, name), out var frame);
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
|
||||
private void TrackWindow((WinoWindowKind Kind, string Name) key, WindowEx window)
|
||||
{
|
||||
_windows[key] = window;
|
||||
_windowKeys[window] = key;
|
||||
window.Activated += WindowActivated;
|
||||
window.Closed += WindowClosed;
|
||||
}
|
||||
|
||||
private void WindowActivated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (sender is not WindowEx window)
|
||||
return;
|
||||
|
||||
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
||||
return;
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_windowKeys.ContainsKey(window))
|
||||
{
|
||||
ActiveWindow = window;
|
||||
ActiveWindowChanged?.Invoke(this, window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WindowClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
if (sender is not WindowEx window)
|
||||
return;
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!_windowKeys.TryGetValue(window, out var key))
|
||||
return;
|
||||
|
||||
window.Activated -= WindowActivated;
|
||||
window.Closed -= WindowClosed;
|
||||
|
||||
_windowKeys.Remove(window);
|
||||
_windows.Remove(key);
|
||||
_primaryNavigationFrames.Remove(key);
|
||||
WindowRemoved?.Invoke(this, window);
|
||||
|
||||
if (ReferenceEquals(ActiveWindow, window))
|
||||
{
|
||||
ActiveWindow = null;
|
||||
ActiveWindowChanged?.Invoke(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CloseAllWindows()
|
||||
{
|
||||
List<WindowEx> windows;
|
||||
lock (_syncLock)
|
||||
{
|
||||
windows = _windows.Values.Distinct().ToList();
|
||||
}
|
||||
|
||||
foreach (var window in windows)
|
||||
{
|
||||
try
|
||||
{
|
||||
window.Activated -= WindowActivated;
|
||||
window.Closed -= WindowClosed;
|
||||
window.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort shutdown for all tracked windows.
|
||||
}
|
||||
}
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
_windowKeys.Clear();
|
||||
_windows.Clear();
|
||||
_primaryNavigationFrames.Clear();
|
||||
ActiveWindow = null;
|
||||
}
|
||||
|
||||
ActiveWindowChanged?.Invoke(this, null);
|
||||
}
|
||||
|
||||
private static (WinoWindowKind Kind, string Name) CreateKey(WinoWindowKind kind, string? name)
|
||||
{
|
||||
var resolvedName = NormalizeName(name ?? kind.ToString());
|
||||
return (kind, string.IsNullOrWhiteSpace(resolvedName) ? kind.ToString() : resolvedName);
|
||||
}
|
||||
|
||||
private static string NormalizeName(string name) => name.Trim();
|
||||
}
|
||||
Reference in New Issue
Block a user