|
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Windows;
|
|
|
|
|
using System.Windows.Controls;
|
|
|
|
|
using System.Windows.Controls.Primitives;
|
|
|
|
|
using System.Windows.Threading;
|
|
|
|
|
using Housing.Services;
|
|
|
|
|
|
|
|
|
|
namespace Housing.Login;
|
|
|
|
|
|
|
|
|
|
public sealed class StartupLoginWindowResult
|
|
|
|
|
{
|
|
|
|
|
public string Maker { get; set; } = string.Empty;
|
|
|
|
|
public string Model { get; set; } = string.Empty;
|
|
|
|
|
public string Variant1 { get; set; } = string.Empty;
|
|
|
|
|
public string Variant2 { get; set; } = string.Empty;
|
|
|
|
|
public string Operator { get; set; } = string.Empty;
|
|
|
|
|
public string LineNo { get; set; } = string.Empty;
|
|
|
|
|
public string LotNo { get; set; } = string.Empty;
|
|
|
|
|
public string JigNo { get; set; } = string.Empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public partial class StartupLoginWindow : Window
|
|
|
|
|
{
|
|
|
|
|
private const string OperatorHistoryFieldName = "Operator";
|
|
|
|
|
private const string SuppressedOperatorHistoryValue = "su";
|
|
|
|
|
private readonly LoginHistoryStore _loginHistoryStore;
|
|
|
|
|
private readonly LoginSelectionOptions _loginOptions;
|
|
|
|
|
private Popup? _openHistoryPopup;
|
|
|
|
|
|
|
|
|
|
public StartupLoginWindowResult Result { get; private set; } = new();
|
|
|
|
|
|
|
|
|
|
public StartupLoginWindow()
|
|
|
|
|
{
|
|
|
|
|
_loginHistoryStore = LoginHistoryStore.Load(GetLoginHistoryPath());
|
|
|
|
|
_loginOptions = LoginSelectionOptions.Load(GetLoginOptionsPath());
|
|
|
|
|
RemoveSuppressedOperatorHistory();
|
|
|
|
|
InitializeComponent();
|
|
|
|
|
ApplyLoginOptions();
|
|
|
|
|
AttachLoginHistoryMenus();
|
|
|
|
|
AttachSelectionDropDowns();
|
|
|
|
|
PasswordBox.GotKeyboardFocus += (_, _) => CloseHistoryPopup();
|
|
|
|
|
LoginButton.GotKeyboardFocus += (_, _) => CloseHistoryPopup();
|
|
|
|
|
MakerComboBox.Focus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void LogInButton_Click(object sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
CloseHistoryPopup();
|
|
|
|
|
|
|
|
|
|
if (!TryReadRequiredFields(out var loginResult, out var password))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sender is Button button)
|
|
|
|
|
{
|
|
|
|
|
button.IsEnabled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (!await IsValidLoginAsync(loginResult.Operator, password))
|
|
|
|
|
{
|
|
|
|
|
MessageBox.Show(this, "등록되지 않은 ID/PW입니다.", "Login", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
|
|
|
PasswordBox.Clear();
|
|
|
|
|
PasswordBox.Focus();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Result = loginResult;
|
|
|
|
|
SaveLoginHistory(loginResult);
|
|
|
|
|
DialogResult = true;
|
|
|
|
|
Close();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
MessageBox.Show(this, $"로그인 확인 실패: {ex.Message}", "Login", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
|
|
|
OperatorTextBox.Focus();
|
|
|
|
|
OperatorTextBox.SelectAll();
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
if (sender is Button loginButton)
|
|
|
|
|
{
|
|
|
|
|
loginButton.IsEnabled = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<bool> IsValidLoginAsync(string loginId, string password)
|
|
|
|
|
{
|
|
|
|
|
if (_loginOptions.LoginAccounts.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
return _loginOptions.LoginAccounts.Any(account =>
|
|
|
|
|
string.Equals(account.Id, loginId, StringComparison.OrdinalIgnoreCase) &&
|
|
|
|
|
string.Equals(account.Password, password, StringComparison.Ordinal));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var loginSettings = LoginSettings.Load(GetDatabaseIniPath());
|
|
|
|
|
if (loginSettings.OfflinePreview)
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var repository = new LoginAccountRepository(
|
|
|
|
|
DatabaseSettings.Load(GetDatabaseIniPath()),
|
|
|
|
|
loginSettings);
|
|
|
|
|
|
|
|
|
|
return await repository.ExistsAsync(loginId, password);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool TryReadRequiredFields(out StartupLoginWindowResult result, out string password)
|
|
|
|
|
{
|
|
|
|
|
result = new StartupLoginWindowResult();
|
|
|
|
|
password = string.Empty;
|
|
|
|
|
|
|
|
|
|
if (!TryReadRequiredComboBox(MakerComboBox, "Maker", out var maker) ||
|
|
|
|
|
!TryReadRequiredComboBox(ModelComboBox, "Model", out var model) ||
|
|
|
|
|
!TryReadTextBox(OperatorTextBox, "ID", out var loginId) ||
|
|
|
|
|
!TryReadPasswordBox(PasswordBox, "PW", out password) ||
|
|
|
|
|
!TryReadRequiredComboBox(LineNoComboBox, "Line", out var lineNo) ||
|
|
|
|
|
!TryReadTextBox(LotNoTextBox, "Lot No", out var lotNo) ||
|
|
|
|
|
!TryReadRequiredComboBox(JigNoComboBox, "Jig No", out var jigNo))
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var variant1 = ReadOptionalComboBox(Variant1ComboBox);
|
|
|
|
|
var variant2 = ReadOptionalComboBox(Variant2ComboBox);
|
|
|
|
|
|
|
|
|
|
result = new StartupLoginWindowResult
|
|
|
|
|
{
|
|
|
|
|
Maker = maker,
|
|
|
|
|
Model = model,
|
|
|
|
|
Variant1 = variant1,
|
|
|
|
|
Variant2 = variant2,
|
|
|
|
|
Operator = loginId,
|
|
|
|
|
LineNo = lineNo,
|
|
|
|
|
LotNo = lotNo,
|
|
|
|
|
JigNo = jigNo
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ApplyLoginOptions()
|
|
|
|
|
{
|
|
|
|
|
SetComboBoxOptions(MakerComboBox, _loginOptions.Makers, allowBlank: false, historyFieldName: "Maker");
|
|
|
|
|
SetComboBoxOptions(ModelComboBox, _loginOptions.Models, allowBlank: false, historyFieldName: "Model");
|
|
|
|
|
SetComboBoxOptions(Variant1ComboBox, _loginOptions.Variant1Values, allowBlank: true, historyFieldName: "Variant1");
|
|
|
|
|
SetComboBoxOptions(Variant2ComboBox, _loginOptions.Variant2Values, allowBlank: true, historyFieldName: "Variant2");
|
|
|
|
|
SetComboBoxOptions(LineNoComboBox, _loginOptions.LineValues, allowBlank: false, historyFieldName: "LineNo");
|
|
|
|
|
SetComboBoxOptions(JigNoComboBox, _loginOptions.JigNoValues, allowBlank: false, historyFieldName: "JigNo");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SetComboBoxOptions(
|
|
|
|
|
ComboBox comboBox,
|
|
|
|
|
IReadOnlyList<string> values,
|
|
|
|
|
bool allowBlank,
|
|
|
|
|
string historyFieldName)
|
|
|
|
|
{
|
|
|
|
|
var items = new List<LoginOptionItem>();
|
|
|
|
|
if (allowBlank)
|
|
|
|
|
{
|
|
|
|
|
items.Add(new LoginOptionItem("(None)", string.Empty));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var value in values.Where(value => !string.IsNullOrWhiteSpace(value)))
|
|
|
|
|
{
|
|
|
|
|
if (items.Any(item => string.Equals(item.Value, value, StringComparison.OrdinalIgnoreCase)))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items.Add(new LoginOptionItem(value, value));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
comboBox.ItemsSource = items;
|
|
|
|
|
comboBox.DisplayMemberPath = nameof(LoginOptionItem.DisplayText);
|
|
|
|
|
|
|
|
|
|
var rememberedValue = _loginHistoryStore.GetValues(historyFieldName).FirstOrDefault();
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(rememberedValue) &&
|
|
|
|
|
TrySelectComboBoxValue(comboBox, rememberedValue))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (allowBlank)
|
|
|
|
|
{
|
|
|
|
|
comboBox.SelectedIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
else if (items.Count == 1)
|
|
|
|
|
{
|
|
|
|
|
comboBox.SelectedIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool TrySelectComboBoxValue(ComboBox comboBox, string value)
|
|
|
|
|
{
|
|
|
|
|
for (var index = 0; index < comboBox.Items.Count; index++)
|
|
|
|
|
{
|
|
|
|
|
if (comboBox.Items[index] is LoginOptionItem item &&
|
|
|
|
|
string.Equals(item.Value, value, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
comboBox.SelectedIndex = index;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AttachSelectionDropDowns()
|
|
|
|
|
{
|
|
|
|
|
foreach (var comboBox in new[]
|
|
|
|
|
{
|
|
|
|
|
MakerComboBox,
|
|
|
|
|
ModelComboBox,
|
|
|
|
|
Variant1ComboBox,
|
|
|
|
|
Variant2ComboBox,
|
|
|
|
|
LineNoComboBox,
|
|
|
|
|
JigNoComboBox
|
|
|
|
|
})
|
|
|
|
|
{
|
|
|
|
|
comboBox.GotKeyboardFocus += (_, _) => CloseHistoryPopup();
|
|
|
|
|
comboBox.DropDownOpened += (_, _) => CloseHistoryPopup();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AttachLoginHistoryMenus()
|
|
|
|
|
{
|
|
|
|
|
AttachHistoryMenu(OperatorTextBox, OperatorHistoryFieldName, PasswordBox);
|
|
|
|
|
AttachHistoryMenu(LotNoTextBox, "LotNo", JigNoComboBox);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AttachHistoryMenu(TextBox textBox, string fieldName, Control nextFocusControl)
|
|
|
|
|
{
|
|
|
|
|
textBox.GotKeyboardFocus += (_, _) => ShowHistoryMenu(textBox, fieldName, nextFocusControl);
|
|
|
|
|
textBox.PreviewMouseLeftButtonDown += (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
if (textBox.IsKeyboardFocusWithin)
|
|
|
|
|
{
|
|
|
|
|
ShowHistoryMenu(textBox, fieldName, nextFocusControl);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ShowHistoryMenu(TextBox textBox, string fieldName, Control nextFocusControl)
|
|
|
|
|
{
|
|
|
|
|
var values = _loginHistoryStore.GetValues(fieldName);
|
|
|
|
|
if (values.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
CloseHistoryPopup();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CloseHistoryPopup();
|
|
|
|
|
|
|
|
|
|
var menuWidth = Math.Max(160, textBox.ActualWidth);
|
|
|
|
|
var stackPanel = new StackPanel();
|
|
|
|
|
var popup = new Popup
|
|
|
|
|
{
|
|
|
|
|
PlacementTarget = textBox,
|
|
|
|
|
Placement = PlacementMode.Bottom,
|
|
|
|
|
HorizontalOffset = 0,
|
|
|
|
|
StaysOpen = true,
|
|
|
|
|
AllowsTransparency = true,
|
|
|
|
|
Focusable = false
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
foreach (var value in values)
|
|
|
|
|
{
|
|
|
|
|
var item = new Button
|
|
|
|
|
{
|
|
|
|
|
Content = value,
|
|
|
|
|
MinWidth = menuWidth,
|
|
|
|
|
Padding = new Thickness(10, 6, 10, 6),
|
|
|
|
|
BorderThickness = new Thickness(0),
|
|
|
|
|
Background = textBox.Background,
|
|
|
|
|
Foreground = textBox.Foreground,
|
|
|
|
|
HorizontalContentAlignment = HorizontalAlignment.Left
|
|
|
|
|
};
|
|
|
|
|
item.Click += (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
textBox.Text = value;
|
|
|
|
|
textBox.CaretIndex = textBox.Text.Length;
|
|
|
|
|
CloseHistoryPopup();
|
|
|
|
|
FocusControl(nextFocusControl);
|
|
|
|
|
};
|
|
|
|
|
stackPanel.Children.Add(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
popup.Child = new Border
|
|
|
|
|
{
|
|
|
|
|
Width = menuWidth,
|
|
|
|
|
MaxHeight = 180,
|
|
|
|
|
Background = textBox.Background,
|
|
|
|
|
BorderBrush = textBox.BorderBrush,
|
|
|
|
|
BorderThickness = new Thickness(1),
|
|
|
|
|
Child = new ScrollViewer
|
|
|
|
|
{
|
|
|
|
|
MaxHeight = 180,
|
|
|
|
|
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
|
|
|
|
Content = stackPanel
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_openHistoryPopup = popup;
|
|
|
|
|
popup.IsOpen = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CloseHistoryPopup()
|
|
|
|
|
{
|
|
|
|
|
if (_openHistoryPopup is not null)
|
|
|
|
|
{
|
|
|
|
|
_openHistoryPopup.IsOpen = false;
|
|
|
|
|
_openHistoryPopup = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void FocusControl(Control control)
|
|
|
|
|
{
|
|
|
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
|
|
|
{
|
|
|
|
|
control.Focus();
|
|
|
|
|
|
|
|
|
|
if (control is TextBox textBox)
|
|
|
|
|
{
|
|
|
|
|
textBox.CaretIndex = textBox.Text.Length;
|
|
|
|
|
}
|
|
|
|
|
else if (control is PasswordBox passwordBox)
|
|
|
|
|
{
|
|
|
|
|
passwordBox.Focus();
|
|
|
|
|
}
|
|
|
|
|
}), DispatcherPriority.Input);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SaveLoginHistory(StartupLoginWindowResult result)
|
|
|
|
|
{
|
|
|
|
|
_loginHistoryStore.Remember("Maker", result.Maker);
|
|
|
|
|
_loginHistoryStore.Remember("Model", result.Model);
|
|
|
|
|
_loginHistoryStore.Remember("Variant1", result.Variant1);
|
|
|
|
|
_loginHistoryStore.Remember("Variant2", result.Variant2);
|
|
|
|
|
RememberOperatorHistory(result.Operator);
|
|
|
|
|
_loginHistoryStore.Remember("LineNo", result.LineNo);
|
|
|
|
|
_loginHistoryStore.Remember("LotNo", result.LotNo);
|
|
|
|
|
_loginHistoryStore.Remember("JigNo", result.JigNo);
|
|
|
|
|
_loginHistoryStore.Save();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RememberOperatorHistory(string operatorId)
|
|
|
|
|
{
|
|
|
|
|
if (IsSuppressedOperatorHistoryValue(operatorId))
|
|
|
|
|
{
|
|
|
|
|
_loginHistoryStore.Forget(OperatorHistoryFieldName, operatorId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_loginHistoryStore.Remember(OperatorHistoryFieldName, operatorId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RemoveSuppressedOperatorHistory()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (_loginHistoryStore.Forget(OperatorHistoryFieldName, SuppressedOperatorHistoryValue))
|
|
|
|
|
{
|
|
|
|
|
_loginHistoryStore.Save();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Login history cleanup is optional and should not block the login window.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool IsSuppressedOperatorHistoryValue(string value)
|
|
|
|
|
{
|
|
|
|
|
return string.Equals(value.Trim(), SuppressedOperatorHistoryValue, StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool TryReadRequiredComboBox(ComboBox comboBox, string fieldName, out string value)
|
|
|
|
|
{
|
|
|
|
|
value = ReadComboBoxValue(comboBox);
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(value))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MessageBox.Show(
|
|
|
|
|
this,
|
|
|
|
|
$"{fieldName} value is required. Edit LoginOptions.json and select a value.",
|
|
|
|
|
"Login",
|
|
|
|
|
MessageBoxButton.OK,
|
|
|
|
|
MessageBoxImage.Warning);
|
|
|
|
|
comboBox.Focus();
|
|
|
|
|
comboBox.IsDropDownOpen = true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ReadOptionalComboBox(ComboBox comboBox)
|
|
|
|
|
{
|
|
|
|
|
return ReadComboBoxValue(comboBox);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ReadComboBoxValue(ComboBox comboBox)
|
|
|
|
|
{
|
|
|
|
|
return comboBox.SelectedItem is LoginOptionItem item
|
|
|
|
|
? item.Value.Trim()
|
|
|
|
|
: comboBox.Text.Trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool TryReadTextBox(TextBox textBox, string fieldName, out string value)
|
|
|
|
|
{
|
|
|
|
|
value = textBox.Text.Trim();
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(value))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MessageBox.Show(this, $"{fieldName}을(를) 입력하세요.", "Login", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
|
|
|
textBox.Focus();
|
|
|
|
|
textBox.SelectAll();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool TryReadPasswordBox(PasswordBox passwordBox, string fieldName, out string value)
|
|
|
|
|
{
|
|
|
|
|
value = passwordBox.Password;
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(value))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MessageBox.Show(this, $"{fieldName}을(를) 입력하세요.", "Login", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
|
|
|
passwordBox.Focus();
|
|
|
|
|
passwordBox.SelectAll();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string GetDatabaseIniPath()
|
|
|
|
|
{
|
|
|
|
|
return Path.Combine(AppContext.BaseDirectory, "Database.ini");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string GetLoginOptionsPath()
|
|
|
|
|
{
|
|
|
|
|
var directory = Path.Combine(
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
|
|
|
"Housing");
|
|
|
|
|
var filePath = Path.Combine(directory, "LoginOptions.json");
|
|
|
|
|
if (File.Exists(filePath))
|
|
|
|
|
{
|
|
|
|
|
return filePath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Directory.CreateDirectory(directory);
|
|
|
|
|
|
|
|
|
|
var bundledPath = Path.Combine(AppContext.BaseDirectory, "LoginOptions.json");
|
|
|
|
|
if (File.Exists(bundledPath))
|
|
|
|
|
{
|
|
|
|
|
File.Copy(bundledPath, filePath);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
LoginSelectionOptions.CreateDefaultFile(filePath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return filePath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string GetLoginHistoryPath()
|
|
|
|
|
{
|
|
|
|
|
return Path.Combine(
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
|
|
|
"Housing",
|
|
|
|
|
"LoginHistory.json");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private sealed record LoginOptionItem(string DisplayText, string Value);
|
|
|
|
|
}
|