feat: add ability to change app icon

Add ability to change application icon to any image file inside the src\Ryujinx\Assets\Icons\AppIcons directory. The app should automatically load any resource in that folder as a selectable icon.
merge-requests/128/head
VewDev 2025-08-29 13:56:28 +02:00
parent 462c93e1ff
commit af0f8e2720
16 changed files with 144 additions and 1 deletions

View File

@ -12217,6 +12217,31 @@
"zh_TW": "淺色"
}
},
{
"ID": "SettingsTabGeneralIcon",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Application icon:",
"es_ES": "Icono de aplicación:",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "ControllerSettingsConfigureGeneral",
"Translations": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,12 @@
namespace Ryujinx.Ava.Common.Models
{
public class ApplicationIcon
{
public string Name { get; set; }
public string Filename { get; set; }
public string FullPath
{
get => $"Ryujinx/Assets/Icons/AppIcons/{Filename}";
}
}
}

View File

@ -164,6 +164,7 @@
<EmbeddedResource Include="Assets\Icons\Controller_JoyConPair.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConRight.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_ProCon.svg" />
<EmbeddedResource Include="Assets\Icons\AppIcons\*" />
<EmbeddedResource Include="Assets\UIImages\Icon_NCA.png" />
<EmbeddedResource Include="Assets\UIImages\Icon_NRO.png" />
<EmbeddedResource Include="Assets\UIImages\Icon_NSO.png" />

View File

@ -354,6 +354,11 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary>
public string BaseStyle { get; set; }
/// <summary>
/// The name of the currently selected window icon
/// </summary>
public string SelectedWindowIcon { get; set; }
/// <summary>
/// Chooses the view mode of the game list // Not Used
/// </summary>

View File

@ -129,6 +129,7 @@ namespace Ryujinx.Ava.Systems.Configuration
UI.ShownFileTypes.NSO.Value = shouldLoadFromFile ? cff.ShownFileTypes.NSO : UI.ShownFileTypes.NSO.Value;
UI.LanguageCode.Value = shouldLoadFromFile ? cff.LanguageCode : UI.LanguageCode.Value;
UI.BaseStyle.Value = shouldLoadFromFile ? cff.BaseStyle : UI.BaseStyle.Value;
UI.SelectedWindowIcon.Value = shouldLoadFromFile ? cff.SelectedWindowIcon : UI.SelectedWindowIcon.Value;
UI.GameListViewMode.Value = shouldLoadFromFile ? cff.GameListViewMode : UI.GameListViewMode.Value;
UI.ShowNames.Value = shouldLoadFromFile ? cff.ShowNames : UI.ShowNames.Value;
UI.IsAscendingOrder.Value = shouldLoadFromFile ? cff.IsAscendingOrder : UI.IsAscendingOrder.Value;

View File

@ -150,6 +150,11 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary>
public ReactiveObject<string> BaseStyle { get; private set; }
/// <summary>
/// The currently selected window icon.
/// </summary>
public ReactiveObject<string> SelectedWindowIcon { get; private set; }
/// <summary>
/// Start games in fullscreen mode
/// </summary>
@ -199,6 +204,7 @@ namespace Ryujinx.Ava.Systems.Configuration
ShownFileTypes = new ShownFileTypeSettings();
WindowStartup = new WindowStartupSettings();
BaseStyle = new ReactiveObject<string>();
SelectedWindowIcon = new ReactiveObject<string>();
StartFullscreen = new ReactiveObject<bool>();
StartNoUI = new ReactiveObject<bool>();
GameListViewMode = new ReactiveObject<int>();

View File

@ -126,6 +126,7 @@ namespace Ryujinx.Ava.Systems.Configuration
},
LanguageCode = UI.LanguageCode,
BaseStyle = UI.BaseStyle,
SelectedWindowIcon = UI.SelectedWindowIcon,
GameListViewMode = UI.GameListViewMode,
ShowNames = UI.ShowNames,
GridSize = UI.GridSize,

View File

