mirror of
https://git.743378673.xyz/MeloNX/MeloNX.git
synced 2025-08-02 16:27:09 +02:00
Ava GUI: Restructure Ryujinx.Ava
(#4165)
* Restructure `Ryujinx.Ava` * Stylistic consistency * Update Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Update Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com> * Fix redundancies * Remove redunancies * Add back elses Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
This commit is contained in:
parent
3d1a0bf374
commit
76671d63d4
113 changed files with 624 additions and 506 deletions
198
Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs
Normal file
198
Ryujinx.Ava/UI/Applet/AvaHostUiHandler.cs
Normal file
|
@ -0,0 +1,198 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.HLE.HOS.Applets;
|
||||
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
|
||||
using Ryujinx.HLE.Ui;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Applet
|
||||
{
|
||||
internal class AvaHostUiHandler : IHostUiHandler
|
||||
{
|
||||
private readonly MainWindow _parent;
|
||||
|
||||
public IHostUiTheme HostUiTheme { get; }
|
||||
|
||||
public AvaHostUiHandler(MainWindow parent)
|
||||
{
|
||||
_parent = parent;
|
||||
|
||||
HostUiTheme = new AvaloniaHostUiTheme(parent);
|
||||
}
|
||||
|
||||
public bool DisplayMessageDialog(ControllerAppletUiArgs args)
|
||||
{
|
||||
string playerCount = args.PlayerCountMin == args.PlayerCountMax
|
||||
? args.PlayerCountMin.ToString()
|
||||
: $"{args.PlayerCountMin}-{args.PlayerCountMax}";
|
||||
|
||||
string key = args.PlayerCountMin == args.PlayerCountMax ? "DialogControllerAppletMessage" : "DialogControllerAppletMessagePlayerRange";
|
||||
|
||||
string message = string.Format(LocaleManager.Instance[key],
|
||||
playerCount,
|
||||
args.SupportedStyles,
|
||||
string.Join(", ", args.SupportedPlayers),
|
||||
args.IsDocked ? LocaleManager.Instance["DialogControllerAppletDockModeSet"] : "");
|
||||
|
||||
return DisplayMessageDialog(LocaleManager.Instance["DialogControllerAppletTitle"], message);
|
||||
}
|
||||
|
||||
public bool DisplayMessageDialog(string title, string message)
|
||||
{
|
||||
ManualResetEvent dialogCloseEvent = new(false);
|
||||
|
||||
bool okPressed = false;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ManualResetEvent deferEvent = new(false);
|
||||
|
||||
bool opened = false;
|
||||
|
||||
UserResult response = await ContentDialogHelper.ShowDeferredContentDialog(_parent,
|
||||
title,
|
||||
message,
|
||||
"",
|
||||
LocaleManager.Instance["DialogOpenSettingsWindowLabel"],
|
||||
"",
|
||||
LocaleManager.Instance["SettingsButtonClose"],
|
||||
(int)Symbol.Important,
|
||||
deferEvent,
|
||||
async (window) =>
|
||||
{
|
||||
if (opened)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
|
||||
_parent.SettingsWindow = new SettingsWindow(_parent.VirtualFileSystem, _parent.ContentManager);
|
||||
|
||||
await _parent.SettingsWindow.ShowDialog(window);
|
||||
|
||||
opened = false;
|
||||
});
|
||||
|
||||
if (response == UserResult.Ok)
|
||||
{
|
||||
okPressed = true;
|
||||
}
|
||||
|
||||
dialogCloseEvent.Set();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogMessageDialogErrorExceptionMessage"], ex));
|
||||
|
||||
dialogCloseEvent.Set();
|
||||
}
|
||||
});
|
||||
|
||||
dialogCloseEvent.WaitOne();
|
||||
|
||||
return okPressed;
|
||||
}
|
||||
|
||||
public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
|
||||
{
|
||||
ManualResetEvent dialogCloseEvent = new(false);
|
||||
|
||||
bool okPressed = false;
|
||||
bool error = false;
|
||||
string inputText = args.InitialText ?? "";
|
||||
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await SwkbdAppletDialog.ShowInputDialog(_parent, LocaleManager.Instance["SoftwareKeyboard"], args);
|
||||
|
||||
if (response.Result == UserResult.Ok)
|
||||
{
|
||||
inputText = response.Input;
|
||||
okPressed = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = true;
|
||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogSoftwareKeyboardErrorExceptionMessage"], ex));
|
||||
}
|
||||
finally
|
||||
{
|
||||
dialogCloseEvent.Set();
|
||||
}
|
||||
});
|
||||
|
||||
dialogCloseEvent.WaitOne();
|
||||
|
||||
userText = error ? null : inputText;
|
||||
|
||||
return error || okPressed;
|
||||
}
|
||||
|
||||
public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value)
|
||||
{
|
||||
device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value);
|
||||
if (_parent.AppHost != null)
|
||||
{
|
||||
_parent.AppHost.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
public bool DisplayErrorAppletDialog(string title, string message, string[] buttons)
|
||||
{
|
||||
ManualResetEvent dialogCloseEvent = new(false);
|
||||
|
||||
bool showDetails = false;
|
||||
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ErrorAppletWindow msgDialog = new(_parent, buttons, message)
|
||||
{
|
||||
Title = title,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||||
Width = 400
|
||||
};
|
||||
|
||||
object response = await msgDialog.Run();
|
||||
|
||||
if (response != null && buttons != null && buttons.Length > 1 && (int)response != buttons.Length - 1)
|
||||
{
|
||||
showDetails = true;
|
||||
}
|
||||
|
||||
dialogCloseEvent.Set();
|
||||
|
||||
msgDialog.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
dialogCloseEvent.Set();
|
||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogErrorAppletErrorExceptionMessage"], ex));
|
||||
}
|
||||
});
|
||||
|
||||
dialogCloseEvent.WaitOne();
|
||||
|
||||
return showDetails;
|
||||
}
|
||||
|
||||
public IDynamicTextInputHandler CreateDynamicTextInputHandler()
|
||||
{
|
||||
return new AvaloniaDynamicTextInputHandler(_parent);
|
||||
}
|
||||
}
|
||||
}
|
164
Ryujinx.Ava/UI/Applet/AvaloniaDynamicTextInputHandler.cs
Normal file
164
Ryujinx.Ava/UI/Applet/AvaloniaDynamicTextInputHandler.cs
Normal file
|
@ -0,0 +1,164 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Input;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.HLE.Ui;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
using HidKey = Ryujinx.Common.Configuration.Hid.Key;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Applet
|
||||
{
|
||||
class AvaloniaDynamicTextInputHandler : IDynamicTextInputHandler
|
||||
{
|
||||
private MainWindow _parent;
|
||||
private OffscreenTextBox _hiddenTextBox;
|
||||
private bool _canProcessInput;
|
||||
private IDisposable _textChangedSubscription;
|
||||
private IDisposable _selectionStartChangedSubscription;
|
||||
private IDisposable _selectionEndtextChangedSubscription;
|
||||
|
||||
public AvaloniaDynamicTextInputHandler(MainWindow parent)
|
||||
{
|
||||
_parent = parent;
|
||||
|
||||
(_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed;
|
||||
(_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyRelease += AvaloniaDynamicTextInputHandler_KeyRelease;
|
||||
(_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).TextInput += AvaloniaDynamicTextInputHandler_TextInput;
|
||||
|
||||
_hiddenTextBox = _parent.HiddenTextBox;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_textChangedSubscription = _hiddenTextBox.GetObservable(TextBox.TextProperty).Subscribe(TextChanged);
|
||||
_selectionStartChangedSubscription = _hiddenTextBox.GetObservable(TextBox.SelectionStartProperty).Subscribe(SelectionChanged);
|
||||
_selectionEndtextChangedSubscription = _hiddenTextBox.GetObservable(TextBox.SelectionEndProperty).Subscribe(SelectionChanged);
|
||||
});
|
||||
}
|
||||
|
||||
private void TextChanged(string text)
|
||||
{
|
||||
TextChangedEvent?.Invoke(text ?? string.Empty, _hiddenTextBox.SelectionStart, _hiddenTextBox.SelectionEnd, true);
|
||||
}
|
||||
|
||||
private void SelectionChanged(int selection)
|
||||
{
|
||||
if (_hiddenTextBox.SelectionEnd < _hiddenTextBox.SelectionStart)
|
||||
{
|
||||
_hiddenTextBox.SelectionStart = _hiddenTextBox.SelectionEnd;
|
||||
}
|
||||
|
||||
TextChangedEvent?.Invoke(_hiddenTextBox.Text ?? string.Empty, _hiddenTextBox.SelectionStart, _hiddenTextBox.SelectionEnd, true);
|
||||
}
|
||||
|
||||
private void AvaloniaDynamicTextInputHandler_TextInput(object sender, string text)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (_canProcessInput)
|
||||
{
|
||||
_hiddenTextBox.SendText(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, KeyEventArgs e)
|
||||
{
|
||||
var key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key);
|
||||
|
||||
if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.RoutedEvent = _hiddenTextBox.GetKeyUpRoutedEvent();
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (_canProcessInput)
|
||||
{
|
||||
_hiddenTextBox.SendKeyUpEvent(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void AvaloniaDynamicTextInputHandler_KeyPressed(object sender, KeyEventArgs e)
|
||||
{
|
||||
var key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key);
|
||||
|
||||
if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.RoutedEvent = _hiddenTextBox.GetKeyUpRoutedEvent();
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (_canProcessInput)
|
||||
{
|
||||
_hiddenTextBox.SendKeyDownEvent(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public bool TextProcessingEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
return Volatile.Read(ref _canProcessInput);
|
||||
}
|
||||
set
|
||||
{
|
||||
Volatile.Write(ref _canProcessInput, value);
|
||||
}
|
||||
}
|
||||
|
||||
public event DynamicTextChangedHandler TextChangedEvent;
|
||||
public event KeyPressedHandler KeyPressedEvent;
|
||||
public event KeyReleasedHandler KeyReleasedEvent;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
(_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed;
|
||||
(_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease;
|
||||
(_parent.InputManager.KeyboardDriver as AvaloniaKeyboardDriver).TextInput -= AvaloniaDynamicTextInputHandler_TextInput;
|
||||
|
||||
_textChangedSubscription?.Dispose();
|
||||
_selectionStartChangedSubscription?.Dispose();
|
||||
_selectionEndtextChangedSubscription?.Dispose();
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_hiddenTextBox.Clear();
|
||||
_parent.RendererControl.Focus();
|
||||
|
||||
_parent = null;
|
||||
});
|
||||
}
|
||||
|
||||
public void SetText(string text, int cursorBegin)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_hiddenTextBox.Text = text;
|
||||
_hiddenTextBox.CaretIndex = cursorBegin;
|
||||
});
|
||||
}
|
||||
|
||||
public void SetText(string text, int cursorBegin, int cursorEnd)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_hiddenTextBox.Text = text;
|
||||
_hiddenTextBox.SelectionStart = cursorBegin;
|
||||
_hiddenTextBox.SelectionEnd = cursorEnd;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
43
Ryujinx.Ava/UI/Applet/AvaloniaHostUiTheme.cs
Normal file
43
Ryujinx.Ava/UI/Applet/AvaloniaHostUiTheme.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
using Avalonia.Media;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.HLE.Ui;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Applet
|
||||
{
|
||||
class AvaloniaHostUiTheme : IHostUiTheme
|
||||
{
|
||||
public AvaloniaHostUiTheme(MainWindow parent)
|
||||
{
|
||||
FontFamily = OperatingSystem.IsWindows() && OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000, 0) ? "Segoe UI Variable" : parent.FontFamily.Name;
|
||||
DefaultBackgroundColor = BrushToThemeColor(parent.Background);
|
||||
DefaultForegroundColor = BrushToThemeColor(parent.Foreground);
|
||||
DefaultBorderColor = BrushToThemeColor(parent.BorderBrush);
|
||||
SelectionBackgroundColor = BrushToThemeColor(parent.SearchBox.SelectionBrush);
|
||||
SelectionForegroundColor = BrushToThemeColor(parent.SearchBox.SelectionForegroundBrush);
|
||||
}
|
||||
|
||||
public string FontFamily { get; }
|
||||
|
||||
public ThemeColor DefaultBackgroundColor { get; }
|
||||
public ThemeColor DefaultForegroundColor { get; }
|
||||
public ThemeColor DefaultBorderColor { get; }
|
||||
public ThemeColor SelectionBackgroundColor { get; }
|
||||
public ThemeColor SelectionForegroundColor { get; }
|
||||
|
||||
private ThemeColor BrushToThemeColor(IBrush brush)
|
||||
{
|
||||
if (brush is SolidColorBrush solidColor)
|
||||
{
|
||||
return new ThemeColor((float)solidColor.Color.A / 255,
|
||||
(float)solidColor.Color.R / 255,
|
||||
(float)solidColor.Color.G / 255,
|
||||
(float)solidColor.Color.B / 255);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ThemeColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml
Normal file
52
Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml
Normal file
|
@ -0,0 +1,52 @@
|
|||
<Window
|
||||
x:Class="Ryujinx.Ava.UI.Applet.ErrorAppletWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="{locale:Locale ErrorWindowTitle}"
|
||||
Width="450"
|
||||
Height="340"
|
||||
CanResize="False"
|
||||
SizeToContent="Height"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<Grid
|
||||
Margin="20"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="0"
|
||||
Height="80"
|
||||
MinWidth="50"
|
||||
Margin="5,10,20,10"
|
||||
Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="10"
|
||||
VerticalAlignment="Stretch"
|
||||
Text="{Binding Message}"
|
||||
TextWrapping="Wrap" />
|
||||
<StackPanel
|
||||
Name="ButtonStack"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10" />
|
||||
</Grid>
|
||||
</Window>
|
80
Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml.cs
Normal file
80
Ryujinx.Ava/UI/Applet/ErrorAppletWindow.axaml.cs
Normal file
|
@ -0,0 +1,80 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Applet
|
||||
{
|
||||
internal partial class ErrorAppletWindow : StyleableWindow
|
||||
{
|
||||
private readonly Window _owner;
|
||||
private object _buttonResponse;
|
||||
|
||||
public ErrorAppletWindow(Window owner, string[] buttons, string message)
|
||||
{
|
||||
_owner = owner;
|
||||
Message = message;
|
||||
DataContext = this;
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
int responseId = 0;
|
||||
|
||||
if (buttons != null)
|
||||
{
|
||||
foreach (string buttonText in buttons)
|
||||
{
|
||||
AddButton(buttonText, responseId);
|
||||
responseId++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddButton(LocaleManager.Instance["InputDialogOk"], 0);
|
||||
}
|
||||
}
|
||||
|
||||
public ErrorAppletWindow()
|
||||
{
|
||||
DataContext = this;
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
private void AddButton(string label, object tag)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
Button button = new() { Content = label, Tag = tag };
|
||||
|
||||
button.Click += Button_Click;
|
||||
ButtonStack.Children.Add(button);
|
||||
});
|
||||
}
|
||||
|
||||
private void Button_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
_buttonResponse = button.Tag;
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
public async Task<object> Run()
|
||||
{
|
||||
await ShowDialog(_owner);
|
||||
|
||||
return _buttonResponse;
|
||||
}
|
||||
}
|
||||
}
|
64
Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml
Normal file
64
Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml
Normal file
|
@ -0,0 +1,64 @@
|
|||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Controls.SwkbdAppletDialog"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Width="400"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<Grid
|
||||
Margin="20"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="5"
|
||||
Height="80"
|
||||
MinWidth="50"
|
||||
Margin="5,10,20,10"
|
||||
VerticalAlignment="Center"
|
||||
Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
Text="{Binding MainText}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
Text="{Binding SecondaryText}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBox
|
||||
Name="Input"
|
||||
Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
KeyUp="Message_KeyUp"
|
||||
Text="{Binding Message}"
|
||||
TextInput="Message_TextInput"
|
||||
TextWrapping="Wrap"
|
||||
UseFloatingWatermark="True" />
|
||||
<TextBlock
|
||||
Name="Error"
|
||||
Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</UserControl>
|
177
Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml.cs
Normal file
177
Ryujinx.Ava/UI/Applet/SwkbdAppletDialog.axaml.cs
Normal file
|
@ -0,0 +1,177 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using FluentAvalonia.Core;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.HLE.HOS.Applets;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
internal partial class SwkbdAppletDialog : UserControl
|
||||
{
|
||||
private Predicate<int> _checkLength;
|
||||
private int _inputMax;
|
||||
private int _inputMin;
|
||||
private string _placeholder;
|
||||
|
||||
private ContentDialog _host;
|
||||
|
||||
public SwkbdAppletDialog(string mainText, string secondaryText, string placeholder)
|
||||
{
|
||||
MainText = mainText;
|
||||
SecondaryText = secondaryText;
|
||||
DataContext = this;
|
||||
_placeholder = placeholder;
|
||||
InitializeComponent();
|
||||
|
||||
Input.Watermark = _placeholder;
|
||||
|
||||
Input.AddHandler(TextInputEvent, Message_TextInput, RoutingStrategies.Tunnel, true);
|
||||
|
||||
SetInputLengthValidation(0, int.MaxValue); // Disable by default.
|
||||
}
|
||||
|
||||
public SwkbdAppletDialog()
|
||||
{
|
||||
DataContext = this;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public string Message { get; set; } = "";
|
||||
public string MainText { get; set; } = "";
|
||||
public string SecondaryText { get; set; } = "";
|
||||
|
||||
public static async Task<(UserResult Result, string Input)> ShowInputDialog(StyleableWindow window, string title, SoftwareKeyboardUiArgs args)
|
||||
{
|
||||
ContentDialog contentDialog = new ContentDialog();
|
||||
|
||||
UserResult result = UserResult.Cancel;
|
||||
|
||||
SwkbdAppletDialog content = new SwkbdAppletDialog(args.HeaderText, args.SubtitleText, args.GuideText)
|
||||
{
|
||||
Message = args.InitialText ?? ""
|
||||
};
|
||||
|
||||
string input = string.Empty;
|
||||
|
||||
var overlay = new ContentDialogOverlayWindow()
|
||||
{
|
||||
Height = window.Bounds.Height,
|
||||
Width = window.Bounds.Width,
|
||||
Position = window.PointToScreen(new Point())
|
||||
};
|
||||
|
||||
window.PositionChanged += OverlayOnPositionChanged;
|
||||
|
||||
void OverlayOnPositionChanged(object sender, PixelPointEventArgs e)
|
||||
{
|
||||
overlay.Position = window.PointToScreen(new Point());
|
||||
}
|
||||
|
||||
contentDialog = overlay.ContentDialog;
|
||||
|
||||
bool opened = false;
|
||||
|
||||
content.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax);
|
||||
|
||||
content._host = contentDialog;
|
||||
contentDialog.Title = title;
|
||||
contentDialog.PrimaryButtonText = args.SubmitText;
|
||||
contentDialog.IsPrimaryButtonEnabled = content._checkLength(content.Message.Length);
|
||||
contentDialog.SecondaryButtonText = "";
|
||||
contentDialog.CloseButtonText = LocaleManager.Instance["InputDialogCancel"];
|
||||
contentDialog.Content = content;
|
||||
|
||||
TypedEventHandler<ContentDialog, ContentDialogClosedEventArgs> handler = (sender, eventArgs) =>
|
||||
{
|
||||
if (eventArgs.Result == ContentDialogResult.Primary)
|
||||
{
|
||||
result = UserResult.Ok;
|
||||
input = content.Input.Text;
|
||||
}
|
||||
};
|
||||
contentDialog.Closed += handler;
|
||||
|
||||
overlay.Opened += OverlayOnActivated;
|
||||
|
||||
async void OverlayOnActivated(object sender, EventArgs e)
|
||||
{
|
||||
if (opened)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
|
||||
overlay.Position = window.PointToScreen(new Point());
|
||||
|
||||
await contentDialog.ShowAsync(overlay);
|
||||
contentDialog.Closed -= handler;
|
||||
overlay.Close();
|
||||
};
|
||||
|
||||
await overlay.ShowDialog(window);
|
||||
|
||||
return (result, input);
|
||||
}
|
||||
|
||||
public void SetInputLengthValidation(int min, int max)
|
||||
{
|
||||
_inputMin = Math.Min(min, max);
|
||||
_inputMax = Math.Max(min, max);
|
||||
|
||||
Error.IsVisible = false;
|
||||
Error.FontStyle = FontStyle.Italic;
|
||||
|
||||
if (_inputMin <= 0 && _inputMax == int.MaxValue) // Disable.
|
||||
{
|
||||
Error.IsVisible = false;
|
||||
|
||||
_checkLength = length => true;
|
||||
}
|
||||
else if (_inputMin > 0 && _inputMax == int.MaxValue)
|
||||
{
|
||||
Error.IsVisible = true;
|
||||
Error.Text = string.Format(LocaleManager.Instance["SwkbdMinCharacters"], _inputMin);
|
||||
|
||||
_checkLength = length => _inputMin <= length;
|
||||
}
|
||||
else
|
||||
{
|
||||
Error.IsVisible = true;
|
||||
Error.Text = string.Format(LocaleManager.Instance["SwkbdMinRangeCharacters"], _inputMin, _inputMax);
|
||||
|
||||
_checkLength = length => _inputMin <= length && length <= _inputMax;
|
||||
}
|
||||
|
||||
Message_TextInput(this, new TextInputEventArgs());
|
||||
}
|
||||
|
||||
private void Message_TextInput(object sender, TextInputEventArgs e)
|
||||
{
|
||||
if (_host != null)
|
||||
{
|
||||
_host.IsPrimaryButtonEnabled = _checkLength(Message.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private void Message_KeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter && _host.IsPrimaryButtonEnabled)
|
||||
{
|
||||
_host.Hide(ContentDialogResult.Primary);
|
||||
}
|
||||
else
|
||||
{
|
||||
_host.IsPrimaryButtonEnabled = _checkLength(Message.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
195
Ryujinx.Ava/UI/Controls/GameGridView.axaml
Normal file
195
Ryujinx.Ava/UI/Controls/GameGridView.axaml
Normal file
|
@ -0,0 +1,195 @@
|
|||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Controls.GameGridView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<UserControl.Resources>
|
||||
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||
<MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened">
|
||||
<MenuItem
|
||||
Command="{Binding ToggleFavorite}"
|
||||
Header="{locale:Locale GameListContextMenuToggleFavorite}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{Binding OpenUserSaveDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenDeviceSaveDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuOpenUserDeviceDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserDeviceDirectoryToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenBcatSaveDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuOpenUserBcatDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserBcatDirectoryToolTip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{Binding OpenTitleUpdateManager}"
|
||||
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenDownloadableContentManager}"
|
||||
Header="{locale:Locale GameListContextMenuManageDlc}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenCheatManager}"
|
||||
Header="{locale:Locale GameListContextMenuManageCheat}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenModsDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuOpenModsDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenSdModsDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
|
||||
<Separator />
|
||||
<MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
|
||||
<MenuItem
|
||||
Command="{Binding PurgePtcCache}"
|
||||
Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding PurgeShaderCache}"
|
||||
Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenPtcDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenShaderCacheDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="{locale:Locale GameListContextMenuExtractData}">
|
||||
<MenuItem
|
||||
Command="{Binding ExtractExeFs}"
|
||||
Header="{locale:Locale GameListContextMenuExtractDataExeFS}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding ExtractRomFs}"
|
||||
Header="{locale:Locale GameListContextMenuExtractDataRomFS}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding ExtractLogo}"
|
||||
Header="{locale:Locale GameListContextMenuExtractDataLogo}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
|
||||
</MenuItem>
|
||||
</MenuFlyout>
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListBox
|
||||
Grid.Row="0"
|
||||
Padding="8"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ContextFlyout="{StaticResource GameContextMenu}"
|
||||
DoubleTapped="GameList_DoubleTapped"
|
||||
Items="{Binding AppsObservableList}"
|
||||
SelectionChanged="GameList_SelectionChanged">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<flex:FlexPanel
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
AlignContent="FlexStart"
|
||||
JustifyContent="Center" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="5" />
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
|
||||
<Style.Animations>
|
||||
<Animation Duration="0:0:0.7">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="MaxWidth" Value="0" />
|
||||
<Setter Property="Opacity" Value="0.0" />
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="50%">
|
||||
<Setter Property="MaxWidth" Value="1000" />
|
||||
<Setter Property="Opacity" Value="0.3" />
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="MaxWidth" Value="1000" />
|
||||
<Setter Property="Opacity" Value="1.0" />
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Border
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
|
||||
Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
|
||||
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
|
||||
Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="4">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
|
||||
<Panel
|
||||
Grid.Row="1"
|
||||
Height="50"
|
||||
Margin="0 10 0 0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
IsVisible="{Binding $parent[UserControl].DataContext.ShowNames}">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding TitleName}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</Panel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ui:SymbolIcon
|
||||
Margin="5,5,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource SystemAccentColor}"
|
||||
IsVisible="{Binding Favorite}"
|
||||
Symbol="StarFilled" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</UserControl>
|
83
Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs
Normal file
83
Ryujinx.Ava/UI/Controls/GameGridView.axaml.cs
Normal file
|
@ -0,0 +1,83 @@
|
|||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using LibHac.Common;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class GameGridView : UserControl
|
||||
{
|
||||
private ApplicationData _selectedApplication;
|
||||
public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
|
||||
RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
|
||||
|
||||
public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened
|
||||
{
|
||||
add { AddHandler(ApplicationOpenedEvent, value); }
|
||||
remove { RemoveHandler(ApplicationOpenedEvent, value); }
|
||||
}
|
||||
|
||||
public void GameList_DoubleTapped(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is ListBox listBox)
|
||||
{
|
||||
if (listBox.SelectedItem is ApplicationData selected)
|
||||
{
|
||||
RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args)
|
||||
{
|
||||
if (sender is ListBox listBox)
|
||||
{
|
||||
var selected = listBox.SelectedItem as ApplicationData;
|
||||
|
||||
_selectedApplication = selected;
|
||||
}
|
||||
}
|
||||
|
||||
public ApplicationData SelectedApplication => _selectedApplication;
|
||||
|
||||
public GameGridView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
(DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text;
|
||||
}
|
||||
|
||||
private void MenuBase_OnMenuOpened(object sender, EventArgs e)
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
if (selection != null)
|
||||
{
|
||||
if (sender is ContextMenu menu)
|
||||
{
|
||||
bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0;
|
||||
bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0;
|
||||
bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
||||
|
||||
((menu.Items as AvaloniaList<object>)[2] as MenuItem).IsEnabled = canHaveUserSave;
|
||||
((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave;
|
||||
((menu.Items as AvaloniaList<object>)[4] as MenuItem).IsEnabled = canHaveBcatSave;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
234
Ryujinx.Ava/UI/Controls/GameListView.axaml
Normal file
234
Ryujinx.Ava/UI/Controls/GameListView.axaml
Normal file
|
@ -0,0 +1,234 @@
|
|||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Controls.GameListView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<UserControl.Resources>
|
||||
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||
<MenuFlyout x:Key="GameContextMenu" Opened="MenuBase_OnMenuOpened">
|
||||
<MenuItem
|
||||
Command="{Binding ToggleFavorite}"
|
||||
Header="{locale:Locale GameListContextMenuToggleFavorite}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{Binding OpenUserSaveDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuOpenUserSaveDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserSaveDirectoryToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenDeviceSaveDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuOpenUserDeviceDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserDeviceDirectoryToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenBcatSaveDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuOpenUserBcatDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenUserBcatDirectoryToolTip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{Binding OpenTitleUpdateManager}"
|
||||
Header="{locale:Locale GameListContextMenuManageTitleUpdates}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageTitleUpdatesToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenDownloadableContentManager}"
|
||||
Header="{locale:Locale GameListContextMenuManageDlc}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageDlcToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenCheatManager}"
|
||||
Header="{locale:Locale GameListContextMenuManageCheat}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuManageCheatToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenModsDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuOpenModsDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenModsDirectoryToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenSdModsDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuOpenSdModsDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuOpenSdModsDirectoryToolTip}" />
|
||||
<Separator />
|
||||
<MenuItem Header="{locale:Locale GameListContextMenuCacheManagement}">
|
||||
<MenuItem
|
||||
Command="{Binding PurgePtcCache}"
|
||||
Header="{locale:Locale GameListContextMenuCacheManagementPurgePptc}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding PurgeShaderCache}"
|
||||
Header="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCache}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementPurgeShaderCacheToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenPtcDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenPptcDirectoryToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenShaderCacheDirectory}"
|
||||
Header="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectory}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip}" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="{locale:Locale GameListContextMenuExtractData}">
|
||||
<MenuItem
|
||||
Command="{Binding ExtractExeFs}"
|
||||
Header="{locale:Locale GameListContextMenuExtractDataExeFS}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataExeFSToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding ExtractRomFs}"
|
||||
Header="{locale:Locale GameListContextMenuExtractDataRomFS}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataRomFSToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding ExtractLogo}"
|
||||
Header="{locale:Locale GameListContextMenuExtractDataLogo}"
|
||||
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
|
||||
</MenuItem>
|
||||
</MenuFlyout>
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListBox
|
||||
Name="GameListBox"
|
||||
Grid.Row="0"
|
||||
Padding="8"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ContextFlyout="{StaticResource GameContextMenu}"
|
||||
DoubleTapped="GameList_DoubleTapped"
|
||||
Items="{Binding AppsObservableList}"
|
||||
SelectionChanged="GameList_SelectionChanged">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="2" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="5" />
|
||||
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
|
||||
<Setter Property="BorderThickness" Value="2"/>
|
||||
<Style.Animations>
|
||||
<Animation Duration="0:0:0.7">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="MaxHeight" Value="0" />
|
||||
<Setter Property="Opacity" Value="0.0" />
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="50%">
|
||||
<Setter Property="MaxHeight" Value="1000" />
|
||||
<Setter Property="Opacity" Value="0.3" />
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="MaxHeight" Value="1000" />
|
||||
<Setter Property="Opacity" Value="1.0" />
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
|
||||
<Setter Property="MinHeight" Value="100" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Border
|
||||
Margin="0"
|
||||
Padding="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="5">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="10" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="0"
|
||||
Margin="0"
|
||||
Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
|
||||
Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
|
||||
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
|
||||
Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
|
||||
Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Orientation="Vertical"
|
||||
Spacing="5" >
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding TitleName}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding Developer}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding Version}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Column="3"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Orientation="Vertical"
|
||||
Spacing="5">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding TimePlayed}"
|
||||
TextAlignment="Right"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding LastPlayed}"
|
||||
TextAlignment="Right"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding FileSize}"
|
||||
TextAlignment="Right"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ui:SymbolIcon
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="-5,-5,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
FontSize="16"
|
||||
Foreground="{DynamicResource SystemAccentColor}"
|
||||
IsVisible="{Binding Favorite}"
|
||||
Symbol="StarFilled" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</UserControl>
|
83
Ryujinx.Ava/UI/Controls/GameListView.axaml.cs
Normal file
83
Ryujinx.Ava/UI/Controls/GameListView.axaml.cs
Normal file
|
@ -0,0 +1,83 @@
|
|||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using LibHac.Common;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class GameListView : UserControl
|
||||
{
|
||||
private ApplicationData _selectedApplication;
|
||||
public static readonly RoutedEvent<ApplicationOpenedEventArgs> ApplicationOpenedEvent =
|
||||
RoutedEvent.Register<GameGridView, ApplicationOpenedEventArgs>(nameof(ApplicationOpened), RoutingStrategies.Bubble);
|
||||
|
||||
public event EventHandler<ApplicationOpenedEventArgs> ApplicationOpened
|
||||
{
|
||||
add { AddHandler(ApplicationOpenedEvent, value); }
|
||||
remove { RemoveHandler(ApplicationOpenedEvent, value); }
|
||||
}
|
||||
|
||||
public void GameList_DoubleTapped(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is ListBox listBox)
|
||||
{
|
||||
if (listBox.SelectedItem is ApplicationData selected)
|
||||
{
|
||||
RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void GameList_SelectionChanged(object sender, SelectionChangedEventArgs args)
|
||||
{
|
||||
if (sender is ListBox listBox)
|
||||
{
|
||||
var selected = listBox.SelectedItem as ApplicationData;
|
||||
|
||||
_selectedApplication = selected;
|
||||
}
|
||||
}
|
||||
|
||||
public ApplicationData SelectedApplication => _selectedApplication;
|
||||
|
||||
public GameListView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
(DataContext as MainWindowViewModel).SearchText = (sender as TextBox).Text;
|
||||
}
|
||||
|
||||
private void MenuBase_OnMenuOpened(object sender, EventArgs e)
|
||||
{
|
||||
var selection = SelectedApplication;
|
||||
|
||||
if (selection != null)
|
||||
{
|
||||
if (sender is ContextMenu menu)
|
||||
{
|
||||
bool canHaveUserSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.UserAccountSaveDataSize > 0;
|
||||
bool canHaveDeviceSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.DeviceSaveDataSize > 0;
|
||||
bool canHaveBcatSave = !Utilities.IsZeros(selection.ControlHolder.ByteSpan) && selection.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
||||
|
||||
((menu.Items as AvaloniaList<object>)[2] as MenuItem).IsEnabled = canHaveUserSave;
|
||||
((menu.Items as AvaloniaList<object>)[3] as MenuItem).IsEnabled = canHaveDeviceSave;
|
||||
((menu.Items as AvaloniaList<object>)[4] as MenuItem).IsEnabled = canHaveBcatSave;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
Ryujinx.Ava/UI/Controls/InputDialog.axaml
Normal file
32
Ryujinx.Ava/UI/Controls/InputDialog.axaml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Controls.InputDialog"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<Grid
|
||||
Margin="5,10,5,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock HorizontalAlignment="Center" Text="{Binding Message}" />
|
||||
<TextBox
|
||||
Grid.Row="1"
|
||||
Width="300"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Center"
|
||||
MaxLength="{Binding MaxLength}"
|
||||
Text="{Binding Input, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Margin="5,5,5,10"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{Binding SubMessage}" />
|
||||
</Grid>
|
||||
</UserControl>
|
57
Ryujinx.Ava/UI/Controls/InputDialog.axaml.cs
Normal file
57
Ryujinx.Ava/UI/Controls/InputDialog.axaml.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
using Avalonia.Controls;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class InputDialog : UserControl
|
||||
{
|
||||
public string Message { get; set; }
|
||||
public string Input { get; set; }
|
||||
public string SubMessage { get; set; }
|
||||
|
||||
public uint MaxLength { get; }
|
||||
|
||||
public InputDialog(string message, string input = "", string subMessage = "", uint maxLength = int.MaxValue)
|
||||
{
|
||||
Message = message;
|
||||
Input = input;
|
||||
SubMessage = subMessage;
|
||||
MaxLength = maxLength;
|
||||
|
||||
DataContext = this;
|
||||
}
|
||||
|
||||
public InputDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public static async Task<(UserResult Result, string Input)> ShowInputDialog(string title, string message,
|
||||
string input = "", string subMessage = "", uint maxLength = int.MaxValue)
|
||||
{
|
||||
UserResult result = UserResult.Cancel;
|
||||
|
||||
InputDialog content = new InputDialog(message, input, subMessage, maxLength);
|
||||
ContentDialog contentDialog = new ContentDialog
|
||||
{
|
||||
Title = title,
|
||||
PrimaryButtonText = LocaleManager.Instance["InputDialogOk"],
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = LocaleManager.Instance["InputDialogCancel"],
|
||||
Content = content,
|
||||
PrimaryButtonCommand = MiniCommand.Create(() =>
|
||||
{
|
||||
result = UserResult.Ok;
|
||||
input = content.Input;
|
||||
})
|
||||
};
|
||||
await contentDialog.ShowAsync();
|
||||
|
||||
return (result, input);
|
||||
}
|
||||
}
|
||||
}
|
16
Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml
Normal file
16
Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="800"
|
||||
d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.UI.Controls.NavigationDialogHost"
|
||||
Focusable="True">
|
||||
<ui:Frame
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
x:Name="ContentFrame" />
|
||||
</UserControl>
|
91
Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs
Normal file
91
Ryujinx.Ava/UI/Controls/NavigationDialogHost.axaml.cs
Normal file
|
@ -0,0 +1,91 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using LibHac;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class NavigationDialogHost : UserControl
|
||||
{
|
||||
public AccountManager AccountManager { get; }
|
||||
public ContentManager ContentManager { get; }
|
||||
public VirtualFileSystem VirtualFileSystem { get; }
|
||||
public HorizonClient HorizonClient { get; }
|
||||
public UserProfileViewModel ViewModel { get; set; }
|
||||
|
||||
public NavigationDialogHost()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public NavigationDialogHost(AccountManager accountManager, ContentManager contentManager,
|
||||
VirtualFileSystem virtualFileSystem, HorizonClient horizonClient)
|
||||
{
|
||||
AccountManager = accountManager;
|
||||
ContentManager = contentManager;
|
||||
VirtualFileSystem = virtualFileSystem;
|
||||
HorizonClient = horizonClient;
|
||||
ViewModel = new UserProfileViewModel(this);
|
||||
|
||||
|
||||
if (contentManager.GetCurrentFirmwareVersion() != null)
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
AvatarProfileViewModel.PreloadAvatars(contentManager, virtualFileSystem);
|
||||
});
|
||||
}
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void GoBack(object parameter = null)
|
||||
{
|
||||
if (ContentFrame.BackStack.Count > 0)
|
||||
{
|
||||
ContentFrame.GoBack();
|
||||
}
|
||||
|
||||
ViewModel.LoadProfiles();
|
||||
}
|
||||
|
||||
public void Navigate(Type sourcePageType, object parameter)
|
||||
{
|
||||
ContentFrame.Navigate(sourcePageType, parameter);
|
||||
}
|
||||
|
||||
public static async Task Show(AccountManager ownerAccountManager, ContentManager ownerContentManager,
|
||||
VirtualFileSystem ownerVirtualFileSystem, HorizonClient ownerHorizonClient)
|
||||
{
|
||||
var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient);
|
||||
ContentDialog contentDialog = new ContentDialog
|
||||
{
|
||||
Title = LocaleManager.Instance["UserProfileWindowTitle"],
|
||||
PrimaryButtonText = "",
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = LocaleManager.Instance["UserProfilesClose"],
|
||||
Content = content,
|
||||
Padding = new Thickness(0)
|
||||
};
|
||||
|
||||
contentDialog.Closed += (sender, args) =>
|
||||
{
|
||||
content.ViewModel.Dispose();
|
||||
};
|
||||
|
||||
await contentDialog.ShowAsync();
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
|
||||
Navigate(typeof(UserSelector), this);
|
||||
}
|
||||
}
|
||||
}
|
57
Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml
Normal file
57
Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml
Normal file
|
@ -0,0 +1,57 @@
|
|||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Ryujinx.Ava.UI.Controls.ProfileImageSelectionDialog"
|
||||
Focusable="True">
|
||||
<Grid
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Margin="5,10,5, 5">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="70" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
FontWeight="Bold"
|
||||
FontSize="18"
|
||||
HorizontalAlignment="Center"
|
||||
Grid.Row="1"
|
||||
Text="{locale:Locale ProfileImageSelectionHeader}" />
|
||||
<TextBlock
|
||||
FontWeight="Bold"
|
||||
Grid.Row="2"
|
||||
Margin="10"
|
||||
MaxWidth="400"
|
||||
TextWrapping="Wrap"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
Text="{locale:Locale ProfileImageSelectionNote}" />
|
||||
<StackPanel
|
||||
Margin="5,0"
|
||||
Spacing="10"
|
||||
Grid.Row="4"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Name="Import"
|
||||
Click="Import_OnClick"
|
||||
Width="200">
|
||||
<TextBlock Text="{locale:Locale ProfileImageSelectionImportImage}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="SelectFirmwareImage"
|
||||
IsEnabled="{Binding FirmwareFound}"
|
||||
Click="SelectFirmwareImage_OnClick"
|
||||
Width="200">
|
||||
<TextBlock Text="{locale:Locale ProfileImageSelectionSelectAvatar}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
105
Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs
Normal file
105
Ryujinx.Ava/UI/Controls/ProfileImageSelectionDialog.axaml.cs
Normal file
|
@ -0,0 +1,105 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.VisualTree;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.IO;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class ProfileImageSelectionDialog : UserControl
|
||||
{
|
||||
private ContentManager _contentManager;
|
||||
private NavigationDialogHost _parent;
|
||||
private TempProfile _profile;
|
||||
|
||||
public bool FirmwareFound => _contentManager.GetCurrentFirmwareVersion() != null;
|
||||
|
||||
public ProfileImageSelectionDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||
{
|
||||
NavigatedTo(e);
|
||||
}, RoutingStrategies.Direct);
|
||||
}
|
||||
|
||||
private void NavigatedTo(NavigationEventArgs arg)
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
switch (arg.NavigationMode)
|
||||
{
|
||||
case NavigationMode.New:
|
||||
(_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
|
||||
_contentManager = _parent.ContentManager;
|
||||
break;
|
||||
case NavigationMode.Back:
|
||||
_parent.GoBack();
|
||||
break;
|
||||
}
|
||||
|
||||
DataContext = this;
|
||||
}
|
||||
}
|
||||
|
||||
private async void Import_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
OpenFileDialog dialog = new();
|
||||
dialog.Filters.Add(new FileDialogFilter
|
||||
{
|
||||
Name = LocaleManager.Instance["AllSupportedFormats"],
|
||||
Extensions = { "jpg", "jpeg", "png", "bmp" }
|
||||
});
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "JPEG", Extensions = { "jpg", "jpeg" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "PNG", Extensions = { "png" } });
|
||||
dialog.Filters.Add(new FileDialogFilter { Name = "BMP", Extensions = { "bmp" } });
|
||||
|
||||
dialog.AllowMultiple = false;
|
||||
|
||||
string[] image = await dialog.ShowAsync(((TopLevel)_parent.GetVisualRoot()) as Window);
|
||||
|
||||
if (image != null)
|
||||
{
|
||||
if (image.Length > 0)
|
||||
{
|
||||
string imageFile = image[0];
|
||||
|
||||
_profile.Image = ProcessProfileImage(File.ReadAllBytes(imageFile));
|
||||
}
|
||||
|
||||
_parent.GoBack();
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (FirmwareFound)
|
||||
{
|
||||
_parent.Navigate(typeof(AvatarWindow), (_parent, _profile));
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] ProcessProfileImage(byte[] buffer)
|
||||
{
|
||||
using (Image image = Image.Load(buffer))
|
||||
{
|
||||
image.Mutate(x => x.Resize(256, 256));
|
||||
|
||||
using (MemoryStream streamJpg = new())
|
||||
{
|
||||
image.SaveAsJpeg(streamJpg);
|
||||
|
||||
return streamJpg.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Ryujinx.Ava/UI/Controls/RendererHost.axaml
Normal file
11
Ryujinx.Ava/UI/Controls/RendererHost.axaml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="800"
|
||||
d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.UI.Controls.RendererHost"
|
||||
Focusable="True">
|
||||
</UserControl>
|
127
Ryujinx.Ava/UI/Controls/RendererHost.axaml.cs
Normal file
127
Ryujinx.Ava/UI/Controls/RendererHost.axaml.cs
Normal file
|
@ -0,0 +1,127 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Silk.NET.Vulkan;
|
||||
using SPB.Graphics.OpenGL;
|
||||
using SPB.Windowing;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class RendererHost : UserControl, IDisposable
|
||||
{
|
||||
private readonly GraphicsDebugLevel _graphicsDebugLevel;
|
||||
private EmbeddedWindow _currentWindow;
|
||||
|
||||
public bool IsVulkan { get; private set; }
|
||||
|
||||
public RendererHost(GraphicsDebugLevel graphicsDebugLevel)
|
||||
{
|
||||
_graphicsDebugLevel = graphicsDebugLevel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public RendererHost()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void CreateOpenGL()
|
||||
{
|
||||
Dispose();
|
||||
|
||||
_currentWindow = new OpenGLEmbeddedWindow(3, 3, _graphicsDebugLevel);
|
||||
Initialize();
|
||||
|
||||
IsVulkan = false;
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
_currentWindow.WindowCreated += CurrentWindow_WindowCreated;
|
||||
_currentWindow.SizeChanged += CurrentWindow_SizeChanged;
|
||||
Content = _currentWindow;
|
||||
}
|
||||
|
||||
public void CreateVulkan()
|
||||
{
|
||||
Dispose();
|
||||
|
||||
_currentWindow = new VulkanEmbeddedWindow();
|
||||
Initialize();
|
||||
|
||||
IsVulkan = true;
|
||||
}
|
||||
|
||||
public OpenGLContextBase GetContext()
|
||||
{
|
||||
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
|
||||
{
|
||||
return openGlEmbeddedWindow.Context;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
|
||||
Dispose();
|
||||
}
|
||||
|
||||
private void CurrentWindow_SizeChanged(object sender, Size e)
|
||||
{
|
||||
SizeChanged?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
private void CurrentWindow_WindowCreated(object sender, IntPtr e)
|
||||
{
|
||||
RendererInitialized?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void MakeCurrent()
|
||||
{
|
||||
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
|
||||
{
|
||||
openGlEmbeddedWindow.MakeCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
public void MakeCurrent(SwappableNativeWindowBase window)
|
||||
{
|
||||
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
|
||||
{
|
||||
openGlEmbeddedWindow.MakeCurrent(window);
|
||||
}
|
||||
}
|
||||
|
||||
public void SwapBuffers()
|
||||
{
|
||||
if (_currentWindow is OpenGLEmbeddedWindow openGlEmbeddedWindow)
|
||||
{
|
||||
openGlEmbeddedWindow.SwapBuffers();
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler<EventArgs> RendererInitialized;
|
||||
public event Action<object, Size> SizeChanged;
|
||||
public void Dispose()
|
||||
{
|
||||
if (_currentWindow != null)
|
||||
{
|
||||
_currentWindow.WindowCreated -= CurrentWindow_WindowCreated;
|
||||
_currentWindow.SizeChanged -= CurrentWindow_SizeChanged;
|
||||
}
|
||||
}
|
||||
|
||||
public SurfaceKHR CreateVulkanSurface(Instance instance, Vk api)
|
||||
{
|
||||
return (_currentWindow is VulkanEmbeddedWindow vulkanEmbeddedWindow)
|
||||
? vulkanEmbeddedWindow.CreateSurface(instance)
|
||||
: default;
|
||||
}
|
||||
}
|
||||
}
|
175
Ryujinx.Ava/UI/Controls/SaveManager.axaml
Normal file
175
Ryujinx.Ava/UI/Controls/SaveManager.axaml
Normal file
|
@ -0,0 +1,175 @@
|
|||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="800"
|
||||
d:DesignHeight="450"
|
||||
Height="400"
|
||||
Width="550"
|
||||
x:Class="Ryujinx.Ava.UI.Controls.SaveManager"
|
||||
Focusable="True">
|
||||
<UserControl.Resources>
|
||||
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel
|
||||
Spacing="10"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center">
|
||||
<Label
|
||||
Content="{locale:Locale CommonSort}"
|
||||
VerticalAlignment="Center" />
|
||||
<ComboBox SelectedIndex="{Binding SortIndex}" Width="100">
|
||||
<ComboBoxItem>
|
||||
<Label
|
||||
VerticalAlignment="Center"
|
||||
HorizontalContentAlignment="Left"
|
||||
Content="{locale:Locale Name}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<Label
|
||||
VerticalAlignment="Center"
|
||||
HorizontalContentAlignment="Left"
|
||||
Content="{locale:Locale Size}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
<ComboBox SelectedIndex="{Binding OrderIndex}" Width="150">
|
||||
<ComboBoxItem>
|
||||
<Label
|
||||
VerticalAlignment="Center"
|
||||
HorizontalContentAlignment="Left"
|
||||
Content="{locale:Locale OrderAscending}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<Label
|
||||
VerticalAlignment="Center"
|
||||
HorizontalContentAlignment="Left"
|
||||
Content="{locale:Locale Descending}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="10,0, 0, 0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Content="{locale:Locale Search}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBox
|
||||
Margin="5,0,0,0"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding Search}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Margin="0,5"
|
||||
BorderThickness="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<ListBox
|
||||
Name="SaveList"
|
||||
Items="{Binding View}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:SaveModel">
|
||||
<Grid HorizontalAlignment="Stretch" Margin="0,5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||
<Border
|
||||
Height="42"
|
||||
Margin="2"
|
||||
Width="42"
|
||||
Padding="10"
|
||||
IsVisible="{Binding !InGameList}">
|
||||
<ui:SymbolIcon
|
||||
Symbol="Help"
|
||||
FontSize="30"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Image
|
||||
IsVisible="{Binding InGameList}"
|
||||
Margin="2"
|
||||
Width="42"
|
||||
Height="42"
|
||||
Source="{Binding Icon,
|
||||
Converter={StaticResource ByteImage}}" />
|
||||
<TextBlock
|
||||
MaxLines="3"
|
||||
Width="320"
|
||||
Margin="5"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding Title}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Label
|
||||
Content="{Binding SizeString}"
|
||||
IsVisible="{Binding SizeAvailable}"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right" />
|
||||
<Button
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
Padding="10"
|
||||
MinWidth="0"
|
||||
MinHeight="0"
|
||||
Name="OpenLocation"
|
||||
Command="{Binding OpenLocation}">
|
||||
<ui:SymbolIcon
|
||||
Symbol="OpenFolder"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
<Button
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
Padding="10"
|
||||
MinWidth="0"
|
||||
MinHeight="0"
|
||||
Name="Delete"
|
||||
Command="{Binding Delete}">
|
||||
<ui:SymbolIcon
|
||||
Symbol="Delete"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
160
Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs
Normal file
160
Ryujinx.Ava/UI/Controls/SaveManager.axaml.cs
Normal file
|
@ -0,0 +1,160 @@
|
|||
using Avalonia.Controls;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using LibHac;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Shim;
|
||||
using Ryujinx.Ava.Common;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class SaveManager : UserControl
|
||||
{
|
||||
private readonly UserProfile _userProfile;
|
||||
private readonly HorizonClient _horizonClient;
|
||||
private readonly VirtualFileSystem _virtualFileSystem;
|
||||
private int _sortIndex;
|
||||
private int _orderIndex;
|
||||
private ObservableCollection<SaveModel> _view = new ObservableCollection<SaveModel>();
|
||||
private string _search;
|
||||
|
||||
public ObservableCollection<SaveModel> Saves { get; set; } = new ObservableCollection<SaveModel>();
|
||||
|
||||
public ObservableCollection<SaveModel> View
|
||||
{
|
||||
get => _view;
|
||||
set => _view = value;
|
||||
}
|
||||
|
||||
public int SortIndex
|
||||
{
|
||||
get => _sortIndex;
|
||||
set
|
||||
{
|
||||
_sortIndex = value;
|
||||
Sort();
|
||||
}
|
||||
}
|
||||
|
||||
public int OrderIndex
|
||||
{
|
||||
get => _orderIndex;
|
||||
set
|
||||
{
|
||||
_orderIndex = value;
|
||||
Sort();
|
||||
}
|
||||
}
|
||||
|
||||
public string Search
|
||||
{
|
||||
get => _search;
|
||||
set
|
||||
{
|
||||
_search = value;
|
||||
Sort();
|
||||
}
|
||||
}
|
||||
|
||||
public SaveManager()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public SaveManager(UserProfile userProfile, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem)
|
||||
{
|
||||
_userProfile = userProfile;
|
||||
_horizonClient = horizonClient;
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
InitializeComponent();
|
||||
|
||||
DataContext = this;
|
||||
|
||||
Task.Run(LoadSaves);
|
||||
}
|
||||
|
||||
public void LoadSaves()
|
||||
{
|
||||
Saves.Clear();
|
||||
var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account,
|
||||
new UserId((ulong)_userProfile.UserId.High, (ulong)_userProfile.UserId.Low), saveDataId: default, index: default);
|
||||
|
||||
using var saveDataIterator = new UniqueRef<SaveDataIterator>();
|
||||
|
||||
_horizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
|
||||
|
||||
Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
|
||||
|
||||
while (true)
|
||||
{
|
||||
saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure();
|
||||
|
||||
if (readCount == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < readCount; i++)
|
||||
{
|
||||
var save = saveDataInfo[i];
|
||||
if (save.ProgramId.Value != 0)
|
||||
{
|
||||
var saveModel = new SaveModel(save, _horizonClient, _virtualFileSystem);
|
||||
Saves.Add(saveModel);
|
||||
saveModel.DeleteAction = () => { Saves.Remove(saveModel); };
|
||||
}
|
||||
|
||||
Sort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Sort()
|
||||
{
|
||||
Saves.AsObservableChangeSet()
|
||||
.Filter(Filter)
|
||||
.Sort(GetComparer())
|
||||
.Bind(out var view).AsObservableList();
|
||||
|
||||
_view.Clear();
|
||||
_view.AddRange(view);
|
||||
}
|
||||
|
||||
private IComparer<SaveModel> GetComparer()
|
||||
{
|
||||
switch (SortIndex)
|
||||
{
|
||||
case 0:
|
||||
return OrderIndex == 0
|
||||
? SortExpressionComparer<SaveModel>.Ascending(save => save.Title)
|
||||
: SortExpressionComparer<SaveModel>.Descending(save => save.Title);
|
||||
case 1:
|
||||
return OrderIndex == 0
|
||||
? SortExpressionComparer<SaveModel>.Ascending(save => save.Size)
|
||||
: SortExpressionComparer<SaveModel>.Descending(save => save.Size);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool Filter(object arg)
|
||||
{
|
||||
if (arg is SaveModel save)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(_search) || save.Title.ToLower().Contains(_search.ToLower());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
42
Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml
Normal file
42
Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml
Normal file
|
@ -0,0 +1,42 @@
|
|||
<Window
|
||||
x:Class="Ryujinx.Ava.UI.Controls.UpdateWaitWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="Ryujinx - Waiting"
|
||||
SizeToContent="WidthAndHeight"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<Grid
|
||||
Margin="20"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Grid.Row="1"
|
||||
Height="70"
|
||||
MinWidth="50"
|
||||
Margin="5,10,20,10"
|
||||
Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" />
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical">
|
||||
<TextBlock Name="PrimaryText" Margin="5" />
|
||||
<TextBlock
|
||||
Name="SecondaryText"
|
||||
Margin="5"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
20
Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml.cs
Normal file
20
Ryujinx.Ava/UI/Controls/UpdateWaitWindow.axaml.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using Avalonia.Controls;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class UpdateWaitWindow : StyleableWindow
|
||||
{
|
||||
public UpdateWaitWindow(string primaryText, string secondaryText) : this()
|
||||
{
|
||||
PrimaryText.Text = primaryText;
|
||||
SecondaryText.Text = secondaryText;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
}
|
||||
|
||||
public UpdateWaitWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
86
Ryujinx.Ava/UI/Controls/UserEditor.axaml
Normal file
86
Ryujinx.Ava/UI/Controls/UserEditor.axaml
Normal file
|
@ -0,0 +1,86 @@
|
|||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Controls.UserEditor"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
Margin="0"
|
||||
MinWidth="500"
|
||||
Padding="0"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<UserControl.Resources>
|
||||
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||
</UserControl.Resources>
|
||||
<Grid Margin="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<Image
|
||||
Name="ProfileImage"
|
||||
Width="96"
|
||||
Height="96"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
||||
<Button
|
||||
Name="ChangePictureButton"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="ChangePictureButton_Click"
|
||||
Content="{locale:Locale UserProfilesChangeProfileImage}" />
|
||||
<Button
|
||||
Name="AddPictureButton"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="ChangePictureButton_Click"
|
||||
Content="{locale:Locale UserProfilesSetProfileImage}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="5,10"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Text="{locale:Locale UserProfilesName}" />
|
||||
<TextBox
|
||||
Name="NameBox"
|
||||
Width="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
MaxLength="{Binding MaxProfileNameLength}"
|
||||
Text="{Binding Name}" />
|
||||
<TextBlock Name="IdText" Text="{locale:Locale UserProfilesUserId}" />
|
||||
<TextBlock Name="IdLabel" Text="{Binding UserId}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10">
|
||||
<Button
|
||||
Name="SaveButton"
|
||||
Click="SaveButton_Click"
|
||||
Content="{locale:Locale Save}" />
|
||||
<Button
|
||||
Name="CloseButton"
|
||||
HorizontalAlignment="Right"
|
||||
Click="CloseButton_Click"
|
||||
Content="{locale:Locale Discard}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
118
Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs
Normal file
118
Ryujinx.Ava/UI/Controls/UserEditor.axaml.cs
Normal file
|
@ -0,0 +1,118 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Interactivity;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class UserEditor : UserControl
|
||||
{
|
||||
private NavigationDialogHost _parent;
|
||||
private UserProfile _profile;
|
||||
private bool _isNewUser;
|
||||
|
||||
public TempProfile TempProfile { get; set; }
|
||||
public uint MaxProfileNameLength => 0x20;
|
||||
|
||||
public UserEditor()
|
||||
{
|
||||
InitializeComponent();
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||
{
|
||||
NavigatedTo(e);
|
||||
}, RoutingStrategies.Direct);
|
||||
}
|
||||
|
||||
private void NavigatedTo(NavigationEventArgs arg)
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
switch (arg.NavigationMode)
|
||||
{
|
||||
case NavigationMode.New:
|
||||
var args = ((NavigationDialogHost parent, UserProfile profile, bool isNewUser))arg.Parameter;
|
||||
_isNewUser = args.isNewUser;
|
||||
_profile = args.profile;
|
||||
TempProfile = new TempProfile(_profile);
|
||||
|
||||
_parent = args.parent;
|
||||
break;
|
||||
}
|
||||
|
||||
DataContext = TempProfile;
|
||||
|
||||
AddPictureButton.IsVisible = _isNewUser;
|
||||
IdLabel.IsVisible = _profile != null;
|
||||
IdText.IsVisible = _profile != null;
|
||||
ChangePictureButton.IsVisible = !_isNewUser;
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_parent?.GoBack();
|
||||
}
|
||||
|
||||
private async void SaveButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DataValidationErrors.ClearErrors(NameBox);
|
||||
bool isInvalid = false;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(TempProfile.Name))
|
||||
{
|
||||
DataValidationErrors.SetError(NameBox, new DataValidationException(LocaleManager.Instance["UserProfileEmptyNameError"]));
|
||||
|
||||
isInvalid = true;
|
||||
}
|
||||
|
||||
if (TempProfile.Image == null)
|
||||
{
|
||||
await ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance["UserProfileNoImageError"], "");
|
||||
|
||||
isInvalid = true;
|
||||
}
|
||||
|
||||
if(isInvalid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_profile != null && !_isNewUser)
|
||||
{
|
||||
_profile.Name = TempProfile.Name;
|
||||
_profile.Image = TempProfile.Image;
|
||||
_profile.UpdateState();
|
||||
_parent.AccountManager.SetUserName(_profile.UserId, _profile.Name);
|
||||
_parent.AccountManager.SetUserImage(_profile.UserId, _profile.Image);
|
||||
}
|
||||
else if (_isNewUser)
|
||||
{
|
||||
_parent.AccountManager.AddUser(TempProfile.Name, TempProfile.Image, TempProfile.UserId);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_parent?.GoBack();
|
||||
}
|
||||
|
||||
public void SelectProfileImage()
|
||||
{
|
||||
_parent.Navigate(typeof(ProfileImageSelectionDialog), (_parent, TempProfile));
|
||||
}
|
||||
|
||||
private void ChangePictureButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_profile != null || _isNewUser)
|
||||
{
|
||||
SelectProfileImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
72
Ryujinx.Ava/UI/Controls/UserRecoverer.axaml
Normal file
72
Ryujinx.Ava/UI/Controls/UserRecoverer.axaml
Normal file
|
@ -0,0 +1,72 @@
|
|||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="800"
|
||||
d:DesignHeight="450"
|
||||
MinWidth="500"
|
||||
MinHeight="400"
|
||||
x:Class="Ryujinx.Ava.UI.Controls.UserRecoverer"
|
||||
Focusable="True">
|
||||
<Design.DataContext>
|
||||
<viewModels:UserProfileViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Button Grid.Row="0"
|
||||
Margin="5"
|
||||
Height="30"
|
||||
Width="50"
|
||||
MinWidth="50"
|
||||
HorizontalAlignment="Left"
|
||||
Command="{Binding GoBack}">
|
||||
<ui:SymbolIcon Symbol="Back"/>
|
||||
</Button>
|
||||
<TextBlock Grid.Row="1"
|
||||
Text="{locale:Locale UserProfilesRecoverHeading}"/>
|
||||
<ListBox
|
||||
Margin="5"
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Items="{Binding LostProfiles}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border
|
||||
Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="5">
|
||||
<Grid Margin="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding UserId}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="Wrap" />
|
||||
<Button Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{Binding Recover}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="{locale:Locale Recover}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</UserControl>
|
44
Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs
Normal file
44
Ryujinx.Ava/UI/Controls/UserRecoverer.axaml.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class UserRecoverer : UserControl
|
||||
{
|
||||
private UserProfileViewModel _viewModel;
|
||||
private NavigationDialogHost _parent;
|
||||
|
||||
public UserRecoverer()
|
||||
{
|
||||
InitializeComponent();
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||
{
|
||||
NavigatedTo(e);
|
||||
}, RoutingStrategies.Direct);
|
||||
}
|
||||
|
||||
private void NavigatedTo(NavigationEventArgs arg)
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
switch (arg.NavigationMode)
|
||||
{
|
||||
case NavigationMode.New:
|
||||
var args = ((NavigationDialogHost parent, UserProfileViewModel viewModel))arg.Parameter;
|
||||
|
||||
_viewModel = args.viewModel;
|
||||
_parent = args.parent;
|
||||
break;
|
||||
}
|
||||
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
145
Ryujinx.Ava/UI/Controls/UserSelector.axaml
Normal file
145
Ryujinx.Ava/UI/Controls/UserSelector.axaml
Normal file
|
@ -0,0 +1,145 @@
|
|||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Controls.UserSelector"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
d:DesignHeight="450"
|
||||
MinWidth="500"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<UserControl.Resources>
|
||||
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||
</UserControl.Resources>
|
||||
<Design.DataContext>
|
||||
<viewModels:UserProfileViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListBox
|
||||
Margin="5"
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
DoubleTapped="ProfilesList_DoubleTapped"
|
||||
Items="{Binding Profiles}"
|
||||
SelectionChanged="SelectingItemsControl_SelectionChanged">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<flex:FlexPanel
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
AlignContent="FlexStart"
|
||||
JustifyContent="Center" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Border
|
||||
Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="5">
|
||||
<Grid Margin="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image
|
||||
Grid.Row="0"
|
||||
Width="96"
|
||||
Height="96"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Height="30"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding Name}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border
|
||||
Width="10"
|
||||
Height="10"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Background="LimeGreen"
|
||||
CornerRadius="5"
|
||||
IsVisible="{Binding IsOpened}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="2"
|
||||
Command="{Binding AddUser}"
|
||||
Content="{locale:Locale UserProfilesAddNewProfile}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Grid.Row="0"
|
||||
Margin="2"
|
||||
Grid.Column="1"
|
||||
Command="{Binding EditUser}"
|
||||
Content="{locale:Locale UserProfilesEditProfile}"
|
||||
IsEnabled="{Binding IsSelectedProfiledEditable}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="2"
|
||||
Content="{locale:Locale UserProfilesManageSaves}"
|
||||
Command="{Binding ManageSaves}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="2"
|
||||
Command="{Binding DeleteUser}"
|
||||
Content="{locale:Locale UserProfilesDeleteSelectedProfile}"
|
||||
IsEnabled="{Binding IsSelectedProfileDeletable}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
Grid.Column="0"
|
||||
Margin="2"
|
||||
Command="{Binding RecoverLostAccounts}"
|
||||
Content="{locale:Locale UserProfilesRecoverLostAccounts}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
77
Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs
Normal file
77
Ryujinx.Ava/UI/Controls/UserSelector.axaml.cs
Normal file
|
@ -0,0 +1,77 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
public partial class UserSelector : UserControl
|
||||
{
|
||||
private NavigationDialogHost _parent;
|
||||
public UserProfileViewModel ViewModel { get; set; }
|
||||
|
||||
public UserSelector()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||
{
|
||||
NavigatedTo(e);
|
||||
}, RoutingStrategies.Direct);
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigatedTo(NavigationEventArgs arg)
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
if (arg.NavigationMode == NavigationMode.New)
|
||||
{
|
||||
_parent = (NavigationDialogHost)arg.Parameter;
|
||||
ViewModel = _parent.ViewModel;
|
||||
}
|
||||
|
||||
DataContext = ViewModel;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProfilesList_DoubleTapped(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ListBox listBox)
|
||||
{
|
||||
int selectedIndex = listBox.SelectedIndex;
|
||||
|
||||
if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
|
||||
{
|
||||
ViewModel.SelectedProfile = ViewModel.Profiles[selectedIndex];
|
||||
|
||||
_parent?.AccountManager?.OpenUser(ViewModel.SelectedProfile.UserId);
|
||||
|
||||
ViewModel.LoadProfiles();
|
||||
|
||||
foreach (UserProfile profile in ViewModel.Profiles)
|
||||
{
|
||||
profile.UpdateState();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectingItemsControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (sender is ListBox listBox)
|
||||
{
|
||||
int selectedIndex = listBox.SelectedIndex;
|
||||
|
||||
if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
|
||||
{
|
||||
ViewModel.HighlightedProfile = ViewModel.Profiles[selectedIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs
Normal file
16
Ryujinx.Ava/UI/Helpers/ApplicationOpenedEventArgs.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using Avalonia.Interactivity;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public class ApplicationOpenedEventArgs : RoutedEventArgs
|
||||
{
|
||||
public ApplicationData Application { get; }
|
||||
|
||||
public ApplicationOpenedEventArgs(ApplicationData application, RoutedEvent routedEvent)
|
||||
{
|
||||
Application = application;
|
||||
RoutedEvent = routedEvent;
|
||||
}
|
||||
}
|
||||
}
|
16
Ryujinx.Ava/UI/Helpers/AvaloniaGlxContext.cs
Normal file
16
Ryujinx.Ava/UI/Helpers/AvaloniaGlxContext.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using SPB.Graphics;
|
||||
using System;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
[SupportedOSPlatform("linux")]
|
||||
internal class AvaloniaGlxContext : SPB.Platform.GLX.GLXOpenGLContext
|
||||
{
|
||||
public AvaloniaGlxContext(IntPtr handle)
|
||||
: base(FramebufferFormat.Default, 0, 0, 0, false, null)
|
||||
{
|
||||
ContextHandle = handle;
|
||||
}
|
||||
}
|
||||
}
|
16
Ryujinx.Ava/UI/Helpers/AvaloniaWglContext.cs
Normal file
16
Ryujinx.Ava/UI/Helpers/AvaloniaWglContext.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using SPB.Graphics;
|
||||
using System;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal class AvaloniaWglContext : SPB.Platform.WGL.WGLOpenGLContext
|
||||
{
|
||||
public AvaloniaWglContext(IntPtr handle)
|
||||
: base(FramebufferFormat.Default, 0, 0, 0, false, null)
|
||||
{
|
||||
ContextHandle = handle;
|
||||
}
|
||||
}
|
||||
}
|
35
Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs
Normal file
35
Ryujinx.Ava/UI/Helpers/BitmapArrayValueConverter.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal class BitmapArrayValueConverter : IValueConverter
|
||||
{
|
||||
public static BitmapArrayValueConverter Instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value is byte[] buffer && targetType == typeof(IImage))
|
||||
{
|
||||
MemoryStream mem = new(buffer);
|
||||
return new Bitmap(mem);
|
||||
}
|
||||
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
118
Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs
Normal file
118
Ryujinx.Ava/UI/Helpers/ButtonKeyAssigner.cs
Normal file
|
@ -0,0 +1,118 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Input.Assigner;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal class ButtonKeyAssigner
|
||||
{
|
||||
internal class ButtonAssignedEventArgs : EventArgs
|
||||
{
|
||||
public ToggleButton Button { get; }
|
||||
public bool IsAssigned { get; }
|
||||
|
||||
public ButtonAssignedEventArgs(ToggleButton button, bool isAssigned)
|
||||
{
|
||||
Button = button;
|
||||
IsAssigned = isAssigned;
|
||||
}
|
||||
}
|
||||
|
||||
public ToggleButton ToggledButton { get; set; }
|
||||
|
||||
private bool _isWaitingForInput;
|
||||
private bool _shouldUnbind;
|
||||
public event EventHandler<ButtonAssignedEventArgs> ButtonAssigned;
|
||||
|
||||
public ButtonKeyAssigner(ToggleButton toggleButton)
|
||||
{
|
||||
ToggledButton = toggleButton;
|
||||
}
|
||||
|
||||
public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
ToggledButton.IsChecked = true;
|
||||
});
|
||||
|
||||
if (_isWaitingForInput)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
Cancel();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_isWaitingForInput = true;
|
||||
|
||||
assigner.Initialize();
|
||||
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (!_isWaitingForInput)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(10);
|
||||
|
||||
assigner.ReadInput();
|
||||
|
||||
if (assigner.HasAnyButtonPressed() || assigner.ShouldCancel() || (keyboard != null && keyboard.IsPressed(Key.Escape)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
string pressedButton = assigner.GetPressedButton();
|
||||
|
||||
if (_shouldUnbind)
|
||||
{
|
||||
SetButtonText(ToggledButton, "Unbound");
|
||||
}
|
||||
else if (pressedButton != "")
|
||||
{
|
||||
SetButtonText(ToggledButton, pressedButton);
|
||||
}
|
||||
|
||||
_shouldUnbind = false;
|
||||
_isWaitingForInput = false;
|
||||
|
||||
ToggledButton.IsChecked = false;
|
||||
|
||||
ButtonAssigned?.Invoke(this, new ButtonAssignedEventArgs(ToggledButton, pressedButton != null));
|
||||
|
||||
static void SetButtonText(ToggleButton button, string text)
|
||||
{
|
||||
ILogical textBlock = button.GetLogicalDescendants().First(x => x is TextBlock);
|
||||
|
||||
if (textBlock != null && textBlock is TextBlock block)
|
||||
{
|
||||
block.Text = text;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Cancel(bool shouldUnbind = false)
|
||||
{
|
||||
_isWaitingForInput = false;
|
||||
ToggledButton.IsChecked = false;
|
||||
_shouldUnbind = shouldUnbind;
|
||||
}
|
||||
}
|
||||
}
|
400
Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs
Normal file
400
Ryujinx.Ava/UI/Helpers/ContentDialogHelper.cs
Normal file
|
@ -0,0 +1,400 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.Core;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public static class ContentDialogHelper
|
||||
{
|
||||
private static bool _isChoiceDialogOpen;
|
||||
|
||||
private async static Task<UserResult> ShowContentDialog(
|
||||
string title,
|
||||
string primaryText,
|
||||
string secondaryText,
|
||||
string primaryButton,
|
||||
string secondaryButton,
|
||||
string closeButton,
|
||||
int iconSymbol,
|
||||
UserResult primaryButtonResult = UserResult.Ok,
|
||||
ManualResetEvent deferResetEvent = null,
|
||||
Func<Window, Task> doWhileDeferred = null,
|
||||
TypedEventHandler<ContentDialog, ContentDialogButtonClickEventArgs> deferCloseAction = null)
|
||||
{
|
||||
UserResult result = UserResult.None;
|
||||
|
||||
bool useOverlay = false;
|
||||
Window mainWindow = null;
|
||||
|
||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime al)
|
||||
{
|
||||
foreach (var item in al.Windows)
|
||||
{
|
||||
if (item.IsActive && item is MainWindow window && window.ViewModel.IsGameRunning)
|
||||
{
|
||||
mainWindow = window;
|
||||
useOverlay = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentDialog contentDialog = null;
|
||||
ContentDialogOverlayWindow overlay = null;
|
||||
|
||||
if (useOverlay)
|
||||
{
|
||||
overlay = new ContentDialogOverlayWindow()
|
||||
{
|
||||
Height = mainWindow.Bounds.Height,
|
||||
Width = mainWindow.Bounds.Width,
|
||||
Position = mainWindow.PointToScreen(new Point())
|
||||
};
|
||||
|
||||
mainWindow.PositionChanged += OverlayOnPositionChanged;
|
||||
|
||||
void OverlayOnPositionChanged(object sender, PixelPointEventArgs e)
|
||||
{
|
||||
overlay.Position = mainWindow.PointToScreen(new Point());
|
||||
}
|
||||
|
||||
contentDialog = overlay.ContentDialog;
|
||||
|
||||
bool opened = false;
|
||||
|
||||
overlay.Opened += OverlayOnActivated;
|
||||
|
||||
async void OverlayOnActivated(object sender, EventArgs e)
|
||||
{
|
||||
if (opened)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
|
||||
overlay.Position = mainWindow.PointToScreen(new Point());
|
||||
|
||||
await ShowDialog();
|
||||
}
|
||||
|
||||
await overlay.ShowDialog(mainWindow);
|
||||
}
|
||||
else
|
||||
{
|
||||
contentDialog = new ContentDialog();
|
||||
|
||||
await ShowDialog();
|
||||
}
|
||||
|
||||
async Task ShowDialog()
|
||||
{
|
||||
contentDialog.Title = title;
|
||||
contentDialog.PrimaryButtonText = primaryButton;
|
||||
contentDialog.SecondaryButtonText = secondaryButton;
|
||||
contentDialog.CloseButtonText = closeButton;
|
||||
contentDialog.Content = CreateDialogTextContent(primaryText, secondaryText, iconSymbol);
|
||||
|
||||
contentDialog.PrimaryButtonCommand = MiniCommand.Create(() =>
|
||||
{
|
||||
result = primaryButtonResult;
|
||||
});
|
||||
contentDialog.SecondaryButtonCommand = MiniCommand.Create(() =>
|
||||
{
|
||||
result = UserResult.No;
|
||||
contentDialog.PrimaryButtonClick -= deferCloseAction;
|
||||
});
|
||||
contentDialog.CloseButtonCommand = MiniCommand.Create(() =>
|
||||
{
|
||||
result = UserResult.Cancel;
|
||||
contentDialog.PrimaryButtonClick -= deferCloseAction;
|
||||
});
|
||||
|
||||
if (deferResetEvent != null)
|
||||
{
|
||||
contentDialog.PrimaryButtonClick += deferCloseAction;
|
||||
}
|
||||
|
||||
if (useOverlay)
|
||||
{
|
||||
await contentDialog.ShowAsync(overlay, ContentDialogPlacement.Popup);
|
||||
|
||||
overlay!.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
await contentDialog.ShowAsync(ContentDialogPlacement.Popup);
|
||||
}
|
||||
}
|
||||
|
||||
if (useOverlay)
|
||||
{
|
||||
overlay.Content = null;
|
||||
overlay.Close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async static Task<UserResult> ShowDeferredContentDialog(
|
||||
StyleableWindow window,
|
||||
string title,
|
||||
string primaryText,
|
||||
string secondaryText,
|
||||
string primaryButton,
|
||||
string secondaryButton,
|
||||
string closeButton,
|
||||
int iconSymbol,
|
||||
ManualResetEvent deferResetEvent,
|
||||
Func<Window, Task> doWhileDeferred = null)
|
||||
{
|
||||
bool startedDeferring = false;
|
||||
UserResult result = UserResult.None;
|
||||
|
||||
return await ShowContentDialog(
|
||||
title,
|
||||
primaryText,
|
||||
secondaryText,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
closeButton,
|
||||
iconSymbol,
|
||||
primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok,
|
||||
deferResetEvent,
|
||||
doWhileDeferred,
|
||||
DeferClose);
|
||||
|
||||
async void DeferClose(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
if (startedDeferring)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sender.PrimaryButtonClick -= DeferClose;
|
||||
|
||||
startedDeferring = true;
|
||||
|
||||
var deferral = args.GetDeferral();
|
||||
|
||||
result = primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok;
|
||||
|
||||
sender.PrimaryButtonClick -= DeferClose;
|
||||
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
Task.Run(() =>
|
||||
{
|
||||
deferResetEvent.WaitOne();
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
deferral.Complete();
|
||||
});
|
||||
});
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
|
||||
if (doWhileDeferred != null)
|
||||
{
|
||||
await doWhileDeferred(window);
|
||||
|
||||
deferResetEvent.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Grid CreateDialogTextContent(string primaryText, string secondaryText, int symbol)
|
||||
{
|
||||
Grid content = new Grid();
|
||||
content.RowDefinitions = new RowDefinitions() { new RowDefinition(), new RowDefinition() };
|
||||
content.ColumnDefinitions = new ColumnDefinitions() { new ColumnDefinition(GridLength.Auto), new ColumnDefinition() };
|
||||
|
||||
content.MinHeight = 80;
|
||||
|
||||
SymbolIcon icon = new SymbolIcon { Symbol = (Symbol)symbol, Margin = new Thickness(10) };
|
||||
icon.FontSize = 40;
|
||||
icon.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center;
|
||||
Grid.SetColumn(icon, 0);
|
||||
Grid.SetRowSpan(icon, 2);
|
||||
Grid.SetRow(icon, 0);
|
||||
|
||||
TextBlock primaryLabel = new TextBlock()
|
||||
{
|
||||
Text = primaryText,
|
||||
Margin = new Thickness(5),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MaxWidth = 450
|
||||
};
|
||||
TextBlock secondaryLabel = new TextBlock()
|
||||
{
|
||||
Text = secondaryText,
|
||||
Margin = new Thickness(5),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MaxWidth = 450
|
||||
};
|
||||
|
||||
Grid.SetColumn(primaryLabel, 1);
|
||||
Grid.SetColumn(secondaryLabel, 1);
|
||||
Grid.SetRow(primaryLabel, 0);
|
||||
Grid.SetRow(secondaryLabel, 1);
|
||||
|
||||
content.Children.Add(icon);
|
||||
content.Children.Add(primaryLabel);
|
||||
content.Children.Add(secondaryLabel);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
public static async Task<UserResult> CreateInfoDialog(
|
||||
string primary,
|
||||
string secondaryText,
|
||||
string acceptButton,
|
||||
string closeButton,
|
||||
string title)
|
||||
{
|
||||
return await ShowContentDialog(
|
||||
title,
|
||||
primary,
|
||||
secondaryText,
|
||||
acceptButton,
|
||||
"",
|
||||
closeButton,
|
||||
(int)Symbol.Important);
|
||||
}
|
||||
|
||||
internal static async Task<UserResult> CreateConfirmationDialog(
|
||||
string primaryText,
|
||||
string secondaryText,
|
||||
string acceptButtonText,
|
||||
string cancelButtonText,
|
||||
string title,
|
||||
UserResult primaryButtonResult = UserResult.Yes)
|
||||
{
|
||||
return await ShowContentDialog(
|
||||
string.IsNullOrWhiteSpace(title) ? LocaleManager.Instance["DialogConfirmationTitle"] : title,
|
||||
primaryText,
|
||||
secondaryText,
|
||||
acceptButtonText,
|
||||
"",
|
||||
cancelButtonText,
|
||||
(int)Symbol.Help,
|
||||
primaryButtonResult);
|
||||
}
|
||||
|
||||
internal static UpdateWaitWindow CreateWaitingDialog(string mainText, string secondaryText)
|
||||
{
|
||||
return new(mainText, secondaryText);
|
||||
}
|
||||
|
||||
internal static async Task CreateUpdaterInfoDialog(string primary, string secondaryText)
|
||||
{
|
||||
await ShowContentDialog(
|
||||
LocaleManager.Instance["DialogUpdaterTitle"],
|
||||
primary,
|
||||
secondaryText,
|
||||
"",
|
||||
"",
|
||||
LocaleManager.Instance["InputDialogOk"],
|
||||
(int)Symbol.Important);
|
||||
}
|
||||
|
||||
internal static async Task CreateWarningDialog(string primary, string secondaryText)
|
||||
{
|
||||
await ShowContentDialog(
|
||||
LocaleManager.Instance["DialogWarningTitle"],
|
||||
primary,
|
||||
secondaryText,
|
||||
"",
|
||||
"",
|
||||
LocaleManager.Instance["InputDialogOk"],
|
||||
(int)Symbol.Important);
|
||||
}
|
||||
|
||||
internal static async Task CreateErrorDialog(string errorMessage, string secondaryErrorMessage = "")
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, errorMessage);
|
||||
|
||||
await ShowContentDialog(
|
||||
LocaleManager.Instance["DialogErrorTitle"],
|
||||
LocaleManager.Instance["DialogErrorMessage"],
|
||||
errorMessage,
|
||||
secondaryErrorMessage,
|
||||
"",
|
||||
LocaleManager.Instance["InputDialogOk"],
|
||||
(int)Symbol.Dismiss);
|
||||
}
|
||||
|
||||
internal static async Task<bool> CreateChoiceDialog(string title, string primary, string secondaryText)
|
||||
{
|
||||
if (_isChoiceDialogOpen)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_isChoiceDialogOpen = true;
|
||||
|
||||
UserResult response =
|
||||
await ShowContentDialog(
|
||||
title,
|
||||
primary,
|
||||
secondaryText,
|
||||
LocaleManager.Instance["InputDialogYes"],
|
||||
"",
|
||||
LocaleManager.Instance["InputDialogNo"],
|
||||
(int)Symbol.Help,
|
||||
UserResult.Yes);
|
||||
|
||||
_isChoiceDialogOpen = false;
|
||||
|
||||
return response == UserResult.Yes;
|
||||
}
|
||||
|
||||
internal static async Task<bool> CreateExitDialog()
|
||||
{
|
||||
return await CreateChoiceDialog(
|
||||
LocaleManager.Instance["DialogExitTitle"],
|
||||
LocaleManager.Instance["DialogExitMessage"],
|
||||
LocaleManager.Instance["DialogExitSubMessage"]);
|
||||
}
|
||||
|
||||
internal static async Task<bool> CreateStopEmulationDialog()
|
||||
{
|
||||
return await CreateChoiceDialog(
|
||||
LocaleManager.Instance["DialogStopEmulationTitle"],
|
||||
LocaleManager.Instance["DialogStopEmulationMessage"],
|
||||
LocaleManager.Instance["DialogExitSubMessage"]);
|
||||
}
|
||||
|
||||
internal static async Task<string> CreateInputDialog(
|
||||
string title,
|
||||
string mainText,
|
||||
string subText,
|
||||
uint maxLength = int.MaxValue,
|
||||
string input = "")
|
||||
{
|
||||
var result = await InputDialog.ShowInputDialog(
|
||||
title,
|
||||
mainText,
|
||||
input,
|
||||
subText,
|
||||
maxLength);
|
||||
|
||||
if (result.Result == UserResult.Ok)
|
||||
{
|
||||
return result.Input;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
233
Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
Normal file
233
Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
Normal file
|
@ -0,0 +1,233 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Platform;
|
||||
using Ryujinx.Ava.UI.Helper;
|
||||
using SPB.Graphics;
|
||||
using SPB.Platform;
|
||||
using SPB.Platform.GLX;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public class EmbeddedWindow : NativeControlHost
|
||||
{
|
||||
private WindowProc _wndProcDelegate;
|
||||
private string _className;
|
||||
|
||||
protected GLXWindow X11Window { get; set; }
|
||||
protected IntPtr WindowHandle { get; set; }
|
||||
protected IntPtr X11Display { get; set; }
|
||||
protected IntPtr NsView { get; set; }
|
||||
protected IntPtr MetalLayer { get; set; }
|
||||
|
||||
private UpdateBoundsCallbackDelegate _updateBoundsCallback;
|
||||
|
||||
public event EventHandler<IntPtr> WindowCreated;
|
||||
public event EventHandler<Size> SizeChanged;
|
||||
|
||||
protected virtual void OnWindowDestroyed() { }
|
||||
protected virtual void OnWindowDestroying()
|
||||
{
|
||||
WindowHandle = IntPtr.Zero;
|
||||
X11Display = IntPtr.Zero;
|
||||
}
|
||||
|
||||
public EmbeddedWindow()
|
||||
{
|
||||
var stateObserverable = this.GetObservable(BoundsProperty);
|
||||
|
||||
stateObserverable.Subscribe(StateChanged);
|
||||
|
||||
this.Initialized += NativeEmbeddedWindow_Initialized;
|
||||
}
|
||||
|
||||
public virtual void OnWindowCreated() { }
|
||||
|
||||
private void NativeEmbeddedWindow_Initialized(object sender, EventArgs e)
|
||||
{
|
||||
OnWindowCreated();
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
WindowCreated?.Invoke(this, WindowHandle);
|
||||
});
|
||||
}
|
||||
|
||||
private void StateChanged(Rect rect)
|
||||
{
|
||||
SizeChanged?.Invoke(this, rect.Size);
|
||||
_updateBoundsCallback?.Invoke(rect);
|
||||
}
|
||||
|
||||
protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle parent)
|
||||
{
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
return CreateLinux(parent);
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return CreateWin32(parent);
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
return CreateMacOs(parent);
|
||||
}
|
||||
|
||||
return base.CreateNativeControlCore(parent);
|
||||
}
|
||||
|
||||
protected override void DestroyNativeControlCore(IPlatformHandle control)
|
||||
{
|
||||
OnWindowDestroying();
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
DestroyLinux();
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
DestroyWin32(control);
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
DestroyMacOS();
|
||||
}
|
||||
else
|
||||
{
|
||||
base.DestroyNativeControlCore(control);
|
||||
}
|
||||
|
||||
OnWindowDestroyed();
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
protected virtual IPlatformHandle CreateLinux(IPlatformHandle parent)
|
||||
{
|
||||
X11Window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100) as GLXWindow;
|
||||
WindowHandle = X11Window.WindowHandle.RawHandle;
|
||||
X11Display = X11Window.DisplayHandle.RawHandle;
|
||||
|
||||
return new PlatformHandle(WindowHandle, "X11");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
IPlatformHandle CreateWin32(IPlatformHandle parent)
|
||||
{
|
||||
_className = "NativeWindow-" + Guid.NewGuid();
|
||||
_wndProcDelegate = WndProc;
|
||||
var wndClassEx = new WNDCLASSEX
|
||||
{
|
||||
cbSize = Marshal.SizeOf<WNDCLASSEX>(),
|
||||
hInstance = GetModuleHandle(null),
|
||||
lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate),
|
||||
style = ClassStyles.CS_OWNDC,
|
||||
lpszClassName = Marshal.StringToHGlobalUni(_className),
|
||||
hCursor = LoadCursor(IntPtr.Zero, (IntPtr)Cursors.IDC_ARROW)
|
||||
};
|
||||
|
||||
var atom = RegisterClassEx(ref wndClassEx);
|
||||
|
||||
var handle = CreateWindowEx(
|
||||
0,
|
||||
_className,
|
||||
"NativeWindow",
|
||||
WindowStyles.WS_CHILD,
|
||||
0,
|
||||
0,
|
||||
640,
|
||||
480,
|
||||
parent.Handle,
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero);
|
||||
|
||||
WindowHandle = handle;
|
||||
|
||||
Marshal.FreeHGlobal(wndClassEx.lpszClassName);
|
||||
|
||||
return new PlatformHandle(WindowHandle, "HWND");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
IntPtr WndProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam)
|
||||
{
|
||||
var point = new Point((long)lParam & 0xFFFF, ((long)lParam >> 16) & 0xFFFF);
|
||||
var root = VisualRoot as Window;
|
||||
bool isLeft = false;
|
||||
switch (msg)
|
||||
{
|
||||
case WindowsMessages.LBUTTONDOWN:
|
||||
case WindowsMessages.RBUTTONDOWN:
|
||||
isLeft = msg == WindowsMessages.LBUTTONDOWN;
|
||||
this.RaiseEvent(new PointerPressedEventArgs(
|
||||
this,
|
||||
new Pointer(0, PointerType.Mouse, true),
|
||||
root,
|
||||
this.TranslatePoint(point, root).Value,
|
||||
(ulong)Environment.TickCount64,
|
||||
new PointerPointProperties(isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton, isLeft ? PointerUpdateKind.LeftButtonPressed : PointerUpdateKind.RightButtonPressed),
|
||||
KeyModifiers.None));
|
||||
break;
|
||||
case WindowsMessages.LBUTTONUP:
|
||||
case WindowsMessages.RBUTTONUP:
|
||||
isLeft = msg == WindowsMessages.LBUTTONUP;
|
||||
this.RaiseEvent(new PointerReleasedEventArgs(
|
||||
this,
|
||||
new Pointer(0, PointerType.Mouse, true),
|
||||
root,
|
||||
this.TranslatePoint(point, root).Value,
|
||||
(ulong)Environment.TickCount64,
|
||||
new PointerPointProperties(isLeft ? RawInputModifiers.LeftMouseButton : RawInputModifiers.RightMouseButton, isLeft ? PointerUpdateKind.LeftButtonReleased : PointerUpdateKind.RightButtonReleased),
|
||||
KeyModifiers.None,
|
||||
isLeft ? MouseButton.Left : MouseButton.Right));
|
||||
break;
|
||||
case WindowsMessages.MOUSEMOVE:
|
||||
this.RaiseEvent(new PointerEventArgs(
|
||||
PointerMovedEvent,
|
||||
this,
|
||||
new Pointer(0, PointerType.Mouse, true),
|
||||
root,
|
||||
this.TranslatePoint(point, root).Value,
|
||||
(ulong)Environment.TickCount64,
|
||||
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.Other),
|
||||
KeyModifiers.None));
|
||||
break;
|
||||
}
|
||||
return DefWindowProc(hWnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("macos")]
|
||||
IPlatformHandle CreateMacOs(IPlatformHandle parent)
|
||||
{
|
||||
MetalLayer = MetalHelper.GetMetalLayer(out IntPtr nsView, out _updateBoundsCallback);
|
||||
|
||||
NsView = nsView;
|
||||
|
||||
return new PlatformHandle(nsView, "NSView");
|
||||
}
|
||||
|
||||
void DestroyLinux()
|
||||
{
|
||||
X11Window?.Dispose();
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
void DestroyWin32(IPlatformHandle handle)
|
||||
{
|
||||
DestroyWindow(handle.Handle);
|
||||
UnregisterClass(_className, GetModuleHandle(null));
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("macos")]
|
||||
void DestroyMacOS()
|
||||
{
|
||||
MetalHelper.DestroyMetalLayer(NsView, MetalLayer);
|
||||
}
|
||||
}
|
||||
}
|
9
Ryujinx.Ava/UI/Helpers/Glyph.cs
Normal file
9
Ryujinx.Ava/UI/Helpers/Glyph.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public enum Glyph
|
||||
{
|
||||
List,
|
||||
Grid,
|
||||
Chip
|
||||
}
|
||||
}
|
49
Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs
Normal file
49
Ryujinx.Ava/UI/Helpers/GlyphValueConverter.cs
Normal file
|
@ -0,0 +1,49 @@
|
|||
using Avalonia.Data;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public class GlyphValueConverter : MarkupExtension
|
||||
{
|
||||
private string _key;
|
||||
|
||||
private static Dictionary<Glyph, string> _glyphs = new Dictionary<Glyph, string>
|
||||
{
|
||||
{ Glyph.List, char.ConvertFromUtf32((int)Symbol.List).ToString() },
|
||||
{ Glyph.Grid, char.ConvertFromUtf32((int)Symbol.ViewAll).ToString() },
|
||||
{ Glyph.Chip, char.ConvertFromUtf32(59748).ToString() }
|
||||
};
|
||||
|
||||
public GlyphValueConverter(string key)
|
||||
{
|
||||
_key = key;
|
||||
}
|
||||
|
||||
public string this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_glyphs.TryGetValue(Enum.Parse<Glyph>(key), out var val))
|
||||
{
|
||||
return val;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension binding = new($"[{_key}]")
|
||||
{
|
||||
Mode = BindingMode.OneWay,
|
||||
Source = this
|
||||
};
|
||||
|
||||
return binding.ProvideValue(serviceProvider);
|
||||
}
|
||||
}
|
||||
}
|
52
Ryujinx.Ava/UI/Helpers/HotKeyControl.cs
Normal file
52
Ryujinx.Ava/UI/Helpers/HotKeyControl.cs
Normal file
|
@ -0,0 +1,52 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public class HotKeyControl : ContentControl, ICommandSource
|
||||
{
|
||||
public static readonly StyledProperty<object> CommandParameterProperty =
|
||||
AvaloniaProperty.Register<HotKeyControl, object>(nameof(CommandParameter));
|
||||
|
||||
public static readonly DirectProperty<HotKeyControl, ICommand> CommandProperty =
|
||||
AvaloniaProperty.RegisterDirect<HotKeyControl, ICommand>(nameof(Command),
|
||||
control => control.Command, (control, command) => control.Command = command, enableDataValidation: true);
|
||||
|
||||
public static readonly StyledProperty<KeyGesture> HotKeyProperty = HotKeyManager.HotKeyProperty.AddOwner<Button>();
|
||||
|
||||
private ICommand _command;
|
||||
private bool _commandCanExecute;
|
||||
|
||||
public ICommand Command
|
||||
{
|
||||
get { return _command; }
|
||||
set { SetAndRaise(CommandProperty, ref _command, value); }
|
||||
}
|
||||
|
||||
public KeyGesture HotKey
|
||||
{
|
||||
get { return GetValue(HotKeyProperty); }
|
||||
set { SetValue(HotKeyProperty, value); }
|
||||
}
|
||||
|
||||
public object CommandParameter
|
||||
{
|
||||
get { return GetValue(CommandParameterProperty); }
|
||||
set { SetValue(CommandParameterProperty, value); }
|
||||
}
|
||||
|
||||
public void CanExecuteChanged(object sender, EventArgs e)
|
||||
{
|
||||
var canExecute = Command == null || Command.CanExecute(CommandParameter);
|
||||
|
||||
if (canExecute != _commandCanExecute)
|
||||
{
|
||||
_commandCanExecute = canExecute;
|
||||
UpdateIsEffectivelyEnabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
Ryujinx.Ava/UI/Helpers/IGlContextExtension.cs
Normal file
25
Ryujinx.Ava/UI/Helpers/IGlContextExtension.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using Avalonia.OpenGL;
|
||||
using SPB.Graphics.OpenGL;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal static class IGlContextExtension
|
||||
{
|
||||
public static OpenGLContextBase AsOpenGLContextBase(this IGlContext context)
|
||||
{
|
||||
var handle = (IntPtr)context.GetType().GetProperty("Handle").GetValue(context);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return new AvaloniaWglContext(handle);
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
return new AvaloniaGlxContext(handle);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
46
Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs
Normal file
46
Ryujinx.Ava/UI/Helpers/KeyValueConverter.cs
Normal file
|
@ -0,0 +1,46 @@
|
|||
using Avalonia.Data.Converters;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal class KeyValueConverter : IValueConverter
|
||||
{
|
||||
public static KeyValueConverter Instance = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
object key = null;
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
if (targetType == typeof(Key))
|
||||
{
|
||||
key = Enum.Parse<Key>(value.ToString());
|
||||
}
|
||||
else if (targetType == typeof(GamepadInputId))
|
||||
{
|
||||
key = Enum.Parse<GamepadInputId>(value.ToString());
|
||||
}
|
||||
else if (targetType == typeof(StickInputId))
|
||||
{
|
||||
key = Enum.Parse<StickInputId>(value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
71
Ryujinx.Ava/UI/Helpers/MiniCommand.cs
Normal file
71
Ryujinx.Ava/UI/Helpers/MiniCommand.cs
Normal file
|
@ -0,0 +1,71 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public sealed class MiniCommand<T> : MiniCommand, ICommand
|
||||
{
|
||||
private readonly Action<T> _callback;
|
||||
private bool _busy;
|
||||
private Func<T, Task> _asyncCallback;
|
||||
|
||||
public MiniCommand(Action<T> callback)
|
||||
{
|
||||
_callback = callback;
|
||||
}
|
||||
|
||||
public MiniCommand(Func<T, Task> callback)
|
||||
{
|
||||
_asyncCallback = callback;
|
||||
}
|
||||
|
||||
private bool Busy
|
||||
{
|
||||
get => _busy;
|
||||
set
|
||||
{
|
||||
_busy = value;
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public override event EventHandler CanExecuteChanged;
|
||||
public override bool CanExecute(object parameter) => !_busy;
|
||||
|
||||
public override async void Execute(object parameter)
|
||||
{
|
||||
if (Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
Busy = true;
|
||||
if (_callback != null)
|
||||
{
|
||||
_callback((T)parameter);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _asyncCallback((T)parameter);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Busy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class MiniCommand : ICommand
|
||||
{
|
||||
public static MiniCommand Create(Action callback) => new MiniCommand<object>(_ => callback());
|
||||
public static MiniCommand Create<TArg>(Action<TArg> callback) => new MiniCommand<TArg>(callback);
|
||||
public static MiniCommand CreateFromTask(Func<Task> callback) => new MiniCommand<object>(_ => callback());
|
||||
|
||||
public abstract bool CanExecute(object parameter);
|
||||
public abstract void Execute(object parameter);
|
||||
public abstract event EventHandler CanExecuteChanged;
|
||||
}
|
||||
}
|
40
Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs
Normal file
40
Ryujinx.Ava/UI/Helpers/OffscreenTextBox.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public class OffscreenTextBox : TextBox
|
||||
{
|
||||
public RoutedEvent<KeyEventArgs> GetKeyDownRoutedEvent()
|
||||
{
|
||||
return KeyDownEvent;
|
||||
}
|
||||
|
||||
public RoutedEvent<KeyEventArgs> GetKeyUpRoutedEvent()
|
||||
{
|
||||
return KeyUpEvent;
|
||||
}
|
||||
|
||||
public void SendKeyDownEvent(KeyEventArgs keyEvent)
|
||||
{
|
||||
OnKeyDown(keyEvent);
|
||||
}
|
||||
|
||||
public void SendKeyUpEvent(KeyEventArgs keyEvent)
|
||||
{
|
||||
OnKeyUp(keyEvent);
|
||||
}
|
||||
|
||||
public void SendText(string text)
|
||||
{
|
||||
OnTextInput(new TextInputEventArgs()
|
||||
{
|
||||
Text = text,
|
||||
Device = KeyboardDevice.Instance,
|
||||
Source = this,
|
||||
RoutedEvent = TextInputEvent
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
82
Ryujinx.Ava/UI/Helpers/OpenGLEmbeddedWindow.cs
Normal file
82
Ryujinx.Ava/UI/Helpers/OpenGLEmbeddedWindow.cs
Normal file
|
@ -0,0 +1,82 @@
|
|||
using OpenTK.Graphics.OpenGL;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using SPB.Graphics;
|
||||
using SPB.Graphics.OpenGL;
|
||||
using SPB.Platform;
|
||||
using SPB.Platform.WGL;
|
||||
using SPB.Windowing;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public class OpenGLEmbeddedWindow : EmbeddedWindow
|
||||
{
|
||||
private readonly int _major;
|
||||
private readonly int _minor;
|
||||
private readonly GraphicsDebugLevel _graphicsDebugLevel;
|
||||
private SwappableNativeWindowBase _window;
|
||||
public OpenGLContextBase Context { get; set; }
|
||||
|
||||
public OpenGLEmbeddedWindow(int major, int minor, GraphicsDebugLevel graphicsDebugLevel)
|
||||
{
|
||||
_major = major;
|
||||
_minor = minor;
|
||||
_graphicsDebugLevel = graphicsDebugLevel;
|
||||
}
|
||||
|
||||
protected override void OnWindowDestroying()
|
||||
{
|
||||
Context.Dispose();
|
||||
base.OnWindowDestroying();
|
||||
}
|
||||
|
||||
public override void OnWindowCreated()
|
||||
{
|
||||
base.OnWindowCreated();
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_window = new WGLWindow(new NativeHandle(WindowHandle));
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
_window = X11Window;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
var flags = OpenGLContextFlags.Compat;
|
||||
if (_graphicsDebugLevel != GraphicsDebugLevel.None)
|
||||
{
|
||||
flags |= OpenGLContextFlags.Debug;
|
||||
}
|
||||
|
||||
Context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, _major, _minor, flags);
|
||||
|
||||
Context.Initialize(_window);
|
||||
Context.MakeCurrent(_window);
|
||||
|
||||
var bindingsContext = new OpenToolkitBindingsContext(Context.GetProcAddress);
|
||||
|
||||
GL.LoadBindings(bindingsContext);
|
||||
Context.MakeCurrent(null);
|
||||
}
|
||||
|
||||
public void MakeCurrent()
|
||||
{
|
||||
Context?.MakeCurrent(_window);
|
||||
}
|
||||
|
||||
public void MakeCurrent(NativeWindowBase window)
|
||||
{
|
||||
Context?.MakeCurrent(window);
|
||||
}
|
||||
|
||||
public void SwapBuffers()
|
||||
{
|
||||
_window.SwapBuffers();
|
||||
}
|
||||
}
|
||||
}
|
20
Ryujinx.Ava/UI/Helpers/OpenToolkitBindingsContext.cs
Normal file
20
Ryujinx.Ava/UI/Helpers/OpenToolkitBindingsContext.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using OpenTK;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal class OpenToolkitBindingsContext : IBindingsContext
|
||||
{
|
||||
private readonly Func<string, IntPtr> _getProcAddress;
|
||||
|
||||
public OpenToolkitBindingsContext(Func<string, IntPtr> getProcAddress)
|
||||
{
|
||||
_getProcAddress = getProcAddress;
|
||||
}
|
||||
|
||||
public IntPtr GetProcAddress(string procName)
|
||||
{
|
||||
return _getProcAddress(procName);
|
||||
}
|
||||
}
|
||||
}
|
47
Ryujinx.Ava/UI/Helpers/SPBOpenGLContext.cs
Normal file
47
Ryujinx.Ava/UI/Helpers/SPBOpenGLContext.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
using OpenTK.Graphics.OpenGL;
|
||||
using Ryujinx.Graphics.OpenGL;
|
||||
using SPB.Graphics;
|
||||
using SPB.Graphics.OpenGL;
|
||||
using SPB.Platform;
|
||||
using SPB.Windowing;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
class SPBOpenGLContext : IOpenGLContext
|
||||
{
|
||||
private OpenGLContextBase _context;
|
||||
private NativeWindowBase _window;
|
||||
|
||||
private SPBOpenGLContext(OpenGLContextBase context, NativeWindowBase window)
|
||||
{
|
||||
_context = context;
|
||||
_window = window;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Dispose();
|
||||
_window.Dispose();
|
||||
}
|
||||
|
||||
public void MakeCurrent()
|
||||
{
|
||||
_context.MakeCurrent(_window);
|
||||
}
|
||||
|
||||
public static SPBOpenGLContext CreateBackgroundContext(OpenGLContextBase sharedContext)
|
||||
{
|
||||
OpenGLContextBase context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, 3, 3, OpenGLContextFlags.Compat, true, sharedContext);
|
||||
NativeWindowBase window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
|
||||
|
||||
context.Initialize(window);
|
||||
context.MakeCurrent(window);
|
||||
|
||||
GL.LoadBindings(new OpenToolkitBindingsContext(context.GetProcAddress));
|
||||
|
||||
context.MakeCurrent(null);
|
||||
|
||||
return new SPBOpenGLContext(context, window);
|
||||
}
|
||||
}
|
||||
}
|
91
Ryujinx.Ava/UI/Helpers/UserErrorDialog.cs
Normal file
91
Ryujinx.Ava/UI/Helpers/UserErrorDialog.cs
Normal file
|
@ -0,0 +1,91 @@
|
|||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Ui.Common;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
internal class UserErrorDialog
|
||||
{
|
||||
private const string SetupGuideUrl = "https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide";
|
||||
|
||||
private static string GetErrorCode(UserError error)
|
||||
{
|
||||
return $"RYU-{(uint)error:X4}";
|
||||
}
|
||||
|
||||
private static string GetErrorTitle(UserError error)
|
||||
{
|
||||
return error switch
|
||||
{
|
||||
UserError.NoKeys => LocaleManager.Instance["UserErrorNoKeys"],
|
||||
UserError.NoFirmware => LocaleManager.Instance["UserErrorNoFirmware"],
|
||||
UserError.FirmwareParsingFailed => LocaleManager.Instance["UserErrorFirmwareParsingFailed"],
|
||||
UserError.ApplicationNotFound => LocaleManager.Instance["UserErrorApplicationNotFound"],
|
||||
UserError.Unknown => LocaleManager.Instance["UserErrorUnknown"],
|
||||
_ => LocaleManager.Instance["UserErrorUndefined"]
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetErrorDescription(UserError error)
|
||||
{
|
||||
return error switch
|
||||
{
|
||||
UserError.NoKeys => LocaleManager.Instance["UserErrorNoKeysDescription"],
|
||||
UserError.NoFirmware => LocaleManager.Instance["UserErrorNoFirmwareDescription"],
|
||||
UserError.FirmwareParsingFailed => LocaleManager.Instance["UserErrorFirmwareParsingFailedDescription"],
|
||||
UserError.ApplicationNotFound => LocaleManager.Instance["UserErrorApplicationNotFoundDescription"],
|
||||
UserError.Unknown => LocaleManager.Instance["UserErrorUnknownDescription"],
|
||||
_ => LocaleManager.Instance["UserErrorUndefinedDescription"]
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsCoveredBySetupGuide(UserError error)
|
||||
{
|
||||
return error switch
|
||||
{
|
||||
UserError.NoKeys or
|
||||
UserError.NoFirmware or
|
||||
UserError.FirmwareParsingFailed => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetSetupGuideUrl(UserError error)
|
||||
{
|
||||
if (!IsCoveredBySetupGuide(error))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return error switch
|
||||
{
|
||||
UserError.NoKeys => SetupGuideUrl + "#initial-setup---placement-of-prodkeys",
|
||||
UserError.NoFirmware => SetupGuideUrl + "#initial-setup-continued---installation-of-firmware",
|
||||
_ => SetupGuideUrl
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task ShowUserErrorDialog(UserError error, StyleableWindow owner)
|
||||
{
|
||||
string errorCode = GetErrorCode(error);
|
||||
|
||||
bool isInSetupGuide = IsCoveredBySetupGuide(error);
|
||||
|
||||
string setupButtonLabel = isInSetupGuide ? LocaleManager.Instance["OpenSetupGuideMessage"] : "";
|
||||
|
||||
var result = await ContentDialogHelper.CreateInfoDialog(
|
||||
string.Format(LocaleManager.Instance["DialogUserErrorDialogMessage"], errorCode, GetErrorTitle(error)),
|
||||
GetErrorDescription(error) + (isInSetupGuide
|
||||
? LocaleManager.Instance["DialogUserErrorDialogInfoMessage"]
|
||||
: ""), setupButtonLabel, LocaleManager.Instance["InputDialogOk"],
|
||||
string.Format(LocaleManager.Instance["DialogUserErrorDialogTitle"], errorCode));
|
||||
|
||||
if (result == UserResult.Ok)
|
||||
{
|
||||
OpenHelper.OpenUrl(GetSetupGuideUrl(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
Ryujinx.Ava/UI/Helpers/UserResult.cs
Normal file
12
Ryujinx.Ava/UI/Helpers/UserResult.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public enum UserResult
|
||||
{
|
||||
Ok,
|
||||
Yes,
|
||||
No,
|
||||
Abort,
|
||||
Cancel,
|
||||
None,
|
||||
}
|
||||
}
|
52
Ryujinx.Ava/UI/Helpers/VulkanEmbeddedWindow.cs
Normal file
52
Ryujinx.Ava/UI/Helpers/VulkanEmbeddedWindow.cs
Normal file
|
@ -0,0 +1,52 @@
|
|||
using Avalonia.Platform;
|
||||
using Silk.NET.Vulkan;
|
||||
using SPB.Graphics.Vulkan;
|
||||
using SPB.Platform.GLX;
|
||||
using SPB.Platform.Metal;
|
||||
using SPB.Platform.Win32;
|
||||
using SPB.Platform.X11;
|
||||
using SPB.Windowing;
|
||||
using System;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public class VulkanEmbeddedWindow : EmbeddedWindow
|
||||
{
|
||||
private NativeWindowBase _window;
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
protected override IPlatformHandle CreateLinux(IPlatformHandle parent)
|
||||
{
|
||||
X11Window = new GLXWindow(new NativeHandle(X11.DefaultDisplay), new NativeHandle(parent.Handle));
|
||||
WindowHandle = X11Window.WindowHandle.RawHandle;
|
||||
X11Display = X11Window.DisplayHandle.RawHandle;
|
||||
|
||||
X11Window.Hide();
|
||||
|
||||
return new PlatformHandle(WindowHandle, "X11");
|
||||
}
|
||||
|
||||
public SurfaceKHR CreateSurface(Instance instance)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_window = new SimpleWin32Window(new NativeHandle(WindowHandle));
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
_window = new SimpleX11Window(new NativeHandle(X11Display), new NativeHandle(WindowHandle));
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
_window = new SimpleMetalWindow(new NativeHandle(NsView), new NativeHandle(MetalLayer));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
return new SurfaceKHR((ulong?)VulkanHelper.CreateWindowSurface(instance.Handle, _window));
|
||||
}
|
||||
}
|
||||
}
|
107
Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs
Normal file
107
Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs
Normal file
|
@ -0,0 +1,107 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal partial class Win32NativeInterop
|
||||
{
|
||||
[Flags]
|
||||
public enum ClassStyles : uint
|
||||
{
|
||||
CS_CLASSDC = 0x40,
|
||||
CS_OWNDC = 0x20,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum WindowStyles : uint
|
||||
{
|
||||
WS_CHILD = 0x40000000
|
||||
}
|
||||
|
||||
public enum Cursors : uint
|
||||
{
|
||||
IDC_ARROW = 32512
|
||||
}
|
||||
|
||||
public enum WindowsMessages : uint
|
||||
{
|
||||
MOUSEMOVE = 0x0200,
|
||||
LBUTTONDOWN = 0x0201,
|
||||
LBUTTONUP = 0x0202,
|
||||
LBUTTONDBLCLK = 0x0203,
|
||||
RBUTTONDOWN = 0x0204,
|
||||
RBUTTONUP = 0x0205,
|
||||
RBUTTONDBLCLK = 0x0206,
|
||||
MBUTTONDOWN = 0x0207,
|
||||
MBUTTONUP = 0x0208,
|
||||
MBUTTONDBLCLK = 0x0209,
|
||||
MOUSEWHEEL = 0x020A,
|
||||
XBUTTONDOWN = 0x020B,
|
||||
XBUTTONUP = 0x020C,
|
||||
XBUTTONDBLCLK = 0x020D,
|
||||
MOUSEHWHEEL = 0x020E,
|
||||
MOUSELAST = 0x020E
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
internal delegate IntPtr WindowProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct WNDCLASSEX
|
||||
{
|
||||
public int cbSize;
|
||||
public ClassStyles style;
|
||||
public IntPtr lpfnWndProc; // not WndProc
|
||||
public int cbClsExtra;
|
||||
public int cbWndExtra;
|
||||
public IntPtr hInstance;
|
||||
public IntPtr hIcon;
|
||||
public IntPtr hCursor;
|
||||
public IntPtr hbrBackground;
|
||||
public IntPtr lpszMenuName;
|
||||
public IntPtr lpszClassName;
|
||||
public IntPtr hIconSm;
|
||||
|
||||
public WNDCLASSEX()
|
||||
{
|
||||
cbSize = Marshal.SizeOf<WNDCLASSEX>();
|
||||
}
|
||||
}
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")]
|
||||
public static partial ushort RegisterClassEx(ref WNDCLASSEX param);
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "UnregisterClassW")]
|
||||
public static partial short UnregisterClass([MarshalAs(UnmanagedType.LPWStr)] string lpClassName, IntPtr instance);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "DefWindowProcW")]
|
||||
public static partial IntPtr DefWindowProc(IntPtr hWnd, WindowsMessages msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "GetModuleHandleA")]
|
||||
public static partial IntPtr GetModuleHandle([MarshalAs(UnmanagedType.LPStr)] string lpModuleName);
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static partial bool DestroyWindow(IntPtr hwnd);
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "LoadCursorA")]
|
||||
public static partial IntPtr LoadCursor(IntPtr hInstance, IntPtr lpCursorName);
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "CreateWindowExW")]
|
||||
public static partial IntPtr CreateWindowEx(
|
||||
uint dwExStyle,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpClassName,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpWindowName,
|
||||
WindowStyles dwStyle,
|
||||
int x,
|
||||
int y,
|
||||
int nWidth,
|
||||
int nHeight,
|
||||
IntPtr hWndParent,
|
||||
IntPtr hMenu,
|
||||
IntPtr hInstance,
|
||||
IntPtr lpParam);
|
||||
}
|
||||
}
|
72
Ryujinx.Ava/UI/Models/Amiibo.cs
Normal file
72
Ryujinx.Ava/UI/Models/Amiibo.cs
Normal file
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
public class Amiibo
|
||||
{
|
||||
public struct AmiiboJson
|
||||
{
|
||||
[JsonPropertyName("amiibo")] public List<AmiiboApi> Amiibo { get; set; }
|
||||
[JsonPropertyName("lastUpdated")] public DateTime LastUpdated { get; set; }
|
||||
}
|
||||
|
||||
public struct AmiiboApi
|
||||
{
|
||||
[JsonPropertyName("name")] public string Name { get; set; }
|
||||
[JsonPropertyName("head")] public string Head { get; set; }
|
||||
[JsonPropertyName("tail")] public string Tail { get; set; }
|
||||
[JsonPropertyName("image")] public string Image { get; set; }
|
||||
[JsonPropertyName("amiiboSeries")] public string AmiiboSeries { get; set; }
|
||||
[JsonPropertyName("character")] public string Character { get; set; }
|
||||
[JsonPropertyName("gameSeries")] public string GameSeries { get; set; }
|
||||
[JsonPropertyName("type")] public string Type { get; set; }
|
||||
|
||||
[JsonPropertyName("release")] public Dictionary<string, string> Release { get; set; }
|
||||
|
||||
[JsonPropertyName("gamesSwitch")] public List<AmiiboApiGamesSwitch> GamesSwitch { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
public string GetId()
|
||||
{
|
||||
return Head + Tail;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is AmiiboApi amiibo)
|
||||
{
|
||||
return amiibo.Head + amiibo.Tail == Head + Tail;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
public class AmiiboApiGamesSwitch
|
||||
{
|
||||
[JsonPropertyName("amiiboUsage")] public List<AmiiboApiUsage> AmiiboUsage { get; set; }
|
||||
|
||||
[JsonPropertyName("gameID")] public List<string> GameId { get; set; }
|
||||
|
||||
[JsonPropertyName("gameName")] public string GameName { get; set; }
|
||||
}
|
||||
|
||||
public class AmiiboApiUsage
|
||||
{
|
||||
[JsonPropertyName("Usage")] public string Usage { get; set; }
|
||||
|
||||
[JsonPropertyName("write")] public bool Write { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
40
Ryujinx.Ava/UI/Models/CheatModel.cs
Normal file
40
Ryujinx.Ava/UI/Models/CheatModel.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
public class CheatModel : BaseModel
|
||||
{
|
||||
private bool _isEnabled;
|
||||
|
||||
public event EventHandler<bool> EnableToggled;
|
||||
|
||||
public CheatModel(string name, string buildId, bool isEnabled)
|
||||
{
|
||||
Name = name;
|
||||
BuildId = buildId;
|
||||
IsEnabled = isEnabled;
|
||||
}
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => _isEnabled;
|
||||
set
|
||||
{
|
||||
_isEnabled = value;
|
||||
|
||||
EnableToggled?.Invoke(this, _isEnabled);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string BuildId { get; }
|
||||
|
||||
public string BuildIdKey => $"{BuildId}-{Name}";
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string CleanName => Name.Substring(1, Name.Length - 8);
|
||||
}
|
||||
}
|
51
Ryujinx.Ava/UI/Models/CheatsList.cs
Normal file
51
Ryujinx.Ava/UI/Models/CheatsList.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
public class CheatsList : ObservableCollection<CheatModel>
|
||||
{
|
||||
public CheatsList(string buildId, string path)
|
||||
{
|
||||
BuildId = buildId;
|
||||
Path = path;
|
||||
|
||||
CollectionChanged += CheatsList_CollectionChanged;
|
||||
}
|
||||
|
||||
public string BuildId { get; }
|
||||
public string Path { get; }
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.ToList().TrueForAll(x => x.IsEnabled);
|
||||
}
|
||||
set
|
||||
{
|
||||
foreach (var cheat in this)
|
||||
{
|
||||
cheat.IsEnabled = value;
|
||||
}
|
||||
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
|
||||
}
|
||||
}
|
||||
|
||||
private void CheatsList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.Action == NotifyCollectionChangedAction.Add)
|
||||
{
|
||||
(e.NewItems[0] as CheatModel).EnableToggled += Item_EnableToggled;
|
||||
}
|
||||
}
|
||||
|
||||
private void Item_EnableToggled(object sender, bool e)
|
||||
{
|
||||
OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsEnabled)));
|
||||
}
|
||||
}
|
||||
}
|
6
Ryujinx.Ava/UI/Models/ControllerModel.cs
Normal file
6
Ryujinx.Ava/UI/Models/ControllerModel.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
using Ryujinx.Common.Configuration.Hid;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
internal record ControllerModel(ControllerType Type, string Name);
|
||||
}
|
9
Ryujinx.Ava/UI/Models/DeviceType.cs
Normal file
9
Ryujinx.Ava/UI/Models/DeviceType.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
public enum DeviceType
|
||||
{
|
||||
None,
|
||||
Keyboard,
|
||||
Controller
|
||||
}
|
||||
}
|
32
Ryujinx.Ava/UI/Models/DownloadableContentModel.cs
Normal file
32
Ryujinx.Ava/UI/Models/DownloadableContentModel.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using Ryujinx.Ava.UI.ViewModels;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
public class DownloadableContentModel : BaseModel
|
||||
{
|
||||
private bool _enabled;
|
||||
|
||||
public bool Enabled
|
||||
{
|
||||
get => _enabled;
|
||||
set
|
||||
{
|
||||
_enabled = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string TitleId { get; }
|
||||
public string ContainerPath { get; }
|
||||
public string FullPath { get; }
|
||||
|
||||
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
|
||||
{
|
||||
TitleId = titleId;
|
||||
ContainerPath = containerPath;
|
||||
FullPath = fullPath;
|
||||
Enabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
33
Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs
Normal file
33
Ryujinx.Ava/UI/Models/Generic/LastPlayedSortComparer.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models.Generic
|
||||
{
|
||||
internal class LastPlayedSortComparer : IComparer<ApplicationData>
|
||||
{
|
||||
public LastPlayedSortComparer() { }
|
||||
public LastPlayedSortComparer(bool isAscending) { IsAscending = isAscending; }
|
||||
|
||||
public bool IsAscending { get; }
|
||||
|
||||
public int Compare(ApplicationData x, ApplicationData y)
|
||||
{
|
||||
string aValue = x.LastPlayed;
|
||||
string bValue = y.LastPlayed;
|
||||
|
||||
if (aValue == LocaleManager.Instance["Never"])
|
||||
{
|
||||
aValue = DateTime.UnixEpoch.ToString();
|
||||
}
|
||||
|
||||
if (bValue == LocaleManager.Instance["Never"])
|
||||
{
|
||||
bValue = DateTime.UnixEpoch.ToString();
|
||||
}
|
||||
|
||||
return (IsAscending ? 1 : -1) * DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue));
|
||||
}
|
||||
}
|
||||
}
|
456
Ryujinx.Ava/UI/Models/InputConfiguration.cs
Normal file
456
Ryujinx.Ava/UI/Models/InputConfiguration.cs
Normal file
|
@ -0,0 +1,456 @@
|
|||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
internal class InputConfiguration<Key, Stick> : BaseModel
|
||||
{
|
||||
private float _deadzoneRight;
|
||||
private float _triggerThreshold;
|
||||
private float _deadzoneLeft;
|
||||
private double _gyroDeadzone;
|
||||
private int _sensitivity;
|
||||
private bool enableMotion;
|
||||
private float weakRumble;
|
||||
private float strongRumble;
|
||||
private float _rangeLeft;
|
||||
private float _rangeRight;
|
||||
|
||||
public InputBackendType Backend { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controller id
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controller's Type
|
||||
/// </summary>
|
||||
public ControllerType ControllerType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Player's Index for the controller
|
||||
/// </summary>
|
||||
public PlayerIndex PlayerIndex { get; set; }
|
||||
|
||||
public Stick LeftJoystick { get; set; }
|
||||
public bool LeftInvertStickX { get; set; }
|
||||
public bool LeftInvertStickY { get; set; }
|
||||
public bool RightRotate90 { get; set; }
|
||||
public Key LeftControllerStickButton { get; set; }
|
||||
|
||||
public Stick RightJoystick { get; set; }
|
||||
public bool RightInvertStickX { get; set; }
|
||||
public bool RightInvertStickY { get; set; }
|
||||
public bool LeftRotate90 { get; set; }
|
||||
public Key RightControllerStickButton { get; set; }
|
||||
|
||||
public float DeadzoneLeft
|
||||
{
|
||||
get => _deadzoneLeft;
|
||||
set
|
||||
{
|
||||
_deadzoneLeft = MathF.Round(value, 3);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public float RangeLeft
|
||||
{
|
||||
get => _rangeLeft;
|
||||
set
|
||||
{
|
||||
_rangeLeft = MathF.Round(value, 3);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public float DeadzoneRight
|
||||
{
|
||||
get => _deadzoneRight;
|
||||
set
|
||||
{
|
||||
_deadzoneRight = MathF.Round(value, 3);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public float RangeRight
|
||||
{
|
||||
get => _rangeRight;
|
||||
set
|
||||
{
|
||||
_rangeRight = MathF.Round(value, 3);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public float TriggerThreshold
|
||||
{
|
||||
get => _triggerThreshold;
|
||||
set
|
||||
{
|
||||
_triggerThreshold = MathF.Round(value, 3);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public MotionInputBackendType MotionBackend { get; set; }
|
||||
|
||||
public Key ButtonMinus { get; set; }
|
||||
public Key ButtonL { get; set; }
|
||||
public Key ButtonZl { get; set; }
|
||||
public Key LeftButtonSl { get; set; }
|
||||
public Key LeftButtonSr { get; set; }
|
||||
public Key DpadUp { get; set; }
|
||||
public Key DpadDown { get; set; }
|
||||
public Key DpadLeft { get; set; }
|
||||
public Key DpadRight { get; set; }
|
||||
|
||||
public Key ButtonPlus { get; set; }
|
||||
public Key ButtonR { get; set; }
|
||||
public Key ButtonZr { get; set; }
|
||||
public Key RightButtonSl { get; set; }
|
||||
public Key RightButtonSr { get; set; }
|
||||
public Key ButtonX { get; set; }
|
||||
public Key ButtonB { get; set; }
|
||||
public Key ButtonY { get; set; }
|
||||
public Key ButtonA { get; set; }
|
||||
|
||||
|
||||
public Key LeftStickUp { get; set; }
|
||||
public Key LeftStickDown { get; set; }
|
||||
public Key LeftStickLeft { get; set; }
|
||||
public Key LeftStickRight { get; set; }
|
||||
public Key LeftKeyboardStickButton { get; set; }
|
||||
|
||||
public Key RightStickUp { get; set; }
|
||||
public Key RightStickDown { get; set; }
|
||||
public Key RightStickLeft { get; set; }
|
||||
public Key RightStickRight { get; set; }
|
||||
public Key RightKeyboardStickButton { get; set; }
|
||||
|
||||
public int Sensitivity
|
||||
{
|
||||
get => _sensitivity;
|
||||
set
|
||||
{
|
||||
_sensitivity = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public double GyroDeadzone
|
||||
{
|
||||
get => _gyroDeadzone;
|
||||
set
|
||||
{
|
||||
_gyroDeadzone = Math.Round(value, 3);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableMotion
|
||||
{
|
||||
get => enableMotion; set
|
||||
{
|
||||
enableMotion = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableCemuHookMotion { get; set; }
|
||||
public int Slot { get; set; }
|
||||
public int AltSlot { get; set; }
|
||||
public bool MirrorInput { get; set; }
|
||||
public string DsuServerHost { get; set; }
|
||||
public int DsuServerPort { get; set; }
|
||||
|
||||
public bool EnableRumble { get; set; }
|
||||
public float WeakRumble
|
||||
{
|
||||
get => weakRumble; set
|
||||
{
|
||||
weakRumble = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
public float StrongRumble
|
||||
{
|
||||
get => strongRumble; set
|
||||
{
|
||||
strongRumble = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public InputConfiguration(InputConfig config)
|
||||
{
|
||||
if (config != null)
|
||||
{
|
||||
Backend = config.Backend;
|
||||
Id = config.Id;
|
||||
ControllerType = config.ControllerType;
|
||||
PlayerIndex = config.PlayerIndex;
|
||||
|
||||
if (config is StandardKeyboardInputConfig keyboardConfig)
|
||||
{
|
||||
LeftStickUp = (Key)(object)keyboardConfig.LeftJoyconStick.StickUp;
|
||||
LeftStickDown = (Key)(object)keyboardConfig.LeftJoyconStick.StickDown;
|
||||
LeftStickLeft = (Key)(object)keyboardConfig.LeftJoyconStick.StickLeft;
|
||||
LeftStickRight = (Key)(object)keyboardConfig.LeftJoyconStick.StickRight;
|
||||
LeftKeyboardStickButton = (Key)(object)keyboardConfig.LeftJoyconStick.StickButton;
|
||||
|
||||
RightStickUp = (Key)(object)keyboardConfig.RightJoyconStick.StickUp;
|
||||
RightStickDown = (Key)(object)keyboardConfig.RightJoyconStick.StickDown;
|
||||
RightStickLeft = (Key)(object)keyboardConfig.RightJoyconStick.StickLeft;
|
||||
RightStickRight = (Key)(object)keyboardConfig.RightJoyconStick.StickRight;
|
||||
RightKeyboardStickButton = (Key)(object)keyboardConfig.RightJoyconStick.StickButton;
|
||||
|
||||
ButtonA = (Key)(object)keyboardConfig.RightJoycon.ButtonA;
|
||||
ButtonB = (Key)(object)keyboardConfig.RightJoycon.ButtonB;
|
||||
ButtonX = (Key)(object)keyboardConfig.RightJoycon.ButtonX;
|
||||
ButtonY = (Key)(object)keyboardConfig.RightJoycon.ButtonY;
|
||||
ButtonR = (Key)(object)keyboardConfig.RightJoycon.ButtonR;
|
||||
RightButtonSl = (Key)(object)keyboardConfig.RightJoycon.ButtonSl;
|
||||
RightButtonSr = (Key)(object)keyboardConfig.RightJoycon.ButtonSr;
|
||||
ButtonZr = (Key)(object)keyboardConfig.RightJoycon.ButtonZr;
|
||||
ButtonPlus = (Key)(object)keyboardConfig.RightJoycon.ButtonPlus;
|
||||
|
||||
DpadUp = (Key)(object)keyboardConfig.LeftJoycon.DpadUp;
|
||||
DpadDown = (Key)(object)keyboardConfig.LeftJoycon.DpadDown;
|
||||
DpadLeft = (Key)(object)keyboardConfig.LeftJoycon.DpadLeft;
|
||||
DpadRight = (Key)(object)keyboardConfig.LeftJoycon.DpadRight;
|
||||
ButtonMinus = (Key)(object)keyboardConfig.LeftJoycon.ButtonMinus;
|
||||
LeftButtonSl = (Key)(object)keyboardConfig.LeftJoycon.ButtonSl;
|
||||
LeftButtonSr = (Key)(object)keyboardConfig.LeftJoycon.ButtonSr;
|
||||
ButtonZl = (Key)(object)keyboardConfig.LeftJoycon.ButtonZl;
|
||||
ButtonL = (Key)(object)keyboardConfig.LeftJoycon.ButtonL;
|
||||
}
|
||||
else if (config is StandardControllerInputConfig controllerConfig)
|
||||
{
|
||||
LeftJoystick = (Stick)(object)controllerConfig.LeftJoyconStick.Joystick;
|
||||
LeftInvertStickX = controllerConfig.LeftJoyconStick.InvertStickX;
|
||||
LeftInvertStickY = controllerConfig.LeftJoyconStick.InvertStickY;
|
||||
LeftRotate90 = controllerConfig.LeftJoyconStick.Rotate90CW;
|
||||
LeftControllerStickButton = (Key)(object)controllerConfig.LeftJoyconStick.StickButton;
|
||||
|
||||
RightJoystick = (Stick)(object)controllerConfig.RightJoyconStick.Joystick;
|
||||
RightInvertStickX = controllerConfig.RightJoyconStick.InvertStickX;
|
||||
RightInvertStickY = controllerConfig.RightJoyconStick.InvertStickY;
|
||||
RightRotate90 = controllerConfig.RightJoyconStick.Rotate90CW;
|
||||
RightControllerStickButton = (Key)(object)controllerConfig.RightJoyconStick.StickButton;
|
||||
|
||||
ButtonA = (Key)(object)controllerConfig.RightJoycon.ButtonA;
|
||||
ButtonB = (Key)(object)controllerConfig.RightJoycon.ButtonB;
|
||||
ButtonX = (Key)(object)controllerConfig.RightJoycon.ButtonX;
|
||||
ButtonY = (Key)(object)controllerConfig.RightJoycon.ButtonY;
|
||||
ButtonR = (Key)(object)controllerConfig.RightJoycon.ButtonR;
|
||||
RightButtonSl = (Key)(object)controllerConfig.RightJoycon.ButtonSl;
|
||||
RightButtonSr = (Key)(object)controllerConfig.RightJoycon.ButtonSr;
|
||||
ButtonZr = (Key)(object)controllerConfig.RightJoycon.ButtonZr;
|
||||
ButtonPlus = (Key)(object)controllerConfig.RightJoycon.ButtonPlus;
|
||||
|
||||
DpadUp = (Key)(object)controllerConfig.LeftJoycon.DpadUp;
|
||||
DpadDown = (Key)(object)controllerConfig.LeftJoycon.DpadDown;
|
||||
DpadLeft = (Key)(object)controllerConfig.LeftJoycon.DpadLeft;
|
||||
DpadRight = (Key)(object)controllerConfig.LeftJoycon.DpadRight;
|
||||
ButtonMinus = (Key)(object)controllerConfig.LeftJoycon.ButtonMinus;
|
||||
LeftButtonSl = (Key)(object)controllerConfig.LeftJoycon.ButtonSl;
|
||||
LeftButtonSr = (Key)(object)controllerConfig.LeftJoycon.ButtonSr;
|
||||
ButtonZl = (Key)(object)controllerConfig.LeftJoycon.ButtonZl;
|
||||
ButtonL = (Key)(object)controllerConfig.LeftJoycon.ButtonL;
|
||||
|
||||
DeadzoneLeft = controllerConfig.DeadzoneLeft;
|
||||
DeadzoneRight = controllerConfig.DeadzoneRight;
|
||||
RangeLeft = controllerConfig.RangeLeft;
|
||||
RangeRight = controllerConfig.RangeRight;
|
||||
TriggerThreshold = controllerConfig.TriggerThreshold;
|
||||
|
||||
if (controllerConfig.Motion != null)
|
||||
{
|
||||
EnableMotion = controllerConfig.Motion.EnableMotion;
|
||||
MotionBackend = controllerConfig.Motion.MotionBackend;
|
||||
GyroDeadzone = controllerConfig.Motion.GyroDeadzone;
|
||||
Sensitivity = controllerConfig.Motion.Sensitivity;
|
||||
|
||||
if (controllerConfig.Motion is CemuHookMotionConfigController cemuHook)
|
||||
{
|
||||
EnableCemuHookMotion = true;
|
||||
DsuServerHost = cemuHook.DsuServerHost;
|
||||
DsuServerPort = cemuHook.DsuServerPort;
|
||||
Slot = cemuHook.Slot;
|
||||
AltSlot = cemuHook.AltSlot;
|
||||
MirrorInput = cemuHook.MirrorInput;
|
||||
}
|
||||
|
||||
if (controllerConfig.Rumble != null)
|
||||
{
|
||||
EnableRumble = controllerConfig.Rumble.EnableRumble;
|
||||
WeakRumble = controllerConfig.Rumble.WeakRumble;
|
||||
StrongRumble = controllerConfig.Rumble.StrongRumble;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public InputConfiguration()
|
||||
{
|
||||
}
|
||||
|
||||
public InputConfig GetConfig()
|
||||
{
|
||||
if (Backend == InputBackendType.WindowKeyboard)
|
||||
{
|
||||
return new StandardKeyboardInputConfig()
|
||||
{
|
||||
Id = Id,
|
||||
Backend = Backend,
|
||||
PlayerIndex = PlayerIndex,
|
||||
ControllerType = ControllerType,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<Ryujinx.Common.Configuration.Hid.Key>()
|
||||
{
|
||||
DpadUp = (Ryujinx.Common.Configuration.Hid.Key)(object)DpadUp,
|
||||
DpadDown = (Ryujinx.Common.Configuration.Hid.Key)(object)DpadDown,
|
||||
DpadLeft = (Ryujinx.Common.Configuration.Hid.Key)(object)DpadLeft,
|
||||
DpadRight = (Ryujinx.Common.Configuration.Hid.Key)(object)DpadRight,
|
||||
ButtonL = (Ryujinx.Common.Configuration.Hid.Key)(object)ButtonL,
|
||||
ButtonZl = (Ryujinx.Common.Configuration.Hid.Key)(object)ButtonZl,
|
||||
ButtonSl = (Ryujinx.Common.Configuration.Hid.Key)(object)LeftButtonSl,
|
||||
ButtonSr = (Ryujinx.Common.Configuration.Hid.Key)(object)LeftButtonSr,
|
||||
ButtonMinus = (Ryujinx.Common.Configuration.Hid.Key)(object)ButtonMinus
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<Ryujinx.Common.Configuration.Hid.Key>()
|
||||
{
|
||||
ButtonA = (Ryujinx.Common.Configuration.Hid.Key)(object)ButtonA,
|
||||
ButtonB = (Ryujinx.Common.Configuration.Hid.Key)(object)ButtonB,
|
||||
ButtonX = (Ryujinx.Common.Configuration.Hid.Key)(object)ButtonX,
|
||||
ButtonY = (Ryujinx.Common.Configuration.Hid.Key)(object)ButtonY,
|
||||
ButtonPlus = (Ryujinx.Common.Configuration.Hid.Key)(object)ButtonPlus,
|
||||
ButtonSl = (Ryujinx.Common.Configuration.Hid.Key)(object)RightButtonSl,
|
||||
ButtonSr = (Ryujinx.Common.Configuration.Hid.Key)(object)RightButtonSr,
|
||||
ButtonR = (Ryujinx.Common.Configuration.Hid.Key)(object)ButtonR,
|
||||
ButtonZr = (Ryujinx.Common.Configuration.Hid.Key)(object)ButtonZr
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Ryujinx.Common.Configuration.Hid.Key>()
|
||||
{
|
||||
StickUp = (Ryujinx.Common.Configuration.Hid.Key)(object)LeftStickUp,
|
||||
StickDown = (Ryujinx.Common.Configuration.Hid.Key)(object)LeftStickDown,
|
||||
StickRight = (Ryujinx.Common.Configuration.Hid.Key)(object)LeftStickRight,
|
||||
StickLeft = (Ryujinx.Common.Configuration.Hid.Key)(object)LeftStickLeft,
|
||||
StickButton = (Ryujinx.Common.Configuration.Hid.Key)(object)LeftKeyboardStickButton
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<Ryujinx.Common.Configuration.Hid.Key>()
|
||||
{
|
||||
StickUp = (Ryujinx.Common.Configuration.Hid.Key)(object)RightStickUp,
|
||||
StickDown = (Ryujinx.Common.Configuration.Hid.Key)(object)RightStickDown,
|
||||
StickLeft = (Ryujinx.Common.Configuration.Hid.Key)(object)RightStickLeft,
|
||||
StickRight = (Ryujinx.Common.Configuration.Hid.Key)(object)RightStickRight,
|
||||
StickButton = (Ryujinx.Common.Configuration.Hid.Key)(object)RightKeyboardStickButton
|
||||
},
|
||||
Version = InputConfig.CurrentVersion
|
||||
};
|
||||
|
||||
}
|
||||
else if (Backend == InputBackendType.GamepadSDL2)
|
||||
{
|
||||
var config = new StandardControllerInputConfig()
|
||||
{
|
||||
Id = Id,
|
||||
Backend = Backend,
|
||||
PlayerIndex = PlayerIndex,
|
||||
ControllerType = ControllerType,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<GamepadInputId>()
|
||||
{
|
||||
DpadUp = (GamepadInputId)(object)DpadUp,
|
||||
DpadDown = (GamepadInputId)(object)DpadDown,
|
||||
DpadLeft = (GamepadInputId)(object)DpadLeft,
|
||||
DpadRight = (GamepadInputId)(object)DpadRight,
|
||||
ButtonL = (GamepadInputId)(object)ButtonL,
|
||||
ButtonZl = (GamepadInputId)(object)ButtonZl,
|
||||
ButtonSl = (GamepadInputId)(object)LeftButtonSl,
|
||||
ButtonSr = (GamepadInputId)(object)LeftButtonSr,
|
||||
ButtonMinus = (GamepadInputId)(object)ButtonMinus,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<GamepadInputId>()
|
||||
{
|
||||
ButtonA = (GamepadInputId)(object)ButtonA,
|
||||
ButtonB = (GamepadInputId)(object)ButtonB,
|
||||
ButtonX = (GamepadInputId)(object)ButtonX,
|
||||
ButtonY = (GamepadInputId)(object)ButtonY,
|
||||
ButtonPlus = (GamepadInputId)(object)ButtonPlus,
|
||||
ButtonSl = (GamepadInputId)(object)RightButtonSl,
|
||||
ButtonSr = (GamepadInputId)(object)RightButtonSr,
|
||||
ButtonR = (GamepadInputId)(object)ButtonR,
|
||||
ButtonZr = (GamepadInputId)(object)ButtonZr,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigControllerStick<GamepadInputId, StickInputId>()
|
||||
{
|
||||
Joystick = (StickInputId)(object)LeftJoystick,
|
||||
InvertStickX = LeftInvertStickX,
|
||||
InvertStickY = LeftInvertStickY,
|
||||
Rotate90CW = LeftRotate90,
|
||||
StickButton = (GamepadInputId)(object)LeftControllerStickButton,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigControllerStick<GamepadInputId, StickInputId>()
|
||||
{
|
||||
Joystick = (StickInputId)(object)RightJoystick,
|
||||
InvertStickX = RightInvertStickX,
|
||||
InvertStickY = RightInvertStickY,
|
||||
Rotate90CW = RightRotate90,
|
||||
StickButton = (GamepadInputId)(object)RightControllerStickButton,
|
||||
},
|
||||
Rumble = new RumbleConfigController()
|
||||
{
|
||||
EnableRumble = EnableRumble,
|
||||
WeakRumble = WeakRumble,
|
||||
StrongRumble = StrongRumble
|
||||
},
|
||||
Version = InputConfig.CurrentVersion,
|
||||
DeadzoneLeft = DeadzoneLeft,
|
||||
DeadzoneRight = DeadzoneRight,
|
||||
RangeLeft = RangeLeft,
|
||||
RangeRight = RangeRight,
|
||||
TriggerThreshold = TriggerThreshold,
|
||||
Motion = EnableCemuHookMotion
|
||||
? new CemuHookMotionConfigController()
|
||||
{
|
||||
DsuServerHost = DsuServerHost,
|
||||
DsuServerPort = DsuServerPort,
|
||||
Slot = Slot,
|
||||
AltSlot = AltSlot,
|
||||
MirrorInput = MirrorInput,
|
||||
MotionBackend = MotionInputBackendType.CemuHook
|
||||
}
|
||||
: new StandardMotionConfigController()
|
||||
{
|
||||
MotionBackend = MotionInputBackendType.GamepadDriver
|
||||
}
|
||||
};
|
||||
|
||||
config.Motion.Sensitivity = Sensitivity;
|
||||
config.Motion.EnableMotion = EnableMotion;
|
||||
config.Motion.GyroDeadzone = GyroDeadzone;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
6
Ryujinx.Ava/UI/Models/PlayerModel.cs
Normal file
6
Ryujinx.Ava/UI/Models/PlayerModel.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
using Ryujinx.Common.Configuration.Hid;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
public record PlayerModel(PlayerIndex Id, string Name);
|
||||
}
|
14
Ryujinx.Ava/UI/Models/ProfileImageModel.cs
Normal file
14
Ryujinx.Ava/UI/Models/ProfileImageModel.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
public class ProfileImageModel
|
||||
{
|
||||
public ProfileImageModel(string name, byte[] data)
|
||||
{
|
||||
Name = name;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public byte[] Data { get; set; }
|
||||
}
|
||||
}
|
123
Ryujinx.Ava/UI/Models/SaveModel.cs
Normal file
123
Ryujinx.Ava/UI/Models/SaveModel.cs
Normal file
|
@ -0,0 +1,123 @@
|
|||
using LibHac;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Shim;
|
||||
using LibHac.Ncm;
|
||||
using Ryujinx.Ava.Common;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
public class SaveModel : BaseModel
|
||||
{
|
||||
private readonly HorizonClient _horizonClient;
|
||||
private long _size;
|
||||
|
||||
public Action DeleteAction { get; set; }
|
||||
public ulong SaveId { get; }
|
||||
public ProgramId TitleId { get; }
|
||||
public string TitleIdString => $"{TitleId.Value:X16}";
|
||||
public UserId UserId { get; }
|
||||
public bool InGameList { get; }
|
||||
public string Title { get; }
|
||||
public byte[] Icon { get; }
|
||||
|
||||
public long Size
|
||||
{
|
||||
get => _size; set
|
||||
{
|
||||
_size = value;
|
||||
SizeAvailable = true;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(SizeString));
|
||||
OnPropertyChanged(nameof(SizeAvailable));
|
||||
}
|
||||
}
|
||||
|
||||
public bool SizeAvailable { get; set; }
|
||||
|
||||
public string SizeString => $"{((float)_size * 0.000000954):0.###}MB";
|
||||
|
||||
public SaveModel(SaveDataInfo info, HorizonClient horizonClient, VirtualFileSystem virtualFileSystem)
|
||||
{
|
||||
_horizonClient = horizonClient;
|
||||
SaveId = info.SaveDataId;
|
||||
TitleId = info.ProgramId;
|
||||
UserId = info.UserId;
|
||||
|
||||
var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString);
|
||||
|
||||
InGameList = appData != null;
|
||||
|
||||
if (InGameList)
|
||||
{
|
||||
Icon = appData.Icon;
|
||||
Title = appData.TitleName;
|
||||
}
|
||||
else
|
||||
{
|
||||
var appMetadata = MainWindow.MainWindowViewModel.ApplicationLibrary.LoadAndSaveMetaData(TitleIdString);
|
||||
Title = appMetadata.Title ?? TitleIdString;
|
||||
}
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
var saveRoot = System.IO.Path.Combine(virtualFileSystem.GetNandPath(), $"user/save/{info.SaveDataId:x16}");
|
||||
|
||||
long total_size = GetDirectorySize(saveRoot);
|
||||
long GetDirectorySize(string path)
|
||||
{
|
||||
long size = 0;
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
var directories = Directory.GetDirectories(path);
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
size += GetDirectorySize(directory);
|
||||
}
|
||||
|
||||
var files = Directory.GetFiles(path);
|
||||
foreach (var file in files)
|
||||
{
|
||||
size += new FileInfo(file).Length;
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
Size = total_size;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public void OpenLocation()
|
||||
{
|
||||
ApplicationHelper.OpenSaveDir(SaveId);
|
||||
}
|
||||
|
||||
public async void Delete()
|
||||
{
|
||||
var result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance["DeleteUserSave"],
|
||||
LocaleManager.Instance["IrreversibleActionNote"],
|
||||
LocaleManager.Instance["InputDialogYes"],
|
||||
LocaleManager.Instance["InputDialogNo"], "");
|
||||
|
||||
if (result == UserResult.Yes)
|
||||
{
|
||||
_horizonClient.Fs.DeleteSaveData(SaveDataSpaceId.User, SaveId);
|
||||
|
||||
DeleteAction?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
Ryujinx.Ava/UI/Models/StatusUpdatedEventArgs.cs
Normal file
28
Ryujinx.Ava/UI/Models/StatusUpdatedEventArgs.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
internal class StatusUpdatedEventArgs : EventArgs
|
||||
{
|
||||
public bool VSyncEnabled { get; }
|
||||
public string VolumeStatus { get; }
|
||||
public string GpuBackend { get; }
|
||||
public string AspectRatio { get; }
|
||||
public string DockedMode { get; }
|
||||
public string FifoStatus { get; }
|
||||
public string GameStatus { get; }
|
||||
public string GpuName { get; }
|
||||
|
||||
public StatusUpdatedEventArgs(bool vSyncEnabled, string volumeStatus, string gpuBackend, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName)
|
||||
{
|
||||
VSyncEnabled = vSyncEnabled;
|
||||
VolumeStatus = volumeStatus;
|
||||
GpuBackend = gpuBackend;
|
||||
DockedMode = dockedMode;
|
||||
AspectRatio = aspectRatio;
|
||||
GameStatus = gameStatus;
|
||||
FifoStatus = fifoStatus;
|
||||
GpuName = gpuName;
|
||||
}
|
||||
}
|
||||
}
|
58
Ryujinx.Ava/UI/Models/TempProfile.cs
Normal file
58
Ryujinx.Ava/UI/Models/TempProfile.cs
Normal file
|
@ -0,0 +1,58 @@
|
|||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
public class TempProfile : BaseModel
|
||||
{
|
||||
private readonly UserProfile _profile;
|
||||
private byte[] _image = null;
|
||||
private string _name = String.Empty;
|
||||
private UserId _userId;
|
||||
|
||||
public byte[] Image
|
||||
{
|
||||
get => _image;
|
||||
set
|
||||
{
|
||||
_image = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public UserId UserId
|
||||
{
|
||||
get => _userId;
|
||||
set
|
||||
{
|
||||
_userId = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
_name = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public TempProfile(UserProfile profile)
|
||||
{
|
||||
_profile = profile;
|
||||
|
||||
if (_profile != null)
|
||||
{
|
||||
Image = profile.Image;
|
||||
Name = profile.Name;
|
||||
UserId = profile.UserId;
|
||||
}
|
||||
}
|
||||
|
||||
public TempProfile(){}
|
||||
}
|
||||
}
|
16
Ryujinx.Ava/UI/Models/TimeZone.cs
Normal file
16
Ryujinx.Ava/UI/Models/TimeZone.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
internal class TimeZone
|
||||
{
|
||||
public TimeZone(string utcDifference, string location, string abbreviation)
|
||||
{
|
||||
UtcDifference = utcDifference;
|
||||
Location = location;
|
||||
Abbreviation = abbreviation;
|
||||
}
|
||||
|
||||
public string UtcDifference { get; set; }
|
||||
public string Location { get; set; }
|
||||
public string Abbreviation { get; set; }
|
||||
}
|
||||
}
|
25
Ryujinx.Ava/UI/Models/TitleUpdateModel.cs
Normal file
25
Ryujinx.Ava/UI/Models/TitleUpdateModel.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using LibHac.Ns;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
internal class TitleUpdateModel
|
||||
{
|
||||
public bool IsEnabled { get; set; }
|
||||
public bool IsNoUpdate { get; }
|
||||
public ApplicationControlProperty Control { get; }
|
||||
public string Path { get; }
|
||||
|
||||
public string Label => IsNoUpdate
|
||||
? LocaleManager.Instance["NoUpdate"]
|
||||
: string.Format(LocaleManager.Instance["TitleUpdateVersionLabel"], Control.DisplayVersionString.ToString(),
|
||||
Path);
|
||||
|
||||
public TitleUpdateModel(ApplicationControlProperty control, string path, bool isNoUpdate = false)
|
||||
{
|
||||
Control = control;
|
||||
Path = path;
|
||||
IsNoUpdate = isNoUpdate;
|
||||
}
|
||||
}
|
||||
}
|
69
Ryujinx.Ava/UI/Models/UserProfile.cs
Normal file
69
Ryujinx.Ava/UI/Models/UserProfile.cs
Normal file
|
@ -0,0 +1,69 @@
|
|||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Profile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models
|
||||
{
|
||||
public class UserProfile : BaseModel
|
||||
{
|
||||
private readonly Profile _profile;
|
||||
private readonly NavigationDialogHost _owner;
|
||||
private byte[] _image;
|
||||
private string _name;
|
||||
private UserId _userId;
|
||||
|
||||
public byte[] Image
|
||||
{
|
||||
get => _image;
|
||||
set
|
||||
{
|
||||
_image = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public UserId UserId
|
||||
{
|
||||
get => _userId;
|
||||
set
|
||||
{
|
||||
_userId = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
_name = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public UserProfile(Profile profile, NavigationDialogHost owner)
|
||||
{
|
||||
_profile = profile;
|
||||
_owner = owner;
|
||||
|
||||
Image = profile.Image;
|
||||
Name = profile.Name;
|
||||
UserId = profile.UserId;
|
||||
}
|
||||
|
||||
public bool IsOpened => _profile.AccountState == AccountState.Open;
|
||||
|
||||
public void UpdateState()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsOpened));
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
|
||||
public void Recover(UserProfile userProfile)
|
||||
{
|
||||
_owner.Navigate(typeof(UserEditor), (_owner, userProfile, true));
|
||||
}
|
||||
}
|
||||
}
|
452
Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs
Normal file
452
Ryujinx.Ava/UI/ViewModels/AmiiboWindowViewModel.cs
Normal file
|
@ -0,0 +1,452 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public class AmiiboWindowViewModel : BaseModel, IDisposable
|
||||
{
|
||||
private const string DefaultJson = "{ \"amiibo\": [] }";
|
||||
private const float AmiiboImageSize = 350f;
|
||||
|
||||
private readonly string _amiiboJsonPath;
|
||||
private readonly byte[] _amiiboLogoBytes;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly StyleableWindow _owner;
|
||||
|
||||
private Bitmap _amiiboImage;
|
||||
private List<Amiibo.AmiiboApi> _amiiboList;
|
||||
private AvaloniaList<Amiibo.AmiiboApi> _amiibos;
|
||||
private ObservableCollection<string> _amiiboSeries;
|
||||
|
||||
private int _amiiboSelectedIndex;
|
||||
private int _seriesSelectedIndex;
|
||||
private bool _enableScanning;
|
||||
private bool _showAllAmiibo;
|
||||
private bool _useRandomUuid;
|
||||
private string _usage;
|
||||
|
||||
public AmiiboWindowViewModel(StyleableWindow owner, string lastScannedAmiiboId, string titleId)
|
||||
{
|
||||
_owner = owner;
|
||||
_httpClient = new HttpClient { Timeout = TimeSpan.FromMilliseconds(5000) };
|
||||
LastScannedAmiiboId = lastScannedAmiiboId;
|
||||
TitleId = titleId;
|
||||
|
||||
Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"));
|
||||
|
||||
_amiiboJsonPath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json");
|
||||
_amiiboList = new List<Amiibo.AmiiboApi>();
|
||||
_amiiboSeries = new ObservableCollection<string>();
|
||||
_amiibos = new AvaloniaList<Amiibo.AmiiboApi>();
|
||||
|
||||
_amiiboLogoBytes = EmbeddedResources.Read("Ryujinx.Ui.Common/Resources/Logo_Amiibo.png");
|
||||
|
||||
_ = LoadContentAsync();
|
||||
}
|
||||
|
||||
public AmiiboWindowViewModel() { }
|
||||
|
||||
public string TitleId { get; set; }
|
||||
public string LastScannedAmiiboId { get; set; }
|
||||
|
||||
public UserResult Response { get; private set; }
|
||||
|
||||
public bool UseRandomUuid
|
||||
{
|
||||
get => _useRandomUuid;
|
||||
set
|
||||
{
|
||||
_useRandomUuid = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowAllAmiibo
|
||||
{
|
||||
get => _showAllAmiibo;
|
||||
set
|
||||
{
|
||||
_showAllAmiibo = value;
|
||||
|
||||
#pragma warning disable 4014
|
||||
ParseAmiiboData();
|
||||
#pragma warning restore 4014
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public AvaloniaList<Amiibo.AmiiboApi> AmiiboList
|
||||
{
|
||||
get => _amiibos;
|
||||
set
|
||||
{
|
||||
_amiibos = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<string> AmiiboSeries
|
||||
{
|
||||
get => _amiiboSeries;
|
||||
set
|
||||
{
|
||||
_amiiboSeries = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int SeriesSelectedIndex
|
||||
{
|
||||
get => _seriesSelectedIndex;
|
||||
set
|
||||
{
|
||||
_seriesSelectedIndex = value;
|
||||
|
||||
FilterAmiibo();
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int AmiiboSelectedIndex
|
||||
{
|
||||
get => _amiiboSelectedIndex;
|
||||
set
|
||||
{
|
||||
_amiiboSelectedIndex = value;
|
||||
|
||||
EnableScanning = _amiiboSelectedIndex >= 0 && _amiiboSelectedIndex < _amiibos.Count;
|
||||
|
||||
SetAmiiboDetails();
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public Bitmap AmiiboImage
|
||||
{
|
||||
get => _amiiboImage;
|
||||
set
|
||||
{
|
||||
_amiiboImage = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string Usage
|
||||
{
|
||||
get => _usage;
|
||||
set
|
||||
{
|
||||
_usage = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableScanning
|
||||
{
|
||||
get => _enableScanning;
|
||||
set
|
||||
{
|
||||
_enableScanning = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
|
||||
private async Task LoadContentAsync()
|
||||
{
|
||||
string amiiboJsonString = DefaultJson;
|
||||
|
||||
if (File.Exists(_amiiboJsonPath))
|
||||
{
|
||||
amiiboJsonString = File.ReadAllText(_amiiboJsonPath);
|
||||
|
||||
if (await NeedsUpdate(JsonHelper.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).LastUpdated))
|
||||
{
|
||||
amiiboJsonString = await DownloadAmiiboJson();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
amiiboJsonString = await DownloadAmiiboJson();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ShowInfoDialog();
|
||||
}
|
||||
}
|
||||
|
||||
_amiiboList = JsonHelper.Deserialize<Amiibo.AmiiboJson>(amiiboJsonString).Amiibo;
|
||||
_amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
|
||||
|
||||
ParseAmiiboData();
|
||||
}
|
||||
|
||||
private void ParseAmiiboData()
|
||||
{
|
||||
_amiiboSeries.Clear();
|
||||
_amiibos.Clear();
|
||||
|
||||
for (int i = 0; i < _amiiboList.Count; i++)
|
||||
{
|
||||
if (!_amiiboSeries.Contains(_amiiboList[i].AmiiboSeries))
|
||||
{
|
||||
if (!ShowAllAmiibo)
|
||||
{
|
||||
foreach (Amiibo.AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch)
|
||||
{
|
||||
if (game != null)
|
||||
{
|
||||
if (game.GameId.Contains(TitleId))
|
||||
{
|
||||
AmiiboSeries.Add(_amiiboList[i].AmiiboSeries);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AmiiboSeries.Add(_amiiboList[i].AmiiboSeries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (LastScannedAmiiboId != "")
|
||||
{
|
||||
SelectLastScannedAmiibo();
|
||||
}
|
||||
else
|
||||
{
|
||||
SeriesSelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectLastScannedAmiibo()
|
||||
{
|
||||
Amiibo.AmiiboApi scanned = _amiiboList.FirstOrDefault(amiibo => amiibo.GetId() == LastScannedAmiiboId);
|
||||
|
||||
SeriesSelectedIndex = AmiiboSeries.IndexOf(scanned.AmiiboSeries);
|
||||
AmiiboSelectedIndex = AmiiboList.IndexOf(scanned);
|
||||
}
|
||||
|
||||
private void FilterAmiibo()
|
||||
{
|
||||
_amiibos.Clear();
|
||||
|
||||
if (_seriesSelectedIndex < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<Amiibo.AmiiboApi> amiiboSortedList = _amiiboList
|
||||
.Where(amiibo => amiibo.AmiiboSeries == _amiiboSeries[SeriesSelectedIndex])
|
||||
.OrderBy(amiibo => amiibo.Name).ToList();
|
||||
|
||||
for (int i = 0; i < amiiboSortedList.Count; i++)
|
||||
{
|
||||
if (!_amiibos.Contains(amiiboSortedList[i]))
|
||||
{
|
||||
if (!_showAllAmiibo)
|
||||
{
|
||||
foreach (Amiibo.AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch)
|
||||
{
|
||||
if (game != null)
|
||||
{
|
||||
if (game.GameId.Contains(TitleId))
|
||||
{
|
||||
_amiibos.Add(amiiboSortedList[i]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_amiibos.Add(amiiboSortedList[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AmiiboSelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void SetAmiiboDetails()
|
||||
{
|
||||
ResetAmiiboPreview();
|
||||
|
||||
Usage = string.Empty;
|
||||
|
||||
if (_amiiboSelectedIndex < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Amiibo.AmiiboApi selected = _amiibos[_amiiboSelectedIndex];
|
||||
|
||||
string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Equals(selected)).Image;
|
||||
|
||||
string usageString = "";
|
||||
|
||||
for (int i = 0; i < _amiiboList.Count; i++)
|
||||
{
|
||||
if (_amiiboList[i].Equals(selected))
|
||||
{
|
||||
bool writable = false;
|
||||
|
||||
foreach (Amiibo.AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch)
|
||||
{
|
||||
if (item.GameId.Contains(TitleId))
|
||||
{
|
||||
foreach (Amiibo.AmiiboApiUsage usageItem in item.AmiiboUsage)
|
||||
{
|
||||
usageString += Environment.NewLine +
|
||||
$"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}";
|
||||
|
||||
writable = usageItem.Write;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (usageString.Length == 0)
|
||||
{
|
||||
usageString = LocaleManager.Instance["Unknown"] + ".";
|
||||
}
|
||||
|
||||
Usage = $"{LocaleManager.Instance["Usage"]} {(writable ? $" ({LocaleManager.Instance["Writable"]})" : "")} : {usageString}";
|
||||
}
|
||||
}
|
||||
|
||||
_ = UpdateAmiiboPreview(imageUrl);
|
||||
}
|
||||
|
||||
private async Task<bool> NeedsUpdate(DateTime oldLastModified)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response =
|
||||
await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://amiibo.ryujinx.org/"));
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return response.Content.Headers.LastModified != oldLastModified;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ShowInfoDialog();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> DownloadAmiiboJson()
|
||||
{
|
||||
HttpResponseMessage response = await _httpClient.GetAsync("https://amiibo.ryujinx.org/");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string amiiboJsonString = await response.Content.ReadAsStringAsync();
|
||||
|
||||
using (FileStream amiiboJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough))
|
||||
{
|
||||
amiiboJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString));
|
||||
}
|
||||
|
||||
return amiiboJsonString;
|
||||
}
|
||||
|
||||
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance["DialogAmiiboApiTitle"],
|
||||
LocaleManager.Instance["DialogAmiiboApiFailFetchMessage"],
|
||||
LocaleManager.Instance["InputDialogOk"],
|
||||
"",
|
||||
LocaleManager.Instance["RyujinxInfo"]);
|
||||
|
||||
Close();
|
||||
|
||||
return DefaultJson;
|
||||
}
|
||||
|
||||
private void Close()
|
||||
{
|
||||
Dispatcher.UIThread.Post(_owner.Close);
|
||||
}
|
||||
|
||||
private async Task UpdateAmiiboPreview(string imageUrl)
|
||||
{
|
||||
HttpResponseMessage response = await _httpClient.GetAsync(imageUrl);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
byte[] amiiboPreviewBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
using (MemoryStream memoryStream = new(amiiboPreviewBytes))
|
||||
{
|
||||
Bitmap bitmap = new(memoryStream);
|
||||
|
||||
double ratio = Math.Min(AmiiboImageSize / bitmap.Size.Width,
|
||||
AmiiboImageSize / bitmap.Size.Height);
|
||||
|
||||
int resizeHeight = (int)(bitmap.Size.Height * ratio);
|
||||
int resizeWidth = (int)(bitmap.Size.Width * ratio);
|
||||
|
||||
AmiiboImage = bitmap.CreateScaledBitmap(new PixelSize(resizeWidth, resizeHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetAmiiboPreview()
|
||||
{
|
||||
using (MemoryStream memoryStream = new(_amiiboLogoBytes))
|
||||
{
|
||||
Bitmap bitmap = new(memoryStream);
|
||||
|
||||
AmiiboImage = bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
private async void ShowInfoDialog()
|
||||
{
|
||||
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance["DialogAmiiboApiTitle"],
|
||||
LocaleManager.Instance["DialogAmiiboApiConnectErrorMessage"],
|
||||
LocaleManager.Instance["InputDialogOk"],
|
||||
"",
|
||||
LocaleManager.Instance["RyujinxInfo"]);
|
||||
}
|
||||
}
|
||||
}
|
363
Ryujinx.Ava/UI/ViewModels/AvatarProfileViewModel.cs
Normal file
363
Ryujinx.Ava/UI/ViewModels/AvatarProfileViewModel.cs
Normal file
|
@ -0,0 +1,363 @@
|
|||
using Avalonia.Media;
|
||||
using DynamicData;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Ncm;
|
||||
using LibHac.Tools.Fs;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Color = Avalonia.Media.Color;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
internal class AvatarProfileViewModel : BaseModel, IDisposable
|
||||
{
|
||||
private const int MaxImageTasks = 4;
|
||||
|
||||
private static readonly Dictionary<string, byte[]> _avatarStore = new();
|
||||
private static bool _isPreloading;
|
||||
private static Action _loadCompleteAction;
|
||||
|
||||
private ObservableCollection<ProfileImageModel> _images;
|
||||
private Color _backgroundColor = Colors.White;
|
||||
|
||||
private int _selectedIndex;
|
||||
private int _imagesLoaded;
|
||||
private bool _isActive;
|
||||
private byte[] _selectedImage;
|
||||
private bool _isIndeterminate = true;
|
||||
|
||||
public bool IsActive
|
||||
{
|
||||
get => _isActive;
|
||||
set => _isActive = value;
|
||||
}
|
||||
|
||||
public AvatarProfileViewModel()
|
||||
{
|
||||
_images = new ObservableCollection<ProfileImageModel>();
|
||||
}
|
||||
|
||||
public AvatarProfileViewModel(Action loadCompleteAction)
|
||||
{
|
||||
_images = new ObservableCollection<ProfileImageModel>();
|
||||
|
||||
if (_isPreloading)
|
||||
{
|
||||
_loadCompleteAction = loadCompleteAction;
|
||||
}
|
||||
else
|
||||
{
|
||||
ReloadImages();
|
||||
}
|
||||
}
|
||||
|
||||
public Color BackgroundColor
|
||||
{
|
||||
get => _backgroundColor;
|
||||
set
|
||||
{
|
||||
_backgroundColor = value;
|
||||
|
||||
IsActive = false;
|
||||
|
||||
ReloadImages();
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<ProfileImageModel> Images
|
||||
{
|
||||
get => _images;
|
||||
set
|
||||
{
|
||||
_images = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsIndeterminate
|
||||
{
|
||||
get => _isIndeterminate;
|
||||
set
|
||||
{
|
||||
_isIndeterminate = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int ImageCount => _avatarStore.Count;
|
||||
|
||||
public int ImagesLoaded
|
||||
{
|
||||
get => _imagesLoaded;
|
||||
set
|
||||
{
|
||||
_imagesLoaded = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int SelectedIndex
|
||||
{
|
||||
get => _selectedIndex;
|
||||
set
|
||||
{
|
||||
_selectedIndex = value;
|
||||
|
||||
if (_selectedIndex == -1)
|
||||
{
|
||||
SelectedImage = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedImage = _images[_selectedIndex].Data;
|
||||
}
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] SelectedImage
|
||||
{
|
||||
get => _selectedImage;
|
||||
private set => _selectedImage = value;
|
||||
}
|
||||
|
||||
public void ReloadImages()
|
||||
{
|
||||
if (_isPreloading)
|
||||
{
|
||||
IsIndeterminate = false;
|
||||
return;
|
||||
}
|
||||
Task.Run(() =>
|
||||
{
|
||||
IsActive = true;
|
||||
|
||||
Images.Clear();
|
||||
int selectedIndex = _selectedIndex;
|
||||
int index = 0;
|
||||
|
||||
ImagesLoaded = 0;
|
||||
IsIndeterminate = false;
|
||||
|
||||
var keys = _avatarStore.Keys.ToList();
|
||||
|
||||
var newImages = new List<ProfileImageModel>();
|
||||
var tasks = new List<Task>();
|
||||
|
||||
for (int i = 0; i < MaxImageTasks; i++)
|
||||
{
|
||||
var start = i;
|
||||
tasks.Add(Task.Run(() => ImageTask(start)));
|
||||
}
|
||||
|
||||
Task.WaitAll(tasks.ToArray());
|
||||
|
||||
Images.AddRange(newImages);
|
||||
|
||||
void ImageTask(int start)
|
||||
{
|
||||
for (int i = start; i < keys.Count; i += MaxImageTasks)
|
||||
{
|
||||
if (!IsActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = keys[i];
|
||||
var image = _avatarStore[keys[i]];
|
||||
|
||||
var data = ProcessImage(image);
|
||||
newImages.Add(new ProfileImageModel(key, data));
|
||||
if (index++ == selectedIndex)
|
||||
{
|
||||
SelectedImage = data;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _imagesLoaded);
|
||||
OnPropertyChanged(nameof(ImagesLoaded));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private byte[] ProcessImage(byte[] data)
|
||||
{
|
||||
using (MemoryStream streamJpg = new())
|
||||
{
|
||||
Image avatarImage = Image.Load(data, new PngDecoder());
|
||||
|
||||
avatarImage.Mutate(x => x.BackgroundColor(new Rgba32(BackgroundColor.R,
|
||||
BackgroundColor.G,
|
||||
BackgroundColor.B,
|
||||
BackgroundColor.A)));
|
||||
avatarImage.SaveAsJpeg(streamJpg);
|
||||
|
||||
return streamJpg.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_avatarStore.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isPreloading = true;
|
||||
|
||||
string contentPath =
|
||||
contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem,
|
||||
NcaContentType.Data);
|
||||
string avatarPath = virtualFileSystem.SwitchPathToSystemPath(contentPath);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(avatarPath))
|
||||
{
|
||||
using (IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open))
|
||||
{
|
||||
Nca nca = new(virtualFileSystem.KeySet, ncaFileStream);
|
||||
IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
||||
|
||||
foreach (DirectoryEntryEx item in romfs.EnumerateEntries())
|
||||
{
|
||||
// TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy.
|
||||
if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") &&
|
||||
item.FullPath.Contains("szs"))
|
||||
{
|
||||
using var file = new UniqueRef<IFile>();
|
||||
|
||||
romfs.OpenFile(ref file.Ref(), ("/" + item.FullPath).ToU8Span(), OpenMode.Read)
|
||||
.ThrowIfFailure();
|
||||
|
||||
using (MemoryStream stream = new())
|
||||
using (MemoryStream streamPng = new())
|
||||
{
|
||||
file.Get.AsStream().CopyTo(stream);
|
||||
|
||||
stream.Position = 0;
|
||||
|
||||
Image avatarImage = Image.LoadPixelData<Rgba32>(DecompressYaz0(stream), 256, 256);
|
||||
|
||||
avatarImage.SaveAsPng(streamPng);
|
||||
|
||||
_avatarStore.Add(item.FullPath, streamPng.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isPreloading = false;
|
||||
_loadCompleteAction?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] DecompressYaz0(Stream stream)
|
||||
{
|
||||
using (BinaryReader reader = new(stream))
|
||||
{
|
||||
reader.ReadInt32(); // Magic
|
||||
|
||||
uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32());
|
||||
|
||||
reader.ReadInt64(); // Padding
|
||||
|
||||
byte[] input = new byte[stream.Length - stream.Position];
|
||||
stream.Read(input, 0, input.Length);
|
||||
|
||||
uint inputOffset = 0;
|
||||
|
||||
byte[] output = new byte[decodedLength];
|
||||
uint outputOffset = 0;
|
||||
|
||||
ushort mask = 0;
|
||||
byte header = 0;
|
||||
|
||||
while (outputOffset < decodedLength)
|
||||
{
|
||||
if ((mask >>= 1) == 0)
|
||||
{
|
||||
header = input[inputOffset++];
|
||||
mask = 0x80;
|
||||
}
|
||||
|
||||
if ((header & mask) != 0)
|
||||
{
|
||||
if (outputOffset == output.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
output[outputOffset++] = input[inputOffset++];
|
||||
}
|
||||
else
|
||||
{
|
||||
byte byte1 = input[inputOffset++];
|
||||
byte byte2 = input[inputOffset++];
|
||||
|
||||
uint dist = (uint)((byte1 & 0xF) << 8) | byte2;
|
||||
uint position = outputOffset - (dist + 1);
|
||||
|
||||
uint length = (uint)byte1 >> 4;
|
||||
if (length == 0)
|
||||
{
|
||||
length = (uint)input[inputOffset++] + 0x12;
|
||||
}
|
||||
else
|
||||
{
|
||||
length += 2;
|
||||
}
|
||||
|
||||
uint gap = outputOffset - position;
|
||||
uint nonOverlappingLength = length;
|
||||
|
||||
if (nonOverlappingLength > gap)
|
||||
{
|
||||
nonOverlappingLength = gap;
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength);
|
||||
outputOffset += nonOverlappingLength;
|
||||
position += nonOverlappingLength;
|
||||
length -= nonOverlappingLength;
|
||||
|
||||
while (length-- > 0)
|
||||
{
|
||||
output[outputOffset++] = output[position++];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_loadCompleteAction = null;
|
||||
IsActive = false;
|
||||
}
|
||||
}
|
||||
}
|
15
Ryujinx.Ava/UI/ViewModels/BaseModel.cs
Normal file
15
Ryujinx.Ava/UI/ViewModels/BaseModel.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public class BaseModel : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
}
|
901
Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs
Normal file
901
Ryujinx.Ava/UI/ViewModels/ControllerSettingsViewModel.cs
Normal file
|
@ -0,0 +1,901 @@
|
|||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Svg.Skia;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Input;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
|
||||
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
|
||||
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public class ControllerSettingsViewModel : BaseModel, IDisposable
|
||||
{
|
||||
private const string Disabled = "disabled";
|
||||
private const string ProControllerResource = "Ryujinx.Ui.Common/Resources/Controller_ProCon.svg";
|
||||
private const string JoyConPairResource = "Ryujinx.Ui.Common/Resources/Controller_JoyConPair.svg";
|
||||
private const string JoyConLeftResource = "Ryujinx.Ui.Common/Resources/Controller_JoyConLeft.svg";
|
||||
private const string JoyConRightResource = "Ryujinx.Ui.Common/Resources/Controller_JoyConRight.svg";
|
||||
private const string KeyboardString = "keyboard";
|
||||
private const string ControllerString = "controller";
|
||||
private readonly MainWindow _mainWindow;
|
||||
|
||||
private PlayerIndex _playerId;
|
||||
private int _controller;
|
||||
private int _controllerNumber = 0;
|
||||
private string _controllerImage;
|
||||
private int _device;
|
||||
private object _configuration;
|
||||
private string _profileName;
|
||||
private bool _isLoaded;
|
||||
private readonly UserControl _owner;
|
||||
|
||||
public IGamepadDriver AvaloniaKeyboardDriver { get; }
|
||||
public IGamepad SelectedGamepad { get; private set; }
|
||||
|
||||
public ObservableCollection<PlayerModel> PlayerIndexes { get; set; }
|
||||
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
|
||||
internal ObservableCollection<ControllerModel> Controllers { get; set; }
|
||||
public AvaloniaList<string> ProfilesList { get; set; }
|
||||
public AvaloniaList<string> DeviceList { get; set; }
|
||||
|
||||
// XAML Flags
|
||||
public bool ShowSettings => _device > 0;
|
||||
public bool IsController => _device > 1;
|
||||
public bool IsKeyboard => !IsController;
|
||||
public bool IsRight { get; set; }
|
||||
public bool IsLeft { get; set; }
|
||||
|
||||
public bool IsModified { get; set; }
|
||||
|
||||
public object Configuration
|
||||
{
|
||||
get => _configuration;
|
||||
set
|
||||
{
|
||||
_configuration = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerIndex PlayerId
|
||||
{
|
||||
get => _playerId;
|
||||
set
|
||||
{
|
||||
if (IsModified)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsModified = false;
|
||||
_playerId = value;
|
||||
|
||||
if (!Enum.IsDefined(typeof(PlayerIndex), _playerId))
|
||||
{
|
||||
_playerId = PlayerIndex.Player1;
|
||||
}
|
||||
|
||||
LoadConfiguration();
|
||||
LoadDevice();
|
||||
LoadProfiles();
|
||||
|
||||
_isLoaded = true;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int Controller
|
||||
{
|
||||
get => _controller;
|
||||
set
|
||||
{
|
||||
_controller = value;
|
||||
|
||||
if (_controller == -1)
|
||||
{
|
||||
_controller = 0;
|
||||
}
|
||||
|
||||
if (Controllers.Count > 0 && value < Controllers.Count && _controller > -1)
|
||||
{
|
||||
ControllerType controller = Controllers[_controller].Type;
|
||||
|
||||
IsLeft = true;
|
||||
IsRight = true;
|
||||
|
||||
switch (controller)
|
||||
{
|
||||
case ControllerType.Handheld:
|
||||
ControllerImage = JoyConPairResource;
|
||||
break;
|
||||
case ControllerType.ProController:
|
||||
ControllerImage = ProControllerResource;
|
||||
break;
|
||||
case ControllerType.JoyconPair:
|
||||
ControllerImage = JoyConPairResource;
|
||||
break;
|
||||
case ControllerType.JoyconLeft:
|
||||
ControllerImage = JoyConLeftResource;
|
||||
IsRight = false;
|
||||
break;
|
||||
case ControllerType.JoyconRight:
|
||||
ControllerImage = JoyConRightResource;
|
||||
IsLeft = false;
|
||||
break;
|
||||
}
|
||||
|
||||
LoadInputDriver();
|
||||
LoadProfiles();
|
||||
}
|
||||
|
||||
OnPropertyChanged();
|
||||
NotifyChanges();
|
||||
}
|
||||
}
|
||||
|
||||
public string ControllerImage
|
||||
{
|
||||
get => _controllerImage;
|
||||
set
|
||||
{
|
||||
_controllerImage = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(Image));
|
||||
}
|
||||
}
|
||||
|
||||
public SvgImage Image
|
||||
{
|
||||
get
|
||||
{
|
||||
SvgImage image = new SvgImage();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_controllerImage))
|
||||
{
|
||||
SvgSource source = new SvgSource();
|
||||
|
||||
source.Load(EmbeddedResources.GetStream(_controllerImage));
|
||||
|
||||
image.Source = source;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
public string ProfileName
|
||||
{
|
||||
get => _profileName; set
|
||||
{
|
||||
_profileName = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int Device
|
||||
{
|
||||
get => _device;
|
||||
set
|
||||
{
|
||||
_device = value < 0 ? 0 : value;
|
||||
|
||||
if (_device >= Devices.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var selected = Devices[_device].Type;
|
||||
|
||||
if (selected != DeviceType.None)
|
||||
{
|
||||
LoadControllers();
|
||||
|
||||
if (_isLoaded)
|
||||
{
|
||||
LoadConfiguration(LoadDefaultConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
OnPropertyChanged();
|
||||
NotifyChanges();
|
||||
}
|
||||
}
|
||||
|
||||
public InputConfig Config { get; set; }
|
||||
|
||||
public ControllerSettingsViewModel(UserControl owner) : this()
|
||||
{
|
||||
_owner = owner;
|
||||
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
_mainWindow =
|
||||
(MainWindow)((IClassicDesktopStyleApplicationLifetime)Avalonia.Application.Current
|
||||
.ApplicationLifetime).MainWindow;
|
||||
|
||||
AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(owner);
|
||||
|
||||
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
|
||||
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
|
||||
if (_mainWindow.AppHost != null)
|
||||
{
|
||||
_mainWindow.AppHost.NpadManager.BlockInputUpdates();
|
||||
}
|
||||
|
||||
_isLoaded = false;
|
||||
|
||||
LoadDevices();
|
||||
|
||||
PlayerId = PlayerIndex.Player1;
|
||||
}
|
||||
}
|
||||
|
||||
public ControllerSettingsViewModel()
|
||||
{
|
||||
PlayerIndexes = new ObservableCollection<PlayerModel>();
|
||||
Controllers = new ObservableCollection<ControllerModel>();
|
||||
Devices = new ObservableCollection<(DeviceType Type, string Id, string Name)>();
|
||||
ProfilesList = new AvaloniaList<string>();
|
||||
DeviceList = new AvaloniaList<string>();
|
||||
|
||||
ControllerImage = ProControllerResource;
|
||||
|
||||
PlayerIndexes.Add(new(PlayerIndex.Player1, LocaleManager.Instance["ControllerSettingsPlayer1"]));
|
||||
PlayerIndexes.Add(new(PlayerIndex.Player2, LocaleManager.Instance["ControllerSettingsPlayer2"]));
|
||||
PlayerIndexes.Add(new(PlayerIndex.Player3, LocaleManager.Instance["ControllerSettingsPlayer3"]));
|
||||
PlayerIndexes.Add(new(PlayerIndex.Player4, LocaleManager.Instance["ControllerSettingsPlayer4"]));
|
||||
PlayerIndexes.Add(new(PlayerIndex.Player5, LocaleManager.Instance["ControllerSettingsPlayer5"]));
|
||||
PlayerIndexes.Add(new(PlayerIndex.Player6, LocaleManager.Instance["ControllerSettingsPlayer6"]));
|
||||
PlayerIndexes.Add(new(PlayerIndex.Player7, LocaleManager.Instance["ControllerSettingsPlayer7"]));
|
||||
PlayerIndexes.Add(new(PlayerIndex.Player8, LocaleManager.Instance["ControllerSettingsPlayer8"]));
|
||||
PlayerIndexes.Add(new(PlayerIndex.Handheld, LocaleManager.Instance["ControllerSettingsHandheld"]));
|
||||
}
|
||||
|
||||
private void LoadConfiguration(InputConfig inputConfig = null)
|
||||
{
|
||||
Config = inputConfig ?? ConfigurationState.Instance.Hid.InputConfig.Value.Find(inputConfig => inputConfig.PlayerIndex == _playerId);
|
||||
|
||||
if (Config is StandardKeyboardInputConfig keyboardInputConfig)
|
||||
{
|
||||
Configuration = new InputConfiguration<Key, ConfigStickInputId>(keyboardInputConfig);
|
||||
}
|
||||
|
||||
if (Config is StandardControllerInputConfig controllerInputConfig)
|
||||
{
|
||||
Configuration = new InputConfiguration<ConfigGamepadInputId, ConfigStickInputId>(controllerInputConfig);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadDevice()
|
||||
{
|
||||
if (Config == null || Config.Backend == InputBackendType.Invalid)
|
||||
{
|
||||
Device = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var type = DeviceType.None;
|
||||
|
||||
if (Config is StandardKeyboardInputConfig)
|
||||
{
|
||||
type = DeviceType.Keyboard;
|
||||
}
|
||||
|
||||
if (Config is StandardControllerInputConfig)
|
||||
{
|
||||
type = DeviceType.Controller;
|
||||
}
|
||||
|
||||
var item = Devices.FirstOrDefault(x => x.Type == type && x.Id == Config.Id);
|
||||
if (item != default)
|
||||
{
|
||||
Device = Devices.ToList().FindIndex(x => x.Id == item.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
Device = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async void ShowMotionConfig()
|
||||
{
|
||||
await MotionSettingsWindow.Show(this);
|
||||
}
|
||||
|
||||
public async void ShowRumbleConfig()
|
||||
{
|
||||
await RumbleSettingsWindow.Show(this);
|
||||
}
|
||||
|
||||
private void LoadInputDriver()
|
||||
{
|
||||
if (_device < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string id = GetCurrentGamepadId();
|
||||
var type = Devices[Device].Type;
|
||||
|
||||
if (type == DeviceType.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (type == DeviceType.Keyboard)
|
||||
{
|
||||
if (_mainWindow.InputManager.KeyboardDriver is AvaloniaKeyboardDriver)
|
||||
{
|
||||
// NOTE: To get input in this window, we need to bind a custom keyboard driver instead of using the InputManager one as the main window isn't focused...
|
||||
SelectedGamepad = AvaloniaKeyboardDriver.GetGamepad(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedGamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedGamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleOnGamepadDisconnected(string id)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
LoadDevices();
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleOnGamepadConnected(string id)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
LoadDevices();
|
||||
});
|
||||
}
|
||||
|
||||
private string GetCurrentGamepadId()
|
||||
{
|
||||
if (_device < 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var device = Devices[Device];
|
||||
|
||||
if (device.Type == DeviceType.None)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return device.Id.Split(" ")[0];
|
||||
}
|
||||
|
||||
public void LoadControllers()
|
||||
{
|
||||
Controllers.Clear();
|
||||
|
||||
if (_playerId == PlayerIndex.Handheld)
|
||||
{
|
||||
Controllers.Add(new(ControllerType.Handheld, LocaleManager.Instance["ControllerSettingsControllerTypeHandheld"]));
|
||||
|
||||
Controller = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Controllers.Add(new(ControllerType.ProController, LocaleManager.Instance["ControllerSettingsControllerTypeProController"]));
|
||||
Controllers.Add(new(ControllerType.JoyconPair, LocaleManager.Instance["ControllerSettingsControllerTypeJoyConPair"]));
|
||||
Controllers.Add(new(ControllerType.JoyconLeft, LocaleManager.Instance["ControllerSettingsControllerTypeJoyConLeft"]));
|
||||
Controllers.Add(new(ControllerType.JoyconRight, LocaleManager.Instance["ControllerSettingsControllerTypeJoyConRight"]));
|
||||
|
||||
if (Config != null && Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType) != -1)
|
||||
{
|
||||
Controller = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType);
|
||||
}
|
||||
else
|
||||
{
|
||||
Controller = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetShortGamepadName(string str)
|
||||
{
|
||||
const string Ellipsis = "...";
|
||||
const int MaxSize = 50;
|
||||
|
||||
if (str.Length > MaxSize)
|
||||
{
|
||||
return str.Substring(0, MaxSize - Ellipsis.Length) + Ellipsis;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
private static string GetShortGamepadId(string str)
|
||||
{
|
||||
const string Hyphen = "-";
|
||||
const int Offset = 1;
|
||||
|
||||
return str.Substring(str.IndexOf(Hyphen) + Offset);
|
||||
}
|
||||
|
||||
public void LoadDevices()
|
||||
{
|
||||
lock (Devices)
|
||||
{
|
||||
Devices.Clear();
|
||||
DeviceList.Clear();
|
||||
Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance["ControllerSettingsDeviceDisabled"]));
|
||||
|
||||
foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds)
|
||||
{
|
||||
using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
|
||||
|
||||
if (gamepad != null)
|
||||
{
|
||||
Devices.Add((DeviceType.Keyboard, id, $"{GetShortGamepadName(gamepad.Name)}"));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string id in _mainWindow.InputManager.GamepadDriver.GamepadsIds)
|
||||
{
|
||||
using IGamepad gamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id);
|
||||
|
||||
if (gamepad != null)
|
||||
{
|
||||
if (Devices.Any(controller => GetShortGamepadId(controller.Id) == GetShortGamepadId(gamepad.Id)))
|
||||
{
|
||||
_controllerNumber++;
|
||||
}
|
||||
|
||||
Devices.Add((DeviceType.Controller, id, $"{GetShortGamepadName(gamepad.Name)} ({_controllerNumber})"));
|
||||
}
|
||||
}
|
||||
|
||||
_controllerNumber = 0;
|
||||
|
||||
DeviceList.AddRange(Devices.Select(x => x.Name));
|
||||
Device = Math.Min(Device, DeviceList.Count);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetProfileBasePath()
|
||||
{
|
||||
string path = AppDataManager.ProfilesDirPath;
|
||||
var type = Devices[Device == -1 ? 0 : Device].Type;
|
||||
|
||||
if (type == DeviceType.Keyboard)
|
||||
{
|
||||
path = Path.Combine(path, KeyboardString);
|
||||
}
|
||||
else if (type == DeviceType.Controller)
|
||||
{
|
||||
path = Path.Combine(path, ControllerString);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private void LoadProfiles()
|
||||
{
|
||||
ProfilesList.Clear();
|
||||
|
||||
string basePath = GetProfileBasePath();
|
||||
|
||||
if (!Directory.Exists(basePath))
|
||||
{
|
||||
Directory.CreateDirectory(basePath);
|
||||
}
|
||||
|
||||
ProfilesList.Add((LocaleManager.Instance["ControllerSettingsProfileDefault"]));
|
||||
|
||||
foreach (string profile in Directory.GetFiles(basePath, "*.json", SearchOption.AllDirectories))
|
||||
{
|
||||
ProfilesList.Add(Path.GetFileNameWithoutExtension(profile));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ProfileName))
|
||||
{
|
||||
ProfileName = LocaleManager.Instance["ControllerSettingsProfileDefault"];
|
||||
}
|
||||
}
|
||||
|
||||
public InputConfig LoadDefaultConfiguration()
|
||||
{
|
||||
var activeDevice = Devices.FirstOrDefault();
|
||||
|
||||
if (Devices.Count > 0 && Device < Devices.Count && Device >= 0)
|
||||
{
|
||||
activeDevice = Devices[Device];
|
||||
}
|
||||
|
||||
InputConfig config;
|
||||
if (activeDevice.Type == DeviceType.Keyboard)
|
||||
{
|
||||
string id = activeDevice.Id;
|
||||
|
||||
config = new StandardKeyboardInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.WindowKeyboard,
|
||||
Id = id,
|
||||
ControllerType = ControllerType.ProController,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
||||
{
|
||||
DpadUp = Key.Up,
|
||||
DpadDown = Key.Down,
|
||||
DpadLeft = Key.Left,
|
||||
DpadRight = Key.Right,
|
||||
ButtonMinus = Key.Minus,
|
||||
ButtonL = Key.E,
|
||||
ButtonZl = Key.Q,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound
|
||||
},
|
||||
LeftJoyconStick =
|
||||
new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.W,
|
||||
StickDown = Key.S,
|
||||
StickLeft = Key.A,
|
||||
StickRight = Key.D,
|
||||
StickButton = Key.F
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
||||
{
|
||||
ButtonA = Key.Z,
|
||||
ButtonB = Key.X,
|
||||
ButtonX = Key.C,
|
||||
ButtonY = Key.V,
|
||||
ButtonPlus = Key.Plus,
|
||||
ButtonR = Key.U,
|
||||
ButtonZr = Key.O,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.I,
|
||||
StickDown = Key.K,
|
||||
StickLeft = Key.J,
|
||||
StickRight = Key.L,
|
||||
StickButton = Key.H
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (activeDevice.Type == DeviceType.Controller)
|
||||
{
|
||||
bool isNintendoStyle = Devices.ToList().Find(x => x.Id == activeDevice.Id).Name.Contains("Nintendo");
|
||||
|
||||
string id = activeDevice.Id.Split(" ")[0];
|
||||
|
||||
config = new StandardControllerInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.GamepadSDL2,
|
||||
Id = id,
|
||||
ControllerType = ControllerType.ProController,
|
||||
DeadzoneLeft = 0.1f,
|
||||
DeadzoneRight = 0.1f,
|
||||
RangeLeft = 1.0f,
|
||||
RangeRight = 1.0f,
|
||||
TriggerThreshold = 0.5f,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
DpadUp = ConfigGamepadInputId.DpadUp,
|
||||
DpadDown = ConfigGamepadInputId.DpadDown,
|
||||
DpadLeft = ConfigGamepadInputId.DpadLeft,
|
||||
DpadRight = ConfigGamepadInputId.DpadRight,
|
||||
ButtonMinus = ConfigGamepadInputId.Minus,
|
||||
ButtonL = ConfigGamepadInputId.LeftShoulder,
|
||||
ButtonZl = ConfigGamepadInputId.LeftTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.Unbound,
|
||||
ButtonSr = ConfigGamepadInputId.Unbound
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Left,
|
||||
StickButton = ConfigGamepadInputId.LeftStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
|
||||
ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
|
||||
ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
|
||||
ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
|
||||
ButtonPlus = ConfigGamepadInputId.Plus,
|
||||
ButtonR = ConfigGamepadInputId.RightShoulder,
|
||||
ButtonZr = ConfigGamepadInputId.RightTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.Unbound,
|
||||
ButtonSr = ConfigGamepadInputId.Unbound
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Right,
|
||||
StickButton = ConfigGamepadInputId.RightStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false
|
||||
},
|
||||
Motion = new StandardMotionConfigController
|
||||
{
|
||||
MotionBackend = MotionInputBackendType.GamepadDriver,
|
||||
EnableMotion = true,
|
||||
Sensitivity = 100,
|
||||
GyroDeadzone = 1
|
||||
},
|
||||
Rumble = new RumbleConfigController
|
||||
{
|
||||
StrongRumble = 1f,
|
||||
WeakRumble = 1f,
|
||||
EnableRumble = false
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
config = new InputConfig();
|
||||
}
|
||||
|
||||
config.PlayerIndex = _playerId;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public async void LoadProfile()
|
||||
{
|
||||
if (Device == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InputConfig config = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ProfileName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ProfileName == LocaleManager.Instance["ControllerSettingsProfileDefault"])
|
||||
{
|
||||
config = LoadDefaultConfiguration();
|
||||
}
|
||||
else
|
||||
{
|
||||
string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json");
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
var index = ProfilesList.IndexOf(ProfileName);
|
||||
if (index != -1)
|
||||
{
|
||||
ProfilesList.RemoveAt(index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (Stream stream = File.OpenRead(path))
|
||||
{
|
||||
config = JsonHelper.Deserialize<InputConfig>(stream);
|
||||
}
|
||||
}
|
||||
catch (JsonException) { }
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Configuration, $"Profile {ProfileName} is incompatible with the current input configuration system.");
|
||||
|
||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogProfileInvalidProfileErrorMessage"], ProfileName));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (config != null)
|
||||
{
|
||||
_isLoaded = false;
|
||||
|
||||
LoadConfiguration(config);
|
||||
|
||||
LoadDevice();
|
||||
|
||||
_isLoaded = true;
|
||||
|
||||
NotifyChanges();
|
||||
}
|
||||
}
|
||||
|
||||
public async void SaveProfile()
|
||||
{
|
||||
if (Device == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Configuration == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ProfileName == LocaleManager.Instance["ControllerSettingsProfileDefault"])
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogProfileDefaultProfileOverwriteErrorMessage"]);
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1;
|
||||
|
||||
if (validFileName)
|
||||
{
|
||||
string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json");
|
||||
|
||||
InputConfig config = null;
|
||||
|
||||
if (IsKeyboard)
|
||||
{
|
||||
config = (Configuration as InputConfiguration<Key, ConfigStickInputId>).GetConfig();
|
||||
}
|
||||
else if (IsController)
|
||||
{
|
||||
config = (Configuration as InputConfiguration<GamepadInputId, ConfigStickInputId>).GetConfig();
|
||||
}
|
||||
|
||||
config.ControllerType = Controllers[_controller].Type;
|
||||
|
||||
string jsonString = JsonHelper.Serialize(config, true);
|
||||
|
||||
await File.WriteAllTextAsync(path, jsonString);
|
||||
|
||||
LoadProfiles();
|
||||
}
|
||||
else
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogProfileInvalidProfileNameErrorMessage"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async void RemoveProfile()
|
||||
{
|
||||
if (Device == 0 || ProfileName == LocaleManager.Instance["ControllerSettingsProfileDefault"] || ProfilesList.IndexOf(ProfileName) == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
LocaleManager.Instance["DialogProfileDeleteProfileTitle"],
|
||||
LocaleManager.Instance["DialogProfileDeleteProfileMessage"],
|
||||
LocaleManager.Instance["InputDialogYes"],
|
||||
LocaleManager.Instance["InputDialogNo"],
|
||||
LocaleManager.Instance["RyujinxConfirm"]);
|
||||
|
||||
if (result == UserResult.Yes)
|
||||
{
|
||||
string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json");
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
LoadProfiles();
|
||||
}
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
IsModified = false;
|
||||
|
||||
List<InputConfig> newConfig = new();
|
||||
|
||||
newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value);
|
||||
|
||||
newConfig.Remove(newConfig.Find(x => x == null));
|
||||
|
||||
if (Device == 0)
|
||||
{
|
||||
newConfig.Remove(newConfig.Find(x => x.PlayerIndex == this.PlayerId));
|
||||
}
|
||||
else
|
||||
{
|
||||
var device = Devices[Device];
|
||||
|
||||
if (device.Type == DeviceType.Keyboard)
|
||||
{
|
||||
var inputConfig = Configuration as InputConfiguration<Key, ConfigStickInputId>;
|
||||
inputConfig.Id = device.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
var inputConfig = Configuration as InputConfiguration<GamepadInputId, ConfigStickInputId>;
|
||||
inputConfig.Id = device.Id.Split(" ")[0];
|
||||
}
|
||||
|
||||
var config = !IsController
|
||||
? (Configuration as InputConfiguration<Key, ConfigStickInputId>).GetConfig()
|
||||
: (Configuration as InputConfiguration<GamepadInputId, ConfigStickInputId>).GetConfig();
|
||||
config.ControllerType = Controllers[_controller].Type;
|
||||
config.PlayerIndex = _playerId;
|
||||
|
||||
int i = newConfig.FindIndex(x => x.PlayerIndex == PlayerId);
|
||||
if (i == -1)
|
||||
{
|
||||
newConfig.Add(config);
|
||||
}
|
||||
else
|
||||
{
|
||||
newConfig[i] = config;
|
||||
}
|
||||
}
|
||||
|
||||
_mainWindow.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
|
||||
|
||||
// Atomically replace and signal input change.
|
||||
// NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event.
|
||||
ConfigurationState.Instance.Hid.InputConfig.Value = newConfig;
|
||||
|
||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||
}
|
||||
|
||||
public void NotifyChange(string property)
|
||||
{
|
||||
OnPropertyChanged(property);
|
||||
}
|
||||
|
||||
public void NotifyChanges()
|
||||
{
|
||||
OnPropertyChanged(nameof(Configuration));
|
||||
OnPropertyChanged(nameof(IsController));
|
||||
OnPropertyChanged(nameof(ShowSettings));
|
||||
OnPropertyChanged(nameof(IsKeyboard));
|
||||
OnPropertyChanged(nameof(IsRight));
|
||||
OnPropertyChanged(nameof(IsLeft));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
|
||||
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
|
||||
|
||||
_mainWindow.AppHost?.NpadManager.UnblockInputUpdates();
|
||||
|
||||
SelectedGamepad?.Dispose();
|
||||
|
||||
AvaloniaKeyboardDriver.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
1540
Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
Normal file
1540
Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
Normal file
File diff suppressed because it is too large
Load diff
516
Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs
Normal file
516
Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs
Normal file
|
@ -0,0 +1,516 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using DynamicData;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using Ryujinx.Audio.Backends.OpenAL;
|
||||
using Ryujinx.Audio.Backends.SDL2;
|
||||
using Ryujinx.Audio.Backends.SoundIo;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Input;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.GraphicsDriver;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.Vulkan;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS.Services.Time.TimeZone;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Common.Configuration.System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
internal class SettingsViewModel : BaseModel
|
||||
{
|
||||
private readonly VirtualFileSystem _virtualFileSystem;
|
||||
private readonly ContentManager _contentManager;
|
||||
private readonly StyleableWindow _owner;
|
||||
private TimeZoneContentManager _timeZoneContentManager;
|
||||
|
||||
private readonly List<string> _validTzRegions;
|
||||
|
||||
private float _customResolutionScale;
|
||||
private int _resolutionScale;
|
||||
private int _graphicsBackendMultithreadingIndex;
|
||||
private float _volume;
|
||||
private bool _isVulkanAvailable = true;
|
||||
private bool _directoryChanged = false;
|
||||
private List<string> _gpuIds = new List<string>();
|
||||
private KeyboardHotkeys _keyboardHotkeys;
|
||||
private int _graphicsBackendIndex;
|
||||
|
||||
public int ResolutionScale
|
||||
{
|
||||
get => _resolutionScale;
|
||||
set
|
||||
{
|
||||
_resolutionScale = value;
|
||||
|
||||
OnPropertyChanged(nameof(CustomResolutionScale));
|
||||
OnPropertyChanged(nameof(IsCustomResolutionScaleActive));
|
||||
}
|
||||
}
|
||||
public int GraphicsBackendMultithreadingIndex
|
||||
{
|
||||
get => _graphicsBackendMultithreadingIndex;
|
||||
set
|
||||
{
|
||||
_graphicsBackendMultithreadingIndex = value;
|
||||
|
||||
if (_owner != null)
|
||||
{
|
||||
if (_graphicsBackendMultithreadingIndex != (int)ConfigurationState.Instance.Graphics.BackendThreading.Value)
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance["DialogSettingsBackendThreadingWarningMessage"],
|
||||
"",
|
||||
"",
|
||||
LocaleManager.Instance["InputDialogOk"],
|
||||
LocaleManager.Instance["DialogSettingsBackendThreadingWarningTitle"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public float CustomResolutionScale
|
||||
{
|
||||
get => _customResolutionScale;
|
||||
set
|
||||
{
|
||||
_customResolutionScale = MathF.Round(value, 1);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsVulkanAvailable
|
||||
{
|
||||
get => _isVulkanAvailable;
|
||||
set
|
||||
{
|
||||
_isVulkanAvailable = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsOpenGLAvailable => !OperatingSystem.IsMacOS();
|
||||
|
||||
public bool DirectoryChanged
|
||||
{
|
||||
get => _directoryChanged;
|
||||
set
|
||||
{
|
||||
_directoryChanged = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsMacOS
|
||||
{
|
||||
get => OperatingSystem.IsMacOS();
|
||||
}
|
||||
|
||||
public bool EnableDiscordIntegration { get; set; }
|
||||
public bool CheckUpdatesOnStart { get; set; }
|
||||
public bool ShowConfirmExit { get; set; }
|
||||
public bool HideCursorOnIdle { get; set; }
|
||||
public bool EnableDockedMode { get; set; }
|
||||
public bool EnableKeyboard { get; set; }
|
||||
public bool EnableMouse { get; set; }
|
||||
public bool EnableVsync { get; set; }
|
||||
public bool EnablePptc { get; set; }
|
||||
public bool EnableInternetAccess { get; set; }
|
||||
public bool EnableFsIntegrityChecks { get; set; }
|
||||
public bool IgnoreMissingServices { get; set; }
|
||||
public bool ExpandDramSize { get; set; }
|
||||
public bool EnableShaderCache { get; set; }
|
||||
public bool EnableTextureRecompression { get; set; }
|
||||
public bool EnableMacroHLE { get; set; }
|
||||
public bool EnableFileLog { get; set; }
|
||||
public bool EnableStub { get; set; }
|
||||
public bool EnableInfo { get; set; }
|
||||
public bool EnableWarn { get; set; }
|
||||
public bool EnableError { get; set; }
|
||||
public bool EnableTrace { get; set; }
|
||||
public bool EnableGuest { get; set; }
|
||||
public bool EnableFsAccessLog { get; set; }
|
||||
public bool EnableDebug { get; set; }
|
||||
public bool IsOpenAlEnabled { get; set; }
|
||||
public bool IsSoundIoEnabled { get; set; }
|
||||
public bool IsSDL2Enabled { get; set; }
|
||||
public bool EnableCustomTheme { get; set; }
|
||||
public bool IsCustomResolutionScaleActive => _resolutionScale == 0;
|
||||
public bool IsVulkanSelected => GraphicsBackendIndex == 0;
|
||||
|
||||
public string TimeZone { get; set; }
|
||||
public string ShaderDumpPath { get; set; }
|
||||
public string CustomThemePath { get; set; }
|
||||
|
||||
public int Language { get; set; }
|
||||
public int Region { get; set; }
|
||||
public int FsGlobalAccessLogMode { get; set; }
|
||||
public int AudioBackend { get; set; }
|
||||
public int MaxAnisotropy { get; set; }
|
||||
public int AspectRatio { get; set; }
|
||||
public int OpenglDebugLevel { get; set; }
|
||||
public int MemoryMode { get; set; }
|
||||
public int BaseStyleIndex { get; set; }
|
||||
public int GraphicsBackendIndex
|
||||
{
|
||||
get => _graphicsBackendIndex;
|
||||
set
|
||||
{
|
||||
_graphicsBackendIndex = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsVulkanSelected));
|
||||
}
|
||||
}
|
||||
|
||||
public int PreferredGpuIndex { get; set; }
|
||||
|
||||
public float Volume
|
||||
{
|
||||
get => _volume;
|
||||
set
|
||||
{
|
||||
_volume = value;
|
||||
|
||||
ConfigurationState.Instance.System.AudioVolume.Value = (float)(_volume / 100);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset DateOffset { get; set; }
|
||||
public TimeSpan TimeOffset { get; set; }
|
||||
public AvaloniaList<TimeZone> TimeZones { get; set; }
|
||||
public AvaloniaList<string> GameDirectories { get; set; }
|
||||
public ObservableCollection<ComboBoxItem> AvailableGpus { get; set; }
|
||||
|
||||
public KeyboardHotkeys KeyboardHotkeys
|
||||
{
|
||||
get => _keyboardHotkeys;
|
||||
set
|
||||
{
|
||||
_keyboardHotkeys = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public IGamepadDriver AvaloniaKeyboardDriver { get; }
|
||||
|
||||
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager, StyleableWindow owner) : this()
|
||||
{
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_contentManager = contentManager;
|
||||
_owner = owner;
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
LoadTimeZones();
|
||||
AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(owner);
|
||||
}
|
||||
}
|
||||
|
||||
public SettingsViewModel()
|
||||
{
|
||||
GameDirectories = new AvaloniaList<string>();
|
||||
TimeZones = new AvaloniaList<TimeZone>();
|
||||
AvailableGpus = new ObservableCollection<ComboBoxItem>();
|
||||
_validTzRegions = new List<string>();
|
||||
|
||||
CheckSoundBackends();
|
||||
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
LoadAvailableGpus();
|
||||
LoadCurrentConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
public void CheckSoundBackends()
|
||||
{
|
||||
IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported;
|
||||
IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported;
|
||||
IsSDL2Enabled = SDL2HardwareDeviceDriver.IsSupported;
|
||||
}
|
||||
|
||||
private unsafe void LoadAvailableGpus()
|
||||
{
|
||||
_gpuIds = new List<string>();
|
||||
List<string> names = new List<string>();
|
||||
var devices = VulkanRenderer.GetPhysicalDevices();
|
||||
|
||||
if (devices.Length == 0)
|
||||
{
|
||||
IsVulkanAvailable = false;
|
||||
GraphicsBackendIndex = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var device in devices)
|
||||
{
|
||||
_gpuIds.Add(device.Id);
|
||||
names.Add($"{device.Name} {(device.IsDiscrete ? "(dGPU)" : "")}");
|
||||
}
|
||||
}
|
||||
|
||||
AvailableGpus.Clear();
|
||||
AvailableGpus.AddRange(names.Select(x => new ComboBoxItem() { Content = x }));
|
||||
}
|
||||
|
||||
public void LoadTimeZones()
|
||||
{
|
||||
_timeZoneContentManager = new TimeZoneContentManager();
|
||||
|
||||
_timeZoneContentManager.InitializeInstance(_virtualFileSystem, _contentManager, IntegrityCheckLevel.None);
|
||||
|
||||
foreach ((int offset, string location, string abbr) in _timeZoneContentManager.ParseTzOffsets())
|
||||
{
|
||||
int hours = Math.DivRem(offset, 3600, out int seconds);
|
||||
int minutes = Math.Abs(seconds) / 60;
|
||||
|
||||
string abbr2 = abbr.StartsWith('+') || abbr.StartsWith('-') ? string.Empty : abbr;
|
||||
|
||||
TimeZones.Add(new TimeZone($"UTC{hours:+0#;-0#;+00}:{minutes:D2}", location, abbr2));
|
||||
|
||||
_validTzRegions.Add(location);
|
||||
}
|
||||
}
|
||||
|
||||
public void ValidateAndSetTimeZone(string location)
|
||||
{
|
||||
if (_validTzRegions.Contains(location))
|
||||
{
|
||||
TimeZone = location;
|
||||
}
|
||||
}
|
||||
|
||||
public async void BrowseTheme()
|
||||
{
|
||||
var dialog = new OpenFileDialog()
|
||||
{
|
||||
Title = LocaleManager.Instance["SettingsSelectThemeFileDialogTitle"],
|
||||
AllowMultiple = false
|
||||
};
|
||||
|
||||
dialog.Filters.Add(new FileDialogFilter() { Extensions = { "xaml" }, Name = LocaleManager.Instance["SettingsXamlThemeFile"] });
|
||||
|
||||
var file = await dialog.ShowAsync(_owner);
|
||||
|
||||
if (file != null && file.Length > 0)
|
||||
{
|
||||
CustomThemePath = file[0];
|
||||
OnPropertyChanged(nameof(CustomThemePath));
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadCurrentConfiguration()
|
||||
{
|
||||
ConfigurationState config = ConfigurationState.Instance;
|
||||
|
||||
GameDirectories.Clear();
|
||||
GameDirectories.AddRange(config.Ui.GameDirs.Value);
|
||||
|
||||
EnableDiscordIntegration = config.EnableDiscordIntegration;
|
||||
CheckUpdatesOnStart = config.CheckUpdatesOnStart;
|
||||
ShowConfirmExit = config.ShowConfirmExit;
|
||||
HideCursorOnIdle = config.HideCursorOnIdle;
|
||||
EnableDockedMode = config.System.EnableDockedMode;
|
||||
EnableKeyboard = config.Hid.EnableKeyboard;
|
||||
EnableMouse = config.Hid.EnableMouse;
|
||||
EnableVsync = config.Graphics.EnableVsync;
|
||||
EnablePptc = config.System.EnablePtc;
|
||||
EnableInternetAccess = config.System.EnableInternetAccess;
|
||||
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
|
||||
IgnoreMissingServices = config.System.IgnoreMissingServices;
|
||||
ExpandDramSize = config.System.ExpandRam;
|
||||
EnableShaderCache = config.Graphics.EnableShaderCache;
|
||||
EnableTextureRecompression = config.Graphics.EnableTextureRecompression;
|
||||
EnableMacroHLE = config.Graphics.EnableMacroHLE;
|
||||
EnableFileLog = config.Logger.EnableFileLog;
|
||||
EnableStub = config.Logger.EnableStub;
|
||||
EnableInfo = config.Logger.EnableInfo;
|
||||
EnableWarn = config.Logger.EnableWarn;
|
||||
EnableError = config.Logger.EnableError;
|
||||
EnableTrace = config.Logger.EnableTrace;
|
||||
EnableGuest = config.Logger.EnableGuest;
|
||||
EnableDebug = config.Logger.EnableDebug;
|
||||
EnableFsAccessLog = config.Logger.EnableFsAccessLog;
|
||||
EnableCustomTheme = config.Ui.EnableCustomTheme;
|
||||
Volume = config.System.AudioVolume * 100;
|
||||
|
||||
GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value;
|
||||
|
||||
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
|
||||
|
||||
TimeZone = config.System.TimeZone;
|
||||
ShaderDumpPath = config.Graphics.ShadersDumpPath;
|
||||
CustomThemePath = config.Ui.CustomThemePath;
|
||||
BaseStyleIndex = config.Ui.BaseStyle == "Light" ? 0 : 1;
|
||||
GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value;
|
||||
|
||||
PreferredGpuIndex = _gpuIds.Contains(config.Graphics.PreferredGpu) ? _gpuIds.IndexOf(config.Graphics.PreferredGpu) : 0;
|
||||
|
||||
Language = (int)config.System.Language.Value;
|
||||
Region = (int)config.System.Region.Value;
|
||||
FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode;
|
||||
AudioBackend = (int)config.System.AudioBackend.Value;
|
||||
MemoryMode = (int)config.System.MemoryManagerMode.Value;
|
||||
|
||||
float anisotropy = config.Graphics.MaxAnisotropy;
|
||||
|
||||
MaxAnisotropy = anisotropy == -1 ? 0 : (int)(MathF.Log2(anisotropy));
|
||||
AspectRatio = (int)config.Graphics.AspectRatio.Value;
|
||||
|
||||
int resolution = config.Graphics.ResScale;
|
||||
|
||||
ResolutionScale = resolution == -1 ? 0 : resolution;
|
||||
CustomResolutionScale = config.Graphics.ResScaleCustom;
|
||||
|
||||
DateTime dateTimeOffset = DateTime.Now.AddSeconds(config.System.SystemTimeOffset);
|
||||
|
||||
DateOffset = dateTimeOffset.Date;
|
||||
TimeOffset = dateTimeOffset.TimeOfDay;
|
||||
|
||||
KeyboardHotkeys = config.Hid.Hotkeys.Value;
|
||||
}
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
ConfigurationState config = ConfigurationState.Instance;
|
||||
|
||||
if (_directoryChanged)
|
||||
{
|
||||
List<string> gameDirs = new List<string>(GameDirectories);
|
||||
config.Ui.GameDirs.Value = gameDirs;
|
||||
}
|
||||
|
||||
if (_validTzRegions.Contains(TimeZone))
|
||||
{
|
||||
config.System.TimeZone.Value = TimeZone;
|
||||
}
|
||||
|
||||
config.Logger.EnableError.Value = EnableError;
|
||||
config.Logger.EnableTrace.Value = EnableTrace;
|
||||
config.Logger.EnableWarn.Value = EnableWarn;
|
||||
config.Logger.EnableInfo.Value = EnableInfo;
|
||||
config.Logger.EnableStub.Value = EnableStub;
|
||||
config.Logger.EnableDebug.Value = EnableDebug;
|
||||
config.Logger.EnableGuest.Value = EnableGuest;
|
||||
config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog;
|
||||
config.Logger.EnableFileLog.Value = EnableFileLog;
|
||||
config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;
|
||||
config.System.EnableDockedMode.Value = EnableDockedMode;
|
||||
config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
|
||||
config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
|
||||
config.ShowConfirmExit.Value = ShowConfirmExit;
|
||||
config.HideCursorOnIdle.Value = HideCursorOnIdle;
|
||||
config.Graphics.EnableVsync.Value = EnableVsync;
|
||||
config.Graphics.EnableShaderCache.Value = EnableShaderCache;
|
||||
config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression;
|
||||
config.Graphics.EnableMacroHLE.Value = EnableMacroHLE;
|
||||
config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex;
|
||||
config.System.EnablePtc.Value = EnablePptc;
|
||||
config.System.EnableInternetAccess.Value = EnableInternetAccess;
|
||||
config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks;
|
||||
config.System.IgnoreMissingServices.Value = IgnoreMissingServices;
|
||||
config.System.ExpandRam.Value = ExpandDramSize;
|
||||
config.Hid.EnableKeyboard.Value = EnableKeyboard;
|
||||
config.Hid.EnableMouse.Value = EnableMouse;
|
||||
config.Ui.CustomThemePath.Value = CustomThemePath;
|
||||
config.Ui.EnableCustomTheme.Value = EnableCustomTheme;
|
||||
config.Ui.BaseStyle.Value = BaseStyleIndex == 0 ? "Light" : "Dark";
|
||||
config.System.Language.Value = (Language)Language;
|
||||
config.System.Region.Value = (Region)Region;
|
||||
|
||||
config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex);
|
||||
|
||||
if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex)
|
||||
{
|
||||
DriverUtilities.ToggleOGLThreading(GraphicsBackendMultithreadingIndex == (int)BackendThreading.Off);
|
||||
}
|
||||
|
||||
config.Graphics.BackendThreading.Value = (BackendThreading)GraphicsBackendMultithreadingIndex;
|
||||
|
||||
TimeSpan systemTimeOffset = DateOffset - DateTime.Now;
|
||||
|
||||
config.System.SystemTimeOffset.Value = systemTimeOffset.Seconds;
|
||||
config.Graphics.ShadersDumpPath.Value = ShaderDumpPath;
|
||||
config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode;
|
||||
config.System.MemoryManagerMode.Value = (MemoryManagerMode)MemoryMode;
|
||||
|
||||
float anisotropy = MaxAnisotropy == 0 ? -1 : MathF.Pow(2, MaxAnisotropy);
|
||||
|
||||
config.Graphics.MaxAnisotropy.Value = anisotropy;
|
||||
config.Graphics.AspectRatio.Value = (AspectRatio)AspectRatio;
|
||||
config.Graphics.ResScale.Value = ResolutionScale == 0 ? -1 : ResolutionScale;
|
||||
config.Graphics.ResScaleCustom.Value = CustomResolutionScale;
|
||||
config.System.AudioVolume.Value = Volume / 100;
|
||||
|
||||
AudioBackend audioBackend = (AudioBackend)AudioBackend;
|
||||
if (audioBackend != config.System.AudioBackend.Value)
|
||||
{
|
||||
config.System.AudioBackend.Value = audioBackend;
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, $"AudioBackend toggled to: {audioBackend}");
|
||||
}
|
||||
|
||||
config.Hid.Hotkeys.Value = KeyboardHotkeys;
|
||||
|
||||
config.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||
|
||||
MainWindow.UpdateGraphicsConfig();
|
||||
|
||||
if (_owner is SettingsWindow owner)
|
||||
{
|
||||
owner.ControllerSettings?.SaveCurrentProfile();
|
||||
}
|
||||
|
||||
if (_owner.Owner is MainWindow window && _directoryChanged)
|
||||
{
|
||||
window.ViewModel.LoadApplications();
|
||||
}
|
||||
|
||||
_directoryChanged = false;
|
||||
}
|
||||
|
||||
public void RevertIfNotSaved()
|
||||
{
|
||||
Program.ReloadConfig();
|
||||
}
|
||||
|
||||
public void ApplyButton()
|
||||
{
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
public void OkButton()
|
||||
{
|
||||
SaveSettings();
|
||||
_owner.Close();
|
||||
}
|
||||
|
||||
public void CancelButton()
|
||||
{
|
||||
RevertIfNotSaved();
|
||||
_owner.Close();
|
||||
}
|
||||
}
|
||||
}
|
215
Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs
Normal file
215
Ryujinx.Ava/UI/ViewModels/UserProfileViewModel.cs
Normal file
|
@ -0,0 +1,215 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Shim;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId;
|
||||
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public class UserProfileViewModel : BaseModel, IDisposable
|
||||
{
|
||||
private readonly NavigationDialogHost _owner;
|
||||
|
||||
private UserProfile _selectedProfile;
|
||||
private UserProfile _highlightedProfile;
|
||||
|
||||
public UserProfileViewModel()
|
||||
{
|
||||
Profiles = new ObservableCollection<UserProfile>();
|
||||
LostProfiles = new ObservableCollection<UserProfile>();
|
||||
}
|
||||
|
||||
public UserProfileViewModel(NavigationDialogHost owner) : this()
|
||||
{
|
||||
_owner = owner;
|
||||
|
||||
LoadProfiles();
|
||||
}
|
||||
|
||||
public ObservableCollection<UserProfile> Profiles { get; set; }
|
||||
|
||||
public ObservableCollection<UserProfile> LostProfiles { get; set; }
|
||||
|
||||
public UserProfile SelectedProfile
|
||||
{
|
||||
get => _selectedProfile;
|
||||
set
|
||||
{
|
||||
_selectedProfile = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsHighlightedProfileDeletable));
|
||||
OnPropertyChanged(nameof(IsHighlightedProfileEditable));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsHighlightedProfileEditable => _highlightedProfile != null;
|
||||
|
||||
public bool IsHighlightedProfileDeletable => _highlightedProfile != null && _highlightedProfile.UserId != AccountManager.DefaultUserId;
|
||||
|
||||
public UserProfile HighlightedProfile
|
||||
{
|
||||
get => _highlightedProfile;
|
||||
set
|
||||
{
|
||||
_highlightedProfile = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsHighlightedProfileDeletable));
|
||||
OnPropertyChanged(nameof(IsHighlightedProfileEditable));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public void LoadProfiles()
|
||||
{
|
||||
Profiles.Clear();
|
||||
LostProfiles.Clear();
|
||||
|
||||
var profiles = _owner.AccountManager.GetAllUsers().OrderByDescending(x => x.AccountState == AccountState.Open);
|
||||
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
Profiles.Add(new UserProfile(profile, _owner));
|
||||
}
|
||||
|
||||
SelectedProfile = Profiles.FirstOrDefault(x => x.UserId == _owner.AccountManager.LastOpenedUser.UserId);
|
||||
|
||||
if (SelectedProfile == null)
|
||||
{
|
||||
SelectedProfile = Profiles.First();
|
||||
|
||||
if (SelectedProfile != null)
|
||||
{
|
||||
_owner.AccountManager.OpenUser(_selectedProfile.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
var saveDataFilter = SaveDataFilter.Make(programId: default, saveType: SaveDataType.Account,
|
||||
default, saveDataId: default, index: default);
|
||||
|
||||
using var saveDataIterator = new UniqueRef<SaveDataIterator>();
|
||||
|
||||
_owner.HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure();
|
||||
|
||||
Span<SaveDataInfo> saveDataInfo = stackalloc SaveDataInfo[10];
|
||||
|
||||
HashSet<UserId> lostAccounts = new HashSet<UserId>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
saveDataIterator.Get.ReadSaveDataInfo(out long readCount, saveDataInfo).ThrowIfFailure();
|
||||
|
||||
if (readCount == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < readCount; i++)
|
||||
{
|
||||
var save = saveDataInfo[i];
|
||||
var id = new UserId((long)save.UserId.Id.Low, (long)save.UserId.Id.High);
|
||||
if (Profiles.FirstOrDefault( x=> x.UserId == id) == null)
|
||||
{
|
||||
lostAccounts.Add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach(var account in lostAccounts)
|
||||
{
|
||||
LostProfiles.Add(new UserProfile(new HLE.HOS.Services.Account.Acc.UserProfile(account, "", null), _owner));
|
||||
}
|
||||
}
|
||||
|
||||
public void AddUser()
|
||||
{
|
||||
UserProfile userProfile = null;
|
||||
|
||||
_owner.Navigate(typeof(UserEditor), (this._owner, userProfile, true));
|
||||
}
|
||||
|
||||
public async void ManageSaves()
|
||||
{
|
||||
UserProfile userProfile = _highlightedProfile ?? SelectedProfile;
|
||||
|
||||
SaveManager manager = new SaveManager(userProfile, _owner.HorizonClient, _owner.VirtualFileSystem);
|
||||
|
||||
ContentDialog contentDialog = new ContentDialog
|
||||
{
|
||||
Title = string.Format(LocaleManager.Instance["SaveManagerHeading"], userProfile.Name),
|
||||
PrimaryButtonText = "",
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = LocaleManager.Instance["UserProfilesClose"],
|
||||
Content = manager,
|
||||
Padding = new Thickness(0)
|
||||
};
|
||||
|
||||
await contentDialog.ShowAsync();
|
||||
}
|
||||
|
||||
public void EditUser()
|
||||
{
|
||||
_owner.Navigate(typeof(UserEditor), (this._owner, _highlightedProfile ?? SelectedProfile, false));
|
||||
}
|
||||
|
||||
public async void DeleteUser()
|
||||
{
|
||||
if (_highlightedProfile != null)
|
||||
{
|
||||
var lastUserId = _owner.AccountManager.LastOpenedUser.UserId;
|
||||
|
||||
if (_highlightedProfile.UserId == lastUserId)
|
||||
{
|
||||
// If we are deleting the currently open profile, then we must open something else before deleting.
|
||||
var profile = Profiles.FirstOrDefault(x => x.UserId != lastUserId);
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogUserProfileDeletionWarningMessage"]);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_owner.AccountManager.OpenUser(profile.UserId);
|
||||
}
|
||||
|
||||
var result =
|
||||
await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance["DialogUserProfileDeletionConfirmMessage"], "",
|
||||
LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"], "");
|
||||
|
||||
if (result == UserResult.Yes)
|
||||
{
|
||||
_owner.AccountManager.DeleteUser(_highlightedProfile.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
LoadProfiles();
|
||||
}
|
||||
|
||||
public void GoBack()
|
||||
{
|
||||
_owner.GoBack();
|
||||
}
|
||||
|
||||
public void RecoverLostAccounts()
|
||||
{
|
||||
_owner.Navigate(typeof(UserRecoverer), (this._owner, this));
|
||||
}
|
||||
}
|
||||
}
|
282
Ryujinx.Ava/UI/Windows/AboutWindow.axaml
Normal file
282
Ryujinx.Ava/UI/Windows/AboutWindow.axaml
Normal file
|
@ -0,0 +1,282 @@
|
|||
<window:StyleableWindow
|
||||
x:Class="Ryujinx.Ava.UI.Windows.AboutWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
Width="850"
|
||||
Height="550"
|
||||
MinWidth="500"
|
||||
MinHeight="550"
|
||||
d:DesignHeight="350"
|
||||
d:DesignWidth="400"
|
||||
CanResize="False"
|
||||
SizeToContent="Width"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<Grid
|
||||
Margin="15"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="20"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="0"
|
||||
Height="110"
|
||||
MinWidth="50"
|
||||
Margin="5,10,20,10"
|
||||
Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="0,20,0,0"
|
||||
FontSize="35"
|
||||
Text="Ryujinx"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="0,0,0,0"
|
||||
FontSize="16"
|
||||
Text="(REE-YOU-JINX)"
|
||||
TextAlignment="Center" />
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://www.ryujinx.org/">
|
||||
<TextBlock
|
||||
Text="www.ryujinx.org"
|
||||
TextAlignment="Center"
|
||||
TextDecorations="Underline"
|
||||
ToolTip.Tip="{locale:Locale AboutUrlTooltipMessage}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Version}"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Margin="20"
|
||||
HorizontalAlignment="Center"
|
||||
MaxLines="2"
|
||||
Text="{locale:Locale AboutDisclaimerMessage}"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
Name="AmiiboLabel"
|
||||
Grid.Row="3"
|
||||
Margin="20"
|
||||
HorizontalAlignment="Center"
|
||||
MaxLines="2"
|
||||
PointerPressed="AmiiboLabel_OnPointerPressed"
|
||||
Text="{locale:Locale AboutAmiiboDisclaimerMessage}"
|
||||
TextAlignment="Center" />
|
||||
<StackPanel
|
||||
Grid.Row="4"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10">
|
||||
<StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutPatreonUrlTooltipMessage}">
|
||||
<Button
|
||||
Height="65"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://www.patreon.com/ryujinx">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_Patreon.png?assembly=Ryujinx.Ui.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="0,5,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="Patreon" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutGithubUrlTooltipMessage}">
|
||||
<Button
|
||||
Height="65"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://github.com/Ryujinx/Ryujinx">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_GitHub.png?assembly=Ryujinx.Ui.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="0,5,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="GitHub" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutDiscordUrlTooltipMessage}">
|
||||
<Button
|
||||
Height="65"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://discordapp.com/invite/N2FmfVc">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_Discord.png?assembly=Ryujinx.Ui.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="0,5,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="Discord" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutTwitterUrlTooltipMessage}">
|
||||
<Button
|
||||
Height="65"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://twitter.com/RyujinxEmu">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_Twitter.png?assembly=Ryujinx.Ui.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="0,5,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="Twitter" />
|
||||
</Grid>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Width="2"
|
||||
Margin="5"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="White"
|
||||
BorderThickness="1,0,0,0">
|
||||
<Separator Width="0" />
|
||||
</Border>
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Margin="20"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
FontWeight="Bold"
|
||||
Text="{locale:Locale AboutRyujinxAboutTitle}"
|
||||
TextDecorations="Underline" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="20,5,5,5"
|
||||
LineHeight="20"
|
||||
Text="{locale:Locale AboutRyujinxAboutContent}" />
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Margin="0,10,0,0"
|
||||
FontWeight="Bold"
|
||||
Text="{locale:Locale AboutRyujinxMaintainersTitle}"
|
||||
TextDecorations="Underline" />
|
||||
<TextBlock
|
||||
Grid.Row="3"
|
||||
Margin="20,5,5,5"
|
||||
LineHeight="20"
|
||||
Text="{Binding Developers}" />
|
||||
<Button
|
||||
Grid.Row="4"
|
||||
HorizontalAlignment="Right"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a">
|
||||
<TextBlock
|
||||
Text="{locale:Locale AboutRyujinxContributorsButtonHeader}"
|
||||
TextAlignment="Right"
|
||||
TextDecorations="Underline"
|
||||
ToolTip.Tip="{locale:Locale AboutRyujinxMaintainersContentTooltipMessage}" />
|
||||
</Button>
|
||||
<TextBlock
|
||||
Grid.Row="5"
|
||||
Margin="0,0,0,0"
|
||||
FontWeight="Bold"
|
||||
Text="{locale:Locale AboutRyujinxSupprtersTitle}"
|
||||
TextDecorations="Underline" />
|
||||
<Border
|
||||
Grid.Row="6"
|
||||
Width="460"
|
||||
Height="200"
|
||||
Margin="20,5"
|
||||
Padding="5"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="White"
|
||||
BorderThickness="1">
|
||||
<TextBlock
|
||||
Name="SupportersTextBlock"
|
||||
VerticalAlignment="Top"
|
||||
Text="{Binding Supporters}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</window:StyleableWindow>
|
78
Ryujinx.Ava/UI/Windows/AboutWindow.axaml.cs
Normal file
78
Ryujinx.Ava/UI/Windows/AboutWindow.axaml.cs
Normal file
|
@ -0,0 +1,78 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System.Net.Http;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class AboutWindow : StyleableWindow
|
||||
{
|
||||
public AboutWindow()
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["MenuBarHelpAbout"];
|
||||
}
|
||||
|
||||
Version = Program.Version;
|
||||
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
_ = DownloadPatronsJson();
|
||||
}
|
||||
|
||||
public string Supporters { get; set; }
|
||||
public string Version { get; set; }
|
||||
|
||||
public string Developers => string.Format(LocaleManager.Instance["AboutPageDeveloperListMore"], "gdkchan, Ac_K, Thog, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, Xpl0itR, GoffyDude, »jD«");
|
||||
|
||||
private void Button_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button)
|
||||
{
|
||||
OpenHelper.OpenUrl(button.Tag.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadPatronsJson()
|
||||
{
|
||||
if (!NetworkInterface.GetIsNetworkAvailable())
|
||||
{
|
||||
Supporters = LocaleManager.Instance["ConnectionError"];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
HttpClient httpClient = new();
|
||||
|
||||
try
|
||||
{
|
||||
string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/");
|
||||
|
||||
Supporters = string.Join(", ", JsonHelper.Deserialize<string[]>(patreonJsonString));
|
||||
}
|
||||
catch
|
||||
{
|
||||
Supporters = LocaleManager.Instance["ApiError"];
|
||||
}
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() => SupportersTextBlock.Text = Supporters);
|
||||
}
|
||||
|
||||
private void AmiiboLabel_OnPointerPressed(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (sender is TextBlock)
|
||||
{
|
||||
OpenHelper.OpenUrl("https://amiiboapi.com");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
74
Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml
Normal file
74
Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml
Normal file
|
@ -0,0 +1,74 @@
|
|||
<window:StyleableWindow
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="400"
|
||||
d:DesignHeight="350"
|
||||
x:Class="Ryujinx.Ava.UI.Windows.AmiiboWindow"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Width="800"
|
||||
MinHeight="650"
|
||||
Height="650"
|
||||
SizeToContent="Manual"
|
||||
MinWidth="600"
|
||||
Focusable="True">
|
||||
<Design.DataContext>
|
||||
<viewModels:AmiiboWindowViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid Margin="15" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="1" HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Spacing="10" Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale AmiiboSeriesLabel}" />
|
||||
<ComboBox SelectedIndex="{Binding SeriesSelectedIndex}" Items="{Binding AmiiboSeries}" MinWidth="100" />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="10" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale AmiiboCharacterLabel}" />
|
||||
<ComboBox SelectedIndex="{Binding AmiiboSelectedIndex}" MinWidth="100" Items="{Binding AmiiboList}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<StackPanel Margin="20" Grid.Row="2">
|
||||
<Image Source="{Binding AmiiboImage}" Height="350" Width="350" HorizontalAlignment="Center" />
|
||||
<ScrollViewer MaxHeight="120" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"
|
||||
Margin="20" VerticalAlignment="Top" HorizontalAlignment="Stretch">
|
||||
<TextBlock TextWrapping="Wrap" Text="{Binding Usage}" HorizontalAlignment="Center"
|
||||
TextAlignment="Center" />
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
<Grid Grid.Row="3">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<CheckBox Margin="10" Grid.Column="0" VerticalContentAlignment="Center" IsChecked="{Binding ShowAllAmiibo}"
|
||||
Content="{locale:Locale AmiiboOptionsShowAllLabel}" />
|
||||
<CheckBox Margin="10" VerticalContentAlignment="Center" Grid.Column="1" IsChecked="{Binding UseRandomUuid}"
|
||||
Content="{locale:Locale AmiiboOptionsUsRandomTagLabel}" />
|
||||
|
||||
<Button Grid.Column="3" IsEnabled="{Binding EnableScanning}" Width="80"
|
||||
Content="{locale:Locale AmiiboScanButtonLabel}" Name="ScanButton"
|
||||
Click="ScanButton_Click" />
|
||||
<Button Grid.Column="4" Margin="10,0" Width="80" Content="{locale:Locale InputDialogCancel}"
|
||||
Name="CancelButton"
|
||||
Click="CancelButton_Click" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</window:StyleableWindow>
|
59
Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs
Normal file
59
Ryujinx.Ava/UI/Windows/AmiiboWindow.axaml.cs
Normal file
|
@ -0,0 +1,59 @@
|
|||
using Avalonia.Interactivity;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class AmiiboWindow : StyleableWindow
|
||||
{
|
||||
public AmiiboWindow(bool showAll, string lastScannedAmiiboId, string titleId)
|
||||
{
|
||||
ViewModel = new AmiiboWindowViewModel(this, lastScannedAmiiboId, titleId);
|
||||
|
||||
ViewModel.ShowAllAmiibo = showAll;
|
||||
|
||||
DataContext = ViewModel;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
||||
}
|
||||
|
||||
public AmiiboWindow()
|
||||
{
|
||||
ViewModel = new AmiiboWindowViewModel(this, string.Empty, string.Empty);
|
||||
|
||||
DataContext = ViewModel;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["Amiibo"];
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsScanned { get; set; }
|
||||
public Amiibo.AmiiboApi ScannedAmiibo { get; set; }
|
||||
public AmiiboWindowViewModel ViewModel { get; set; }
|
||||
|
||||
private void ScanButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ViewModel.AmiiboSelectedIndex > -1)
|
||||
{
|
||||
Amiibo.AmiiboApi amiibo = ViewModel.AmiiboList[ViewModel.AmiiboSelectedIndex];
|
||||
ScannedAmiibo = amiibo;
|
||||
IsScanned = true;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
IsScanned = false;
|
||||
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
54
Ryujinx.Ava/UI/Windows/AvatarWindow.axaml
Normal file
54
Ryujinx.Ava/UI/Windows/AvatarWindow.axaml
Normal file
|
@ -0,0 +1,54 @@
|
|||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
|
||||
x:Class="Ryujinx.Ava.UI.Windows.AvatarWindow"
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:AvatarProfileViewModel"
|
||||
Focusable="True">
|
||||
<Design.DataContext>
|
||||
<viewModels:AvatarProfileViewModel />
|
||||
</Design.DataContext>
|
||||
<UserControl.Resources>
|
||||
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||
</UserControl.Resources>
|
||||
<Grid Margin="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListBox Grid.Row="1" BorderThickness="0" SelectedIndex="{Binding SelectedIndex}" Height="400"
|
||||
Items="{Binding Images}" HorizontalAlignment="Stretch" VerticalAlignment="Center">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal" MaxWidth="700" Margin="0" HorizontalAlignment="Center" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Image Margin="5" Height="96" Width="96"
|
||||
Source="{Binding Data, Converter={StaticResource ByteImage}}" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<ProgressBar Grid.Row="2" IsIndeterminate="{Binding IsIndeterminate}" Value="{Binding ImagesLoaded}" HorizontalAlignment="Stretch" Margin="5"
|
||||
Maximum="{Binding ImageCount}" Minimum="0" />
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="10" Margin="10" HorizontalAlignment="Center">
|
||||
<Button Content="{locale:Locale AvatarChoose}" Width="200" Name="ChooseButton" Click="ChooseButton_OnClick" />
|
||||
<ui:ColorPickerButton Color="{Binding BackgroundColor, Mode=TwoWay}" Name="ColorButton" />
|
||||
<Button HorizontalAlignment="Right" Content="{locale:Locale Discard}" Click="CloseButton_OnClick"
|
||||
Name="CloseButton"
|
||||
Width="200" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
77
Ryujinx.Ava/UI/Windows/AvatarWindow.axaml.cs
Normal file
77
Ryujinx.Ava/UI/Windows/AvatarWindow.axaml.cs
Normal file
|
@ -0,0 +1,77 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class AvatarWindow : UserControl
|
||||
{
|
||||
private NavigationDialogHost _parent;
|
||||
private TempProfile _profile;
|
||||
|
||||
public AvatarWindow(ContentManager contentManager)
|
||||
{
|
||||
ContentManager = contentManager;
|
||||
|
||||
DataContext = ViewModel;
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public AvatarWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||
{
|
||||
NavigatedTo(e);
|
||||
}, RoutingStrategies.Direct);
|
||||
}
|
||||
|
||||
private void NavigatedTo(NavigationEventArgs arg)
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
if (arg.NavigationMode == NavigationMode.New)
|
||||
{
|
||||
(_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
|
||||
ContentManager = _parent.ContentManager;
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
ViewModel = new AvatarProfileViewModel(() => ViewModel.ReloadImages());
|
||||
}
|
||||
|
||||
DataContext = ViewModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ContentManager ContentManager { get; private set; }
|
||||
|
||||
internal AvatarProfileViewModel ViewModel { get; set; }
|
||||
|
||||
private void CloseButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.Dispose();
|
||||
|
||||
_parent.GoBack();
|
||||
}
|
||||
|
||||
private void ChooseButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ViewModel.SelectedIndex > -1)
|
||||
{
|
||||
_profile.Image = ViewModel.SelectedImage;
|
||||
|
||||
ViewModel.Dispose();
|
||||
|
||||
_parent.GoBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
106
Ryujinx.Ava/UI/Windows/CheatWindow.axaml
Normal file
106
Ryujinx.Ava/UI/Windows/CheatWindow.axaml
Normal file
|
@ -0,0 +1,106 @@
|
|||
<window:StyleableWindow
|
||||
x:Class="Ryujinx.Ava.UI.Windows.CheatWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:model="clr-namespace:Ryujinx.Ava.UI.Models"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
Width="500"
|
||||
Height="500"
|
||||
MinWidth="500"
|
||||
MinHeight="500"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<Window.Styles>
|
||||
<Style Selector="TreeViewItem">
|
||||
<Setter Property="IsExpanded" Value="True" />
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
<Grid Name="CheatGrid" Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
MaxWidth="500"
|
||||
Margin="20,15,20,20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
LineHeight="18"
|
||||
Text="{Binding Heading}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1">
|
||||
<TreeView
|
||||
Name="CheatsView"
|
||||
MinHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Items="{Binding LoadedCheats}">
|
||||
<TreeView.Styles>
|
||||
<Styles>
|
||||
<Style Selector="TreeViewItem:empty /template/ ItemsPresenter">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
</Styles>
|
||||
</TreeView.Styles>
|
||||
<TreeView.DataTemplates>
|
||||
<TreeDataTemplate DataType="model:CheatsList" ItemsSource="{Binding}">
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
||||
<CheckBox MinWidth="20" IsChecked="{Binding IsEnabled}" />
|
||||
<TextBlock Width="150" Text="{Binding BuildId}" />
|
||||
<TextBlock Text="{Binding Path}" />
|
||||
</StackPanel>
|
||||
</TreeDataTemplate>
|
||||
<DataTemplate x:DataType="model:CheatModel">
|
||||
<StackPanel
|
||||
Margin="0"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Horizontal">
|
||||
<CheckBox
|
||||
MinWidth="20"
|
||||
Margin="5,0"
|
||||
Padding="0"
|
||||
IsChecked="{Binding IsEnabled}" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding CleanName}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</TreeView.DataTemplates>
|
||||
</TreeView>
|
||||
</Border>
|
||||
<DockPanel
|
||||
Grid.Row="3"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch">
|
||||
<DockPanel Margin="0" HorizontalAlignment="Right">
|
||||
<Button
|
||||
Name="SaveButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Save}"
|
||||
IsVisible="{Binding !NoCheatsFound}">
|
||||
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="CancelButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Close}">
|
||||
<TextBlock Text="{locale:Locale InputDialogCancel}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</window:StyleableWindow>
|
119
Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs
Normal file
119
Ryujinx.Ava/UI/Windows/CheatWindow.axaml.cs
Normal file
|
@ -0,0 +1,119 @@
|
|||
using Avalonia.Collections;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class CheatWindow : StyleableWindow
|
||||
{
|
||||
private readonly string _enabledCheatsPath;
|
||||
public bool NoCheatsFound { get; }
|
||||
|
||||
private AvaloniaList<CheatsList> LoadedCheats { get; }
|
||||
|
||||
public string Heading { get; }
|
||||
|
||||
public CheatWindow()
|
||||
{
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
|
||||
}
|
||||
|
||||
public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName)
|
||||
{
|
||||
LoadedCheats = new AvaloniaList<CheatsList>();
|
||||
|
||||
Heading = string.Format(LocaleManager.Instance["CheatWindowHeading"], titleName, titleId.ToUpper());
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath();
|
||||
string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId);
|
||||
ulong titleIdValue = ulong.Parse(titleId, System.Globalization.NumberStyles.HexNumber);
|
||||
|
||||
_enabledCheatsPath = Path.Combine(titleModsPath, "cheats", "enabled.txt");
|
||||
|
||||
string[] enabled = { };
|
||||
|
||||
if (File.Exists(_enabledCheatsPath))
|
||||
{
|
||||
enabled = File.ReadAllLines(_enabledCheatsPath);
|
||||
}
|
||||
|
||||
int cheatAdded = 0;
|
||||
|
||||
var mods = new ModLoader.ModCache();
|
||||
|
||||
ModLoader.QueryContentsDir(mods, new DirectoryInfo(Path.Combine(modsBasePath, "contents")), titleIdValue);
|
||||
|
||||
string currentCheatFile = string.Empty;
|
||||
string buildId = string.Empty;
|
||||
string parentPath = string.Empty;
|
||||
|
||||
CheatsList currentGroup = null;
|
||||
|
||||
foreach (var cheat in mods.Cheats)
|
||||
{
|
||||
if (cheat.Path.FullName != currentCheatFile)
|
||||
{
|
||||
currentCheatFile = cheat.Path.FullName;
|
||||
parentPath = currentCheatFile.Replace(titleModsPath, "");
|
||||
|
||||
buildId = Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper();
|
||||
currentGroup = new CheatsList(buildId, parentPath);
|
||||
|
||||
LoadedCheats.Add(currentGroup);
|
||||
}
|
||||
|
||||
var model = new CheatModel(cheat.Name, buildId, enabled.Contains($"{buildId}-{cheat.Name}"));
|
||||
currentGroup?.Add(model);
|
||||
|
||||
cheatAdded++;
|
||||
}
|
||||
|
||||
if (cheatAdded == 0)
|
||||
{
|
||||
NoCheatsFound = true;
|
||||
}
|
||||
|
||||
DataContext = this;
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["CheatWindowTitle"];
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
if (NoCheatsFound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<string> enabledCheats = new List<string>();
|
||||
|
||||
foreach (var cheats in LoadedCheats)
|
||||
{
|
||||
foreach (var cheat in cheats)
|
||||
{
|
||||
if (cheat.IsEnabled)
|
||||
{
|
||||
enabledCheats.Add(cheat.BuildIdKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_enabledCheatsPath));
|
||||
|
||||
File.WriteAllLines(_enabledCheatsPath, enabledCheats);
|
||||
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
29
Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml
Normal file
29
Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml
Normal file
|
@ -0,0 +1,29 @@
|
|||
<window:StyleableWindow
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="800"
|
||||
d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.UI.Windows.ContentDialogOverlayWindow"
|
||||
Title="ContentDialogOverlayWindow"
|
||||
Focusable="True">
|
||||
<window:StyleableWindow.Styles>
|
||||
<Style Selector="ui|ContentDialog /template/ Panel#LayoutRoot">
|
||||
<Setter Property="Background"
|
||||
Value="Transparent" />
|
||||
</Style>
|
||||
</window:StyleableWindow.Styles>
|
||||
<ContentControl
|
||||
Focusable="False"
|
||||
IsVisible="False"
|
||||
KeyboardNavigation.IsTabStop="False">
|
||||
<ui:ContentDialog Name="ContentDialog"
|
||||
IsPrimaryButtonEnabled="True"
|
||||
IsSecondaryButtonEnabled="True"
|
||||
IsVisible="False" />
|
||||
</ContentControl>
|
||||
</window:StyleableWindow>
|
25
Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml.cs
Normal file
25
Ryujinx.Ava/UI/Windows/ContentDialogOverlayWindow.axaml.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class ContentDialogOverlayWindow : StyleableWindow
|
||||
{
|
||||
public ContentDialogOverlayWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
ExtendClientAreaToDecorationsHint = true;
|
||||
TransparencyLevelHint = WindowTransparencyLevel.Transparent;
|
||||
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
SystemDecorations = SystemDecorations.None;
|
||||
ExtendClientAreaTitleBarHeightHint = 0;
|
||||
Background = Brushes.Transparent;
|
||||
CanResize = false;
|
||||
}
|
||||
}
|
||||
}
|
1169
Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml
Normal file
1169
Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml
Normal file
File diff suppressed because it is too large
Load diff
181
Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml.cs
Normal file
181
Ryujinx.Ava/UI/Windows/ControllerSettingsWindow.axaml.cs
Normal file
|
@ -0,0 +1,181 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.LogicalTree;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Input.Assigner;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class ControllerSettingsWindow : UserControl
|
||||
{
|
||||
private bool _dialogOpen;
|
||||
|
||||
private ButtonKeyAssigner _currentAssigner;
|
||||
internal ControllerSettingsViewModel ViewModel { get; set; }
|
||||
|
||||
public ControllerSettingsWindow()
|
||||
{
|
||||
DataContext = ViewModel = new ControllerSettingsViewModel(this);
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
foreach (ILogical visual in SettingButtons.GetLogicalDescendants())
|
||||
{
|
||||
if (visual is ToggleButton button && !(visual is CheckBox))
|
||||
{
|
||||
button.Checked += Button_Checked;
|
||||
button.Unchecked += Button_Unchecked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||
{
|
||||
base.OnPointerReleased(e);
|
||||
|
||||
if (_currentAssigner != null && _currentAssigner.ToggledButton != null && !_currentAssigner.ToggledButton.IsPointerOver)
|
||||
{
|
||||
_currentAssigner.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void Button_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ToggleButton button)
|
||||
{
|
||||
if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool isStick = button.Tag != null && button.Tag.ToString() == "stick";
|
||||
|
||||
if (_currentAssigner == null && (bool)button.IsChecked)
|
||||
{
|
||||
_currentAssigner = new ButtonKeyAssigner(button);
|
||||
|
||||
FocusManager.Instance.Focus(this, NavigationMethod.Pointer);
|
||||
|
||||
PointerPressed += MouseClick;
|
||||
|
||||
IKeyboard keyboard = (IKeyboard)ViewModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations.
|
||||
IButtonAssigner assigner = CreateButtonAssigner(isStick);
|
||||
|
||||
_currentAssigner.ButtonAssigned += (sender, e) =>
|
||||
{
|
||||
if (e.IsAssigned)
|
||||
{
|
||||
ViewModel.IsModified = true;
|
||||
}
|
||||
};
|
||||
|
||||
_currentAssigner.GetInputAndAssign(assigner, keyboard);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_currentAssigner != null)
|
||||
{
|
||||
ToggleButton oldButton = _currentAssigner.ToggledButton;
|
||||
|
||||
_currentAssigner.Cancel();
|
||||
_currentAssigner = null;
|
||||
button.IsChecked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveCurrentProfile()
|
||||
{
|
||||
ViewModel.Save();
|
||||
}
|
||||
|
||||
private IButtonAssigner CreateButtonAssigner(bool forStick)
|
||||
{
|
||||
IButtonAssigner assigner;
|
||||
|
||||
var device = ViewModel.Devices[ViewModel.Device];
|
||||
|
||||
if (device.Type == DeviceType.Keyboard)
|
||||
{
|
||||
assigner = new KeyboardKeyAssigner((IKeyboard)ViewModel.SelectedGamepad);
|
||||
}
|
||||
else if (device.Type == DeviceType.Controller)
|
||||
{
|
||||
assigner = new GamepadButtonAssigner(ViewModel.SelectedGamepad, (ViewModel.Config as StandardControllerInputConfig).TriggerThreshold, forStick);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Controller not supported");
|
||||
}
|
||||
|
||||
return assigner;
|
||||
}
|
||||
|
||||
private void Button_Unchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_currentAssigner?.Cancel();
|
||||
_currentAssigner = null;
|
||||
}
|
||||
|
||||
private void MouseClick(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
bool shouldUnbind = false;
|
||||
|
||||
if (e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed)
|
||||
{
|
||||
shouldUnbind = true;
|
||||
}
|
||||
|
||||
_currentAssigner?.Cancel(shouldUnbind);
|
||||
|
||||
PointerPressed -= MouseClick;
|
||||
}
|
||||
|
||||
private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (ViewModel.IsModified && !_dialogOpen)
|
||||
{
|
||||
_dialogOpen = true;
|
||||
|
||||
var result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
LocaleManager.Instance["DialogControllerSettingsModifiedConfirmMessage"],
|
||||
LocaleManager.Instance["DialogControllerSettingsModifiedConfirmSubMessage"],
|
||||
LocaleManager.Instance["InputDialogYes"],
|
||||
LocaleManager.Instance["InputDialogNo"],
|
||||
LocaleManager.Instance["RyujinxConfirm"]);
|
||||
|
||||
if (result == UserResult.Yes)
|
||||
{
|
||||
ViewModel.Save();
|
||||
}
|
||||
|
||||
_dialogOpen = false;
|
||||
|
||||
ViewModel.IsModified = false;
|
||||
|
||||
if (e.AddedItems.Count > 0)
|
||||
{
|
||||
var player = (PlayerModel)e.AddedItems[0];
|
||||
ViewModel.PlayerId = player.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_currentAssigner?.Cancel();
|
||||
_currentAssigner = null;
|
||||
ViewModel.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
172
Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml
Normal file
172
Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml
Normal file
|
@ -0,0 +1,172 @@
|
|||
<window:StyleableWindow
|
||||
x:Class="Ryujinx.Ava.UI.Windows.DownloadableContentManagerWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
Width="800"
|
||||
Height="500"
|
||||
MinWidth="800"
|
||||
MinHeight="500"
|
||||
MaxWidth="800"
|
||||
MaxHeight="500"
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<Grid Name="DownloadableContentGrid" Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Name="Heading"
|
||||
Grid.Row="1"
|
||||
MaxWidth="500"
|
||||
Margin="20,15,20,20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
LineHeight="18"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<DockPanel
|
||||
Grid.Row="2"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Left">
|
||||
<Button
|
||||
Name="EnableAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding EnableAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerEnableAllButton}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="DisableAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding DisableAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerDisableAllButton}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
<Border
|
||||
Grid.Row="3"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1">
|
||||
<ScrollViewer
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<DataGrid
|
||||
Name="DlcDataGrid"
|
||||
MinHeight="200"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserResizeColumns="True"
|
||||
CanUserSortColumns="True"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
Items="{Binding _downloadableContents}"
|
||||
SelectionMode="Extended"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<DataGrid.Styles>
|
||||
<Styles>
|
||||
<Style Selector="DataGridCell:nth-child(3), DataGridCell:nth-child(4)">
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
</Style>
|
||||
</Styles>
|
||||
<Styles>
|
||||
<Style Selector="DataGridCell:nth-child(1)">
|
||||
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Right" />
|
||||
</Style>
|
||||
</Styles>
|
||||
</DataGrid.Styles>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="90">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox
|
||||
Width="50"
|
||||
MinWidth="40"
|
||||
HorizontalAlignment="Center"
|
||||
IsChecked="{Binding Enabled}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
<DataGridTemplateColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingEnabledLabel}" />
|
||||
</DataGridTemplateColumn.Header>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Width="140" Binding="{Binding TitleId}">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingTitleIdLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn Width="280" Binding="{Binding FullPath}">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingFullPathLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
<DataGridTextColumn Binding="{Binding ContainerPath}">
|
||||
<DataGridTextColumn.Header>
|
||||
<TextBlock Text="{locale:Locale DlcManagerTableHeadingContainerPathLabel}" />
|
||||
</DataGridTextColumn.Header>
|
||||
</DataGridTextColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
<DockPanel
|
||||
Grid.Row="4"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch">
|
||||
<DockPanel Margin="0" HorizontalAlignment="Left">
|
||||
<Button
|
||||
Name="AddButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Add}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding RemoveSelected}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding RemoveAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
<DockPanel Margin="0" HorizontalAlignment="Right">
|
||||
<Button
|
||||
Name="SaveButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding SaveAndClose}">
|
||||
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="CancelButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Close}">
|
||||
<TextBlock Text="{locale:Locale InputDialogCancel}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</window:StyleableWindow>
|
314
Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs
Normal file
314
Ryujinx.Ava/UI/Windows/DownloadableContentManagerWindow.axaml.cs
Normal file
|
@ -0,0 +1,314 @@
|
|||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Tools.Fs;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class DownloadableContentManagerWindow : StyleableWindow
|
||||
{
|
||||
private readonly List<DownloadableContentContainer> _downloadableContentContainerList;
|
||||
private readonly string _downloadableContentJsonPath;
|
||||
|
||||
private VirtualFileSystem _virtualFileSystem { get; }
|
||||
private AvaloniaList<DownloadableContentModel> _downloadableContents { get; set; }
|
||||
|
||||
private ulong _titleId { get; }
|
||||
private string _titleName { get; }
|
||||
|
||||
public DownloadableContentManagerWindow()
|
||||
{
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["DlcWindowTitle"]} - {_titleName} ({_titleId:X16})";
|
||||
}
|
||||
|
||||
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||
{
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_downloadableContents = new AvaloniaList<DownloadableContentModel>();
|
||||
|
||||
_titleId = titleId;
|
||||
_titleName = titleName;
|
||||
|
||||
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
|
||||
|
||||
try
|
||||
{
|
||||
_downloadableContentContainerList = JsonHelper.DeserializeFromFile<List<DownloadableContentContainer>>(_downloadableContentJsonPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_downloadableContentContainerList = new List<DownloadableContentContainer>();
|
||||
}
|
||||
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
RemoveButton.IsEnabled = false;
|
||||
|
||||
DlcDataGrid.SelectionChanged += DlcDataGrid_SelectionChanged;
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["DlcWindowTitle"]} - {_titleName} ({_titleId:X16})";
|
||||
|
||||
LoadDownloadableContents();
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
private void DlcDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
RemoveButton.IsEnabled = (DlcDataGrid.SelectedItems.Count > 0);
|
||||
}
|
||||
|
||||
private void PrintHeading()
|
||||
{
|
||||
Heading.Text = string.Format(LocaleManager.Instance["DlcWindowHeading"], _downloadableContents.Count, _titleName, _titleId.ToString("X16"));
|
||||
}
|
||||
|
||||
private void LoadDownloadableContents()
|
||||
{
|
||||
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
|
||||
{
|
||||
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||
{
|
||||
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
||||
|
||||
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
|
||||
|
||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
|
||||
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
|
||||
{
|
||||
using UniqueRef<IFile> ncaFile = new();
|
||||
|
||||
partitionFileSystem.OpenFile(ref ncaFile.Ref(), downloadableContentNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), downloadableContentContainer.ContainerPath);
|
||||
if (nca != null)
|
||||
{
|
||||
_downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"),
|
||||
downloadableContentContainer.ContainerPath,
|
||||
downloadableContentNca.FullPath,
|
||||
downloadableContentNca.Enabled));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Save the list again to remove leftovers.
|
||||
Save();
|
||||
}
|
||||
|
||||
private Nca TryOpenNca(IStorage ncaStorage, string containerPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Nca(_virtualFileSystem.KeySet, ncaStorage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogDlcLoadNcaErrorMessage"], ex.Message, containerPath));
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task AddDownloadableContent(string path)
|
||||
{
|
||||
if (!File.Exists(path) || _downloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using FileStream containerFile = File.OpenRead(path);
|
||||
|
||||
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
|
||||
bool containsDownloadableContent = false;
|
||||
|
||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
partitionFileSystem.OpenFile(ref ncaFile.Ref(), fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryOpenNca(ncaFile.Get.AsStorage(), path);
|
||||
if (nca == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||
{
|
||||
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
_downloadableContents.Add(new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true));
|
||||
|
||||
containsDownloadableContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!containsDownloadableContent)
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogDlcNoDlcErrorMessage"]);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveDownloadableContents(bool removeSelectedOnly = false)
|
||||
{
|
||||
if (removeSelectedOnly)
|
||||
{
|
||||
AvaloniaList<DownloadableContentModel> removedItems = new();
|
||||
|
||||
foreach (var item in DlcDataGrid.SelectedItems)
|
||||
{
|
||||
removedItems.Add(item as DownloadableContentModel);
|
||||
}
|
||||
|
||||
DlcDataGrid.SelectedItems.Clear();
|
||||
|
||||
foreach (var item in removedItems)
|
||||
{
|
||||
_downloadableContents.RemoveAll(_downloadableContents.Where(x => x.TitleId == item.TitleId).ToList());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_downloadableContents.Clear();
|
||||
}
|
||||
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
public void RemoveSelected()
|
||||
{
|
||||
RemoveDownloadableContents(true);
|
||||
}
|
||||
|
||||
public void RemoveAll()
|
||||
{
|
||||
RemoveDownloadableContents();
|
||||
}
|
||||
|
||||
public void EnableAll()
|
||||
{
|
||||
foreach(var item in _downloadableContents)
|
||||
{
|
||||
item.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void DisableAll()
|
||||
{
|
||||
foreach (var item in _downloadableContents)
|
||||
{
|
||||
item.Enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async void Add()
|
||||
{
|
||||
OpenFileDialog dialog = new OpenFileDialog()
|
||||
{
|
||||
Title = LocaleManager.Instance["SelectDlcDialogTitle"],
|
||||
AllowMultiple = true
|
||||
};
|
||||
|
||||
dialog.Filters.Add(new FileDialogFilter
|
||||
{
|
||||
Name = "NSP",
|
||||
Extensions = { "nsp" }
|
||||
});
|
||||
|
||||
string[] files = await dialog.ShowAsync(this);
|
||||
|
||||
if (files != null)
|
||||
{
|
||||
foreach (string file in files)
|
||||
{
|
||||
await AddDownloadableContent(file);
|
||||
}
|
||||
}
|
||||
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
_downloadableContentContainerList.Clear();
|
||||
|
||||
DownloadableContentContainer container = default;
|
||||
|
||||
foreach (DownloadableContentModel downloadableContent in _downloadableContents)
|
||||
{
|
||||
if (container.ContainerPath != downloadableContent.ContainerPath)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
|
||||
{
|
||||
_downloadableContentContainerList.Add(container);
|
||||
}
|
||||
|
||||
container = new DownloadableContentContainer
|
||||
{
|
||||
ContainerPath = downloadableContent.ContainerPath,
|
||||
DownloadableContentNcaList = new List<DownloadableContentNca>()
|
||||
};
|
||||
}
|
||||
|
||||
container.DownloadableContentNcaList.Add(new DownloadableContentNca
|
||||
{
|
||||
Enabled = downloadableContent.Enabled,
|
||||
TitleId = Convert.ToUInt64(downloadableContent.TitleId, 16),
|
||||
FullPath = downloadableContent.FullPath
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(container.ContainerPath))
|
||||
{
|
||||
_downloadableContentContainerList.Add(container);
|
||||
}
|
||||
|
||||
using (FileStream downloadableContentJsonStream = File.Create(_downloadableContentJsonPath, 4096, FileOptions.WriteThrough))
|
||||
{
|
||||
downloadableContentJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_downloadableContentContainerList, true)));
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveAndClose()
|
||||
{
|
||||
Save();
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
192
Ryujinx.Ava/UI/Windows/IconColorPicker.cs
Normal file
192
Ryujinx.Ava/UI/Windows/IconColorPicker.cs
Normal file
|
@ -0,0 +1,192 @@
|
|||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
static class IconColorPicker
|
||||
{
|
||||
private const int ColorsPerLine = 64;
|
||||
private const int TotalColors = ColorsPerLine * ColorsPerLine;
|
||||
|
||||
private const int UvQuantBits = 3;
|
||||
private const int UvQuantShift = BitsPerComponent - UvQuantBits;
|
||||
|
||||
private const int SatQuantBits = 5;
|
||||
private const int SatQuantShift = BitsPerComponent - SatQuantBits;
|
||||
|
||||
private const int BitsPerComponent = 8;
|
||||
|
||||
private const int CutOffLuminosity = 64;
|
||||
|
||||
private readonly struct PaletteColor
|
||||
{
|
||||
public int Qck { get; }
|
||||
public byte R { get; }
|
||||
public byte G { get; }
|
||||
public byte B { get; }
|
||||
|
||||
public PaletteColor(int qck, byte r, byte g, byte b)
|
||||
{
|
||||
Qck = qck;
|
||||
R = r;
|
||||
G = g;
|
||||
B = b;
|
||||
}
|
||||
}
|
||||
|
||||
public static Color GetFilteredColor(Image<Bgra32> image)
|
||||
{
|
||||
var color = GetColor(image).ToPixel<Bgra32>();
|
||||
|
||||
// We don't want colors that are too dark.
|
||||
// If the color is too dark, make it brighter by reducing the range
|
||||
// and adding a constant color.
|
||||
int luminosity = GetColorApproximateLuminosity(color.R, color.G, color.B);
|
||||
if (luminosity < CutOffLuminosity)
|
||||
{
|
||||
color = Color.FromRgb(
|
||||
(byte)Math.Min(CutOffLuminosity + color.R, byte.MaxValue),
|
||||
(byte)Math.Min(CutOffLuminosity + color.G, byte.MaxValue),
|
||||
(byte)Math.Min(CutOffLuminosity + color.B, byte.MaxValue));
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
public static Color GetColor(Image<Bgra32> image)
|
||||
{
|
||||
var colors = new PaletteColor[TotalColors];
|
||||
|
||||
var dominantColorBin = new Dictionary<int, int>();
|
||||
|
||||
var buffer = GetBuffer(image);
|
||||
|
||||
int w = image.Width;
|
||||
|
||||
int w8 = w << 8;
|
||||
int h8 = image.Height << 8;
|
||||
|
||||
int xStep = w8 / ColorsPerLine;
|
||||
int yStep = h8 / ColorsPerLine;
|
||||
|
||||
int i = 0;
|
||||
int maxHitCount = 0;
|
||||
|
||||
for (int y = 0; y < image.Height; y++)
|
||||
{
|
||||
int yOffset = y * image.Width;
|
||||
|
||||
for (int x = 0; x < image.Width && i < TotalColors; x++)
|
||||
{
|
||||
int offset = x + yOffset;
|
||||
|
||||
byte cb = buffer[offset].B;
|
||||
byte cg = buffer[offset].G;
|
||||
byte cr = buffer[offset].R;
|
||||
|
||||
var qck = GetQuantizedColorKey(cr, cg, cb);
|
||||
|
||||
if (dominantColorBin.TryGetValue(qck, out int hitCount))
|
||||
{
|
||||
dominantColorBin[qck] = hitCount + 1;
|
||||
|
||||
if (maxHitCount < hitCount)
|
||||
{
|
||||
maxHitCount = hitCount;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
dominantColorBin.Add(qck, 1);
|
||||
}
|
||||
|
||||
colors[i++] = new PaletteColor(qck, cr, cg, cb);
|
||||
}
|
||||
}
|
||||
|
||||
int highScore = -1;
|
||||
PaletteColor bestCandidate = default;
|
||||
|
||||
for (i = 0; i < TotalColors; i++)
|
||||
{
|
||||
var score = GetColorScore(dominantColorBin, maxHitCount, colors[i]);
|
||||
|
||||
if (highScore < score)
|
||||
{
|
||||
highScore = score;
|
||||
bestCandidate = colors[i];
|
||||
}
|
||||
}
|
||||
|
||||
return Color.FromRgb(bestCandidate.R, bestCandidate.G, bestCandidate.B);
|
||||
}
|
||||
|
||||
public static Bgra32[] GetBuffer(Image<Bgra32> image)
|
||||
{
|
||||
return image.TryGetSinglePixelSpan(out var data) ? data.ToArray() : new Bgra32[0];
|
||||
}
|
||||
|
||||
private static int GetColorScore(Dictionary<int, int> dominantColorBin, int maxHitCount, PaletteColor color)
|
||||
{
|
||||
var hitCount = dominantColorBin[color.Qck];
|
||||
var balancedHitCount = BalanceHitCount(hitCount, maxHitCount);
|
||||
var quantSat = (GetColorSaturation(color) >> SatQuantShift) << SatQuantShift;
|
||||
var value = GetColorValue(color);
|
||||
|
||||
// If the color is rarely used on the image,
|
||||
// then chances are that theres a better candidate, even if the saturation value
|
||||
// is high. By multiplying the saturation value with a weight, we can lower
|
||||
// it if the color is almost never used (hit count is low).
|
||||
var satWeighted = quantSat;
|
||||
var satWeight = balancedHitCount << 5;
|
||||
if (satWeight < 0x100)
|
||||
{
|
||||
satWeighted = (satWeighted * satWeight) >> 8;
|
||||
}
|
||||
|
||||
// Compute score from saturation and dominance of the color.
|
||||
// We prefer more vivid colors over dominant ones, so give more weight to the saturation.
|
||||
var score = ((satWeighted << 1) + balancedHitCount) * value;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private static int BalanceHitCount(int hitCount, int maxHitCount)
|
||||
{
|
||||
return (hitCount << 8) / maxHitCount;
|
||||
}
|
||||
|
||||
private static int GetColorApproximateLuminosity(byte r, byte g, byte b)
|
||||
{
|
||||
return (r + g + b) / 3;
|
||||
}
|
||||
|
||||
private static int GetColorSaturation(PaletteColor color)
|
||||
{
|
||||
int cMax = Math.Max(Math.Max(color.R, color.G), color.B);
|
||||
|
||||
if (cMax == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cMin = Math.Min(Math.Min(color.R, color.G), color.B);
|
||||
int delta = cMax - cMin;
|
||||
return (delta << 8) / cMax;
|
||||
}
|
||||
|
||||
private static int GetColorValue(PaletteColor color)
|
||||
{
|
||||
return Math.Max(Math.Max(color.R, color.G), color.B);
|
||||
}
|
||||
|
||||
private static int GetQuantizedColorKey(byte r, byte g, byte b)
|
||||
{
|
||||
int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
|
||||
int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
|
||||
return (v >> UvQuantShift) | ((u >> UvQuantShift) << UvQuantBits);
|
||||
}
|
||||
}
|
||||
}
|
770
Ryujinx.Ava/UI/Windows/MainWindow.axaml
Normal file
770
Ryujinx.Ava/UI/Windows/MainWindow.axaml
Normal file
|
@ -0,0 +1,770 @@
|
|||
<window:StyleableWindow
|
||||
x:Class="Ryujinx.Ava.UI.Windows.MainWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
Title="Ryujinx"
|
||||
Width="1280"
|
||||
Height="777"
|
||||
MinWidth="1092"
|
||||
MinHeight="672"
|
||||
d:DesignHeight="720"
|
||||
d:DesignWidth="1280"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:MainWindowViewModel"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<Window.Styles>
|
||||
<Style Selector="TitleBar:fullscreen">
|
||||
<Setter Property="Background" Value="#000000" />
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
<Design.DataContext>
|
||||
<viewModels:MainWindowViewModel />
|
||||
</Design.DataContext>
|
||||
<Window.Resources>
|
||||
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
|
||||
</Window.Resources>
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<helpers:OffscreenTextBox Name="HiddenTextBox" Grid.Row="0" />
|
||||
<StackPanel Grid.Row="0" IsVisible="False">
|
||||
<helpers:HotKeyControl Name="FullscreenHotKey" Command="{ReflectionBinding ToggleFullscreen}" />
|
||||
<helpers:HotKeyControl Name="FullscreenHotKey2" Command="{ReflectionBinding ToggleFullscreen}" />
|
||||
<helpers:HotKeyControl Name="DockToggleHotKey" Command="{ReflectionBinding ToggleDockMode}" />
|
||||
<helpers:HotKeyControl Name="ExitHotKey" Command="{ReflectionBinding ExitCurrentState}" />
|
||||
</StackPanel>
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel
|
||||
Name="MenuBar"
|
||||
MinHeight="35"
|
||||
Grid.Row="0"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
IsVisible="{Binding ShowMenuAndStatusBar}"
|
||||
Orientation="Vertical">
|
||||
<DockPanel HorizontalAlignment="Stretch">
|
||||
<Menu
|
||||
Name="Menu"
|
||||
Height="35"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Left">
|
||||
<Menu.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<DockPanel Margin="0" HorizontalAlignment="Stretch" />
|
||||
</ItemsPanelTemplate>
|
||||
</Menu.ItemsPanel>
|
||||
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarFile}">
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenFile}"
|
||||
Header="{locale:Locale MenuBarFileOpenFromFile}"
|
||||
IsEnabled="{Binding EnableNonGameRunningControls}"
|
||||
ToolTip.Tip="{locale:Locale LoadApplicationFileTooltip}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenFolder}"
|
||||
Header="{locale:Locale MenuBarFileOpenUnpacked}"
|
||||
IsEnabled="{Binding EnableNonGameRunningControls}"
|
||||
ToolTip.Tip="{locale:Locale LoadApplicationFolderTooltip}" />
|
||||
<MenuItem Header="{locale:Locale MenuBarFileOpenApplet}" IsEnabled="{Binding IsAppletMenuActive}">
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenMiiApplet}"
|
||||
Header="Mii Edit Applet"
|
||||
ToolTip.Tip="{locale:Locale MenuBarFileOpenAppletOpenMiiAppletToolTip}" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenRyujinxFolder}"
|
||||
Header="{locale:Locale MenuBarFileOpenEmuFolder}"
|
||||
ToolTip.Tip="{locale:Locale OpenRyujinxFolderTooltip}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenLogsFolder}"
|
||||
Header="{locale:Locale MenuBarFileOpenLogsFolder}"
|
||||
ToolTip.Tip="{locale:Locale OpenRyujinxLogsTooltip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding CloseWindow}"
|
||||
Header="{locale:Locale MenuBarFileExit}"
|
||||
ToolTip.Tip="{locale:Locale ExitTooltip}" />
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarOptions}">
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ToggleFullscreen}"
|
||||
Header="{locale:Locale MenuBarOptionsToggleFullscreen}"
|
||||
InputGesture="F11" />
|
||||
<MenuItem>
|
||||
<MenuItem.Icon>
|
||||
<CheckBox IsChecked="{Binding StartGamesInFullscreen, Mode=TwoWay}"
|
||||
MinWidth="250">
|
||||
<TextBlock Text="{locale:Locale MenuBarOptionsStartGamesInFullscreen}"/>
|
||||
</CheckBox>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem IsVisible="{Binding ShowConsoleVisible}">
|
||||
<MenuItem.Icon>
|
||||
<CheckBox IsChecked="{Binding ShowConsole, Mode=TwoWay}"
|
||||
MinWidth="250">
|
||||
<TextBlock Text="{locale:Locale MenuBarOptionsShowConsole}"/>
|
||||
</CheckBox>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="{locale:Locale MenuBarOptionsChangeLanguage}">
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="de_DE"
|
||||
Header="Deutsch" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="en_US"
|
||||
Header="English (US)" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="es_ES"
|
||||
Header="Español (ES)" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="fr_FR"
|
||||
Header="Français" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="it_IT"
|
||||
Header="Italiano" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="pt_BR"
|
||||
Header="Português (BR)" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="tr_TR"
|
||||
Header="Türkçe" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="el_GR"
|
||||
Header="Ελληνικά" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="pl_PL"
|
||||
Header="Polski" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="ru_RU"
|
||||
Header="Русский" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="zh_CN"
|
||||
Header="简体中文" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="zh_TW"
|
||||
Header="繁體中文" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="ja_JP"
|
||||
Header="日本語" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="ko_KR"
|
||||
Header="한국어" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenSettings}"
|
||||
Header="{locale:Locale MenuBarOptionsSettings}"
|
||||
ToolTip.Tip="{locale:Locale OpenSettingsTooltip}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ManageProfiles}"
|
||||
Header="{locale:Locale MenuBarOptionsManageUserProfiles}"
|
||||
IsEnabled="{Binding EnableNonGameRunningControls}"
|
||||
ToolTip.Tip="{locale:Locale OpenProfileManagerTooltip}" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
Name="ActionsMenuItem"
|
||||
VerticalAlignment="Center"
|
||||
Header="{locale:Locale MenuBarActions}"
|
||||
IsEnabled="{Binding IsGameRunning}">
|
||||
<MenuItem
|
||||
Click="PauseEmulation_Click"
|
||||
Header="{locale:Locale MenuBarOptionsPauseEmulation}"
|
||||
InputGesture="{Binding PauseKey}"
|
||||
IsEnabled="{Binding !IsPaused}"
|
||||
IsVisible="{Binding !IsPaused}" />
|
||||
<MenuItem
|
||||
Click="ResumeEmulation_Click"
|
||||
Header="{locale:Locale MenuBarOptionsResumeEmulation}"
|
||||
InputGesture="{Binding PauseKey}"
|
||||
IsEnabled="{Binding IsPaused}"
|
||||
IsVisible="{Binding IsPaused}" />
|
||||
<MenuItem
|
||||
Click="StopEmulation_Click"
|
||||
Header="{locale:Locale MenuBarOptionsStopEmulation}"
|
||||
InputGesture="Escape"
|
||||
IsEnabled="{Binding IsGameRunning}"
|
||||
ToolTip.Tip="{locale:Locale StopEmulationTooltip}" />
|
||||
<MenuItem Command="{ReflectionBinding SimulateWakeUpMessage}" Header="{locale:Locale MenuBarOptionsSimulateWakeUpMessage}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Name="ScanAmiiboMenuItem"
|
||||
AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree"
|
||||
Command="{ReflectionBinding OpenAmiiboWindow}"
|
||||
Header="{locale:Locale MenuBarActionsScanAmiibo}"
|
||||
IsEnabled="{Binding IsAmiiboRequested}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding TakeScreenshot}"
|
||||
Header="{locale:Locale MenuBarFileToolsTakeScreenshot}"
|
||||
InputGesture="{Binding ScreenshotKey}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding HideUi}"
|
||||
Header="{locale:Locale MenuBarFileToolsHideUi}"
|
||||
InputGesture="{Binding ShowUiKey}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenCheatManagerForCurrentApp}"
|
||||
Header="{locale:Locale GameListContextMenuManageCheat}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarTools}">
|
||||
<MenuItem Header="{locale:Locale MenuBarToolsInstallFirmware}" IsEnabled="{Binding EnableNonGameRunningControls}">
|
||||
<MenuItem Command="{ReflectionBinding InstallFirmwareFromFile}" Header="{locale:Locale MenuBarFileToolsInstallFirmwareFromFile}" />
|
||||
<MenuItem Command="{ReflectionBinding InstallFirmwareFromFolder}" Header="{locale:Locale MenuBarFileToolsInstallFirmwareFromDirectory}" />
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarHelp}">
|
||||
<MenuItem
|
||||
Name="UpdateMenuItem"
|
||||
Command="{ReflectionBinding CheckForUpdates}"
|
||||
Header="{locale:Locale MenuBarHelpCheckForUpdates}"
|
||||
ToolTip.Tip="{locale:Locale CheckUpdatesTooltip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenAboutWindow}"
|
||||
Header="{locale:Locale MenuBarHelpAbout}"
|
||||
ToolTip.Tip="{locale:Locale OpenAboutTooltip}" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</DockPanel>
|
||||
</StackPanel>
|
||||
<ContentControl
|
||||
Name="MainContent"
|
||||
Grid.Row="1"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="0,0,0,0"
|
||||
DockPanel.Dock="Top"
|
||||
IsVisible="{Binding ShowContent}">
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<DockPanel
|
||||
Grid.Row="0"
|
||||
Margin="0,0,0,5"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Button
|
||||
Width="40"
|
||||
MinWidth="40"
|
||||
Margin="5,2,0,2"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{ReflectionBinding SetListMode}"
|
||||
IsEnabled="{Binding IsGrid}">
|
||||
<ui:FontIcon
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="avares://FluentAvalonia/Fonts#Symbols"
|
||||
Glyph="{helpers:GlyphValueConverter List}" />
|
||||
</Button>
|
||||
<Button
|
||||
Width="40"
|
||||
MinWidth="40"
|
||||
Margin="5,2,5,2"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{ReflectionBinding SetGridMode}"
|
||||
IsEnabled="{Binding IsList}">
|
||||
<ui:FontIcon
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="avares://FluentAvalonia/Fonts#Symbols"
|
||||
Glyph="{helpers:GlyphValueConverter Grid}" />
|
||||
</Button>
|
||||
<TextBlock
|
||||
Margin="10,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{locale:Locale IconSize}"
|
||||
ToolTip.Tip="{locale:Locale IconSizeTooltip}" />
|
||||
<Slider
|
||||
Width="150"
|
||||
Height="35"
|
||||
Margin="5,-10,5,0"
|
||||
VerticalAlignment="Center"
|
||||
IsSnapToTickEnabled="True"
|
||||
Maximum="4"
|
||||
Minimum="1"
|
||||
TickFrequency="1"
|
||||
ToolTip.Tip="{locale:Locale IconSizeTooltip}"
|
||||
Value="{Binding GridSizeScale}" />
|
||||
<CheckBox
|
||||
Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding ShowNames, Mode=TwoWay}"
|
||||
IsVisible="{Binding IsGrid}">
|
||||
<TextBlock Margin="5,3,0,0" Text="{locale:Locale CommonShowNames}" />
|
||||
</CheckBox>
|
||||
<TextBox
|
||||
Name="SearchBox"
|
||||
MinWidth="200"
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
KeyUp="SearchBox_OnKeyUp"
|
||||
Text="{Binding SearchText}"
|
||||
Watermark="{locale:Locale MenuSearch}" />
|
||||
<ui:DropDownButton
|
||||
Width="150"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Content="{Binding SortName}"
|
||||
DockPanel.Dock="Right">
|
||||
<ui:DropDownButton.Flyout>
|
||||
<Flyout Placement="Bottom">
|
||||
<StackPanel
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<StackPanel>
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale CommonFavorite}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByFavorite, Mode=OneTime}"
|
||||
Tag="Favorite" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderApplication}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
|
||||
Tag="Title" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderDeveloper}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByDeveloper, Mode=OneTime}"
|
||||
Tag="Developer" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderTimePlayed}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByTimePlayed, Mode=OneTime}"
|
||||
Tag="TotalTimePlayed" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderLastPlayed}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByLastPlayed, Mode=OneTime}"
|
||||
Tag="LastPlayed" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderFileExtension}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByType, Mode=OneTime}"
|
||||
Tag="FileType" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderFileSize}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedBySize, Mode=OneTime}"
|
||||
Tag="FileSize" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderPath}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByPath, Mode=OneTime}"
|
||||
Tag="Path" />
|
||||
</StackPanel>
|
||||
<Border
|
||||
Width="60"
|
||||
Height="2"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
BorderBrush="White"
|
||||
BorderThickness="0,1,0,0">
|
||||
<Separator Height="0" HorizontalAlignment="Stretch" />
|
||||
</Border>
|
||||
<RadioButton
|
||||
Checked="Order_Checked"
|
||||
Content="{locale:Locale OrderAscending}"
|
||||
GroupName="Order"
|
||||
IsChecked="{Binding IsAscending, Mode=OneTime}"
|
||||
Tag="Ascending" />
|
||||
<RadioButton
|
||||
Checked="Order_Checked"
|
||||
Content="{locale:Locale OrderDescending}"
|
||||
GroupName="Order"
|
||||
IsChecked="{Binding !IsAscending, Mode=OneTime}"
|
||||
Tag="Descending" />
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</ui:DropDownButton.Flyout>
|
||||
</ui:DropDownButton>
|
||||
<TextBlock
|
||||
Margin="10,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
Text="{locale:Locale CommonSort}" />
|
||||
</DockPanel>
|
||||
<controls:GameListView
|
||||
x:Name="GameList"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
IsVisible="{Binding IsList}" />
|
||||
<controls:GameGridView
|
||||
x:Name="GameGrid"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
IsVisible="{Binding IsGrid}" />
|
||||
</Grid>
|
||||
</ContentControl>
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{DynamicResource ThemeContentBackgroundColor}"
|
||||
IsVisible="{Binding ShowLoadProgress}"
|
||||
Name="LoadingView"
|
||||
ZIndex="1000">
|
||||
<Grid
|
||||
Margin="40"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding ShowLoadProgress}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="0"
|
||||
Width="256"
|
||||
Height="256"
|
||||
Margin="10"
|
||||
Padding="4"
|
||||
BorderBrush="Black"
|
||||
BorderThickness="2"
|
||||
BoxShadow="4 4 32 8 #40000000"
|
||||
CornerRadius="3"
|
||||
IsVisible="{Binding ShowLoadProgress}">
|
||||
<Image
|
||||
Width="256"
|
||||
Height="256"
|
||||
IsVisible="{Binding ShowLoadProgress}"
|
||||
Source="{Binding SelectedIcon, Converter={StaticResource ByteImage}}" />
|
||||
</Border>
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding ShowLoadProgress}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Margin="10"
|
||||
FontSize="30"
|
||||
FontWeight="Bold"
|
||||
IsVisible="{Binding ShowLoadProgress}"
|
||||
Text="{Binding LoadHeading}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="Wrap" />
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Margin="10"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
BorderBrush="{Binding ProgressBarBackgroundColor}"
|
||||
BorderThickness="1"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="5"
|
||||
IsVisible="{Binding ShowLoadProgress}">
|
||||
<ProgressBar
|
||||
Height="10"
|
||||
MinWidth="500"
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{Binding ProgressBarBackgroundColor}"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="5"
|
||||
Foreground="{Binding ProgressBarForegroundColor}"
|
||||
IsIndeterminate="{Binding IsLoadingIndeterminate}"
|
||||
IsVisible="{Binding ShowLoadProgress}"
|
||||
Maximum="{Binding ProgressMaximum}"
|
||||
Minimum="0"
|
||||
Value="{Binding ProgressValue}" />
|
||||
</Border>
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Margin="10"
|
||||
FontSize="18"
|
||||
IsVisible="{Binding ShowLoadProgress}"
|
||||
Text="{Binding CacheLoadStatus}"
|
||||
TextAlignment="Left" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
Name="StatusBar"
|
||||
Grid.Row="2"
|
||||
Margin="0"
|
||||
MinHeight="22"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{DynamicResource ThemeContentBackgroundColor}"
|
||||
DockPanel.Dock="Bottom"
|
||||
IsVisible="{Binding ShowMenuAndStatusBar}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel
|
||||
Grid.Column="0"
|
||||
Margin="5"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding EnableNonGameRunningControls}">
|
||||
<Grid Margin="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Width="25"
|
||||
Height="25"
|
||||
MinWidth="0"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
Command="{ReflectionBinding LoadApplications}">
|
||||
<ui:SymbolIcon
|
||||
Width="50"
|
||||
Height="100"
|
||||
Symbol="Refresh" />
|
||||
</Button>
|
||||
<TextBlock
|
||||
Name="LoadStatus"
|
||||
Grid.Column="1"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding EnableNonGameRunningControls}"
|
||||
Text="{locale:Locale StatusBarGamesLoaded}" />
|
||||
<ProgressBar
|
||||
Name="LoadProgressBar"
|
||||
Grid.Column="2"
|
||||
Height="6"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource HighlightColor}"
|
||||
IsVisible="{Binding EnableNonGameRunningControls}"
|
||||
Maximum="{Binding StatusBarProgressMaximum}"
|
||||
Value="{Binding StatusBarProgressValue}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Margin="0,2"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding IsGameRunning}"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Name="VsyncStatus"
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{Binding VsyncColor}"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
PointerReleased="VsyncStatus_PointerReleased"
|
||||
Text="VSync"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Name="DockedStatus"
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
PointerReleased="DockedStatus_PointerReleased"
|
||||
Text="{Binding DockedStatusText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Name="AspectRatioStatus"
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
PointerReleased="AspectRatioStatus_PointerReleased"
|
||||
Text="{Binding AspectRatioStatusText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<ui:ToggleSplitButton
|
||||
Name="VolumeStatus"
|
||||
Padding="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Background="{DynamicResource ThemeContentBackgroundColor}"
|
||||
BorderThickness="0"
|
||||
Content="{Binding VolumeStatusText}"
|
||||
IsChecked="{Binding VolumeMuted}"
|
||||
IsVisible="{Binding !ShowLoadProgress}">
|
||||
<ui:ToggleSplitButton.Flyout>
|
||||
<Flyout Placement="Bottom" ShowMode="TransientWithDismissOnPointerMoveAway">
|
||||
<Grid Margin="0">
|
||||
<Slider
|
||||
MaxHeight="40"
|
||||
Width="150"
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
IsSnapToTickEnabled="True"
|
||||
LargeChange="0.05"
|
||||
Maximum="1"
|
||||
Minimum="0"
|
||||
SmallChange="0.01"
|
||||
TickFrequency="0.05"
|
||||
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
|
||||
Value="{Binding Volume}" />
|
||||
</Grid>
|
||||
</Flyout>
|
||||
</ui:ToggleSplitButton.Flyout>
|
||||
</ui:ToggleSplitButton>
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
Text="{Binding GameStatusText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
Text="{Binding FifoStatusText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
Text="{Binding BackendText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
Text="{Binding GpuNameText}"
|
||||
TextAlignment="Left" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Column="3"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding ShowFirmwareStatus}"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Name="FirmwareStatus"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Text="{locale:Locale StatusBarSystemVersion}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</window:StyleableWindow>
|
721
Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs
Normal file
721
Ryujinx.Ava/UI/Windows/MainWindow.axaml.cs
Normal file
|
@ -0,0 +1,721 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Input;
|
||||
using Ryujinx.Ava.UI.Applet;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.Gpu;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Ryujinx.Input.SDL2;
|
||||
using Ryujinx.Modules;
|
||||
using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.Ui.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using InputManager = Ryujinx.Input.HLE.InputManager;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class MainWindow : StyleableWindow
|
||||
{
|
||||
internal static MainWindowViewModel MainWindowViewModel { get; private set; }
|
||||
private bool _canUpdate;
|
||||
private bool _isClosing;
|
||||
private bool _isLoading;
|
||||
|
||||
private Control _mainViewContent;
|
||||
|
||||
private UserChannelPersistence _userChannelPersistence;
|
||||
private static bool _deferLoad;
|
||||
private static string _launchPath;
|
||||
private static bool _startFullscreen;
|
||||
private string _currentEmulatedGamePath;
|
||||
internal readonly AvaHostUiHandler UiHandler;
|
||||
private AutoResetEvent _rendererWaitEvent;
|
||||
|
||||
public VirtualFileSystem VirtualFileSystem { get; private set; }
|
||||
public ContentManager ContentManager { get; private set; }
|
||||
public AccountManager AccountManager { get; private set; }
|
||||
|
||||
public LibHacHorizonManager LibHacHorizonManager { get; private set; }
|
||||
|
||||
internal AppHost AppHost { get; private set; }
|
||||
public InputManager InputManager { get; private set; }
|
||||
|
||||
internal RendererHost RendererControl { get; private set; }
|
||||
internal MainWindowViewModel ViewModel { get; private set; }
|
||||
public SettingsWindow SettingsWindow { get; set; }
|
||||
|
||||
public bool CanUpdate
|
||||
{
|
||||
get => _canUpdate;
|
||||
set
|
||||
{
|
||||
_canUpdate = value;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() => UpdateMenuItem.IsEnabled = _canUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ShowKeyErrorOnLoad { get; set; }
|
||||
public ApplicationLibrary ApplicationLibrary { get; set; }
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
ViewModel = new MainWindowViewModel(this);
|
||||
|
||||
MainWindowViewModel = ViewModel;
|
||||
|
||||
DataContext = ViewModel;
|
||||
|
||||
InitializeComponent();
|
||||
Load();
|
||||
|
||||
UiHandler = new AvaHostUiHandler(this);
|
||||
|
||||
Title = $"Ryujinx {Program.Version}";
|
||||
|
||||
// NOTE: Height of MenuBar and StatusBar is not usable here, since it would still be 0 at this point.
|
||||
double barHeight = MenuBar.MinHeight + StatusBar.MinHeight;
|
||||
Height = ((Height - barHeight) / Program.WindowScaleFactor) + barHeight;
|
||||
Width /= Program.WindowScaleFactor;
|
||||
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
Initialize();
|
||||
|
||||
ViewModel.Initialize();
|
||||
|
||||
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver());
|
||||
|
||||
LoadGameList();
|
||||
}
|
||||
|
||||
_rendererWaitEvent = new AutoResetEvent(false);
|
||||
}
|
||||
|
||||
public void LoadGameList()
|
||||
{
|
||||
if (_isLoading)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
|
||||
ViewModel.LoadApplications();
|
||||
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
private void Update_StatusBar(object sender, StatusUpdatedEventArgs args)
|
||||
{
|
||||
if (ViewModel.ShowMenuAndStatusBar && !ViewModel.ShowLoadProgress)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (args.VSyncEnabled)
|
||||
{
|
||||
ViewModel.VsyncColor = new SolidColorBrush(Color.Parse("#ff2eeac9"));
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewModel.VsyncColor = new SolidColorBrush(Color.Parse("#ffff4554"));
|
||||
}
|
||||
|
||||
ViewModel.DockedStatusText = args.DockedMode;
|
||||
ViewModel.AspectRatioStatusText = args.AspectRatio;
|
||||
ViewModel.GameStatusText = args.GameStatus;
|
||||
ViewModel.VolumeStatusText = args.VolumeStatus;
|
||||
ViewModel.FifoStatusText = args.FifoStatus;
|
||||
ViewModel.GpuNameText = args.GpuName;
|
||||
ViewModel.BackendText = args.GpuBackend;
|
||||
|
||||
ViewModel.ShowStatusSeparator = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void HandleScalingChanged(double scale)
|
||||
{
|
||||
Program.DesktopScaleFactor = scale;
|
||||
base.HandleScalingChanged(scale);
|
||||
}
|
||||
|
||||
public void Application_Opened(object sender, ApplicationOpenedEventArgs args)
|
||||
{
|
||||
if (args.Application != null)
|
||||
{
|
||||
ViewModel.SelectedIcon = args.Application.Icon;
|
||||
|
||||
string path = new FileInfo(args.Application.Path).FullName;
|
||||
|
||||
LoadApplication(path);
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public async Task PerformanceCheck()
|
||||
{
|
||||
if (ConfigurationState.Instance.Logger.EnableTrace.Value)
|
||||
{
|
||||
string mainMessage = LocaleManager.Instance["DialogPerformanceCheckLoggingEnabledMessage"];
|
||||
string secondaryMessage = LocaleManager.Instance["DialogPerformanceCheckLoggingEnabledConfirmMessage"];
|
||||
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(mainMessage, secondaryMessage,
|
||||
LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"],
|
||||
LocaleManager.Instance["RyujinxConfirm"]);
|
||||
|
||||
if (result != UserResult.Yes)
|
||||
{
|
||||
ConfigurationState.Instance.Logger.EnableTrace.Value = false;
|
||||
|
||||
SaveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ConfigurationState.Instance.Graphics.ShadersDumpPath.Value))
|
||||
{
|
||||
string mainMessage = LocaleManager.Instance["DialogPerformanceCheckShaderDumpEnabledMessage"];
|
||||
string secondaryMessage =
|
||||
LocaleManager.Instance["DialogPerformanceCheckShaderDumpEnabledConfirmMessage"];
|
||||
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(mainMessage, secondaryMessage,
|
||||
LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"],
|
||||
LocaleManager.Instance["RyujinxConfirm"]);
|
||||
|
||||
if (result != UserResult.Yes)
|
||||
{
|
||||
ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = "";
|
||||
|
||||
SaveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void DeferLoadApplication(string launchPathArg, bool startFullscreenArg)
|
||||
{
|
||||
_deferLoad = true;
|
||||
_launchPath = launchPathArg;
|
||||
_startFullscreen = startFullscreenArg;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998
|
||||
public async void LoadApplication(string path, bool startFullscreen = false, string titleName = "")
|
||||
#pragma warning restore CS1998
|
||||
{
|
||||
if (AppHost != null)
|
||||
{
|
||||
await ContentDialogHelper.CreateInfoDialog(
|
||||
LocaleManager.Instance["DialogLoadAppGameAlreadyLoadedMessage"],
|
||||
LocaleManager.Instance["DialogLoadAppGameAlreadyLoadedSubMessage"],
|
||||
LocaleManager.Instance["InputDialogOk"],
|
||||
"",
|
||||
LocaleManager.Instance["RyujinxInfo"]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#if RELEASE
|
||||
await PerformanceCheck();
|
||||
#endif
|
||||
|
||||
Logger.RestartTime();
|
||||
|
||||
if (ViewModel.SelectedIcon == null)
|
||||
{
|
||||
ViewModel.SelectedIcon = ApplicationLibrary.GetApplicationIcon(path);
|
||||
}
|
||||
|
||||
PrepareLoadScreen();
|
||||
|
||||
_mainViewContent = MainContent.Content as Control;
|
||||
|
||||
RendererControl = new RendererHost(ConfigurationState.Instance.Logger.GraphicsDebugLevel);
|
||||
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl)
|
||||
{
|
||||
RendererControl.CreateOpenGL();
|
||||
}
|
||||
else
|
||||
{
|
||||
RendererControl.CreateVulkan();
|
||||
}
|
||||
|
||||
AppHost = new AppHost(RendererControl, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
|
||||
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
if (!await AppHost.LoadGuestApplication())
|
||||
{
|
||||
AppHost.DisposeContext();
|
||||
AppHost = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
CanUpdate = false;
|
||||
ViewModel.LoadHeading = string.IsNullOrWhiteSpace(titleName) ? string.Format(LocaleManager.Instance["LoadingHeading"], AppHost.Device.Application.TitleName) : titleName;
|
||||
ViewModel.TitleName = string.IsNullOrWhiteSpace(titleName) ? AppHost.Device.Application.TitleName : titleName;
|
||||
|
||||
SwitchToGameControl(startFullscreen);
|
||||
|
||||
_currentEmulatedGamePath = path;
|
||||
|
||||
Thread gameThread = new(InitializeGame)
|
||||
{
|
||||
Name = "GUI.WindowThread"
|
||||
};
|
||||
gameThread.Start();
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeGame()
|
||||
{
|
||||
RendererControl.RendererInitialized += GlRenderer_Created;
|
||||
|
||||
AppHost.StatusUpdatedEvent += Update_StatusBar;
|
||||
AppHost.AppExit += AppHost_AppExit;
|
||||
|
||||
_rendererWaitEvent.WaitOne();
|
||||
|
||||
AppHost?.Start();
|
||||
|
||||
AppHost.DisposeContext();
|
||||
}
|
||||
|
||||
|
||||
private void HandleRelaunch()
|
||||
{
|
||||
if (_userChannelPersistence.PreviousIndex != -1 && _userChannelPersistence.ShouldRestart)
|
||||
{
|
||||
_userChannelPersistence.ShouldRestart = false;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
LoadApplication(_currentEmulatedGamePath);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// otherwise, clear state.
|
||||
_userChannelPersistence = new UserChannelPersistence();
|
||||
_currentEmulatedGamePath = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SwitchToGameControl(bool startFullscreen = false)
|
||||
{
|
||||
ViewModel.ShowLoadProgress = false;
|
||||
ViewModel.ShowContent = true;
|
||||
ViewModel.IsLoadingIndeterminate = false;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
MainContent.Content = RendererControl;
|
||||
|
||||
if (startFullscreen && WindowState != WindowState.FullScreen)
|
||||
{
|
||||
ViewModel.ToggleFullscreen();
|
||||
}
|
||||
|
||||
RendererControl.Focus();
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowLoading(bool startFullscreen = false)
|
||||
{
|
||||
ViewModel.ShowContent = false;
|
||||
ViewModel.ShowLoadProgress = true;
|
||||
ViewModel.IsLoadingIndeterminate = true;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (startFullscreen && WindowState != WindowState.FullScreen)
|
||||
{
|
||||
ViewModel.ToggleFullscreen();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void GlRenderer_Created(object sender, EventArgs e)
|
||||
{
|
||||
ShowLoading();
|
||||
|
||||
_rendererWaitEvent.Set();
|
||||
}
|
||||
|
||||
private void AppHost_AppExit(object sender, EventArgs e)
|
||||
{
|
||||
if (_isClosing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel.IsGameRunning = false;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
ViewModel.ShowMenuAndStatusBar = true;
|
||||
ViewModel.ShowContent = true;
|
||||
ViewModel.ShowLoadProgress = false;
|
||||
ViewModel.IsLoadingIndeterminate = false;
|
||||
CanUpdate = true;
|
||||
Cursor = Cursor.Default;
|
||||
|
||||
if (MainContent.Content != _mainViewContent)
|
||||
{
|
||||
MainContent.Content = _mainViewContent;
|
||||
}
|
||||
|
||||
AppHost = null;
|
||||
|
||||
HandleRelaunch();
|
||||
});
|
||||
|
||||
RendererControl.RendererInitialized -= GlRenderer_Created;
|
||||
RendererControl = null;
|
||||
|
||||
ViewModel.SelectedIcon = null;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
Title = $"Ryujinx {Program.Version}";
|
||||
});
|
||||
}
|
||||
|
||||
public void Sort_Checked(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is RadioButton button)
|
||||
{
|
||||
var sort = Enum.Parse<ApplicationSort>(button.Tag.ToString());
|
||||
ViewModel.Sort(sort);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void HandleWindowStateChanged(WindowState state)
|
||||
{
|
||||
WindowState = state;
|
||||
|
||||
if (state != WindowState.Minimized)
|
||||
{
|
||||
Renderer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public void Order_Checked(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is RadioButton button)
|
||||
{
|
||||
var tag = button.Tag.ToString();
|
||||
ViewModel.Sort(tag != "Descending");
|
||||
}
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
_userChannelPersistence = new UserChannelPersistence();
|
||||
VirtualFileSystem = VirtualFileSystem.CreateInstance();
|
||||
LibHacHorizonManager = new LibHacHorizonManager();
|
||||
ContentManager = new ContentManager(VirtualFileSystem);
|
||||
|
||||
LibHacHorizonManager.InitializeFsServer(VirtualFileSystem);
|
||||
LibHacHorizonManager.InitializeArpServer();
|
||||
LibHacHorizonManager.InitializeBcatServer();
|
||||
LibHacHorizonManager.InitializeSystemClients();
|
||||
|
||||
ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem);
|
||||
|
||||
// Save data created before we supported extra data in directory save data will not work properly if
|
||||
// given empty extra data. Luckily some of that extra data can be created using the data from the
|
||||
// save data indexer, which should be enough to check access permissions for user saves.
|
||||
// Every single save data's extra data will be checked and fixed if needed each time the emulator is opened.
|
||||
// Consider removing this at some point in the future when we don't need to worry about old saves.
|
||||
VirtualFileSystem.FixExtraData(LibHacHorizonManager.RyujinxClient);
|
||||
|
||||
AccountManager = new AccountManager(LibHacHorizonManager.RyujinxClient, CommandLineState.Profile);
|
||||
|
||||
VirtualFileSystem.ReloadKeySet();
|
||||
|
||||
ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient, this);
|
||||
|
||||
RefreshFirmwareStatus();
|
||||
}
|
||||
|
||||
protected void CheckLaunchState()
|
||||
{
|
||||
if (ShowKeyErrorOnLoad)
|
||||
{
|
||||
ShowKeyErrorOnLoad = false;
|
||||
|
||||
Dispatcher.UIThread.Post(async () => await
|
||||
UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys, this));
|
||||
}
|
||||
|
||||
if (_deferLoad)
|
||||
{
|
||||
_deferLoad = false;
|
||||
|
||||
LoadApplication(_launchPath, _startFullscreen);
|
||||
}
|
||||
|
||||
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false, this))
|
||||
{
|
||||
Updater.BeginParse(this, false).ContinueWith(task =>
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}");
|
||||
}, TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshFirmwareStatus()
|
||||
{
|
||||
SystemVersion version = null;
|
||||
try
|
||||
{
|
||||
version = ContentManager.GetCurrentFirmwareVersion();
|
||||
}
|
||||
catch (Exception) { }
|
||||
|
||||
bool hasApplet = false;
|
||||
|
||||
if (version != null)
|
||||
{
|
||||
LocaleManager.Instance.UpdateDynamicValue("StatusBarSystemVersion",
|
||||
version.VersionString);
|
||||
|
||||
hasApplet = version.Major > 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
LocaleManager.Instance.UpdateDynamicValue("StatusBarSystemVersion", "0.0");
|
||||
}
|
||||
|
||||
ViewModel.IsAppletMenuActive = hasApplet;
|
||||
}
|
||||
|
||||
private void Load()
|
||||
{
|
||||
VolumeStatus.Click += VolumeStatus_CheckedChanged;
|
||||
|
||||
GameGrid.ApplicationOpened += Application_Opened;
|
||||
|
||||
GameGrid.DataContext = ViewModel;
|
||||
|
||||
GameList.ApplicationOpened += Application_Opened;
|
||||
|
||||
GameList.DataContext = ViewModel;
|
||||
|
||||
LoadHotKeys();
|
||||
}
|
||||
|
||||
protected override void OnOpened(EventArgs e)
|
||||
{
|
||||
base.OnOpened(e);
|
||||
|
||||
CheckLaunchState();
|
||||
}
|
||||
|
||||
public static void UpdateGraphicsConfig()
|
||||
{
|
||||
GraphicsConfig.ResScale = ConfigurationState.Instance.Graphics.ResScale == -1 ? ConfigurationState.Instance.Graphics.ResScaleCustom : ConfigurationState.Instance.Graphics.ResScale;
|
||||
GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy;
|
||||
GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath;
|
||||
GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache;
|
||||
GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression;
|
||||
GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE;
|
||||
}
|
||||
|
||||
public void LoadHotKeys()
|
||||
{
|
||||
HotKeyManager.SetHotKey(FullscreenHotKey, new KeyGesture(Key.Enter, KeyModifiers.Alt));
|
||||
HotKeyManager.SetHotKey(FullscreenHotKey2, new KeyGesture(Key.F11));
|
||||
HotKeyManager.SetHotKey(DockToggleHotKey, new KeyGesture(Key.F9));
|
||||
HotKeyManager.SetHotKey(ExitHotKey, new KeyGesture(Key.Escape));
|
||||
}
|
||||
|
||||
public static void SaveConfig()
|
||||
{
|
||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||
}
|
||||
|
||||
public void UpdateGameMetadata(string titleId)
|
||||
{
|
||||
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
|
||||
{
|
||||
if (DateTime.TryParse(appMetadata.LastPlayed, out DateTime lastPlayedDateTime))
|
||||
{
|
||||
double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds;
|
||||
|
||||
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void PrepareLoadScreen()
|
||||
{
|
||||
using MemoryStream stream = new MemoryStream(ViewModel.SelectedIcon);
|
||||
using var gameIconBmp = SixLabors.ImageSharp.Image.Load<Bgra32>(stream);
|
||||
|
||||
var dominantColor = IconColorPicker.GetFilteredColor(gameIconBmp).ToPixel<Bgra32>();
|
||||
|
||||
const int ColorDivisor = 4;
|
||||
|
||||
Color progressFgColor = Color.FromRgb(dominantColor.R, dominantColor.G, dominantColor.B);
|
||||
Color progressBgColor = Color.FromRgb(
|
||||
(byte)(dominantColor.R / ColorDivisor),
|
||||
(byte)(dominantColor.G / ColorDivisor),
|
||||
(byte)(dominantColor.B / ColorDivisor));
|
||||
|
||||
ViewModel.ProgressBarForegroundColor = new SolidColorBrush(progressFgColor);
|
||||
ViewModel.ProgressBarBackgroundColor = new SolidColorBrush(progressBgColor);
|
||||
}
|
||||
|
||||
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
ViewModel.SearchText = SearchBox.Text;
|
||||
}
|
||||
|
||||
private async void StopEmulation_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (AppHost != null)
|
||||
{
|
||||
await AppHost.ShowExitPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
private async void PauseEmulation_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
AppHost?.Pause();
|
||||
});
|
||||
}
|
||||
|
||||
private async void ResumeEmulation_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
AppHost?.Resume();
|
||||
});
|
||||
}
|
||||
|
||||
private void ScanAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
if (sender is MenuItem)
|
||||
{
|
||||
ViewModel.IsAmiiboRequested = AppHost.Device.System.SearchingForAmiibo(out _);
|
||||
}
|
||||
}
|
||||
|
||||
private void VsyncStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
AppHost.Device.EnableDeviceVsync = !AppHost.Device.EnableDeviceVsync;
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {AppHost.Device.EnableDeviceVsync}");
|
||||
}
|
||||
|
||||
private void DockedStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value;
|
||||
}
|
||||
|
||||
private void AspectRatioStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value;
|
||||
|
||||
ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1;
|
||||
}
|
||||
|
||||
private void VolumeStatus_CheckedChanged(object sender, SplitButtonClickEventArgs e)
|
||||
{
|
||||
var volumeSplitButton = sender as ToggleSplitButton;
|
||||
if (ViewModel.IsGameRunning)
|
||||
{
|
||||
if (!volumeSplitButton.IsChecked)
|
||||
{
|
||||
AppHost.Device.SetVolume(ConfigurationState.Instance.System.AudioVolume);
|
||||
}
|
||||
else
|
||||
{
|
||||
AppHost.Device.SetVolume(0);
|
||||
}
|
||||
|
||||
ViewModel.Volume = AppHost.Device.GetVolume();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClosing(CancelEventArgs e)
|
||||
{
|
||||
if (!_isClosing && AppHost != null && ConfigurationState.Instance.ShowConfirmExit)
|
||||
{
|
||||
e.Cancel = true;
|
||||
|
||||
ConfirmExit();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_isClosing = true;
|
||||
|
||||
if (AppHost != null)
|
||||
{
|
||||
AppHost.AppExit -= AppHost_AppExit;
|
||||
AppHost.AppExit += (sender, e) =>
|
||||
{
|
||||
AppHost = null;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
MainContent = null;
|
||||
|
||||
Close();
|
||||
});
|
||||
};
|
||||
AppHost?.Stop();
|
||||
|
||||
e.Cancel = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationLibrary.CancelLoading();
|
||||
InputManager.Dispose();
|
||||
Program.Exit();
|
||||
|
||||
base.OnClosing(e);
|
||||
}
|
||||
|
||||
private void ConfirmExit()
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
_isClosing = await ContentDialogHelper.CreateExitDialog();
|
||||
|
||||
if (_isClosing)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
141
Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml
Normal file
141
Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml
Normal file
|
@ -0,0 +1,141 @@
|
|||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Ryujinx.Ava.UI.Windows.MotionSettingsWindow"
|
||||
Focusable="True">
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<TextBlock
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{locale:Locale ControllerSettingsMotionGyroSensitivity}" />
|
||||
<Slider
|
||||
Margin="0,-5,0,-5"
|
||||
Width="150"
|
||||
MaxWidth="150"
|
||||
TickFrequency="0.01"
|
||||
IsSnapToTickEnabled="True"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Value="{Binding Sensitivity, Mode=TwoWay}" />
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Margin="5, 0"
|
||||
Text="{Binding Sensitivity, StringFormat=\{0:0\}%}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<TextBlock
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{locale:Locale ControllerSettingsMotionGyroDeadzone}" />
|
||||
<Slider
|
||||
Margin="0,-5,0,-5"
|
||||
Width="150"
|
||||
MaxWidth="150"
|
||||
TickFrequency="0.01"
|
||||
IsSnapToTickEnabled="True"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Value="{Binding GyroDeadzone, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Margin="5, 0"
|
||||
Text="{Binding GyroDeadzone, StringFormat=\{0:0.00\}}" />
|
||||
</StackPanel>
|
||||
<Separator Height="1" Margin="0,5" />
|
||||
<CheckBox Margin="5" IsChecked="{Binding EnableCemuHookMotion}">
|
||||
<TextBlock Margin="0,3,0,0" VerticalAlignment="Center"
|
||||
Text="{locale:Locale ControllerSettingsMotionUseCemuhookCompatibleMotion}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Border Grid.Row="1"
|
||||
Padding="20,5"
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Grid VerticalAlignment="Top">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical">
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{locale:Locale ControllerSettingsMotionServerHost}" />
|
||||
<TextBox
|
||||
Height="30"
|
||||
MinWidth="100"
|
||||
MaxWidth="100"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding DsuServerHost, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Margin="5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text=":" />
|
||||
<TextBox
|
||||
Height="30"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding DsuServerPort, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Margin="0,10,0,0" VerticalAlignment="Center"
|
||||
Text="{locale:Locale ControllerSettingsMotionControllerSlot}" />
|
||||
<ui:NumberBox Grid.Row="0" Grid.Column="1"
|
||||
Name="CemuHookSlotUpDown"
|
||||
SmallChange="1"
|
||||
LargeChange="1"
|
||||
Maximum="4"
|
||||
Minimum="0"
|
||||
Value="{Binding Slot}" />
|
||||
<TextBlock Margin="0,10,0,0" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center"
|
||||
Text="{locale:Locale ControllerSettingsMotionRightJoyConSlot}" />
|
||||
<ui:NumberBox Grid.Row="1" Grid.Column="1"
|
||||
Name="CemuHookRightJoyConSlotUpDown"
|
||||
SmallChange="1"
|
||||
LargeChange="1"
|
||||
Maximum="4"
|
||||
Minimum="0"
|
||||
Value="{Binding AltSlot}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<CheckBox HorizontalAlignment="Center"
|
||||
IsChecked="{Binding MirrorInput, Mode=TwoWay}">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Text="{locale:Locale ControllerSettingsMotionMirrorInput}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
71
Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml.cs
Normal file
71
Ryujinx.Ava/UI/Windows/MotionSettingsWindow.axaml.cs
Normal file
|
@ -0,0 +1,71 @@
|
|||
using Avalonia.Controls;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class MotionSettingsWindow : UserControl
|
||||
{
|
||||
private readonly InputConfiguration<GamepadInputId, StickInputId> _viewmodel;
|
||||
|
||||
public MotionSettingsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = _viewmodel;
|
||||
}
|
||||
|
||||
public MotionSettingsWindow(ControllerSettingsViewModel viewmodel)
|
||||
{
|
||||
var config = viewmodel.Configuration as InputConfiguration<GamepadInputId, StickInputId>;
|
||||
|
||||
_viewmodel = new InputConfiguration<GamepadInputId, StickInputId>()
|
||||
{
|
||||
Slot = config.Slot,
|
||||
AltSlot = config.AltSlot,
|
||||
DsuServerHost = config.DsuServerHost,
|
||||
DsuServerPort = config.DsuServerPort,
|
||||
MirrorInput = config.MirrorInput,
|
||||
EnableMotion = config.EnableMotion,
|
||||
Sensitivity = config.Sensitivity,
|
||||
GyroDeadzone = config.GyroDeadzone,
|
||||
EnableCemuHookMotion = config.EnableCemuHookMotion
|
||||
};
|
||||
|
||||
InitializeComponent();
|
||||
DataContext = _viewmodel;
|
||||
}
|
||||
|
||||
public static async Task Show(ControllerSettingsViewModel viewmodel)
|
||||
{
|
||||
MotionSettingsWindow content = new MotionSettingsWindow(viewmodel);
|
||||
|
||||
ContentDialog contentDialog = new ContentDialog
|
||||
{
|
||||
Title = LocaleManager.Instance["ControllerMotionTitle"],
|
||||
PrimaryButtonText = LocaleManager.Instance["ControllerSettingsSave"],
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = LocaleManager.Instance["ControllerSettingsClose"],
|
||||
Content = content
|
||||
};
|
||||
contentDialog.PrimaryButtonClick += (sender, args) =>
|
||||
{
|
||||
var config = viewmodel.Configuration as InputConfiguration<GamepadInputId, StickInputId>;
|
||||
config.Slot = content._viewmodel.Slot;
|
||||
config.EnableMotion = content._viewmodel.EnableMotion;
|
||||
config.Sensitivity = content._viewmodel.Sensitivity;
|
||||
config.GyroDeadzone = content._viewmodel.GyroDeadzone;
|
||||
config.AltSlot = content._viewmodel.AltSlot;
|
||||
config.DsuServerHost = content._viewmodel.DsuServerHost;
|
||||
config.DsuServerPort = content._viewmodel.DsuServerPort;
|
||||
config.EnableCemuHookMotion = content._viewmodel.EnableCemuHookMotion;
|
||||
config.MirrorInput = content._viewmodel.MirrorInput;
|
||||
};
|
||||
|
||||
await contentDialog.ShowAsync();
|
||||
}
|
||||
}
|
||||
}
|
57
Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml
Normal file
57
Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml
Normal file
|
@ -0,0 +1,57 @@
|
|||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Ryujinx.Ava.UI.Windows.RumbleSettingsWindow"
|
||||
Focusable="True">
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel Orientation="Vertical">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Width="100"
|
||||
TextWrapping="WrapWithOverflow"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{locale:Locale ControllerSettingsRumbleStrongMultiplier}" />
|
||||
<Slider
|
||||
Margin="0,-5,0,-5"
|
||||
Width="200"
|
||||
TickFrequency="0.01"
|
||||
IsSnapToTickEnabled="True"
|
||||
Maximum="10"
|
||||
Minimum="0"
|
||||
Value="{Binding StrongRumble, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Margin="5,0"
|
||||
Text="{Binding StrongRumble, StringFormat=\{0:0.00\}}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Width="100"
|
||||
TextWrapping="WrapWithOverflow"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{locale:Locale ControllerSettingsRumbleWeakMultiplier}" />
|
||||
<Slider
|
||||
Margin="0,-5,0,-5"
|
||||
Width="200"
|
||||
MaxWidth="200"
|
||||
Maximum="10"
|
||||
TickFrequency="0.01"
|
||||
IsSnapToTickEnabled="True"
|
||||
Minimum="0"
|
||||
Value="{Binding WeakRumble, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Margin="5,0"
|
||||
Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
57
Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml.cs
Normal file
57
Ryujinx.Ava/UI/Windows/RumbleSettingsWindow.axaml.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
using Avalonia.Controls;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class RumbleSettingsWindow : UserControl
|
||||
{
|
||||
private readonly InputConfiguration<GamepadInputId, StickInputId> _viewmodel;
|
||||
|
||||
public RumbleSettingsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = _viewmodel;
|
||||
}
|
||||
|
||||
public RumbleSettingsWindow(ControllerSettingsViewModel viewmodel)
|
||||
{
|
||||
var config = viewmodel.Configuration as InputConfiguration<GamepadInputId, StickInputId>;
|
||||
|
||||
_viewmodel = new InputConfiguration<GamepadInputId, StickInputId>()
|
||||
{
|
||||
StrongRumble = config.StrongRumble, WeakRumble = config.WeakRumble
|
||||
};
|
||||
|
||||
InitializeComponent();
|
||||
DataContext = _viewmodel;
|
||||
}
|
||||
|
||||
public static async Task Show(ControllerSettingsViewModel viewmodel)
|
||||
{
|
||||
RumbleSettingsWindow content = new RumbleSettingsWindow(viewmodel);
|
||||
|
||||
ContentDialog contentDialog = new ContentDialog
|
||||
{
|
||||
Title = LocaleManager.Instance["ControllerRumbleTitle"],
|
||||
PrimaryButtonText = LocaleManager.Instance["ControllerSettingsSave"],
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = LocaleManager.Instance["ControllerSettingsClose"],
|
||||
Content = content,
|
||||
};
|
||||
|
||||
contentDialog.PrimaryButtonClick += (sender, args) =>
|
||||
{
|
||||
var config = viewmodel.Configuration as InputConfiguration<GamepadInputId, StickInputId>;
|
||||
config.StrongRumble = content._viewmodel.StrongRumble;
|
||||
config.WeakRumble = content._viewmodel.WeakRumble;
|
||||
};
|
||||
|
||||
await contentDialog.ShowAsync();
|
||||
}
|
||||
}
|
||||
}
|
980
Ryujinx.Ava/UI/Windows/SettingsWindow.axaml
Normal file
980
Ryujinx.Ava/UI/Windows/SettingsWindow.axaml
Normal file
|
@ -0,0 +1,980 @@
|
|||
<window:StyleableWindow
|
||||
x:Class="Ryujinx.Ava.UI.Windows.SettingsWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
Width="1100"
|
||||
Height="768"
|
||||
d:DesignWidth="800"
|
||||
d:DesignHeight="950"
|
||||
MinWidth="800"
|
||||
MinHeight="480"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:SettingsViewModel"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<Design.DataContext>
|
||||
<viewModels:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
<Window.Resources>
|
||||
<helpers:KeyValueConverter x:Key="Key" />
|
||||
</Window.Resources>
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinWidth="600">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Grid.Row="1"
|
||||
IsVisible="False"
|
||||
KeyboardNavigation.IsTabStop="False"/>
|
||||
<Grid Name="Pages" IsVisible="False" Grid.Row="2">
|
||||
<ScrollViewer Name="UiPage"
|
||||
Margin="0,0,2,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGeneral}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnableDiscordIntegration}">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale ToggleDiscordTooltip}"
|
||||
Text="{locale:Locale SettingsTabGeneralEnableDiscordRichPresence}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding CheckUpdatesOnStart}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralCheckUpdatesOnLaunch}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding ShowConfirmExit}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralShowConfirmExitDialog}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding HideCursorOnIdle}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorOnIdle}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGameDirectories}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<ListBox
|
||||
Name="GameList"
|
||||
MinHeight="250"
|
||||
Items="{Binding GameDirectories}" />
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
Name="PathBox"
|
||||
Margin="0"
|
||||
ToolTip.Tip="{locale:Locale AddGameDirBoxTooltip}"
|
||||
VerticalAlignment="Stretch" />
|
||||
<Button
|
||||
Name="AddButton"
|
||||
Grid.Column="1"
|
||||
MinWidth="90"
|
||||
Margin="10,0,0,0"
|
||||
ToolTip.Tip="{locale:Locale AddGameDirTooltip}"
|
||||
Click="AddButton_OnClick">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabGeneralAdd}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveButton"
|
||||
Grid.Column="2"
|
||||
MinWidth="90"
|
||||
Margin="10,0,0,0"
|
||||
ToolTip.Tip="{locale:Locale RemoveGameDirTooltip}"
|
||||
Click="RemoveButton_OnClick">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabGeneralRemove}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralTheme}" />
|
||||
<Grid Margin="10,0,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<CheckBox IsChecked="{Binding EnableCustomTheme}"
|
||||
ToolTip.Tip="{locale:Locale CustomThemeCheckTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeEnableCustomTheme}" />
|
||||
</CheckBox>
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Margin="0,10,0,0"
|
||||
Grid.Row="1"
|
||||
Text="{locale:Locale SettingsTabGeneralThemeCustomTheme}"
|
||||
ToolTip.Tip="{locale:Locale CustomThemePathTooltip}" />
|
||||
<TextBox Margin="0,10,0,0"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Text="{Binding CustomThemePath}" />
|
||||
<Button Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Margin="10,10,0,0"
|
||||
Command="{ReflectionBinding BrowseTheme}"
|
||||
ToolTip.Tip="{locale:Locale CustomThemeBrowseTooltip}"
|
||||
Content="{locale:Locale ButtonBrowse}" />
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Margin="0,10,0,0"
|
||||
Grid.Row="2"
|
||||
Text="{locale:Locale SettingsTabGeneralThemeBaseStyle}" />
|
||||
<ComboBox VerticalAlignment="Center"
|
||||
Margin="0,10,0,0"
|
||||
Grid.Column="1"
|
||||
Grid.Row="2"
|
||||
MinWidth="100"
|
||||
SelectedIndex="{Binding BaseStyleIndex}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeBaseStyleLight}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeBaseStyleDark}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer Name="InputPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Padding="0,0,2,0"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel Margin="4" Orientation="Vertical">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<CheckBox Margin="5,0"
|
||||
ToolTip.Tip="{locale:Locale DockModeToggleTooltip}"
|
||||
IsChecked="{Binding EnableDockedMode}">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabInputEnableDockedMode}" />
|
||||
</CheckBox>
|
||||
<CheckBox Margin="5,0"
|
||||
ToolTip.Tip="{locale:Locale DirectKeyboardTooltip}"
|
||||
IsChecked="{Binding EnableKeyboard}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabInputDirectKeyboardAccess}" />
|
||||
</CheckBox>
|
||||
<CheckBox Margin="5,0"
|
||||
ToolTip.Tip="{locale:Locale DirectMouseTooltip}"
|
||||
IsChecked="{Binding EnableMouse}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabInputDirectMouseAccess}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<window:ControllerSettingsWindow Name="ControllerSettings" Margin="0,0,0,0" MinHeight="600" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer Name="HotkeysPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel Margin="10,5" Orientation="Vertical" Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabHotkeysHotkeys}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleVsyncHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ToggleVsync, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysScreenshotHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.Screenshot, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysShowUiHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ShowUi, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysPauseHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.Pause, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleMuteHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ToggleMute, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleUpHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ResScaleUp, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleDownHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ResScaleDown, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeUpHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.VolumeUp, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeDownHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.VolumeDown, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer Name="SystemPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemCore}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
|
||||
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemSystemRegion}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding Region}"
|
||||
ToolTip.Tip="{locale:Locale RegionTooltip}"
|
||||
HorizontalContentAlignment="Left"
|
||||
Width="350">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionJapan}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionUSA}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionEurope}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionAustralia}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionChina}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionKorea}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionTaiwan}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguage}"
|
||||
ToolTip.Tip="{locale:Locale LanguageTooltip}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding Language}"
|
||||
ToolTip.Tip="{locale:Locale LanguageTooltip}"
|
||||
HorizontalContentAlignment="Left"
|
||||
Width="350">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageJapanese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageAmericanEnglish}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageFrench}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageGerman}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageItalian}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageSpanish}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageChinese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageKorean}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageDutch}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguagePortuguese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageRussian}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageTaiwanese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageBritishEnglish}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageCanadianFrench}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageLatinAmericanSpanish}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageSimplifiedChinese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageTraditionalChinese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageBrazilianPortuguese}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemSystemTimeZone}"
|
||||
ToolTip.Tip="{locale:Locale TimezoneTooltip}"
|
||||
Width="250" />
|
||||
<AutoCompleteBox
|
||||
Name="TimeZoneBox"
|
||||
Width="350"
|
||||
MaxDropDownHeight="500"
|
||||
FilterMode="Contains"
|
||||
Items="{Binding TimeZones}"
|
||||
SelectionChanged="TimeZoneBox_OnSelectionChanged"
|
||||
Text="{Binding Path=TimeZone, Mode=OneWay}"
|
||||
TextChanged="TimeZoneBox_OnTextChanged"
|
||||
ValueMemberBinding="{ReflectionBinding TzMultiBinding}"
|
||||
ToolTip.Tip="{locale:Locale TimezoneTooltip}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemSystemTime}"
|
||||
ToolTip.Tip="{locale:Locale TimeTooltip}"
|
||||
Width="250"/>
|
||||
<DatePicker VerticalAlignment="Center" SelectedDate="{Binding DateOffset}"
|
||||
ToolTip.Tip="{locale:Locale TimeTooltip}"
|
||||
Width="350" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="250,0,0,10" Orientation="Horizontal">
|
||||
<TimePicker
|
||||
VerticalAlignment="Center"
|
||||
ClockIdentifier="24HourClock"
|
||||
SelectedTime="{Binding TimeOffset}"
|
||||
Width="350"
|
||||
ToolTip.Tip="{locale:Locale TimeTooltip}" />
|
||||
</StackPanel>
|
||||
<CheckBox IsChecked="{Binding EnableVsync}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemEnableVsync}"
|
||||
ToolTip.Tip="{locale:Locale VSyncToggleTooltip}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableFsIntegrityChecks}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemEnableFsIntegrityChecks}"
|
||||
ToolTip.Tip="{locale:Locale FsIntegrityToggleTooltip}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemHacks}" />
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemHacksNote}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding ExpandDramSize}"
|
||||
ToolTip.Tip="{locale:Locale DRamTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemExpandDramSize}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding IgnoreMissingServices}"
|
||||
ToolTip.Tip="{locale:Locale IgnoreMissingServicesTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemIgnoreMissingServices}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer
|
||||
Name="CpuPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabCpuCache}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnablePptc}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemEnablePptc}"
|
||||
ToolTip.Tip="{locale:Locale PptcToggleTooltip}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabCpuMemory}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemMemoryManagerMode}"
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerTooltip}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding MemoryMode}"
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerTooltip}"
|
||||
HorizontalContentAlignment="Left"
|
||||
Width="350">
|
||||
<ComboBoxItem
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerSoftwareTooltip}">
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemMemoryManagerModeSoftware}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerHostTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemMemoryManagerModeHost}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerUnsafeTooltip}">
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemMemoryManagerModeHostUnchecked}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer
|
||||
Name="GraphicsPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsAPI}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsBackend}"
|
||||
Width="250" />
|
||||
<ComboBox Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
|
||||
SelectedIndex="{Binding GraphicsBackendIndex}">
|
||||
<ComboBoxItem IsVisible="{Binding IsVulkanAvailable}">
|
||||
<TextBlock Text="Vulkan" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding IsOpenGLAvailable}">
|
||||
<TextBlock Text="OpenGL" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsVulkanSelected}">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsPreferredGpu}"
|
||||
Width="250" />
|
||||
<ComboBox Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}"
|
||||
SelectedIndex="{Binding PreferredGpuIndex}"
|
||||
Items="{Binding AvailableGpus}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsFeatures}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnableShaderCache}"
|
||||
ToolTip.Tip="{locale:Locale ShaderCacheToggleTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsEnableShaderCache}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableTextureRecompression}"
|
||||
ToolTip.Tip="{locale:Locale SettingsEnableTextureRecompressionTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsEnableTextureRecompression}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableMacroHLE}"
|
||||
ToolTip.Tip="{locale:Locale SettingsEnableMacroHLETooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsEnableMacroHLE}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsResolutionScale}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding ResolutionScale}"
|
||||
Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleCustom}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleNative}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale2x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale3x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale4x}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
<ui:NumberBox
|
||||
Margin="10,0,0,0"
|
||||
ToolTip.Tip="{locale:Locale ResolutionScaleEntryTooltip}"
|
||||
MinWidth="150"
|
||||
SmallChange="0.1"
|
||||
LargeChange="1"
|
||||
SimpleNumberFormat="F2"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
IsVisible="{Binding IsCustomResolutionScaleActive}"
|
||||
Maximum="100"
|
||||
Minimum="0.1"
|
||||
Value="{Binding CustomResolutionScale}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale AnisotropyTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding MaxAnisotropy}"
|
||||
Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale AnisotropyTooltip}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabGraphicsAnisotropicFilteringAuto}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering2x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering4x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering8x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering16x}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale AspectRatioTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsAspectRatio}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding AspectRatio}"
|
||||
Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale AspectRatioTooltip}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio4x3}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio16x9}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio16x10}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio21x9}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio32x9}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatioStretch}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale GraphicsBackendThreadingTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsBackendMultithreading}"
|
||||
Width="250" />
|
||||
<ComboBox Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale GalThreadingTooltip}"
|
||||
SelectedIndex="{Binding GraphicsBackendMultithreadingIndex}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale CommonAuto}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale CommonOff}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale CommonOn}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsDeveloperOptions}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsShaderDumpPath}"
|
||||
Width="250" />
|
||||
<TextBox Text="{Binding ShaderDumpPath}"
|
||||
Width="350"
|
||||
ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer
|
||||
Name="AudioPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabAudio}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemAudioBackend}"
|
||||
ToolTip.Tip="{locale:Locale AudioBackendTooltip}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding AudioBackend}"
|
||||
Width="350"
|
||||
HorizontalContentAlignment="Left">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendDummy}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding IsOpenAlEnabled}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendOpenAL}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding IsSoundIoEnabled}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendSoundIO}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding IsSDL2Enabled}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendSDL2}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemAudioVolume}"
|
||||
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
|
||||
Width="250" />
|
||||
<ui:NumberBox Value="{Binding Volume}"
|
||||
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
|
||||
Width="350"
|
||||
SmallChange="1"
|
||||
LargeChange="10"
|
||||
SimpleNumberFormat="F0"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
Minimum="0"
|
||||
Maximum="100" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<Slider Value="{Binding Volume}"
|
||||
Margin="250,0,0,0"
|
||||
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
SmallChange="5"
|
||||
TickFrequency="5"
|
||||
IsSnapToTickEnabled="True"
|
||||
LargeChange="10"
|
||||
Width="350" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer
|
||||
Name="NetworkPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabNetworkConnection}" />
|
||||
<CheckBox Margin="10,0,0,0" IsChecked="{Binding EnableInternetAccess}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemEnableInternetAccess}"
|
||||
ToolTip.Tip="{locale:Locale EnableInternetAccessTooltip}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer
|
||||
Name="LoggingPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingLogging}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnableFileLog}"
|
||||
ToolTip.Tip="{locale:Locale FileLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableLoggingToFile}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableStub}"
|
||||
ToolTip.Tip="{locale:Locale StubLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableStubLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableInfo}"
|
||||
ToolTip.Tip="{locale:Locale InfoLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableInfoLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableWarn}"
|
||||
ToolTip.Tip="{locale:Locale WarnLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableWarningLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableError}"
|
||||
ToolTip.Tip="{locale:Locale ErrorLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableErrorLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableTrace}"
|
||||
ToolTip.Tip="{locale:Locale TraceLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableTraceLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableGuest}"
|
||||
ToolTip.Tip="{locale:Locale GuestLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableGuestLogs}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingDeveloperOptions}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnableDebug}"
|
||||
ToolTip.Tip="{locale:Locale DebugLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableDebugLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableFsAccessLog}"
|
||||
ToolTip.Tip="{locale:Locale FileAccessLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableFsAccessLogs}" />
|
||||
</CheckBox>
|
||||
<StackPanel Margin="0,10,0,0" Orientation="Horizontal" VerticalAlignment="Stretch">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale FSAccessLogModeTooltip}"
|
||||
Text="{locale:Locale SettingsTabLoggingFsGlobalAccessLogMode}"
|
||||
Width="285" />
|
||||
<ui:NumberBox
|
||||
Maximum="3"
|
||||
Minimum="0"
|
||||
Width="150"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
SmallChange="1"
|
||||
LargeChange="1"
|
||||
Value="{Binding FsGlobalAccessLogMode}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,10,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevel}"
|
||||
ToolTip.Tip="{locale:Locale OpenGlLogLevel}"
|
||||
Width="285" />
|
||||
<ComboBox SelectedIndex="{Binding OpenglDebugLevel}"
|
||||
Width="150"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale OpenGlLogLevel}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelNone}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelError}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelPerformance}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelAll}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
<ui:NavigationView Grid.Row="1"
|
||||
IsSettingsVisible="False"
|
||||
Name="NavPanel"
|
||||
IsBackEnabled="False"
|
||||
PaneDisplayMode="Left"
|
||||
Margin="2,10,10,0"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
OpenPaneLength="200">
|
||||
<ui:NavigationView.MenuItems>
|
||||
<ui:NavigationViewItem IsSelected="True"
|
||||
Content="{locale:Locale SettingsTabGeneral}"
|
||||
Tag="UiPage"
|
||||
Icon="New" />
|
||||
<ui:NavigationViewItem
|
||||
Content="{locale:Locale SettingsTabInput}"
|
||||
Tag="InputPage"
|
||||
Icon="Games" />
|
||||
<ui:NavigationViewItem
|
||||
Content="{locale:Locale SettingsTabHotkeys}"
|
||||
Tag="HotkeysPage"
|
||||
Icon="Keyboard" />
|
||||
<ui:NavigationViewItem
|
||||
Content="{locale:Locale SettingsTabSystem}"
|
||||
Tag="SystemPage"
|
||||
Icon="Settings" />
|
||||
<ui:NavigationViewItem
|
||||
Content="{locale:Locale SettingsTabCpu}"
|
||||
Tag="CpuPage">
|
||||
<ui:NavigationViewItem.Icon>
|
||||
<ui:FontIcon FontFamily="avares://Ryujinx.Ava/Assets/Fonts#Segoe Fluent Icons"
|
||||
Glyph="{helpers:GlyphValueConverter Chip}" />
|
||||
</ui:NavigationViewItem.Icon>
|
||||
</ui:NavigationViewItem>
|
||||
<ui:NavigationViewItem
|
||||
Content="{locale:Locale SettingsTabGraphics}"
|
||||
Tag="GraphicsPage"
|
||||
Icon="Image" />
|
||||
<ui:NavigationViewItem
|
||||
Content="{locale:Locale SettingsTabAudio}"
|
||||
Icon="Audio"
|
||||
Tag="AudioPage" />
|
||||
<ui:NavigationViewItem
|
||||
Content="{locale:Locale SettingsTabNetwork}"
|
||||
Tag="NetworkPage"
|
||||
Icon="Globe" />
|
||||
<ui:NavigationViewItem
|
||||
Content="{locale:Locale SettingsTabLogging}"
|
||||
Tag="LoggingPage"
|
||||
Icon="Document" />
|
||||
</ui:NavigationView.MenuItems>
|
||||
</ui:NavigationView>
|
||||
<ReversibleStackPanel
|
||||
Grid.Row="2"
|
||||
Margin="10"
|
||||
Spacing="10"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
ReverseOrder="{ReflectionBinding IsMacOS}">
|
||||
<Button
|
||||
HotKey="Enter"
|
||||
Classes="accent"
|
||||
Content="{locale:Locale SettingsButtonOk}"
|
||||
Command="{ReflectionBinding OkButton}" />
|
||||
<Button
|
||||
HotKey="Escape"
|
||||
Content="{locale:Locale SettingsButtonCancel}"
|
||||
Command="{ReflectionBinding CancelButton}" />
|
||||
<Button
|
||||
Content="{locale:Locale SettingsButtonApply}"
|
||||
Command="{ReflectionBinding ApplyButton}" />
|
||||
</ReversibleStackPanel>
|
||||
</Grid>
|
||||
</window:StyleableWindow>
|
213
Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs
Normal file
213
Ryujinx.Ava/UI/Windows/SettingsWindow.axaml.cs
Normal file
|
@ -0,0 +1,213 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using FluentAvalonia.Core;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Input.Assigner;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class SettingsWindow : StyleableWindow
|
||||
{
|
||||
private ButtonKeyAssigner _currentAssigner;
|
||||
|
||||
internal SettingsViewModel ViewModel { get; set; }
|
||||
|
||||
public SettingsWindow(VirtualFileSystem virtualFileSystem, ContentManager contentManager)
|
||||
{
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["Settings"]}";
|
||||
|
||||
ViewModel = new SettingsViewModel(virtualFileSystem, contentManager, this);
|
||||
DataContext = ViewModel;
|
||||
|
||||
InitializeComponent();
|
||||
Load();
|
||||
|
||||
FuncMultiValueConverter<string, string> converter = new(parts => string.Format("{0} {1} {2}", parts.ToArray()).Trim());
|
||||
MultiBinding tzMultiBinding = new() { Converter = converter };
|
||||
tzMultiBinding.Bindings.Add(new Binding("UtcDifference"));
|
||||
tzMultiBinding.Bindings.Add(new Binding("Location"));
|
||||
tzMultiBinding.Bindings.Add(new Binding("Abbreviation"));
|
||||
|
||||
TimeZoneBox.ValueMemberBinding = tzMultiBinding;
|
||||
}
|
||||
|
||||
public SettingsWindow()
|
||||
{
|
||||
ViewModel = new SettingsViewModel();
|
||||
DataContext = ViewModel;
|
||||
|
||||
InitializeComponent();
|
||||
Load();
|
||||
}
|
||||
|
||||
private void Load()
|
||||
{
|
||||
Pages.Children.Clear();
|
||||
NavPanel.SelectionChanged += NavPanelOnSelectionChanged;
|
||||
NavPanel.SelectedItem = NavPanel.MenuItems.ElementAt(0);
|
||||
}
|
||||
|
||||
private void Button_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ToggleButton button)
|
||||
{
|
||||
if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentAssigner == null && (bool)button.IsChecked)
|
||||
{
|
||||
_currentAssigner = new ButtonKeyAssigner(button);
|
||||
|
||||
FocusManager.Instance.Focus(this, NavigationMethod.Pointer);
|
||||
|
||||
PointerPressed += MouseClick;
|
||||
|
||||
IKeyboard keyboard = (IKeyboard)ViewModel.AvaloniaKeyboardDriver.GetGamepad(ViewModel.AvaloniaKeyboardDriver.GamepadsIds[0]);
|
||||
IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard);
|
||||
|
||||
_currentAssigner.GetInputAndAssign(assigner);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_currentAssigner != null)
|
||||
{
|
||||
ToggleButton oldButton = _currentAssigner.ToggledButton;
|
||||
|
||||
_currentAssigner.Cancel();
|
||||
_currentAssigner = null;
|
||||
|
||||
button.IsChecked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Button_Unchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_currentAssigner?.Cancel();
|
||||
_currentAssigner = null;
|
||||
}
|
||||
|
||||
private void MouseClick(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
bool shouldUnbind = false;
|
||||
|
||||
if (e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed)
|
||||
{
|
||||
shouldUnbind = true;
|
||||
}
|
||||
|
||||
_currentAssigner?.Cancel(shouldUnbind);
|
||||
|
||||
PointerPressed -= MouseClick;
|
||||
}
|
||||
|
||||
private void NavPanelOnSelectionChanged(object sender, NavigationViewSelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.SelectedItem is NavigationViewItem navitem)
|
||||
{
|
||||
NavPanel.Content = navitem.Tag.ToString() switch
|
||||
{
|
||||
"UiPage" => UiPage,
|
||||
"InputPage" => InputPage,
|
||||
"HotkeysPage" => HotkeysPage,
|
||||
"SystemPage" => SystemPage,
|
||||
"CpuPage" => CpuPage,
|
||||
"GraphicsPage" => GraphicsPage,
|
||||
"AudioPage" => AudioPage,
|
||||
"NetworkPage" => NetworkPage,
|
||||
"LoggingPage" => LoggingPage,
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async void AddButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
string path = PathBox.Text;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.GameDirectories.Contains(path))
|
||||
{
|
||||
ViewModel.GameDirectories.Add(path);
|
||||
ViewModel.DirectoryChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
path = await new OpenFolderDialog().ShowAsync(this);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
ViewModel.GameDirectories.Add(path);
|
||||
ViewModel.DirectoryChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
int oldIndex = GameList.SelectedIndex;
|
||||
|
||||
foreach (string path in new List<string>(GameList.SelectedItems.Cast<string>()))
|
||||
{
|
||||
ViewModel.GameDirectories.Remove(path);
|
||||
ViewModel.DirectoryChanged = true;
|
||||
}
|
||||
|
||||
if (GameList.ItemCount > 0)
|
||||
{
|
||||
GameList.SelectedIndex = oldIndex < GameList.ItemCount ? oldIndex : 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void TimeZoneBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.AddedItems != null && e.AddedItems.Count > 0)
|
||||
{
|
||||
if (e.AddedItems[0] is TimeZone timeZone)
|
||||
{
|
||||
e.Handled = true;
|
||||
|
||||
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TimeZoneBox_OnTextChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is AutoCompleteBox box)
|
||||
{
|
||||
if (box.SelectedItem != null && box.SelectedItem is TimeZone timeZone)
|
||||
{
|
||||
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
ControllerSettings.Dispose();
|
||||
|
||||
_currentAssigner?.Cancel();
|
||||
_currentAssigner = null;
|
||||
|
||||
base.OnClosed(e);
|
||||
}
|
||||
}
|
||||
}
|
39
Ryujinx.Ava/UI/Windows/StyleableWindow.cs
Normal file
39
Ryujinx.Ava/UI/Windows/StyleableWindow.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public class StyleableWindow : Window
|
||||
{
|
||||
public IBitmap IconImage { get; set; }
|
||||
|
||||
public StyleableWindow()
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
TransparencyLevelHint = WindowTransparencyLevel.None;
|
||||
|
||||
using Stream stream = Assembly.GetAssembly(typeof(Ryujinx.Ui.Common.Configuration.ConfigurationState)).GetManifestResourceStream("Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png");
|
||||
|
||||
Icon = new WindowIcon(stream);
|
||||
stream.Position = 0;
|
||||
IconImage = new Bitmap(stream);
|
||||
}
|
||||
|
||||
protected override void OnOpened(EventArgs e)
|
||||
{
|
||||
base.OnOpened(e);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
|
||||
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome | ExtendClientAreaChromeHints.OSXThickTitleBar;
|
||||
}
|
||||
}
|
||||
}
|
115
Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml
Normal file
115
Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml
Normal file
|
@ -0,0 +1,115 @@
|
|||
<window:StyleableWindow
|
||||
x:Class="Ryujinx.Ava.UI.Windows.TitleUpdateWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
Width="600"
|
||||
Height="400"
|
||||
MinWidth="600"
|
||||
MinHeight="400"
|
||||
MaxWidth="600"
|
||||
MaxHeight="400"
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<Grid Margin="15">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Name="Heading"
|
||||
Grid.Row="1"
|
||||
MaxWidth="500"
|
||||
Margin="20,15,20,20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
LineHeight="18"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1">
|
||||
<ScrollViewer
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Items="{Binding _titleUpdates}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<RadioButton
|
||||
Padding="8,0"
|
||||
VerticalContentAlignment="Center"
|
||||
GroupName="Update"
|
||||
IsChecked="{Binding IsEnabled, Mode=TwoWay}">
|
||||
<Label
|
||||
Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
Content="{Binding Label}"
|
||||
FontSize="12" />
|
||||
</RadioButton>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
<DockPanel
|
||||
Grid.Row="3"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch">
|
||||
<DockPanel Margin="0" HorizontalAlignment="Left">
|
||||
<Button
|
||||
Name="AddButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Add}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralAdd}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding RemoveSelected}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralRemove}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveAllButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding RemoveAll}">
|
||||
<TextBlock Text="{locale:Locale DlcManagerRemoveAllButton}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
<DockPanel Margin="0" HorizontalAlignment="Right">
|
||||
<Button
|
||||
Name="SaveButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Save}">
|
||||
<TextBlock Text="{locale:Locale SettingsButtonSave}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="CancelButton"
|
||||
MinWidth="90"
|
||||
Margin="5"
|
||||
Command="{Binding Close}">
|
||||
<TextBlock Text="{locale:Locale InputDialogCancel}" />
|
||||
</Button>
|
||||
</DockPanel>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</window:StyleableWindow>
|
271
Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs
Normal file
271
Ryujinx.Ava/UI/Windows/TitleUpdateWindow.axaml.cs
Normal file
|
@ -0,0 +1,271 @@
|
|||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Ns;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Path = System.IO.Path;
|
||||
using SpanHelpers = LibHac.Common.SpanHelpers;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class TitleUpdateWindow : StyleableWindow
|
||||
{
|
||||
private readonly string _titleUpdateJsonPath;
|
||||
private TitleUpdateMetadata _titleUpdateWindowData;
|
||||
|
||||
private VirtualFileSystem _virtualFileSystem { get; }
|
||||
private AvaloniaList<TitleUpdateModel> _titleUpdates { get; set; }
|
||||
|
||||
private ulong _titleId { get; }
|
||||
private string _titleName { get; }
|
||||
|
||||
public TitleUpdateWindow()
|
||||
{
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["UpdateWindowTitle"]} - {_titleName} ({_titleId:X16})";
|
||||
}
|
||||
|
||||
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
||||
{
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_titleUpdates = new AvaloniaList<TitleUpdateModel>();
|
||||
|
||||
_titleId = titleId;
|
||||
_titleName = titleName;
|
||||
|
||||
_titleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
|
||||
|
||||
try
|
||||
{
|
||||
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_titleUpdateJsonPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_titleUpdateWindowData = new TitleUpdateMetadata
|
||||
{
|
||||
Selected = "",
|
||||
Paths = new List<string>()
|
||||
};
|
||||
}
|
||||
|
||||
DataContext = this;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance["UpdateWindowTitle"]} - {_titleName} ({_titleId:X16})";
|
||||
|
||||
LoadUpdates();
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
private void PrintHeading()
|
||||
{
|
||||
Heading.Text = string.Format(LocaleManager.Instance["GameUpdateWindowHeading"], _titleUpdates.Count, _titleName, _titleId.ToString("X16"));
|
||||
}
|
||||
|
||||
private void LoadUpdates()
|
||||
{
|
||||
_titleUpdates.Add(new TitleUpdateModel(default, string.Empty, true));
|
||||
|
||||
foreach (string path in _titleUpdateWindowData.Paths)
|
||||
{
|
||||
AddUpdate(path);
|
||||
}
|
||||
|
||||
if (_titleUpdateWindowData.Selected == "")
|
||||
{
|
||||
_titleUpdates[0].IsEnabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
TitleUpdateModel selected = _titleUpdates.FirstOrDefault(x => x.Path == _titleUpdateWindowData.Selected);
|
||||
List<TitleUpdateModel> enabled = _titleUpdates.Where(x => x.IsEnabled).ToList();
|
||||
|
||||
foreach (TitleUpdateModel update in enabled)
|
||||
{
|
||||
update.IsEnabled = false;
|
||||
}
|
||||
|
||||
if (selected != null)
|
||||
{
|
||||
selected.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
SortUpdates();
|
||||
}
|
||||
|
||||
private void AddUpdate(string path)
|
||||
{
|
||||
if (File.Exists(path) && !_titleUpdates.Any(x => x.Path == path))
|
||||
{
|
||||
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
||||
|
||||
try
|
||||
{
|
||||
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, new PartitionFileSystem(file.AsStorage()), _titleId.ToString("x16"), 0);
|
||||
|
||||
if (controlNca != null && patchNca != null)
|
||||
{
|
||||
ApplicationControlProperty controlData = new();
|
||||
|
||||
using UniqueRef<IFile> nacpFile = new();
|
||||
|
||||
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref(), "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
|
||||
|
||||
_titleUpdates.Add(new TitleUpdateModel(controlData, path));
|
||||
|
||||
foreach (var update in _titleUpdates)
|
||||
{
|
||||
update.IsEnabled = false;
|
||||
}
|
||||
|
||||
_titleUpdates.Last().IsEnabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance["DialogUpdateAddUpdateErrorMessage"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(string.Format(LocaleManager.Instance["DialogDlcLoadNcaErrorMessage"], ex.Message, path));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveUpdates(bool removeSelectedOnly = false)
|
||||
{
|
||||
if (removeSelectedOnly)
|
||||
{
|
||||
_titleUpdates.RemoveAll(_titleUpdates.Where(x => x.IsEnabled && !x.IsNoUpdate).ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
_titleUpdates.RemoveAll(_titleUpdates.Where(x => !x.IsNoUpdate).ToList());
|
||||
}
|
||||
|
||||
_titleUpdates.FirstOrDefault(x => x.IsNoUpdate).IsEnabled = true;
|
||||
|
||||
SortUpdates();
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
public void RemoveSelected()
|
||||
{
|
||||
RemoveUpdates(true);
|
||||
}
|
||||
|
||||
public void RemoveAll()
|
||||
{
|
||||
RemoveUpdates();
|
||||
}
|
||||
|
||||
public async void Add()
|
||||
{
|
||||
OpenFileDialog dialog = new()
|
||||
{
|
||||
Title = LocaleManager.Instance["SelectUpdateDialogTitle"],
|
||||
AllowMultiple = true
|
||||
};
|
||||
|
||||
dialog.Filters.Add(new FileDialogFilter
|
||||
{
|
||||
Name = "NSP",
|
||||
Extensions = { "nsp" }
|
||||
});
|
||||
|
||||
string[] files = await dialog.ShowAsync(this);
|
||||
|
||||
if (files != null)
|
||||
{
|
||||
foreach (string file in files)
|
||||
{
|
||||
AddUpdate(file);
|
||||
}
|
||||
}
|
||||
|
||||
SortUpdates();
|
||||
PrintHeading();
|
||||
}
|
||||
|
||||
private void SortUpdates()
|
||||
{
|
||||
var list = _titleUpdates.ToList();
|
||||
|
||||
list.Sort((first, second) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(first.Control.DisplayVersionString.ToString()))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (string.IsNullOrEmpty(second.Control.DisplayVersionString.ToString()))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Version.Parse(first.Control.DisplayVersionString.ToString()).CompareTo(Version.Parse(second.Control.DisplayVersionString.ToString())) * -1;
|
||||
});
|
||||
|
||||
_titleUpdates.Clear();
|
||||
_titleUpdates.AddRange(list);
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
_titleUpdateWindowData.Paths.Clear();
|
||||
|
||||
_titleUpdateWindowData.Selected = "";
|
||||
|
||||
foreach (TitleUpdateModel update in _titleUpdates)
|
||||
{
|
||||
_titleUpdateWindowData.Paths.Add(update.Path);
|
||||
|
||||
if (update.IsEnabled)
|
||||
{
|
||||
_titleUpdateWindowData.Selected = update.Path;
|
||||
}
|
||||
}
|
||||
|
||||
using (FileStream titleUpdateJsonStream = File.Create(_titleUpdateJsonPath, 4096, FileOptions.WriteThrough))
|
||||
{
|
||||
titleUpdateJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
|
||||
}
|
||||
|
||||
if (Owner is MainWindow window)
|
||||
{
|
||||
window.ViewModel.LoadApplications();
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue