Avalonia UI - Part 1 (#3270)

* avalonia part 1

* remove vulkan ui backend

* move ui common files to ui common project

* get name for oading screen from device

* rebase.

* review 1

* review 1.1

* review

* cleanup

* addressed review

* use cancellation token

* review

* review

* rebased

* cancel library loading when closing window

* remove star  image, use fonticon instead

* delete render control frame buffer when game ends. change position of fav star

* addressed @Thog review

* ensure the right ui is downloaded in updates

* fix crash when showing not supported dialog during controller request

* add prefix to artifact names

* Auto-format Avalonia project

* Fix input

* Fix build, simplify app disposal

* remove nv stutter thread

* addressed review

* add missing change

* maintain window size if new size is zero length

* add game, handheld, docked to local

* reverse scale main window

* Update de_DE.json

* Update de_DE.json

* Update de_DE.json

* Update italian json

* Update it_IT.json

* let render timer poll with no wait

* remove unused code

* more unused code

* enabled tiered compilation and trimming

* check if window event is not closed before signaling

* fix atmospher case

* locale fix

* locale fix

* remove explicit tiered compilation declarations

* Remove ) it_IT.json

* Remove ) de_DE.json

* Update it_IT.json

* Update pt_BR locale with latest strings

* Remove ')'

* add more strings to locale

* update locale

* remove extra slash

* remove extra slash

* set firmware version to 0 if key's not found

* fix

* revert timer changes

* lock  on object instead

* Update it_IT.json

* remove unused method

* add load screen text to locale

* drop swap event

* Update de_DE.json

* Update de_DE.json

* do null check when stopping emulator

* Update de_DE.json

* Create tr_TR.json

* Add tr_TR

* Add tr_TR + Turkish

* Update it_IT.json

* Update Ryujinx.Ava/Input/AvaloniaMappingHelper.cs

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Apply suggestions from code review

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* Apply suggestions from code review

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* addressed review

* Update Ryujinx.Ava/Ui/Backend/OpenGl/OpenGlRenderTarget.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* use avalonia's inbuilt renderer on linux

* removed whitespace

* workaround for queue render crash with vsync off

* drop custom backend

* format files

* fix not closing issue

* remove warnings

* rebase

* update avalonia library

* Reposition the Text and Button on About Page

* Assign build version

* Remove appveyor text

Co-authored-by: gdk <gab.dark.100@gmail.com>
Co-authored-by: Niwu34 <67392333+Niwu34@users.noreply.github.com>
Co-authored-by: Antonio Brugnolo <36473846+AntoSkate@users.noreply.github.com>
Co-authored-by: aegiff <99728970+aegiff@users.noreply.github.com>
Co-authored-by: Ac_K <Acoustik666@gmail.com>
Co-authored-by: MostlyWhat <78652091+MostlyWhat@users.noreply.github.com>
This commit is contained in:
Emmanuel Hansen 2022-05-15 11:30:15 +00:00 committed by GitHub
parent 9ba73ffbe5
commit deb99d2cae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
161 changed files with 17179 additions and 855 deletions

View file

@ -0,0 +1,149 @@
using Avalonia.Controls;
using Avalonia.Threading;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Controls;
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;
using System.Threading.Tasks;
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)
{
// TODO : Show controller applet. Needs settings window to be implemented.
Dispatcher.UIThread.InvokeAsync(() =>
{
ContentDialogHelper.ShowNotAvailableMessage(_parent);
});
return true;
}
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;
ContentDialogHelper.CreateErrorDialog(_parent, 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.Length > 1 && (int)response != buttons.Length - 1)
{
showDetails = true;
}
dialogCloseEvent.Set();
msgDialog.Close();
}
catch (Exception ex)
{
dialogCloseEvent.Set();
ContentDialogHelper.CreateErrorDialog(_parent, string.Format(LocaleManager.Instance["DialogErrorAppletErrorExceptionMessage"], ex));
}
});
dialogCloseEvent.WaitOne();
return showDetails;
}
public IDynamicTextInputHandler CreateDynamicTextInputHandler()
{
return new AvaloniaDynamicTextInputHandler(_parent);
}
}
}

View file

@ -0,0 +1,162 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Threading;
using OpenTK.Windowing.Common;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.Ui.Controls;
using Ryujinx.Ava.Ui.Windows;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.HLE.Ui;
using System;
using System.Threading;
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, TextInputEventArgs e)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
if (_canProcessInput)
{
_hiddenTextBox.SendText(e.AsString);
}
});
}
private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, Avalonia.Input.KeyEventArgs e)
{
var key = (Key)AvaloniaMappingHelper.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, Avalonia.Input.KeyEventArgs e)
{
var key = (Key)AvaloniaMappingHelper.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.GlRenderer.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;
});
}
}
}

View 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();
}
}
}
}

View file

@ -0,0 +1,31 @@
<Window 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"
mc:Ignorable="d"
x:Class="Ryujinx.Ava.Ui.Applet.ErrorAppletWindow"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
CanResize="False"
SizeToContent="Height"
Width="450"
Height="340"
Title="{locale:Locale ErrorWindowTitle}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="20">
<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" Margin="5, 10, 20 , 10" Grid.Column="0"
Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" Height="80" MinWidth="50" />
<TextBlock Grid.Row="1" Margin="10" Grid.Column="1" VerticalAlignment="Stretch" TextWrapping="Wrap"
Text="{Binding Message}" />
<StackPanel Name="ButtonStack" Margin="10" Spacing="10" Grid.Row="2" Grid.Column="1"
HorizontalAlignment="Right" Orientation="Horizontal" />
</Grid>
</Window>

View file

@ -0,0 +1,89 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Windows;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Ui.Applet
{
public 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; }
public StackPanel ButtonStack { 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;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
ButtonStack = this.FindControl<StackPanel>("ButtonStack");
}
}
}

View file

@ -0,0 +1,32 @@
<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:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
mc:Ignorable="d"
x:Class="Ryujinx.Ava.Ui.Controls.SwkbdAppletDialog"
Width="400">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="20">
<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" VerticalAlignment="Center" Grid.RowSpan="5" Margin="5, 10, 20 , 10"
Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" Height="80"
MinWidth="50" />
<TextBlock Grid.Row="1" Margin="5" Grid.Column="1" Text="{Binding MainText}" TextWrapping="Wrap" />
<TextBlock Grid.Row="2" Margin="5" Grid.Column="1" Text="{Binding SecondaryText}" TextWrapping="Wrap" />
<TextBox Name="Input" KeyUp="Message_KeyUp" UseFloatingWatermark="True" TextInput="Message_TextInput"
Text="{Binding Message}" Grid.Row="2"
Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Stretch" TextWrapping="Wrap" />
<TextBlock Name="Error" Margin="5" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Stretch"
TextWrapping="Wrap" />
</Grid>
</UserControl>

View file

@ -0,0 +1,152 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Windows;
using Ryujinx.HLE.HOS.Applets;
using System;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Ui.Controls
{
public 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();
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 TextBlock Error { get; private set; }
public TextBox Input { get; set; }
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
Error = this.FindControl<TextBlock>("Error");
Input = this.FindControl<TextBox>("Input");
Input.Watermark = _placeholder;
Input.AddHandler(TextInputEvent, Message_TextInput, RoutingStrategies.Tunnel, true);
}
public static async Task<(UserResult Result, string Input)> ShowInputDialog(StyleableWindow window, string title, SoftwareKeyboardUiArgs args)
{
ContentDialog contentDialog = window.ContentDialog;
UserResult result = UserResult.Cancel;
SwkbdAppletDialog content = new SwkbdAppletDialog(args.HeaderText, args.SubtitleText, args.GuideText)
{
Message = args.InitialText ?? ""
};
string input = string.Empty;
content.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax);
if (contentDialog != null)
{
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;
await contentDialog.ShowAsync();
contentDialog.Closed -= handler;
}
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);
}
}
}
}

View file

@ -0,0 +1,16 @@
using Avalonia.Interactivity;
using Ryujinx.Ui.App.Common;
namespace Ryujinx.Ava.Ui.Controls
{
public class ApplicationOpenedEventArgs : RoutedEventArgs
{
public ApplicationData Application { get; }
public ApplicationOpenedEventArgs(ApplicationData application, RoutedEvent routedEvent)
{
Application = application;
RoutedEvent = routedEvent;
}
}
}

View file

@ -0,0 +1,16 @@
using SPB.Graphics;
using System;
using System.Runtime.Versioning;
namespace Ryujinx.Ava.Ui.Controls
{
[SupportedOSPlatform("linux")]
public class AvaloniaGlxContext : SPB.Platform.GLX.GLXOpenGLContext
{
public AvaloniaGlxContext(IntPtr handle)
: base(FramebufferFormat.Default, 0, 0, 0, false, null)
{
ContextHandle = handle;
}
}
}

View file

@ -0,0 +1,16 @@
using SPB.Graphics;
using System;
using System.Runtime.Versioning;
namespace Ryujinx.Ava.Ui.Controls
{
[SupportedOSPlatform("windows")]
public class AvaloniaWglContext : SPB.Platform.WGL.WGLOpenGLContext
{
public AvaloniaWglContext(IntPtr handle)
: base(FramebufferFormat.Default, 0, 0, 0, false, null)
{
ContextHandle = handle;
}
}
}

