diff options
author | NitroTears <73270647+NitroTears@users.noreply.github.com> | 2023-10-21 05:51:15 +1100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-20 20:51:15 +0200 |
commit | a42f0bbb87b890d4f16b1148f9398210a5bfedfa (patch) | |
tree | 7c30a2d1fc44283846b67d98928e5f628222e3c6 /src | |
parent | b4bb22ba06f89168c948e6001c51972575ca968b (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.json | 2 | ||||
-rw-r--r-- | src/Ryujinx.Ava/Ryujinx.Ava.csproj | 2 | ||||
-rw-r--r-- | src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml | 7 | ||||
-rw-r--r-- | src/Ryujinx.Ava/UI/Controls/ApplicationContextMenu.axaml.cs | 11 | ||||
-rw-r--r-- | src/Ryujinx.Ava/UI/ViewModels/MainWindowViewModel.cs | 5 | ||||
-rw-r--r-- | src/Ryujinx.Ui.Common/App/ApplicationLibrary.cs | 4 | ||||
-rw-r--r-- | src/Ryujinx.Ui.Common/Helper/ShortcutHelper.cs | 171 | ||||
-rw-r--r-- | src/Ryujinx.Ui.Common/Ryujinx.Ui.Common.csproj | 10 | ||||
-rw-r--r-- | src/Ryujinx/Ryujinx.csproj | 18 | ||||
-rw-r--r-- | src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs | 11 | ||||
-rw-r--r-- | src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs | 9 |
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); + } } } |