wip: Windows desktop app scaffold (WinUI 3/.NET 8) + NSIS installer + docs
Owner decision pending (STATE.md DC-100 tick): adopt/ship/park. Preserved from fragile git stash to named branch 2026-08-23. NOTE: requires a Windows build machine (WinUI XAML compiler + MSIX do not cross-build on Linux) — see WINDOWS_APP_BUILD.md in this tree. Secret-scanned clean 2026-08-23 (no keys/tokens/PEM in tree).
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using DashCaddy.Desktop.ViewModels;
|
||||
using DashCaddy.Desktop.Services;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DashCaddy.Desktop;
|
||||
|
||||
public sealed partial class AddServiceDialog : ContentDialog
|
||||
{
|
||||
public MainViewModel ViewModel { get; }
|
||||
|
||||
public AddServiceDialog(MainViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
InitializeComponent();
|
||||
LoadTemplates();
|
||||
}
|
||||
|
||||
private void LoadTemplates()
|
||||
{
|
||||
var templates = TemplateRegistry.GetAll().ToList();
|
||||
TemplateCombo.ItemsSource = templates;
|
||||
if (templates.Count > 0)
|
||||
TemplateCombo.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void AddMethod_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (AddMethodRadio.SelectedItem is RadioButton rb)
|
||||
{
|
||||
var method = rb.Tag?.ToString();
|
||||
TemplatePanel.Visibility = method == "template" ? Visibility.Visible : Visibility.Collapsed;
|
||||
ComposePanel.Visibility = method == "compose" ? Visibility.Visible : Visibility.Collapsed;
|
||||
CustomPanel.Visibility = method == "custom" ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void TemplateCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (TemplateCombo.SelectedItem is ServiceTemplate template)
|
||||
{
|
||||
ViewModel.TemplateVariables = template.RequiredVariables.Length > 0 || template.Environment.Count > 0
|
||||
? BuildVariables(template)
|
||||
: new List<VariableViewModel>();
|
||||
VariablesPanel.Visibility = ViewModel.TemplateVariables.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private List<VariableViewModel> BuildVariables(ServiceTemplate template)
|
||||
{
|
||||
var vars = new List<VariableViewModel>();
|
||||
|
||||
foreach (var (key, value) in template.Environment)
|
||||
{
|
||||
vars.Add(new VariableViewModel
|
||||
{
|
||||
Key = key,
|
||||
Value = value,
|
||||
Placeholder = key,
|
||||
Description = $"Environment variable: {key}"
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var required in template.RequiredVariables)
|
||||
{
|
||||
if (!vars.Exists(v => v.Key == required))
|
||||
{
|
||||
vars.Add(new VariableViewModel
|
||||
{
|
||||
Key = required,
|
||||
Value = "",
|
||||
Placeholder = required,
|
||||
Description = $"Required: {required}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
|
||||
private async void BrowseCompose_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var picker = new Windows.Storage.Pickers.FileOpenPicker();
|
||||
picker.FileTypeFilter.Add(".yaml");
|
||||
picker.FileTypeFilter.Add(".yml");
|
||||
|
||||
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
|
||||
|
||||
var file = await picker.PickSingleFileAsync();
|
||||
if (file != null)
|
||||
{
|
||||
ComposePathText.Text = file.Path;
|
||||
ViewModel.SelectedComposeFile = file.Path;
|
||||
}
|
||||
}
|
||||
|
||||
private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
if (AddMethodRadio.SelectedItem is RadioButton rb)
|
||||
{
|
||||
var method = rb.Tag?.ToString();
|
||||
|
||||
switch (method)
|
||||
{
|
||||
case "template":
|
||||
if (TemplateCombo.SelectedItem is ServiceTemplate template)
|
||||
{
|
||||
var variables = new Dictionary<string, string>();
|
||||
foreach (var v in ViewModel.TemplateVariables)
|
||||
{
|
||||
variables[v.Key] = v.Value;
|
||||
}
|
||||
ViewModel.SelectedTemplate = template;
|
||||
ViewModel.TemplateVariablesDict = variables;
|
||||
}
|
||||
else
|
||||
{
|
||||
args.Cancel = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "compose":
|
||||
if (string.IsNullOrEmpty(ViewModel.SelectedComposeFile))
|
||||
{
|
||||
args.Cancel = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "custom":
|
||||
if (string.IsNullOrWhiteSpace(CustomName.Text) || string.IsNullOrWhiteSpace(CustomImage.Text))
|
||||
{
|
||||
args.Cancel = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewModel.CustomService = new
|
||||
{
|
||||
Name = CustomName.Text,
|
||||
Image = CustomImage.Text,
|
||||
Port = int.TryParse(CustomPort.Text, out var p) ? p : 80,
|
||||
Domain = CustomDomain.Text,
|
||||
Volumes = CustomVolumes.Text.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(v => v.Trim()).ToList(),
|
||||
Environment = ParseEnv(CustomEnv.Text)
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ParseEnv(string text)
|
||||
{
|
||||
var dict = new Dictionary<string, string>();
|
||||
foreach (var line in text.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var parts = line.Split('=', 2);
|
||||
if (parts.Length == 2)
|
||||
dict[parts[0].Trim()] = parts[1].Trim();
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
|
||||
public class VariableViewModel
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string Placeholder { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user