View 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.Controls
{
public 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();
}
}
}

View file

@ -0,0 +1,357 @@
using Avalonia.Controls;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.Ava.Ui.Windows;
using Ryujinx.Common.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Ui.Controls
{
public static class ContentDialogHelper
{
private static bool _isChoiceDialogOpen;
private async static Task<UserResult> ShowContentDialog(
StyleableWindow window,
string title,
string primaryText,
string secondaryText,
string primaryButton,
string secondaryButton,
string closeButton,
int iconSymbol,
UserResult primaryButtonResult = UserResult.Ok)
{
UserResult result = UserResult.None;
ContentDialog contentDialog = window.ContentDialog;
await ShowDialog();
async Task ShowDialog()
{
if (contentDialog != null)
{
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.CloseButtonCommand = MiniCommand.Create(() =>
{
result = UserResult.Cancel;
});
await contentDialog.ShowAsync(ContentDialogPlacement.Popup);
};
}
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;
ContentDialog contentDialog = window.ContentDialog;
Window overlay = window;
if (contentDialog != null)
{
contentDialog.PrimaryButtonClick += DeferClose;
contentDialog.Title = title;
contentDialog.PrimaryButtonText = primaryButton;
contentDialog.SecondaryButtonText = secondaryButton;
contentDialog.CloseButtonText = closeButton;
contentDialog.Content = CreateDialogTextContent(primaryText, secondaryText, iconSymbol);
contentDialog.PrimaryButtonCommand = MiniCommand.Create(() =>
{
result = primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok;
});
contentDialog.SecondaryButtonCommand = MiniCommand.Create(() =>
{
result = UserResult.No;
});
contentDialog.CloseButtonCommand = MiniCommand.Create(() =>
{
result = UserResult.Cancel;
});
await contentDialog.ShowAsync(ContentDialogPlacement.Popup);
};
return result;
async void DeferClose(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
if (startedDeferring)
{
return;
}
startedDeferring = true;
var deferral = args.GetDeferral();
result = primaryButton == LocaleManager.Instance["InputDialogYes"] ? UserResult.Yes : UserResult.Ok;
contentDialog.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(overlay);
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 Avalonia.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 Avalonia.Thickness(5),
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
MaxWidth = 450
};
TextBlock secondaryLabel = new TextBlock()
{
Text = secondaryText,
Margin = new Avalonia.Thickness(5),
TextWrapping = Avalonia.Media.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(
StyleableWindow window,
string primary,
string secondaryText,
string acceptButton,
string closeButton,
string title)
{
return await ShowContentDialog(
window,
title,
primary,
secondaryText,
acceptButton,
"",
closeButton,
(int)Symbol.Important);
}
internal static async Task<UserResult> CreateConfirmationDialog(
StyleableWindow window,
string primaryText,
string secondaryText,
string acceptButtonText,
string cancelButtonText,
string title,
UserResult primaryButtonResult = UserResult.Yes)
{
return await ShowContentDialog(
window,
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 void CreateUpdaterInfoDialog(StyleableWindow window, string primary, string secondaryText)
{
await ShowContentDialog(
window,
LocaleManager.Instance["DialogUpdaterTitle"],
primary,
secondaryText,
"",
"",
LocaleManager.Instance["InputDialogOk"],
(int)Symbol.Important);
}
internal static async void ShowNotAvailableMessage(StyleableWindow window)
{
// Temporary placeholder for features to be added
await ShowContentDialog(
window,
"Feature Not Available",
"The selected feature is not available in this version.",
"",
"",
"",
LocaleManager.Instance["InputDialogOk"],
(int)Symbol.Important);
}
internal static async void CreateWarningDialog(StyleableWindow window, string primary, string secondaryText)
{
await ShowContentDialog(
window,
LocaleManager.Instance["DialogWarningTitle"],
primary,
secondaryText,
"",
"",
LocaleManager.Instance["InputDialogOk"],
(int)Symbol.Important);
}
internal static async void CreateErrorDialog(StyleableWindow owner, string errorMessage, string secondaryErrorMessage = "")
{
Logger.Error?.Print(LogClass.Application, errorMessage);
await ShowContentDialog(
owner,
LocaleManager.Instance["DialogErrorTitle"],
LocaleManager.Instance["DialogErrorMessage"],
errorMessage,
secondaryErrorMessage,
"",
LocaleManager.Instance["InputDialogOk"],
(int)Symbol.Dismiss);
}
internal static async Task<bool> CreateChoiceDialog(StyleableWindow window, string title, string primary, string secondaryText)
{
if (_isChoiceDialogOpen)
{
return false;
}
_isChoiceDialogOpen = true;
UserResult response =
await ShowContentDialog(
window,
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(StyleableWindow owner)
{
return await CreateChoiceDialog(
owner,
LocaleManager.Instance["DialogExitTitle"],
LocaleManager.Instance["DialogExitMessage"],
LocaleManager.Instance["DialogExitSubMessage"]);
}
internal static async Task<bool> CreateStopEmulationDialog(StyleableWindow owner)
{
return await CreateChoiceDialog(
owner,
LocaleManager.Instance["DialogStopEmulationTitle"],
LocaleManager.Instance["DialogStopEmulationMessage"],
LocaleManager.Instance["DialogExitSubMessage"]);
}
internal static async Task<string> CreateInputDialog(
string title,
string mainText,
string subText,
StyleableWindow owner,
uint maxLength = int.MaxValue,
string input = "")
{
var result = await InputDialog.ShowInputDialog(
owner,
title,
mainText,
input,
subText,
maxLength);
if (result.Result == UserResult.Ok)
{
return result.Input;
}
return string.Empty;
}
}
}

View file

@ -0,0 +1,188 @@
<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:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.Ui.Controls.GameGridView">
<UserControl.Resources>
<controls: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 OpenDlcManager}"
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"
DoubleTapped="GameList_DoubleTapped"
SelectionChanged="GameList_SelectionChanged"
ContextFlyout="{StaticResource GameContextMenu}"
VerticalAlignment="Stretch"
Items="{Binding AppsObservableList}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<flex:FlexPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" JustifyContent="Center"
AlignContent="FlexStart" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="5" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Background" Value="{DynamicResource SystemAccentColorDark3}" />
<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>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.Styles>
<Style Selector="ui|SymbolIcon.small.icon">
<Setter Property="FontSize" Value="15" />
</Style>
<Style Selector="ui|SymbolIcon.normal.icon">
<Setter Property="FontSize" Value="19" />
</Style>
<Style Selector="ui|SymbolIcon.large.icon">
<Setter Property="FontSize" Value="23" />
</Style>
<Style Selector="ui|SymbolIcon.huge.icon">
<Setter Property="FontSize" Value="26" />
</Style>
</Grid.Styles>
<Border
Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
HorizontalAlignment="Stretch"
Padding="{Binding $parent[UserControl].DataContext.GridItemPadding}" CornerRadius="5"
VerticalAlignment="Stretch" Margin="0" ClipToBounds="True">
<Grid Margin="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="0" Grid.Row="0"
Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
<StackPanel IsVisible="{Binding $parent[UserControl].DataContext.ShowNames}"
Height="50" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Margin="5" Grid.Row="1">
<TextBlock Text="{Binding TitleName}" TextAlignment="Center" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
</StackPanel>
</Grid>
</Border>
<ui:SymbolIcon Classes.icon="true" Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
Foreground="Yellow" Symbol="StarFilled"
IsVisible="{Binding Favorite}" Margin="5" VerticalAlignment="Top"
HorizontalAlignment="Left" />
<ui:SymbolIcon Classes.icon="true" Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
Foreground="Black" Symbol="Star"
IsVisible="{Binding Favorite}" Margin="5" VerticalAlignment="Top"
HorizontalAlignment="Left" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>

View file

@ -0,0 +1,82 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using LibHac.Common;
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;
}
}
}
}
}

View file

