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 Nacp ControlData { get; set; }
public string CurrentTitle { get; private set; }
public string TitleName { 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; } public IntegrityCheckLevel FsIntegrityCheckLevel { get; set; }
@ -366,7 +364,7 @@ namespace Ryujinx.HLE.HOS
{ {
ControlData = new Nacp(controlFile.AsStream()); 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()); Nacp controlData = new Nacp(controlFile.AsStream());
TitleName = CurrentTitle = controlData.Descriptions[(int)State.DesiredTitleLanguage].Title; TitleName = controlData.Descriptions[(int)State.DesiredTitleLanguage].Title;
TitleID = metaData.Aci0.TitleId.ToString("x16"); 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; return controlData;
@ -517,7 +515,7 @@ namespace Ryujinx.HLE.HOS
} }
else 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("rtld");
LoadNso("main"); LoadNso("main");
@ -659,8 +657,8 @@ namespace Ryujinx.HLE.HOS
ContentManager.LoadEntries(); ContentManager.LoadEntries();
TitleName = CurrentTitle = metaData.TitleName; TitleName = metaData.TitleName;
TitleID = metaData.Aci0.TitleId.ToString("x16"); TitleId = metaData.Aci0.TitleId.ToString("x16");
ProgramLoader.LoadStaticObjects(this, metaData, new IExecutable[] { staticObject }); ProgramLoader.LoadStaticObjects(this, metaData, new IExecutable[] { staticObject });
} }

View file

@ -33,7 +33,7 @@ namespace Ryujinx.HLE.HOS.Services.Arp
return new ApplicationLaunchProperty 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, Version = 0x00,
BaseGameStorageId = (byte)StorageId.NandSystem, BaseGameStorageId = (byte)StorageId.NandSystem,
UpdateGameStorageId = (byte)StorageId.None UpdateGameStorageId = (byte)StorageId.None

View file

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

View file

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

View file

