diff --git a/assets/logo/Icon-Windows-Linux.png b/assets/logo/Icon-Windows-Linux.png new file mode 100644 index 0000000000..ed16485342 Binary files /dev/null and b/assets/logo/Icon-Windows-Linux.png differ diff --git a/src/app/dev/DevToys.Api/DevToys.Api.csproj b/src/app/dev/DevToys.Api/DevToys.Api.csproj index 1f0b7fb951..a071940137 100644 --- a/src/app/dev/DevToys.Api/DevToys.Api.csproj +++ b/src/app/dev/DevToys.Api/DevToys.Api.csproj @@ -1,4 +1,4 @@ - + $(NetCore) true @@ -11,9 +11,12 @@ veler,btiteux SDK for developing extensions for DevToys 2.0 and higher https://devtoys.app - https://github.com/veler/DevToys - devtoys + https://github.com/DevToys-app/DevToys + devtoys-app LICENSE.md + README.md + true + Icon-Windows-Linux.png @@ -23,15 +26,14 @@ - + - - True - \ - + + + diff --git a/src/app/dev/DevToys.Api/DevToys.Api.props b/src/app/dev/DevToys.Api/DevToys.Api.props index 93b5a49557..4dbd99e69d 100644 --- a/src/app/dev/DevToys.Api/DevToys.Api.props +++ b/src/app/dev/DevToys.Api/DevToys.Api.props @@ -1,4 +1,9 @@ + true + + + devtoys-app $(PackageTags) + \ No newline at end of file diff --git a/src/app/dev/DevToys.Api/README.md b/src/app/dev/DevToys.Api/README.md new file mode 100644 index 0000000000..882b51d550 --- /dev/null +++ b/src/app/dev/DevToys.Api/README.md @@ -0,0 +1 @@ +Build an extension for [DevToys](https://devtoys.app). \ No newline at end of file diff --git a/src/app/dev/DevToys.Blazor/Pages/Index.razor b/src/app/dev/DevToys.Blazor/Pages/Index.razor index 22e5652925..92762b0008 100644 --- a/src/app/dev/DevToys.Blazor/Pages/Index.razor +++ b/src/app/dev/DevToys.Blazor/Pages/Index.razor @@ -118,7 +118,7 @@ diff --git a/src/app/dev/DevToys.Blazor/Pages/SubPages/ToolPage.razor b/src/app/dev/DevToys.Blazor/Pages/SubPages/ToolPage.razor index 45a592534c..0983566d19 100644 --- a/src/app/dev/DevToys.Blazor/Pages/SubPages/ToolPage.razor +++ b/src/app/dev/DevToys.Blazor/Pages/SubPages/ToolPage.razor @@ -4,7 +4,7 @@ + IsScrollable="@(!IsInFullScreenMode && ViewModel.ToolView is not null && ViewModel.ToolView.IsScrollable)"> @RenderToolWithHeader(true) @@ -15,7 +15,7 @@ { UIDialogService.DialogContent = (__builder) => { - @if (ViewModel.ToolView.CurrentOpenedDialog is not null) + @if (ViewModel.ToolView?.CurrentOpenedDialog is not null) { } @@ -23,7 +23,7 @@ UIDialogService.FooterContent = (__builder) => { - @if (ViewModel.ToolView.CurrentOpenedDialog is not null) + @if (ViewModel.ToolView?.CurrentOpenedDialog is not null) { } @@ -62,6 +62,12 @@ + diff --git a/src/app/dev/DevToys.Blazor/Pages/SubPages/ToolPage.razor.cs b/src/app/dev/DevToys.Blazor/Pages/SubPages/ToolPage.razor.cs index 986171af1f..0a9927bfee 100644 --- a/src/app/dev/DevToys.Blazor/Pages/SubPages/ToolPage.razor.cs +++ b/src/app/dev/DevToys.Blazor/Pages/SubPages/ToolPage.razor.cs @@ -90,6 +90,7 @@ private void ToolView_PropertyChanged(object? sender, PropertyChangedEventArgs e private void ToolView_CurrentOpenedDialogChanged(object? sender, EventArgs e) { + Guard.IsNotNull(ViewModel.ToolView); if (ViewModel.ToolView.CurrentOpenedDialog is null) { UIDialogService.CloseDialog(); @@ -103,6 +104,7 @@ private void ToolView_CurrentOpenedDialogChanged(object? sender, EventArgs e) private void DialogService_CloseDialogRequested(object? sender, EventArgs e) { + Guard.IsNotNull(ViewModel.ToolView); if (ViewModel.ToolView.CurrentOpenedDialog is not null) { ViewModel.ToolView.CurrentOpenedDialog.IsOpenedChanged -= DialogService_CloseDialogRequested; @@ -128,4 +130,9 @@ private void OnHotReloadRequestUpdateApplication(object? sender, HotReloadEventA { ViewModel.RebuildViewCommand.Execute(null); } + + private void OnHotReloadButtonClick() + { + ViewModel.RebuildViewCommand.Execute(null); + } } diff --git a/src/app/dev/DevToys.Business/Services/CommandLineLauncherService.cs b/src/app/dev/DevToys.Business/Services/CommandLineLauncherService.cs index 3f6f1ab770..b6cedab8a8 100644 --- a/src/app/dev/DevToys.Business/Services/CommandLineLauncherService.cs +++ b/src/app/dev/DevToys.Business/Services/CommandLineLauncherService.cs @@ -11,7 +11,7 @@ namespace DevToys.Business.Services; [Export] internal sealed class CommandLineLauncherService : ObservableRecipient { - internal const string ToolArgument = "--tool:"; + internal const string ToolArgument = "tool"; private readonly GuiToolProvider _guiToolProvider; private readonly IMefProvider _mefProvider; @@ -25,9 +25,9 @@ public CommandLineLauncherService(GuiToolProvider guiToolProvider, IMefProvider internal void HandleCommandLineArguments() { - if (Environment.CommandLine.Contains(ToolArgument)) + string toolName = AppHelper.GetCommandLineArgument(ToolArgument); + if (!string.IsNullOrEmpty(toolName)) { - string toolName = Environment.CommandLine.Substring(Environment.CommandLine.IndexOf(ToolArgument) + ToolArgument.Length); GuiToolInstance? tool = _guiToolProvider.GetToolFromInternalName(toolName); if (tool is not null) { @@ -44,7 +44,7 @@ internal void LaunchTool(string toolInternalName) if (tool is not null) { string appStartExe = Process.GetCurrentProcess().MainModule!.FileName; - OSHelper.OpenFileInShell(appStartExe, $"{ToolArgument}{toolInternalName}"); + OSHelper.OpenFileInShell(appStartExe, $"--{ToolArgument}:\"{toolInternalName}\""); } } } diff --git a/src/app/dev/DevToys.Business/ViewModels/ToolPageViewModel.cs b/src/app/dev/DevToys.Business/ViewModels/ToolPageViewModel.cs index 67ea03849b..a43267bae3 100644 --- a/src/app/dev/DevToys.Business/ViewModels/ToolPageViewModel.cs +++ b/src/app/dev/DevToys.Business/ViewModels/ToolPageViewModel.cs @@ -34,7 +34,7 @@ internal sealed partial class ToolPageViewModel : ObservableRecipient /// /// Gets the UI of the tool. /// - internal UIToolView ToolView + internal UIToolView? ToolView { get { diff --git a/src/app/dev/DevToys.Core/AppHelper.cs b/src/app/dev/DevToys.Core/AppHelper.cs index 428adc1e64..bdac5663dc 100644 --- a/src/app/dev/DevToys.Core/AppHelper.cs +++ b/src/app/dev/DevToys.Core/AppHelper.cs @@ -16,4 +16,57 @@ var assemblyInformationalVersion .GetCustomAttribute(typeof(AssemblyInformationalVersionAttribute))!; return assemblyInformationalVersion.InformationalVersion.Contains("pre", StringComparison.CurrentCultureIgnoreCase); }); + + /// + /// Gets the value of the specified command line argument. + /// + /// The name of the argument. The name will be interpreted as "--name:". The string should not contains "--" and ":". + /// The argument value or if not found. + /// + /// This method will search for the argument in the command line arguments. The search is case-insensitive. + /// Arguments must be like `--name:value` or `--name: value` or `--name: "value with space"`. + /// + public static string GetCommandLineArgument(string argumentName) + { + return GetCommandLineArgument(Environment.GetCommandLineArgs(), argumentName); + } + + /// + /// Gets the value of the specified command line argument. + /// + /// The list of command line arguments + /// The name of the argument. The name will be interpreted as "--name:". The string should not contains "--" and ":". + /// The argument value or if not found. + /// + /// This method will search for the argument in the command line arguments. The search is case-insensitive. + /// Arguments must be like `--name:value` or `--name: value` or `--name: "value with space"`. + /// + public static string GetCommandLineArgument(string[] arguments, string argumentName) + { + Guard.IsNotNull(arguments); + Guard.IsNotNullOrWhiteSpace(argumentName); + + argumentName = $"--{argumentName}:"; + for (int i = 0; i < arguments.Length; i++) + { + string argument = arguments[i]; + if (argument.StartsWith(argumentName, StringComparison.OrdinalIgnoreCase)) + { + if (argument.Length == argumentName.Length) + { + if (i + 1 < arguments.Length) + { + return arguments[i + 1].Trim('\"'); + } + + return string.Empty; + } + else + { + return argument.Substring(argumentName.Length).Trim('\"'); + } + } + } + return string.Empty; + } } diff --git a/src/app/dev/DevToys.Core/Mef/MefComposer.cs b/src/app/dev/DevToys.Core/Mef/MefComposer.cs index 206999639c..b6e24bfe4a 100644 --- a/src/app/dev/DevToys.Core/Mef/MefComposer.cs +++ b/src/app/dev/DevToys.Core/Mef/MefComposer.cs @@ -9,9 +9,12 @@ namespace DevToys.Core.Mef; /// public sealed partial class MefComposer : IDisposable { + private const string ExtraPluginArgument = "extraplugin"; + private readonly ILogger _logger; private readonly Assembly[] _assemblies; private readonly string[]? _pluginFolders; + private readonly string _extraPlugin; private readonly object[] _customExports; private bool _isExportProviderDisposed = true; @@ -21,13 +24,10 @@ public sealed partial class MefComposer : IDisposable public MefComposer(Assembly[]? assemblies = null, string[]? pluginFolders = null, params object[] customExports) { - if (Provider is not null) - { - throw new InvalidOperationException("Mef composer already initialized."); - } - _logger = this.Log(); + _extraPlugin = AppHelper.GetCommandLineArgument(ExtraPluginArgument); + _assemblies = assemblies ?? Array.Empty(); _pluginFolders = pluginFolders; _customExports = customExports ?? Array.Empty(); @@ -127,6 +127,11 @@ private IEnumerable GetPotentialPluginFolders() pluginFolders = new[] { Path.Combine(appFolder!, "Plugins") }; } + if (!string.IsNullOrWhiteSpace(_extraPlugin) && Directory.Exists(_extraPlugin)) + { + yield return _extraPlugin; + } + for (int i = 0; i < pluginFolders.Length; i++) { string pluginFolder = pluginFolders[i]; diff --git a/src/app/dev/DevToys.Core/Tools/GuiToolInstance.cs b/src/app/dev/DevToys.Core/Tools/GuiToolInstance.cs index 25c02b3798..05ee6989f6 100644 --- a/src/app/dev/DevToys.Core/Tools/GuiToolInstance.cs +++ b/src/app/dev/DevToys.Core/Tools/GuiToolInstance.cs @@ -9,7 +9,7 @@ namespace DevToys.Core.Tools; [DebuggerDisplay($"InternalComponentName = {{{nameof(InternalComponentName)}}}")] public sealed partial class GuiToolInstance : ObservableObject, IDisposable { - private Lazy _view; + private Lazy _view; private readonly ILogger _logger; private readonly Lazy _guiToolDefinition; private readonly Lazy _instance; @@ -79,7 +79,7 @@ internal GuiToolInstance(Lazy guiToolDefinition, Asse /// Gets the view of the tool. /// Calling this property is expensive the first time as it will create the instance of the tool and the instance of the view. /// - public UIToolView View => _view.Value; + public UIToolView? View => _view.Value; public void Dispose() { @@ -124,7 +124,7 @@ public void RebuildView() { if (_view is not null && _view.IsValueCreated) { - _view.Value.CurrentOpenedDialog?.Dispose(); + _view.Value?.CurrentOpenedDialog?.Dispose(); } _view @@ -133,7 +133,7 @@ public void RebuildView() Exception? exception = null; try { - return _instance.Value.View; + return _instance.Value?.View; } catch (NotImplementedException) { } catch (Exception ex) diff --git a/src/app/dev/DevToys.Core/Tools/GuiToolProvider.cs b/src/app/dev/DevToys.Core/Tools/GuiToolProvider.cs index 56e3ba9a97..1b7fd83e0d 100644 --- a/src/app/dev/DevToys.Core/Tools/GuiToolProvider.cs +++ b/src/app/dev/DevToys.Core/Tools/GuiToolProvider.cs @@ -339,8 +339,7 @@ public void SearchTools(string searchQuery, ObservableCollection spans); + WeightMatch(query, tool.LongOrShortDisplayTitle, out double titleWeight, out IReadOnlyList spans); WeightMatch(query, tool.SearchKeywords, out double searchKeywordsWeight, out _); WeightMatch(query, tool.Description, out double descriptionWeight, out _); diff --git a/src/app/dev/platforms/desktop/DevToys.MacOS/DevToys.MacOS.csproj b/src/app/dev/platforms/desktop/DevToys.MacOS/DevToys.MacOS.csproj index 4437daa380..20fbe79094 100644 --- a/src/app/dev/platforms/desktop/DevToys.MacOS/DevToys.MacOS.csproj +++ b/src/app/dev/platforms/desktop/DevToys.MacOS/DevToys.MacOS.csproj @@ -8,16 +8,8 @@ false $(DefineConstants);__MACOS__ - - - - - + + None com.devtoys diff --git a/src/app/dev/platforms/desktop/DevToys.Windows/Core/TaskbarJumpListService.cs b/src/app/dev/platforms/desktop/DevToys.Windows/Core/TaskbarJumpListService.cs index 4d8af14c1b..f346065e87 100644 --- a/src/app/dev/platforms/desktop/DevToys.Windows/Core/TaskbarJumpListService.cs +++ b/src/app/dev/platforms/desktop/DevToys.Windows/Core/TaskbarJumpListService.cs @@ -69,7 +69,7 @@ private static void AddToolToJumpList(JumpList jumpList, GuiToolInstance tool, s Title = tool.LongOrShortDisplayTitle, Description = tool.Description, CustomCategory = category, - Arguments = $"{CommandLineLauncherService.ToolArgument}{tool.InternalComponentName}" + Arguments = $"--{CommandLineLauncherService.ToolArgument}:\"{tool.InternalComponentName}\"" }; jumpList.JumpItems.Add(jumpTask); diff --git a/src/app/tests/DevToys.UnitTests/Core/AppHelperTests.cs b/src/app/tests/DevToys.UnitTests/Core/AppHelperTests.cs new file mode 100644 index 0000000000..eb89017ab8 --- /dev/null +++ b/src/app/tests/DevToys.UnitTests/Core/AppHelperTests.cs @@ -0,0 +1,18 @@ +using DevToys.Core; + +namespace DevToys.UnitTests.Core; + +public class AppHelperTests +{ + [Theory] + [InlineData(new[] { "" }, "tool", "")] + [InlineData(new[] { "tool" }, "tool", "")] + [InlineData(new[] { "--tool:" }, "tool", "")] + [InlineData(new[] { "--tool:value" }, "tool", "value")] + [InlineData(new[] { "--tool:", "value" }, "tool", "value")] + [InlineData(new[] { "--tool:", "\"value with space\"" }, "tool", "value with space")] + public void GetCommandLineArgument(string[] arguments, string searchedArgumentName, string expectedResult) + { + AppHelper.GetCommandLineArgument(arguments, searchedArgumentName).Should().Be(expectedResult); + } +} diff --git a/src/build/Build.cs b/src/build/Build.cs index 225ae2d4a0..ec2b61c86a 100644 --- a/src/build/Build.cs +++ b/src/build/Build.cs @@ -241,12 +241,14 @@ void PublishMacApp() DotNetBuild(s => s .SetProjectFile(dotnetParameters.ProjectOrSolutionPath) .SetConfiguration(Configuration) - .SetSelfContained(true) - .SetPublishSingleFile(false) // Not supported by MacCatalyst as it would require UseAppHost to be true, which isn't supported on Mac + .SetFramework(dotnetParameters.TargetFramework) + .SetRuntime(dotnetParameters.RuntimeIdentifier) + .SetPlatform(dotnetParameters.Platform) + .SetSelfContained(dotnetParameters.Portable) + .SetPublishSingleFile(false) .SetPublishReadyToRun(false) - .SetPublishTrimmed(true) // Should be true, even though the CSPROJ disable AOT and Trimming. + .SetPublishTrimmed(true) // HACK: Required for MacOS. However, None in the CSPROJ disables trimming. .SetVerbosity(DotNetVerbosity.quiet) - .SetNoRestore(true) /* workaround for https://github.com/xamarin/xamarin-macios/issues/15664#issuecomment-1233123515 */ .SetProcessArgumentConfigurator(_ => _ .Add("/p:RuntimeIdentifierOverride=" + dotnetParameters.RuntimeIdentifier) .Add("/p:CreatePackage=True") /* Will create an installable .pkg */ @@ -403,11 +405,11 @@ IEnumerable GetDotnetParametersForMacOSApp() if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - project = MacSolution!.GetProject(publishProject); + project = MacSolution!.GetAllProjects(publishProject).Single(); foreach (string targetFramework in project.GetTargetFrameworks()) { - yield return new DotnetParameters(project.Path, "maccatalyst-arm64", targetFramework, portable: true); - yield return new DotnetParameters(project.Path, "maccatalyst-x64", targetFramework, portable: true); + yield return new DotnetParameters(project.Path, "osx-arm64", targetFramework, portable: true); + yield return new DotnetParameters(project.Path, "osx-x64", targetFramework, portable: true); } } else