@ -0,0 +1,188 @@
<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:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
xmlns:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.Ui.Controls.GameListView">
<UserControl.Resources>
<controls: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 OpenDlcManager}"
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"
DoubleTapped="GameList_DoubleTapped"
SelectionChanged="GameList_SelectionChanged"
ContextFlyout="{StaticResource GameContextMenu}"
VerticalAlignment="Stretch"
Name="GameListBox"
Items="{Binding AppsObservableList}">
<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="BorderBrush" Value="{DynamicResource SystemAccentColorDark3}" />
<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>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Border HorizontalAlignment="Stretch"
Padding="10" CornerRadius="5"
VerticalAlignment="Stretch" Margin="0" ClipToBounds="True">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<Image
Classes.small="{Binding $parent[UserControl].DataContext.IsGridSmall}"
Classes.normal="{Binding $parent[UserControl].DataContext.IsGridMedium}"
Classes.large="{Binding $parent[UserControl].DataContext.IsGridLarge}"
Classes.huge="{Binding $parent[UserControl].DataContext.IsGridHuge}"
Grid.RowSpan="3" Grid.Column="0" Margin="0"
Source="{Binding Icon, Converter={StaticResource ByteImage}}" />
<StackPanel Orientation="Vertical" Spacing="5" VerticalAlignment="Top" HorizontalAlignment="Left"
Grid.Column="2">
<TextBlock Text="{Binding TitleName}" TextAlignment="Left" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
<TextBlock Text="{Binding Developer}" TextAlignment="Left" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
<TextBlock Text="{Binding Version}" TextAlignment="Left" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="5" VerticalAlignment="Top" HorizontalAlignment="Right"
Grid.Column="3">
<TextBlock Text="{Binding TimePlayed}" TextAlignment="Right" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
<TextBlock Text="{Binding LastPlayed}" TextAlignment="Right" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
<TextBlock Text="{Binding FileSize}" TextAlignment="Right" TextWrapping="Wrap"
HorizontalAlignment="Stretch" />
</StackPanel>
<ui:SymbolIcon Grid.Row="0" Grid.Column="0" FontSize="20"
Foreground="Yellow"
Symbol="StarFilled"
IsVisible="{Binding Favorite}" Margin="-5, -5, 0, 0" VerticalAlignment="Top"
HorizontalAlignment="Left" />
<ui:SymbolIcon Grid.Row="0" Grid.Column="0" FontSize="20"
Foreground="Black"
Symbol="Star"
IsVisible="{Binding Favorite}" Margin="-5, -5, 0, 0" VerticalAlignment="Top"
HorizontalAlignment="Left" />
</Grid>
</Border>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>

View file

@ -0,0 +1,82 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using LibHac.Common;
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;
}
}
}
}
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.Ava.Ui.Controls
{
public enum Glyph : int
{
List,
Grid,
Chip
}
}

View 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.Controls
{
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);
}
}
}

View file

@ -0,0 +1,52 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using System;
using System.Windows.Input;
namespace Ryujinx.Ava.Ui.Controls
{
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();
}
}
}
}

View file

@ -0,0 +1,25 @@
using Avalonia.OpenGL;
using SPB.Graphics.OpenGL;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
public 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;
}
}
}

View file

@ -0,0 +1,18 @@
<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"
x:Class="Ryujinx.Ava.Ui.Controls.InputDialog">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="5,10,5, 5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding Message}" />
<TextBox MaxLength="{Binding MaxLength}" Grid.Row="1" Margin="10" Width="300" HorizontalAlignment="Center"
Text="{Binding Input, Mode=TwoWay}" />
<TextBlock Grid.Row="2" Margin="5, 5, 5, 10" HorizontalAlignment="Center" Text="{Binding SubMessage}" />
</Grid>
</UserControl>

View file

@ -0,0 +1,67 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Ui.Models;
using Ryujinx.Ava.Ui.Windows;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Ui.Controls
{
public 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;
InitializeComponent();
}
public InputDialog()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public static async Task<(UserResult Result, string Input)> ShowInputDialog(StyleableWindow window, string title, string message, string input = "", string subMessage = "", uint maxLength = int.MaxValue)
{
ContentDialog contentDialog = window.ContentDialog;
UserResult result = UserResult.Cancel;
InputDialog content = new InputDialog(message, input = "", subMessage = "", maxLength);
if (contentDialog != null)
{
contentDialog.Title = title;
contentDialog.PrimaryButtonText = LocaleManager.Instance["InputDialogOk"];
contentDialog.SecondaryButtonText = "";
contentDialog.CloseButtonText = LocaleManager.Instance["InputDialogCancel"];
contentDialog.Content = content;
contentDialog.PrimaryButtonCommand = MiniCommand.Create(() =>
{
result = UserResult.Ok;
input = content.Input;
});
await contentDialog.ShowAsync();
}
return (result, input);
}
}
}

View 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.Controls
{
public 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;
}
}
}

View file

@ -0,0 +1,71 @@
using System;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Ryujinx.Ava.Ui.Models
{
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;
}
}

View file

@ -0,0 +1,40 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Ryujinx.Ava.Ui.Controls
{
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
});
}
}
}

View file

@ -0,0 +1,20 @@
using OpenTK;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
public 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);
}
}
}

View file

@ -0,0 +1,100 @@
using Avalonia.Rendering;
using System;
using System.Threading;
using System.Timers;
namespace Ryujinx.Ava.Ui.Controls
{
internal class RenderTimer : IRenderTimer, IDisposable
{
public event Action<TimeSpan> Tick
{
add
{
_tick += value;
if (_subscriberCount++ == 0)
{
Start();
}
}
remove
{
if (--_subscriberCount == 0)
{
Stop();
}
_tick -= value;
}
}
private Thread _tickThread;
private readonly System.Timers.Timer _timer;
private Action<TimeSpan> _tick;
private int _subscriberCount;
private bool _isRunning;
private AutoResetEvent _resetEvent;
public RenderTimer()
{
_timer = new System.Timers.Timer(15);
_resetEvent = new AutoResetEvent(true);
_timer.Elapsed += Timer_Elapsed;
}
private void Timer_Elapsed(object sender, ElapsedEventArgs e)
{
TickNow();
}
public void Start()
{
_timer.Start();
if (_tickThread == null)
{
_tickThread = new Thread(RunTick);
_tickThread.Name = "RenderTimerTickThread";
_tickThread.IsBackground = true;
_isRunning = true;
_tickThread.Start();
}
}
public void RunTick()
{
while (_isRunning)
{
_resetEvent.WaitOne();
_tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount));
}
}
public void TickNow()
{
lock (_timer)
{
_resetEvent.Set();
}
}
public void Stop()
{
_timer.Stop();
}
public void Dispose()
{
_timer.Elapsed -= Timer_Elapsed;
_timer.Stop();
_isRunning = false;
_resetEvent.Set();
_tickThread.Join();
_resetEvent.Dispose();
}
}
}

View file

@ -0,0 +1,273 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using Avalonia.Threading;
using OpenTK.Graphics.OpenGL;
using Ryujinx.Common.Configuration;
using SkiaSharp;
using SPB.Graphics;
using SPB.Graphics.OpenGL;
using SPB.Platform;
using SPB.Windowing;
using System;
namespace Ryujinx.Ava.Ui.Controls
{
public class RendererControl : Control
{
private int _image;
static RendererControl()
{
AffectsRender<RendererControl>(ImageProperty);
}
public readonly static StyledProperty<int> ImageProperty =
AvaloniaProperty.Register<RendererControl, int>(nameof(Image), 0, inherits: true, defaultBindingMode: BindingMode.TwoWay);
protected int Image
{
get => _image;
set => SetAndRaise(ImageProperty, ref _image, value);
}
public event EventHandler<EventArgs> GlInitialized;
public event EventHandler<Size> SizeChanged;
protected Size RenderSize { get; private set; }
public bool IsStarted { get; private set; }
public int Major { get; }
public int Minor { get; }
public GraphicsDebugLevel DebugLevel { get; }
public OpenGLContextBase GameContext { get; set; }
public static OpenGLContextBase PrimaryContext => AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>().PrimaryContext.AsOpenGLContextBase();
private SwappableNativeWindowBase _gameBackgroundWindow;
private bool _isInitialized;
private IntPtr _fence;
private GlDrawOperation _glDrawOperation;
public RendererControl(int major, int minor, GraphicsDebugLevel graphicsDebugLevel)
{
Major = major;
Minor = minor;
DebugLevel = graphicsDebugLevel;
IObservable<Rect> resizeObservable = this.GetObservable(BoundsProperty);
resizeObservable.Subscribe(Resized);
Focusable = true;
}
private void Resized(Rect rect)
{
SizeChanged?.Invoke(this, rect.Size);
RenderSize = rect.Size * Program.WindowScaleFactor;
_glDrawOperation?.Dispose();
_glDrawOperation = new GlDrawOperation(this);
}
public override void Render(DrawingContext context)
{
if (!_isInitialized)
{
CreateWindow();
OnGlInitialized();
_isInitialized = true;
}
if (GameContext == null || !IsStarted || Image == 0)
{
return;
}
if (_glDrawOperation != null)
{
context.Custom(_glDrawOperation);
}
base.Render(context);
}
protected void OnGlInitialized()
{
GlInitialized?.Invoke(this, EventArgs.Empty);
}
public void QueueRender()
{
Program.RenderTimer.TickNow();
}
internal void Present(object image)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
Image = (int)image;
}).Wait();
if (_fence != IntPtr.Zero)
{
GL.DeleteSync(_fence);
}
_fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None);
QueueRender();
_gameBackgroundWindow.SwapBuffers();
}
internal void Start()
{
IsStarted = true;
QueueRender();
}
internal void Stop()
{
IsStarted = false;
}
public void DestroyBackgroundContext()
{
_image = 0;
if (_fence != IntPtr.Zero)
{
_glDrawOperation.Dispose();
GL.DeleteSync(_fence);
}
GlDrawOperation.DeleteFramebuffer();
GameContext?.Dispose();
_gameBackgroundWindow?.Dispose();
}
internal void MakeCurrent()
{
GameContext.MakeCurrent(_gameBackgroundWindow);
}
internal void MakeCurrent(SwappableNativeWindowBase window)
{
GameContext.MakeCurrent(window);
}
protected void CreateWindow()
{
var flags = OpenGLContextFlags.Compat;
if (DebugLevel != GraphicsDebugLevel.None)
{
flags |= OpenGLContextFlags.Debug;
}
_gameBackgroundWindow = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
_gameBackgroundWindow.Hide();
GameContext = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, Major, Minor, flags, shareContext: PrimaryContext);
GameContext.Initialize(_gameBackgroundWindow);
}
private class GlDrawOperation : ICustomDrawOperation
{
private static int _framebuffer;
public Rect Bounds { get; }
private readonly RendererControl _control;
public GlDrawOperation(RendererControl control)
{
_control = control;
Bounds = _control.Bounds;
}
public void Dispose() { }
public static void DeleteFramebuffer()
{
if (_framebuffer == 0)
{
GL.DeleteFramebuffer(_framebuffer);
}
_framebuffer = 0;
}
public bool Equals(ICustomDrawOperation other)
{
return other is GlDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds;
}
public bool HitTest(Point p)
{
return Bounds.Contains(p);
}
private void CreateRenderTarget()
{
_framebuffer = GL.GenFramebuffer();
}
public void Render(IDrawingContextImpl context)
{
if (_control.Image == 0)
{
return;
}
if (_framebuffer == 0)
{
CreateRenderTarget();
}
int currentFramebuffer = GL.GetInteger(GetPName.FramebufferBinding);
var image = _control.Image;
var fence = _control._fence;
GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer);
GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, image, 0);
GL.BindFramebuffer(FramebufferTarget.Framebuffer, currentFramebuffer);
if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl)
{
return;
}
var imageInfo = new SKImageInfo((int)_control.RenderSize.Width, (int)_control.RenderSize.Height, SKColorType.Rgba8888);
var glInfo = new GRGlFramebufferInfo((uint)_framebuffer, SKColorType.Rgba8888.ToGlSizedFormat());
GL.WaitSync(fence, WaitSyncFlags.None, ulong.MaxValue);
using var backendTexture = new GRBackendRenderTarget(imageInfo.Width, imageInfo.Height, 1, 0, glInfo);
using var surface = SKSurface.Create(skiaDrawingContextImpl.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888);
if (surface == null)
{
return;
}
var rect = new Rect(new Point(), _control.RenderSize);
using var snapshot = surface.Snapshot();
skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint());
}
}
}
}

