Initial import

This commit is contained in:
2025-04-02 08:33:20 +02:00
commit 2b1e4def50
11 changed files with 948 additions and 0 deletions

396
.gitignore vendored Normal file
View File

@ -0,0 +1,396 @@
### Csharp ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
#*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml

View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35919.96 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proxmox-spice-launcher", "src\proxmox-spice-launcher\proxmox-spice-launcher\proxmox-spice-launcher.csproj", "{9A4EC4BE-8920-461F-8653-A9650A68B727}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9A4EC4BE-8920-461F-8653-A9650A68B727}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9A4EC4BE-8920-461F-8653-A9650A68B727}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A4EC4BE-8920-461F-8653-A9650A68B727}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9A4EC4BE-8920-461F-8653-A9650A68B727}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {18005A22-977A-47D6-AA19-915753D1F935}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,69 @@
<Application x:Class="Proxmox.SpiceLauncher.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Proxmox.SpiceLauncher"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
Startup="Application_Startup">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- MahApps.Metro resource dictionaries. Make sure that all file names are Case Sensitive! -->
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<!-- Theme setting -->
<ResourceDictionary
Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Dark.Orange.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style x:Key="MahApps.Styles.MetroHeader.Horizontal"
BasedOn="{StaticResource MahApps.Styles.MetroHeader}"
TargetType="mah:MetroHeader">
<Setter Property="Padding" Value="0 2" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="mah:MetroHeader">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="HeaderGroup" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Background="{TemplateBinding mah:HeaderedControlHelper.HeaderBackground}">
<mah:ContentControlEx x:Name="PART_Header"
Margin="{TemplateBinding mah:HeaderedControlHelper.HeaderMargin}"
HorizontalAlignment="{TemplateBinding mah:HeaderedControlHelper.HeaderHorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding mah:HeaderedControlHelper.HeaderVerticalContentAlignment}"
Content="{TemplateBinding Header}"
ContentCharacterCasing="{TemplateBinding mah:ControlsHelper.ContentCharacterCasing}"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
FontFamily="{TemplateBinding mah:HeaderedControlHelper.HeaderFontFamily}"
FontSize="{TemplateBinding mah:HeaderedControlHelper.HeaderFontSize}"
FontStretch="{TemplateBinding mah:HeaderedControlHelper.HeaderFontStretch}"
FontWeight="{TemplateBinding mah:HeaderedControlHelper.HeaderFontWeight}"
Foreground="{TemplateBinding mah:HeaderedControlHelper.HeaderForeground}"
IsTabStop="False"
RecognizesAccessKey="{TemplateBinding mah:ControlsHelper.RecognizesAccessKey}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
<Grid Grid.Column="1" Background="{TemplateBinding Background}">
<ContentPresenter x:Name="PART_Content"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
ContentSource="Content"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="mah:HeaderedControlHelper.HeaderMargin" Value="0 0 4 0" />
<Setter Property="mah:HeaderedControlHelper.HeaderVerticalContentAlignment" Value="Center" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@ -0,0 +1,15 @@
using System.Windows;
namespace Proxmox.SpiceLauncher;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App
{
private void Application_Startup(object sender, StartupEventArgs e)
{
var mainWindow = new MainWindow();
mainWindow.Show();
}
}

View File

@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@ -0,0 +1,115 @@
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace Proxmox.SpiceLauncher;
static class DPApi
{
private static int EntropySize = 16;
public static byte[] CreateRandomEntropy()
{
// Create a byte array to hold the random value.
var entropy = new byte[EntropySize];
// Create a new instance of the RNGCryptoServiceProvider.
// Fill the array with a random value.
RandomNumberGenerator.Create().GetBytes(entropy);
// Return the array.
return entropy;
}
public static byte[] EncryptString(string s)
{
var data = System.Text.Encoding.UTF8.GetBytes(s);
var ms = new MemoryStream();
var entropy = CreateRandomEntropy();
ms.Write(entropy);
EncryptDataToStream(data, entropy, DataProtectionScope.CurrentUser, ms);
return ms.ToArray();
}
public static string EncryptStringToBase64(string s)
{
var data = EncryptString(s);
return Convert.ToBase64String(data);
}
public static string DecryptString(byte[] data)
{
var ms = new MemoryStream(data);
var entropy = new byte[EntropySize];
var entropySize = ms.Read(entropy);
if (entropySize != EntropySize)
throw new IOException("Entropy size was not correct.");
var dec = DecryptDataFromStream(entropy, DataProtectionScope.CurrentUser, ms, data.Length - entropySize);
return Encoding.UTF8.GetString(dec);
}
public static string DecryptStringFromBase64(string s)
{
var data = Convert.FromBase64String(s);
return DecryptString(data);
}
public static int EncryptDataToStream(byte[] buffer, byte[] entropy, DataProtectionScope scope, Stream s)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (buffer.Length <= 0)
throw new ArgumentException("The buffer length was 0.", nameof(buffer));
if (entropy == null)
throw new ArgumentNullException(nameof(entropy));
if (entropy.Length <= 0)
throw new ArgumentException("The entropy length was 0.", nameof(entropy));
if (s == null)
throw new ArgumentNullException(nameof(s));
var length = 0;
// Encrypt the data and store the result in a new byte array. The original data remains unchanged.
var encryptedData = ProtectedData.Protect(buffer, entropy, scope);
// Write the encrypted data to a stream.
if (s.CanWrite && encryptedData != null)
{
s.Write(encryptedData, 0, encryptedData.Length);
length = encryptedData.Length;
}
// Return the length that was written to the stream.
return length;
}
public static byte[] DecryptDataFromStream(byte[] entropy, DataProtectionScope scope, Stream s, int length)
{
if (s == null)
throw new ArgumentNullException(nameof(s));
if (length <= 0)
throw new ArgumentException("The given length was 0.", nameof(length));
if (entropy == null)
throw new ArgumentNullException(nameof(entropy));
if (entropy.Length <= 0)
throw new ArgumentException("The entropy length was 0.", nameof(entropy));
var inBuffer = new byte[length];
byte[] outBuffer;
// Read the encrypted data from a stream.
if (s.CanRead)
{
s.Read(inBuffer, 0, length);
outBuffer = ProtectedData.Unprotect(inBuffer, entropy, scope);
}
else
{
throw new IOException("Could not read the stream.");
}
// Return the decrypted data
return outBuffer;
}
}

