UI: File Menu → General Improvements (#127)

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
This commit is contained in:
_Neo_
2026-06-14 19:47:20 +00:00
committed by sh0inx
parent 8fe1a9c672
commit 223f20868a
15 changed files with 893 additions and 747 deletions

View File

@@ -136,9 +136,6 @@ namespace Ryujinx.Ava.UI.ViewModels
[ObservableProperty] public partial float VolumeBeforeMute { get; set; }
[ObservableProperty]
public partial bool AreMimeTypesRegistered { get; set; } = FileAssociationHelper.AreMimeTypesRegistered;
[ObservableProperty] public partial Cursor Cursor { get; set; }
[ObservableProperty] public partial string Title { get; set; }
@@ -219,6 +216,11 @@ namespace Ryujinx.Ava.UI.ViewModels
_rendererWaitEvent = new AutoResetEvent(false);
LocaleManager.Instance.PropertyChanged += (sender, args) =>
{
RefreshFileTypeAssociationToggle();
};
if (Program.PreviewerDetached)
{
LoadConfigurableHotKeys();
@@ -679,11 +681,6 @@ namespace Ryujinx.Ava.UI.ViewModels
get => ConsoleHelper.SetConsoleWindowStateSupported;
}
public bool ManageFileTypesVisible
{
get => FileAssociationHelper.IsTypeAssociationSupported;
}
public Glyph Glyph
{
get => (Glyph)ConfigurationState.Instance.UI.GameListViewMode.Value;
@@ -941,6 +938,71 @@ namespace Ryujinx.Ava.UI.ViewModels
return false;
}
public bool FileTypeAssociationsVisible
{
get => FileAssociationHelper.IsTypeAssociationSupported;
}
private void RefreshFileTypeAssociationToggle()
{
OnPropertyChanged(nameof(FileTypeAssociationsMenuHeader));
OnPropertyChanged(nameof(FileTypeAssociationsIcon));
}
private bool _areMimeTypesRegistered = FileAssociationHelper.AreMimeTypesRegistered;
public bool AreMimeTypesRegistered
{
get => _areMimeTypesRegistered;
set
{
if (_areMimeTypesRegistered != value)
{
_areMimeTypesRegistered = value;
RefreshFileTypeAssociationToggle();
}
}
}
public string FileTypeAssociationsMenuHeader =>
AreMimeTypesRegistered
? LocaleManager.Instance[LocaleKeys.MenuBar_File_RemoveFileTypeAssociationsButton]
: LocaleManager.Instance[LocaleKeys.MenuBar_File_AssociateFileTypesButton];
public string FileTypeAssociationsIcon =>
AreMimeTypesRegistered
? "fa-solid fa-link-slash"
: "fa-solid fa-link";
[RelayCommand]
private async Task ToggleFileTypeAssociations()
{
if (AreMimeTypesRegistered)
await RemoveFileTypeAssociations();
else
await AssociateFileTypes();
}
[RelayCommand]
private async Task AssociateFileTypes()
{
AreMimeTypesRegistered = FileAssociationHelper.Install();
if (AreMimeTypesRegistered)
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.Dialog_FileTypeAssociations_AssociationSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
else
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.Dialog_FileTypeAssociations_AssociationFailedMessage]);
}
[RelayCommand]
private async Task RemoveFileTypeAssociations()
{
AreMimeTypesRegistered = !FileAssociationHelper.Uninstall();
if (!AreMimeTypesRegistered)
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.Dialog_FileTypeAssociations_RemoveAssociationSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
else
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.Dialog_FileTypeAssociations_RemoveAssociationFailedMessage]);
}
public async Task HandleFirmwareInstallation(string filename)
{
try
@@ -1597,11 +1659,14 @@ namespace Ryujinx.Ava.UI.ViewModels
AppHost.Device.System.SimulateWakeUpMessage();
}
public async Task OpenFile()
public KeyGesture LoadApplicationFromFileGesture => KeyGesture.Parse(OperatingSystem.IsMacOS() ? "Cmd+O" : "Ctrl+O");
public KeyGesture LoadUnpackedGameFromFolderFileGesture => KeyGesture.Parse(OperatingSystem.IsMacOS() ? "Cmd+Shift+O" : "Ctrl+Shift+O");
public async Task LoadApplicationFromFile()
{
Optional<IStorageFile> result = await StorageProvider.OpenSingleFilePickerAsync(new FilePickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.LoadApplicationFromFileDialogTitle],
Title = LocaleManager.Instance[LocaleKeys.Dialog_FileMenu_LoadApplicationFromFileFilePickerTitle],
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
@@ -1667,35 +1732,17 @@ namespace Ryujinx.Ava.UI.ViewModels
else
{
await ContentDialogHelper.CreateErrorDialog(
LocaleManager.Instance[LocaleKeys.MenuBarFileOpenFromFileError]);
LocaleManager.Instance[LocaleKeys.Error_NoApplicationFoundInFile]);
}
}
}
public async Task LoadDlcFromFolder()
{
await LoadContentFromFolder(
LocaleKeys.AutoloadDlcAddedMessage,
LocaleKeys.AutoloadDlcRemovedMessage,
ApplicationLibrary.AutoLoadDownloadableContents,
LocaleKeys.LoadDLCFromFolderDialogTitle);
}
public async Task LoadTitleUpdatesFromFolder()
{
await LoadContentFromFolder(
LocaleKeys.AutoloadUpdateAddedMessage,
LocaleKeys.AutoloadUpdateRemovedMessage,
ApplicationLibrary.AutoLoadTitleUpdates,
LocaleKeys.LoadTitleUpdatesFromFolderDialogTitle);
}
public async Task OpenFolder()
public async Task LoadUnpackedGameFromFolder()
{
Optional<IStorageFolder> result = await StorageProvider.OpenSingleFolderPickerAsync(
new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.LoadUnpackedGameFromFolderDialogTitle]
Title = LocaleManager.Instance[LocaleKeys.Dialog_FileMenu_LoadUnpackedApplicationFromFolderFilePickerTitle]
});
if (result.TryGet(out IStorageFolder value))
@@ -1709,6 +1756,36 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
private async Task<IReadOnlyList<string>?> PickFolders(LocaleKeys titleKey)
{
return (await StorageProvider.OpenMultiFolderPickerAsync(new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[titleKey]
})).TryGet(out IReadOnlyList<IStorageFolder> folders)
? folders.Select(f => f.Path.LocalPath).ToList()
: null;
}
public async Task LoadTitleUpdatesAndDLCFromFolder()
{
if (await PickFolders(LocaleKeys.Dialog_FileMenu_LoadUpdatesAndDLCFromFolderFilePickerTitle) is not { } dirs)
return;
int updAdded = ApplicationLibrary.AutoLoadTitleUpdates(dirs.ToList(), out _);
int dlcAdded = ApplicationLibrary.AutoLoadDownloadableContents(dirs.ToList(), out _);
await Dispatcher.UIThread.InvokeAsync(async () =>
{
await ContentDialogHelper.ShowTextDialog(
LocaleManager.Instance[LocaleKeys.RyujinxConfirm],
string.Format(LocaleManager.Instance[LocaleKeys.Dialog_ContentLoading_UpdatesAddedMessage], updAdded) + "\n\n" +
string.Format(LocaleManager.Instance[LocaleKeys.Dialog_ContentLoading_DLCAddedMessage], dlcAdded),
string.Empty, string.Empty, string.Empty,
LocaleManager.Instance[LocaleKeys.InputDialogOk],
(int)Symbol.Checkmark);
});
}
public static bool InitializeUserConfig(ApplicationData application)
{
// Code where conditions will be met before loading the user configuration (Global Config)