View 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.Controls
{
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);
}
}
}

View file

@ -0,0 +1,28 @@
<Window 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"
mc:Ignorable="d"
x:Class="Ryujinx.Ava.Ui.Controls.UpdateWaitWindow"
WindowStartupLocation="CenterOwner"
SizeToContent="WidthAndHeight"
Title="Ryujinx - Waiting">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Image Grid.Row="1" Margin="5, 10, 20 , 10" Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common"
Height="70"
MinWidth="50" />
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Orientation="Vertical">
<TextBlock Margin="5" Name="PrimaryText" />
<TextBlock VerticalAlignment="Center" Name="SecondaryText" Margin="5" />
</StackPanel>
</Grid>
</Window>

View file

@ -0,0 +1,35 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ryujinx.Ava.Ui.Windows;
namespace Ryujinx.Ava.Ui.Controls
{
public class UpdateWaitWindow : StyleableWindow
{
public UpdateWaitWindow(string primaryText, string secondaryText) : this()
{
PrimaryText.Text = primaryText;
SecondaryText.Text = secondaryText;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
}
public UpdateWaitWindow()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
public TextBlock PrimaryText { get; private set; }
public TextBlock SecondaryText { get; private set; }
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
PrimaryText = this.FindControl<TextBlock>("PrimaryText");
SecondaryText = this.FindControl<TextBlock>("SecondaryText");
}
}
}

View 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.Controls
{
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(owner,
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));
}
}
}
}

View file

@ -0,0 +1,12 @@
namespace Ryujinx.Ava.Ui.Controls
{
public enum UserResult
{
Ok,
Yes,
No,
Abort,
Cancel,
None,
}
}

View file

@ -0,0 +1,45 @@
using Ryujinx.Ui.App.Common;
using System.Collections;
namespace Ryujinx.Ava.Ui.Models
{
public class FileSizeSortComparer : IComparer
{
public int Compare(object x, object y)
{
string aValue = (x as ApplicationData).TimePlayed;
string bValue = (y as ApplicationData).TimePlayed;
if (aValue[^2..] == "GB")
{
aValue = (float.Parse(aValue[0..^2]) * 1024).ToString();
}
else
{
aValue = aValue[0..^2];
}
if (bValue[^2..] == "GB")
{
bValue = (float.Parse(bValue[0..^2]) * 1024).ToString();
}
else
{
bValue = bValue[0..^2];
}
if (float.Parse(aValue) > float.Parse(bValue))
{
return -1;
}
else if (float.Parse(bValue) > float.Parse(aValue))
{
return 1;
}
else
{
return 0;
}
}
}
}

View file

@ -0,0 +1,50 @@
using Ryujinx.Ui.App.Common;
using System.Collections.Generic;
namespace Ryujinx.Ava.Ui.Models.Generic
{
public class FileSizeSortComparer : IComparer<ApplicationData>
{
public FileSizeSortComparer() { }
public FileSizeSortComparer(bool isAscending) { _order = isAscending ? 1 : -1; }
private int _order;
public int Compare(ApplicationData x, ApplicationData y)
{
string aValue = x.FileSize;
string bValue = y.FileSize;
if (aValue[^2..] == "GB")
{
aValue = (float.Parse(aValue[0..^2]) * 1024).ToString();
}
else
{
aValue = aValue[0..^2];
}
if (bValue[^2..] == "GB")
{
bValue = (float.Parse(bValue[0..^2]) * 1024).ToString();
}
else
{
bValue = bValue[0..^2];
}
if (float.Parse(aValue) > float.Parse(bValue))
{
return -1 * _order;
}
else if (float.Parse(bValue) > float.Parse(aValue))
{
return 1 * _order;
}
else
{
return 0;
}
}
}
}

View 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
{
public 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));
}
}
}

View file

@ -0,0 +1,66 @@
using Ryujinx.Ui.App.Common;
using System.Collections.Generic;
namespace Ryujinx.Ava.Ui.Models.Generic
{
public class TimePlayedSortComparer : IComparer<ApplicationData>
{
public TimePlayedSortComparer() { }
public TimePlayedSortComparer(bool isAscending) { _order = isAscending ? 1 : -1; }
private int _order;
public int Compare(ApplicationData x, ApplicationData y)
{
string aValue = x.TimePlayed;
string bValue = y.TimePlayed;
if (aValue.Length > 4 && aValue[^4..] == "mins")
{
aValue = (float.Parse(aValue[0..^5]) * 60).ToString();
}
else if (aValue.Length > 3 && aValue[^3..] == "hrs")
{
aValue = (float.Parse(aValue[0..^4]) * 3600).ToString();
}
else if (aValue.Length > 4 && aValue[^4..] == "days")
{
aValue = (float.Parse(aValue[0..^5]) * 86400).ToString();
}
else
{
aValue = aValue[0..^1];
}
if (bValue.Length > 4 && bValue[^4..] == "mins")
{
bValue = (float.Parse(bValue[0..^5]) * 60).ToString();
}
else if (bValue.Length > 3 && bValue[^3..] == "hrs")
{
bValue = (float.Parse(bValue[0..^4]) * 3600).ToString();
}
else if (bValue.Length > 4 && bValue[^4..] == "days")
{
bValue = (float.Parse(bValue[0..^5]) * 86400).ToString();
}
else
{
bValue = bValue[0..^1];
}
if (float.Parse(aValue) > float.Parse(bValue))
{
return -1 * _order;
}
else if (float.Parse(bValue) > float.Parse(aValue))
{
return 1 * _order;
}
else
{
return 0;
}
}
}
}

View 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
{
public 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;
}
}
}

View file

@ -0,0 +1,27 @@
using Ryujinx.Ui.App.Common;
using System;
using System.Collections;
namespace Ryujinx.Ava.Ui.Models
{
public class LastPlayedSortComparer : IComparer
{
public int Compare(object x, object y)
{
string aValue = (x as ApplicationData).LastPlayed;
string bValue = (y as ApplicationData).LastPlayed;
if (aValue == "Never")
{
aValue = DateTime.UnixEpoch.ToString();
}
if (bValue == "Never")
{
bValue = DateTime.UnixEpoch.ToString();
}
return DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue));
}
}
}

View 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; }
}
}

View file

@ -0,0 +1,26 @@
using System;
namespace Ryujinx.Ava.Ui.Models
{
public class StatusUpdatedEventArgs : EventArgs
{
public bool VSyncEnabled { get; }
public float Volume { 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, float volume, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName)
{
VSyncEnabled = vSyncEnabled;
Volume = volume;
DockedMode = dockedMode;
AspectRatio = aspectRatio;
GameStatus = gameStatus;
FifoStatus = fifoStatus;
GpuName = gpuName;
}
}
}

View file