View File

@ -0,0 +1,47 @@
<mah:MetroWindow x:Class="Proxmox.SpiceLauncher.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:local="clr-namespace:Proxmox.SpiceLauncher"
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
mc:Ignorable="d"
Title="Proxmox Spice Launcher" Height="391" Width="350" ResizeMode="NoResize"
Initialized="MetroWindow_Initialized" Closing="MetroWindow_Closing" SizeToContent="Height"
Icon="pack://application:,,,/Proxmox.SpiceLauncher.png">
<mah:MetroWindow.Resources>
<Style BasedOn="{StaticResource MahApps.Styles.MetroHeader.Horizontal}" TargetType="mah:MetroHeader" />
</mah:MetroWindow.Resources>
<mah:MetroWindow.IconTemplate>
<DataTemplate>
<!-- Setting a Margin and enable hight-quality image -->
<Image Margin="4"
RenderOptions.BitmapScalingMode="HighQuality"
Source="{Binding}" />
</DataTemplate>
</mah:MetroWindow.IconTemplate>
<mah:MetroWindow.RightWindowCommands>
<mah:WindowCommands>
<Button x:Name="ButtonSettings" Content="{iconPacks:FontAwesome GearSolid}" Click="ButtonSettings_Click" />
</mah:WindowCommands>
</mah:MetroWindow.RightWindowCommands>
<Grid>
<StackPanel Margin="10,10,10,10">
<StackPanel VerticalAlignment="Stretch" Orientation="Horizontal">
<mah:MetroHeader Header="Profile">
<ComboBox x:Name="ComboProfiles" SelectionChanged="ComboProfiles_OnSelectionChanged" />
</mah:MetroHeader>
<Button x:Name="ButtonConnect" Content="Connect" Click="ButtonConnect_Click" Margin="5,0,0,0" />
</StackPanel>
<StackPanel VerticalAlignment="Stretch" Orientation="Horizontal">
<mah:MetroHeader Header="VM">
<ComboBox x:Name="ComboVms" />
</mah:MetroHeader>
<Button x:Name="ButtonSpice" Content="Spice" Click="ButtonSpice_Click" IsEnabled="False" Margin="5,0,0,0" RenderTransformOrigin="4.474,0.491" />
</StackPanel>
</StackPanel>
</Grid>
</mah:MetroWindow>

View File

