aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.Ava/Ui/Windows
diff options
context:
space:
mode:
authorEmmanuel Hansen <emmausssss@gmail.com>2022-05-15 11:30:15 +0000
committerGitHub <noreply@github.com>2022-05-15 13:30:15 +0200
commitdeb99d2cae3e80bdf70cb52c6c160094dc7c9292 (patch)
treee60f44d1b4bd45bbf36fcfa750fb99787febfdbe /Ryujinx.Ava/Ui/Windows
parent9ba73ffbe5f78c0403cf102b95768f388da05122 (diff)
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>
Diffstat (limited to 'Ryujinx.Ava/Ui/Windows')
-rw-r--r--Ryujinx.Ava/Ui/Windows/AboutWindow.axaml163
-rw-r--r--Ryujinx.Ava/Ui/Windows/AboutWindow.axaml.cs92
-rw-r--r--Ryujinx.Ava/Ui/Windows/IconColorPicker.cs192
-rw-r--r--Ryujinx.Ava/Ui/Windows/MainWindow.axaml639
-rw-r--r--Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs719
-rw-r--r--Ryujinx.Ava/Ui/Windows/StyleableWindow.cs47
-rw-r--r--Ryujinx.Ava/Ui/Windows/TimeZone.cs16
-rw-r--r--Ryujinx.Ava/Ui/Windows/UpdaterWindow.axaml37
-rw-r--r--Ryujinx.Ava/Ui/Windows/UpdaterWindow.axaml.cs93
9 files changed, 1998 insertions, 0 deletions
diff --git a/Ryujinx.Ava/Ui/Windows/AboutWindow.axaml b/Ryujinx.Ava/Ui/Windows/AboutWindow.axaml
new file mode 100644
index 00000000..20185fd2
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/AboutWindow.axaml
@@ -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>
diff --git a/Ryujinx.Ava/Ui/Windows/AboutWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/AboutWindow.axaml.cs
new file mode 100644
index 00000000..3f93ccb7
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/AboutWindow.axaml.cs
@@ -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");
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/IconColorPicker.cs b/Ryujinx.Ava/Ui/Windows/IconColorPicker.cs
new file mode 100644
index 00000000..7cf4c2ed
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/IconColorPicker.cs
@@ -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);
+ }
+ }
+}
diff --git a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml
new file mode 100644
index 00000000..642f241c
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml
@@ -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> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs
new file mode 100644
index 00000000..33a29be7
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs
@@ -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();
+ }
+ });
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/StyleableWindow.cs b/Ryujinx.Ava/Ui/Windows/StyleableWindow.cs
new file mode 100644
index 00000000..e268cac1
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/StyleableWindow.cs
@@ -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;
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/TimeZone.cs b/Ryujinx.Ava/Ui/Windows/TimeZone.cs
new file mode 100644
index 00000000..e6806f15
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/TimeZone.cs
@@ -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; }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/UpdaterWindow.axaml b/Ryujinx.Ava/Ui/Windows/UpdaterWindow.axaml
new file mode 100644
index 00000000..31654a22
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/UpdaterWindow.axaml
@@ -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> \ No newline at end of file
diff --git a/Ryujinx.Ava/Ui/Windows/UpdaterWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/UpdaterWindow.axaml.cs
new file mode 100644
index 00000000..a3dd75a3
--- /dev/null
+++ b/Ryujinx.Ava/Ui/Windows/UpdaterWindow.axaml.cs
@@ -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();
+ }
+ }
+} \ No newline at end of file