@ -2,7 +2,7 @@
namespace Ryujinx.HLE.Loaders.Npdm namespace Ryujinx.HLE.Loaders.Npdm
{ {
class KernelAccessControl public class KernelAccessControl
{ {
public int[] Capabilities { get; private set; } 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.c
// https://github.com/SciresM/hactool/blob/master/npdm.h // https://github.com/SciresM/hactool/blob/master/npdm.h
// http://switchbrew.org/index.php?title=NPDM // http://switchbrew.org/index.php?title=NPDM
class Npdm public class Npdm
{ {
private const int MetaMagic = 'M' << 0 | 'E' << 8 | 'T' << 16 | 'A' << 24; 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 namespace Ryujinx.HLE.Loaders.Npdm
{ {
class ServiceAccessControl public class ServiceAccessControl
{ {
public IReadOnlyDictionary<string, bool> Services { get; private set; } 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/ForOtherTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@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: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/=Astc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Luma/@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/=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/=Sint/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Snorm/@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> <s:Boolean x:Key="/Default/UserDictionary/Words/=Srgb/@EntryIndexedValue">True</s:Boolean>

View file

@ -7,7 +7,9 @@
"logging_enable_error": true, "logging_enable_error": true,
"logging_enable_guest": true, "logging_enable_guest": true,
"logging_enable_fs_access_log": false, "logging_enable_fs_access_log": false,
"logging_filtered_classes": [ ], "logging_filtered_classes": [
],
"enable_file_log": true, "enable_file_log": true,
"system_language": "AmericanEnglish", "system_language": "AmericanEnglish",
"docked_mode": false, "docked_mode": false,
@ -15,12 +17,27 @@
"enable_vsync": true, "enable_vsync": true,
"enable_multicore_scheduling": true, "enable_multicore_scheduling": true,
"enable_fs_integrity_checks": true, "enable_fs_integrity_checks": true,
"fs_global_access_log_mode": 0,
"ignore_missing_services": false, "ignore_missing_services": false,
"controller_type": "Handheld", "controller_type": "Handheld",
"gui_columns": [ true, true, true, true, true, true, true, true, true ], "gui_columns": {
"game_dirs": [], "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, "enable_custom_theme": false,
"custom_theme_path": "", "custom_theme_path": "",
"enable_keyboard": false,
"keyboard_controls": { "keyboard_controls": {
"left_joycon": { "left_joycon": {
"stick_up": "W", "stick_up": "W",
@ -54,7 +71,7 @@
"toggle_vsync": "Tab" "toggle_vsync": "Tab"
} }
}, },
"joystick_controls": { "joystick_controls": {
"enabled": true, "enabled": true,
"index": 0, "index": 0,
"deadzone": 0.05, "deadzone": 0.05,

View file

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

View file

@ -1,7 +1,7 @@
using Gtk; using Gtk;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Profiler; using Ryujinx.Profiler;
using Ryujinx.UI; using Ryujinx.Ui;
using System; using System;
using System.IO; using System.IO;
@ -18,16 +18,20 @@ namespace Ryujinx
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit; AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
GLib.ExceptionManager.UnhandledException += Glib_UnhandledException;
Profile.Initialize(); Profile.Initialize();
Application.Init(); Application.Init();
Application gtkApplication = new Application("Ryujinx.Ryujinx", GLib.ApplicationFlags.None); string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs", "system", "prod.keys");
MainWindow mainWindow = new MainWindow(args, gtkApplication); 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); MainWindow mainWindow = new MainWindow();
gtkApplication.AddWindow(mainWindow);
mainWindow.Show(); mainWindow.Show();
if (args.Length == 1) if (args.Length == 1)
@ -45,7 +49,7 @@ namespace Ryujinx
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) 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}"); Logger.PrintError(LogClass.Emulation, $"Unhandled exception caught: {exception}");
@ -54,5 +58,17 @@ namespace Ryujinx
Logger.Shutdown(); 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 010034e005c9c000
01004f8006a78000 01004f8006a78000
010051f00ac5e000 010051f00ac5e000
010056e00853a000
0100574009f9e000 0100574009f9e000
0100628004bce000 0100628004bce000
0100633007d48000 0100633007d48000
@ -16,15 +17,20 @@
010068f00aa78000 010068f00aa78000
01006a800016e000 01006a800016e000
010072800cbe8000 010072800cbe8000
01007300020fa000
01007330027ee000 01007330027ee000
0100749009844000 0100749009844000
01007a4008486000 01007a4008486000
01007ef00011e000
010080b00ad66000 010080b00ad66000
01008db008c2c000
010094e00b52e000 010094e00b52e000
01009aa000faa000 01009aa000faa000
01009b90006dc000 01009b90006dc000
01009cc00c97c000
0100a4200a284000 0100a4200a284000
0100a5c00d162000 0100a5c00d162000
0100abf008968000
0100ae000aebc000 0100ae000aebc000
0100b3f000be2000 0100b3f000be2000
0100bc2004ff4000 0100bc2004ff4000

View file

@ -18,23 +18,50 @@
<Optimize>false</Optimize> <Optimize>false</Optimize>
</PropertyGroup> </PropertyGroup>
<!-- Due to GtkSharp. --> <!-- Due to .net core 3.0 embedded resource loading -->
<PropertyGroup> <PropertyGroup>
<EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention> <EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>
</PropertyGroup> </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> <ItemGroup>
<EmbeddedResource Include="Ui\AboutWindow.glade" /> <EmbeddedResource Include="Ui\AboutWindow.glade" />
<EmbeddedResource Include="Ui\assets\ryujinxNCAIcon.png" /> <EmbeddedResource Include="Ui\assets\BlueCon.png" />
<EmbeddedResource Include="Ui\assets\ryujinxNROIcon.png" /> <EmbeddedResource Include="Ui\assets\ProCon.png" />
<EmbeddedResource Include="Ui\assets\ryujinxNSOIcon.png" /> <EmbeddedResource Include="Ui\assets\RedCon.png" />
<EmbeddedResource Include="Ui\assets\ryujinxNSPIcon.png" /> <EmbeddedResource Include="Ui\assets\NCAIcon.png" />
<EmbeddedResource Include="Ui\assets\ryujinxXCIIcon.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\DiscordLogo.png" />
<EmbeddedResource Include="Ui\assets\GitHubLogo.png" /> <EmbeddedResource Include="Ui\assets\GitHubLogo.png" />
<EmbeddedResource Include="Ui\assets\JoyCon.png" /> <EmbeddedResource Include="Ui\assets\JoyCon.png" />
<EmbeddedResource Include="Ui\assets\PatreonLogo.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\assets\TwitterLogo.png" />
<EmbeddedResource Include="Ui\MainWindow.glade" /> <EmbeddedResource Include="Ui\MainWindow.glade" />
<EmbeddedResource Include="Ui\SwitchSettings.glade" /> <EmbeddedResource Include="Ui\SwitchSettings.glade" />
@ -42,8 +69,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DiscordRichPresence" Version="1.0.121" /> <PackageReference Include="DiscordRichPresence" Version="1.0.121" />
<PackageReference Include="GtkSharp" Version="3.22.24.37" /> <PackageReference Include="GtkSharp" Version="3.22.25.24" />
<PackageReference Include="GtkSharp.Dependencies" Version="1.0.1" /> <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="JsonPrettyPrinter" Version="1.0.1.1" />
<PackageReference Include="OpenTK.NetStandard" Version="1.0.4" /> <PackageReference Include="OpenTK.NetStandard" Version="1.0.4" />
</ItemGroup> </ItemGroup>
@ -61,9 +88,6 @@
<None Update="Config.json"> <None Update="Config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="Theme.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="RPsupported.dat"> <None Update="RPsupported.dat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </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 Gtk;
using GUI = Gtk.Builder.ObjectAttribute;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Utf8Json; using Utf8Json;
using Utf8Json.Resolvers; 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 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] Window _aboutWin;
[GUI] Label _versionText; [GUI] Label _versionText;
[GUI] Image _ryujinxLogo; [GUI] Image _ryujinxLogo;
@ -29,7 +24,8 @@ namespace Ryujinx.UI
[GUI] Image _gitHubLogo; [GUI] Image _gitHubLogo;
[GUI] Image _discordLogo; [GUI] Image _discordLogo;
[GUI] Image _twitterLogo; [GUI] Image _twitterLogo;
#pragma warning restore 649 #pragma warning restore CS0649
#pragma warning restore IDE0044
public AboutWindow() : this(new Builder("Ryujinx.Ui.AboutWindow.glade")) { } public AboutWindow() : this(new Builder("Ryujinx.Ui.AboutWindow.glade")) { }
@ -37,8 +33,8 @@ namespace Ryujinx.UI
{ {
builder.Autoconnect(this); builder.Autoconnect(this);
_aboutWin.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ryujinxIcon.png"); _aboutWin.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png");
_ryujinxLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ryujinxIcon.png", 100, 100); _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 ); _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 ); _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 ); _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"))) 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 catch
{ {
@ -61,7 +57,7 @@ namespace Ryujinx.UI
} }
} }
public void OpenUrl(string url) private static void OpenUrl(string url)
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
@ -78,39 +74,39 @@ namespace Ryujinx.UI
} }
//Events //Events
private void RyujinxButton_Pressed(object obj, ButtonPressEventArgs args) private void RyujinxButton_Pressed(object sender, ButtonPressEventArgs args)
{ {
OpenUrl("https://ryujinx.org"); 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"); 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"); 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"); 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"); 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"); 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> </packing>
</child> </child>
<child> <child>
<object class="GtkLabel"> <object class="GtkLabel" id="license">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</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> <property name="justify">center</property>
</object> </object>
<packing> <packing>
@ -168,7 +168,7 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkLabel"> <object class="GtkLabel" id="disclaimer">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="label" translatable="yes">Ryujinx is not affiliated with Nintendo, <property name="label" translatable="yes">Ryujinx is not affiliated with Nintendo,
@ -523,11 +523,11 @@ Andy A (BaronKiko)</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkEventBox" id="ContributersButton"> <object class="GtkEventBox" id="ContributorsButton">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="valign">start</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> <child>
<object class="GtkLabel"> <object class="GtkLabel">
<property name="visible">True</property> <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.Fs;
using LibHac.FsSystem; using LibHac.FsSystem;
using LibHac.FsSystem.NcaUtils; using LibHac.FsSystem.NcaUtils;
using LibHac.Spl; using LibHac.Spl;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Npdm;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; 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 public class ApplicationLibrary
{ {
private static Keyset KeySet; public static event EventHandler<ApplicationAddedEventArgs> ApplicationAdded;
private static SystemState.TitleLanguage DesiredTitleLanguage;
private const double SecondsPerMinute = 60.0; private static readonly byte[] _nspIcon = GetResourceBytes("Ryujinx.Ui.assets.NSPIcon.png");
private const double SecondsPerHour = SecondsPerMinute * 60; private static readonly byte[] _xciIcon = GetResourceBytes("Ryujinx.Ui.assets.XCIIcon.png");
private const double SecondsPerDay = SecondsPerHour * 24; 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; } private static Keyset _keySet;
public static byte[] RyujinxXciIcon { get; private set; } private static TitleLanguage _desiredTitleLanguage;
public static byte[] RyujinxNcaIcon { get; private set; } private static ApplicationMetadata _appMetadata;
public static byte[] RyujinxNroIcon { get; private set; }
public static byte[] RyujinxNsoIcon { get; private set; }
public static List<ApplicationData> ApplicationLibraryData { get; private set; } public static void LoadApplications(List<string> appDirs, Keyset keySet, TitleLanguage desiredTitleLanguage)
public struct ApplicationData
{ {
public byte[] Icon; int numApplicationsFound = 0;
public string TitleName; int numApplicationsLoaded = 0;
public string TitleId;
public string Developer;
public string Version;
public string TimePlayed;
public string LastPlayed;
public string FileExt;
public string FileSize;
public string Path;
}
public static void Init(List<string> AppDirs, Keyset keySet, SystemState.TitleLanguage desiredTitleLanguage) _keySet = keySet;
{ _desiredTitleLanguage = 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");
// Builds the applications list with paths to found applications // Builds the applications list with paths to found applications
List<string> applications = new List<string>(); List<string> applications = new List<string>();
foreach (string appDir in AppDirs) foreach (string appDir in appDirs)
{ {
if (Directory.Exists(appDir) == false) if (Directory.Exists(appDir) == false)
{ {
@ -69,30 +53,80 @@ namespace Ryujinx.UI
continue; continue;
} }
DirectoryInfo AppDirInfo = new DirectoryInfo(appDir); foreach (string app in Directory.GetFiles(appDir, "*.*", SearchOption.AllDirectories))
foreach (FileInfo App in AppDirInfo.GetFiles())
{ {
if ((Path.GetExtension(App.ToString()) == ".xci") || if ((Path.GetExtension(app) == ".xci") ||
(Path.GetExtension(App.ToString()) == ".nca") || (Path.GetExtension(app) == ".nro") ||
(Path.GetExtension(App.ToString()) == ".nsp") || (Path.GetExtension(app) == ".nso") ||
(Path.GetExtension(App.ToString()) == ".pfs0") || (Path.GetFileName(app) == "hbl.nsp"))
(Path.GetExtension(App.ToString()) == ".nro") ||
(Path.GetExtension(App.ToString()) == ".nso"))
{ {
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 // Loops through applications list, creating a struct and then firing an event containing the struct for each application
ApplicationLibraryData = new List<ApplicationData>();
foreach (string applicationPath in applications) foreach (string applicationPath in applications)
{ {
double filesize = new FileInfo(applicationPath).Length * 0.000000000931; double fileSize = new FileInfo(applicationPath).Length * 0.000000000931;
string titleName = null; string titleName = "Unknown";
string titleId = null; string titleId = "0000000000000000";
string developer = null; string developer = "Unknown";
string version = null; string version = "0";
byte[] applicationIcon = null; byte[] applicationIcon = null;
using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read)) using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read))
@ -103,158 +137,48 @@ namespace Ryujinx.UI
{ {
try try
{ {
IFileSystem controlFs = null; PartitionFileSystem pfs;
// Store the ControlFS in variable called controlFs
if (Path.GetExtension(applicationPath) == ".xci") 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 else
{ {
controlFs = GetControlFs(new PartitionFileSystem(file.AsStorage())); pfs = new PartitionFileSystem(file.AsStorage());
} }
// Creates NACP class from the NACP file // Store the ControlFS in variable called controlFs
controlFs.OpenFile(out IFile controlNacpFile, "/control.nacp", OpenMode.Read).ThrowIfFailure(); IFileSystem controlFs = GetControlFs(pfs);
Nacp controlData = new Nacp(controlNacpFile.AsStream()); // If this is null then this is probably not a normal NSP, it's probably an ExeFS as an NSP
if (controlFs == null)
// 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; applicationIcon = _nspIcon;
}
titleId = controlData.PresenceGroupId.ToString("x16"); Result result = pfs.OpenFile(out IFile npdmFile, "/main.npdm", OpenMode.Read);
if (string.IsNullOrWhiteSpace(titleId)) if (result != ResultFs.PathNotFound)
{
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())
{ {
icon.AsStream().CopyTo(stream); Npdm npdm = new Npdm(npdmFile.AsStream());
applicationIcon = stream.ToArray();
titleName = npdm.TitleName;
titleId = npdm.Aci0.TitleId.ToString("x16");
} }
} }
catch (HorizonResultException) else
{ {
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) // Creates NACP class from the NACP file
{ controlFs.OpenFile(out IFile controlNacpFile, "/control.nacp", OpenMode.Read).ThrowIfFailure();
if (entry.Name == "control.nacp")
{
continue;
}
controlFs.OpenFile(out IFile icon, entry.FullPath, OpenMode.Read).ThrowIfFailure(); Nacp controlData = new Nacp(controlNacpFile.AsStream());
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);
// Get the title name, title ID, developer name and version number from the NACP // Get the title name, title ID, developer name and version number from the NACP
version = controlData.DisplayVersion; version = controlData.DisplayVersion;
titleName = controlData.Descriptions[(int)DesiredTitleLanguage].Title; titleName = controlData.Descriptions[(int)_desiredTitleLanguage].Title;
if (string.IsNullOrWhiteSpace(titleName)) if (string.IsNullOrWhiteSpace(titleName))
{ {
@ -273,7 +197,123 @@ namespace Ryujinx.UI
titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); 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)) if (string.IsNullOrWhiteSpace(developer))
{ {
@ -283,59 +323,50 @@ namespace Ryujinx.UI
} }
else else
{ {
applicationIcon = RyujinxNroIcon; applicationIcon = _nroIcon;
titleName = "Application";
titleId = "0000000000000000";
developer = "Unknown";
version = "?";
} }
} }
// If its an NCA or NSO we just set defaults // If its an NCA or NSO we just set defaults
else if ((Path.GetExtension(applicationPath) == ".nca") || (Path.GetExtension(applicationPath) == ".nso")) else if ((Path.GetExtension(applicationPath) == ".nca") || (Path.GetExtension(applicationPath) == ".nso"))
{ {
if (Path.GetExtension(applicationPath) == ".nca") applicationIcon = Path.GetExtension(applicationPath) == ".nca" ? _ncaIcon : _nsoIcon;
{ titleName = Path.GetFileNameWithoutExtension(applicationPath);
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";
} }
} }
string[] playedData = GetPlayedData(titleId, "00000000000000000000000000000001"); (bool favorite, string timePlayed, string lastPlayed) = GetMetadata(titleId);
ApplicationData data = new ApplicationData() ApplicationData data = new ApplicationData()
{ {
Icon = applicationIcon, Favorite = favorite,
TitleName = titleName, Icon = applicationIcon,
TitleId = titleId, TitleName = titleName,
Developer = developer, TitleId = titleId,
Version = version, Developer = developer,
TimePlayed = playedData[0], Version = version,
LastPlayed = playedData[1], TimePlayed = timePlayed,
FileExt = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1), LastPlayed = lastPlayed,
FileSize = (filesize < 1) ? (filesize * 1024).ToString("0.##") + "MB" : filesize.ToString("0.##") + "GB", FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0 ,1),
Path = applicationPath, 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) private static byte[] GetResourceBytes(string resourceName)
{ {
Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName); Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName);
@ -346,29 +377,29 @@ namespace Ryujinx.UI
return resourceByteArray; return resourceByteArray;
} }
private static IFileSystem GetControlFs(PartitionFileSystem Pfs) private static IFileSystem GetControlFs(PartitionFileSystem pfs)
{ {
Nca controlNca = null; Nca controlNca = null;
// Add keys to keyset if needed // Add keys to key set if needed
foreach (DirectoryEntryEx ticketEntry in Pfs.EnumerateEntries("/", "*.tik")) 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()) if (result.IsSuccess())
{ {
Ticket ticket = new Ticket(ticketFile.AsStream()); 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 // 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) if (nca.Header.ContentType == NcaContentType.Control)
{ {
@ -377,84 +408,65 @@ namespace Ryujinx.UI
} }
// Return the ControlFS // 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]; Directory.CreateDirectory(metadataFolder);
string savePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFS", "nand", "user", "save", "0000000000000000", UserId, TitleId);
if (File.Exists(Path.Combine(savePath, "TimePlayed.dat")) == false) _appMetadata = new ApplicationMetadata
{ {
Directory.CreateDirectory(savePath); Favorite = false,
using (FileStream file = File.OpenWrite(Path.Combine(savePath, "TimePlayed.dat"))) TimePlayed = 0,
{ LastPlayed = "Never"
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());
if (timePlayed < SecondsPerMinute) byte[] saveData = JsonSerializer.Serialize(_appMetadata, resolver);
{ File.WriteAllText(metadataFile, Encoding.UTF8.GetString(saveData, 0, saveData.Length).PrettyPrintJson());
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;
} }
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 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; using Stopwatch = System.Diagnostics.Stopwatch;
namespace Ryujinx.UI namespace Ryujinx.Ui
{ {
public class GlScreen : GameWindow public class GlScreen : GameWindow
{ {
@ -297,10 +297,13 @@ namespace Ryujinx.UI
double hostFps = _device.Statistics.GetSystemFrameRate(); double hostFps = _device.Statistics.GetSystemFrameRate();
double gameFps = _device.Statistics.GetGameFrameRate(); double gameFps = _device.Statistics.GetGameFrameRate();
string titleSection = string.IsNullOrWhiteSpace(_device.System.CurrentTitle) ? string.Empty string titleNameSection = string.IsNullOrWhiteSpace(_device.System.TitleName) ? string.Empty
: " | " + _device.System.CurrentTitle; : " | " + _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")}"; $"Game Vsync: {(_device.EnableDeviceVsync ? "On" : "Off")}";
_titleEvent = true; _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 DiscordRPC;
using Gtk; using Gtk;
using GUI = Gtk.Builder.ObjectAttribute; using JsonPrettyPrinterPlus;
using Ryujinx.Audio; using Ryujinx.Audio;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gal;
using Ryujinx.Graphics.Gal.OpenGL; using Ryujinx.Graphics.Gal.OpenGL;
using Ryujinx.Graphics.Gal;
using Ryujinx.HLE.FileSystem;
using Ryujinx.Profiler; using Ryujinx.Profiler;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
@ -12,25 +13,42 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks;
using System.Threading; using System.Threading;
using Utf8Json;
using Utf8Json.Resolvers;
namespace Ryujinx.UI using GUI = Gtk.Builder.ObjectAttribute;
namespace Ryujinx.Ui
{ {
public class MainWindow : Window public class MainWindow : Window
{ {
internal static HLE.Switch _device; private static HLE.Switch _device;
private static IGalRenderer _renderer; private static IGalRenderer _renderer;
private static IAalOutput _audioOut; private static IAalOutput _audioOut;
private static Application _gtkApplication; private static GlScreen _screen;
private static ListStore _tableStore; 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; } public static bool DiscordIntegrationEnabled { get; set; }
@ -38,12 +56,14 @@ namespace Ryujinx.UI
public static RichPresence DiscordPresence; public static RichPresence DiscordPresence;
#pragma warning disable 649 #pragma warning disable CS0649
#pragma warning disable IDE0044
[GUI] Window _mainWin; [GUI] Window _mainWin;
[GUI] CheckMenuItem _fullScreen; [GUI] CheckMenuItem _fullScreen;
[GUI] MenuItem _stopEmulation; [GUI] MenuItem _stopEmulation;
[GUI] CheckMenuItem _favToggle;
[GUI] CheckMenuItem _iconToggle; [GUI] CheckMenuItem _iconToggle;
[GUI] CheckMenuItem _titleToggle; [GUI] CheckMenuItem _appToggle;
[GUI] CheckMenuItem _developerToggle; [GUI] CheckMenuItem _developerToggle;
[GUI] CheckMenuItem _versionToggle; [GUI] CheckMenuItem _versionToggle;
[GUI] CheckMenuItem _timePlayedToggle; [GUI] CheckMenuItem _timePlayedToggle;
@ -51,28 +71,33 @@ namespace Ryujinx.UI
[GUI] CheckMenuItem _fileExtToggle; [GUI] CheckMenuItem _fileExtToggle;
[GUI] CheckMenuItem _fileSizeToggle; [GUI] CheckMenuItem _fileSizeToggle;
[GUI] CheckMenuItem _pathToggle; [GUI] CheckMenuItem _pathToggle;
[GUI] Box _box;
[GUI] TreeView _gameTable; [GUI] TreeView _gameTable;
[GUI] GLArea _glScreen; [GUI] Label _progressLabel;
#pragma warning restore 649 [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(); _renderer = new OglRenderer();
_audioOut = InitializeAudioEngine(); _audioOut = InitializeAudioEngine();
_device = new HLE.Switch(_renderer, _audioOut); _device = new HLE.Switch(_renderer, _audioOut);
_treeView = _gameTable;
Configuration.Load(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json")); Configuration.Load(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
Configuration.InitialConfigure(_device); Configuration.InitialConfigure(_device);
ApplicationLibrary.Init(SwitchSettings.SwitchConfig.GameDirs, _device.System.KeySet, _device.System.State.DesiredTitleLanguage);
_gtkApplication = gtkApplication;
ApplyTheme(); ApplyTheme();
if (DiscordIntegrationEnabled) if (DiscordIntegrationEnabled)
@ -94,117 +119,130 @@ namespace Ryujinx.UI
DiscordClient.SetPresence(DiscordPresence); DiscordClient.SetPresence(DiscordPresence);
} }
builder.Autoconnect(this); _mainWin.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png");
DeleteEvent += Window_Close;
_mainWin.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ryujinxIcon.png");
_stopEmulation.Sensitive = false; _stopEmulation.Sensitive = false;
if (SwitchSettings.SwitchConfig.GuiColumns[0]) { _iconToggle.Active = true; } if (SwitchSettings.SwitchConfig.GuiColumns.FavColumn) { _favToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[1]) { _titleToggle.Active = true; } if (SwitchSettings.SwitchConfig.GuiColumns.IconColumn) { _iconToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[2]) { _developerToggle.Active = true; } if (SwitchSettings.SwitchConfig.GuiColumns.AppColumn) { _appToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[3]) { _versionToggle.Active = true; } if (SwitchSettings.SwitchConfig.GuiColumns.DevColumn) { _developerToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[4]) { _timePlayedToggle.Active = true; } if (SwitchSettings.SwitchConfig.GuiColumns.VersionColumn) { _versionToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[5]) { _lastPlayedToggle.Active = true; } if (SwitchSettings.SwitchConfig.GuiColumns.TimePlayedColumn) { _timePlayedToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[6]) { _fileExtToggle.Active = true; } if (SwitchSettings.SwitchConfig.GuiColumns.LastPlayedColumn) { _lastPlayedToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[7]) { _fileSizeToggle.Active = true; } if (SwitchSettings.SwitchConfig.GuiColumns.FileExtColumn) { _fileExtToggle.Active = true; }
if (SwitchSettings.SwitchConfig.GuiColumns[8]) { _pathToggle.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 return;
_box.Remove(_glScreen); }
if (SwitchSettings.SwitchConfig.GuiColumns[0]) { _gameTable.AppendColumn("Icon", new CellRendererPixbuf(), "pixbuf", 0); } if (File.Exists(SwitchSettings.SwitchConfig.CustomThemePath) && (System.IO.Path.GetExtension(SwitchSettings.SwitchConfig.CustomThemePath) == ".css"))
if (SwitchSettings.SwitchConfig.GuiColumns[1]) { _gameTable.AppendColumn("Application", new CellRendererText(), "text", 1); } {
if (SwitchSettings.SwitchConfig.GuiColumns[2]) { _gameTable.AppendColumn("Developer", new CellRendererText(), "text", 2); } CssProvider cssProvider = new CssProvider();
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)); cssProvider.LoadFromPath(SwitchSettings.SwitchConfig.CustomThemePath);
_gameTable.Model = _tableStore;
UpdateGameTable(); StyleContext.AddProviderForScreen(Gdk.Screen.Default, cssProvider, 800);
// Temporary code section end
} }
else else
{ {
_box.Remove(_glScreen); Logger.PrintWarning(LogClass.Application, $"The \"custom_theme_path\" section in \"Config.json\" contains an invalid path: \"{SwitchSettings.SwitchConfig.CustomThemePath}\".");
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();
} }
} }
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", _gameTable.RemoveColumn(column);
Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ryujinxIcon.png"), }
WindowPosition = WindowPosition.Center
}; CellRendererToggle favToggle = new CellRendererToggle();
errorDialog.SetSizeRequest(100, 20); favToggle.Toggled += FavToggle_Toggled;
errorDialog.Run();
errorDialog.Destroy(); 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(); _tableStore.Clear();
ApplicationLibrary.Init(SwitchSettings.SwitchConfig.GameDirs, _device.System.KeySet, _device.System.State.DesiredTitleLanguage);
foreach (ApplicationLibrary.ApplicationData AppData in ApplicationLibrary.ApplicationLibraryData) await Task.Run(() => ApplicationLibrary.LoadApplications(SwitchSettings.SwitchConfig.GameDirs, _device.System.KeySet, _device.System.State.DesiredTitleLanguage));
{
_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);
}
}
public static void ApplyTheme() _updatingGameTable = false;
{
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);
} }
internal void LoadApplication(string path) internal void LoadApplication(string path)
{ {
if (_gameLoaded) 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 else
{ {
@ -266,19 +304,23 @@ namespace Ryujinx.UI
End(); End();
} }
new Thread(new ThreadStart(CreateGameWindow)).Start(); #if MACOS_BUILD
CreateGameWindow();
#else
new Thread(CreateGameWindow).Start();
#endif
_gameLoaded = true; _gameLoaded = true;
_stopEmulation.Sensitive = true; _stopEmulation.Sensitive = true;
if (DiscordIntegrationEnabled) 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) if (state == null)
{ {
@ -306,40 +348,37 @@ namespace Ryujinx.UI
DiscordClient.SetPresence(DiscordPresence); 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); Favorite = false,
using (FileStream stream = File.OpenWrite(System.IO.Path.Combine(savePath, "TimePlayed.dat"))) TimePlayed = 0,
{ LastPlayed = "Never"
stream.Write(Encoding.ASCII.GetBytes("0")); };
}
}
if (File.Exists(System.IO.Path.Combine(savePath, "LastPlayed.dat")) == false) byte[] data = JsonSerializer.Serialize(appMetadata, resolver);
{ File.WriteAllText(metadataFile, Encoding.UTF8.GetString(data, 0, data.Length).PrettyPrintJson());
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);
}
}
} }
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); Configuration.ConfigureHid(_device, SwitchSettings.SwitchConfig);
using (GlScreen screen = new GlScreen(_device, _renderer)) using (_screen = new GlScreen(_device, _renderer))
{ {
screen.MainLoop(); _screen.MainLoop();
End(); End();
} }
@ -357,41 +396,49 @@ namespace Ryujinx.UI
private static void End() private static void End()
{ {
if (_ending)
{
return;
}
_ending = true;
if (_gameLoaded) if (_gameLoaded)
{ {
try string metadataFolder = System.IO.Path.Combine(new VirtualFileSystem().GetBasePath(), "games", _device.System.TitleId, "gui");
{ string metadataFile = System.IO.Path.Combine(metadataFolder, "metadata.json");
string savePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFS", "nand", "user", "save", "0000000000000000", _userId, _device.System.TitleID);
double currentPlayTime = 0;
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)) Favorite = false,
{ TimePlayed = 0,
DateTime startTime = DateTime.Parse(reader.ReadLine()); LastPlayed = "Never"
};
using (FileStream lastPlayedStream = File.OpenRead(System.IO.Path.Combine(savePath, "TimePlayed.dat"))) byte[] data = JsonSerializer.Serialize(appMetadata, resolver);
{ File.WriteAllText(metadataFile, Encoding.UTF8.GetString(data, 0, data.Length).PrettyPrintJson());
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));
}
}
}
}
} }
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(); Profile.FinishProfiling();
@ -423,15 +470,69 @@ namespace Ryujinx.UI
} }
//Events //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())); _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); 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); 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); 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); 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); 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() Process.Start(new ProcessStartInfo()
{ {
FileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RyuFs"), FileName = new VirtualFileSystem().GetBasePath(),
UseShellExecute = true, UseShellExecute = true,
Verb = "open" Verb = "open"
}); });
} }
private void Exit_Pressed(object o, EventArgs args) private void Exit_Pressed(object sender, EventArgs args)
{ {
_screen?.Exit();
End(); End();
} }
private void Window_Close(object o, DeleteEventArgs args) private void Window_Close(object sender, DeleteEventArgs args)
{ {
_screen?.Exit();
End(); End();
} }
private void StopEmulation_Pressed(object o, EventArgs args) private void StopEmulation_Pressed(object sender, EventArgs args)
{ {
// TODO: Write logic to kill running game // 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) 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); SwitchSettings settingsWin = new SwitchSettings(_device);
settingsWin.Show();
_gtkApplication.Register(GLib.Cancellable.Current);
_gtkApplication.AddWindow(SettingsWin);
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 try
{ {
@ -520,81 +621,249 @@ namespace Ryujinx.UI
} }
catch(System.ComponentModel.Win32Exception) 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(); AboutWindow aboutWin = new AboutWindow();
aboutWin.Show();
_gtkApplication.Register(GLib.Cancellable.Current);
_gtkApplication.AddWindow(AboutWin);
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")); 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")); 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")); 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")); 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")); 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")); 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")); 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")); 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")); 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"> <object class="GtkMenuItem" id="GUIColumns">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</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="label" translatable="yes">Enable GUI Columns</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<child type="submenu"> <child type="submenu">
<object class="GtkMenu"> <object class="GtkMenu">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</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> <child>
<object class="GtkCheckMenuItem" id="_iconToggle"> <object class="GtkCheckMenuItem" id="_iconToggle">
<property name="visible">True</property> <property name="visible">True</property>
@ -144,7 +154,7 @@
</object> </object>
</child> </child>
<child> <child>
<object class="GtkCheckMenuItem" id="_titleToggle"> <object class="GtkCheckMenuItem" id="_appToggle">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</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> <property name="tooltip_text" translatable="yes">Enable or Disable Title Name/ID Column in the game list</property>
@ -303,22 +313,96 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkScrolledWindow" id="_gameTableWindow"> <object class="GtkBox" id="MainBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">False</property>
<property name="shadow_type">in</property> <property name="orientation">vertical</property>
<child> <child>
<object class="GtkTreeView" id="_gameTable"> <object class="GtkScrolledWindow" id="_gameTableWindow">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="headers_clickable">False</property> <property name="shadow_type">in</property>
<property name="reorderable">True</property> <child>
<property name="hover_selection">True</property> <object class="GtkTreeView" id="_gameTable">
<signal name="row-activated" handler="Row_Activated" swapped="no"/> <property name="visible">True</property>
<child internal-child="selection"> <property name="can_focus">True</property>
<object class="GtkTreeSelection"/> <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> </child>
</object> </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> </child>
</object> </object>
<packing> <packing>
@ -327,20 +411,6 @@
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </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> </object>
</child> </child>
</object> </object>

