Migrate UI to SettingsUIViewModel
This commit is contained in:
parent
9b6241985f
commit
47c491271a
5 changed files with 137 additions and 125 deletions
116
src/Ryujinx/UI/ViewModels/Settings/SettingsUIViewModel.cs
Normal file
116
src/Ryujinx/UI/ViewModels/Settings/SettingsUIViewModel.cs
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.UI.Common.Configuration;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.ViewModels.Settings
|
||||||
|
{
|
||||||
|
public class SettingsUIViewModel : BaseModel
|
||||||
|
{
|
||||||
|
public event Action DirtyEvent;
|
||||||
|
|
||||||
|
private bool _enableDiscordIntegration;
|
||||||
|
public bool EnableDiscordIntegration
|
||||||
|
{
|
||||||
|
get => _enableDiscordIntegration;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_enableDiscordIntegration = value;
|
||||||
|
DirtyEvent?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _checkUpdatesOnStart;
|
||||||
|
public bool CheckUpdatesOnStart
|
||||||
|
{
|
||||||
|
get => _checkUpdatesOnStart;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_checkUpdatesOnStart = value;
|
||||||
|
DirtyEvent?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _showConfirmExit;
|
||||||
|
public bool ShowConfirmExit
|
||||||
|
{
|
||||||
|
get => _showConfirmExit;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_showConfirmExit = value;
|
||||||
|
DirtyEvent?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _hideCursor;
|
||||||
|
public int HideCursor
|
||||||
|
{
|
||||||
|
get => _hideCursor;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_hideCursor = value;
|
||||||
|
DirtyEvent?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _baseStyleIndex;
|
||||||
|
public int BaseStyleIndex
|
||||||
|
{
|
||||||
|
get => _baseStyleIndex;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_baseStyleIndex = value;
|
||||||
|
DirtyEvent?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DirsChanged;
|
||||||
|
|
||||||
|
public AvaloniaList<string> GameDirectories { get; set; }
|
||||||
|
|
||||||
|
public SettingsUIViewModel()
|
||||||
|
{
|
||||||
|
ConfigurationState config = ConfigurationState.Instance;
|
||||||
|
|
||||||
|
GameDirectories = new();
|
||||||
|
|
||||||
|
EnableDiscordIntegration = config.EnableDiscordIntegration;
|
||||||
|
CheckUpdatesOnStart = config.CheckUpdatesOnStart;
|
||||||
|
ShowConfirmExit = config.ShowConfirmExit;
|
||||||
|
HideCursor = (int)config.HideCursor.Value;
|
||||||
|
|
||||||
|
GameDirectories.Clear();
|
||||||
|
GameDirectories.AddRange(config.UI.GameDirs.Value);
|
||||||
|
GameDirectories.CollectionChanged += (_, _) => DirtyEvent?.Invoke();
|
||||||
|
|
||||||
|
BaseStyleIndex = config.UI.BaseStyle == "Light" ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CheckIfModified(ConfigurationState config)
|
||||||
|
{
|
||||||
|
bool isDirty = false;
|
||||||
|
|
||||||
|
DirsChanged = !config.UI.GameDirs.Value.SequenceEqual(GameDirectories);
|
||||||
|
|
||||||
|
isDirty |= config.EnableDiscordIntegration.Value != EnableDiscordIntegration;
|
||||||
|
isDirty |= config.CheckUpdatesOnStart.Value != CheckUpdatesOnStart;
|
||||||
|
isDirty |= config.ShowConfirmExit.Value != ShowConfirmExit;
|
||||||
|
isDirty |= config.HideCursor.Value != (HideCursorMode)HideCursor;
|
||||||
|
isDirty |= DirsChanged;
|
||||||
|
isDirty |= config.UI.BaseStyle.Value != (BaseStyleIndex == 0 ? "Light" : "Dark");
|
||||||
|
|
||||||
|
return isDirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save(ConfigurationState config)
|
||||||
|
{
|
||||||
|
config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
|
||||||
|
config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
|
||||||
|
config.ShowConfirmExit.Value = ShowConfirmExit;
|
||||||
|
config.HideCursor.Value = (HideCursorMode)HideCursor;
|
||||||
|
config.UI.GameDirs.Value = GameDirectories.ToList();
|
||||||
|
config.UI.BaseStyle.Value = BaseStyleIndex == 0 ? "Light" : "Dark";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,11 @@
|
||||||
using Avalonia.Collections;
|
|
||||||
using Ryujinx.Ava.UI.Windows;
|
using Ryujinx.Ava.UI.Windows;
|
||||||
using Ryujinx.Common.Configuration;
|
|
||||||
using Ryujinx.UI.Common.Configuration;
|
using Ryujinx.UI.Common.Configuration;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.ViewModels.Settings
|
namespace Ryujinx.Ava.UI.ViewModels.Settings
|
||||||
{
|
{
|
||||||
public class SettingsViewModel : BaseModel
|
public class SettingsViewModel : BaseModel
|
||||||
{
|
{
|
||||||
private bool _directoryChanged;
|
|
||||||
|
|
||||||
private bool _isModified;
|
private bool _isModified;
|
||||||
|
|
||||||
public bool IsModified
|
public bool IsModified
|
||||||
|
@ -27,65 +22,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Settings
|
||||||
public event Action<bool> DirtyEvent;
|
public event Action<bool> DirtyEvent;
|
||||||
public event Action<bool> ToggleButtons;
|
public event Action<bool> ToggleButtons;
|
||||||
|
|
||||||
public bool DirectoryChanged
|
|
||||||
{
|
|
||||||
get => _directoryChanged;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_directoryChanged = value;
|
|
||||||
|
|
||||||
OnPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsMacOS => OperatingSystem.IsMacOS();
|
public bool IsMacOS => OperatingSystem.IsMacOS();
|
||||||
|
|
||||||
private bool _enableDiscordIntegration;
|
|
||||||
public bool EnableDiscordIntegration
|
|
||||||
{
|
|
||||||
get => _enableDiscordIntegration;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_enableDiscordIntegration = value;
|
|
||||||
CheckIfModified();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _checkUpdatesOnStart;
|
|
||||||
public bool CheckUpdatesOnStart
|
|
||||||
{
|
|
||||||
get => _checkUpdatesOnStart;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_checkUpdatesOnStart = value;
|
|
||||||
CheckIfModified();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _showConfirmExit;
|
|
||||||
public bool ShowConfirmExit
|
|
||||||
{
|
|
||||||
get => _showConfirmExit;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_showConfirmExit = value;
|
|
||||||
CheckIfModified();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int _hideCursor;
|
|
||||||
public int HideCursor
|
|
||||||
{
|
|
||||||
get => _hideCursor;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_hideCursor = value;
|
|
||||||
CheckIfModified();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int BaseStyleIndex { get; set; }
|
|
||||||
|
|
||||||
private readonly SettingsAudioViewModel _audioViewModel;
|
private readonly SettingsAudioViewModel _audioViewModel;
|
||||||
private readonly SettingsCpuViewModel _cpuViewModel;
|
private readonly SettingsCpuViewModel _cpuViewModel;
|
||||||
private readonly SettingsGraphicsViewModel _graphicsViewModel;
|
private readonly SettingsGraphicsViewModel _graphicsViewModel;
|
||||||
|
@ -94,8 +32,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Settings
|
||||||
private readonly SettingsLoggingViewModel _loggingViewModel;
|
private readonly SettingsLoggingViewModel _loggingViewModel;
|
||||||
private readonly SettingsNetworkViewModel _networkViewModel;
|
private readonly SettingsNetworkViewModel _networkViewModel;
|
||||||
private readonly SettingsSystemViewModel _systemViewModel;
|
private readonly SettingsSystemViewModel _systemViewModel;
|
||||||
|
private readonly SettingsUIViewModel _uiViewModel;
|
||||||
public AvaloniaList<string> GameDirectories { get; set; }
|
|
||||||
|
|
||||||
public SettingsViewModel(
|
public SettingsViewModel(
|
||||||
SettingsAudioViewModel audioViewModel,
|
SettingsAudioViewModel audioViewModel,
|
||||||
|
@ -105,7 +42,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Settings
|
||||||
SettingsInputViewModel inputViewModel,
|
SettingsInputViewModel inputViewModel,
|
||||||
SettingsLoggingViewModel loggingViewModel,
|
SettingsLoggingViewModel loggingViewModel,
|
||||||
SettingsNetworkViewModel networkViewModel,
|
SettingsNetworkViewModel networkViewModel,
|
||||||
SettingsSystemViewModel systemViewModel) : this()
|
SettingsSystemViewModel systemViewModel,
|
||||||
|
SettingsUIViewModel uiViewModel) : this()
|
||||||
{
|
{
|
||||||
_audioViewModel = audioViewModel;
|
_audioViewModel = audioViewModel;
|
||||||
_cpuViewModel = cpuViewModel;
|
_cpuViewModel = cpuViewModel;
|
||||||
|
@ -115,6 +53,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Settings
|
||||||
_loggingViewModel = loggingViewModel;
|
_loggingViewModel = loggingViewModel;
|
||||||
_networkViewModel = networkViewModel;
|
_networkViewModel = networkViewModel;
|
||||||
_systemViewModel = systemViewModel;
|
_systemViewModel = systemViewModel;
|
||||||
|
_uiViewModel = uiViewModel;
|
||||||
|
|
||||||
_audioViewModel.DirtyEvent += CheckIfModified;
|
_audioViewModel.DirtyEvent += CheckIfModified;
|
||||||
_cpuViewModel.DirtyEvent += CheckIfModified;
|
_cpuViewModel.DirtyEvent += CheckIfModified;
|
||||||
|
@ -124,15 +63,14 @@ namespace Ryujinx.Ava.UI.ViewModels.Settings
|
||||||
_loggingViewModel.DirtyEvent += CheckIfModified;
|
_loggingViewModel.DirtyEvent += CheckIfModified;
|
||||||
_networkViewModel.DirtyEvent += CheckIfModified;
|
_networkViewModel.DirtyEvent += CheckIfModified;
|
||||||
_systemViewModel.DirtyEvent += CheckIfModified;
|
_systemViewModel.DirtyEvent += CheckIfModified;
|
||||||
|
_uiViewModel.DirtyEvent += CheckIfModified;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SettingsViewModel()
|
public SettingsViewModel()
|
||||||
{
|
{
|
||||||
GameDirectories = new AvaloniaList<string>();
|
|
||||||
|
|
||||||
if (Program.PreviewerDetached)
|
if (Program.PreviewerDetached)
|
||||||
{
|
{
|
||||||
LoadCurrentConfiguration();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,15 +80,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Settings
|
||||||
|
|
||||||
ConfigurationState config = ConfigurationState.Instance;
|
ConfigurationState config = ConfigurationState.Instance;
|
||||||
|
|
||||||
isDirty |= config.EnableDiscordIntegration.Value != EnableDiscordIntegration;
|
|
||||||
isDirty |= config.CheckUpdatesOnStart.Value != CheckUpdatesOnStart;
|
|
||||||
isDirty |= config.ShowConfirmExit.Value != ShowConfirmExit;
|
|
||||||
isDirty |= config.HideCursor.Value != (HideCursorMode)HideCursor;
|
|
||||||
|
|
||||||
// isDirty |= config.UI.GameDirs.Value != GameDirectories.ToList();
|
|
||||||
|
|
||||||
isDirty |= config.UI.BaseStyle.Value != (BaseStyleIndex == 0 ? "Light" : "Dark");
|
|
||||||
|
|
||||||
isDirty |= _audioViewModel?.CheckIfModified(config) ?? false;
|
isDirty |= _audioViewModel?.CheckIfModified(config) ?? false;
|
||||||
isDirty |= _cpuViewModel?.CheckIfModified(config) ?? false;
|
isDirty |= _cpuViewModel?.CheckIfModified(config) ?? false;
|
||||||
isDirty |= _graphicsViewModel?.CheckIfModified(config) ?? false;
|
isDirty |= _graphicsViewModel?.CheckIfModified(config) ?? false;
|
||||||
|
@ -160,44 +89,15 @@ namespace Ryujinx.Ava.UI.ViewModels.Settings
|
||||||
isDirty |= _loggingViewModel?.CheckIfModified(config) ?? false;
|
isDirty |= _loggingViewModel?.CheckIfModified(config) ?? false;
|
||||||
isDirty |= _networkViewModel?.CheckIfModified(config) ?? false;
|
isDirty |= _networkViewModel?.CheckIfModified(config) ?? false;
|
||||||
isDirty |= _systemViewModel?.CheckIfModified(config) ?? false;
|
isDirty |= _systemViewModel?.CheckIfModified(config) ?? false;
|
||||||
|
isDirty |= _uiViewModel?.CheckIfModified(config) ?? false;
|
||||||
|
|
||||||
IsModified = isDirty;
|
IsModified = isDirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadCurrentConfiguration()
|
|
||||||
{
|
|
||||||
ConfigurationState config = ConfigurationState.Instance;
|
|
||||||
|
|
||||||
// User Interface
|
|
||||||
EnableDiscordIntegration = config.EnableDiscordIntegration;
|
|
||||||
CheckUpdatesOnStart = config.CheckUpdatesOnStart;
|
|
||||||
ShowConfirmExit = config.ShowConfirmExit;
|
|
||||||
HideCursor = (int)config.HideCursor.Value;
|
|
||||||
|
|
||||||
GameDirectories.Clear();
|
|
||||||
GameDirectories.AddRange(config.UI.GameDirs.Value);
|
|
||||||
|
|
||||||
BaseStyleIndex = config.UI.BaseStyle == "Light" ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SaveSettings()
|
public void SaveSettings()
|
||||||
{
|
{
|
||||||
ConfigurationState config = ConfigurationState.Instance;
|
ConfigurationState config = ConfigurationState.Instance;
|
||||||
|
|
||||||
// User Interface
|
|
||||||
config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
|
|
||||||
config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
|
|
||||||
config.ShowConfirmExit.Value = ShowConfirmExit;
|
|
||||||
config.HideCursor.Value = (HideCursorMode)HideCursor;
|
|
||||||
|
|
||||||
if (_directoryChanged)
|
|
||||||
{
|
|
||||||
List<string> gameDirs = new(GameDirectories);
|
|
||||||
config.UI.GameDirs.Value = gameDirs;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.UI.BaseStyle.Value = BaseStyleIndex == 0 ? "Light" : "Dark";
|
|
||||||
|
|
||||||
_audioViewModel?.Save(config);
|
_audioViewModel?.Save(config);
|
||||||
_cpuViewModel?.Save(config);
|
_cpuViewModel?.Save(config);
|
||||||
_graphicsViewModel?.Save(config);
|
_graphicsViewModel?.Save(config);
|
||||||
|
@ -206,12 +106,11 @@ namespace Ryujinx.Ava.UI.ViewModels.Settings
|
||||||
_loggingViewModel?.Save(config);
|
_loggingViewModel?.Save(config);
|
||||||
_networkViewModel?.Save(config);
|
_networkViewModel?.Save(config);
|
||||||
_systemViewModel?.Save(config);
|
_systemViewModel?.Save(config);
|
||||||
|
_uiViewModel?.Save(config);
|
||||||
|
|
||||||
config.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
config.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||||
|
|
||||||
MainWindow.UpdateGraphicsConfig();
|
MainWindow.UpdateGraphicsConfig();
|
||||||
|
|
||||||
_directoryChanged = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ApplyButton()
|
public void ApplyButton()
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Settings"
|
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Settings"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
x:DataType="viewModels:SettingsViewModel">
|
x:DataType="viewModels:SettingsUIViewModel">
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<viewModels:SettingsViewModel />
|
<viewModels:SettingsUIViewModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
<ScrollViewer
|
<ScrollViewer
|
||||||
Name="UiPage"
|
Name="UiPage"
|
||||||
|
|
|
@ -11,11 +11,11 @@ namespace Ryujinx.Ava.UI.Views.Settings
|
||||||
{
|
{
|
||||||
public partial class SettingsUiView : UserControl
|
public partial class SettingsUiView : UserControl
|
||||||
{
|
{
|
||||||
private readonly SettingsViewModel _viewModel;
|
public SettingsUIViewModel ViewModel;
|
||||||
|
|
||||||
public SettingsUiView(SettingsViewModel viewModel)
|
public SettingsUiView()
|
||||||
{
|
{
|
||||||
_viewModel = viewModel;
|
DataContext = ViewModel = new SettingsUIViewModel();
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,10 +23,9 @@ namespace Ryujinx.Ava.UI.Views.Settings
|
||||||
{
|
{
|
||||||
string path = PathBox.Text;
|
string path = PathBox.Text;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !_viewModel.GameDirectories.Contains(path))
|
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.GameDirectories.Contains(path))
|
||||||
{
|
{
|
||||||
_viewModel.GameDirectories.Add(path);
|
ViewModel.GameDirectories.Add(path);
|
||||||
_viewModel.DirectoryChanged = true;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -39,8 +38,7 @@ namespace Ryujinx.Ava.UI.Views.Settings
|
||||||
|
|
||||||
if (result.Count > 0)
|
if (result.Count > 0)
|
||||||
{
|
{
|
||||||
_viewModel.GameDirectories.Add(result[0].Path.LocalPath);
|
ViewModel.GameDirectories.Add(result[0].Path.LocalPath);
|
||||||
_viewModel.DirectoryChanged = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,8 +50,7 @@ namespace Ryujinx.Ava.UI.Views.Settings
|
||||||
|
|
||||||
foreach (string path in new List<string>(GameList.SelectedItems.Cast<string>()))
|
foreach (string path in new List<string>(GameList.SelectedItems.Cast<string>()))
|
||||||
{
|
{
|
||||||
_viewModel.GameDirectories.Remove(path);
|
ViewModel.GameDirectories.Remove(path);
|
||||||
_viewModel.DirectoryChanged = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GameList.ItemCount > 0)
|
if (GameList.ItemCount > 0)
|
||||||
|
|
|
@ -37,6 +37,7 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
LoggingPage = new SettingsLoggingView();
|
LoggingPage = new SettingsLoggingView();
|
||||||
NetworkPage = new SettingsNetworkView();
|
NetworkPage = new SettingsNetworkView();
|
||||||
SystemPage = new SettingsSystemView(virtualFileSystem, contentManager);
|
SystemPage = new SettingsSystemView(virtualFileSystem, contentManager);
|
||||||
|
UiPage = new SettingsUiView();
|
||||||
|
|
||||||
ViewModel = new SettingsViewModel(
|
ViewModel = new SettingsViewModel(
|
||||||
AudioPage.ViewModel,
|
AudioPage.ViewModel,
|
||||||
|
@ -46,9 +47,8 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
InputPage.ViewModel,
|
InputPage.ViewModel,
|
||||||
LoggingPage.ViewModel,
|
LoggingPage.ViewModel,
|
||||||
NetworkPage.ViewModel,
|
NetworkPage.ViewModel,
|
||||||
SystemPage.ViewModel);
|
SystemPage.ViewModel,
|
||||||
|
UiPage.ViewModel);
|
||||||
UiPage = new SettingsUiView(ViewModel);
|
|
||||||
|
|
||||||
DataContext = ViewModel;
|
DataContext = ViewModel;
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ namespace Ryujinx.Ava.UI.Windows
|
||||||
|
|
||||||
protected override void OnClosing(WindowClosingEventArgs e)
|
protected override void OnClosing(WindowClosingEventArgs e)
|
||||||
{
|
{
|
||||||
if (Owner is MainWindow window && ViewModel.DirectoryChanged)
|
if (Owner is MainWindow window && UiPage.ViewModel.DirsChanged)
|
||||||
{
|
{
|
||||||
window.LoadApplications();
|
window.LoadApplications();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue