Add features to GUI (#757)

* controller image changes depending on the selected controller type

the new controller image assets are temporary until i get new ones

* Game list scans subdirs for games

* Key file existence check

* Only shows Program NCAs in Application list

* Change shown GUI columns without restarting

* Sort by column if you click on the column header

Columns are sorted as text so there are inaccuracies on some columns

* Fix sort on Time Played, Last Played and File Size columns

* Add ability to designate favourite games #1

TODO:
- Make fav games persistent
- Fix invisible check marks due to theme

* Add ability to designate favourite games #2

Also removed default theme

* Added a Windows specific build condition and a Linux bug fix

* bugfix

* Load metadata from JSONs

* Temp bug fix for MacOS

* lil clean up

* requested changes

* Misc fixes

* edited schema and config

* Show the TitleID of games on the title bar

* gui column config option have names

* Async loading of game list

* bugfix and cleanup

* thog's requested changes

* requested changes and cleanup

still need to fix the gtk seizure

* Fix issue where an ExeFS as a NSP didn't show up in the application list

* Minor fixes

* catch glib unhandled exceptions

* Make sure to do UI manipulation in the main thread

* Print path of invalid files

* Ac_k's requested changes

* Return of the dark theme

* move AboutInfo struct to another file

* sort usings

* changes

- gdkchan's requested changes that have been marked resolved
- made some structs internal as they aren't used outside of the GUI
- renamed Ryujinx.UI to Ryujinx.Ui to fit naming convention and folder structure
- fixed bug where controller type dropdown box is stretched
This commit is contained in:
Xpl0itR 2019-11-29 04:32:51 +00:00 committed by jduncanator
parent c24e1892ad
commit da4e0856c9
46 changed files with 1838 additions and 5511 deletions

View file

@ -105,11 +105,9 @@ namespace Ryujinx.HLE.HOS
public Nacp ControlData { get; set; }
public string CurrentTitle { get; private set; }
public string TitleName { get; private set; }
public string TitleID { get; private set; }
public string TitleId { get; private set; }
public IntegrityCheckLevel FsIntegrityCheckLevel { get; set; }
@ -366,7 +364,7 @@ namespace Ryujinx.HLE.HOS
{
ControlData = new Nacp(controlFile.AsStream());
TitleName = CurrentTitle = ControlData.Descriptions[(int) State.DesiredTitleLanguage].Title;
TitleName = ControlData.Descriptions[(int)State.DesiredTitleLanguage].Title;
}
}
@ -500,12 +498,12 @@ namespace Ryujinx.HLE.HOS
Nacp controlData = new Nacp(controlFile.AsStream());
TitleName = CurrentTitle = controlData.Descriptions[(int)State.DesiredTitleLanguage].Title;
TitleID = metaData.Aci0.TitleId.ToString("x16");
TitleName = controlData.Descriptions[(int)State.DesiredTitleLanguage].Title;
TitleId = metaData.Aci0.TitleId.ToString("x16");
if (string.IsNullOrWhiteSpace(CurrentTitle))
if (string.IsNullOrWhiteSpace(TitleName))
{
TitleName = CurrentTitle = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title;
TitleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title;
}
return controlData;
@ -517,7 +515,7 @@ namespace Ryujinx.HLE.HOS
}
else
{
TitleID = CurrentTitle = metaData.Aci0.TitleId.ToString("x16");
TitleId = metaData.Aci0.TitleId.ToString("x16");
}
}
@ -557,7 +555,7 @@ namespace Ryujinx.HLE.HOS
}
}
TitleID = CurrentTitle = metaData.Aci0.TitleId.ToString("x16");
TitleId = metaData.Aci0.TitleId.ToString("x16");
LoadNso("rtld");
LoadNso("main");
@ -659,8 +657,8 @@ namespace Ryujinx.HLE.HOS
ContentManager.LoadEntries();
TitleName = CurrentTitle = metaData.TitleName;
TitleID = metaData.Aci0.TitleId.ToString("x16");
TitleName = metaData.TitleName;
TitleId = metaData.Aci0.TitleId.ToString("x16");
ProgramLoader.LoadStaticObjects(this, metaData, new IExecutable[] { staticObject });
}

View file

@ -33,7 +33,7 @@ namespace Ryujinx.HLE.HOS.Services.Arp
return new ApplicationLaunchProperty
{
TitleId = BitConverter.ToInt64(StringUtils.HexToBytes(context.Device.System.TitleID), 0),
TitleId = BitConverter.ToInt64(StringUtils.HexToBytes(context.Device.System.TitleId), 0),
Version = 0x00,
BaseGameStorageId = (byte)StorageId.NandSystem,
UpdateGameStorageId = (byte)StorageId.None

View file

@ -3,7 +3,7 @@ using System.IO;
namespace Ryujinx.HLE.Loaders.Npdm
{
class Aci0
public class Aci0
{
private const int Aci0Magic = 'A' << 0 | 'C' << 8 | 'I' << 16 | '0' << 24;

View file

@ -3,7 +3,7 @@ using System.IO;
namespace Ryujinx.HLE.Loaders.Npdm
{
class Acid
public class Acid
{
private const int AcidMagic = 'A' << 0 | 'C' << 8 | 'I' << 16 | 'D' << 24;

View file

@ -2,7 +2,7 @@
namespace Ryujinx.HLE.Loaders.Npdm
{
class FsAccessControl
public class FsAccessControl
{
public int Version { get; private set; }
public ulong PermissionsBitmask { get; private set; }

View file

@ -2,7 +2,7 @@
namespace Ryujinx.HLE.Loaders.Npdm
{
class KernelAccessControl
public class KernelAccessControl
{
public int[] Capabilities { get; private set; }

View file

@ -7,7 +7,7 @@ namespace Ryujinx.HLE.Loaders.Npdm
// https://github.com/SciresM/hactool/blob/master/npdm.c
// https://github.com/SciresM/hactool/blob/master/npdm.h
// http://switchbrew.org/index.php?title=NPDM
class Npdm
public class Npdm
{
private const int MetaMagic = 'M' << 0 | 'E' << 8 | 'T' << 16 | 'A' << 24;

View file

@ -5,7 +5,7 @@ using System.Text;
namespace Ryujinx.HLE.Loaders.Npdm
{
class ServiceAccessControl
public class ServiceAccessControl
{
public IReadOnlyDictionary<string, bool> Services { get; private set; }

View file

@ -4,9 +4,15 @@
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="I" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ASET/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Astc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Luma/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mins/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=nacp/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Npad/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=patreon/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Probs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ryujinx/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Sint/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Snorm/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Srgb/@EntryIndexedValue">True</s:Boolean>

View file

@ -7,7 +7,9 @@
"logging_enable_error": true,
"logging_enable_guest": true,
"logging_enable_fs_access_log": false,
"logging_filtered_classes": [ ],
"logging_filtered_classes": [
],
"enable_file_log": true,
"system_language": "AmericanEnglish",
"docked_mode": false,
@ -15,12 +17,27 @@
"enable_vsync": true,
"enable_multicore_scheduling": true,
"enable_fs_integrity_checks": true,
"fs_global_access_log_mode": 0,
"ignore_missing_services": false,
"controller_type": "Handheld",
"gui_columns": [ true, true, true, true, true, true, true, true, true ],
"game_dirs": [],
"gui_columns": {
"fav_column": true,
"icon_column": true,
"app_column": true,
"dev_column": true,
"version_column": true,
"time_played_column": true,
"last_played_column": true,
"file_ext_column": true,
"file_size_column": true,
"path_column": true
},
"game_dirs": [
],
"enable_custom_theme": false,
"custom_theme_path": "",
"enable_keyboard": false,
"keyboard_controls": {
"left_joycon": {
"stick_up": "W",
@ -54,7 +71,7 @@
"toggle_vsync": "Tab"
}
},
"joystick_controls": {
"joystick_controls": {
"enabled": true,
"index": 0,
"deadzone": 0.05,
@ -82,4 +99,4 @@
"button_zr": "Axis5"
}
}
}
}

View file

@ -7,8 +7,8 @@ using Ryujinx.HLE;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.HOS.Services;
using Ryujinx.HLE.Input;
using Ryujinx.UI;
using Ryujinx.UI.Input;
using Ryujinx.Ui;
using Ryujinx.Ui.Input;
using System;
using System.Collections.Generic;
using System.IO;
@ -124,7 +124,7 @@ namespace Ryujinx
/// <summary>
/// Used to toggle columns in the GUI
/// </summary>
public List<bool> GuiColumns { get; set; }
public GuiColumns GuiColumns { get; set; }
/// <summary>
/// A list of directories containing games to be used to load games into the games list
@ -154,7 +154,7 @@ namespace Ryujinx
/// <summary>
/// Controller control bindings
/// </summary>
public UI.Input.NpadController JoystickControls { get; private set; }
public Ui.Input.NpadController JoystickControls { get; private set; }
/// <summary>
/// Loads a configuration file from disk

View file

@ -1,7 +1,7 @@
using Gtk;
using Ryujinx.Common.Logging;
using Ryujinx.Profiler;
using Ryujinx.UI;
using Ryujinx.Ui;
using System;
using System.IO;
@ -18,16 +18,20 @@ namespace Ryujinx
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
GLib.ExceptionManager.UnhandledException += Glib_UnhandledException;
Profile.Initialize();
Application.Init();
Application gtkApplication = new Application("Ryujinx.Ryujinx", GLib.ApplicationFlags.None);
MainWindow mainWindow = new MainWindow(args, gtkApplication);
string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "system", "prod.keys");
string userProfilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".switch", "prod.keys");
if (!File.Exists(appDataPath) && !File.Exists(userProfilePath))
{
GtkDialog.CreateErrorDialog($"Key file was not found. Please refer to `KEYS.md` for more info");
}
gtkApplication.Register(GLib.Cancellable.Current);
gtkApplication.AddWindow(mainWindow);
MainWindow mainWindow = new MainWindow();
mainWindow.Show();
if (args.Length == 1)
@ -45,7 +49,7 @@ namespace Ryujinx
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var exception = e.ExceptionObject as Exception;
Exception exception = e.ExceptionObject as Exception;
Logger.PrintError(LogClass.Emulation, $"Unhandled exception caught: {exception}");
@ -54,5 +58,17 @@ namespace Ryujinx
Logger.Shutdown();
}
}
private static void Glib_UnhandledException(GLib.UnhandledExceptionArgs e)
{
Exception exception = e.ExceptionObject as Exception;
Logger.PrintError(LogClass.Application, $"Unhandled exception caught: {exception}");
if (e.IsTerminating)
{
Logger.Shutdown();
}
}
}
}

View file

@ -9,6 +9,7 @@
010034e005c9c000
01004f8006a78000
010051f00ac5e000
010056e00853a000
0100574009f9e000
0100628004bce000
0100633007d48000
@ -16,15 +17,20 @@
010068f00aa78000
01006a800016e000
010072800cbe8000
01007300020fa000
01007330027ee000
0100749009844000
01007a4008486000
01007ef00011e000
010080b00ad66000
01008db008c2c000
010094e00b52e000
01009aa000faa000
01009b90006dc000
01009cc00c97c000
0100a4200a284000
0100a5c00d162000
0100abf008968000
0100ae000aebc000
0100b3f000be2000
0100bc2004ff4000

View file

@ -18,23 +18,50 @@
<Optimize>false</Optimize>
</PropertyGroup>
<!-- Due to GtkSharp. -->
<!-- Due to .net core 3.0 embedded resource loading -->
<PropertyGroup>
<EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'osx-x64'">
<DefineConstants>MACOS_BUILD</DefineConstants>
</PropertyGroup>
<ItemGroup>
<None Remove="Ui\AboutWindow.glade" />
<None Remove="Ui\assets\BlueCon.png" />
<None Remove="Ui\assets\ProCon.png" />
<None Remove="Ui\assets\RedCon.png" />
<None Remove="Ui\assets\NCAIcon.png" />
<None Remove="Ui\assets\NROIcon.png" />
<None Remove="Ui\assets\NSOIcon.png" />
<None Remove="Ui\assets\NSPIcon.png" />
<None Remove="Ui\assets\XCIIcon.png" />
<None Remove="Ui\assets\DiscordLogo.png" />
<None Remove="Ui\assets\GitHubLogo.png" />
<None Remove="Ui\assets\JoyCon.png" />
<None Remove="Ui\assets\PatreonLogo.png" />
<None Remove="Ui\assets\Icon.png" />
<None Remove="Ui\assets\TwitterLogo.png" />
<None Remove="Ui\MainWindow.glade" />
<None Remove="Ui\SwitchSettings.glade" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Ui\AboutWindow.glade" />
<EmbeddedResource Include="Ui\assets\ryujinxNCAIcon.png" />
<EmbeddedResource Include="Ui\assets\ryujinxNROIcon.png" />
<EmbeddedResource Include="Ui\assets\ryujinxNSOIcon.png" />
<EmbeddedResource Include="Ui\assets\ryujinxNSPIcon.png" />
<EmbeddedResource Include="Ui\assets\ryujinxXCIIcon.png" />
<EmbeddedResource Include="Ui\assets\BlueCon.png" />
<EmbeddedResource Include="Ui\assets\ProCon.png" />
<EmbeddedResource Include="Ui\assets\RedCon.png" />
<EmbeddedResource Include="Ui\assets\NCAIcon.png" />
<EmbeddedResource Include="Ui\assets\NROIcon.png" />
<EmbeddedResource Include="Ui\assets\NSOIcon.png" />
<EmbeddedResource Include="Ui\assets\NSPIcon.png" />
<EmbeddedResource Include="Ui\assets\XCIIcon.png" />
<EmbeddedResource Include="Ui\assets\DiscordLogo.png" />
<EmbeddedResource Include="Ui\assets\GitHubLogo.png" />
<EmbeddedResource Include="Ui\assets\JoyCon.png" />
<EmbeddedResource Include="Ui\assets\PatreonLogo.png" />
<EmbeddedResource Include="Ui\assets\ryujinxIcon.png" />
<EmbeddedResource Include="Ui\assets\Icon.png" />
<EmbeddedResource Include="Ui\assets\TwitterLogo.png" />
<EmbeddedResource Include="Ui\MainWindow.glade" />
<EmbeddedResource Include="Ui\SwitchSettings.glade" />
@ -42,8 +69,8 @@
<ItemGroup>
<PackageReference Include="DiscordRichPresence" Version="1.0.121" />
<PackageReference Include="GtkSharp" Version="3.22.24.37" />
<PackageReference Include="GtkSharp.Dependencies" Version="1.0.1" />
<PackageReference Include="GtkSharp" Version="3.22.25.24" />
<PackageReference Include="GtkSharp.Dependencies" Version="1.1.0" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" />
<PackageReference Include="JsonPrettyPrinter" Version="1.0.1.1" />
<PackageReference Include="OpenTK.NetStandard" Version="1.0.4" />
</ItemGroup>
@ -61,9 +88,6 @@
<None Update="Config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Theme.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="RPsupported.dat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

File diff suppressed because it is too large Load diff

9
Ryujinx/Ui/AboutInfo.cs Normal file
View file

@ -0,0 +1,9 @@
namespace Ryujinx.Ui
{
internal struct AboutInfo
{
public string InstallVersion;
public string InstallCommit;
public string InstallBranch;
}
}

View file

@ -1,27 +1,22 @@
using Gtk;
using GUI = Gtk.Builder.ObjectAttribute;
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using Utf8Json;
using Utf8Json.Resolvers;
using System.IO;
namespace Ryujinx.UI
using GUI = Gtk.Builder.ObjectAttribute;
namespace Ryujinx.Ui
{
public struct Info
{
public string InstallVersion;
public string InstallCommit;
public string InstallBranch;
}
public class AboutWindow : Window
{
public static Info Information { get; private set; }
private static AboutInfo AboutInformation { get; set; }
#pragma warning disable 649
#pragma warning disable CS0649
#pragma warning disable IDE0044
[GUI] Window _aboutWin;
[GUI] Label _versionText;
[GUI] Image _ryujinxLogo;
@ -29,7 +24,8 @@ namespace Ryujinx.UI
[GUI] Image _gitHubLogo;
[GUI] Image _discordLogo;
[GUI] Image _twitterLogo;
#pragma warning restore 649
#pragma warning restore CS0649
#pragma warning restore IDE0044
public AboutWindow() : this(new Builder("Ryujinx.Ui.AboutWindow.glade")) { }
@ -37,8 +33,8 @@ namespace Ryujinx.UI
{
builder.Autoconnect(this);
_aboutWin.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ryujinxIcon.png");
_ryujinxLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ryujinxIcon.png", 100, 100);
_aboutWin.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png");
_ryujinxLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png" , 100, 100);
_patreonLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.PatreonLogo.png", 30 , 30 );
_gitHubLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.GitHubLogo.png" , 30 , 30 );
_discordLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.DiscordLogo.png", 30 , 30 );
@ -50,10 +46,10 @@ namespace Ryujinx.UI
using (Stream stream = File.OpenRead(System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFS", "Installer", "Config", "Config.json")))
{
Information = JsonSerializer.Deserialize<Info>(stream, resolver);
AboutInformation = JsonSerializer.Deserialize<AboutInfo>(stream, resolver);
}
_versionText.Text = $"Version {Information.InstallVersion} - {Information.InstallBranch} ({Information.InstallCommit})";
_versionText.Text = $"Version {AboutInformation.InstallVersion} - {AboutInformation.InstallBranch} ({AboutInformation.InstallCommit})";
}
catch
{
@ -61,7 +57,7 @@ namespace Ryujinx.UI
}
}
public void OpenUrl(string url)
private static void OpenUrl(string url)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
@ -78,39 +74,39 @@ namespace Ryujinx.UI
}
//Events
private void RyujinxButton_Pressed(object obj, ButtonPressEventArgs args)
private void RyujinxButton_Pressed(object sender, ButtonPressEventArgs args)
{
OpenUrl("https://ryujinx.org");
}
private void PatreonButton_Pressed(object obj, ButtonPressEventArgs args)
private void PatreonButton_Pressed(object sender, ButtonPressEventArgs args)
{
OpenUrl("https://www.patreon.com/ryujinx");
}
private void GitHubButton_Pressed(object obj, ButtonPressEventArgs args)
private void GitHubButton_Pressed(object sender, ButtonPressEventArgs args)
{
OpenUrl("https://github.com/Ryujinx/Ryujinx");
}
private void DiscordButton_Pressed(object obj, ButtonPressEventArgs args)
private void DiscordButton_Pressed(object sender, ButtonPressEventArgs args)
{
OpenUrl("https://discordapp.com/invite/N2FmfVc");
}
private void TwitterButton_Pressed(object obj, ButtonPressEventArgs args)
private void TwitterButton_Pressed(object sender, ButtonPressEventArgs args)
{
OpenUrl("https://twitter.com/RyujinxEmu");
}
private void ContributersButton_Pressed(object obj, ButtonPressEventArgs args)
private void ContributorsButton_Pressed(object sender, ButtonPressEventArgs args)
{
OpenUrl("https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a");
}
private void CloseToggle_Activated(object obj, EventArgs args)
private void CloseToggle_Activated(object sender, EventArgs args)
{
Destroy();
Dispose();
}
}
}

View file

@ -154,10 +154,10 @@
</packing>
</child>
<child>
<object class="GtkLabel">
<object class="GtkLabel" id="license">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Unlicenced</property>
<property name="label" translatable="yes">MIT License</property>
<property name="justify">center</property>
</object>
<packing>
@ -168,7 +168,7 @@
</packing>
</child>
<child>
<object class="GtkLabel">
<object class="GtkLabel" id="disclaimer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Ryujinx is not affiliated with Nintendo,
@ -523,11 +523,11 @@ Andy A (BaronKiko)</property>
</packing>
</child>
<child>
<object class="GtkEventBox" id="ContributersButton">
<object class="GtkEventBox" id="ContributorsButton">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">start</property>
<signal name="button-press-event" handler="ContributersButton_Pressed" swapped="no"/>
<signal name="button-press-event" handler="ContributorsButton_Pressed" swapped="no"/>
<child>
<object class="GtkLabel">
<property name="visible">True</property>

View file

@ -0,0 +1,11 @@
using System;
namespace Ryujinx.Ui
{
public class ApplicationAddedEventArgs : EventArgs
{
public ApplicationData AppData { get; set; }
public int NumAppsFound { get; set; }
public int NumAppsLoaded { get; set; }
}
}

View file

@ -0,0 +1,17 @@
namespace Ryujinx.Ui
{
public struct ApplicationData
{
public bool Favorite { get; set; }
public byte[] Icon { get; set; }
public string TitleName { get; set; }
public string TitleId { get; set; }
public string Developer { get; set; }
public string Version { get; set; }
public string TimePlayed { get; set; }
public string LastPlayed { get; set; }
public string FileExtension { get; set; }
public string FileSize { get; set; }
public string Path { get; set; }
}
}

View file

@ -1,66 +1,50 @@
using LibHac;
using JsonPrettyPrinterPlus;
using LibHac;
using LibHac.Fs;
using LibHac.FsSystem;
using LibHac.FsSystem.NcaUtils;
using LibHac.Spl;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Npdm;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Utf8Json;
using Utf8Json.Resolvers;
using SystemState = Ryujinx.HLE.HOS.SystemState;
using TitleLanguage = Ryujinx.HLE.HOS.SystemState.TitleLanguage;
namespace Ryujinx.UI
namespace Ryujinx.Ui
{
public class ApplicationLibrary
{
private static Keyset KeySet;
private static SystemState.TitleLanguage DesiredTitleLanguage;
public static event EventHandler<ApplicationAddedEventArgs> ApplicationAdded;
private const double SecondsPerMinute = 60.0;
private const double SecondsPerHour = SecondsPerMinute * 60;
private const double SecondsPerDay = SecondsPerHour * 24;
private static readonly byte[] _nspIcon = GetResourceBytes("Ryujinx.Ui.assets.NSPIcon.png");
private static readonly byte[] _xciIcon = GetResourceBytes("Ryujinx.Ui.assets.XCIIcon.png");
private static readonly byte[] _ncaIcon = GetResourceBytes("Ryujinx.Ui.assets.NCAIcon.png");
private static readonly byte[] _nroIcon = GetResourceBytes("Ryujinx.Ui.assets.NROIcon.png");
private static readonly byte[] _nsoIcon = GetResourceBytes("Ryujinx.Ui.assets.NSOIcon.png");
public static byte[] RyujinxNspIcon { get; private set; }
public static byte[] RyujinxXciIcon { get; private set; }
public static byte[] RyujinxNcaIcon { get; private set; }
public static byte[] RyujinxNroIcon { get; private set; }
public static byte[] RyujinxNsoIcon { get; private set; }
private static Keyset _keySet;
private static TitleLanguage _desiredTitleLanguage;
private static ApplicationMetadata _appMetadata;
public static List<ApplicationData> ApplicationLibraryData { get; private set; }
public struct ApplicationData
public static void LoadApplications(List<string> appDirs, Keyset keySet, TitleLanguage desiredTitleLanguage)
{
public byte[] Icon;
public string TitleName;
public string TitleId;
public string Developer;
public string Version;
public string TimePlayed;
public string LastPlayed;
public string FileExt;
public string FileSize;
public string Path;
}
int numApplicationsFound = 0;
int numApplicationsLoaded = 0;
public static void Init(List<string> AppDirs, Keyset keySet, SystemState.TitleLanguage desiredTitleLanguage)
{
KeySet = keySet;
DesiredTitleLanguage = desiredTitleLanguage;
// Loads the default application Icons
RyujinxNspIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNSPIcon.png");
RyujinxXciIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxXCIIcon.png");
RyujinxNcaIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNCAIcon.png");
RyujinxNroIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNROIcon.png");
RyujinxNsoIcon = GetResourceBytes("Ryujinx.Ui.assets.ryujinxNSOIcon.png");
_keySet = keySet;
_desiredTitleLanguage = desiredTitleLanguage;
// Builds the applications list with paths to found applications
List<string> applications = new List<string>();
foreach (string appDir in AppDirs)
foreach (string appDir in appDirs)
{
if (Directory.Exists(appDir) == false)
{
@ -69,30 +53,80 @@ namespace Ryujinx.UI
continue;
}
DirectoryInfo AppDirInfo = new DirectoryInfo(appDir);
foreach (FileInfo App in AppDirInfo.GetFiles())
foreach (string app in Directory.GetFiles(appDir, "*.*", SearchOption.AllDirectories))
{
if ((Path.GetExtension(App.ToString()) == ".xci") ||
(Path.GetExtension(App.ToString()) == ".nca") ||
(Path.GetExtension(App.ToString()) == ".nsp") ||
(Path.GetExtension(App.ToString()) == ".pfs0") ||
(Path.GetExtension(App.ToString()) == ".nro") ||
(Path.GetExtension(App.ToString()) == ".nso"))
if ((Path.GetExtension(app) == ".xci") ||
(Path.GetExtension(app) == ".nro") ||
(Path.GetExtension(app) == ".nso") ||
(Path.GetFileName(app) == "hbl.nsp"))
{
applications.Add(App.ToString());
applications.Add(app);
numApplicationsFound++;
}
else if ((Path.GetExtension(app) == ".nsp") || (Path.GetExtension(app) == ".pfs0"))
{
try
{
bool hasMainNca = false;
PartitionFileSystem nsp = new PartitionFileSystem(new FileStream(app, FileMode.Open, FileAccess.Read).AsStorage());
foreach (DirectoryEntryEx fileEntry in nsp.EnumerateEntries("/", "*.nca"))
{
nsp.OpenFile(out IFile ncaFile, fileEntry.FullPath, OpenMode.Read).ThrowIfFailure();
Nca nca = new Nca(_keySet, ncaFile.AsStorage());
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.Header.ContentType == NcaContentType.Program && !nca.Header.GetFsHeader(dataIndex).IsPatchSection())
{
hasMainNca = true;
}
}
if (!hasMainNca)
{
continue;
}
}
catch (InvalidDataException)
{
Logger.PrintWarning(LogClass.Application, $"{app}: The header key is incorrect or missing and therefore the NCA header content type check has failed.");
}
applications.Add(app);
numApplicationsFound++;
}
else if (Path.GetExtension(app) == ".nca")
{
try
{
Nca nca = new Nca(_keySet, new FileStream(app, FileMode.Open, FileAccess.Read).AsStorage());
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
if (nca.Header.ContentType != NcaContentType.Program || nca.Header.GetFsHeader(dataIndex).IsPatchSection())
{
continue;
}
}
catch (InvalidDataException)
{
Logger.PrintWarning(LogClass.Application, $"{app}: The header key is incorrect or missing and therefore the NCA header content type check has failed.");
}
applications.Add(app);
numApplicationsFound++;
}
}
}
// Loops through applications list, creating a struct for each application and then adding the struct to a list of structs
ApplicationLibraryData = new List<ApplicationData>();
// Loops through applications list, creating a struct and then firing an event containing the struct for each application
foreach (string applicationPath in applications)
{
double filesize = new FileInfo(applicationPath).Length * 0.000000000931;
string titleName = null;
string titleId = null;
string developer = null;
string version = null;
double fileSize = new FileInfo(applicationPath).Length * 0.000000000931;
string titleName = "Unknown";
string titleId = "0000000000000000";
string developer = "Unknown";
string version = "0";
byte[] applicationIcon = null;
using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read))
@ -103,158 +137,48 @@ namespace Ryujinx.UI
{
try
{
IFileSystem controlFs = null;
// Store the ControlFS in variable called controlFs
PartitionFileSystem pfs;
if (Path.GetExtension(applicationPath) == ".xci")
{
Xci xci = new Xci(KeySet, file.AsStorage());
Xci xci = new Xci(_keySet, file.AsStorage());
controlFs = GetControlFs(xci.OpenPartition(XciPartitionType.Secure));
pfs = xci.OpenPartition(XciPartitionType.Secure);
}
else
{
controlFs = GetControlFs(new PartitionFileSystem(file.AsStorage()));
pfs = new PartitionFileSystem(file.AsStorage());
}
// Creates NACP class from the NACP file
controlFs.OpenFile(out IFile controlNacpFile, "/control.nacp", OpenMode.Read).ThrowIfFailure();
// Store the ControlFS in variable called controlFs
IFileSystem controlFs = GetControlFs(pfs);
Nacp controlData = new Nacp(controlNacpFile.AsStream());
// Get the title name, title ID, developer name and version number from the NACP
version = controlData.DisplayVersion;
titleName = controlData.Descriptions[(int)DesiredTitleLanguage].Title;
if (string.IsNullOrWhiteSpace(titleName))
// If this is null then this is probably not a normal NSP, it's probably an ExeFS as an NSP
if (controlFs == null)
{
titleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title;
}
applicationIcon = _nspIcon;
titleId = controlData.PresenceGroupId.ToString("x16");
Result result = pfs.OpenFile(out IFile npdmFile, "/main.npdm", OpenMode.Read);
if (string.IsNullOrWhiteSpace(titleId))
{
titleId = controlData.SaveDataOwnerId.ToString("x16");
}
if (string.IsNullOrWhiteSpace(titleId))
{
titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16");
}
developer = controlData.Descriptions[(int)DesiredTitleLanguage].Developer;
if (string.IsNullOrWhiteSpace(developer))
{
developer = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Developer)).Developer;
}
// Read the icon from the ControlFS and store it as a byte array
try
{
controlFs.OpenFile(out IFile icon, $"/icon_{DesiredTitleLanguage}.dat", OpenMode.Read).ThrowIfFailure();
using (MemoryStream stream = new MemoryStream())
if (result != ResultFs.PathNotFound)
{
icon.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
Npdm npdm = new Npdm(npdmFile.AsStream());
titleName = npdm.TitleName;
titleId = npdm.Aci0.TitleId.ToString("x16");
}
}
catch (HorizonResultException)
else
{
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
{
if (entry.Name == "control.nacp")
{
continue;
}
// Creates NACP class from the NACP file
controlFs.OpenFile(out IFile controlNacpFile, "/control.nacp", OpenMode.Read).ThrowIfFailure();
controlFs.OpenFile(out IFile icon, entry.FullPath, OpenMode.Read).ThrowIfFailure();
using (MemoryStream stream = new MemoryStream())
{
icon.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
}
if (applicationIcon != null)
{
break;
}
}
if (applicationIcon == null)
{
applicationIcon = NspOrXciIcon(applicationPath);
}
}
}
catch (MissingKeyException exception)
{
titleName = "Unknown";
titleId = "Unknown";
developer = "Unknown";
version = "?";
applicationIcon = NspOrXciIcon(applicationPath);
Logger.PrintWarning(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
}
catch (InvalidDataException)
{
titleName = "Unknown";
titleId = "Unknown";
developer = "Unknown";
version = "?";
applicationIcon = NspOrXciIcon(applicationPath);
Logger.PrintWarning(LogClass.Application, $"The file is not an NCA file or the header key is incorrect. Errored File: {applicationPath}");
}
catch (Exception exception)
{
Logger.PrintWarning(LogClass.Application, $"This warning usualy means that you have a DLC in one of you game directories\n{exception}");
continue;
}
}
else if (Path.GetExtension(applicationPath) == ".nro")
{
BinaryReader reader = new BinaryReader(file);
byte[] Read(long Position, int Size)
{
file.Seek(Position, SeekOrigin.Begin);
return reader.ReadBytes(Size);
}
file.Seek(24, SeekOrigin.Begin);
int AssetOffset = reader.ReadInt32();
if (Encoding.ASCII.GetString(Read(AssetOffset, 4)) == "ASET")
{
byte[] IconSectionInfo = Read(AssetOffset + 8, 0x10);
long iconOffset = BitConverter.ToInt64(IconSectionInfo, 0);
long iconSize = BitConverter.ToInt64(IconSectionInfo, 8);
ulong nacpOffset = reader.ReadUInt64();
ulong nacpSize = reader.ReadUInt64();
// Reads and stores game icon as byte array
applicationIcon = Read(AssetOffset + iconOffset, (int)iconSize);
// Creates memory stream out of byte array which is the NACP
using (MemoryStream stream = new MemoryStream(Read(AssetOffset + (int)nacpOffset, (int)nacpSize)))
{
// Creates NACP class from the memory stream
Nacp controlData = new Nacp(stream);
Nacp controlData = new Nacp(controlNacpFile.AsStream());
// Get the title name, title ID, developer name and version number from the NACP
version = controlData.DisplayVersion;
titleName = controlData.Descriptions[(int)DesiredTitleLanguage].Title;
titleName = controlData.Descriptions[(int)_desiredTitleLanguage].Title;
if (string.IsNullOrWhiteSpace(titleName))
{
@ -273,7 +197,123 @@ namespace Ryujinx.UI
titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16");
}
developer = controlData.Descriptions[(int)DesiredTitleLanguage].Developer;
developer = controlData.Descriptions[(int)_desiredTitleLanguage].Developer;
if (string.IsNullOrWhiteSpace(developer))
{
developer = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Developer)).Developer;
}
// Read the icon from the ControlFS and store it as a byte array
try
{
controlFs.OpenFile(out IFile icon, $"/icon_{_desiredTitleLanguage}.dat", OpenMode.Read).ThrowIfFailure();
using (MemoryStream stream = new MemoryStream())
{
icon.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
}
}
catch (HorizonResultException)
{
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
{
if (entry.Name == "control.nacp")
{
continue;
}
controlFs.OpenFile(out IFile icon, entry.FullPath, OpenMode.Read).ThrowIfFailure();
using (MemoryStream stream = new MemoryStream())
{
icon.AsStream().CopyTo(stream);
applicationIcon = stream.ToArray();
}
if (applicationIcon != null)
{
break;
}
}
if (applicationIcon == null)
{
applicationIcon = Path.GetExtension(applicationPath) == ".xci" ? _xciIcon : _nspIcon;
}
}
}
}
catch (MissingKeyException exception)
{
applicationIcon = Path.GetExtension(applicationPath) == ".xci" ? _xciIcon : _nspIcon;
Logger.PrintWarning(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
}
catch (InvalidDataException)
{
applicationIcon = Path.GetExtension(applicationPath) == ".xci" ? _xciIcon : _nspIcon;
Logger.PrintWarning(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}");
}
}
else if (Path.GetExtension(applicationPath) == ".nro")
{
BinaryReader reader = new BinaryReader(file);
byte[] Read(long position, int size)
{
file.Seek(position, SeekOrigin.Begin);
return reader.ReadBytes(size);
}
file.Seek(24, SeekOrigin.Begin);
int assetOffset = reader.ReadInt32();
if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET")
{
byte[] iconSectionInfo = Read(assetOffset + 8, 0x10);
long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0);
long iconSize = BitConverter.ToInt64(iconSectionInfo, 8);
ulong nacpOffset = reader.ReadUInt64();
ulong nacpSize = reader.ReadUInt64();
// Reads and stores game icon as byte array
applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
// Creates memory stream out of byte array which is the NACP
using (MemoryStream stream = new MemoryStream(Read(assetOffset + (int)nacpOffset, (int)nacpSize)))
{
// Creates NACP class from the memory stream
Nacp controlData = new Nacp(stream);
// Get the title name, title ID, developer name and version number from the NACP
version = controlData.DisplayVersion;
titleName = controlData.Descriptions[(int)_desiredTitleLanguage].Title;
if (string.IsNullOrWhiteSpace(titleName))
{
titleName = controlData.Descriptions.ToList().Find(x => !string.IsNullOrWhiteSpace(x.Title)).Title;
}
titleId = controlData.PresenceGroupId.ToString("x16");
if (string.IsNullOrWhiteSpace(titleId))
{
titleId = controlData.SaveDataOwnerId.ToString("x16");
}
if (string.IsNullOrWhiteSpace(titleId))
{
titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16");
}
developer = controlData.Descriptions[(int)_desiredTitleLanguage].Developer;
if (string.IsNullOrWhiteSpace(developer))
{
@ -283,59 +323,50 @@ namespace Ryujinx.UI
}
else
{
applicationIcon = RyujinxNroIcon;
titleName = "Application";
titleId = "0000000000000000";
developer = "Unknown";
version = "?";
applicationIcon = _nroIcon;
}
}
// If its an NCA or NSO we just set defaults
else if ((Path.GetExtension(applicationPath) == ".nca") || (Path.GetExtension(applicationPath) == ".nso"))
{
if (Path.GetExtension(applicationPath) == ".nca")
{
applicationIcon = RyujinxNcaIcon;
}
else if (Path.GetExtension(applicationPath) == ".nso")
{
applicationIcon = RyujinxNsoIcon;
}
string fileName = Path.GetFileName(applicationPath);
string fileExt = Path.GetExtension(applicationPath);
StringBuilder titlename = new StringBuilder();
titlename.Append(fileName);
titlename.Remove(fileName.Length - fileExt.Length, fileExt.Length);
titleName = titlename.ToString();
titleId = "0000000000000000";
version = "?";
developer = "Unknown";
applicationIcon = Path.GetExtension(applicationPath) == ".nca" ? _ncaIcon : _nsoIcon;
titleName = Path.GetFileNameWithoutExtension(applicationPath);
}
}
string[] playedData = GetPlayedData(titleId, "00000000000000000000000000000001");
(bool favorite, string timePlayed, string lastPlayed) = GetMetadata(titleId);
ApplicationData data = new ApplicationData()
{
Icon = applicationIcon,
TitleName = titleName,
TitleId = titleId,
Developer = developer,
Version = version,
TimePlayed = playedData[0],
LastPlayed = playedData[1],
FileExt = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1),
FileSize = (filesize < 1) ? (filesize * 1024).ToString("0.##") + "MB" : filesize.ToString("0.##") + "GB",
Path = applicationPath,
Favorite = favorite,
Icon = applicationIcon,
TitleName = titleName,
TitleId = titleId,
Developer = developer,
Version = version,
TimePlayed = timePlayed,
LastPlayed = lastPlayed,
FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1),
FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + "MB" : fileSize.ToString("0.##") + "GB",
Path = applicationPath,
};
ApplicationLibraryData.Add(data);
numApplicationsLoaded++;
OnApplicationAdded(new ApplicationAddedEventArgs()
{
AppData = data,
NumAppsFound = numApplicationsFound,
NumAppsLoaded = numApplicationsLoaded
});
}
}
protected static void OnApplicationAdded(ApplicationAddedEventArgs e)
{
ApplicationAdded?.Invoke(null, e);
}
private static byte[] GetResourceBytes(string resourceName)
{
Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName);
@ -346,29 +377,29 @@ namespace Ryujinx.UI
return resourceByteArray;
}
private static IFileSystem GetControlFs(PartitionFileSystem Pfs)
private static IFileSystem GetControlFs(PartitionFileSystem pfs)
{
Nca controlNca = null;
// Add keys to keyset if needed
foreach (DirectoryEntryEx ticketEntry in Pfs.EnumerateEntries("/", "*.tik"))
// Add keys to key set if needed
foreach (DirectoryEntryEx ticketEntry in pfs.EnumerateEntries("/", "*.tik"))
{
Result result = Pfs.OpenFile(out IFile ticketFile, ticketEntry.FullPath, OpenMode.Read);
Result result = pfs.OpenFile(out IFile ticketFile, ticketEntry.FullPath, OpenMode.Read);
if (result.IsSuccess())
{
Ticket ticket = new Ticket(ticketFile.AsStream());
KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(KeySet)));
_keySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(ticket.GetTitleKey(_keySet)));
}
}
// Find the Control NCA and store it in variable called controlNca
foreach (DirectoryEntryEx fileEntry in Pfs.EnumerateEntries("/", "*.nca"))
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
Pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath, OpenMode.Read).ThrowIfFailure();
pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath, OpenMode.Read).ThrowIfFailure();
Nca nca = new Nca(KeySet, ncaFile.AsStorage());
Nca nca = new Nca(_keySet, ncaFile.AsStorage());
if (nca.Header.ContentType == NcaContentType.Control)
{
@ -377,84 +408,65 @@ namespace Ryujinx.UI
}
// Return the ControlFS
return controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
return controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
}
private static string[] GetPlayedData(string TitleId, string UserId)
private static (bool favorite, string timePlayed, string lastPlayed) GetMetadata(string titleId)
{
try
string metadataFolder = Path.Combine(new VirtualFileSystem().GetBasePath(), "games", titleId, "gui");
string metadataFile = Path.Combine(metadataFolder, "metadata.json");
IJsonFormatterResolver resolver = CompositeResolver.Create(StandardResolver.AllowPrivateSnakeCase);
if (!File.Exists(metadataFile))
{
string[] playedData = new string[2];
string savePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFS", "nand", "user", "save", "0000000000000000", UserId, TitleId);
Directory.CreateDirectory(metadataFolder);
if (File.Exists(Path.Combine(savePath, "TimePlayed.dat")) == false)
_appMetadata = new ApplicationMetadata
{
Directory.CreateDirectory(savePath);
using (FileStream file = File.OpenWrite(Path.Combine(savePath, "TimePlayed.dat")))
{
file.Write(Encoding.ASCII.GetBytes("0"));
}
}
using (FileStream fs = File.OpenRead(Path.Combine(savePath, "TimePlayed.dat")))
{
using (StreamReader sr = new StreamReader(fs))
{
float timePlayed = float.Parse(sr.ReadLine());
Favorite = false,
TimePlayed = 0,
LastPlayed = "Never"
};
if (timePlayed < SecondsPerMinute)
{
playedData[0] = $"{timePlayed}s";
}
else if (timePlayed < SecondsPerHour)
{
playedData[0] = $"{Math.Round(timePlayed / SecondsPerMinute, 2, MidpointRounding.AwayFromZero)} mins";
}
else if (timePlayed < SecondsPerDay)
{
playedData[0] = $"{Math.Round(timePlayed / SecondsPerHour , 2, MidpointRounding.AwayFromZero)} hrs";
}
else
{
playedData[0] = $"{Math.Round(timePlayed / SecondsPerDay , 2, MidpointRounding.AwayFromZero)} days";
}
}
}
if (File.Exists(Path.Combine(savePath, "LastPlayed.dat")) == false)
{
Directory.CreateDirectory(savePath);
using (FileStream file = File.OpenWrite(Path.Combine(savePath, "LastPlayed.dat")))
{
file.Write(Encoding.ASCII.GetBytes("Never"));
}
}
using (FileStream fs = File.OpenRead(Path.Combine(savePath, "LastPlayed.dat")))
{
using (StreamReader sr = new StreamReader(fs))
{
playedData[1] = sr.ReadLine();
}
}
return playedData;
byte[] saveData = JsonSerializer.Serialize(_appMetadata, resolver);
File.WriteAllText(metadataFile, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson());
}
catch
using (Stream stream = File.OpenRead(metadataFile))
{
return new string[] { "Unknown", "Unknown" };
_appMetadata = JsonSerializer.Deserialize<ApplicationMetadata>(stream, resolver);
}
return (_appMetadata.Favorite, ConvertSecondsToReadableString(_appMetadata.TimePlayed), _appMetadata.LastPlayed);
}
private static byte[] NspOrXciIcon(string applicationPath)
private static string ConvertSecondsToReadableString(double seconds)
{
if (Path.GetExtension(applicationPath) == ".xci")
const int secondsPerMinute = 60;
const int secondsPerHour = secondsPerMinute * 60;
const int secondsPerDay = secondsPerHour * 24;
string readableString;
if (seconds < secondsPerMinute)
{
return RyujinxXciIcon;
readableString = $"{seconds}s";
}
else if (seconds < secondsPerHour)
{
readableString = $"{Math.Round(seconds / secondsPerMinute, 2, MidpointRounding.AwayFromZero)} mins";
}
else if (seconds < secondsPerDay)
{
readableString = $"{Math.Round(seconds / secondsPerHour, 2, MidpointRounding.AwayFromZero)} hrs";
}
else
{
return RyujinxNspIcon;
readableString = $"{Math.Round(seconds / secondsPerDay, 2, MidpointRounding.AwayFromZero)} days";
}
return readableString;
}
}
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.Ui
{
internal struct ApplicationMetadata
{
public bool Favorite { get; set; }
public double TimePlayed { get; set; }
public string LastPlayed { get; set; }
}
}

View file

@ -10,7 +10,7 @@ using System.Threading;
using Stopwatch = System.Diagnostics.Stopwatch;
namespace Ryujinx.UI
namespace Ryujinx.Ui
{
public class GlScreen : GameWindow
{
@ -297,10 +297,13 @@ namespace Ryujinx.UI
double hostFps = _device.Statistics.GetSystemFrameRate();
double gameFps = _device.Statistics.GetGameFrameRate();
string titleSection = string.IsNullOrWhiteSpace(_device.System.CurrentTitle) ? string.Empty
: " | " + _device.System.CurrentTitle;
string titleNameSection = string.IsNullOrWhiteSpace(_device.System.TitleName) ? string.Empty
: " | " + _device.System.TitleName;
_newTitle = $"Ryujinx{titleSection} | Host FPS: {hostFps:0.0} | Game FPS: {gameFps:0.0} | " +
string titleIDSection = string.IsNullOrWhiteSpace(_device.System.TitleId) ? string.Empty
: " | " + _device.System.TitleId.ToUpper();
_newTitle = $"Ryujinx{titleNameSection}{titleIDSection} | Host FPS: {hostFps:0.0} | Game FPS: {gameFps:0.0} | " +
$"Game Vsync: {(_device.EnableDeviceVsync ? "On" : "Off")}";
_titleEvent = true;

23
Ryujinx/Ui/GtkDialog.cs Normal file
View file

@ -0,0 +1,23 @@
using Gtk;
using System.Reflection;
namespace Ryujinx.Ui
{
internal class GtkDialog
{
internal static void CreateErrorDialog(string errorMessage)
{
MessageDialog errorDialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, null)
{
Title = "Ryujinx - Error",
Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"),
Text = "Ryujinx has encountered an error",
SecondaryText = errorMessage,
WindowPosition = WindowPosition.Center
};
errorDialog.SetSizeRequest(100, 20);
errorDialog.Run();
errorDialog.Dispose();
}
}
}

16
Ryujinx/Ui/GuiColumns.cs Normal file
View file

@ -0,0 +1,16 @@
namespace Ryujinx.Ui
{
public struct GuiColumns
{
public bool FavColumn;
public bool IconColumn;
public bool AppColumn;
public bool DevColumn;
public bool VersionColumn;
public bool TimePlayedColumn;
public bool LastPlayedColumn;
public bool FileExtColumn;
public bool FileSizeColumn;
public bool PathColumn;
}
}

View file

@ -1,10 +1,11 @@
using DiscordRPC;
using Gtk;
using GUI = Gtk.Builder.ObjectAttribute;
using JsonPrettyPrinterPlus;
using Ryujinx.Audio;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gal;
using Ryujinx.Graphics.Gal.OpenGL;
using Ryujinx.Graphics.Gal;
using Ryujinx.HLE.FileSystem;
using Ryujinx.Profiler;
using System;
using System.Diagnostics;
@ -12,25 +13,42 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using Utf8Json;
using Utf8Json.Resolvers;
namespace Ryujinx.UI
using GUI = Gtk.Builder.ObjectAttribute;
namespace Ryujinx.Ui
{
public class MainWindow : Window
{
internal static HLE.Switch _device;
private static HLE.Switch _device;
private static IGalRenderer _renderer;
private static IAalOutput _audioOut;
private static Application _gtkApplication;
private static GlScreen _screen;
private static ListStore _tableStore;
private static bool _gameLoaded = false;
private static bool _updatingGameTable;
private static bool _gameLoaded;
private static bool _ending;
private static string _userId = "00000000000000000000000000000001";
private static TreeViewColumn _favColumn;
private static TreeViewColumn _appColumn;
private static TreeViewColumn _devColumn;
private static TreeViewColumn _versionColumn;
private static TreeViewColumn _timePlayedColumn;
private static TreeViewColumn _lastPlayedColumn;
private static TreeViewColumn _fileExtColumn;
private static TreeViewColumn _fileSizeColumn;
private static TreeViewColumn _pathColumn;
private static TreeView _treeView;
public static bool DiscordIntegrationEnabled { get; set; }
@ -38,12 +56,14 @@ namespace Ryujinx.UI
public static RichPresence DiscordPresence;
#pragma warning disable 649
#pragma warning disable CS0649
#pragma warning disable IDE0044
[GUI] Window _mainWin;
[GUI] CheckMenuItem _fullScreen;
[GUI] MenuItem _stopEmulation;
[GUI] CheckMenuItem _favToggle;
[GUI] CheckMenuItem _iconToggle;
[GUI] CheckMenuItem _titleToggle;
[GUI] CheckMenuItem _appToggle;
[GUI] CheckMenuItem _developerToggle;
[GUI] CheckMenuItem _versionToggle;
[GUI] CheckMenuItem _timePlayedToggle;
@ -51,28 +71,33 @@ namespace Ryujinx.UI
[GUI] CheckMenuItem _fileExtToggle;
[GUI] CheckMenuItem _fileSizeToggle;
[GUI] CheckMenuItem _pathToggle;
[GUI] Box _box;
[GUI] TreeView _gameTable;
[GUI] GLArea _glScreen;
#pragma warning restore 649
[GUI] Label _progressLabel;
[GUI] LevelBar _progressBar;
#pragma warning restore CS0649
#pragma warning restore IDE0044
public MainWindow(string[] args, Application gtkApplication) : this(new Builder("Ryujinx.Ui.MainWindow.glade"), args, gtkApplication) { }
public MainWindow() : this(new Builder("Ryujinx.Ui.MainWindow.glade")) { }
private MainWindow(Builder builder, string[] args, Application gtkApplication) : base(builder.GetObject("_mainWin").Handle)
private MainWindow(Builder builder) : base(builder.GetObject("_mainWin").Handle)
{
builder.Autoconnect(this);
DeleteEvent += Window_Close;
ApplicationLibrary.ApplicationAdded += Application_Added;
_renderer = new OglRenderer();
_audioOut = InitializeAudioEngine();
_device = new HLE.Switch(_renderer, _audioOut);
_treeView = _gameTable;
Configuration.Load(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
Configuration.InitialConfigure(_device);
ApplicationLibrary.Init(SwitchSettings.SwitchConfig.GameDirs, _device.System.KeySet, _device.System.State.DesiredTitleLanguage);
_gtkApplication = gtkApplication;
ApplyTheme();
if (DiscordIntegrationEnabled)
@ -94,117 +119,130 @@ namespace Ryujinx.UI
DiscordClient.SetPresence(DiscordPresence);
}
builder.Autoconnect(this);
DeleteEvent += Window_Close;
_mainWin.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ryujinxIcon.png");
_mainWin.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png");
_stopEmulation.Sensitive = false;
if (SwitchSettings.SwitchConfig.GuiColumns[0]) { _iconToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[1]) { _titleToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[2]) { _developerToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[3]) { _versionToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[4]) { _timePlayedToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[5]) { _lastPlayedToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[6]) { _fileExtToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[7]) { _fileSizeToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[8]) { _pathToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns.FavColumn) { _favToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns.IconColumn) { _iconToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns.AppColumn) { _appToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns.DevColumn) { _developerToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns.VersionColumn) { _versionToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns.TimePlayedColumn) { _timePlayedToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns.LastPlayedColumn) { _lastPlayedToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns.FileExtColumn) { _fileExtToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns.FileSizeColumn) { _fileSizeToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns.PathColumn) { _pathToggle.Active = true; }
if (args.Length == 1)
_gameTable.Model = _tableStore = new ListStore(
typeof(bool),
typeof(Gdk.Pixbuf),
typeof(string),
typeof(string),
typeof(string),
typeof(string),
typeof(string),
typeof(string),
typeof(string),
typeof(string));
_tableStore.SetSortFunc(5, TimePlayedSort);
_tableStore.SetSortFunc(6, LastPlayedSort);
_tableStore.SetSortFunc(8, FileSizeSort);
_tableStore.SetSortColumnId(0, SortType.Descending);
UpdateColumns();
#pragma warning disable CS4014
UpdateGameTable();
#pragma warning restore CS4014
}
internal static void ApplyTheme()
{
if (!SwitchSettings.SwitchConfig.EnableCustomTheme)
{
// Temporary code section start, remove this section when game is rendered to the GLArea in the GUI
_box.Remove(_glScreen);
return;
}
if (SwitchSettings.SwitchConfig.GuiColumns[0]) { _gameTable.AppendColumn("Icon", new CellRendererPixbuf(), "pixbuf", 0); }
if (SwitchSettings.SwitchConfig.GuiColumns[1]) { _gameTable.AppendColumn("Application", new CellRendererText(), "text", 1); }
if (SwitchSettings.SwitchConfig.GuiColumns[2]) { _gameTable.AppendColumn("Developer", new CellRendererText(), "text", 2); }
if (SwitchSettings.SwitchConfig.GuiColumns[3]) { _gameTable.AppendColumn("Version", new CellRendererText(), "text", 3); }
if (SwitchSettings.SwitchConfig.GuiColumns[4]) { _gameTable.AppendColumn("Time Played", new CellRendererText(), "text", 4); }
if (SwitchSettings.SwitchConfig.GuiColumns[5]) { _gameTable.AppendColumn("Last Played", new CellRendererText(), "text", 5); }
if (SwitchSettings.SwitchConfig.GuiColumns[6]) { _gameTable.AppendColumn("File Ext", new CellRendererText(), "text", 6); }
if (SwitchSettings.SwitchConfig.GuiColumns[7]) { _gameTable.AppendColumn("File Size", new CellRendererText(), "text", 7); }
if (SwitchSettings.SwitchConfig.GuiColumns[8]) { _gameTable.AppendColumn("Path", new CellRendererText(), "text", 8); }
if (File.Exists(SwitchSettings.SwitchConfig.CustomThemePath) && (System.IO.Path.GetExtension(SwitchSettings.SwitchConfig.CustomThemePath) == ".css"))
{
CssProvider cssProvider = new CssProvider();
_tableStore = new ListStore(typeof(Gdk.Pixbuf), typeof(string), typeof(string), typeof(string), typeof(string), typeof(string), typeof(string), typeof(string), typeof(string));
_gameTable.Model = _tableStore;
cssProvider.LoadFromPath(SwitchSettings.SwitchConfig.CustomThemePath);
UpdateGameTable();
// Temporary code section end
StyleContext.AddProviderForScreen(Gdk.Screen.Default, cssProvider, 800);
}
else
{
_box.Remove(_glScreen);
if (SwitchSettings.SwitchConfig.GuiColumns[0]) { _gameTable.AppendColumn("Icon", new CellRendererPixbuf(), "pixbuf", 0); }
if (SwitchSettings.SwitchConfig.GuiColumns[1]) { _gameTable.AppendColumn("Application", new CellRendererText(), "text", 1); }
if (SwitchSettings.SwitchConfig.GuiColumns[2]) { _gameTable.AppendColumn("Developer", new CellRendererText(), "text", 2); }
if (SwitchSettings.SwitchConfig.GuiColumns[3]) { _gameTable.AppendColumn("Version", new CellRendererText(), "text", 3); }
if (SwitchSettings.SwitchConfig.GuiColumns[4]) { _gameTable.AppendColumn("Time Played", new CellRendererText(), "text", 4); }
if (SwitchSettings.SwitchConfig.GuiColumns[5]) { _gameTable.AppendColumn("Last Played", new CellRendererText(), "text", 5); }
if (SwitchSettings.SwitchConfig.GuiColumns[6]) { _gameTable.AppendColumn("File Ext", new CellRendererText(), "text", 6); }
if (SwitchSettings.SwitchConfig.GuiColumns[7]) { _gameTable.AppendColumn("File Size", new CellRendererText(), "text", 7); }
if (SwitchSettings.SwitchConfig.GuiColumns[8]) { _gameTable.AppendColumn("Path", new CellRendererText(), "text", 8); }
_tableStore = new ListStore(typeof(Gdk.Pixbuf), typeof(string), typeof(string), typeof(string), typeof(string), typeof(string), typeof(string), typeof(string), typeof(string));
_gameTable.Model = _tableStore;
UpdateGameTable();
Logger.PrintWarning(LogClass.Application, $"The \"custom_theme_path\" section in \"Config.json\" contains an invalid path: \"{SwitchSettings.SwitchConfig.CustomThemePath}\".");
}
}
public static void CreateErrorDialog(string errorMessage)
private void UpdateColumns()
{
MessageDialog errorDialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Error, ButtonsType.Ok, errorMessage)
foreach (TreeViewColumn column in _gameTable.Columns)
{
Title = "Ryujinx - Error",
Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ryujinxIcon.png"),
WindowPosition = WindowPosition.Center
};
errorDialog.SetSizeRequest(100, 20);
errorDialog.Run();
errorDialog.Destroy();
_gameTable.RemoveColumn(column);
}
CellRendererToggle favToggle = new CellRendererToggle();
favToggle.Toggled += FavToggle_Toggled;
if (SwitchSettings.SwitchConfig.GuiColumns.FavColumn) { _gameTable.AppendColumn("Fav", favToggle, "active", 0); }
if (SwitchSettings.SwitchConfig.GuiColumns.IconColumn) { _gameTable.AppendColumn("Icon", new CellRendererPixbuf(), "pixbuf", 1); }
if (SwitchSettings.SwitchConfig.GuiColumns.AppColumn) { _gameTable.AppendColumn("Application", new CellRendererText(), "text", 2); }
if (SwitchSettings.SwitchConfig.GuiColumns.DevColumn) { _gameTable.AppendColumn("Developer", new CellRendererText(), "text", 3); }
if (SwitchSettings.SwitchConfig.GuiColumns.VersionColumn) { _gameTable.AppendColumn("Version", new CellRendererText(), "text", 4); }
if (SwitchSettings.SwitchConfig.GuiColumns.TimePlayedColumn) { _gameTable.AppendColumn("Time Played", new CellRendererText(), "text", 5); }
if (SwitchSettings.SwitchConfig.GuiColumns.LastPlayedColumn) { _gameTable.AppendColumn("Last Played", new CellRendererText(), "text", 6); }
if (SwitchSettings.SwitchConfig.GuiColumns.FileExtColumn) { _gameTable.AppendColumn("File Ext", new CellRendererText(), "text", 7); }
if (SwitchSettings.SwitchConfig.GuiColumns.FileSizeColumn) { _gameTable.AppendColumn("File Size", new CellRendererText(), "text", 8); }
if (SwitchSettings.SwitchConfig.GuiColumns.PathColumn) { _gameTable.AppendColumn("Path", new CellRendererText(), "text", 9); }
foreach (TreeViewColumn column in _gameTable.Columns)
{
if (column.Title == "Fav") { _favColumn = column; }
else if (column.Title == "Application") { _appColumn = column; }
else if (column.Title == "Developer") { _devColumn = column; }
else if (column.Title == "Version") { _versionColumn = column; }
else if (column.Title == "Time Played") { _timePlayedColumn = column; }
else if (column.Title == "Last Played") { _lastPlayedColumn = column; }
else if (column.Title == "File Ext") { _fileExtColumn = column; }
else if (column.Title == "File Size") { _fileSizeColumn = column; }
else if (column.Title == "Path") { _pathColumn = column; }
}
if (SwitchSettings.SwitchConfig.GuiColumns.FavColumn) { _favColumn.SortColumnId = 0; }
if (SwitchSettings.SwitchConfig.GuiColumns.IconColumn) { _appColumn.SortColumnId = 2; }
if (SwitchSettings.SwitchConfig.GuiColumns.AppColumn) { _devColumn.SortColumnId = 3; }
if (SwitchSettings.SwitchConfig.GuiColumns.DevColumn) { _versionColumn.SortColumnId = 4; }
if (SwitchSettings.SwitchConfig.GuiColumns.TimePlayedColumn) { _timePlayedColumn.SortColumnId = 5; }
if (SwitchSettings.SwitchConfig.GuiColumns.LastPlayedColumn) { _lastPlayedColumn.SortColumnId = 6; }
if (SwitchSettings.SwitchConfig.GuiColumns.FileExtColumn) { _fileExtColumn.SortColumnId = 7; }
if (SwitchSettings.SwitchConfig.GuiColumns.FileSizeColumn) { _fileSizeColumn.SortColumnId = 8; }
if (SwitchSettings.SwitchConfig.GuiColumns.PathColumn) { _pathColumn.SortColumnId = 9; }
}
public static void UpdateGameTable()
internal static async Task UpdateGameTable()
{
if (_updatingGameTable)
{
return;
}
_updatingGameTable = true;
_tableStore.Clear();
ApplicationLibrary.Init(SwitchSettings.SwitchConfig.GameDirs, _device.System.KeySet, _device.System.State.DesiredTitleLanguage);
foreach (ApplicationLibrary.ApplicationData AppData in ApplicationLibrary.ApplicationLibraryData)
{
_tableStore.AppendValues(new Gdk.Pixbuf(AppData.Icon, 75, 75), $"{AppData.TitleName}\n{AppData.TitleId.ToUpper()}", AppData.Developer, AppData.Version, AppData.TimePlayed, AppData.LastPlayed, AppData.FileExt, AppData.FileSize, AppData.Path);
}
}
await Task.Run(() => ApplicationLibrary.LoadApplications(SwitchSettings.SwitchConfig.GameDirs, _device.System.KeySet, _device.System.State.DesiredTitleLanguage));
public static void ApplyTheme()
{
CssProvider cssProvider = new CssProvider();
if (SwitchSettings.SwitchConfig.EnableCustomTheme)
{
if (File.Exists(SwitchSettings.SwitchConfig.CustomThemePath) && (System.IO.Path.GetExtension(SwitchSettings.SwitchConfig.CustomThemePath) == ".css"))
{
cssProvider.LoadFromPath(SwitchSettings.SwitchConfig.CustomThemePath);
}
else
{
Logger.PrintWarning(LogClass.Application, $"The \"custom_theme_path\" section in \"Config.json\" contains an invalid path: \"{SwitchSettings.SwitchConfig.CustomThemePath}\"");
}
}
else
{
cssProvider.LoadFromPath(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Theme.css"));
}
StyleContext.AddProviderForScreen(Gdk.Screen.Default, cssProvider, 800);
_updatingGameTable = false;
}
internal void LoadApplication(string path)
{
if (_gameLoaded)
{
CreateErrorDialog("A game has already been loaded. Please close the emulator and try again");
GtkDialog.CreateErrorDialog("A game has already been loaded. Please close the emulator and try again");
}
else
{
@ -266,19 +304,23 @@ namespace Ryujinx.UI
End();
}
new Thread(new ThreadStart(CreateGameWindow)).Start();
#if MACOS_BUILD
CreateGameWindow();
#else
new Thread(CreateGameWindow).Start();
#endif
_gameLoaded = true;
_stopEmulation.Sensitive = true;
if (DiscordIntegrationEnabled)
{
if (File.ReadAllLines(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "RPsupported.dat")).Contains(_device.System.TitleID))
if (File.ReadAllLines(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "RPsupported.dat")).Contains(_device.System.TitleId))
{
DiscordPresence.Assets.LargeImageKey = _device.System.TitleID;
DiscordPresence.Assets.LargeImageKey = _device.System.TitleId;
}
string state = _device.System.TitleID;
string state = _device.System.TitleId;
if (state == null)
{
@ -306,40 +348,37 @@ namespace Ryujinx.UI
DiscordClient.SetPresence(DiscordPresence);
}
try
string metadataFolder = System.IO.Path.Combine(new VirtualFileSystem().GetBasePath(), "games", _device.System.TitleId, "gui");
string metadataFile = System.IO.Path.Combine(metadataFolder, "metadata.json");
IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase });
ApplicationMetadata appMetadata;
if (!File.Exists(metadataFile))
{
string savePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFS", "nand", "user", "save", "0000000000000000", _userId, _device.System.TitleID);
Directory.CreateDirectory(metadataFolder);
if (File.Exists(System.IO.Path.Combine(savePath, "TimePlayed.dat")) == false)
appMetadata = new ApplicationMetadata
{
Directory.CreateDirectory(savePath);
using (FileStream stream = File.OpenWrite(System.IO.Path.Combine(savePath, "TimePlayed.dat")))
{
stream.Write(Encoding.ASCII.GetBytes("0"));
}
}
Favorite = false,
TimePlayed = 0,
LastPlayed = "Never"
};
if (File.Exists(System.IO.Path.Combine(savePath, "LastPlayed.dat")) == false)
{
Directory.CreateDirectory(savePath);
using (FileStream stream = File.OpenWrite(System.IO.Path.Combine(savePath, "LastPlayed.dat")))
{
stream.Write(Encoding.ASCII.GetBytes("Never"));
}
}
using (FileStream stream = File.OpenWrite(System.IO.Path.Combine(savePath, "LastPlayed.dat")))
{
using (StreamWriter writer = new StreamWriter(stream))
{
writer.WriteLine(DateTime.UtcNow);
}
}
byte[] data = JsonSerializer.Serialize(appMetadata, resolver);
File.WriteAllText(metadataFile, Encoding.UTF8.GetString(data, 0, data.Length).PrettyPrintJson());
}
catch (ArgumentNullException)
using (Stream stream = File.OpenRead(metadataFile))
{
Logger.PrintWarning(LogClass.Application, $"Could not access save path to retrieve time/last played data using: UserID: {_userId}, TitleID: {_device.System.TitleID}");
appMetadata = JsonSerializer.Deserialize<ApplicationMetadata>(stream, resolver);
}
appMetadata.LastPlayed = DateTime.UtcNow.ToString();
byte[] saveData = JsonSerializer.Serialize(appMetadata, resolver);
File.WriteAllText(metadataFile, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson());
}
}
@ -347,9 +386,9 @@ namespace Ryujinx.UI
{
Configuration.ConfigureHid(_device, SwitchSettings.SwitchConfig);
using (GlScreen screen = new GlScreen(_device, _renderer))
using (_screen = new GlScreen(_device, _renderer))
{
screen.MainLoop();
_screen.MainLoop();
End();
}
@ -357,41 +396,49 @@ namespace Ryujinx.UI
private static void End()
{
if (_ending)
{
return;
}
_ending = true;
if (_gameLoaded)
{
try
{
string savePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFS", "nand", "user", "save", "0000000000000000", _userId, _device.System.TitleID);
double currentPlayTime = 0;
string metadataFolder = System.IO.Path.Combine(new VirtualFileSystem().GetBasePath(), "games", _device.System.TitleId, "gui");
string metadataFile = System.IO.Path.Combine(metadataFolder, "metadata.json");
using (FileStream stream = File.OpenRead(System.IO.Path.Combine(savePath, "LastPlayed.dat")))
IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase });
ApplicationMetadata appMetadata;
if (!File.Exists(metadataFile))
{
Directory.CreateDirectory(metadataFolder);
appMetadata = new ApplicationMetadata
{
using (StreamReader reader = new StreamReader(stream))
{
DateTime startTime = DateTime.Parse(reader.ReadLine());
Favorite = false,
TimePlayed = 0,
LastPlayed = "Never"
};
using (FileStream lastPlayedStream = File.OpenRead(System.IO.Path.Combine(savePath, "TimePlayed.dat")))
{
using (StreamReader lastPlayedReader = new StreamReader(lastPlayedStream))
{
currentPlayTime = double.Parse(lastPlayedReader.ReadLine());
}
}
using (FileStream timePlayedStream = File.OpenWrite(System.IO.Path.Combine(savePath, "TimePlayed.dat")))
{
using (StreamWriter timePlayedWriter = new StreamWriter(timePlayedStream))
{
timePlayedWriter.WriteLine(currentPlayTime + Math.Round(DateTime.UtcNow.Subtract(startTime).TotalSeconds, MidpointRounding.AwayFromZero));
}
}
}
}
byte[] data = JsonSerializer.Serialize(appMetadata, resolver);
File.WriteAllText(metadataFile, Encoding.UTF8.GetString(data, 0, data.Length).PrettyPrintJson());
}
catch (ArgumentNullException)
using (Stream stream = File.OpenRead(metadataFile))
{
Logger.PrintWarning(LogClass.Application, $"Could not access save path to retrieve time/last played data using: UserID: {_userId}, TitleID: {_device.System.TitleID}");
appMetadata = JsonSerializer.Deserialize<ApplicationMetadata>(stream, resolver);
}
DateTime lastPlayedDateTime = DateTime.Parse(appMetadata.LastPlayed);
double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds;
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
byte[] saveData = JsonSerializer.Serialize(appMetadata, resolver);
File.WriteAllText(metadataFile, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson());
}
Profile.FinishProfiling();
@ -423,15 +470,69 @@ namespace Ryujinx.UI
}
//Events
private void Row_Activated(object o, RowActivatedArgs args)
private void Application_Added(object sender, ApplicationAddedEventArgs e)
{
Application.Invoke(delegate
{
_tableStore.AppendValues(
e.AppData.Favorite,
new Gdk.Pixbuf(e.AppData.Icon, 75, 75),
$"{e.AppData.TitleName}\n{e.AppData.TitleId.ToUpper()}",
e.AppData.Developer,
e.AppData.Version,
e.AppData.TimePlayed,
e.AppData.LastPlayed,
e.AppData.FileExtension,
e.AppData.FileSize,
e.AppData.Path);
_progressLabel.Text = $"{e.NumAppsLoaded}/{e.NumAppsFound} Games Loaded";
_progressBar.Value = (float)e.NumAppsLoaded / e.NumAppsFound;
});
}
private void FavToggle_Toggled(object sender, ToggledArgs args)
{
_tableStore.GetIter(out TreeIter treeIter, new TreePath(args.Path));
string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower();
string metadataPath = System.IO.Path.Combine(new VirtualFileSystem().GetBasePath(), "games", titleId, "gui", "metadata.json");
IJsonFormatterResolver resolver = CompositeResolver.Create(new[] { StandardResolver.AllowPrivateSnakeCase });
ApplicationMetadata appMetadata;
using (Stream stream = File.OpenRead(metadataPath))
{
appMetadata = JsonSerializer.Deserialize<ApplicationMetadata>(stream, resolver);
}
if ((bool)_tableStore.GetValue(treeIter, 0))
{
_tableStore.SetValue(treeIter, 0, false);
appMetadata.Favorite = false;
}
else
{
_tableStore.SetValue(treeIter, 0, true);
appMetadata.Favorite = true;
}
byte[] saveData = JsonSerializer.Serialize(appMetadata, resolver);
File.WriteAllText(metadataPath, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson());
}
private void Row_Activated(object sender, RowActivatedArgs args)
{
_tableStore.GetIter(out TreeIter treeIter, new TreePath(args.Path.ToString()));
string path = (string)_tableStore.GetValue(treeIter, 8);
string path = (string)_tableStore.GetValue(treeIter, 9);
LoadApplication(path);
}
private void Load_Application_File(object o, EventArgs args)
private void Load_Application_File(object sender, EventArgs args)
{
FileChooserDialog fileChooser = new FileChooserDialog("Choose the file to open", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept);
@ -448,10 +549,10 @@ namespace Ryujinx.UI
LoadApplication(fileChooser.Filename);
}
fileChooser.Destroy();
fileChooser.Dispose();
}
private void Load_Application_Folder(object o, EventArgs args)
private void Load_Application_Folder(object sender, EventArgs args)
{
FileChooserDialog fileChooser = new FileChooserDialog("Choose the folder to open", this, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept);
@ -460,35 +561,39 @@ namespace Ryujinx.UI
LoadApplication(fileChooser.Filename);
}
fileChooser.Destroy();
fileChooser.Dispose();
}
private void Open_Ryu_Folder(object o, EventArgs args)
private void Open_Ryu_Folder(object sender, EventArgs args)
{
Process.Start(new ProcessStartInfo()
{
FileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs"),
FileName = new VirtualFileSystem().GetBasePath(),
UseShellExecute = true,
Verb = "open"
});
}
private void Exit_Pressed(object o, EventArgs args)
private void Exit_Pressed(object sender, EventArgs args)
{
_screen?.Exit();
End();
}
private void Window_Close(object o, DeleteEventArgs args)
private void Window_Close(object sender, DeleteEventArgs args)
{
_screen?.Exit();
End();
}
private void StopEmulation_Pressed(object o, EventArgs args)
private void StopEmulation_Pressed(object sender, EventArgs args)
{
// TODO: Write logic to kill running game
_gameLoaded = false;
}
private void FullScreen_Toggled(object o, EventArgs args)
private void FullScreen_Toggled(object sender, EventArgs args)
{
if (_fullScreen.Active)
{
@ -500,19 +605,15 @@ namespace Ryujinx.UI
}
}
private void Settings_Pressed(object o, EventArgs args)
private void Settings_Pressed(object sender, EventArgs args)
{
SwitchSettings SettingsWin = new SwitchSettings(_device);
_gtkApplication.Register(GLib.Cancellable.Current);
_gtkApplication.AddWindow(SettingsWin);
SettingsWin.Show();
SwitchSettings settingsWin = new SwitchSettings(_device);
settingsWin.Show();
}
private void Update_Pressed(object o, EventArgs args)
private void Update_Pressed(object sender, EventArgs args)
{
string ryuUpdater = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFS", "RyuUpdater.exe");
string ryuUpdater = System.IO.Path.Combine(new VirtualFileSystem().GetBasePath(), "RyuUpdater.exe");
try
{
@ -520,81 +621,249 @@ namespace Ryujinx.UI
}
catch(System.ComponentModel.Win32Exception)
{
CreateErrorDialog("Update canceled by user or updater was not found");
GtkDialog.CreateErrorDialog("Update canceled by user or updater was not found");
}
}
private void About_Pressed(object o, EventArgs args)
private void About_Pressed(object sender, EventArgs args)
{
AboutWindow AboutWin = new AboutWindow();
_gtkApplication.Register(GLib.Cancellable.Current);
_gtkApplication.AddWindow(AboutWin);
AboutWin.Show();
AboutWindow aboutWin = new AboutWindow();
aboutWin.Show();
}
private void Icon_Toggled(object o, EventArgs args)
private void Fav_Toggled(object sender, EventArgs args)
{
SwitchSettings.SwitchConfig.GuiColumns[0] = _iconToggle.Active;
GuiColumns updatedColumns = SwitchSettings.SwitchConfig.GuiColumns;
updatedColumns.FavColumn = _favToggle.Active;
SwitchSettings.SwitchConfig.GuiColumns = updatedColumns;
Configuration.SaveConfig(SwitchSettings.SwitchConfig, System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
UpdateColumns();
}
private void Title_Toggled(object o, EventArgs args)
private void Icon_Toggled(object sender, EventArgs args)
{
SwitchSettings.SwitchConfig.GuiColumns[1] = _titleToggle.Active;
GuiColumns updatedColumns = SwitchSettings.SwitchConfig.GuiColumns;
updatedColumns.IconColumn = _iconToggle.Active;
SwitchSettings.SwitchConfig.GuiColumns = updatedColumns;
Configuration.SaveConfig(SwitchSettings.SwitchConfig, System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
UpdateColumns();
}
private void Developer_Toggled(object o, EventArgs args)
private void Title_Toggled(object sender, EventArgs args)
{
SwitchSettings.SwitchConfig.GuiColumns[2] = _developerToggle.Active;
GuiColumns updatedColumns = SwitchSettings.SwitchConfig.GuiColumns;
updatedColumns.AppColumn = _appToggle.Active;
SwitchSettings.SwitchConfig.GuiColumns = updatedColumns;
Configuration.SaveConfig(SwitchSettings.SwitchConfig, System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
UpdateColumns();
}
private void Version_Toggled(object o, EventArgs args)
private void Developer_Toggled(object sender, EventArgs args)
{
SwitchSettings.SwitchConfig.GuiColumns[3] = _versionToggle.Active;
GuiColumns updatedColumns = SwitchSettings.SwitchConfig.GuiColumns;
updatedColumns.DevColumn = _developerToggle.Active;
SwitchSettings.SwitchConfig.GuiColumns = updatedColumns;
Configuration.SaveConfig(SwitchSettings.SwitchConfig, System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
UpdateColumns();
}
private void TimePlayed_Toggled(object o, EventArgs args)
private void Version_Toggled(object sender, EventArgs args)
{
SwitchSettings.SwitchConfig.GuiColumns[4] = _timePlayedToggle.Active;
GuiColumns updatedColumns = SwitchSettings.SwitchConfig.GuiColumns;
updatedColumns.VersionColumn = _versionToggle.Active;
SwitchSettings.SwitchConfig.GuiColumns = updatedColumns;
Configuration.SaveConfig(SwitchSettings.SwitchConfig, System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
UpdateColumns();
}
private void LastPlayed_Toggled(object o, EventArgs args)
private void TimePlayed_Toggled(object sender, EventArgs args)
{
SwitchSettings.SwitchConfig.GuiColumns[5] = _lastPlayedToggle.Active;
GuiColumns updatedColumns = SwitchSettings.SwitchConfig.GuiColumns;
updatedColumns.TimePlayedColumn = _timePlayedToggle.Active;
SwitchSettings.SwitchConfig.GuiColumns = updatedColumns;
Configuration.SaveConfig(SwitchSettings.SwitchConfig, System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
UpdateColumns();
}
private void FileExt_Toggled(object o, EventArgs args)
private void LastPlayed_Toggled(object sender, EventArgs args)
{
SwitchSettings.SwitchConfig.GuiColumns[6] = _fileExtToggle.Active;
GuiColumns updatedColumns = SwitchSettings.SwitchConfig.GuiColumns;
updatedColumns.LastPlayedColumn = _lastPlayedToggle.Active;
SwitchSettings.SwitchConfig.GuiColumns = updatedColumns;
Configuration.SaveConfig(SwitchSettings.SwitchConfig, System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
UpdateColumns();
}
private void FileSize_Toggled(object o, EventArgs args)
private void FileExt_Toggled(object sender, EventArgs args)
{
SwitchSettings.SwitchConfig.GuiColumns[7] = _fileSizeToggle.Active;
GuiColumns updatedColumns = SwitchSettings.SwitchConfig.GuiColumns;
updatedColumns.FileExtColumn = _fileExtToggle.Active;
SwitchSettings.SwitchConfig.GuiColumns = updatedColumns;
Configuration.SaveConfig(SwitchSettings.SwitchConfig, System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
UpdateColumns();
}
private void Path_Toggled(object o, EventArgs args)
private void FileSize_Toggled(object sender, EventArgs args)
{
SwitchSettings.SwitchConfig.GuiColumns[8] = _pathToggle.Active;
GuiColumns updatedColumns = SwitchSettings.SwitchConfig.GuiColumns;
updatedColumns.FileSizeColumn = _fileSizeToggle.Active;
SwitchSettings.SwitchConfig.GuiColumns = updatedColumns;
Configuration.SaveConfig(SwitchSettings.SwitchConfig, System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
UpdateColumns();
}
private void Path_Toggled(object sender, EventArgs args)
{
GuiColumns updatedColumns = SwitchSettings.SwitchConfig.GuiColumns;
updatedColumns.PathColumn = _pathToggle.Active;
SwitchSettings.SwitchConfig.GuiColumns = updatedColumns;
Configuration.SaveConfig(SwitchSettings.SwitchConfig, System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
UpdateColumns();
}
private void RefreshList_Pressed(object sender, ButtonReleaseEventArgs args)
{
#pragma warning disable CS4014
UpdateGameTable();
#pragma warning restore CS4014
}
private static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b)
{
string aValue = model.GetValue(a, 5).ToString();
string bValue = model.GetValue(b, 5).ToString();
if (aValue.Length > 4 && aValue.Substring(aValue.Length - 4) == "mins")
{
aValue = (float.Parse(aValue.Substring(0, aValue.Length - 5)) * 60).ToString();
}
else if (aValue.Length > 3 && aValue.Substring(aValue.Length - 3) == "hrs")
{
aValue = (float.Parse(aValue.Substring(0, aValue.Length - 4)) * 3600).ToString();
}
else if (aValue.Length > 4 && aValue.Substring(aValue.Length - 4) == "days")
{
aValue = (float.Parse(aValue.Substring(0, aValue.Length - 5)) * 86400).ToString();
}
else
{
aValue = aValue.Substring(0, aValue.Length - 1);
}
if (bValue.Length > 4 && bValue.Substring(bValue.Length - 4) == "mins")
{
bValue = (float.Parse(bValue.Substring(0, bValue.Length - 5)) * 60).ToString();
}
else if (bValue.Length > 3 && bValue.Substring(bValue.Length - 3) == "hrs")
{
bValue = (float.Parse(bValue.Substring(0, bValue.Length - 4)) * 3600).ToString();
}
else if (bValue.Length > 4 && bValue.Substring(bValue.Length - 4) == "days")
{
bValue = (float.Parse(bValue.Substring(0, bValue.Length - 5)) * 86400).ToString();
}
else
{
bValue = bValue.Substring(0, bValue.Length - 1);
}
if (float.Parse(aValue) > float.Parse(bValue))
{
return -1;
}
else if (float.Parse(bValue) > float.Parse(aValue))
{
return 1;
}
else
{
return 0;
}
}
private static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b)
{
string aValue = model.GetValue(a, 6).ToString();
string bValue = model.GetValue(b, 6).ToString();
if (aValue == "Never")
{
aValue = DateTime.UnixEpoch.ToString();
}
if (bValue == "Never")
{
bValue = DateTime.UnixEpoch.ToString();
}
return DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue));
}
private static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b)
{
string aValue = model.GetValue(a, 8).ToString();
string bValue = model.GetValue(b, 8).ToString();
if (aValue.Substring(aValue.Length - 2) == "GB")
{
aValue = (float.Parse(aValue[0..^2]) * 1024).ToString();
}
else
{
aValue = aValue[0..^2];
}
if (bValue.Substring(bValue.Length - 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

@ -126,13 +126,23 @@
<object class="GtkMenuItem" id="GUIColumns">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Select which GUI columns to enable (restart Ryujinx for these changes to take effect)</property>
<property name="tooltip_text" translatable="yes">Select which GUI columns to enable</property>
<property name="label" translatable="yes">Enable GUI Columns</property>
<property name="use_underline">True</property>
<child type="submenu">
<object class="GtkMenu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkCheckMenuItem" id="_favToggle">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Enable or Disable Favorite Games Column in the game list</property>
<property name="label" translatable="yes">Enable Favorite Games Column</property>
<property name="use_underline">True</property>
<signal name="toggled" handler="Fav_Toggled" swapped="no"/>
</object>
</child>
<child>
<object class="GtkCheckMenuItem" id="_iconToggle">
<property name="visible">True</property>
@ -144,7 +154,7 @@
</object>
</child>
<child>
<object class="GtkCheckMenuItem" id="_titleToggle">
<object class="GtkCheckMenuItem" id="_appToggle">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Enable or Disable Title Name/ID Column in the game list</property>
@ -303,22 +313,96 @@
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="_gameTableWindow">
<object class="GtkBox" id="MainBox">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkTreeView" id="_gameTable">
<object class="GtkScrolledWindow" id="_gameTableWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="headers_clickable">False</property>
<property name="reorderable">True</property>
<property name="hover_selection">True</property>
<signal name="row-activated" handler="Row_Activated" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="_gameTable">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="reorderable">True</property>
<property name="hover_selection">True</property>
<signal name="row-activated" handler="Row_Activated" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="FooterBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkEventBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<signal name="button-release-event" handler="RefreshList_Pressed" swapped="no"/>
<child>
<object class="GtkImage">
<property name="name">RefreshList</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-refresh</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="_progressLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="label" translatable="yes">0/0 Games Loaded</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLevelBar" id="_progressBar">
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
@ -327,20 +411,6 @@
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkGLArea" id="_glScreen">
<property name="width_request">1280</property>
<property name="height_request">720</property>
<property name="visible">True</property>
<property name="app_paintable">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>

View file

@ -3,7 +3,7 @@ using OpenTK.Input;
using Ryujinx.HLE.Input;
using System;
namespace Ryujinx.UI.Input
namespace Ryujinx.Ui.Input
{
public enum ControllerInputId
{
@ -64,7 +64,6 @@ namespace Ryujinx.UI.Input
public struct NpadControllerRight
{
public ControllerInputId Stick;
public ControllerInputId StickY;
public ControllerInputId StickButton;
public ControllerInputId ButtonA;
public ControllerInputId ButtonB;

View file

@ -1,7 +1,7 @@
using OpenTK.Input;
using Ryujinx.HLE.Input;
namespace Ryujinx.UI.Input
namespace Ryujinx.Ui.Input
{
public struct NpadKeyboardLeft
{

View file

@ -1,27 +1,29 @@
using Gtk;
using GUI = Gtk.Builder.ObjectAttribute;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Input;
using Ryujinx.UI.Input;
using Ryujinx.Ui.Input;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
namespace Ryujinx.UI
using GUI = Gtk.Builder.ObjectAttribute;
namespace Ryujinx.Ui
{
public class SwitchSettings : Window
{
internal static Configuration SwitchConfig { get; set; }
internal HLE.Switch Device { get; set; }
private readonly HLE.Switch _device;
private static ListStore _gameDirsBoxStore;
private static bool _listeningForKeypress;
#pragma warning disable 649
#pragma warning disable CS0649
#pragma warning disable IDE0044
[GUI] Window _settingsWin;
[GUI] CheckButton _errorLogToggle;
[GUI] CheckButton _warningLogToggle;
@ -51,7 +53,7 @@ namespace Ryujinx.UI
[GUI] ToggleButton _removeDir;
[GUI] Entry _logPath;
[GUI] Entry _graphicsShadersDumpPath;
[GUI] Image _controllerImage;
[GUI] Image _controller1Image;
[GUI] ComboBoxText _controller1Type;
[GUI] ToggleButton _lStickUp1;
@ -78,67 +80,70 @@ namespace Ryujinx.UI
[GUI] ToggleButton _plus1;
[GUI] ToggleButton _r1;
[GUI] ToggleButton _zR1;
#pragma warning restore 649
#pragma warning restore CS0649
#pragma warning restore IDE0044
public static void ConfigureSettings(Configuration Instance) { SwitchConfig = Instance; }
public static void ConfigureSettings(Configuration instance) { SwitchConfig = instance; }
public SwitchSettings(HLE.Switch device) : this(new Builder("Ryujinx.Ui.SwitchSettings.glade"), device) { }
private SwitchSettings(Builder builder, HLE.Switch device) : base(builder.GetObject("_settingsWin").Handle)
{
Device = device;
builder.Autoconnect(this);
_settingsWin.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ryujinxIcon.png");
_controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.JoyCon.png", 500, 500);
_device = device;
_settingsWin.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png");
_controller1Image.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.JoyCon.png", 500, 500);
//Bind Events
_lStickUp1.Clicked += (o, args) => Button_Pressed(o, args, _lStickUp1);
_lStickDown1.Clicked += (o, args) => Button_Pressed(o, args, _lStickDown1);
_lStickLeft1.Clicked += (o, args) => Button_Pressed(o, args, _lStickLeft1);
_lStickRight1.Clicked += (o, args) => Button_Pressed(o, args, _lStickRight1);
_lStickButton1.Clicked += (o, args) => Button_Pressed(o, args, _lStickButton1);
_dpadUp1.Clicked += (o, args) => Button_Pressed(o, args, _dpadUp1);
_dpadDown1.Clicked += (o, args) => Button_Pressed(o, args, _dpadDown1);
_dpadLeft1.Clicked += (o, args) => Button_Pressed(o, args, _dpadLeft1);
_dpadRight1.Clicked += (o, args) => Button_Pressed(o, args, _dpadRight1);
_minus1.Clicked += (o, args) => Button_Pressed(o, args, _minus1);
_l1.Clicked += (o, args) => Button_Pressed(o, args, _l1);
_zL1.Clicked += (o, args) => Button_Pressed(o, args, _zL1);
_rStickUp1.Clicked += (o, args) => Button_Pressed(o, args, _rStickUp1);
_rStickDown1.Clicked += (o, args) => Button_Pressed(o, args, _rStickDown1);
_rStickLeft1.Clicked += (o, args) => Button_Pressed(o, args, _rStickLeft1);
_rStickRight1.Clicked += (o, args) => Button_Pressed(o, args, _rStickRight1);
_rStickButton1.Clicked += (o, args) => Button_Pressed(o, args, _rStickButton1);
_a1.Clicked += (o, args) => Button_Pressed(o, args, _a1);
_b1.Clicked += (o, args) => Button_Pressed(o, args, _b1);
_x1.Clicked += (o, args) => Button_Pressed(o, args, _x1);
_y1.Clicked += (o, args) => Button_Pressed(o, args, _y1);
_plus1.Clicked += (o, args) => Button_Pressed(o, args, _plus1);
_r1.Clicked += (o, args) => Button_Pressed(o, args, _r1);
_zR1.Clicked += (o, args) => Button_Pressed(o, args, _zR1);
_lStickUp1.Clicked += (sender, args) => Button_Pressed(sender, args, _lStickUp1);
_lStickDown1.Clicked += (sender, args) => Button_Pressed(sender, args, _lStickDown1);
_lStickLeft1.Clicked += (sender, args) => Button_Pressed(sender, args, _lStickLeft1);
_lStickRight1.Clicked += (sender, args) => Button_Pressed(sender, args, _lStickRight1);
_lStickButton1.Clicked += (sender, args) => Button_Pressed(sender, args, _lStickButton1);
_dpadUp1.Clicked += (sender, args) => Button_Pressed(sender, args, _dpadUp1);
_dpadDown1.Clicked += (sender, args) => Button_Pressed(sender, args, _dpadDown1);
_dpadLeft1.Clicked += (sender, args) => Button_Pressed(sender, args, _dpadLeft1);
_dpadRight1.Clicked += (sender, args) => Button_Pressed(sender, args, _dpadRight1);
_minus1.Clicked += (sender, args) => Button_Pressed(sender, args, _minus1);
_l1.Clicked += (sender, args) => Button_Pressed(sender, args, _l1);
_zL1.Clicked += (sender, args) => Button_Pressed(sender, args, _zL1);
_rStickUp1.Clicked += (sender, args) => Button_Pressed(sender, args, _rStickUp1);
_rStickDown1.Clicked += (sender, args) => Button_Pressed(sender, args, _rStickDown1);
_rStickLeft1.Clicked += (sender, args) => Button_Pressed(sender, args, _rStickLeft1);
_rStickRight1.Clicked += (sender, args) => Button_Pressed(sender, args, _rStickRight1);
_rStickButton1.Clicked += (sender, args) => Button_Pressed(sender, args, _rStickButton1);
_a1.Clicked += (sender, args) => Button_Pressed(sender, args, _a1);
_b1.Clicked += (sender, args) => Button_Pressed(sender, args, _b1);
_x1.Clicked += (sender, args) => Button_Pressed(sender, args, _x1);
_y1.Clicked += (sender, args) => Button_Pressed(sender, args, _y1);
_plus1.Clicked += (sender, args) => Button_Pressed(sender, args, _plus1);
_r1.Clicked += (sender, args) => Button_Pressed(sender, args, _r1);
_zR1.Clicked += (sender, args) => Button_Pressed(sender, args, _zR1);
_controller1Type.Changed += (sender, args) => Controller_Changed(sender, args, _controller1Type.ActiveId, _controller1Image);
//Setup Currents
if (SwitchConfig.EnableFileLog) { _fileLogToggle.Click(); }
if (SwitchConfig.LoggingEnableError) { _errorLogToggle.Click(); }
if (SwitchConfig.LoggingEnableWarn) { _warningLogToggle.Click(); }
if (SwitchConfig.LoggingEnableInfo) { _infoLogToggle.Click(); }
if (SwitchConfig.LoggingEnableStub) { _stubLogToggle.Click(); }
if (SwitchConfig.LoggingEnableDebug) { _debugLogToggle.Click(); }
if (SwitchConfig.LoggingEnableGuest) { _guestLogToggle.Click(); }
if (SwitchConfig.LoggingEnableFsAccessLog) { _fsAccessLogToggle.Click(); }
if (SwitchConfig.DockedMode) { _dockedModeToggle.Click(); }
if (SwitchConfig.EnableDiscordIntegration) { _discordToggle.Click(); }
if (SwitchConfig.EnableVsync) { _vSyncToggle.Click(); }
if (SwitchConfig.EnableMulticoreScheduling) { _multiSchedToggle.Click(); }
if (SwitchConfig.EnableFsIntegrityChecks) { _fsicToggle.Click(); }
if (SwitchConfig.IgnoreMissingServices) { _ignoreToggle.Click(); }
if (SwitchConfig.EnableKeyboard) { _directKeyboardAccess.Click(); }
if (SwitchConfig.EnableCustomTheme) { _custThemeToggle.Click(); }
if (SwitchConfig.EnableFileLog) _fileLogToggle.Click();
if (SwitchConfig.LoggingEnableError) _errorLogToggle.Click();
if (SwitchConfig.LoggingEnableWarn) _warningLogToggle.Click();
if (SwitchConfig.LoggingEnableInfo) _infoLogToggle.Click();
if (SwitchConfig.LoggingEnableStub) _stubLogToggle.Click();
if (SwitchConfig.LoggingEnableDebug) _debugLogToggle.Click();
if (SwitchConfig.LoggingEnableGuest) _guestLogToggle.Click();
if (SwitchConfig.LoggingEnableFsAccessLog) _fsAccessLogToggle.Click();
if (SwitchConfig.DockedMode) _dockedModeToggle.Click();
if (SwitchConfig.EnableDiscordIntegration) _discordToggle.Click();
if (SwitchConfig.EnableVsync) _vSyncToggle.Click();
if (SwitchConfig.EnableMulticoreScheduling) _multiSchedToggle.Click();
if (SwitchConfig.EnableFsIntegrityChecks) _fsicToggle.Click();
if (SwitchConfig.IgnoreMissingServices) _ignoreToggle.Click();
if (SwitchConfig.EnableKeyboard) _directKeyboardAccess.Click();
if (SwitchConfig.EnableCustomTheme) _custThemeToggle.Click();
_systemLanguageSelect.SetActiveId(SwitchConfig.SystemLanguage.ToString());
_controller1Type .SetActiveId(SwitchConfig.ControllerType.ToString());
Controller_Changed(null, null, _controller1Type.ActiveId, _controller1Image);
_lStickUp1.Label = SwitchConfig.KeyboardControls.LeftJoycon.StickUp.ToString();
_lStickDown1.Label = SwitchConfig.KeyboardControls.LeftJoycon.StickDown.ToString();
@ -190,7 +195,7 @@ namespace Ryujinx.UI
}
//Events
private void Button_Pressed(object obj, EventArgs args, ToggleButton Button)
private void Button_Pressed(object sender, EventArgs args, ToggleButton button)
{
if (_listeningForKeypress == false)
{
@ -198,25 +203,25 @@ namespace Ryujinx.UI
_listeningForKeypress = true;
void On_KeyPress(object Obj, KeyPressEventArgs KeyPressed)
void On_KeyPress(object o, KeyPressEventArgs keyPressed)
{
string key = KeyPressed.Event.Key.ToString();
string key = keyPressed.Event.Key.ToString();
string capKey = key.First().ToString().ToUpper() + key.Substring(1);
if (Enum.IsDefined(typeof(OpenTK.Input.Key), capKey))
{
Button.Label = capKey;
button.Label = capKey;
}
else if (GdkToOpenTKInput.ContainsKey(key))
else if (GdkToOpenTkInput.ContainsKey(key))
{
Button.Label = GdkToOpenTKInput[key];
button.Label = GdkToOpenTkInput[key];
}
else
{
Button.Label = "Space";
button.Label = "Space";
}
Button.SetStateFlags(0, true);
button.SetStateFlags(0, true);
KeyPressEvent -= On_KeyPress;
@ -225,11 +230,30 @@ namespace Ryujinx.UI
}
else
{
Button.SetStateFlags(0, true);
button.SetStateFlags(0, true);
}
}
private void AddDir_Pressed(object obj, EventArgs args)
private void Controller_Changed(object sender, EventArgs args, string controllerType, Image controllerImage)
{
switch (controllerType)
{
case "ProController":
controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ProCon.png", 500, 500);
break;
case "NpadLeft":
controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.BlueCon.png", 500, 500);
break;
case "NpadRight":
controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.RedCon.png", 500, 500);
break;
default:
controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.JoyCon.png", 500, 500);
break;
}
}
private void AddDir_Pressed(object sender, EventArgs args)
{
if (Directory.Exists(_addGameDirBox.Buffer.Text))
{
@ -239,7 +263,7 @@ namespace Ryujinx.UI
_addDir.SetStateFlags(0, true);
}
private void BrowseDir_Pressed(object obj, EventArgs args)
private void BrowseDir_Pressed(object sender, EventArgs args)
{
FileChooserDialog fileChooser = new FileChooserDialog("Choose the game directory to add to the list", this, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Add", ResponseType.Accept);
@ -248,12 +272,12 @@ namespace Ryujinx.UI
_gameDirsBoxStore.AppendValues(fileChooser.Filename);
}
fileChooser.Destroy();
fileChooser.Dispose();
_browseDir.SetStateFlags(0, true);
}
private void RemoveDir_Pressed(object obj, EventArgs args)
private void RemoveDir_Pressed(object sender, EventArgs args)
{
TreeSelection selection = _gameDirsBox.Selection;
@ -263,14 +287,14 @@ namespace Ryujinx.UI
_removeDir.SetStateFlags(0, true);
}
private void CustThemeToggle_Activated(object obj, EventArgs args)
private void CustThemeToggle_Activated(object sender, EventArgs args)
{
_custThemePath.Sensitive = _custThemeToggle.Active;
_custThemePathLabel.Sensitive = _custThemeToggle.Active;
_browseThemePath.Sensitive = _custThemeToggle.Active;
}
private void BrowseThemeDir_Pressed(object obj, EventArgs args)
private void BrowseThemeDir_Pressed(object sender, EventArgs args)
{
FileChooserDialog fileChooser = new FileChooserDialog("Choose the theme to load", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Select", ResponseType.Accept);
@ -282,12 +306,12 @@ namespace Ryujinx.UI
_custThemePath.Buffer.Text = fileChooser.Filename;
}
fileChooser.Destroy();
fileChooser.Dispose();
_browseThemePath.SetStateFlags(0, true);
}
private void SaveToggle_Activated(object obj, EventArgs args)
private void SaveToggle_Activated(object sender, EventArgs args)
{
List<string> gameDirs = new List<string>();
@ -358,20 +382,21 @@ namespace Ryujinx.UI
SwitchConfig.FsGlobalAccessLogMode = (int)_fsLogSpinAdjustment.Value;
Configuration.SaveConfig(SwitchConfig, System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
Configuration.Configure(Device, SwitchConfig);
Configuration.Configure(_device, SwitchConfig);
MainWindow.ApplyTheme();
#pragma warning disable CS4014
MainWindow.UpdateGameTable();
Destroy();
#pragma warning restore CS4014
Dispose();
}
private void CloseToggle_Activated(object obj, EventArgs args)
private void CloseToggle_Activated(object sender, EventArgs args)
{
Destroy();
Dispose();
}
public readonly Dictionary<string, string> GdkToOpenTKInput = new Dictionary<string, string>()
public readonly Dictionary<string, string> GdkToOpenTkInput = new Dictionary<string, string>()
{
{ "Key_0", "Number0" },
{ "Key_1", "Number1" },

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View file

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

After

Width:  |  Height:  |  Size: 324 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -484,17 +484,28 @@
},
"game_dirs": {
"$id": "#/properties/game_dirs",
"type": "string list",
"type": "array",
"title": "List of Game Directories",
"description": "A list of directories containing games to be used to load games into the games list",
"default": []
},
"gui_columns": {
"$id": "#/properties/gui_columns",
"type": "bool list",
"type": "array",
"title": "Used to toggle columns in the GUI",
"description": "Used to toggle columns in the GUI",
"default": [ true, true, true, true, true, true, true, true, true ]
"default": {
"fav_column": true,
"icon_column": true,
"app_column": true,
"dev_column": true,
"version_column": true,
"time_played_column": true,
"last_played_column": true,
"file_ext_column": true,
"file_size_column": true,
"path_column": true
}
},
"enable_custom_theme": {
"$id": "#/properties/enable_custom_theme",