@ -0,0 +1,210 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
using Corsinvest.ProxmoxVE.Api;
using Corsinvest.ProxmoxVE.Api.Extension;
using Corsinvest.ProxmoxVE.Api.Shared.Models.Cluster;
using Corsinvest.ProxmoxVE.Api.Shared.Models.Vm;
using MahApps.Metro.Controls.Dialogs;
using Proxmox.SpiceLauncher.Models;
namespace Proxmox.SpiceLauncher;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow
{
private PveClient? _client;
private List<Settings> _settings;
private List<VmWrapper> _vms = new();
private readonly string _settingsFile;
public MainWindow()
{
_settingsFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ProxmoxSpiceLauncher", "settings.json");
InitializeComponent();
}
private async void MetroWindow_Initialized(object sender, EventArgs e)
{
List<Settings>? settings = null;
if (File.Exists(_settingsFile))
{
await using var f = File.OpenRead(_settingsFile);
settings = await JsonSerializer.DeserializeAsync<List<Settings>>(f);
}
if (settings == null || settings.Count == 0)
{
settings =
[
new Settings()
{
Server = "192.168.0.190"
}
];
}
_settings = settings;
ComboProfiles.ItemsSource = _settings;
ComboProfiles.SelectedIndex = 0;
ComboVms.ItemsSource = _vms;
}
private void ButtonSettings_Click(object sender, RoutedEventArgs e)
{
/*
var settings = new SettingsWindow
{
Owner = this
};
settings.ShowDialog();
*/
}
private async Task DoEvent(Func<Task> e)
{
try
{
await e();
}
catch (Exception ex)
{
await this.ShowMessageAsync("Error", ex.Message);
}
}
private async Task DoConnect()
{
_client = null;
_vms.Clear();
ButtonSpice.IsEnabled = false;
if (ComboProfiles.SelectedItem is not Settings settings)
{
return;
}
_client = new PveClient(settings.Server);
try
{
if (string.IsNullOrWhiteSpace(settings.Username) || string.IsNullOrWhiteSpace(settings.Password))
{
var login = await this.ShowLoginAsync("Login", $"Login to {settings.Server}", new LoginDialogSettings
{
InitialUsername = settings.Username,
InitialPassword = settings.Password
});
settings.Username = login.Username;
settings.Password = login.Password;
}
if (!await _client.LoginAsync(settings.Username, settings.Password))
{
settings.Password = "";
throw new Exception("Login failed");
}
var vms = await _client.GetVmsAsync();
foreach (var vm in vms)
{
if (!vm.IsRunning)
{
continue;
}
if (vm.VmType != VmType.Qemu)
{
continue;
}
var config = await _client.GetVmConfigAsync(vm.Node, vm.VmType, vm.VmId) as VmConfigQemu;
if (config?.Vga != null && config.Vga.StartsWith("qxl"))
{
_vms.Add(new VmWrapper(vm));
}
}
if (_vms.Count > 0)
{
ComboVms.SelectedIndex = 0;
ButtonSpice.IsEnabled = true;
}
}
catch
{
_client = null;
throw;
}
}
private async void ButtonConnect_Click(object sender, RoutedEventArgs e)
{
await DoEvent(DoConnect);
}
private async Task DoSpice()
{
if (_client == null)
{
return;
}
if (ComboVms.SelectedItem is not VmWrapper vm)
{
return;
}
var (success, reasonPhrase, content) = await _client.Nodes[vm.Vm.Node].Qemu[vm.Vm.VmId].Spiceproxy.GetSpiceFileVVAsync(_client.Host);
if (!success)
{
throw new Exception(reasonPhrase);
}
var tempFile = Path.ChangeExtension(Path.GetTempFileName(), ".vv");
await File.WriteAllTextAsync(tempFile, content);
var startInfo = new ProcessStartInfo(tempFile)
{
UseShellExecute = true
};
Process.Start(startInfo);
}
private async void ButtonSpice_Click(object sender, RoutedEventArgs e)
{
await DoEvent(DoSpice);
}
private async void MetroWindow_Closing(object? sender, CancelEventArgs e)
{
var dir = Path.GetDirectoryName(_settingsFile);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
await File.WriteAllTextAsync(_settingsFile, JsonSerializer.Serialize(_settings));
}
private void ComboProfiles_OnSelectionChanged(object sender, RoutedEventArgs e)
{
_vms.Clear();
_client = null;
ButtonSpice.IsEnabled = false;
}
}
internal class VmWrapper
{
public VmWrapper(IClusterResourceVm vm)
{
Vm = vm;
}
public IClusterResourceVm Vm { get; set; }
public override string ToString()
{
return $"{Vm.VmId} {Vm.Name}";
}
}

View File

@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
namespace Proxmox.SpiceLauncher.Models;
class Settings
{
private string _password;
private string _passwordEnc;
public string Server { get; set; }
public string Username { get; set; }
[JsonIgnore]
public string Password
{
get => _password;
set
{
_password = value;
_passwordEnc = DPApi.EncryptStringToBase64(value);
}
}
public string PasswordEnc
{
get => _passwordEnc;
set
{
_passwordEnc = value;
_password = DPApi.DecryptStringFromBase64(value);
}
}
public override string ToString()
{
return Server;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<RootNamespace>Proxmox.SpiceLauncher</RootNamespace>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Corsinvest.ProxmoxVE.Api" Version="8.3.3" />
<PackageReference Include="Corsinvest.ProxmoxVE.Api.Extension" Version="8.3.3" />
<PackageReference Include="MahApps.Metro" Version="2.4.10" />
<PackageReference Include="MahApps.Metro.IconPacks.FontAwesome" Version="5.1.0" />
</ItemGroup>
<ItemGroup>
<Resource Include="Proxmox.SpiceLauncher.png" />
</ItemGroup>
</Project>