View file

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

View file

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

View file

@ -1,27 +1,29 @@
using Gtk; using Gtk;
using GUI = Gtk.Builder.ObjectAttribute;
using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Input; using Ryujinx.HLE.Input;
using Ryujinx.UI.Input; using Ryujinx.Ui.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
namespace Ryujinx.UI using GUI = Gtk.Builder.ObjectAttribute;
namespace Ryujinx.Ui
{ {
public class SwitchSettings : Window public class SwitchSettings : Window
{ {
internal static Configuration SwitchConfig { get; set; } internal static Configuration SwitchConfig { get; set; }
internal HLE.Switch Device { get; set; } private readonly HLE.Switch _device;
private static ListStore _gameDirsBoxStore; private static ListStore _gameDirsBoxStore;
private static bool _listeningForKeypress; private static bool _listeningForKeypress;
#pragma warning disable 649 #pragma warning disable CS0649
#pragma warning disable IDE0044
[GUI] Window _settingsWin; [GUI] Window _settingsWin;
[GUI] CheckButton _errorLogToggle; [GUI] CheckButton _errorLogToggle;
[GUI] CheckButton _warningLogToggle; [GUI] CheckButton _warningLogToggle;
@ -51,7 +53,7 @@ namespace Ryujinx.UI
[GUI] ToggleButton _removeDir; [GUI] ToggleButton _removeDir;
[GUI] Entry _logPath; [GUI] Entry _logPath;
[GUI] Entry _graphicsShadersDumpPath; [GUI] Entry _graphicsShadersDumpPath;
[GUI] Image _controllerImage; [GUI] Image _controller1Image;
[GUI] ComboBoxText _controller1Type; [GUI] ComboBoxText _controller1Type;
[GUI] ToggleButton _lStickUp1; [GUI] ToggleButton _lStickUp1;
@ -78,67 +80,70 @@ namespace Ryujinx.UI
[GUI] ToggleButton _plus1; [GUI] ToggleButton _plus1;
[GUI] ToggleButton _r1; [GUI] ToggleButton _r1;
[GUI] ToggleButton _zR1; [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) { } 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) private SwitchSettings(Builder builder, HLE.Switch device) : base(builder.GetObject("_settingsWin").Handle)
{ {
Device = device;
builder.Autoconnect(this); builder.Autoconnect(this);
_settingsWin.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ryujinxIcon.png"); _device = device;
_controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.JoyCon.png", 500, 500);
_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 //Bind Events
_lStickUp1.Clicked += (o, args) => Button_Pressed(o, args, _lStickUp1); _lStickUp1.Clicked += (sender, args) => Button_Pressed(sender, args, _lStickUp1);
_lStickDown1.Clicked += (o, args) => Button_Pressed(o, args, _lStickDown1); _lStickDown1.Clicked += (sender, args) => Button_Pressed(sender, args, _lStickDown1);
_lStickLeft1.Clicked += (o, args) => Button_Pressed(o, args, _lStickLeft1); _lStickLeft1.Clicked += (sender, args) => Button_Pressed(sender, args, _lStickLeft1);
_lStickRight1.Clicked += (o, args) => Button_Pressed(o, args, _lStickRight1); _lStickRight1.Clicked += (sender, args) => Button_Pressed(sender, args, _lStickRight1);
_lStickButton1.Clicked += (o, args) => Button_Pressed(o, args, _lStickButton1); _lStickButton1.Clicked += (sender, args) => Button_Pressed(sender, args, _lStickButton1);
_dpadUp1.Clicked += (o, args) => Button_Pressed(o, args, _dpadUp1); _dpadUp1.Clicked += (sender, args) => Button_Pressed(sender, args, _dpadUp1);
_dpadDown1.Clicked += (o, args) => Button_Pressed(o, args, _dpadDown1); _dpadDown1.Clicked += (sender, args) => Button_Pressed(sender, args, _dpadDown1);
_dpadLeft1.Clicked += (o, args) => Button_Pressed(o, args, _dpadLeft1); _dpadLeft1.Clicked += (sender, args) => Button_Pressed(sender, args, _dpadLeft1);
_dpadRight1.Clicked += (o, args) => Button_Pressed(o, args, _dpadRight1); _dpadRight1.Clicked += (sender, args) => Button_Pressed(sender, args, _dpadRight1);
_minus1.Clicked += (o, args) => Button_Pressed(o, args, _minus1); _minus1.Clicked += (sender, args) => Button_Pressed(sender, args, _minus1);
_l1.Clicked += (o, args) => Button_Pressed(o, args, _l1); _l1.Clicked += (sender, args) => Button_Pressed(sender, args, _l1);
_zL1.Clicked += (o, args) => Button_Pressed(o, args, _zL1); _zL1.Clicked += (sender, args) => Button_Pressed(sender, args, _zL1);
_rStickUp1.Clicked += (o, args) => Button_Pressed(o, args, _rStickUp1); _rStickUp1.Clicked += (sender, args) => Button_Pressed(sender, args, _rStickUp1);
_rStickDown1.Clicked += (o, args) => Button_Pressed(o, args, _rStickDown1); _rStickDown1.Clicked += (sender, args) => Button_Pressed(sender, args, _rStickDown1);
_rStickLeft1.Clicked += (o, args) => Button_Pressed(o, args, _rStickLeft1); _rStickLeft1.Clicked += (sender, args) => Button_Pressed(sender, args, _rStickLeft1);
_rStickRight1.Clicked += (o, args) => Button_Pressed(o, args, _rStickRight1); _rStickRight1.Clicked += (sender, args) => Button_Pressed(sender, args, _rStickRight1);
_rStickButton1.Clicked += (o, args) => Button_Pressed(o, args, _rStickButton1); _rStickButton1.Clicked += (sender, args) => Button_Pressed(sender, args, _rStickButton1);
_a1.Clicked += (o, args) => Button_Pressed(o, args, _a1); _a1.Clicked += (sender, args) => Button_Pressed(sender, args, _a1);
_b1.Clicked += (o, args) => Button_Pressed(o, args, _b1); _b1.Clicked += (sender, args) => Button_Pressed(sender, args, _b1);
_x1.Clicked += (o, args) => Button_Pressed(o, args, _x1); _x1.Clicked += (sender, args) => Button_Pressed(sender, args, _x1);
_y1.Clicked += (o, args) => Button_Pressed(o, args, _y1); _y1.Clicked += (sender, args) => Button_Pressed(sender, args, _y1);
_plus1.Clicked += (o, args) => Button_Pressed(o, args, _plus1); _plus1.Clicked += (sender, args) => Button_Pressed(sender, args, _plus1);
_r1.Clicked += (o, args) => Button_Pressed(o, args, _r1); _r1.Clicked += (sender, args) => Button_Pressed(sender, args, _r1);
_zR1.Clicked += (o, args) => Button_Pressed(o, args, _zR1); _zR1.Clicked += (sender, args) => Button_Pressed(sender, args, _zR1);
_controller1Type.Changed += (sender, args) => Controller_Changed(sender, args, _controller1Type.ActiveId, _controller1Image);
//Setup Currents //Setup Currents
if (SwitchConfig.EnableFileLog) { _fileLogToggle.Click(); } if (SwitchConfig.EnableFileLog) _fileLogToggle.Click();
if (SwitchConfig.LoggingEnableError) { _errorLogToggle.Click(); } if (SwitchConfig.LoggingEnableError) _errorLogToggle.Click();
if (SwitchConfig.LoggingEnableWarn) { _warningLogToggle.Click(); } if (SwitchConfig.LoggingEnableWarn) _warningLogToggle.Click();
if (SwitchConfig.LoggingEnableInfo) { _infoLogToggle.Click(); } if (SwitchConfig.LoggingEnableInfo) _infoLogToggle.Click();
if (SwitchConfig.LoggingEnableStub) { _stubLogToggle.Click(); } if (SwitchConfig.LoggingEnableStub) _stubLogToggle.Click();
if (SwitchConfig.LoggingEnableDebug) { _debugLogToggle.Click(); } if (SwitchConfig.LoggingEnableDebug) _debugLogToggle.Click();
if (SwitchConfig.LoggingEnableGuest) { _guestLogToggle.Click(); } if (SwitchConfig.LoggingEnableGuest) _guestLogToggle.Click();
if (SwitchConfig.LoggingEnableFsAccessLog) { _fsAccessLogToggle.Click(); } if (SwitchConfig.LoggingEnableFsAccessLog) _fsAccessLogToggle.Click();
if (SwitchConfig.DockedMode) { _dockedModeToggle.Click(); } if (SwitchConfig.DockedMode) _dockedModeToggle.Click();
if (SwitchConfig.EnableDiscordIntegration) { _discordToggle.Click(); } if (SwitchConfig.EnableDiscordIntegration) _discordToggle.Click();
if (SwitchConfig.EnableVsync) { _vSyncToggle.Click(); } if (SwitchConfig.EnableVsync) _vSyncToggle.Click();
if (SwitchConfig.EnableMulticoreScheduling) { _multiSchedToggle.Click(); } if (SwitchConfig.EnableMulticoreScheduling) _multiSchedToggle.Click();
if (SwitchConfig.EnableFsIntegrityChecks) { _fsicToggle.Click(); } if (SwitchConfig.EnableFsIntegrityChecks) _fsicToggle.Click();
if (SwitchConfig.IgnoreMissingServices) { _ignoreToggle.Click(); } if (SwitchConfig.IgnoreMissingServices) _ignoreToggle.Click();
if (SwitchConfig.EnableKeyboard) { _directKeyboardAccess.Click(); } if (SwitchConfig.EnableKeyboard) _directKeyboardAccess.Click();
if (SwitchConfig.EnableCustomTheme) { _custThemeToggle.Click(); } if (SwitchConfig.EnableCustomTheme) _custThemeToggle.Click();
_systemLanguageSelect.SetActiveId(SwitchConfig.SystemLanguage.ToString()); _systemLanguageSelect.SetActiveId(SwitchConfig.SystemLanguage.ToString());
_controller1Type .SetActiveId(SwitchConfig.ControllerType.ToString()); _controller1Type .SetActiveId(SwitchConfig.ControllerType.ToString());
Controller_Changed(null, null, _controller1Type.ActiveId, _controller1Image);
_lStickUp1.Label = SwitchConfig.KeyboardControls.LeftJoycon.StickUp.ToString(); _lStickUp1.Label = SwitchConfig.KeyboardControls.LeftJoycon.StickUp.ToString();
_lStickDown1.Label = SwitchConfig.KeyboardControls.LeftJoycon.StickDown.ToString(); _lStickDown1.Label = SwitchConfig.KeyboardControls.LeftJoycon.StickDown.ToString();
@ -190,7 +195,7 @@ namespace Ryujinx.UI
} }
//Events //Events
private void Button_Pressed(object obj, EventArgs args, ToggleButton Button) private void Button_Pressed(object sender, EventArgs args, ToggleButton button)
{ {
if (_listeningForKeypress == false) if (_listeningForKeypress == false)
{ {
@ -198,25 +203,25 @@ namespace Ryujinx.UI
_listeningForKeypress = true; _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); string capKey = key.First().ToString().ToUpper() + key.Substring(1);
if (Enum.IsDefined(typeof(OpenTK.Input.Key), capKey)) 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 else
{ {
Button.Label = "Space"; button.Label = "Space";
} }
Button.SetStateFlags(0, true); button.SetStateFlags(0, true);
KeyPressEvent -= On_KeyPress; KeyPressEvent -= On_KeyPress;
@ -225,11 +230,30 @@ namespace Ryujinx.UI
} }
else 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)) if (Directory.Exists(_addGameDirBox.Buffer.Text))
{ {
@ -239,7 +263,7 @@ namespace Ryujinx.UI
_addDir.SetStateFlags(0, true); _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); 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); _gameDirsBoxStore.AppendValues(fileChooser.Filename);
} }
fileChooser.Destroy(); fileChooser.Dispose();
_browseDir.SetStateFlags(0, true); _browseDir.SetStateFlags(0, true);
} }
private void RemoveDir_Pressed(object obj, EventArgs args) private void RemoveDir_Pressed(object sender, EventArgs args)
{ {
TreeSelection selection = _gameDirsBox.Selection; TreeSelection selection = _gameDirsBox.Selection;
@ -263,14 +287,14 @@ namespace Ryujinx.UI
_removeDir.SetStateFlags(0, true); _removeDir.SetStateFlags(0, true);
} }
private void CustThemeToggle_Activated(object obj, EventArgs args) private void CustThemeToggle_Activated(object sender, EventArgs args)
{ {
_custThemePath.Sensitive = _custThemeToggle.Active; _custThemePath.Sensitive = _custThemeToggle.Active;
_custThemePathLabel.Sensitive = _custThemeToggle.Active; _custThemePathLabel.Sensitive = _custThemeToggle.Active;
_browseThemePath.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); 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; _custThemePath.Buffer.Text = fileChooser.Filename;
} }
fileChooser.Destroy(); fileChooser.Dispose();
_browseThemePath.SetStateFlags(0, true); _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>(); List<string> gameDirs = new List<string>();
@ -358,20 +382,21 @@ namespace Ryujinx.UI
SwitchConfig.FsGlobalAccessLogMode = (int)_fsLogSpinAdjustment.Value; SwitchConfig.FsGlobalAccessLogMode = (int)_fsLogSpinAdjustment.Value;
Configuration.SaveConfig(SwitchConfig, System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json")); Configuration.SaveConfig(SwitchConfig, System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"));
Configuration.Configure(Device, SwitchConfig); Configuration.Configure(_device, SwitchConfig);
MainWindow.ApplyTheme(); MainWindow.ApplyTheme();
#pragma warning disable CS4014
MainWindow.UpdateGameTable(); MainWindow.UpdateGameTable();
#pragma warning restore CS4014
Destroy(); 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_0", "Number0" },
{ "Key_1", "Number1" }, { "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": { "game_dirs": {
"$id": "#/properties/game_dirs", "$id": "#/properties/game_dirs",
"type": "string list", "type": "array",
"title": "List of Game Directories", "title": "List of Game Directories",
"description": "A list of directories containing games to be used to load games into the games list", "description": "A list of directories containing games to be used to load games into the games list",
"default": [] "default": []
}, },
"gui_columns": { "gui_columns": {
"$id": "#/properties/gui_columns", "$id": "#/properties/gui_columns",
"type": "bool list", "type": "array",
"title": "Used to toggle columns in the GUI", "title": "Used to toggle columns in the GUI",
"description": "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": { "enable_custom_theme": {
"$id": "#/properties/enable_custom_theme", "$id": "#/properties/enable_custom_theme",