WIP authoring app and Visual Studio support added

This commit is contained in:
2026-05-04 21:16:53 -05:00
parent 900291abba
commit 7ee9626ac5
349 changed files with 4769 additions and 135 deletions
+41
View File
@@ -155,6 +155,47 @@ public class MainViewModelTests
Assert.DoesNotContain(@"\\", viewModel.TomlPreview, StringComparison.Ordinal);
}
[Fact]
public void Generated_shortcut_tracks_app_info_and_allows_description_override()
{
var viewModel = new MainViewModel(() => null)
{
AppName = "Renamed App",
ApplicationFolder = "RenamedApp",
InstallRootToken = "{ProgramFilesX64}",
PrimaryPayload = @"payload\renamed.exe"
};
var defaultDocument = viewModel.BuildDocument();
var defaultShortcut = Assert.Single(defaultDocument.Shortcuts);
Assert.Equal(@"{Desktop}\RenamedApp.lnk", defaultShortcut.Path);
Assert.Equal(@"{ProgramFilesX64}\RenamedApp\bin\renamed.exe", defaultShortcut.Target);
Assert.Equal(@"{ProgramFilesX64}\RenamedApp", defaultShortcut.WorkingDirectory);
Assert.Equal("Launch Renamed App", defaultShortcut.Description);
viewModel.ShortcutDescription = "Start Renamed App";
var customShortcut = Assert.Single(viewModel.BuildDocument().Shortcuts);
Assert.Equal("Start Renamed App", customShortcut.Description);
Assert.Contains("description = 'Start Renamed App'", viewModel.TomlPreview);
}
[Fact]
public void Purge_registry_branches_track_registry_entries()
{
var viewModel = new MainViewModel(() => null)
{
ApplicationFolder = "RenamedApp"
};
viewModel.AddRegistry(@"HKLM\Software\Vendor\RenamedApp", "InstallRoot", @"{ProgramFilesX64}\RenamedApp");
var branches = viewModel.BuildDocument().Purge.RegistryBranches;
Assert.Contains(@"HKCU\Software\RenamedApp", branches);
Assert.Contains(@"HKLM\Software\Vendor\RenamedApp", branches);
Assert.Contains(@"HKLM\Software\Vendor\RenamedApp", viewModel.PurgeRegistryBranchesPreview);
}
[Fact]
public void Packaging_is_disabled_when_covenant_setup_is_missing()
{
Binary file not shown.
@@ -0,0 +1,54 @@
{
"Version": 1,
"WorkspaceRootPath": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\",
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|c:\\users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\app.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}",
"RelativeMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|solutionrelative:app.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}"
},
{
"AbsoluteMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|c:\\users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|solutionrelative:program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
}
],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 1,
"Children": [
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "Program.cs",
"DocumentMoniker": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\Program.cs",
"RelativeDocumentMoniker": "Program.cs",
"ToolTip": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\Program.cs",
"RelativeToolTip": "Program.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-05T00:32:51.733Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "App.xaml",
"DocumentMoniker": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\App.xaml",
"RelativeDocumentMoniker": "App.xaml",
"ToolTip": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\App.xaml",
"RelativeToolTip": "App.xaml",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2026-05-05T00:30:19.531Z",
"EditorCaption": ""
}
]
}
]
}
]
}
@@ -0,0 +1,54 @@
{
"Version": 1,
"WorkspaceRootPath": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\",
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|c:\\users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\app.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}",
"RelativeMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|solutionrelative:app.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}"
},
{
"AbsoluteMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|c:\\users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}|Covenant.Setup.Authoring.csproj|solutionrelative:program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
}
],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 1,
"Children": [
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "Program.cs",
"DocumentMoniker": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\Program.cs",
"RelativeDocumentMoniker": "Program.cs",
"ToolTip": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\Program.cs",
"RelativeToolTip": "Program.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-05T00:32:51.733Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "App.xaml",
"DocumentMoniker": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\App.xaml",
"RelativeDocumentMoniker": "App.xaml",
"ToolTip": "C:\\Users\\jasonross\\workspace\\covenant-setup\\authoring-ui\\App.xaml",
"RelativeToolTip": "App.xaml",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2026-05-05T00:30:19.531Z",
"EditorCaption": ""
}
]
}
]
}
]
}
+3 -3
View File
@@ -4,11 +4,11 @@
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
<UseWinUI>true</UseWinUI>
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained Condition="'$(RuntimeIdentifier)' != ''">true</WindowsAppSDKSelfContained>
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-x64</RuntimeIdentifier>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<WindowsAppSdkBootstrapInitialize>false</WindowsAppSdkBootstrapInitialize>
<WindowsAppSdkDeploymentManagerInitialize>false</WindowsAppSdkDeploymentManagerInitialize>
<WindowsAppSdkUndockedRegFreeWinRTInitialize Condition="'$(RuntimeIdentifier)' != ''">true</WindowsAppSdkUndockedRegFreeWinRTInitialize>
<WindowsAppSdkUndockedRegFreeWinRTInitialize Condition="'$(RuntimeIdentifier)' == ''">false</WindowsAppSdkUndockedRegFreeWinRTInitialize>
<WindowsAppSdkUndockedRegFreeWinRTInitialize>true</WindowsAppSdkUndockedRegFreeWinRTInitialize>
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
+25
View File
@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.5.11723.231 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Covenant.Setup.Authoring", "Covenant.Setup.Authoring.csproj", "{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D26E66D-8F74-39AC-8CD2-24BC7C6E0C0F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A95CF113-E203-4D42-901D-77B6B1147DBB}
EndGlobalSection
EndGlobal
+129
View File
@@ -11,6 +11,11 @@ internal sealed record PackageResult(
string Output,
string Error);
internal sealed record ToolValidationResult(
bool IsValid,
CovenantSetupTool? Tool,
string Message);
internal static class CovenantSetupToolLocator
{
public static CovenantSetupTool? Find()
@@ -82,6 +87,130 @@ internal static class CovenantSetupToolLocator
}
}
internal static class CovenantSetupToolValidator
{
public static async Task<ToolValidationResult> ValidateAsync(
string path,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(path))
{
return Invalid("Pick a valid covenant-setup executable.");
}
string fullPath;
try
{
fullPath = System.IO.Path.GetFullPath(path.Trim());
}
catch (Exception ex)
{
return Invalid("The covenant-setup path is invalid: " + ex.Message);
}
if (!File.Exists(fullPath))
{
return Invalid("Pick a valid covenant-setup executable.");
}
var output = new StringBuilder();
var error = new StringBuilder();
using var process = new Process
{
StartInfo = CreateHelpStartInfo(fullPath),
EnableRaisingEvents = true
};
process.OutputDataReceived += (_, args) =>
{
if (args.Data is not null)
{
output.AppendLine(args.Data);
}
};
process.ErrorDataReceived += (_, args) =>
{
if (args.Data is not null)
{
error.AppendLine(args.Data);
}
};
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(TimeSpan.FromSeconds(5));
try
{
if (!process.Start())
{
return Invalid("Pick a valid covenant-setup executable.");
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync(timeout.Token);
process.WaitForExit();
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
TryKill(process);
return Invalid("The selected executable did not respond to the covenant-setup help command.");
}
catch (Exception ex)
{
return Invalid("The selected executable could not be started: " + ex.Message);
}
var combinedOutput = string.Join(
Environment.NewLine,
new[] { output.ToString(), error.ToString() }.Where(part => !string.IsNullOrWhiteSpace(part)));
if (process.ExitCode == 0 && LooksLikeCovenantSetupHelp(combinedOutput))
{
return new ToolValidationResult(true, new CovenantSetupTool(fullPath), "Packaging enabled: " + fullPath);
}
return Invalid("Pick a valid covenant-setup executable.");
}
private static ProcessStartInfo CreateHelpStartInfo(string path)
{
var startInfo = new ProcessStartInfo
{
FileName = path,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
startInfo.ArgumentList.Add("--help");
return startInfo;
}
private static bool LooksLikeCovenantSetupHelp(string output) =>
!string.IsNullOrWhiteSpace(output) &&
(output.Contains("covenant", StringComparison.OrdinalIgnoreCase) ||
(output.Contains("package", StringComparison.OrdinalIgnoreCase) &&
output.Contains("install", StringComparison.OrdinalIgnoreCase)));
private static ToolValidationResult Invalid(string message) =>
new(false, null, message);
private static void TryKill(Process process)
{
try
{
if (!process.HasExited)
{
process.Kill(entireProcessTree: true);
}
}
catch
{
// Best effort; the caller reports validation failure either way.
}
}
}
internal static class CovenantSetupPackager
{
public static async Task<PackageResult> PackageAsync(
+255 -26
View File
@@ -13,6 +13,7 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
private string _applicationFolder = "CovenantSetupSample";
private string _primaryPayload = @"payload\sample_app.cmd";
private string _outputDirectory = Path.Combine(Environment.CurrentDirectory, "dist");
private string? _shortcutDescriptionOverride;
private CovenantSetupTool? _covenantSetupTool;
private string _covenantSetupStatus = "covenant-setup.exe was not found. Packaging is disabled.";
private string _tomlPreview = string.Empty;
@@ -69,6 +70,29 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
set => SetProperty(ref _primaryPayload, value);
}
public string ShortcutDescription
{
get => EffectiveShortcutDescription();
set
{
var normalized = string.IsNullOrWhiteSpace(value) ? null : value;
var defaultDescription = DefaultShortcutDescription(AppName.Trim());
var overrideValue = string.Equals(normalized, defaultDescription, StringComparison.Ordinal)
? null
: normalized;
if (string.Equals(_shortcutDescriptionOverride, overrideValue, StringComparison.Ordinal))
{
return;
}
_shortcutDescriptionOverride = overrideValue;
OnPropertyChanged();
NotifyGeneratedManifestFieldsChanged();
RefreshPreview();
}
}
public string OutputDirectory
{
get => _outputDirectory;
@@ -83,6 +107,7 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
if (SetProperty(ref _covenantSetupTool, value, refreshPreview: false))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasCovenantSetupTool)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CovenantSetupPath)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanPackage)));
}
}
@@ -90,6 +115,8 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
public bool HasCovenantSetupTool => CovenantSetupTool is not null;
public string CovenantSetupPath => CovenantSetupTool?.Path ?? string.Empty;
public string CovenantSetupStatus
{
get => _covenantSetupStatus;
@@ -125,6 +152,16 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
}
}
public string ShortcutPath => GeneratedShortcut()?.Path ?? string.Empty;
public string ShortcutTarget => GeneratedShortcut()?.Target ?? string.Empty;
public string ShortcutWorkingDirectory => GeneratedShortcut()?.WorkingDirectory ?? string.Empty;
public string PurgePathsPreview => JoinLines(BuildDocument().Purge.Paths);
public string PurgeRegistryBranchesPreview => JoinLines(BuildDocument().Purge.RegistryBranches);
public ObservableCollection<string> Directories { get; } = [];
public ObservableCollection<FileSpec> Files { get; } = [];
public ObservableCollection<RegistrySpec> Registry { get; } = [];
@@ -135,12 +172,23 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
public void RefreshCovenantSetupTool()
{
CovenantSetupTool = _locateCovenantSetupTool();
SetCovenantSetupTool(_locateCovenantSetupTool());
}
public void SetCovenantSetupTool(CovenantSetupTool? tool)
{
CovenantSetupTool = tool;
CovenantSetupStatus = CovenantSetupTool is null
? "covenant-setup.exe was not found. Packaging is disabled."
: "Packaging enabled: " + CovenantSetupTool.Path;
}
public void RejectCovenantSetupTool(string message)
{
CovenantSetupTool = null;
CovenantSetupStatus = message;
}
public void AddDirectory(string path)
{
AddTrimmed(Directories, path, value => value);
@@ -206,18 +254,23 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
public ManifestDocument BuildDocument()
{
var template = BuildTemplateEntries();
var directories = CombineUnique(template.Directories, Directories);
var files = CombineUnique(template.Files, Files);
var registry = CombineUnique(template.Registry, Registry);
var shortcuts = CombineUnique(BuildShortcutEntries(files), Shortcuts);
var purgePaths = CombineUnique(BuildPurgePaths(files, registry), PurgePaths);
return new ManifestDocument
{
AppName = AppName.Trim(),
Directories = CombineUnique(template.Directories, Directories),
Files = CombineUnique(template.Files, Files),
Registry = CombineUnique(template.Registry, Registry),
Shortcuts = CombineUnique(template.Shortcuts, Shortcuts),
Directories = directories,
Files = files,
Registry = registry,
Shortcuts = shortcuts,
Scripts = Scripts.ToArray(),
Purge = new PurgeSpec
{
Paths = CombineUnique(template.PurgePaths, PurgePaths),
RegistryBranches = CombineUnique(template.PurgeRegistryBranches, PurgeRegistryBranches)
Paths = purgePaths,
RegistryBranches = CombineUnique(registry.Select(entry => entry.Key), PurgeRegistryBranches)
}
};
}
@@ -308,15 +361,22 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
}
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
OnPropertyChanged(propertyName);
if (propertyName == nameof(AppName))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ExpectedManifestFileName)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ManifestSubtitle)));
OnPropertyChanged(nameof(ExpectedManifestFileName));
OnPropertyChanged(nameof(ManifestSubtitle));
}
if (propertyName == nameof(OutputDirectory))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CanPackage)));
OnPropertyChanged(nameof(CanPackage));
}
if (propertyName is nameof(AppName)
or nameof(InstallRootToken)
or nameof(ApplicationFolder)
or nameof(PrimaryPayload))
{
NotifyGeneratedManifestFieldsChanged();
}
if (refreshPreview)
{
@@ -327,9 +387,43 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
private void CollectionChanged(object? sender, NotifyCollectionChangedEventArgs args)
{
if (ReferenceEquals(sender, Files))
{
OnPropertyChanged(nameof(ShortcutPath));
OnPropertyChanged(nameof(ShortcutTarget));
OnPropertyChanged(nameof(ShortcutWorkingDirectory));
OnPropertyChanged(nameof(PurgePathsPreview));
}
if (ReferenceEquals(sender, Registry) || ReferenceEquals(sender, PurgePaths))
{
OnPropertyChanged(nameof(PurgePathsPreview));
}
if (ReferenceEquals(sender, Registry) || ReferenceEquals(sender, PurgeRegistryBranches))
{
OnPropertyChanged(nameof(PurgeRegistryBranchesPreview));
}
RefreshPreview();
}
private void NotifyGeneratedManifestFieldsChanged()
{
OnPropertyChanged(nameof(ShortcutDescription));
OnPropertyChanged(nameof(ShortcutPath));
OnPropertyChanged(nameof(ShortcutTarget));
OnPropertyChanged(nameof(ShortcutWorkingDirectory));
OnPropertyChanged(nameof(PurgePathsPreview));
OnPropertyChanged(nameof(PurgeRegistryBranchesPreview));
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
if (propertyName is not null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
private static void AddTrimmed<T>(
ObservableCollection<T> collection,
string value,
@@ -352,6 +446,154 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
private static string? NullIfWhiteSpace(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
private string EffectiveShortcutDescription() =>
_shortcutDescriptionOverride ?? DefaultShortcutDescription(AppName.Trim());
private string ManifestShortcutDescription() =>
NullIfWhiteSpace(EffectiveShortcutDescription()) ?? DefaultShortcutDescription(AppName.Trim());
private static string DefaultShortcutDescription(string appName) =>
string.IsNullOrWhiteSpace(appName) ? "Launch application" : "Launch " + appName;
private ShortcutSpec? GeneratedShortcut()
{
var template = BuildTemplateEntries();
var files = CombineUnique(template.Files, Files);
return BuildShortcutEntries(files).FirstOrDefault();
}
private static string JoinLines(IEnumerable<string> values) =>
string.Join(Environment.NewLine, values);
private IReadOnlyList<ShortcutSpec> BuildShortcutEntries(IReadOnlyList<FileSpec> files)
{
var target = files
.Select(file => file.Destination.Trim())
.FirstOrDefault(destination => !string.IsNullOrWhiteSpace(destination));
if (string.IsNullOrWhiteSpace(target))
{
return [];
}
return
[
new ShortcutSpec(
@"{Desktop}\" + ShortcutFileName(target) + ".lnk",
target,
null,
ParentPath(target),
ManifestShortcutDescription())
];
}
private static IReadOnlyList<string> BuildPurgePaths(
IReadOnlyList<FileSpec> files,
IReadOnlyList<RegistrySpec> registry)
{
var candidates = new List<string>();
foreach (var entry in registry)
{
var value = entry.Value.Trim();
if (IsManifestPath(value) && !IsUnsafePurgeRoot(value))
{
candidates.Add(value);
}
}
foreach (var file in files)
{
var parent = ParentPath(file.Destination);
if (!string.IsNullOrWhiteSpace(parent) && !IsUnsafePurgeRoot(parent))
{
candidates.Add(parent);
}
}
return RemoveCoveredChildPaths(CombineUnique(candidates, Array.Empty<string>()));
}
private string ShortcutFileName(string target)
{
var fileName = FileNameWithoutExtension(target);
var sanitized = SanitizeIdentifier(fileName);
if (!string.IsNullOrWhiteSpace(sanitized))
{
return sanitized;
}
var fallbackName = string.IsNullOrWhiteSpace(ApplicationFolder)
? AppName
: ApplicationFolder;
return SanitizeIdentifier(fallbackName);
}
private static string FileNameWithoutExtension(string path)
{
var fileName = path.Trim().TrimEnd('\\');
var separatorIndex = fileName.LastIndexOf('\\');
if (separatorIndex >= 0 && separatorIndex < fileName.Length - 1)
{
fileName = fileName[(separatorIndex + 1)..];
}
var extensionIndex = fileName.LastIndexOf('.');
return extensionIndex > 0 ? fileName[..extensionIndex] : fileName;
}
private static string? ParentPath(string path)
{
var trimmed = path.Trim().TrimEnd('\\');
var separatorIndex = trimmed.LastIndexOf('\\');
if (separatorIndex < 0)
{
return null;
}
if (separatorIndex == 2 && trimmed[1] == ':')
{
return trimmed[..(separatorIndex + 1)];
}
return separatorIndex == 0 ? null : trimmed[..separatorIndex];
}
private static bool IsManifestPath(string value) =>
ManifestTokens.KnownPathTokens.Any(token =>
value.StartsWith(token + @"\", StringComparison.OrdinalIgnoreCase)) ||
Path.IsPathRooted(value);
private static bool IsUnsafePurgeRoot(string value)
{
var normalized = value.Trim().TrimEnd('\\');
if (ManifestTokens.KnownPathTokens.Any(token =>
string.Equals(normalized, token, StringComparison.OrdinalIgnoreCase)))
{
return true;
}
return normalized.Length == 2 &&
normalized[1] == ':' &&
IsAsciiLetter(normalized[0]);
}
private static bool IsAsciiLetter(char value) =>
value is >= 'A' and <= 'Z' or >= 'a' and <= 'z';
private static IReadOnlyList<string> RemoveCoveredChildPaths(IReadOnlyList<string> paths) =>
paths
.Where(path => !paths.Any(parent =>
!string.Equals(parent, path, StringComparison.OrdinalIgnoreCase) &&
IsParentPath(parent, path)))
.ToArray();
private static bool IsParentPath(string parent, string child)
{
var normalizedParent = parent.TrimEnd('\\');
return child.Length > normalizedParent.Length &&
child.StartsWith(normalizedParent + "\\", StringComparison.OrdinalIgnoreCase);
}
private static bool IsSupportedRegistryRoot(string value) =>
value.StartsWith(@"HKCU\", StringComparison.OrdinalIgnoreCase) ||
value.StartsWith(@"HKLM\", StringComparison.OrdinalIgnoreCase);
@@ -461,7 +703,6 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
var root = InstallRootToken.TrimEnd('\\') + "\\" + folder;
var registryBranch = @"HKCU\Software\" + folder;
var files = new List<FileSpec>();
var shortcuts = new List<ShortcutSpec>();
if (!string.IsNullOrWhiteSpace(PrimaryPayload))
{
@@ -471,22 +712,13 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
{
var destination = root + @"\bin\" + fileName;
files.Add(new FileSpec(source, destination));
shortcuts.Add(new ShortcutSpec(
@"{Desktop}\" + folder + ".lnk",
destination,
null,
root,
"Launch " + appName));
}
}
return new TemplateManifestEntries(
[root, root + @"\bin"],
files,
[new RegistrySpec(registryBranch, "InstallRoot", root)],
shortcuts,
[root],
[registryBranch]);
[new RegistrySpec(registryBranch, "InstallRoot", root)]);
}
private static IReadOnlyList<T> CombineUnique<T>(IEnumerable<T> first, IEnumerable<T> second)
@@ -506,8 +738,5 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
private sealed record TemplateManifestEntries(
IReadOnlyList<string> Directories,
IReadOnlyList<FileSpec> Files,
IReadOnlyList<RegistrySpec> Registry,
IReadOnlyList<ShortcutSpec> Shortcuts,
IReadOnlyList<string> PurgePaths,
IReadOnlyList<string> PurgeRegistryBranches);
IReadOnlyList<RegistrySpec> Registry);
}
+335 -104
View File
@@ -3,6 +3,7 @@ using Microsoft.UI;
using Microsoft.UI.Text;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
@@ -24,7 +25,12 @@ internal sealed class MainWindow : Window
private readonly List<TextBlock> _secondaryTextBlocks = [];
private TextBlock _statusText = null!;
private TextBlock _toolStatusText = null!;
private TextBlock _installerDialogMessageText = null!;
private TextBox _previewBox = null!;
private Button _copyPreviewButton = null!;
private TextBox _covenantSetupPathBox = null!;
private Button _browseCovenantSetupButton = null!;
private Button _refreshToolButton = null!;
private TextBox _outputDirectoryBox = null!;
private Button _chooseOutputButton = null!;
private Button _generateInstallerButton = null!;
@@ -32,8 +38,10 @@ internal sealed class MainWindow : Window
private ToggleSwitch _themeToggle = null!;
private string? _lastSavedManifestPath;
private bool _isPackaging;
private int _copyPreviewFeedbackVersion;
private bool _syncingThemeToggle;
private bool _usingSystemTheme;
private bool _syncingCovenantSetupPath;
public MainWindow()
{
@@ -50,6 +58,7 @@ internal sealed class MainWindow : Window
if (args.PropertyName is nameof(MainViewModel.HasCovenantSetupTool)
or nameof(MainViewModel.CanPackage)
or nameof(MainViewModel.CovenantSetupStatus)
or nameof(MainViewModel.CovenantSetupPath)
or nameof(MainViewModel.OutputDirectory))
{
RefreshPackageControls();
@@ -143,19 +152,40 @@ internal sealed class MainWindow : Window
Spacing = 8
};
var themeControl = new StackPanel
{
Orientation = Orientation.Horizontal,
VerticalAlignment = VerticalAlignment.Center,
Spacing = 6
};
themeControl.Children.Add(new TextBlock
{
Text = "Light",
VerticalAlignment = VerticalAlignment.Center
});
_themeToggle = new ToggleSwitch
{
OnContent = "Dark",
OffContent = "Light",
MinWidth = 96
OffContent = string.Empty,
MinWidth = 72
};
_themeToggle.Toggled += (_, _) => ToggleTheme();
actions.Children.Add(_themeToggle);
themeControl.Children.Add(_themeToggle);
actions.Children.Add(themeControl);
var validateButton = new Button { Content = "Validate", MinWidth = 92 };
validateButton.Click += async (_, _) => await ShowValidationAsync();
actions.Children.Add(validateButton);
var installerConfigButton = new Button { Content = "Installer Config", MinWidth = 124 };
installerConfigButton.Click += async (_, _) => await ShowInstallerConfigAsync();
actions.Children.Add(installerConfigButton);
_generateInstallerButton = new Button { Content = "Save and Build", MinWidth = 124 };
_generateInstallerButton.Click += async (_, _) => await GenerateInstallerAsync();
actions.Children.Add(_generateInstallerButton);
var saveButton = new Button { Content = "Save TOML", MinWidth = 104 };
saveButton.Click += async (_, _) => await SaveManifestAsync();
actions.Children.Add(saveButton);
@@ -169,13 +199,11 @@ internal sealed class MainWindow : Window
{
var stack = new StackPanel { Spacing = 12 };
stack.Children.Add(BuildAppSection());
stack.Children.Add(BuildPackageSection());
stack.Children.Add(BuildDirectoriesSection());
stack.Children.Add(BuildFilesSection());
stack.Children.Add(BuildRegistrySection());
stack.Children.Add(BuildShortcutsSection());
stack.Children.Add(BuildScriptsSection());
stack.Children.Add(BuildPurgeSection());
return new ScrollViewer
{
@@ -202,10 +230,11 @@ internal sealed class MainWindow : Window
VerticalAlignment = VerticalAlignment.Center
});
var copyButton = new Button { Content = "Copy", MinWidth = 80 };
copyButton.Click += async (_, _) => await CopyPreviewAsync();
Grid.SetColumn(copyButton, 1);
header.Children.Add(copyButton);
_copyPreviewButton = new Button { Content = "Copy", MinWidth = 80 };
AutomationProperties.SetName(_copyPreviewButton, "Copy TOML preview");
_copyPreviewButton.Click += async (_, _) => await CopyPreviewAsync();
Grid.SetColumn(_copyPreviewButton, 1);
header.Children.Add(_copyPreviewButton);
Grid.SetRow(header, 0);
preview.Children.Add(header);
@@ -262,7 +291,7 @@ internal sealed class MainWindow : Window
Labeled("Primary Payload", payloadBox));
}
private FrameworkElement BuildPackageSection()
private FrameworkElement BuildInstallerConfigContent()
{
_toolStatusText = new TextBlock
{
@@ -276,28 +305,52 @@ internal sealed class MainWindow : Window
Mode = BindingMode.OneWay
});
_covenantSetupPathBox = new TextBox
{
PlaceholderText = @"C:\path\to\covenant-setup.exe",
Text = _viewModel.CovenantSetupPath
};
_covenantSetupPathBox.TextChanged += (_, _) =>
{
if (_syncingCovenantSetupPath)
{
return;
}
if (!string.Equals(_covenantSetupPathBox.Text, _viewModel.CovenantSetupPath, StringComparison.OrdinalIgnoreCase))
{
_viewModel.RejectCovenantSetupTool("covenant-setup.exe was not validated. Packaging is disabled.");
}
};
_covenantSetupPathBox.LostFocus += async (_, _) => await ValidateTypedCovenantSetupPathAsync(showInvalidMessage: true);
_browseCovenantSetupButton = new Button
{
Content = "Browse",
Width = 96
};
_browseCovenantSetupButton.Click += async (_, _) => await BrowseCovenantSetupPathAsync();
_outputDirectoryBox = BoundTextBox(nameof(MainViewModel.OutputDirectory), @"C:\path\to\dist");
_chooseOutputButton = new Button
{
Content = "Choose",
MinWidth = 84
Width = 96
};
_chooseOutputButton.Click += async (_, _) => await ChooseOutputDirectoryAsync();
var refreshButton = new Button
_refreshToolButton = new Button
{
Content = "Refresh Tool Check",
MinWidth = 140
Width = 156
};
refreshButton.Click += (_, _) => _viewModel.RefreshCovenantSetupTool();
_generateInstallerButton = new Button
_refreshToolButton.Click += (_, _) =>
{
Content = "Generate Installer EXE",
MinWidth = 168
_viewModel.RefreshCovenantSetupTool();
SyncCovenantSetupPathBox();
SetInstallerDialogMessage(string.Empty);
};
_generateInstallerButton.Click += async (_, _) => await GenerateInstallerAsync();
var actionRow = new StackPanel
{
@@ -305,14 +358,28 @@ internal sealed class MainWindow : Window
HorizontalAlignment = HorizontalAlignment.Right,
Spacing = 8
};
actionRow.Children.Add(refreshButton);
actionRow.Children.Add(_generateInstallerButton);
actionRow.Children.Add(_refreshToolButton);
return Section(
"Installer EXE",
_toolStatusText,
Labeled("Output Directory", InputRow(_outputDirectoryBox, _chooseOutputButton)),
actionRow);
_installerDialogMessageText = new TextBlock
{
TextWrapping = TextWrapping.Wrap,
MaxLines = 6
};
_secondaryTextBlocks.Add(_installerDialogMessageText);
var stack = new StackPanel
{
Spacing = 10,
MinWidth = 640,
HorizontalAlignment = HorizontalAlignment.Stretch
};
stack.Children.Add(_toolStatusText);
stack.Children.Add(DialogFieldGrid(
("Covenant Setup Executable", _covenantSetupPathBox, _browseCovenantSetupButton),
("Output Directory", _outputDirectoryBox, _chooseOutputButton)));
stack.Children.Add(actionRow);
stack.Children.Add(_installerDialogMessageText);
return stack;
}
private FrameworkElement BuildDirectoriesSection()
@@ -391,41 +458,11 @@ internal sealed class MainWindow : Window
private FrameworkElement BuildShortcutsSection()
{
var pathBox = new TextBox { PlaceholderText = @"{Desktop}\VendorApp.lnk" };
var targetBox = new TextBox { PlaceholderText = @"{LocalAppData}\Vendor\App\bin\app.exe" };
var argumentsBox = new TextBox { PlaceholderText = "--optional" };
var workingDirectoryBox = new TextBox { PlaceholderText = @"{LocalAppData}\Vendor\App" };
var descriptionBox = new TextBox { PlaceholderText = "Launch application" };
var rows = RemovableRows(_viewModel.Shortcuts);
var addButton = new Button { Content = "Add", MinWidth = 72 };
addButton.Click += async (_, _) =>
{
if (string.IsNullOrWhiteSpace(pathBox.Text) || string.IsNullOrWhiteSpace(targetBox.Text))
{
await ShowNoticeAsync("Shortcut Entry", "Path and target are required.");
return;
}
_viewModel.AddShortcut(
pathBox.Text,
targetBox.Text,
argumentsBox.Text,
workingDirectoryBox.Text,
descriptionBox.Text);
pathBox.Text = string.Empty;
targetBox.Text = string.Empty;
argumentsBox.Text = string.Empty;
workingDirectoryBox.Text = string.Empty;
descriptionBox.Text = string.Empty;
};
var descriptionBox = BoundTextBox(nameof(MainViewModel.ShortcutDescription), "Launch application");
return Section(
"Shortcuts",
TwoColumn(Labeled("Path", pathBox), Labeled("Target", targetBox)),
TwoColumn(Labeled("Arguments", argumentsBox), Labeled("Working Directory", workingDirectoryBox)),
Labeled("Description", descriptionBox),
AlignRight(addButton),
rows);
Labeled("Description", descriptionBox));
}
private FrameworkElement BuildScriptsSection()
@@ -466,33 +503,44 @@ internal sealed class MainWindow : Window
rows);
}
private FrameworkElement BuildPurgeSection()
private async Task ShowInstallerConfigAsync()
{
var pathBox = new TextBox { PlaceholderText = @"{LocalAppData}\Vendor\App" };
var branchBox = new TextBox { PlaceholderText = @"HKCU\Software\VendorApp" };
var pathRows = RemovableRows(_viewModel.PurgePaths);
var branchRows = RemovableRows(_viewModel.PurgeRegistryBranches);
var addPathButton = new Button { Content = "Add Path", MinWidth = 96 };
addPathButton.Click += (_, _) =>
var dialog = new ContentDialog
{
_viewModel.AddPurgePath(pathBox.Text);
pathBox.Text = string.Empty;
Title = "Installer Config",
CloseButtonText = "Close",
Width = 1080,
MinWidth = 1080,
Content = BuildInstallerConfigContent()
};
var addBranchButton = new Button { Content = "Add Branch", MinWidth = 104 };
addBranchButton.Click += (_, _) =>
if (Content is FrameworkElement root)
{
_viewModel.AddPurgeRegistryBranch(branchBox.Text);
branchBox.Text = string.Empty;
dialog.XamlRoot = root.XamlRoot;
}
dialog.Closing += (_, args) =>
{
if (_isPackaging)
{
args.Cancel = true;
}
};
return Section(
"Purge",
Labeled("Path", InputRow(pathBox, addPathButton)),
pathRows,
Labeled("Registry Branch", InputRow(branchBox, addBranchButton)),
branchRows);
RefreshPackageControls();
_ = await dialog.ShowAsync();
if (_installerDialogMessageText is not null)
{
_secondaryTextBlocks.Remove(_installerDialogMessageText);
}
_toolStatusText = null!;
_installerDialogMessageText = null!;
_covenantSetupPathBox = null!;
_browseCovenantSetupButton = null!;
_refreshToolButton = null!;
_outputDirectoryBox = null!;
_chooseOutputButton = null!;
}
private async Task SaveManifestAsync()
@@ -505,11 +553,18 @@ internal sealed class MainWindow : Window
}
private async Task<string?> WriteManifestWithPickerAsync(bool showNotice)
{
return await WriteManifestWithPickerAsync(showNotice, ShowNoticeAsync);
}
private async Task<string?> WriteManifestWithPickerAsync(
bool showNotice,
Func<string, string, Task> showMessageAsync)
{
var validation = _viewModel.Validate();
if (!validation.IsValid)
{
await ShowNoticeAsync("Validation", string.Join(Environment.NewLine, validation.Errors));
await showMessageAsync("Validation", string.Join(Environment.NewLine, validation.Errors));
return null;
}
@@ -524,7 +579,7 @@ internal sealed class MainWindow : Window
await File.WriteAllTextAsync(_lastSavedManifestPath, _viewModel.TomlPreview);
if (showNotice)
{
await ShowNoticeAsync("Manifest Saved", _lastSavedManifestPath);
await showMessageAsync("Manifest Saved", _lastSavedManifestPath);
}
return _lastSavedManifestPath;
}
@@ -543,7 +598,7 @@ internal sealed class MainWindow : Window
if (!_viewModel.IsExpectedManifestPath(file.Path))
{
await ShowNoticeAsync(
await showMessageAsync(
"Manifest File Name",
MainViewModel.ContainsWhitespace(file.Path)
? "The manifest path cannot contain spaces. Save the manifest as " + _viewModel.ExpectedManifestFileName + " in a folder path without spaces."
@@ -554,11 +609,86 @@ internal sealed class MainWindow : Window
await FileIO.WriteTextAsync(file, _viewModel.TomlPreview);
if (showNotice)
{
await ShowNoticeAsync("Manifest Saved", file.Path);
await showMessageAsync("Manifest Saved", file.Path);
}
return file.Path;
}
private async Task BrowseCovenantSetupPathAsync()
{
var picker = new FileOpenPicker();
InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(this));
picker.SuggestedStartLocation = PickerLocationId.ComputerFolder;
picker.FileTypeFilter.Add(".exe");
var file = await picker.PickSingleFileAsync();
if (file is not null)
{
await AcceptCovenantSetupPathAsync(file.Path, showInvalidMessage: true);
}
}
private async Task ValidateTypedCovenantSetupPathAsync(bool showInvalidMessage)
{
if (_covenantSetupPathBox is null)
{
return;
}
var path = _covenantSetupPathBox.Text.Trim();
if (string.IsNullOrWhiteSpace(path))
{
_viewModel.RejectCovenantSetupTool("covenant-setup.exe was not found. Packaging is disabled.");
SetInstallerDialogMessage(string.Empty);
return;
}
if (string.Equals(path, _viewModel.CovenantSetupPath, StringComparison.OrdinalIgnoreCase))
{
return;
}
await AcceptCovenantSetupPathAsync(path, showInvalidMessage);
}
private async Task<bool> AcceptCovenantSetupPathAsync(string path, bool showInvalidMessage)
{
SetInstallerDialogMessage("Checking covenant-setup...");
var result = await CovenantSetupToolValidator.ValidateAsync(path, CancellationToken.None);
if (result.IsValid && result.Tool is not null)
{
_viewModel.SetCovenantSetupTool(result.Tool);
SyncCovenantSetupPathBox();
SetInstallerDialogMessage(string.Empty);
return true;
}
_viewModel.RejectCovenantSetupTool(result.Message);
if (showInvalidMessage)
{
SetInstallerDialogMessage(result.Message + " Run the help check by selecting the real covenant-setup.exe.");
}
return false;
}
private void SyncCovenantSetupPathBox()
{
if (_covenantSetupPathBox is null)
{
return;
}
_syncingCovenantSetupPath = true;
try
{
_covenantSetupPathBox.Text = _viewModel.CovenantSetupPath;
}
finally
{
_syncingCovenantSetupPath = false;
}
}
private async Task ChooseOutputDirectoryAsync()
{
var picker = new FolderPicker();
@@ -573,29 +703,30 @@ internal sealed class MainWindow : Window
}
}
private async Task GenerateInstallerAsync()
private async Task GenerateInstallerAsync(Func<string, string, Task>? showMessageAsync = null)
{
showMessageAsync ??= ShowNoticeAsync;
var tool = _viewModel.CovenantSetupTool;
if (tool is null)
{
await ShowNoticeAsync("Installer EXE", "covenant-setup.exe was not found. Packaging is disabled.");
await showMessageAsync("Installer EXE", "covenant-setup.exe was not found. Packaging is disabled.");
return;
}
var validation = _viewModel.Validate();
if (!validation.IsValid)
{
await ShowNoticeAsync("Validation", string.Join(Environment.NewLine, validation.Errors));
await showMessageAsync("Validation", string.Join(Environment.NewLine, validation.Errors));
return;
}
if (string.IsNullOrWhiteSpace(_viewModel.OutputDirectory))
{
await ShowNoticeAsync("Installer EXE", "Choose an output directory before packaging.");
await showMessageAsync("Installer EXE", "Choose an output directory before packaging.");
return;
}
var manifestPath = await WriteManifestWithPickerAsync(showNotice: false);
var manifestPath = await WriteManifestWithPickerAsync(showNotice: false, showMessageAsync);
if (manifestPath is null)
{
return;
@@ -618,11 +749,11 @@ internal sealed class MainWindow : Window
var detail = string.Join(
Environment.NewLine + Environment.NewLine,
new[] { message, result.Output, result.Error }.Where(part => !string.IsNullOrWhiteSpace(part)));
await ShowNoticeAsync("Installer EXE", detail);
await showMessageAsync("Installer EXE", detail);
}
catch (Exception ex)
{
await ShowNoticeAsync("Installer EXE", "Packaging failed: " + ex.Message);
await showMessageAsync("Installer EXE", "Packaging failed: " + ex.Message);
}
finally
{
@@ -647,7 +778,40 @@ internal sealed class MainWindow : Window
var data = new DataPackage();
data.SetText(_viewModel.TomlPreview);
Clipboard.SetContent(data);
await ShowNoticeAsync("TOML Preview", "Copied to clipboard.");
await ShowCopyPreviewCopiedAsync();
}
private async Task ShowCopyPreviewCopiedAsync()
{
var version = ++_copyPreviewFeedbackVersion;
var brushes = CreateThemeBrushes();
_copyPreviewButton.Content = new FontIcon
{
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 16,
Glyph = "\uE8FB",
Foreground = brushes.SuccessText
};
AutomationProperties.SetName(_copyPreviewButton, "Copied");
await Task.Delay(TimeSpan.FromSeconds(1));
if (version != _copyPreviewFeedbackVersion)
{
return;
}
_copyPreviewButton.Content = "Copy";
AutomationProperties.SetName(_copyPreviewButton, "Copy TOML preview");
}
private void SetInstallerDialogMessage(string message)
{
if (_installerDialogMessageText is not null)
{
_installerDialogMessageText.Text = message;
}
}
private async Task ShowNoticeAsync(string title, string message)
@@ -760,20 +924,39 @@ internal sealed class MainWindow : Window
private void RefreshPackageControls()
{
if (_generateInstallerButton is null)
var controlsAvailable = !_isPackaging;
if (_covenantSetupPathBox is not null)
{
return;
_covenantSetupPathBox.IsEnabled = controlsAvailable;
}
if (_browseCovenantSetupButton is not null)
{
_browseCovenantSetupButton.IsEnabled = controlsAvailable;
}
if (_refreshToolButton is not null)
{
_refreshToolButton.IsEnabled = controlsAvailable;
}
if (_outputDirectoryBox is not null)
{
_outputDirectoryBox.IsEnabled = controlsAvailable;
}
if (_chooseOutputButton is not null)
{
_chooseOutputButton.IsEnabled = controlsAvailable;
}
if (_generateInstallerButton is not null)
{
_generateInstallerButton.IsEnabled = _viewModel.HasCovenantSetupTool && !_isPackaging;
_generateInstallerButton.Content = _isPackaging ? "Building..." : "Save and Build";
}
var toolAvailable = _viewModel.HasCovenantSetupTool && !_isPackaging;
_outputDirectoryBox.IsEnabled = toolAvailable;
_chooseOutputButton.IsEnabled = toolAvailable;
_generateInstallerButton.IsEnabled = _viewModel.CanPackage && !_isPackaging;
_generateInstallerButton.Content = _isPackaging ? "Generating..." : "Generate Installer EXE";
var brushes = CreateThemeBrushes();
_toolStatusText.Foreground = _viewModel.HasCovenantSetupTool
? brushes.SuccessText
: brushes.ErrorText;
if (_toolStatusText is not null)
{
_toolStatusText.Foreground = _viewModel.HasCovenantSetupTool
? brushes.SuccessText
: brushes.ErrorText;
}
}
private void ApplyThemeBrushes()
@@ -925,7 +1108,11 @@ internal sealed class MainWindow : Window
private static FrameworkElement Labeled(string label, FrameworkElement control)
{
var stack = new StackPanel { Spacing = 2 };
var stack = new StackPanel
{
Spacing = 2,
HorizontalAlignment = HorizontalAlignment.Stretch
};
stack.Children.Add(new TextBlock
{
Text = label,
@@ -936,6 +1123,47 @@ internal sealed class MainWindow : Window
return stack;
}
private static Grid DialogFieldGrid(params (string Label, TextBox TextBox, Button Button)[] fields)
{
var grid = new Grid
{
ColumnSpacing = 6,
RowSpacing = 2,
HorizontalAlignment = HorizontalAlignment.Stretch
};
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
for (var index = 0; index < fields.Length; index++)
{
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var label = new TextBlock
{
Text = fields[index].Label,
FontWeight = FontWeights.SemiBold,
FontSize = 12
};
Grid.SetRow(label, index * 2);
Grid.SetColumn(label, 0);
grid.Children.Add(label);
fields[index].TextBox.HorizontalAlignment = HorizontalAlignment.Stretch;
Grid.SetRow(fields[index].TextBox, (index * 2) + 1);
Grid.SetColumn(fields[index].TextBox, 0);
grid.Children.Add(fields[index].TextBox);
fields[index].Button.HorizontalAlignment = HorizontalAlignment.Left;
fields[index].Button.VerticalAlignment = VerticalAlignment.Bottom;
Grid.SetRow(fields[index].Button, (index * 2) + 1);
Grid.SetColumn(fields[index].Button, 1);
grid.Children.Add(fields[index].Button);
}
return grid;
}
private static Grid TwoColumn(FrameworkElement left, FrameworkElement right)
{
var grid = new Grid { ColumnSpacing = 6 };
@@ -951,8 +1179,11 @@ internal sealed class MainWindow : Window
private static Grid InputRow(TextBox textBox, Button button)
{
var grid = new Grid { ColumnSpacing = 6 };
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(9, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(7, GridUnitType.Star) });
grid.HorizontalAlignment = HorizontalAlignment.Stretch;
textBox.HorizontalAlignment = HorizontalAlignment.Stretch;
button.HorizontalAlignment = HorizontalAlignment.Right;
Grid.SetColumn(textBox, 0);
Grid.SetColumn(button, 1);
grid.Children.Add(textBox);
+3 -2
View File
@@ -51,17 +51,18 @@ The app edits the current manifest schema:
- `purge`
It keeps a live TOML preview, validates required fields locally, warns when a manifest appears to require elevation, and saves a TOML file through the Windows file picker.
Shortcut path, target, and working directory values are generated from the Files entries; only the shortcut description is directly edited. Purge paths are generated from file destinations and path-like Registry values, and purge registry branches are generated from the Registry entries.
## Installer EXE Generation
The Installer EXE section is enabled only when the app finds `covenant-setup.exe`. It checks:
The **Installer Config** button opens installer generation settings. Automatic tool detection checks:
- next to the authoring app
- the current working directory
- `target\release` and `target\debug` under nearby repo roots
- directories on `PATH`
When enabled, **Generate Installer EXE** writes the current TOML to the selected `${AppName}-install.toml` location and runs:
If the tool is not found automatically, enter or browse to `covenant-setup.exe`; the app runs its `--help` command before accepting a manually selected path. The top-level **Save and Build** button is enabled when an installer engine is detected, writes the current TOML to the selected `${AppName}-install.toml` location, and runs:
```powershell
covenant-setup.exe --json package <AppName>-install.toml --output <output-directory>
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,337 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v10.0/win-x64",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v10.0": {},
".NETCoreApp,Version=v10.0/win-x64": {
"Covenant.Setup.Authoring/1.0.0": {
"dependencies": {
"Microsoft.WindowsAppSDK": "2.0.1",
"Microsoft.Web.WebView2.Core.Projection": "1.0.3719.77",
"runtimepack.Microsoft.Windows.SDK.NET.Ref": "10.0.19041.57"
},
"runtime": {
"Covenant.Setup.Authoring.dll": {}
}
},
"runtimepack.Microsoft.Windows.SDK.NET.Ref/10.0.19041.57": {
"runtime": {
"Microsoft.Windows.SDK.NET.dll": {
"assemblyVersion": "10.0.19041.38",
"fileVersion": "10.0.19041.55"
},
"WinRT.Runtime.dll": {
"assemblyVersion": "2.2.0.0",
"fileVersion": "2.2.0.48161"
}
}
},
"Microsoft.Web.WebView2/1.0.3719.77": {
"native": {
"runtimes/win-x64/native/WebView2Loader.dll": {
"fileVersion": "1.0.3719.77"
}
}
},
"Microsoft.Windows.AI.MachineLearning/2.0.300": {
"dependencies": {
"System.Numerics.Tensors": "9.0.0"
},
"runtime": {
"lib/net8.0-windows10.0.17763.0/Microsoft.ML.OnnxRuntime.dll": {
"assemblyVersion": "0.0.0.0",
"fileVersion": "0.0.0.0"
},
"lib/net8.0-windows10.0.17763.0/Microsoft.Windows.AI.MachineLearning.Projection.dll": {
"assemblyVersion": "2.0.300.41724",
"fileVersion": "2.0.300.41724"
}
},
"native": {
"runtimes/win-x64/native/DirectML.dll": {
"fileVersion": "1.15.4.0"
},
"runtimes/win-x64/native/Microsoft.Windows.AI.MachineLearning.dll": {
"fileVersion": "2.0.300.41724"
},
"runtimes/win-x64/native/onnxruntime.dll": {
"fileVersion": "1.24.26.417"
}
}
},
"Microsoft.WindowsAppSDK/2.0.1": {
"dependencies": {
"Microsoft.WindowsAppSDK.AI": "2.0.185",
"Microsoft.WindowsAppSDK.Foundation": "2.0.20",
"Microsoft.WindowsAppSDK.InteractiveExperiences": "2.0.12",
"Microsoft.WindowsAppSDK.ML": "2.0.300",
"Microsoft.WindowsAppSDK.Widgets": "2.0.4",
"Microsoft.WindowsAppSDK.WinUI": "2.0.12"
}
},
"Microsoft.WindowsAppSDK.AI/2.0.185": {
"dependencies": {
"Microsoft.WindowsAppSDK.Foundation": "2.0.20"
},
"runtime": {
"lib/net6.0-windows10.0.17763.0/Microsoft.Graphics.Imaging.Projection.dll": {
"assemblyVersion": "2.0.184.54419",
"fileVersion": "2.0.184.54419"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.AI.ContentSafety.Projection.dll": {
"assemblyVersion": "2.0.184.54419",
"fileVersion": "2.0.184.54419"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.AI.Foundation.Projection.dll": {
"assemblyVersion": "2.0.184.54419",
"fileVersion": "2.0.184.54419"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.AI.Imaging.Projection.dll": {
"assemblyVersion": "2.0.184.54419",
"fileVersion": "2.0.184.54419"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.AI.Projection.dll": {
"assemblyVersion": "2.0.184.54419",
"fileVersion": "2.0.184.54419"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.AI.Text.Projection.dll": {
"assemblyVersion": "2.0.184.54419",
"fileVersion": "2.0.184.54419"
}
}
},
"Microsoft.WindowsAppSDK.Foundation/2.0.20": {
"dependencies": {
"Microsoft.WindowsAppSDK.InteractiveExperiences": "2.0.12"
},
"runtime": {
"lib/net6.0-windows10.0.17763.0/Microsoft.Security.Authentication.OAuth.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.AppLifecycle.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.AppNotifications.Builder.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.AppNotifications.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.ApplicationModel.Background.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.ApplicationModel.DynamicDependency.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.ApplicationModel.Resources.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.ApplicationModel.WindowsAppRuntime.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.BadgeNotifications.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.Foundation.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.Management.Deployment.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.Media.Capture.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.PushNotifications.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.Security.AccessControl.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.Storage.Pickers.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.Storage.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.System.Power.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.System.Projection.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
},
"lib/net6.0-windows10.0.17763.0/Microsoft.WindowsAppRuntime.Bootstrap.Net.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
}
},
"native": {
"runtimes/win-x64/native/Microsoft.Windows.ApplicationModel.Background.UniversalBGTask.dll": {
"fileVersion": "2.0.0.0"
},
"runtimes/win-x64/native/Microsoft.WindowsAppRuntime.Bootstrap.dll": {
"fileVersion": "2.0.0.0"
}
}
},
"Microsoft.WindowsAppSDK.InteractiveExperiences/2.0.12": {
"runtime": {
"lib/net6.0-windows10.0.18362.0/Microsoft.InteractiveExperiences.Projection.dll": {
"assemblyVersion": "10.0.0.0",
"fileVersion": "10.0.27200.1014"
}
}
},
"Microsoft.WindowsAppSDK.ML/2.0.300": {
"dependencies": {
"Microsoft.Windows.AI.MachineLearning": "2.0.300",
"Microsoft.WindowsAppSDK.Foundation": "2.0.20"
}
},
"Microsoft.WindowsAppSDK.Widgets/2.0.4": {
"runtime": {
"lib/net6.0-windows10.0.17763.0/Microsoft.Windows.Widgets.Projection.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.0.0"
}
}
},
"Microsoft.WindowsAppSDK.WinUI/2.0.12": {
"dependencies": {
"Microsoft.Web.WebView2": "1.0.3719.77",
"Microsoft.WindowsAppSDK.Foundation": "2.0.20",
"Microsoft.WindowsAppSDK.InteractiveExperiences": "2.0.12"
},
"runtime": {
"lib/net6.0-windows10.0.17763.0/Microsoft.WinUI.dll": {
"assemblyVersion": "3.0.0.0",
"fileVersion": "3.0.0.2604"
}
}
},
"System.Numerics.Tensors/9.0.0": {
"runtime": {
"lib/net9.0/System.Numerics.Tensors.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.24.52809"
}
}
},
"Microsoft.Web.WebView2.Core.Projection/1.0.3719.77": {
"runtime": {
"Microsoft.Web.WebView2.Core.Projection.dll": {
"assemblyVersion": "1.0.3719.77",
"fileVersion": "1.0.3719.77"
}
}
}
}
},
"libraries": {
"Covenant.Setup.Authoring/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"runtimepack.Microsoft.Windows.SDK.NET.Ref/10.0.19041.57": {
"type": "runtimepack",
"serviceable": false,
"sha512": ""
},
"Microsoft.Web.WebView2/1.0.3719.77": {
"type": "package",
"serviceable": true,
"sha512": "sha512-t+ucyKw5NTwMjsUrDF6R9Lk40lpcKQD1/HgyGFxl49tdA4h9dKlsj6FYGEmDRpFNfnTpENuTypMcdbrlkqBdDA==",
"path": "microsoft.web.webview2/1.0.3719.77",
"hashPath": "microsoft.web.webview2.1.0.3719.77.nupkg.sha512"
},
"Microsoft.Windows.AI.MachineLearning/2.0.300": {
"type": "package",
"serviceable": true,
"sha512": "sha512-OiJX15N198kG30Lop7LH8WjmBfk2/9iU9it9eLPnkNZ49Yi/vAa4ntONGiGOgv5mb8/fvFHV5trCWSPM+OQhSw==",
"path": "microsoft.windows.ai.machinelearning/2.0.300",
"hashPath": "microsoft.windows.ai.machinelearning.2.0.300.nupkg.sha512"
},
"Microsoft.WindowsAppSDK/2.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-R+iN0mqIUUppbzpHU178R5malnttTTVBOBIGi1NQVVZAs34bAANM/iLJhjj6uUBgYiPfpvjcLbHT5IZ8kKKaqg==",
"path": "microsoft.windowsappsdk/2.0.1",
"hashPath": "microsoft.windowsappsdk.2.0.1.nupkg.sha512"
},
"Microsoft.WindowsAppSDK.AI/2.0.185": {
"type": "package",
"serviceable": true,
"sha512": "sha512-KkWthQS+QuiTQJ8HgSpobUgfJDRUAwps0/1MjYM3wZ/hQzSKr1UoyiYLgk/I8Z2I+/NNphbZ9NsY7DwHKrZJlg==",
"path": "microsoft.windowsappsdk.ai/2.0.185",
"hashPath": "microsoft.windowsappsdk.ai.2.0.185.nupkg.sha512"
},
"Microsoft.WindowsAppSDK.Foundation/2.0.20": {
"type": "package",
"serviceable": true,
"sha512": "sha512-PwoRG/eddoi9SOSVPyanei5WAHMyAeVFAeqcovd0V0Ebx6dw7S+ksVVGThAoMjrRI6f93Fa6mTmwXqFHExOtlw==",
"path": "microsoft.windowsappsdk.foundation/2.0.20",
"hashPath": "microsoft.windowsappsdk.foundation.2.0.20.nupkg.sha512"
},
"Microsoft.WindowsAppSDK.InteractiveExperiences/2.0.12": {
"type": "package",
"serviceable": true,
"sha512": "sha512-uGNDuIhBHTO1azsa0SCO6/h/0JBVKqoKiJA0K8JSgrlGeC5Lx7g0xppS8aOF/aTnOPSR9BIEHWRvH9JldWTbBg==",
"path": "microsoft.windowsappsdk.interactiveexperiences/2.0.12",
"hashPath": "microsoft.windowsappsdk.interactiveexperiences.2.0.12.nupkg.sha512"
},
"Microsoft.WindowsAppSDK.ML/2.0.300": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3/PInpjZozhI+KryPO2b3gZVNyFVOd5qSBhiGW/qFo2VzKQCMc0+cyaCPVm8uhfm3jqgYjQ4g/EqHFA5gJIaoQ==",
"path": "microsoft.windowsappsdk.ml/2.0.300",
"hashPath": "microsoft.windowsappsdk.ml.2.0.300.nupkg.sha512"
},
"Microsoft.WindowsAppSDK.Widgets/2.0.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-gvfIHLmQKBQ+KQKrWb+Oc+eQEZtpKx2zHQDBzFF7Tio+SDkXtaONU4nvHgaqcnkbqNpDPPT/pYIr7UaI51vNEQ==",
"path": "microsoft.windowsappsdk.widgets/2.0.4",
"hashPath": "microsoft.windowsappsdk.widgets.2.0.4.nupkg.sha512"
},
"Microsoft.WindowsAppSDK.WinUI/2.0.12": {
"type": "package",
"serviceable": true,
"sha512": "sha512-qNyajFTgO9xuxKtcs7vLqg7NLSuZK5+EUKeRh6fHQ9Da8hhmymoKci/srxyQwyLkFOXRL7nnuoxovUpzNjlsCg==",
"path": "microsoft.windowsappsdk.winui/2.0.12",
"hashPath": "microsoft.windowsappsdk.winui.2.0.12.nupkg.sha512"
},
"System.Numerics.Tensors/9.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-hyJB4UlpAi19Xr9AXzu2NuagKC4lPfHObNMEAA0HmqFz2rX7wKgzeYzO/jM/eBHDhnUGFFEjk5cOoJaxqg5J4A==",
"path": "system.numerics.tensors/9.0.0",
"hashPath": "system.numerics.tensors.9.0.0.nupkg.sha512"
},
"Microsoft.Web.WebView2.Core.Projection/1.0.3719.77": {
"type": "reference",
"serviceable": false,
"sha512": ""
}
}
}
@@ -0,0 +1,13 @@
{
"runtimeOptions": {
"tfm": "net10.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "10.0.0"
},
"configProperties": {
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false,
"CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS": false
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,844 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link href="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.css" rel="stylesheet" />
<script src="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.js"></script>
<script src="https://atlas.microsoft.com/sdk/javascript/service/2/atlas-service.min.js"></script>
<script>
/*
This module contains the HtmlMarkerLayer class, which is a layer that renders HTML markers on the map.
Source: https://learn.microsoft.com/en-us/samples/azure-samples/azure-maps-html-marker-layer/azure-maps-html-marker-layer-module/
azure-maps-html-marker-layer Version: 0.0.1
MIT License - Copyright (c) Microsoft Corporation.
*/
!function(e, y) {
"use strict";
var o = function(e, t) {
return (o = Object.setPrototypeOf || {
__proto__: []
}instanceof Array && function(e, t) {
e.__proto__ = t
}
|| function(e, t) {
for (var r in t)
t.hasOwnProperty(r) && (e[r] = t[r])
}
)(e, t)
};
function t(e, t) {
function r() {
this.constructor = e
}
o(e, t),
e.prototype = null === t ? Object.create(t) : (r.prototype = t.prototype,
new r)
}
var n = function() {
return (n = Object.assign || function(e) {
for (var t, r = 1, o = arguments.length; r < o; r++)
for (var a in t = arguments[r])
Object.prototype.hasOwnProperty.call(t, a) && (e[a] = t[a]);
return e
}
).apply(this, arguments)
};
function a(e, n, s, l) {
return new (s = s || Promise)(function(r, t) {
function o(e) {
try {
i(l.next(e))
} catch (e) {
t(e)
}
}
function a(e) {
try {
i(l.throw(e))
} catch (e) {
t(e)
}
}
function i(e) {
var t;
e.done ? r(e.value) : ((t = e.value)instanceof s ? t : new s(function(e) {
e(t)
}
)).then(o, a)
}
i((l = l.apply(e, n || [])).next())
}
)
}
function g(r, o) {
var a, i, n, e, s = {
label: 0,
sent: function() {
if (1 & n[0])
throw n[1];
return n[1]
},
trys: [],
ops: []
};
return e = {
next: t(0),
throw: t(1),
return: t(2)
},
"function" == typeof Symbol && (e[Symbol.iterator] = function() {
return this
}
),
e;
function t(t) {
return function(e) {
return function(t) {
if (a)
throw new TypeError("Generator is already executing.");
for (; s; )
try {
if (a = 1,
i && (n = 2 & t[0] ? i.return : t[0] ? i.throw || ((n = i.return) && n.call(i),
0) : i.next) && !(n = n.call(i, t[1])).done)
return n;
switch (i = 0,
n && (t = [2 & t[0], n.value]),
t[0]) {
case 0:
case 1:
n = t;
break;
case 4:
return s.label++,
{
value: t[1],
done: !1
};
case 5:
s.label++,
i = t[1],
t = [0];
continue;
case 7:
t = s.ops.pop(),
s.trys.pop();
continue;
default:
if (!(n = 0 < (n = s.trys).length && n[n.length - 1]) && (6 === t[0] || 2 === t[0])) {
s = 0;
continue
}
if (3 === t[0] && (!n || t[1] > n[0] && t[1] < n[3])) {
s.label = t[1];
break
}
if (6 === t[0] && s.label < n[1]) {
s.label = n[1],
n = t;
break
}
if (n && s.label < n[2]) {
s.label = n[2],
s.ops.push(t);
break
}
n[2] && s.ops.pop(),
s.trys.pop();
continue
}
t = o.call(r, s)
} catch (e) {
t = [6, e],
i = 0
} finally {
a = n = 0
}
if (5 & t[0])
throw t[1];
return {
value: t[0] ? t[1] : void 0,
done: !0
}
}([t, e])
}
}
}
var r = (i.merge = function(e, t) {
for (var r = window || global, o = 0, a = e.split("."); o < a.length; o++) {
var i = a[o];
if (!r[i])
return t;
r = r[i]
}
return n(n({}, r), t)
}
,
i);
function i() {}
var s, l = (t(u, s = y.layer.BubbleLayer),
u.prototype.getOptions = function() {
return Object.assign({}, this._options)
}
,
u.prototype.setOptions = function(e) {
var t = this._options
, r = {}
, o = !1;
e.source && t.source !== e.source && (t.source = e.source,
r.source = e.source,
o = !0),
e.sourceLayer && t.sourceLayer !== e.sourceLayer && (t.sourceLayer = e.sourceLayer,
r.sourceLayer = e.sourceLayer,
o = !0),
e.filter && t.filter !== e.filter && (t.filter = e.filter,
r.filter = e.filter,
o = !0),
"number" == typeof e.minZoom && t.minZoom !== e.minZoom && (t.minZoom = e.minZoom,
r.minZoom = e.minZoom),
"number" == typeof e.maxZoom && t.maxZoom !== e.maxZoom && (t.maxZoom = e.maxZoom,
r.maxZoom = e.maxZoom),
void 0 !== e.visible && t.visible !== e.visible && (t.visible = e.visible,
r.visible = e.visible),
e.markerCallback && t.markerCallback != e.markerCallback && (t.markerCallback = e.markerCallback,
o = !0),
"boolean" == typeof e.updateWhileMoving && t.updateWhileMoving !== e.updateWhileMoving && (t.updateWhileMoving = e.updateWhileMoving),
o ? this._clearCache(!0) : this._updateMarkers(),
s.prototype.setOptions.call(this, r)
}
,
u.prototype.update = function() {
this._clearCache(!0),
this._updateMarkers()
}
,
u.prototype.onAdd = function(e) {
var t = this
, r = e.events;
e && (r.remove("moveend", t._updateMarkers),
r.remove("move", t._mapMoved),
r.remove("sourcedata", t._sourceUpdated)),
(r = (t._map = e).events).add("moveend", t._updateMarkers),
r.add("move", t._mapMoved),
r.add("sourcedata", t._sourceUpdated),
s.prototype.onAdd.call(this, e)
}
,
u.prototype.onRemove = function() {
var e = this
, t = e._map;
if (t) {
var r = t.events;
r.remove("moveend", e._updateMarkers),
r.remove("move", e._mapMoved),
r.remove("sourcedata", e._sourceUpdated)
}
e._clearCache(!1),
e._map = null,
s.prototype.onRemove.call(this)
}
,
u.prototype._getSourceClass = function() {
var e = this.getSource();
return "string" == typeof e && null !== this._map ? this._map.sources.getById(e) : e instanceof y.source.Source ? e : null
}
,
u.prototype._clearCache = function(e) {
var t = this;
if (t._markerCache = {},
t._map)
for (var r = 0, o = t._markers.length; r < o; r++) {
var a = t._markers[r];
t._removeEvents(a),
a._eventsAttached = !1,
t._map.markers.remove(a)
}
t._markers = [],
t._markerIds = [],
e && t._updateMarkers()
}
,
u.prototype._getMarker = function(o, a, i) {
var n = this
, s = n._markerCache
, e = n._options;
if (o = o || a.join(",") + JSON.stringify(i || {}),
s[o])
return s[o];
var t = e.markerCallback(o, a, i);
if (!(t instanceof y.HtmlMarker))
return new Promise(function(r) {
t.then(function(e) {
var t = n._getExtendedMarker(e, o, a, i);
t && (s[o] = t,
r(t))
})
}
);
var r = n._getExtendedMarker(t, o, a, i);
return r ? (s[o] = r,
Promise.resolve(r)) : null
}
,
u.prototype._getExtendedMarker = function(e, t, r, o) {
var a = e;
return a ? (a.properties = o,
a.id = t,
a.setOptions({
position: r
}),
a) : null
}
,
u.prototype._addEvents = function(t) {
var r = this;
r._supportedEvents.forEach(function(e) {
r.map.events.add(e, t, r._wrappedEvent)
})
}
,
u.prototype._removeEvents = function(t) {
var r = this;
r._supportedEvents.forEach(function(e) {
r.map.events.remove(e, t, r._wrappedEvent)
})
}
,
u);
function u(e, t, r) {
var o = s.call(this, e, t) || this;
return o._options = {
minZoom: 0,
maxZoom: 24,
visible: !0,
updateWhileMoving: !1,
filter: ["==", ["geometry-type"], "Point"],
markerCallback: function(e, t, r) {
return r.cluster ? new y.HtmlMarker({
position: t,
text: r.point_count_abbreviated
}) : new y.HtmlMarker({
position: t
})
}
},
o._markers = [],
o._markerIds = [],
o._markerCache = {},
o._supportedEvents = ["click", "contextmenu", "dblclick", "drag", "dragstart", "dragend", "keydown", "keypress", "keyup", "mousedown", "mouseenter", "mouseleave", "mousemove", "mouseout", "mouseover", "mouseup"],
o._mapMoved = function() {
o._options.updateWhileMoving && o._updateMarkers()
}
,
o._sourceUpdated = function(e) {
var t = o._getSourceClass();
t && t.getId() === e.source.id && (o._timer && clearTimeout(o._timer),
o._timer = setTimeout(o._sourceUpdater, 33))
}
,
o._sourceUpdater = function() {
o._timer = null,
o._clearCache(!0)
}
,
o._updateMarkers = function() {
return a(o, void 0, void 0, function() {
var t, r, o, a, i, n, s, l, u, c, p, d, m, h, f, v, _, k;
return g(this, function(e) {
switch (e.label) {
case 0:
if (r = (t = this)._map,
o = t._markers,
a = t._options,
i = r ? r.getCamera().zoom : void 0,
!(a.visible && void 0 !== i && i >= a.minZoom && i <= a.maxZoom))
return [3, 5];
n = t.getSource(),
s = "string" == typeof n ? n : n.getId(),
l = r.map.querySourceFeatures(s, {
sourceLayer: t.getOptions().sourceLayer,
filter: a.filter
}),
u = [],
c = [],
v = f = h = m = d = p = void 0,
k = 0,
_ = l.length,
e.label = 1;
case 1:
return k < _ ? (p = v = null,
l[k]instanceof y.Shape ? "Point" === (h = l[k]).getType() && (m = h.getCoordinates(),
d = h.getProperties(),
p = h.getId()) : "Point" === (f = l[k]).geometry.type && (m = f.geometry.coordinates,
(d = f.properties) && d.cluster ? p = "cluster_" + f.properties.cluster_id : f.id && (p = f.id)),
m ? [4, t._getMarker(p, m, d)] : [3, 3]) : [3, 4];
case 2:
(v = e.sent())._eventsAttached || (t._addEvents(v),
v._eventsAttached = !0),
v && (v.id && c.push(v.id),
v.id && -1 !== t._markerIds.indexOf(v.id) || (u.push(v),
r.markers.add(v))),
e.label = 3;
case 3:
return k++,
[3, 1];
case 4:
for (k = o.length - 1; 0 <= k; k--)
o[k].id && -1 !== c.indexOf(o[k].id) || (r.markers.remove(o[k]),
o.splice(k, 1));
return t._markers = o.concat(u),
t._markerIds = c,
[3, 6];
case 5:
0 < t._markers.length && (r.markers.remove(t._markers),
t._markers = []),
e.label = 6;
case 6:
return [2]
}
})
})
}
,
o._wrappedEvent = function(e) {
o.map.events.invoke(e.type, o, e)
}
,
s.prototype.setOptions.call(o, {
color: "transparent",
radius: 0,
strokeWidth: 0
}),
o.setOptions(r || {}),
o
}
var b, c = Object.freeze({
__proto__: null,
HtmlMarkerLayer: l
}), p = (t(C, b = y.HtmlMarker),
C.prototype.getTotalValue = function() {
return this._total
}
,
C.prototype.getSliceValue = function(e) {
var t = this._options.values;
return 0 <= e && e < t.length ? t[e] : 0
}
,
C.prototype.getSlicePercentage = function(e) {
return 0 < this._total ? Math.round(this.getSliceValue(e) / this._total * 1e4) / 100 : 0
}
,
C.prototype.getOptions = function() {
return Object.assign({}, b.prototype.getOptions.call(this), this._options)
}
,
C.prototype.setOptions = function(e) {
var t = this._options
, r = JSON.stringify
, o = !1;
e.radius && 0 < e.radius && e.radius != t.radius && (t.radius = e.radius,
o = !0),
0 <= e.innerRadius && e.innerRadius != t.innerRadius && (t.innerRadius = e.innerRadius,
o = !0),
e.colors && r(e.colors) !== r(t.colors) && (t.colors = e.colors,
o = !0),
e.fillColor && r(e.fillColor) !== r(t.fillColor) && (t.fillColor = e.fillColor,
o = !0),
e.strokeColor && e.strokeColor !== t.strokeColor && (t.strokeColor = e.strokeColor,
o = !0),
0 <= e.strokeWidth && e.strokeWidth != t.strokeWidth && (t.strokeWidth = e.strokeWidth,
o = !0),
void 0 !== e.tooltipCallback && t.tooltipCallback != e.tooltipCallback && (t.tooltipCallback = e.tooltipCallback,
o = !0),
e.values && r(e.values) !== r(t.values) && (t.values = e.values,
o = !0),
void 0 !== e.text && e.text !== t.text && (b.prototype.setOptions.call(this, {
text: e.text
}),
o = !0),
void 0 !== e.textClassName && e.textClassName !== t.textClassName && (t.textClassName = e.textClassName,
o = !0),
o && this._render(),
b.prototype.setOptions.call(this, e)
}
,
C.prototype._render = function() {
var e = this
, t = e._options
, r = t.values
, o = t.radius
, a = 0
, i = 0;
if (r) {
e._total = r.reduce(function(e, t) {
return e + t
}, 0);
for (var n = C._moreColors, s = Math.random, l = Math.round, u = 0; r.length > t.colors.length; )
n.length < r.length && n.push("hsl(" + l(360 * s()) + "," + (l(20 * s()) + 70) + "%," + (l(40 * s()) + 30) + "%)"),
t.colors.push(n[u]),
u++;
var c = o + t.strokeWidth
, p = ['<svg xmlns="http://www.w3.org/2000/svg" width="' + 2 * c + 'px" height="' + 2 * c + 'px">']
, d = ""
, m = void 0;
if (0 < t.innerRadius && t.innerRadius <= t.radius && (m = "piechart-innercircle-" + l(1e7 * s()),
p.push('<defs><mask id="' + m + '"><rect width="100%" height="100%" fill="white"/><circle r="' + t.innerRadius + '" cx="' + c + '" cy="' + c + '" fill="black"/></mask></defs>\n <circle r="' + t.innerRadius + '" cx="' + c + '" cy="' + c + '" style="fill:' + t.fillColor + ";stroke:" + t.strokeColor + ";stroke-width:" + 2 * t.strokeWidth + 'px;"/>')),
0 < e._total)
for (var h = t.tooltipCallback, f = 2 * Math.PI / e._total, v = 0; v < r.length; v++) {
i = f * r[v],
h && (d = h(e, v));
var _ = v < t.colors.length ? t.colors[v] : n[v];
p.push(e._createSlice(c, c, o, a, i, _, d, m)),
a += i
}
var k = e.getOptions().text;
k && p.push('<text x="' + c + '" y="' + (c + 7) + '" style="font-size:16px;font-family:arial;fill:#000;font-weight:bold;" class="' + (t.textClassName || "") + '" text-anchor="middle">' + k + "</text>"),
p.push("</svg>"),
b.prototype.getOptions.call(this).htmlContent.innerHTML = p.join("")
}
}
,
C.prototype._createSlice = function(e, t, r, o, a, i, n, s) {
var l = this._options
, u = Math.PI
, c = "";
if (s && (c = ' mask="url(#' + s + '"'),
2 * u * .99 < a)
return '<circle r="' + r + '" cx="' + e + '" cy="' + t + '" style="fill:' + i + ";stroke:" + l.strokeColor + ";stroke-width:" + l.strokeWidth + 'px;"' + c + "><title>" + n + "</title></circle>";
var p = Math.sin
, d = Math.cos
, m = e + r * p(o)
, h = t - r * d(o)
, f = e + r * p(o + a)
, v = t - r * d(o + a)
, _ = (l.innerRadius,
p(o),
l.innerRadius,
d(o),
l.innerRadius,
p(o + a),
l.innerRadius,
d(o + a),
0);
return u < a && (_ = 1),
'<path d="M' + e + " " + t + " L " + m + " " + h + " A " + r + "," + r + " 0 " + _ + " 1 " + f + " " + v + 'z" style="fill:' + i + ";stroke:" + l.strokeColor + ";stroke-width:" + l.strokeWidth + 'px;"' + c + "><title>" + n + "</title></path>"
}
,
C._moreColors = [],
C);
function C(e) {
var t = b.call(this, e) || this;
return t._options = {
values: [],
radius: 40,
colors: ["#d7191c", "#fdae61", "#ffffbf", "#abdda4", "#2b83ba"],
fillColor: "transparent",
strokeWidth: 0,
strokeColor: "#666666",
innerRadius: 0
},
t._total = 0,
t.properties = {},
b.prototype.setOptions.call(t, {
htmlContent: document.createElement("div"),
pixelOffset: [0, 0],
anchor: "center"
}),
t.setOptions(e),
t
}
var d = r.merge("atlas.layer", c);
e.PieChartMarker = p,
e.layer = d
}(this.atlas = this.atlas || {}, atlas);
</script>
</head>
<style>
html,
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
font-family: segoeui;
}
#mapContainer {
position: relative;
width: 100%;
height: 100%;
}
</style>
<body>
<div id="mapContainer"></div>
<script>
var map;
var symbolLayers = [];
var layersData = {};
var pointsData = {};
var dataSource;
var id = 1;
var controls = [];
function initializeMap(longitude, latitude, mapServiceToken) {
try {
//Initialize a map instance.
map = new atlas.Map('mapContainer', {
center: [longitude, latitude],
zoom: 3,
view: 'Auto',
//Add authentication details for connecting to Azure Maps.
// Note: an invalid token doesn't seem to throw any errors.
authOptions: {
authType: 'subscriptionKey',
subscriptionKey: mapServiceToken
}
});
map.events.add('ready', function () {
dataSource = new atlas.source.DataSource();
map.sources.add(dataSource);
});
}
catch (error) {
sendErrorInfo(error);
}
}
function addSymbolLayer() {
try {
var layerSource = new atlas.source.DataSource();
var newLayer = new atlas.layer.HtmlMarkerLayer(layerSource, null, {
markerCallback: (id, position, properties) => {
const pin = new atlas.HtmlMarker({
position: position,
id: id
});
pin.getElement().setAttribute('tabindex', 0);
return pin;
}
});
layersData[newLayer.getId()] = layerSource;
pointsData[newLayer.getId()] = {};
symbolLayers.push(newLayer);
map.events.add('ready', function () {
map.sources.add(layerSource);
map.layers.add(newLayer);
map.events.add('click', newLayer, sendPushpinClickInfo);
});
}
catch (error) {
sendErrorInfo(error);
}
return newLayer.getId();
}
function addPoint(longitude, latitude, layerId) {
var point = new atlas.data.Feature(new atlas.data.Point([longitude, latitude]), {name: 'MapElement'}, id);
id++;
try {
map.events.add('ready', function () {
layersData[layerId].add([point]);
pointsData[layerId][point.id] = point;
});
}
catch (error) {
sendErrorInfo(error);
}
return point.id;
}
function updatePoint(longitude, latitude, pointId, layerId) {
try {
map.events.add('ready', function () {
pointsData[layerId][pointId].setCoordinates([longitude, latitude]);
logSymbolLayers();
});
}
catch (error) {
sendErrorInfo(error);
}
}
function removePoint(pointId, layerId) {
try {
map.events.add('ready', function() {
layersData[layerId].remove(pointsData[layerId][pointId]);
delete pointsData[layerId][pointId];
});
}
catch (error) {
sendErrorInfo(error);
}
}
function clearLayer(layerId) {
try {
map.events.add( 'ready', function() {
layersData[layerId].clear();
pointsData[layerId] = {};
});
}
catch (error) {
sendErrorInfo(error);
}
}
function clearMap() {
try {
map.events.add( 'ready', function() {
symbolLayers = [];
layersData = {};
pointsData = {};
});
}
catch (error) {
sendErrorInfo(error);
}
}
function interactiveControlsVisible(enable) {
if (enable) {
controls = [
new atlas.control.ZoomControl(),
new atlas.control.CompassControl(),
new atlas.control.PitchControl(),
new atlas.control.StyleControl()
]
map.controls.add(controls, {
position: "top-right"
});
} else {
map.controls.remove(controls);
controls = [];
}
}
function updateCenter(latitude, longitude) {
try {
map.setCamera({
center: [longitude, latitude]
});
}
catch (error) {
sendErrorInfo(error);
}
}
function updateZoom(level) {
try {
map.setCamera({
zoom: level
});
}
catch (error) {
sendErrorInfo(error);
}
}
function updateMapServiceToken(mapServiceToken) {
try {
clearMap();
var location = map.getCamera().center;
longitude = location[0];
latitude = location[1];
// create a new map with the new API key
initializeMap(longitude, latitude, mapServiceToken);
} catch (error) {
sendErrorInfo(error);
}
}
function sendPushpinClickInfo(e) {
try {
const latitude = e.target.options.position[0];
const longitude = e.target.options.position[1];
const pointClicked = e.target.id || 'Unknown';
const coordinate = {
latitude,
longitude,
};
const message = {
type: 'pushpinClickEvent',
coordinate,
layer: 0,
point: pointClicked,
text: 'Pushpin clicked!',
};
console.log('Sending message:', message);
window.chrome.webview.postMessage(message);
} catch (error) {
sendErrorInfo(error);
}
}
function getMapLayers() {
try {
layers = map.layers.getLayers();
console.log("Number of layers: " + layers.length + "\n");
// print out the name and type of each layer
for (var i = 0; i < layers.length; i++) {
if (layers[i] instanceof atlas.layer.SymbolLayer) {
console.log(layers[i].getId() + " is a SymbolLayer\n");
console.log("Source: " + layers[i].getSource() + "\n");
}
}
}
catch (error) {
sendErrorInfo(error);
}
}
function logSymbolLayers() {
try {
// print out each symbolLayer in symbolLayers by id
console.log("Number of symbol layers: " + symbolLayers.length + "\n");
for (var i = 0; i < symbolLayers.length; i++) {
console.log("Id: " + symbolLayers[i].getId() + "\n");
}
}
catch (error) {
sendErrorInfo(error);
}
}
function getSymbolLayers() {
return symbolLayers;
}
function sendErrorInfo(e) {
console.log(e);
const coordinate = {
"latitude": 0,
"longitude": 0,
}
const message = {
"type": "javascriptError",
"coordinate": coordinate,
"text": e.stack
};
window.chrome.webview.postMessage(message);
}
</script>
</body>
</html>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More