@ -0,0 +1,61 @@
using Ryujinx.Ui.App.Common;
using System.Collections;
namespace Ryujinx.Ava.Ui.Models
{
public class TimePlayedSortComparer : IComparer
{
public int Compare(object x, object y)
{
string aValue = (x as ApplicationData).TimePlayed;
string bValue = (y as ApplicationData).TimePlayed;
if (aValue.Length > 4 && aValue[^4..] == "mins")
{
aValue = (float.Parse(aValue[0..^5]) * 60).ToString();
}
else if (aValue.Length > 3 && aValue[^3..] == "hrs")
{
aValue = (float.Parse(aValue[0..^4]) * 3600).ToString();
}
else if (aValue.Length > 4 && aValue[^4..] == "days")
{
aValue = (float.Parse(aValue[0..^5]) * 86400).ToString();
}
else
{
aValue = aValue[0..^1];
}
if (bValue.Length > 4 && bValue[^4..] == "mins")
{
bValue = (float.Parse(bValue[0..^5]) * 60).ToString();
}
else if (bValue.Length > 3 && bValue[^3..] == "hrs")
{
bValue = (float.Parse(bValue[0..^4]) * 3600).ToString();
}
else if (bValue.Length > 4 && bValue[^4..] == "days")
{
bValue = (float.Parse(bValue[0..^5]) * 86400).ToString();
}
else
{
bValue = bValue[0..^1];
}
if (float.Parse(aValue) > float.Parse(bValue))
{
return -1;
}
else if (float.Parse(bValue) > float.Parse(aValue))
{
return 1;
}
else
{
return 0;
}
}
}
}

View file

@ -0,0 +1,22 @@
using LibHac.Ns;
using Ryujinx.Ava.Common.Locale;
namespace Ryujinx.Ava.Ui.Models
{
public 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;
}
}
}

View 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));
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,163 @@
<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"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
x:Class="Ryujinx.Ava.Ui.Windows.AboutWindow"
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
CanResize="False"
WindowStartupLocation="CenterOwner"
Width="850" MinHeight="550" Height="550"
SizeToContent="Width"
MinWidth="500"
Title="Ryujinx - About">
<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" Margin="20" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Grid.Column="0">
<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.Column="0" Grid.RowSpan="3" Margin="5, 10, 20 , 10"
Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" Height="110" MinWidth="50" />
<TextBlock FontSize="35" TextAlignment="Center" Grid.Row="0" Grid.Column="1" Text="Ryujinx"
Margin="0,20,0,0" />
<TextBlock FontSize="16" TextAlignment="Center" Grid.Row="1" Grid.Column="1" Text="(REE-YOU-JINX)"
Margin="0,0,0,0" />
<Button Grid.Column="1" Background="Transparent" HorizontalAlignment="Center" Margin="0" Grid.Row="2"
Tag="https://www.ryujinx.org/"
Click="Button_OnClick">
<TextBlock ToolTip.Tip="{locale:Locale AboutUrlTooltipMessage}"
TextAlignment="Center" TextDecorations="Underline" Text="www.ryujinx.org" />
</Button>
</Grid>
<TextBlock TextAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center"
Text="{Binding Version}" Grid.Row="1" />
<TextBlock Grid.Row="2" TextAlignment="Center" HorizontalAlignment="Center" Margin="20"
Text="{locale:Locale AboutDisclaimerMessage}"
MaxLines="2" />
<TextBlock Grid.Row="3" TextAlignment="Center" HorizontalAlignment="Center" Margin="20"
Text="{locale:Locale AboutAmiiboDisclaimerMessage}"
Name="AmiiboLabel"
PointerPressed="AmiiboLabel_OnPointerPressed"
MaxLines="2" />
<StackPanel Spacing="10" Orientation="Horizontal" Grid.Row="4" HorizontalAlignment="Center">
<StackPanel Orientation="Vertical"
ToolTip.Tip="{locale:Locale AboutPatreonUrlTooltipMessage}">
<Button Height="65" Background="Transparent" Tag="https://www.patreon.com/ryujinx"
Click="Button_OnClick">
<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" Text="Patreon" HorizontalAlignment="Center" />
</Grid>
</Button>
</StackPanel>
<StackPanel Orientation="Vertical"
ToolTip.Tip="{locale:Locale AboutGithubUrlTooltipMessage}">
<Button Height="65" Background="Transparent" Tag="https://github.com/Ryujinx/Ryujinx"
Click="Button_OnClick">
<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" Text="GitHub" HorizontalAlignment="Center" />
</Grid>
</Button>
</StackPanel>
<StackPanel Orientation="Vertical"
ToolTip.Tip="{locale:Locale AboutDiscordUrlTooltipMessage}">
<Button Height="65" Background="Transparent" Tag="https://discordapp.com/invite/N2FmfVc"
Click="Button_OnClick">
<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" Text="Discord" HorizontalAlignment="Center" />
</Grid>
</Button>
</StackPanel>
<StackPanel Orientation="Vertical"
ToolTip.Tip="{locale:Locale AboutTwitterUrlTooltipMessage}">
<Button Height="65" Background="Transparent" Tag="https://twitter.com/RyujinxEmu"
Click="Button_OnClick">
<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" Text="Twitter" HorizontalAlignment="Center" />
</Grid>
</Button>
</StackPanel>
</StackPanel>
</Grid>
<Border Grid.Row="1" Grid.Column="1" VerticalAlignment="Stretch" Margin="5" Width="2" BorderBrush="White"
BorderThickness="1,0,0,0">
<Separator Width="0" />
</Border>
<Grid Grid.Row="1" Margin="20" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Grid.Column="2">
<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 Text="{locale:Locale AboutRyujinxAboutTitle}" FontWeight="Bold" TextDecorations="Underline" />
<TextBlock LineHeight="20" Grid.Row="1" Margin="20,5,5,5"
Text="{locale:Locale AboutRyujinxAboutContent}" />
<TextBlock Grid.Row="2" Margin="0,10,0,0" Text="{locale:Locale AboutRyujinxMaintainersTitle}"
FontWeight="Bold"
TextDecorations="Underline" />
<TextBlock LineHeight="20" Grid.Row="3" Margin="20,5,5,5"
Text="{Binding Developers}" />
<Button Background="Transparent" HorizontalAlignment="Right" Grid.Row="4"
Tag="https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a" Click="Button_OnClick">
<TextBlock ToolTip.Tip="{locale:Locale AboutRyujinxMaintainersContentTooltipMessage}"
TextAlignment="Right" TextDecorations="Underline"
Text="{locale:Locale AboutRyujinxContributorsButtonHeader}" />
</Button>
<TextBlock Grid.Row="5" Margin="0,0,0,0" Text="{locale:Locale AboutRyujinxSupprtersTitle}"
FontWeight="Bold"
TextDecorations="Underline" />
<Border Width="460" Grid.Row="6" VerticalAlignment="Stretch" Height="200" BorderThickness="1" Margin="20,5"
BorderBrush="White" Padding="5">
<TextBlock TextWrapping="Wrap" VerticalAlignment="Top" Name="SupportersTextBlock"
Text="{Binding Supporters}" />
</Border>
</Grid>
</Grid>
</window:StyleableWindow>

View file

@ -0,0 +1,92 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
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 class AboutWindow : StyleableWindow
{
public AboutWindow()
{
if (Program.PreviewerDetached)
{
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance["MenuBarHelpAbout"];
}
Version = Program.Version;
DataContext = this;
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
_ = 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«");
public TextBlock SupportersTextBlock { get; set; }
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
SupportersTextBlock = this.FindControl<TextBlock>("SupportersTextBlock");
}
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");
}
}
}
}

View 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 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);
}
}
}

View file

