aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorNitroTears <73270647+NitroTears@users.noreply.github.com>2023-10-21 05:51:15 +1100
committerGitHub <noreply@github.com>2023-10-20 20:51:15 +0200
commita42f0bbb87b890d4f16b1148f9398210a5bfedfa (patch)
tree7c30a2d1fc44283846b67d98928e5f628222e3c6 /src
parentb4bb22ba06f89168c948e6001c51972575ca968b (diff)
Add "Create Shortcut" To app context menu (#4734)1.1.1057
* Added basic implementation for shortcut creation Currently bitmaps (.bmp) are used as the source file, colours are good (unlike .ico rn) but are scaled poorly on desktop. * Icons display properly in shortcut * code cleanup * Moved shortcut logic to specific file, added Ava UI for shortcuts * Added linux .desktop shortcut creation * fixes to .shortcut data * code issue fixes * Added basic implementation for shortcut creation Currently bitmaps (.bmp) are used as the source file, colours are good (unlike .ico rn) but are scaled poorly on desktop. * Icons display properly in shortcut * code cleanup * Moved shortcut logic to specific file, added Ava UI for shortcuts * Added linux .desktop shortcut creation * fixes to .shortcut data * code issue fixes * added back shortcut to new contextmenu file * Replaced COM reference with ComImport for shortcut functionality * remove specific platform values and regions * Move ShortcutHelper to Ryujinx.Ui.Common.Helpers * Adjust styling and structure * code feedback changes * Added MacOS support using .app folder * Added basic implementation for shortcut creation Currently bitmaps (.bmp) are used as the source file, colours are good (unlike .ico rn) but are scaled poorly on desktop. * Icons display properly in shortcut * code cleanup * Moved shortcut logic to specific file, added Ava UI for shortcuts * Added linux .desktop shortcut creation * fixes to .shortcut data * code issue fixes * Added basic implementation for shortcut creation Currently bitmaps (.bmp) are used as the source file, colours are good (unlike .ico rn) but are scaled poorly on desktop. * Icons display properly in shortcut * code cleanup * Moved shortcut logic to specific file, added Ava UI for shortcuts * Added linux .desktop shortcut creation * fixes to .shortcut data * code issue fixes * Replaced COM reference with ComImport for shortcut functionality * remove specific platform values and regions * Move ShortcutHelper to Ryujinx.Ui.Common.Helpers * Adjust styling and structure * code feedback changes * adjust tooltip message * added shortcut-template.desktop file * set shortcut icon location to .local/share/icons * Linux code feedback changes * change InteropServices to new securifybv.ShellLink Package * added ShellLink to readme, updated shortcut comment * Code feedback changes * Added MacOS Support (As per Jose Estrada's PR) * dotnet format * Small restructuring * Embed template files into Ryujinx.Ui.Common * Disable "CreateShortcut" option for flatpak builds --------- Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Co-authored-by: Jose Estrada <joseestradacobo@gmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/Ryujinx.Ava/Assets/Locales/en_US.json2
-rw-r--r--src/Ryujinx.Ava/Ryujinx.Ava.csproj2
-rw-r--r--src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml7
-rw-r--r--src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs11
-rw-r--r--src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs5
-rw-r--r--src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs4
-rw-r--r--src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs171
-rw-r--r--src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj10
-rw-r--r--src/Ryujinx/Ryujinx.csproj18
-rw-r--r--src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs11
-rw-r--r--src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs9
11 files changed, 235 insertions, 15 deletions
diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json
index 53e277ba..a67b796b 100644
--- a/src/Ryujinx.Ava/Assets/Locales/en_US.json
+++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json
@@ -72,6 +72,8 @@
"GameListContextMenuExtractDataRomFSToolTip": "Extract the RomFS section from Application's current config (including updates)",
"GameListContextMenuExtractDataLogo": "Logo",
"GameListContextMenuExtractDataLogoToolTip": "Extract the Logo section from Application's current config (including updates)",
+ "GameListContextMenuCreateShortcut": "Create Application Shortcut",
+ "GameListContextMenuCreateShortcutToolTip": "Create a Desktop Shortcut that launches the selected Application",
"StatusBarGamesLoaded": "{0}/{1} Games Loaded",
"StatusBarSystemVersion": "System Version: {0}",
"LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected",
diff --git a/src/Ryujinx.Ava/Ryujinx.Ava.csproj b/src/Ryujinx.Ava/Ryujinx.Ava.csproj
index a4c1ebf1..f0e99f42 100644
--- a/src/Ryujinx.Ava/Ryujinx.Ava.csproj
+++ b/src/Ryujinx.Ava/Ryujinx.Ava.csproj
@@ -145,4 +145,4 @@
<ItemGroup>
<AdditionalFiles Include="Assets\Locales\en_US.json" />
</ItemGroup>
-</Project> \ No newline at end of file
+</Project>
diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml
index 93638fc5..d81050f8 100644
--- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml
+++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml
@@ -82,4 +82,9 @@
Header="{locale:Locale GameListContextMenuExtractDataLogo}"
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
</MenuItem>
-</MenuFlyout> \ No newline at end of file
+ <MenuItem
+ Click="CreateApplicationShortcut_Click"
+ Header="{locale:Locale GameListContextMenuCreateShortcut}"
+ IsEnabled="{Binding CreateShortcutEnabled}"
+ ToolTip.Tip="{locale:Locale GameListContextMenuCreateShortcutToolTip}" />
+</MenuFlyout>
diff --git a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs
index d75572e6..0f007106 100644
--- a/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs
+++ b/src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs
@@ -337,6 +337,17 @@ namespace Ryujinx.Ava.UI.Controls
}
}
+ public void CreateApplicationShortcut_Click(object sender, RoutedEventArgs args)
+ {
+ var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
+
+ if (viewModel?.SelectedApplication != null)
+ {
+ ApplicationData selectedApplication = viewModel.SelectedApplication;
+ ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon);
+ }
+ }
+
public async void RunApplication_Click(object sender, RoutedEventArgs args)
{
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
diff --git a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
index 7a9e4df1..b1490520 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs
@@ -356,6 +356,8 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
+ public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild();
+
public string LoadHeading
{
get => _loadHeading;
@@ -1488,7 +1490,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Logger.RestartTime();
- SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path);
+ SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language);
PrepareLoadScreen();
@@ -1696,7 +1698,6 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
}
-
#endregion
}
}
diff --git a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
index 33e6c4aa..36b2b727 100644
--- a/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
+++ b/src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs
@@ -546,7 +546,7 @@ namespace Ryujinx.Ui.App.Common
return appMetadata;
}
- public byte[] GetApplicationIcon(string applicationPath)
+ public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage)
{
byte[] applicationIcon = null;
@@ -600,7 +600,7 @@ namespace Ryujinx.Ui.App.Common
{
using var icon = new UniqueRef<IFile>();
- controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ controlFs.OpenFile(ref icon.Ref, $"/icon_{desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
using MemoryStream stream = new();
diff --git a/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs
new file mode 100644
index 00000000..dab473fa
--- /dev/null
+++ b/src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs
@@ -0,0 +1,171 @@
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using ShellLink;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Runtime.Versioning;
+using Image = System.Drawing.Image;
+
+namespace Ryujinx.Ui.Common.Helper
+{
+ public static class ShortcutHelper
+ {
+ [SupportedOSPlatform("windows")]
+ private static void CreateShortcutWindows(string applicationFilePath, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath)
+ {
+ string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName + ".exe");
+ iconPath += ".ico";
+
+ MemoryStream iconDataStream = new(iconData);
+ using Image image = Image.FromStream(iconDataStream);
+ using Bitmap bitmap = new(128, 128);
+ using System.Drawing.Graphics graphic = System.Drawing.Graphics.FromImage(bitmap);
+ graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
+ graphic.DrawImage(image, 0, 0, 128, 128);
+ SaveBitmapAsIcon(bitmap, iconPath);
+
+ var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(basePath, applicationFilePath), iconPath, 0);
+ shortcut.StringData.NameString = cleanedAppName;
+ shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk"));
+ }
+
+ [SupportedOSPlatform("linux")]
+ private static void CreateShortcutLinux(string applicationFilePath, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName)
+ {
+ string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx.sh");
+ var desktopFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.desktop");
+ iconPath += ".png";
+
+ var image = SixLabors.ImageSharp.Image.Load<Rgba32>(iconData);
+ image.SaveAsPng(iconPath);
+
+ using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop"));
+ outputFile.Write(desktopFile, cleanedAppName, iconPath, GetArgsString(basePath, applicationFilePath));
+ }
+
+ [SupportedOSPlatform("macos")]
+ private static void CreateShortcutMacos(string appFilePath, byte[] iconData, string desktopPath, string cleanedAppName)
+ {
+ string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName);
+ var plistFile = EmbeddedResources.ReadAllText("Ryujinx.Ui.Common/shortcut-template.plist");
+ // Macos .App folder
+ string contentFolderPath = Path.Combine(desktopPath, cleanedAppName + ".app", "Contents");
+ string scriptFolderPath = Path.Combine(contentFolderPath, "MacOS");
+
+ if (!Directory.Exists(scriptFolderPath))
+ {
+ Directory.CreateDirectory(scriptFolderPath);
+ }
+
+ // Runner script
+ const string ScriptName = "runner.sh";
+ string scriptPath = Path.Combine(scriptFolderPath, ScriptName);
+ using StreamWriter scriptFile = new(scriptPath);
+
+ scriptFile.WriteLine("#!/bin/sh");
+ scriptFile.WriteLine(GetArgsString(basePath, appFilePath));
+
+ // Set execute permission
+ FileInfo fileInfo = new(scriptPath);
+ fileInfo.UnixFileMode |= UnixFileMode.UserExecute;
+
+ // img
+ string resourceFolderPath = Path.Combine(contentFolderPath, "Resources");
+ if (!Directory.Exists(resourceFolderPath))
+ {
+ Directory.CreateDirectory(resourceFolderPath);
+ }
+
+ const string IconName = "icon.png";
+ var image = SixLabors.ImageSharp.Image.Load<Rgba32>(iconData);
+ image.SaveAsPng(Path.Combine(resourceFolderPath, IconName));
+
+ // plist file
+ using StreamWriter outputFile = new(Path.Combine(contentFolderPath, "Info.plist"));
+ outputFile.Write(plistFile, ScriptName, cleanedAppName, IconName);
+ }
+
+ public static void CreateAppShortcut(string applicationFilePath, string applicationName, string applicationId, byte[] iconData)
+ {
+ string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
+ string cleanedAppName = string.Join("_", applicationName.Split(Path.GetInvalidFileNameChars()));
+
+ if (OperatingSystem.IsWindows())
+ {
+ string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app");
+
+ CreateShortcutWindows(applicationFilePath, iconData, iconPath, cleanedAppName, desktopPath);
+
+ return;
+ }
+
+ if (OperatingSystem.IsLinux())
+ {
+ string iconPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "icons", "Ryujinx");
+
+ Directory.CreateDirectory(iconPath);
+ CreateShortcutLinux(applicationFilePath, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName);
+
+ return;
+ }
+
+ if (OperatingSystem.IsMacOS())
+ {
+ CreateShortcutMacos(applicationFilePath, iconData, desktopPath, cleanedAppName);
+
+ return;
+ }
+
+ throw new NotImplementedException("Shortcut support has not been implemented yet for this OS.");
+ }
+
+ private static string GetArgsString(string basePath, string appFilePath)
+ {
+ // args are first defined as a list, for easier adjustments in the future
+ var argsList = new List<string>
+ {
+ basePath,
+ };
+
+ if (!string.IsNullOrEmpty(CommandLineState.BaseDirPathArg))
+ {
+ argsList.Add("--root-data-dir");
+ argsList.Add($"\"{CommandLineState.BaseDirPathArg}\"");
+ }
+
+ argsList.Add($"\"{appFilePath}\"");
+
+
+ return String.Join(" ", argsList);
+ }
+
+ /// <summary>
+ /// Creates a Icon (.ico) file using the source bitmap image at the specified file path.
+ /// </summary>
+ /// <param name="source">The source bitmap image that will be saved as an .ico file</param>
+ /// <param name="filePath">The location that the new .ico file will be saved too (Make sure to include '.ico' in the path).</param>
+ [SupportedOSPlatform("windows")]
+ private static void SaveBitmapAsIcon(Bitmap source, string filePath)
+ {
+ // Code Modified From https://stackoverflow.com/a/11448060/368354 by Benlitz
+ byte[] header = { 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 32, 0, 0, 0, 0, 0, 22, 0, 0, 0 };
+ using FileStream fs = new(filePath, FileMode.Create);
+
+ fs.Write(header);
+ // Writing actual data
+ source.Save(fs, ImageFormat.Png);
+ // Getting data length (file length minus header)
+ long dataLength = fs.Length - header.Length;
+ // Write it in the correct place
+ fs.Seek(14, SeekOrigin.Begin);
+ fs.WriteByte((byte)dataLength);
+ fs.WriteByte((byte)(dataLength >> 8));
+ }
+ }
+}
diff --git a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj
index 511a0389..3da47431 100644
--- a/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj
+++ b/src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj
@@ -45,8 +45,18 @@
<EmbeddedResource Include="Resources\Logo_Twitter_Light.png" />
</ItemGroup>
+ <ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64' OR '$(RuntimeIdentifier)' == ''">
+ <EmbeddedResource Include="..\..\distribution\linux\shortcut-template.desktop" />
+ </ItemGroup>
+
+ <ItemGroup Condition="'$(RuntimeIdentifier)' == 'osx-x64' OR '$(RuntimeIdentifier)' == 'osx-arm64' OR '$(RuntimeIdentifier)' == ''">
+ <EmbeddedResource Include="..\..\distribution\macos\shortcut-template.plist" />
+ </ItemGroup>
+
<ItemGroup>
<PackageReference Include="DiscordRichPresence" />
+ <PackageReference Include="securifybv.ShellLink" />
+ <PackageReference Include="System.Drawing.Common" />
</ItemGroup>
<ItemGroup>
diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj
index cf4435e5..5b5ed463 100644
--- a/src/Ryujinx/Ryujinx.csproj
+++ b/src/Ryujinx/Ryujinx.csproj
@@ -63,15 +63,15 @@
</Content>
</ItemGroup>
- <ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64'">
- <Content Include="..\..\distribution\linux\Ryujinx.sh">
+ <ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64'">
+ <Content Include="..\..\distribution\linux\Ryujinx.sh">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
- </Content>
- <Content Include="..\..\distribution\linux\mime\Ryujinx.xml">
- <CopyToOutputDirectory>Always</CopyToOutputDirectory>
- <TargetPath>mime\Ryujinx.xml</TargetPath>
- </Content>
- </ItemGroup>
+ </Content>
+ <Content Include="..\..\distribution\linux\mime\Ryujinx.xml">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ <TargetPath>mime\Ryujinx.xml</TargetPath>
+ </Content>
+ </ItemGroup>
<!-- Due to .net core 3.1 embedded resource loading -->
<PropertyGroup>
@@ -101,4 +101,4 @@
<EmbeddedResource Include="Modules\Updater\UpdateDialog.glade" />
</ItemGroup>
-</Project> \ No newline at end of file
+</Project>
diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs
index 0f7b4f22..75b16613 100644
--- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs
+++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs
@@ -23,6 +23,7 @@ namespace Ryujinx.Ui.Widgets
private MenuItem _purgeShaderCacheMenuItem;
private MenuItem _openPtcDirMenuItem;
private MenuItem _openShaderCacheDirMenuItem;
+ private MenuItem _createShortcutMenuItem;
private void InitializeComponent()
{
@@ -187,6 +188,15 @@ namespace Ryujinx.Ui.Widgets
};
_openShaderCacheDirMenuItem.Activated += OpenShaderCacheDir_Clicked;
+ //
+ // _createShortcutMenuItem
+ //
+ _createShortcutMenuItem = new MenuItem("Create Application Shortcut")
+ {
+ TooltipText = "Create a Desktop Shortcut that launches the selected Application."
+ };
+ _createShortcutMenuItem.Activated += CreateShortcut_Clicked;
+
ShowComponent();
}
@@ -213,6 +223,7 @@ namespace Ryujinx.Ui.Widgets
Add(new SeparatorMenuItem());
Add(_manageCacheMenuItem);
Add(_extractMenuItem);
+ Add(_createShortcutMenuItem);
ShowAll();
}
diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
index c2e0d8eb..ea60421f 100644
--- a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
+++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
@@ -10,6 +10,7 @@ using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
+using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
@@ -77,6 +78,8 @@ namespace Ryujinx.Ui.Widgets
_extractExeFsMenuItem.Sensitive = hasNca;
_extractLogoMenuItem.Sensitive = hasNca;
+ _createShortcutMenuItem.Sensitive = !ReleaseInformation.IsFlatHubBuild();
+
PopupAtPointer(null);
}
@@ -629,5 +632,11 @@ namespace Ryujinx.Ui.Widgets
}
}
}
+
+ private void CreateShortcut_Clicked(object sender, EventArgs args)
+ {
+ byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language);
+ ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon);
+ }
}
}