@ -2,12 +2,14 @@ using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input.Platform;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading;
using FluentAvalonia.UI.Windowing;
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Views.Dialog;
@ -16,7 +18,10 @@ using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
namespace Ryujinx.Ava
{
@ -52,6 +57,8 @@ namespace Ryujinx.Ava
{
Name = FormatTitle();
RetrieveAvailableAppIcons();
AvaloniaXamlLoader.Load(this);
if (OperatingSystem.IsMacOS())
@ -72,8 +79,10 @@ namespace Ryujinx.Ava
if (Program.PreviewerDetached)
{
ApplyConfiguredTheme(ConfigurationState.Instance.UI.BaseStyle);
ConfigurationState.Instance.UI.BaseStyle.Event += ThemeChanged_Event;
UpdateAppIcon(ConfigurationState.Instance.UI.SelectedWindowIcon);
ConfigurationState.Instance.UI.SelectedWindowIcon.Event += WindowIconChanged_Event;
}
}
@ -151,5 +160,39 @@ namespace Ryujinx.Ava
{
await AboutView.Show();
}
public static List<ApplicationIcon> AvailableApplicationIcons { get; set; } = [];
private static void RetrieveAvailableAppIcons()
{
AvailableApplicationIcons.Clear();
string resourceAssemblyPrefix = "Ryujinx.Assets.Icons.AppIcons.";
IEnumerable<string> availableAppIconResources = EmbeddedResources
.GetAllAvailableResources("Ryujinx/Assets")
.Where(x => x.StartsWith(resourceAssemblyPrefix));
foreach (string resource in availableAppIconResources)
{
string filename = resource.Remove(0, resourceAssemblyPrefix.Length);
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filename);
AvailableApplicationIcons.Add(new ApplicationIcon()
{
Name = fileNameWithoutExtension,
Filename = filename
});
}
}
public static void UpdateAppIcon(string newIconName)
{
ApplicationIcon selectedIcon = AvailableApplicationIcons.First(x => x.Name == newIconName);
Stream activeIconStream = EmbeddedResources.GetStream(selectedIcon.FullPath);
if (activeIconStream != null)
{
MainWindow.Icon = new Bitmap(activeIconStream);
}
}
private void WindowIconChanged_Event(object _, ReactiveEventArgs<string> rArgs) => UpdateAppIcon(rArgs.NewValue);
}
}

View File

@ -9,12 +9,14 @@ using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL2;
using Ryujinx.Audio.Backends.SoundIo;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.Systems.Configuration.System;
using Ryujinx.Ava.Systems.Configuration.UI;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.GraphicsDriver;
@ -496,6 +498,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Task.Run(CheckSoundBackends);
Task.Run(PopulateNetworkInterfaces);
ApplicationIcons = new(RyujinxApp.AvailableApplicationIcons);
if (Program.PreviewerDetached)
{
@ -621,6 +624,7 @@ namespace Ryujinx.Ava.UI.ViewModels
HideCursor = (int)config.HideCursor.Value;
UpdateCheckerType = (int)config.UpdateCheckerType.Value;
FocusLostActionType = (int)config.FocusLostActionType.Value;
AppIconSelectedIndex = _appIcons.ToList().FindIndex(x => x.Name == config.UI.SelectedWindowIcon.Value);
GameDirectories.Clear();
GameDirectories.AddRange(config.UI.GameDirs.Value);
@ -740,6 +744,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.FocusLostActionType.Value = (FocusLostType)FocusLostActionType;
config.UI.GameDirs.Value = [.. GameDirectories];
config.UI.AutoloadDirs.Value = [.. AutoloadDirectories];
config.UI.SelectedWindowIcon.Value = _appIcons[_appIconSelectedIndex].Name;
config.UI.BaseStyle.Value = BaseStyleIndex switch
{
@ -935,5 +940,27 @@ namespace Ryujinx.Ava.UI.ViewModels
RevertIfNotSaved();
CloseWindow?.Invoke();
}
private AvaloniaList<ApplicationIcon> _appIcons = [];
public AvaloniaList<ApplicationIcon> ApplicationIcons
{
get => _appIcons;
set
{
_appIcons = value;
OnPropertyChanged();
}
}
private int _appIconSelectedIndex;
public int AppIconSelectedIndex
{
get => _appIconSelectedIndex;
set
{
_appIconSelectedIndex = value;
OnPropertyChanged();
}
}
}
}

View File

@ -156,6 +156,28 @@
</ComboBoxItem>
</ComboBox>
<TextBlock Classes="globalConfigMarker" IsVisible="{Binding IsGameTitleNotNull}" />
</StackPanel>
<StackPanel
IsEnabled="{Binding !IsGameTitleNotNull}"
Opacity="{Binding PanelOpacity}"
Margin="0, 15, 0, 10"
Orientation="Horizontal">
<TextBlock
VerticalAlignment="Center"
Text="{ext:Locale SettingsTabGeneralIcon}"
Width="150" />
<ComboBox
MinWidth="100"
HorizontalAlignment="Left"
ItemsSource="{Binding ApplicationIcons}"
SelectedIndex="{Binding AppIconSelectedIndex}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Classes="globalConfigMarker" IsVisible="{Binding IsGameTitleNotNull}" />
</StackPanel>
</StackPanel>
</StackPanel>