@ -0,0 +1,639 @@
<window:StyleableWindow
x:Class="Ryujinx.Ava.Ui.Windows.MainWindow"
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:controls="clr-namespace:Ryujinx.Ava.Ui.Controls"
xmlns:models="clr-namespace:Ryujinx.Ava.Ui.Models"
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"
Title="Ryujinx"
Height="785"
Width="1280"
d:DesignHeight="720"
d:DesignWidth="1280"
MinWidth="1024"
MinHeight="680"
WindowStartupLocation="CenterScreen"
x:CompileBindings="True"
x:DataType="viewModels:MainWindowViewModel"
mc:Ignorable="d">
<Window.Styles>
<Style Selector="TitleBar:fullscreen">
<Setter Property="Background" Value="#000000" />
</Style>
</Window.Styles>
<Design.DataContext>
<viewModels:MainWindowViewModel />
</Design.DataContext>
<Window.Resources>
<controls:BitmapArrayValueConverter x:Key="ByteImage" />
</Window.Resources>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<controls:OffscreenTextBox Name="HiddenTextBox" Grid.Row="0" />
<ContentControl Grid.Row="1"
Focusable="False"
IsVisible="False"
KeyboardNavigation.IsTabStop="False">
<ui:ContentDialog Name="ContentDialog"
KeyboardNavigation.IsTabStop="False"
IsPrimaryButtonEnabled="True"
IsSecondaryButtonEnabled="True"
IsVisible="True" />
</ContentControl>
<StackPanel IsVisible="False" Grid.Row="0">
<controls:HotKeyControl Name="FullscreenHotKey" Command="{ReflectionBinding ToggleFullscreen}" />
<controls:HotKeyControl Name="FullscreenHotKey2" Command="{ReflectionBinding ToggleFullscreen}" />
<controls:HotKeyControl Name="DockToggleHotKey" Command="{ReflectionBinding ToggleDockMode}" />
<controls:HotKeyControl Name="ExitHotKey" Command="{ReflectionBinding ExitCurrentState}" />
</StackPanel>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
Margin="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{Binding ShowMenuAndStatusBar}"
Orientation="Vertical">
<DockPanel HorizontalAlignment="Stretch">
<Menu
Name="Menu"
Margin="0"
Height="35"
HorizontalAlignment="Left">
<Menu.ItemsPanel>
<ItemsPanelTemplate>
<DockPanel HorizontalAlignment="Stretch" Margin="0" />
</ItemsPanelTemplate>
</Menu.ItemsPanel>
<MenuItem
VerticalAlignment="Center"
Header="{locale:Locale MenuBarFile}">
<MenuItem IsEnabled="{Binding EnableNonGameRunningControls}"
Command="{ReflectionBinding OpenFile}"
Header="{locale:Locale MenuBarFileOpenFromFile}"
ToolTip.Tip="{locale:Locale LoadApplicationFileTooltip}" />
<MenuItem IsEnabled="{Binding EnableNonGameRunningControls}"
Command="{ReflectionBinding OpenFolder}"
Header="{locale:Locale MenuBarFileOpenUnpacked}"
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 Header="{locale:Locale MenuBarOptionsStartGamesInFullscreen}">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding StartGamesInFullscreen, Mode=TwoWay}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{locale:Locale MenuBarOptionsShowConsole}">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding ShowConsole, Mode=TwoWay}" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="{locale:Locale MenuBarOptionsChangeLanguage}">
<MenuItem Command="{ReflectionBinding ChangeLanguage}" CommandParameter="en_US"
Header="American English" />
<MenuItem Command="{ReflectionBinding ChangeLanguage}" CommandParameter="pt_BR"
Header="Brazilian Portuguese" />
<MenuItem Command="{ReflectionBinding ChangeLanguage}" CommandParameter="es_ES"
Header="Castilian Spanish" />
<MenuItem Command="{ReflectionBinding ChangeLanguage}" CommandParameter="fr_FR"
Header="French" />
<MenuItem Command="{ReflectionBinding ChangeLanguage}" CommandParameter="de_DE"
Header="German" />
<MenuItem Command="{ReflectionBinding ChangeLanguage}" CommandParameter="el_GR"
Header="Greek" />
<MenuItem Command="{ReflectionBinding ChangeLanguage}" CommandParameter="it_IT"
Header="Italian" />
<MenuItem Command="{ReflectionBinding ChangeLanguage}" CommandParameter="ko_KR"
Header="Korean" />
<MenuItem Command="{ReflectionBinding ChangeLanguage}" CommandParameter="ru_RU"
Header="Russian" />
<MenuItem Command="{ReflectionBinding ChangeLanguage}" CommandParameter="tr_TR"
Header="Turkish" />
</MenuItem>
<Separator />
<MenuItem Command="{ReflectionBinding OpenSettings}"
Header="{locale:Locale MenuBarOptionsSettings}"
ToolTip.Tip="{locale:Locale OpenSettingsTooltip}" />
<MenuItem Command="{ReflectionBinding ManageProfiles}"
IsEnabled="{Binding EnableNonGameRunningControls}"
Header="{locale:Locale MenuBarOptionsManageUserProfiles}"
ToolTip.Tip="{locale:Locale OpenProfileManagerTooltip}" />
</MenuItem>
<MenuItem
VerticalAlignment="Center"
Header="{locale:Locale MenuBarActions}"
Name="ActionsMenuItem"
IsEnabled="{Binding IsGameRunning}">
<MenuItem
Click="PauseEmulation_Click"
Header="{locale:Locale MenuBarOptionsPauseEmulation}"
IsEnabled="{Binding !IsPaused}"
IsVisible="{Binding !IsPaused}"
InputGesture="{Binding PauseKey}" />
<MenuItem
Click="ResumeEmulation_Click"
Header="{locale:Locale MenuBarOptionsResumeEmulation}"
IsEnabled="{Binding IsPaused}"
IsVisible="{Binding IsPaused}"
InputGesture="{Binding PauseKey}" />
<MenuItem
Click="StopEmulation_Click"
Header="{locale:Locale MenuBarOptionsStopEmulation}"
ToolTip.Tip="{locale:Locale StopEmulationTooltip}"
IsEnabled="{Binding IsGameRunning}" InputGesture="Escape" />
<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}"
IsEnabled="{Binding IsGameRunning}"
Header="{locale:Locale MenuBarFileToolsTakeScreenshot}"
InputGesture="{Binding ScreenshotKey}" />
<MenuItem Command="{ReflectionBinding HideUi}"
IsEnabled="{Binding IsGameRunning}"
Header="{locale:Locale MenuBarFileToolsHideUi}"
InputGesture="{Binding ShowUiKey}" />
<MenuItem Command="{ReflectionBinding OpenCheatManagerForCurrentApp}"
IsEnabled="{Binding IsGameRunning}"
Header="{locale:Locale GameListContextMenuManageCheat}" />
</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="Content"
Grid.Row="1"
Padding="0"
IsVisible="{Binding ShowContent}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="0,0,0,0"
DockPanel.Dock="Top">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" HorizontalAlignment="Stretch" Margin="0,0,0,5">
<Button
IsEnabled="{Binding IsGrid}" VerticalAlignment="Stretch" MinWidth="40" Width="40"
Margin="5,2,0,2" Command="{ReflectionBinding SetListMode}">
<ui:FontIcon FontFamily="avares://FluentAvalonia/Fonts#Symbols"
VerticalAlignment="Center"
Margin="0"
Glyph="{controls:GlyphValueConverter List}"
HorizontalAlignment="Stretch" />
</Button>
<Button
IsEnabled="{Binding IsList}" VerticalAlignment="Stretch" MinWidth="40" Width="40"
Margin="5,2,5,2" Command="{ReflectionBinding SetGridMode}">
<ui:FontIcon FontFamily="avares://FluentAvalonia/Fonts#Symbols"
VerticalAlignment="Center"
Margin="0"
Glyph="{controls:GlyphValueConverter Grid}"
HorizontalAlignment="Stretch" />
</Button>
<TextBlock Text="{locale:Locale IconSize}"
VerticalAlignment="Center" Margin="10,0"
ToolTip.Tip="{locale:Locale IconSizeTooltip}" />
<Slider Width="150" Margin="5,-10,5 ,0" Height="35"
ToolTip.Tip="{locale:Locale IconSizeTooltip}"
VerticalAlignment="Center" Minimum="1" Maximum="4" IsSnapToTickEnabled="True"
TickFrequency="1" Value="{Binding GridSizeScale}" />
<CheckBox Margin="0" IsChecked="{Binding ShowNames, Mode=TwoWay}" VerticalAlignment="Center"
IsVisible="{Binding IsGrid}">
<TextBlock Text="{locale:Locale CommonShowNames}" Margin="5,3,0,0" />
</CheckBox>
<TextBox
Name="SearchBox"
DockPanel.Dock="Right"
VerticalAlignment="Center"
MinWidth="200"
Margin="5,0,5,0"
HorizontalAlignment="Right"
KeyUp="SearchBox_OnKeyUp"
Text="{Binding SearchText}"
Watermark="{locale:Locale MenuSearch}" />
<ui:DropDownButton DockPanel.Dock="Right"
HorizontalAlignment="Right" Width="150" VerticalAlignment="Center"
Content="{Binding SortName}">
<ui:DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel Orientation="Vertical" HorizontalAlignment="Stretch" Margin="0">
<StackPanel>
<RadioButton Tag="Favorite"
IsChecked="{Binding IsSortedByFavorite, Mode=OneTime}"
GroupName="Sort"
Content="{locale:Locale CommonFavorite}"
Checked="Sort_Checked" />
<RadioButton Tag="Title" GroupName="Sort"
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
Content="{locale:Locale GameListHeaderApplication}"
Checked="Sort_Checked" />
<RadioButton Tag="Developer" GroupName="Sort"
IsChecked="{Binding IsSortedByDeveloper, Mode=OneTime}"
Content="{locale:Locale GameListHeaderDeveloper}"
Checked="Sort_Checked" />
<RadioButton Tag="TotalTimePlayed" GroupName="Sort"
IsChecked="{Binding IsSortedByTimePlayed, Mode=OneTime}"
Content="{locale:Locale GameListHeaderTimePlayed}"
Checked="Sort_Checked" />
<RadioButton Tag="LastPlayed" GroupName="Sort"
IsChecked="{Binding IsSortedByLastPlayed, Mode=OneTime}"
Content="{locale:Locale GameListHeaderLastPlayed}"
Checked="Sort_Checked" />
<RadioButton Tag="FileType" GroupName="Sort"
IsChecked="{Binding IsSortedByType, Mode=OneTime}"
Content="{locale:Locale GameListHeaderFileExtension}"
Checked="Sort_Checked" />
<RadioButton Tag="FileSize" GroupName="Sort"
IsChecked="{Binding IsSortedBySize, Mode=OneTime}"
Content="{locale:Locale GameListHeaderFileSize}"
Checked="Sort_Checked" />
<RadioButton Tag="Path" GroupName="Sort"
IsChecked="{Binding IsSortedByPath, Mode=OneTime}"
Content="{locale:Locale GameListHeaderPath}"
Checked="Sort_Checked" />
</StackPanel>
<Border HorizontalAlignment="Stretch" Margin="5" Height="2" BorderBrush="White"
Width="60" BorderThickness="0,1,0,0">
<Separator HorizontalAlignment="Stretch" Height="0" />
</Border>
<RadioButton Tag="Ascending" IsChecked="{Binding IsAscending, Mode=OneTime}"
GroupName="Order"
Content="{locale:Locale OrderAscending}" Checked="Order_Checked" />
<RadioButton Tag="Descending" GroupName="Order"
IsChecked="{Binding !IsAscending, Mode=OneTime}"
Content="{locale:Locale OrderDescending}" Checked="Order_Checked" />
</StackPanel>
</Flyout>
</ui:DropDownButton.Flyout>
</ui:DropDownButton>
<TextBlock DockPanel.Dock="Right" HorizontalAlignment="Right"
Text="{locale:Locale CommonSort}" VerticalAlignment="Center" Margin="10,0" />
</DockPanel>
<controls:GameListView
x:Name="GameList"
Grid.Row="1"
HorizontalContentAlignment="Stretch"
HorizontalAlignment="Stretch"
VerticalContentAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{Binding IsList}" />
<controls:GameGridView
x:Name="GameGrid"
Grid.Row="1"
HorizontalContentAlignment="Stretch"
HorizontalAlignment="Stretch"
VerticalContentAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{Binding IsGrid}" />
</Grid>
</ContentControl>
<Grid Grid.Row="1"
VerticalAlignment="Stretch"
Background="{DynamicResource ThemeContentBackgroundColor}"
IsVisible="{Binding ShowLoadProgress}"
ZIndex="1000"
HorizontalAlignment="Stretch">
<Grid
HorizontalAlignment="Center"
IsVisible="{Binding ShowLoadProgress}"
Margin="40"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
Grid.RowSpan="2"
IsVisible="{Binding ShowLoadProgress}"
Width="256"
Height="256"
Margin="10"
Padding="4"
BorderBrush="Black"
BorderThickness="2"
BoxShadow="4 4 32 8 #40000000"
CornerRadius="3">
<Image
IsVisible="{Binding ShowLoadProgress}"
Width="256"
Height="256"
Source="{Binding SelectedIcon, Converter={StaticResource ByteImage}}" />
</Border>
<Grid Grid.Column="1"
IsVisible="{Binding ShowLoadProgress}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
IsVisible="{Binding ShowLoadProgress}"
Grid.Row="0"
Margin="10"
FontSize="30"
FontWeight="Bold"
TextWrapping="Wrap"
Text="{Binding LoadHeading}"
TextAlignment="Left" />
<Border
IsVisible="{Binding ShowLoadProgress}"
Grid.Row="1"
CornerRadius="5"
ClipToBounds="True"
BorderBrush="{Binding ProgressBarBackgroundColor}"
Padding="0"
HorizontalAlignment="Stretch"
Margin="10"
BorderThickness="1">
<ProgressBar
IsVisible="{Binding ShowLoadProgress}"
Height="10"
Margin="0"
Padding="0"
CornerRadius="5"
ClipToBounds="True"
MinWidth="500"
HorizontalAlignment="Stretch"
Background="{Binding ProgressBarBackgroundColor}"
Foreground="{Binding ProgressBarForegroundColor}"
Maximum="{Binding ProgressMaximum}"
Minimum="0"
IsIndeterminate="{Binding IsLoadingIndeterminate}"
Value="{Binding ProgressValue}" />
</Border>
<TextBlock
IsVisible="{Binding ShowLoadProgress}"
Grid.Row="2"
Margin="10"
FontSize="18"
Text="{Binding CacheLoadStatus}"
TextAlignment="Left" />
</Grid>
</Grid>
</Grid>
<Grid
Name="StatusBar"
Grid.Row="2"
Height="30"
Margin="0,0"
HorizontalAlignment="Stretch"
Background="{DynamicResource ThemeContentBackgroundColor}"
VerticalAlignment="Bottom"
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" IsVisible="{Binding EnableNonGameRunningControls}"
VerticalAlignment="Center" Margin="10,0">
<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 Symbol="Refresh" Height="100" Width="50" />
</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"
Maximum="{Binding StatusBarProgressMaximum}"
Value="{Binding StatusBarProgressValue}"
VerticalAlignment="Center"
Foreground="{DynamicResource HighlightColor}"
IsVisible="{Binding EnableNonGameRunningControls}" />
</Grid>
</StackPanel>
<StackPanel
Grid.Column="1"
Margin="10,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding IsGameRunning}"
Orientation="Horizontal">
<TextBlock
Name="VsyncStatus"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{Binding VsyncColor}"
PointerReleased="VsyncStatus_PointerReleased"
IsVisible="{Binding !ShowLoadProgress}"
Margin="0,0,5,0"
Text="VSync"
TextAlignment="Left" />
<Border
Width="2"
Margin="2,0"
IsVisible="{Binding !ShowLoadProgress}"
BorderThickness="1"
Height="12"
BorderBrush="Gray" />
<TextBlock
Margin="5,0,5,0"
Name="DockedStatus"
IsVisible="{Binding !ShowLoadProgress}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
PointerReleased="DockedStatus_PointerReleased"
Text="{Binding DockedStatusText}"
TextAlignment="Left" />
<Border
Width="2"
Margin="2,0"
IsVisible="{Binding !ShowLoadProgress}"
BorderThickness="1"
Height="12"
BorderBrush="Gray" />
<TextBlock
Margin="5,0,5,0"
Name="AspectRatioStatus"
IsVisible="{Binding !ShowLoadProgress}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
PointerReleased="AspectRatioStatus_PointerReleased"
Text="{Binding AspectRatioStatusText}"
TextAlignment="Left" />
<Border
Width="2"
Margin="2,0"
IsVisible="{Binding !ShowLoadProgress}"
BorderThickness="1"
Height="12"
BorderBrush="Gray" />
<ui:ToggleSplitButton
Margin="-2,0,-3,0"
Padding="5,0,0,5"
Name="VolumeStatus"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding !ShowLoadProgress}"
BorderBrush="{DynamicResource ThemeContentBackgroundColor}"
Background="{DynamicResource ThemeContentBackgroundColor}"
IsChecked="{Binding VolumeMuted}"
Content="{Binding VolumeStatusText}">
<ui:ToggleSplitButton.Flyout>
<Flyout Placement="Bottom" ShowMode="TransientWithDismissOnPointerMoveAway">
<Grid Margin="0">
<Slider Value="{Binding Volume}"
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
Minimum="0"
Maximum="1"
TickFrequency="0.05"
IsSnapToTickEnabled="True"
Padding="0"
Margin="0"
SmallChange="0.01"
LargeChange="0.05"
Width="150" />
</Grid>
</Flyout>
</ui:ToggleSplitButton.Flyout>
</ui:ToggleSplitButton>
<Border
Width="2"
Margin="2,0"
IsVisible="{Binding !ShowLoadProgress}"
BorderThickness="1"
Height="12"
BorderBrush="Gray" />
<TextBlock
Margin="5,0,5,0"
IsVisible="{Binding !ShowLoadProgress}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding GameStatusText}"
TextAlignment="Left" />
<Border
Width="2"
IsVisible="{Binding !ShowLoadProgress}"
Margin="2,0"
BorderThickness="1"
Height="12"
BorderBrush="Gray" />
<TextBlock
Margin="5,0,5,0"
IsVisible="{Binding !ShowLoadProgress}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding FifoStatusText}"
TextAlignment="Left" />
<Border
Width="2"
Margin="2,0"
IsVisible="{Binding !ShowLoadProgress}"
BorderThickness="1"
Height="12"
BorderBrush="Gray" />
<TextBlock
Margin="5,0,5,0"
IsVisible="{Binding !ShowLoadProgress}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding GpuStatusText}"
TextAlignment="Left" />
</StackPanel>
<StackPanel VerticalAlignment="Center" IsVisible="{Binding ShowFirmwareStatus}" Grid.Column="3"
Orientation="Horizontal" Margin="10, 0">
<TextBlock
Name="FirmwareStatus"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0"
Text="{locale:Locale StatusBarSystemVersion}" />
</StackPanel>
</Grid>
</Grid>
</Grid>
</window:StyleableWindow>

