diff --git a/src/WingetCreateCLI/Commands/NewCommand.cs b/src/WingetCreateCLI/Commands/NewCommand.cs index 1767eb2e..63308980 100644 --- a/src/WingetCreateCLI/Commands/NewCommand.cs +++ b/src/WingetCreateCLI/Commands/NewCommand.cs @@ -100,10 +100,7 @@ public override async Task Execute() if (!this.InstallerUrls.Any()) { - this.InstallerUrls = PromptProperty( - new Installer(), - this.InstallerUrls, - nameof(Installer.InstallerUrl)); + PromptHelper.PromptPropertyAndSetValue(this, nameof(this.InstallerUrls), this.InstallerUrls, minimum: 1, validationModel: new Installer(), validationName: nameof(Installer.InstallerUrl)); Console.Clear(); } @@ -190,11 +187,16 @@ private static void PromptPropertiesAndDisplayManifests(Manifests manifests) PromptRequiredProperties(manifests.DefaultLocaleManifest, manifests.VersionManifest); Console.WriteLine(); - if (Prompt.Confirm(Resources.ModifyOptionalFields_Message)) + if (Prompt.Confirm(Resources.ModifyOptionalDefaultLocaleFields_Message)) { PromptOptionalProperties(manifests.DefaultLocaleManifest); } + if (Prompt.Confirm(Resources.ModifyOptionalInstallerFields_Message)) + { + DisplayInstallersAsMenuSelection(manifests.InstallerManifest); + } + Console.WriteLine(); DisplayManifestPreview(manifests); } @@ -237,10 +239,89 @@ private static void PromptRequiredProperties(T manifest, VersionManifest vers continue; } - var currentValue = property.GetValue(manifest); - var result = PromptProperty(manifest, currentValue, property.Name); - property.SetValue(manifest, result); - Logger.Trace($"Property [{property.Name}] set to the value [{result}]"); + PromptHelper.PromptPropertyAndSetValue(manifest, property.Name, property.GetValue(manifest)); + Logger.Trace($"Property [{property.Name}] set to the value [{property.GetValue(manifest)}]"); + } + } + } + + /// + /// Displays all installers from an Installer manifest as a selection menu. + /// + private static void DisplayInstallersAsMenuSelection(InstallerManifest installerManifest) + { + Console.Clear(); + + while (true) + { + List selectionList = GenerateInstallerSelectionList(installerManifest.Installers, out Dictionary installerSelectionMap); + var selectedItem = Prompt.Select(Resources.SelectInstallerToEdit_Message, selectionList); + + if (selectedItem == Resources.None_MenuItem) + { + break; + } + else if (selectedItem == Resources.AllInstallers_MenuItem) + { + Installer installerCopy = new Installer(); + PromptHelper.PromptPropertiesWithMenu(installerCopy, Resources.None_MenuItem); + ApplyChangesToIndividualInstallers(installerCopy, installerManifest.Installers); + } + else if (selectedItem == Resources.DisplayPreview_MenuItem) + { + Console.Clear(); + Console.WriteLine(); + Logger.InfoLocalized(nameof(Resources.DisplayPreviewOfSelectedInstaller_Message)); + var serializer = Serialization.CreateSerializer(); + string installerString = serializer.Serialize(installerManifest); + Console.WriteLine(installerString); + Console.WriteLine(); + } + else + { + Installer selectedInstaller = installerSelectionMap[selectedItem]; + PromptHelper.PromptPropertiesWithMenu(selectedInstaller, Resources.None_MenuItem); + } + } + } + + private static List GenerateInstallerSelectionList(List installers, out Dictionary installerSelectionMap) + { + installerSelectionMap = new Dictionary(); + + foreach (Installer installer in installers) + { + var installerTuple = string.Join(" | ", new[] + { + installer.Architecture.ToEnumAttributeValue(), + installer.InstallerType.ToEnumAttributeValue(), + installer.Scope?.ToEnumAttributeValue(), + installer.InstallerLocale, + installer.InstallerUrl, + }.Where(s => !string.IsNullOrEmpty(s))); + + installerSelectionMap.Add(installerTuple, installer); + } + + List selectionList = new List() { Resources.AllInstallers_MenuItem }; + selectionList.AddRange(installerSelectionMap.Keys); + selectionList.AddRange(new[] { Resources.DisplayPreview_MenuItem, Resources.None_MenuItem }); + return selectionList; + } + + private static void ApplyChangesToIndividualInstallers(Installer installerCopy, List installers) + { + // Skip architecture as the default value when instantiated is x86, which we don't want to override other installer archs with. + var modifiedFields = installerCopy.GetType().GetProperties() + .Select(prop => prop) + .Where(pi => pi.GetValue(installerCopy) != null && pi.Name != nameof(Installer.Architecture)); + + foreach (var field in modifiedFields) + { + foreach (Installer installer in installers) + { + var fieldValue = field.GetValue(installerCopy); + installer.GetType().GetProperty(field.Name).SetValue(installer, fieldValue); } } } @@ -254,10 +335,8 @@ private static void PromptOptionalProperties(T manifest) foreach (var property in optionalProperties) { - var currentValue = property.GetValue(manifest); - var result = PromptProperty(manifest, currentValue, property.Name); - property.SetValue(manifest, result); - Logger.Trace($"Property [{property.Name}] set to the value [{result}]"); + PromptHelper.PromptPropertyAndSetValue(manifest, property.Name, property.GetValue(manifest)); + Logger.Trace($"Property [{property.Name}] set to the value [{property.GetValue(manifest)}]"); } } @@ -277,9 +356,8 @@ private static void PromptInstallerProperties(T manifest, PropertyInfo proper if (installer.InstallerType == InstallerType.Exe) { Console.WriteLine(); - Logger.Debug($"Additional metadata needed for installer from {installer.InstallerUrl}"); + Logger.DebugLocalized(nameof(Resources.AdditionalMetadataNeeded_Message), installer.InstallerUrl); prompted = true; - PromptInstallerSwitchesForExe(manifest); } @@ -294,12 +372,11 @@ private static void PromptInstallerProperties(T manifest, PropertyInfo proper if (!prompted) { Console.WriteLine(); - Logger.Debug($"Additional metadata needed for installer from {installer.InstallerUrl}"); + Logger.DebugLocalized(nameof(Resources.AdditionalMetadataNeeded_Message), installer.InstallerUrl); prompted = true; } - var result = PromptProperty(installer, currentValue, requiredProperty.Name); - requiredProperty.SetValue(installer, result); + PromptHelper.PromptPropertyAndSetValue(installer, requiredProperty.Name, requiredProperty.GetValue(installer)); } } } @@ -309,68 +386,15 @@ private static void PromptInstallerSwitchesForExe(T manifest) { InstallerSwitches installerSwitches = new InstallerSwitches(); - var silentSwitchResult = PromptProperty(installerSwitches, installerSwitches.Silent, nameof(InstallerSwitches.Silent)); - var silentWithProgressSwitchResult = PromptProperty(installerSwitches, installerSwitches.SilentWithProgress, nameof(InstallerSwitches.SilentWithProgress)); - - bool updateSwitches = false; - - if (!string.IsNullOrEmpty(silentSwitchResult)) - { - installerSwitches.Silent = silentSwitchResult; - updateSwitches = true; - } - - if (!string.IsNullOrEmpty(silentWithProgressSwitchResult)) - { - installerSwitches.SilentWithProgress = silentWithProgressSwitchResult; - updateSwitches = true; - } + PromptHelper.PromptPropertyAndSetValue(installerSwitches, nameof(InstallerSwitches.Silent), installerSwitches.Silent); + PromptHelper.PromptPropertyAndSetValue(installerSwitches, nameof(InstallerSwitches.SilentWithProgress), installerSwitches.SilentWithProgress); - if (updateSwitches) + if (!string.IsNullOrEmpty(installerSwitches.Silent) || !string.IsNullOrEmpty(installerSwitches.SilentWithProgress)) { manifest.GetType().GetProperty(nameof(InstallerSwitches)).SetValue(manifest, installerSwitches); } } - private static T PromptProperty(object model, T property, string memberName, string message = null) - { - message ??= $"[{memberName}] " + - Resources.ResourceManager.GetString($"{memberName}_KeywordDescription") ?? memberName; - - // Because some properties don't have a current value, we can't rely on T or the property to obtain the type. - // Use reflection to obtain the type by looking up the property type by membername based on the model. - Type typeFromModel = model.GetType().GetProperty(memberName).PropertyType; - if (typeFromModel.IsEnum) - { - // For enums, we want to call Prompt.Select, specifically the overload that takes 4 parameters - var generic = typeof(Prompt) - .GetMethods() - .Where(mi => mi.Name == nameof(Prompt.Select) && mi.GetParameters().Length == 5) - .Single() - .MakeGenericMethod(property.GetType()); - - return (T)generic.Invoke(null, new object[] { message, property.GetType().GetEnumValues(), null, property, null }); - } - else if (typeof(IEnumerable).IsAssignableFrom(typeof(T)) || typeof(IEnumerable).IsAssignableFrom(typeFromModel)) - { - string combinedString = null; - - if (property is IEnumerable propList && propList.Any()) - { - combinedString = string.Join(", ", propList); - } - - // Take in a comma-delimited string, and validate each split item, then return the split array - string promptResult = Prompt.Input(message, combinedString, new[] { FieldValidation.ValidateProperty(model, memberName, property) }); - return (T)(object)promptResult?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); - } - else - { - var promptResult = Prompt.Input(message, property, new[] { FieldValidation.ValidateProperty(model, memberName, property) }); - return promptResult is string str ? (T)(object)str.Trim() : promptResult; - } - } - /// /// Prompts for the package identifier and checks if the package identifier already exists. /// If the package identifier is valid, the value is applied to the other manifests. @@ -380,7 +404,7 @@ private static T PromptProperty(object model, T property, string memberName, private async Task PromptPackageIdentifierAndCheckDuplicates(Manifests manifests) { VersionManifest versionManifest = manifests.VersionManifest; - versionManifest.PackageIdentifier = PromptProperty(versionManifest, versionManifest.PackageIdentifier, nameof(versionManifest.PackageIdentifier)); + PromptHelper.PromptPropertyAndSetValue(versionManifest, nameof(versionManifest.PackageIdentifier), versionManifest.PackageIdentifier); string exactMatch; try diff --git a/src/WingetCreateCLI/PromptHelper.cs b/src/WingetCreateCLI/PromptHelper.cs new file mode 100644 index 00000000..49f5eb81 --- /dev/null +++ b/src/WingetCreateCLI/PromptHelper.cs @@ -0,0 +1,343 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateCLI +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using System.Linq; + using System.Reflection; + using Microsoft.WingetCreateCLI.Logging; + using Microsoft.WingetCreateCLI.Properties; + using Microsoft.WingetCreateCore; + using Microsoft.WingetCreateCore.Common; + using Microsoft.WingetCreateCore.Models.Installer; + using Newtonsoft.Json; + using Sharprompt; + + /// + /// Provides functionality for prompting the user for input to obtain field values for a manifest. + /// + public static class PromptHelper + { + /// + /// List of strings representing the optional fields that should not be editable. + /// + private static readonly string[] NonEditableOptionalFields = new[] + { + nameof(InstallerType), + nameof(InstallerManifest.Channel), + nameof(Installer.InstallerUrl), + nameof(Installer.InstallerSha256), + nameof(Installer.SignatureSha256), + nameof(Installer.Platform), + nameof(Installer.ProductCode), + nameof(Installer.PackageFamilyName), + nameof(Installer.AdditionalProperties), + nameof(Installer.Capabilities), + nameof(Installer.RestrictedCapabilities), + }; + + private static readonly string[] MsixExclusionList = new[] + { + nameof(Installer.Scope), + nameof(Installer.InstallerSwitches), + nameof(Installer.InstallerSuccessCodes), + nameof(Installer.UpgradeBehavior), + nameof(Installer.InstallModes), + }; + + private static readonly string[] MsixInclusionList = new[] + { + nameof(Installer.Capabilities), + nameof(Installer.RestrictedCapabilities), + }; + + /// + /// Displays all properties of a model as a navigational menu selection with some additional filtering logic based on InstallerType. + /// + /// Model type. + /// Instance of object model. + /// Exit keyword to be shown to the user to exit the navigational menu. + public static void PromptPropertiesWithMenu(T model, string exitMenuWord) + { + Console.Clear(); + + var properties = model.GetType().GetProperties().ToList(); + var optionalProperties = properties.Where(p => + p.GetCustomAttribute() == null && + p.GetCustomAttribute() != null).ToList(); + + var fieldList = properties + .Select(property => property.Name) + .Where(pName => !NonEditableOptionalFields.Any(field => field == pName)).ToList(); + + + var installerTypeProperty = model.GetType().GetProperty(nameof(InstallerType)); + if (installerTypeProperty != null) + { + var installerTypeValue = installerTypeProperty.GetValue(model); + if (installerTypeValue != null) + { + var installerType = (InstallerType)installerTypeValue; + if (installerType == InstallerType.Msi || installerType == InstallerType.Exe) + { + fieldList.Add(nameof(Installer.ProductCode)); + } + else if (installerType == InstallerType.Msix || installerType == InstallerType.Appx) + { + fieldList = fieldList.Where(pName => !MsixExclusionList.Any(field => field == pName)).ToList(); + fieldList.AddRange(MsixInclusionList); + } + } + } + + fieldList.Add(exitMenuWord); + + while (true) + { + Utils.WriteLineColored(ConsoleColor.Green, Resources.FilterMenuItems_Message); + var selectedField = Prompt.Select(Resources.SelectPropertyToEdit_Message, fieldList); + Console.Clear(); + + if (selectedField == exitMenuWord) + { + break; + } + + var selectedProperty = properties.First(p => p.Name == selectedField); + PromptPropertyAndSetValue(model, selectedField, selectedProperty.GetValue(model)); + } + } + + /// + /// Generic method for prompting for a property value of any type. + /// + /// Type of the property instance. + /// Object model to be modified. + /// Name of the selected property field. + /// Instance value of the property. + /// Minimum number of entries required for the list. + /// Object model to be validated against if the target field differs from what is specified in the model (i.e. NewCommand.InstallerUrls). + /// Name of the property field to be used for looking up validation constraints if the target field name differs from what specified in the model. + public static void PromptPropertyAndSetValue(object model, string memberName, T instance, int minimum = 0, object validationModel = null, string validationName = null) + { + Type instanceType = typeof(T); + string message = string.Format(Resources.FieldValueIs_Message, memberName); + Console.WriteLine(Resources.ResourceManager.GetString($"{memberName}_KeywordDescription")); + + if (instanceType == typeof(object)) + { + // if the instance type is an object, obtain the type from the property. + instanceType = model.GetType().GetProperty(memberName).PropertyType; + } + + if (instanceType.IsEnumerable()) + { + // elementType is needed so that Prompt.List prompts for type T and not type List + Type elementType = instanceType.GetGenericArguments().SingleOrDefault(); + var mi = typeof(PromptHelper).GetMethod(nameof(PromptHelper.PromptList)); + var generic = mi.MakeGenericMethod(elementType); + generic.Invoke(instance, new[] { message, model, memberName, instance, minimum, validationModel, validationName }); + } + else + { + var mi = typeof(PromptHelper).GetMethod(nameof(PromptHelper.PromptValue)); + var generic = mi.MakeGenericMethod(instanceType); + generic.Invoke(instance, new[] { message, model, memberName, instance }); + } + } + + /// + /// Displays a prompt to the user to enter in a value for the selected field (excludes List properties) and sets the value of the property. + /// + /// Type of the property instance. + /// Prompt message to be displayed to the user. + /// Object model to be modified. + /// Name of the selected property field. + /// Instance value of the property. + public static void PromptValue(string message, object model, string memberName, T instance) + { + var property = model.GetType().GetProperty(memberName); + Type instanceType = typeof(T); + Type elementType; + + if (instanceType == typeof(string)) + { + var result = Prompt.Input(message, property.GetValue(model), new[] { FieldValidation.ValidateProperty(model, memberName, instance) }); + property.SetValue(model, result); + } + else if (instanceType.IsEnum) + { + PromptEnum(model, property, instanceType, true, message, instance.ToString()); + } + else if ((elementType = Nullable.GetUnderlyingType(instanceType)) != null) + { + if (elementType.IsEnum) + { + PromptEnum(model, property, elementType, false, message: message); + } + } + else if (property.PropertyType.IsClass) + { + // Handles InstallerSwitches and Dependencies which both have their own unique class. + var newInstance = (T)Activator.CreateInstance(instanceType); + var initialValue = (T)property.GetValue(model) ?? newInstance; + PromptSubfieldProperties(initialValue, property, model); + } + } + + /// + /// Prompts the user for input for a field that takes in a list a values and sets the value of the property. + /// + /// Type of the property instance. + /// Prompt message to be displayed to the user. + /// Object model to be modified. + /// Name of the selected property field. + /// Instance value of the property. + /// Minimum number of entries required for the list. + /// Object model to be validated against if the target field differs from what is specified in the model (i.e. NewCommand.InstallerUrls). + /// Name of the property field to be used for looking up validation constraints if the target field name differs from what specified in the model. + public static void PromptList(string message, object model, string memberName, List instance, int minimum = 0, object validationModel = null, string validationName = null) + { + var property = model.GetType().GetProperty(memberName); + Type instanceType = typeof(T); + + if (instanceType.IsEnum) + { + // Handles List properties + var value = Prompt.MultiSelect(message, Enum.GetNames(instanceType), minimum: 0); + + if (value.Any()) + { + Type genericListType = typeof(List<>).MakeGenericType(instanceType); + var enumList = (IList)Activator.CreateInstance(genericListType); + + foreach (var item in value) + { + var enumItem = Enum.Parse(instanceType, item); + enumList.Add(enumItem); + } + + property.SetValue(model, enumList); + } + } + else if (instanceType == typeof(PackageDependencies)) + { + // Handles List + List packageDependencies = (List)property.GetValue(model) ?? new List(); + + PromptForItemList(packageDependencies); + if (packageDependencies.Any()) + { + property.SetValue(model, packageDependencies); + } + else + { + property.SetValue(model, null); + } + } + else + { + // Handles all other cases such as List, List, etc. + var maxLengthAttribute = property.GetCustomAttribute(); + int maxEntries = int.MaxValue; + if (maxLengthAttribute != null) + { + maxEntries = maxLengthAttribute.Length; + } + + IEnumerable value; + if (validationModel == null && string.IsNullOrEmpty(validationName)) + { + // Fields that take in a list don't have restrictions on values, only restrictions on the number of entries. + value = Prompt.List(message, minimum: minimum, maximum: maxEntries); + } + else + { + // Special case when the validation name differs from what is specified in the model (i.e. NewCommand.InstallerUrls needs to use Installer.InstallerUrl for validation) + value = Prompt.List(message, minimum: minimum, maximum: maxEntries, validators: new[] { FieldValidation.ValidateProperty(validationModel, validationName, instance) }); + } + + if (!value.Any()) + { + value = null; + } + + property.SetValue(model, value); + } + } + + private static void PromptEnum(object model, PropertyInfo property, Type enumType, bool required, string message, string defaultValue = null) + { + var enumList = Enum.GetNames(enumType); + + if (!required) + { + enumList.Append(Resources.None_MenuItem); + } + + var value = Prompt.Select(message, enumList, defaultValue: defaultValue); + if (value != Resources.None_MenuItem) + { + property.SetValue(model, Enum.Parse(enumType, value)); + } + } + + private static void PromptForItemList(List items) + where T : new() + { + var serializer = Serialization.CreateSerializer(); + string selection = string.Empty; + while (selection != Resources.Back_MenuItem) + { + Console.Clear(); + if (items.Any()) + { + Logger.InfoLocalized(nameof(Resources.DisplayPreviewOfItems), typeof(T).Name); + string serializedString = serializer.Serialize(items); + Console.WriteLine(serializedString); + Console.WriteLine(); + } + + selection = Prompt.Select(Resources.SelectAction_Message, new[] { Resources.Add_MenuItem, Resources.RemoveLastEntry_MenuItem, Resources.Back_MenuItem }); + if (selection == Resources.Add_MenuItem) + { + T newItem = new T(); + PromptPropertiesWithMenu(newItem, Resources.Done_MenuItem); + + // Ignore dictionary types as we don't want to take into account the AdditionalProperties field. + var properties = newItem.GetType().GetProperties().Select(p => p).Where(p => !p.GetValue(newItem).IsDictionary()); + + // Check that all values are present before appending to list. + if (!properties.Any(p => p.GetValue(newItem) == null)) + { + items.Add(newItem); + } + } + else if (selection == Resources.RemoveLastEntry_MenuItem) + { + if (items.Any()) + { + items.RemoveAt(items.Count - 1); + } + } + } + } + + private static void PromptSubfieldProperties(T field, PropertyInfo property, object model) + { + PromptPropertiesWithMenu(field, Resources.None_MenuItem); + if (field.IsEmptyObject()) + { + property.SetValue(model, null); + } + else + { + property.SetValue(model, field); + } + } + } +} diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs index e66757f9..23d86d84 100644 --- a/src/WingetCreateCLI/Properties/Resources.Designer.cs +++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs @@ -60,6 +60,24 @@ internal Resources() { } } + /// + /// Looks up a localized string similar to ADD. + /// + public static string Add_MenuItem { + get { + return ResourceManager.GetString("Add_MenuItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Additional metadata needed for installer from {0}. + /// + public static string AdditionalMetadataNeeded_Message { + get { + return ResourceManager.GetString("AdditionalMetadataNeeded_Message", resourceCulture); + } + } + /// /// Looks up a localized string similar to The additional properties for future minor version compatibility. /// @@ -69,6 +87,15 @@ public static string AdditionalProperties_KeywordDescription { } } + /// + /// Looks up a localized string similar to ALL INSTALLERS. + /// + public static string AllInstallers_MenuItem { + get { + return ResourceManager.GetString("AllInstallers_MenuItem", resourceCulture); + } + } + /// /// Looks up a localized string similar to The manifest creation command line utility generates manifest for submitting apps to the Windows Package Manager repo.. /// @@ -114,6 +141,15 @@ public static string Author_KeywordDescription { } } + /// + /// Looks up a localized string similar to BACK. + /// + public static string Back_MenuItem { + get { + return ResourceManager.GetString("Back_MenuItem", resourceCulture); + } + } + /// /// Looks up a localized string similar to Web browser failed to launch: {0}. /// @@ -312,6 +348,42 @@ public static string DetectedArchMismatch_Message { } } + /// + /// Looks up a localized string similar to DISPLAY PREVIEW. + /// + public static string DisplayPreview_MenuItem { + get { + return ResourceManager.GetString("DisplayPreview_MenuItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Displaying preview of [{0}]:. + /// + public static string DisplayPreviewOfItems { + get { + return ResourceManager.GetString("DisplayPreviewOfItems", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Displaying a preview of the selected installer:. + /// + public static string DisplayPreviewOfSelectedInstaller_Message { + get { + return ResourceManager.GetString("DisplayPreviewOfSelectedInstaller_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DONE. + /// + public static string Done_MenuItem { + get { + return ResourceManager.GetString("Done_MenuItem", resourceCulture); + } + } + /// /// Looks up a localized string similar to DONE. /// @@ -465,6 +537,15 @@ public static string ExternalDependencies_KeywordDescription { } } + /// + /// Looks up a localized string similar to [{0}] value is. + /// + public static string FieldValueIs_Message { + get { + return ResourceManager.GetString("FieldValueIs_Message", resourceCulture); + } + } + /// /// Looks up a localized string similar to List of file extensions the package could support. /// @@ -474,6 +555,15 @@ public static string FileExtensions_KeywordDescription { } } + /// + /// Looks up a localized string similar to Filter menu items by typing a search query below.. + /// + public static string FilterMenuItems_Message { + get { + return ResourceManager.GetString("FilterMenuItems_Message", resourceCulture); + } + } + /// /// Looks up a localized string similar to Generating a preview of your manifests.... /// @@ -916,11 +1006,20 @@ public static string MinOSVersion_KeywordDescription { } /// - /// Looks up a localized string similar to Would you like to modify the optional fields?. + /// Looks up a localized string similar to Would you like to modify the optional default locale fields?. /// - public static string ModifyOptionalFields_Message { + public static string ModifyOptionalDefaultLocaleFields_Message { get { - return ResourceManager.GetString("ModifyOptionalFields_Message", resourceCulture); + return ResourceManager.GetString("ModifyOptionalDefaultLocaleFields_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Would you like to modify the optional installer fields?. + /// + public static string ModifyOptionalInstallerFields_Message { + get { + return ResourceManager.GetString("ModifyOptionalInstallerFields_Message", resourceCulture); } } @@ -1014,6 +1113,15 @@ public static string NoChangeDetectedInUpdatedManifest_Message { } } + /// + /// Looks up a localized string similar to NONE. + /// + public static string None_MenuItem { + get { + return ResourceManager.GetString("None_MenuItem", resourceCulture); + } + } + /// /// Looks up a localized string similar to No token provided, submission to GitHub skipped.. /// @@ -1275,6 +1383,15 @@ public static string RegexFieldValidation_Error { } } + /// + /// Looks up a localized string similar to REMOVE LAST ENTRY. + /// + public static string RemoveLastEntry_MenuItem { + get { + return ResourceManager.GetString("RemoveLastEntry_MenuItem", resourceCulture); + } + } + /// /// Looks up a localized string similar to Repository "{0}/{1}" not found. Please verify the Windows Package Manager repository owner and name in your settings file.. /// @@ -1320,6 +1437,33 @@ public static string Scope_KeywordDescription { } } + /// + /// Looks up a localized string similar to What would you like to do?. + /// + public static string SelectAction_Message { + get { + return ResourceManager.GetString("SelectAction_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Which installer would you like to edit?. + /// + public static string SelectInstallerToEdit_Message { + get { + return ResourceManager.GetString("SelectInstallerToEdit_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Which property would you like to edit?. + /// + public static string SelectPropertyToEdit_Message { + get { + return ResourceManager.GetString("SelectPropertyToEdit_Message", resourceCulture); + } + } + /// /// Looks up a localized string similar to Open settings. /// diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx index eff5113d..00b37cad 100644 --- a/src/WingetCreateCLI/Properties/Resources.resx +++ b/src/WingetCreateCLI/Properties/Resources.resx @@ -595,8 +595,8 @@ Please check and verify the usage of this command by passing in the --help flag. - - Would you like to modify the optional fields? + + Would you like to modify the optional default locale fields? Each new installer URL must have a single match to an existing installer based on installer type and architecture. The following installers failed to match an existing installer: @@ -695,4 +695,55 @@ {1} - will be replaced with the installer type {2} - will be replaced with the installer + + Additional metadata needed for installer from {0} + {0} - Installer URL of the installer + + + ADD + + + ALL INSTALLERS + + + BACK + + + Displaying preview of [{0}]: + {0} - Name of field with list of items + + + Displaying a preview of the selected installer: + + + DISPLAY PREVIEW + + + DONE + + + [{0}] value is + {0} - Name of the field + + + Filter menu items by typing a search query below. + + + Would you like to modify the optional installer fields? + + + NONE + + + REMOVE LAST ENTRY + + + What would you like to do? + + + Which installer would you like to edit? + + + Which property would you like to edit? + \ No newline at end of file diff --git a/src/WingetCreateCLI/WingetCreateCLI.csproj b/src/WingetCreateCLI/WingetCreateCLI.csproj index 8be20969..53060915 100644 --- a/src/WingetCreateCLI/WingetCreateCLI.csproj +++ b/src/WingetCreateCLI/WingetCreateCLI.csproj @@ -10,6 +10,7 @@ win-x64;win-x86 true TELEMETRYEVENTSOURCE_PUBLIC + true @@ -24,7 +25,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/WingetCreateCore/Common/Extensions.cs b/src/WingetCreateCore/Common/Extensions.cs index 985c73b3..a9c0ab49 100644 --- a/src/WingetCreateCore/Common/Extensions.cs +++ b/src/WingetCreateCore/Common/Extensions.cs @@ -4,6 +4,10 @@ namespace Microsoft.WingetCreateCore.Common { using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Runtime.Serialization; /// /// Functionality for manipulating data related to the Manifest object model. @@ -53,5 +57,51 @@ public static bool EqualsIC(this string a, string b) { return string.Equals(a, b, StringComparison.OrdinalIgnoreCase); } + + /// + /// Determines if the object is of dictionary type. + /// + /// Object to be checked. + /// Boolean value indicating whether the object is a dictionary type. + public static bool IsDictionary(this object o) + { + return o is IDictionary && + o.GetType().IsGenericType && + o.GetType().GetGenericTypeDefinition().IsAssignableFrom(typeof(Dictionary<,>)); + } + + /// + /// Determines if the type is a List type. + /// + /// Type to be evaluated. + /// Boolean value indicating whether the type is a List. + public static bool IsEnumerable(this Type type) + { + return type != typeof(string) && typeof(IEnumerable).IsAssignableFrom(type); + } + + /// + /// Returns the enum member attribute value if one exists. + /// + /// Target enum value. + /// Enum member attribute string value. + public static string ToEnumAttributeValue(this Enum enumVal) + { + var type = enumVal.GetType(); + var memInfo = type.GetMember(enumVal.ToString()); + var attributes = memInfo[0].GetCustomAttributes(typeof(EnumMemberAttribute), false); + EnumMemberAttribute attributeValue = (attributes.Length > 0) ? (EnumMemberAttribute)attributes[0] : null; + return attributeValue?.Value ?? enumVal.ToString(); + } + + /// + /// Determines if the properties of an object are all equal to null excluding properties with dictionary type if needed. + /// + /// Object to be evaluated. + /// Boolean value indicating whether the object is empty. + public static bool IsEmptyObject(this object o) + { + return !o.GetType().GetProperties().Select(pi => pi.GetValue(o)).Where(value => !value.IsDictionary() && value != null).Any(); + } } }