mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-06-26 14:19:04 +00:00
Ayyyy, welcome to the UI: File Menu → General Improvements PR!
Wooo, we progressing smoothly!
This PR introduces small visual and "feature" improvements to the `File` menu.
### LOCALISATION:
* **Fractured:** More locales:
* `Dialog_ContentLoading.json` - content loading dialogs (Updates/DLC)
* `Dialog_FileTypeAssociations` - file association dialogs
* `Dialog_FileMenu` - File menu dialog strings (complements `MenuBar_File.json`)
* **Added:** Additional entires to `Error.json`
* **Populated:** `MenuBar_File.json`
### FILE MENU:
* **Added:** Keyboard shortcuts to `Load Application` and `Load Unpacked Game`
* Cmd + O/Ctrl + O and Cmd + Shift + O/Ctrl + Shift + O for macOS and other OS, respectively.
* While many users rely on autoloaded game directories, manually opening content remains a common workflow (for those that don't rely on autoload directories).
* **Merged:** `Load Title Updates` and `Load DLC` → `Load Updates/DLC`
* Both actions follow the same workflow: selecting one or more directories and allowing Ryujinx to load the content.
* To reduce redundancy, they have been consolidated into a single menu item that loads both Updates and DLC simultaneously, mirroring the behavior of the existing autoload functionality.
* As part of this change, Title Updates has been simplified to Updates for consistency with the rest of the UI. The remaining reference in the Game List context menu has also been updated from `Manage Title Updates` to `Manage Updates`.
* **Added/Updated:** File picker titles for content loading actions
* `Load Application`: Select a Switch application file to load
* `Load Unpacked Game`: Select a folder containing an unpacked Switch application to load
* `Load Updates/DLC`: Select one or more folders to bulk load updates and DLC from
* **Improved:** `Associate File Types` and `Remove File Type Associations` (initially moved to the `File` menu in #42)
* These options were previously nested under `Manage File Types` and exposed as `Install File Types` and `Uninstall File Types`. The submenu added unnecessary navigation, while the action names did not clearly communicate their purpose.
* The two actions have been replaced with a single dynamic menu item, whose displayed and performed action updates based on the current association state. The respective icons have been added as well (imported namespace Projektanker.Icons to allow for dynamic switching):
* Link → `Associate File Types`
* Link-Slash →`Remove File Type Associations`
* A tooltip has also been added to clarify the action being performed.
* This option is only usable when a game is not running (why associate file types when running a game? Play the game!)
* **Note:** These options are only available on Windows and Linux. macOS already provides robust per‑file “Open With” handling, so a custom association system isn’t necessary. Support can be added later if needed, but current macOS limitations in Ryujinx prevent certain behaviors; these will be addressed in future PRs.
* **Updated:** Menu Icons
* Several icons have been updated to better reflect their associated actions and improve consistency throughout the menu:
* `Load Updates/DLC...` now uses a single Inbox Tray icon instead of separate Update and DLC icons.
* `Open Ryujinx Folder`, `Open Logs Folder`, and `Open Screenshots Folder` now share the same folder icon, as all three actions ultimately open a folder/directory.
* `Exit` is now an Exit icon (arrow-right-from-bracket) instead of a Power button.
### OTHER:
* **Improved:** `Load Updates/DLC` dialog messages
* Existing dialog messages were longer than necessary and included terms such as "missing" updates and "new" DLC.
* These messages have been simplified and standardized:
* Updates Added: {0} or Updates Removed: {0}
* DLC Added: {0} or DLC Removed: {0}
_If there are any features or changes that you wish to be implemented, please comment down below and I'll be happy to accommodate!_
A GIGANTIC, ENORMOUSE HUUUUUUGEE thank you to @Babib3l for testing this and ensuring the commands work! WOOOOO!!!
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/127
196 lines
6.9 KiB
C#
196 lines
6.9 KiB
C#
using Microsoft.Win32;
|
|
using Ryujinx.Common.Logging;
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Runtime.Versioning;
|
|
|
|
namespace Ryujinx.Common.Helper
|
|
{
|
|
public static partial class FileAssociationHelper
|
|
{
|
|
private static readonly string[] _fileExtensions = [".nca", ".nro", ".nso", ".nsp", ".xci"];
|
|
|
|
[SupportedOSPlatform("linux")]
|
|
private static readonly string _mimeDbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "mime");
|
|
|
|
private const int SHCNE_ASSOCCHANGED = 0x8000000;
|
|
private const int SHCNF_FLUSH = 0x1000;
|
|
|
|
[LibraryImport("shell32.dll", SetLastError = true)]
|
|
public static partial void SHChangeNotify(uint wEventId, uint uFlags, nint dwItem1, nint dwItem2);
|
|
|
|
public static bool IsTypeAssociationSupported => (OperatingSystem.IsLinux() || OperatingSystem.IsWindows());
|
|
|
|
// NOTE: On macOS, users have a more robust file association system (via Right-Click > Get Info > "Open with:" > Ryujinx > "Change All...)
|
|
// Custom file association isn't strictly necessary and will not provide any additional benefit to macOS users.
|
|
|
|
public static bool AreMimeTypesRegistered
|
|
{
|
|
get
|
|
{
|
|
if (OperatingSystem.IsLinux())
|
|
{
|
|
return AreMimeTypesRegisteredLinux();
|
|
}
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
return AreMimeTypesRegisteredWindows();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
[SupportedOSPlatform("linux")]
|
|
private static bool AreMimeTypesRegisteredLinux() => File.Exists(Path.Combine(_mimeDbPath, "packages", "Ryujinx.xml"));
|
|
|
|
[SupportedOSPlatform("linux")]
|
|
private static bool InstallLinuxMimeTypes(bool uninstall = false)
|
|
{
|
|
string installKeyword = uninstall ? "uninstall" : "install";
|
|
|
|
if ((uninstall && AreMimeTypesRegisteredLinux()) || (!uninstall && !AreMimeTypesRegisteredLinux()))
|
|
{
|
|
string mimeTypesFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "mime", "Ryujinx.xml");
|
|
string additionalArgs = !uninstall ? "--novendor" : string.Empty;
|
|
|
|
using Process mimeProcess = new();
|
|
|
|
mimeProcess.StartInfo.FileName = "xdg-mime";
|
|
mimeProcess.StartInfo.Arguments = $"{installKeyword} {additionalArgs} --mode user {mimeTypesFile}";
|
|
|
|
mimeProcess.Start();
|
|
mimeProcess.WaitForExit();
|
|
|
|
if (mimeProcess.ExitCode != 0)
|
|
{
|
|
Logger.Error?.PrintMsg(LogClass.Application, $"Unable to {installKeyword} mime types. Make sure xdg-utils is installed. Process exited with code: {mimeProcess.ExitCode}");
|
|
|
|
return false;
|
|
}
|
|
|
|
using Process updateMimeProcess = new();
|
|
|
|
updateMimeProcess.StartInfo.FileName = "update-mime-database";
|
|
updateMimeProcess.StartInfo.Arguments = _mimeDbPath;
|
|
|
|
updateMimeProcess.Start();
|
|
updateMimeProcess.WaitForExit();
|
|
|
|
if (updateMimeProcess.ExitCode != 0)
|
|
{
|
|
Logger.Error?.PrintMsg(LogClass.Application, $"Could not update local mime database. Process exited with code: {updateMimeProcess.ExitCode}");
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
[SupportedOSPlatform("windows")]
|
|
private static bool AreMimeTypesRegisteredWindows()
|
|
{
|
|
return _fileExtensions.Aggregate(false,
|
|
(current, ext) => current | CheckRegistering(ext)
|
|
);
|
|
|
|
static bool CheckRegistering(string ext)
|
|
{
|
|
RegistryKey key = Registry.CurrentUser.OpenSubKey(@$"Software\Classes\{ext}");
|
|
|
|
RegistryKey openCmd = key?.OpenSubKey(@"shell\open\command");
|
|
|
|
if (openCmd is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string keyValue = (string)openCmd.GetValue(string.Empty);
|
|
|
|
return keyValue is not null && (keyValue.Contains("Ryujinx") || keyValue.Contains(AppDomain.CurrentDomain.FriendlyName));
|
|
}
|
|
}
|
|
|
|
[SupportedOSPlatform("windows")]
|
|
private static bool InstallWindowsMimeTypes(bool uninstall = false)
|
|
{
|
|
bool registered = _fileExtensions.Aggregate(false,
|
|
(current, ext) => current | RegisterExtension(ext, uninstall)
|
|
);
|
|
|
|
// Notify Explorer the file association has been changed.
|
|
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, nint.Zero, nint.Zero);
|
|
|
|
return registered;
|
|
|
|
static bool RegisterExtension(string ext, bool uninstall = false)
|
|
{
|
|
string keyString = @$"Software\Classes\{ext}";
|
|
|
|
if (uninstall)
|
|
{
|
|
// If the types don't already exist, there's nothing to do, and we can call this operation successful.
|
|
if (!AreMimeTypesRegisteredWindows())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
Logger.Debug?.Print(LogClass.Application, $"Removing type association {ext}");
|
|
Registry.CurrentUser.DeleteSubKeyTree(keyString);
|
|
Logger.Debug?.Print(LogClass.Application, $"Removed type association {ext}");
|
|
}
|
|
else
|
|
{
|
|
using RegistryKey key = Registry.CurrentUser.CreateSubKey(keyString);
|
|
|
|
if (key is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Logger.Debug?.Print(LogClass.Application, $"Adding type association {ext}");
|
|
using RegistryKey openCmd = key.CreateSubKey(@"shell\open\command");
|
|
openCmd.SetValue(string.Empty, $"\"{Environment.ProcessPath}\" \"%1\"");
|
|
Logger.Debug?.Print(LogClass.Application, $"Added type association {ext}");
|
|
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public static bool Install()
|
|
{
|
|
if (OperatingSystem.IsLinux())
|
|
{
|
|
return InstallLinuxMimeTypes();
|
|
}
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
return InstallWindowsMimeTypes();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static bool Uninstall()
|
|
{
|
|
if (OperatingSystem.IsLinux())
|
|
{
|
|
return InstallLinuxMimeTypes(true);
|
|
}
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
return InstallWindowsMimeTypes(true);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|