View file

@ -0,0 +1,719 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.Win32;
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.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 SixLabors.ImageSharp.PixelFormats;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using InputManager = Ryujinx.Input.HLE.InputManager;
using ProgressBar = Avalonia.Controls.ProgressBar;
namespace Ryujinx.Ava.Ui.Windows
{
public class MainWindow : StyleableWindow
{
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; }
public AppHost AppHost { get; private set; }
public InputManager InputManager { get; private set; }
public RendererControl GlRenderer { get; private set; }
public ContentControl ContentFrame { get; private set; }
public TextBlock LoadStatus { get; private set; }
public TextBlock FirmwareStatus { get; private set; }
public TextBox SearchBox { get; private set; }
public ProgressBar LoadProgressBar { get; private set; }
public Menu Menu { get; private set; }
public MenuItem UpdateMenuItem { get; private set; }
public MenuItem ActionsMenuItem { get; private set; }
public GameGridView GameGrid { get; private set; }
public GameListView GameList { get; private set; }
public OffscreenTextBox HiddenTextBox { get; private set; }
public HotKeyControl FullscreenHotKey { get; private set; }
public HotKeyControl FullscreenHotKey2 { get; private set; }
public HotKeyControl DockToggleHotKey { get; private set; }
public HotKeyControl ExitHotKey { get; private set; }
public ToggleSplitButton VolumeStatus { get; set; }
public MainWindowViewModel ViewModel { get; private 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);
DataContext = ViewModel;
InitializeComponent();
AttachDebugDevTools();
UiHandler = new AvaHostUiHandler(this);
Title = $"Ryujinx {Program.Version}";
Height = Height / Program.WindowScaleFactor;
Width = Width / Program.WindowScaleFactor;
if (Program.PreviewerDetached)
{
Initialize();
ViewModel.Initialize();
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver());
LoadGameList();
CheckLaunchState();
}
if (OperatingSystem.IsLinux())
{
Program.WindowScaleFactor = this.PlatformImpl.RenderScaling;
}
_rendererWaitEvent = new AutoResetEvent(false);
}
[Conditional("DEBUG")]
private void AttachDebugDevTools()
{
this.AttachDevTools();
}
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.FifoStatusText = args.FifoStatus;
ViewModel.GpuStatusText = args.GpuName;
ViewModel.ShowStatusSeparator = true;
});
}
}
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(this, 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(this, 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(this,
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 = ContentFrame.Content as Control;
GlRenderer = new RendererControl(3, 3, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
AppHost = new AppHost(GlRenderer, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
if (!AppHost.LoadGuestApplication().Result)
{
AppHost.DisposeContext();
return;
}
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 Thread(InitializeGame)
{
Name = "GUI.WindowThread"
};
gameThread.Start();
}
private void InitializeGame()
{
GlRenderer.GlInitialized += 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.ShowContent = true;
ViewModel.ShowLoadProgress = false;
ViewModel.IsLoadingIndeterminate = false;
Dispatcher.UIThread.InvokeAsync(() =>
{
ContentFrame.Content = GlRenderer;
if (startFullscreen && WindowState != WindowState.FullScreen)
{
ViewModel.ToggleFullscreen();
}
GlRenderer.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(() =>
{
if (ContentFrame.Content != _mainViewContent)
{
ContentFrame.Content = _mainViewContent;
}
ViewModel.ShowMenuAndStatusBar = true;
ViewModel.ShowContent = true;
ViewModel.ShowLoadProgress = false;
ViewModel.IsLoadingIndeterminate = false;
AppHost = null;
HandleRelaunch();
});
GlRenderer.GlInitialized -= GlRenderer_Created;
GlRenderer = 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, Program.CommandLineProfile);
VirtualFileSystem.ReloadKeySet();
ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient, this);
RefreshFirmwareStatus();
}
protected async 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))
{
await 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 InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
ContentFrame = this.FindControl<ContentControl>("Content");
GameList = this.FindControl<GameListView>("GameList");
LoadStatus = this.FindControl<TextBlock>("LoadStatus");
FirmwareStatus = this.FindControl<TextBlock>("FirmwareStatus");
LoadProgressBar = this.FindControl<ProgressBar>("LoadProgressBar");
SearchBox = this.FindControl<TextBox>("SearchBox");
Menu = this.FindControl<Menu>("Menu");
UpdateMenuItem = this.FindControl<MenuItem>("UpdateMenuItem");
GameGrid = this.FindControl<GameGridView>("GameGrid");
HiddenTextBox = this.FindControl<OffscreenTextBox>("HiddenTextBox");
FullscreenHotKey = this.FindControl<HotKeyControl>("FullscreenHotKey");
FullscreenHotKey2 = this.FindControl<HotKeyControl>("FullscreenHotKey2");
DockToggleHotKey = this.FindControl<HotKeyControl>("DockToggleHotKey");
ExitHotKey = this.FindControl<HotKeyControl>("ExitHotKey");
VolumeStatus = this.FindControl<ToggleSplitButton>("VolumeStatus");
ActionsMenuItem = this.FindControl<MenuItem>("ActionsMenuItem");
VolumeStatus.Click += VolumeStatus_CheckedChanged;
GameGrid.ApplicationOpened += Application_Opened;
GameGrid.DataContext = ViewModel;
GameList.ApplicationOpened += Application_Opened;
GameList.DataContext = ViewModel;
LoadHotKeys();
}
public static void UpdateGraphicsConfig()
{
int resScale = ConfigurationState.Instance.Graphics.ResScale;
float resScaleCustom = ConfigurationState.Instance.Graphics.ResScaleCustom;
GraphicsConfig.ResScale = resScale == -1 ? resScaleCustom : resScale;
GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy;
GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath;
GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache;
}
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 =>
{
DateTime lastPlayedDateTime = DateTime.Parse(appMetadata.LastPlayed);
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(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(this);
if (_isClosing)
{
Close();
}
});
}
}
}

View file

@ -0,0 +1,47 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using FluentAvalonia.UI.Controls;
using System;
using System.IO;
using System.Reflection;
namespace Ryujinx.Ava.Ui.Windows
{
public class StyleableWindow : Window
{
public ContentDialog ContentDialog { get; private set; }
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);
}
public void LoadDialog()
{
ContentDialog = this.FindControl<ContentDialog>("ContentDialog");
}
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
ContentDialog = this.FindControl<ContentDialog>("ContentDialog");
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.SystemChrome | ExtendClientAreaChromeHints.OSXThickTitleBar;
}
}
}

View file

@ -0,0 +1,16 @@
namespace Ryujinx.Ava.Ui.Windows
{
public 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; }
}
}

View file

@ -0,0 +1,37 @@
<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"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
x:Class="Ryujinx.Ava.Ui.Windows.UpdaterWindow"
xmlns:window="clr-namespace:Ryujinx.Ava.Ui.Windows"
CanResize="False"
SizeToContent="Height"
Width="500" MinHeight="500" Height="500"
WindowStartupLocation="CenterOwner"
MinWidth="500"
Title="Ryujinx Updater">
<Grid Margin="20" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="1" HorizontalAlignment="Stretch" TextAlignment="Center" Height="20" Name="MainText" />
<TextBlock Height="20" HorizontalAlignment="Stretch" TextAlignment="Center" Name="SecondaryText" Grid.Row="2" />
<ProgressBar IsVisible="False" HorizontalAlignment="Stretch" Name="ProgressBar" Maximum="100" Minimum="0"
Margin="20" Grid.Row="3" />
<StackPanel IsVisible="False" Name="ButtonBox" Orientation="Horizontal" Spacing="20" Grid.Row="4"
HorizontalAlignment="Right">
<Button Command="{Binding YesPressed}" MinWidth="50">
<TextBlock TextAlignment="Center" Text="{locale:Locale InputDialogYes}" />
</Button>
<Button Command="{Binding NoPressed}" MinWidth="50">
<TextBlock TextAlignment="Center" Text="{locale:Locale InputDialogNo}" />
</Button>
</StackPanel>
</Grid>
</window:StyleableWindow>

View file

@ -0,0 +1,93 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Modules;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace Ryujinx.Ava.Ui.Windows
{
public class UpdaterWindow : StyleableWindow
{
private readonly string _buildUrl;
private readonly MainWindow _mainWindow;
private readonly Version _newVersion;
private bool _restartQuery;
public UpdaterWindow()
{
DataContext = this;
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
Title = LocaleManager.Instance["RyujinxUpdater"];
}
public UpdaterWindow(MainWindow mainWindow, Version newVersion, string buildUrl) : this()
{
_mainWindow = mainWindow;
_newVersion = newVersion;
_buildUrl = buildUrl;
}
public TextBlock MainText { get; set; }
public TextBlock SecondaryText { get; set; }
public ProgressBar ProgressBar { get; set; }
public StackPanel ButtonBox { get; set; }
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
MainText = this.FindControl<TextBlock>("MainText");
SecondaryText = this.FindControl<TextBlock>("SecondaryText");
ProgressBar = this.FindControl<ProgressBar>("ProgressBar");
ButtonBox = this.FindControl<StackPanel>("ButtonBox");
}
[DllImport("libc", SetLastError = true)]
private static extern int chmod(string path, uint mode);
public void YesPressed()
{
if (_restartQuery)
{
string ryuName = OperatingSystem.IsWindows() ? "Ryujinx.exe" : "Ryujinx";
string ryuExe = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ryuName);
string ryuArg = string.Join(" ", Environment.GetCommandLineArgs().AsEnumerable().Skip(1).ToArray());
if (!OperatingSystem.IsWindows())
{
chmod(ryuExe, 0777);
}
Process.Start(ryuExe, ryuArg);
Environment.Exit(0);
}
else
{
ButtonBox.IsVisible = false;
ProgressBar.IsVisible = true;
SecondaryText.Text = "";
_restartQuery = true;
Updater.UpdateRyujinx(this, _buildUrl);
}
}
public void NoPressed()
{
_mainWindow.UpdateMenuItem.IsEnabled = true;
Close();
}
}
}