Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Proxmox Desktop/Api/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public ApiClient(ServerInfo info)
public ApiClient(string server, string port, bool skipSsl)
: this(new ServerInfo(server, int.Parse(port), skipSsl)) { }

/// <summary>Hostname/IP this client is connected to — used as the friendly server name.</summary>
public string Host => _http.BaseAddress!.Host;

// ─── Auth ─────────────────────────────────────────────────────────────────────────

public async Task<List<RealmData>> GetRealmsAsync(CancellationToken ct = default)
Expand Down
7 changes: 4 additions & 3 deletions Proxmox Desktop/Api/Models/MachineData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ public record MachineData
[JsonPropertyName("serial")] public int Serial { get; init; }
[JsonPropertyName("tags")] public string? Tags { get; init; }

// Set by ApiClient after deserialization
public string NodeName { get; init; } = string.Empty;
public string Type { get; init; } = string.Empty;
// Set by ApiClient / ViewModel after deserialization
public string NodeName { get; init; } = string.Empty;
public string Type { get; init; } = string.Empty;
public string ServerName { get; init; } = string.Empty;

// Computed
public bool IsRunning => Status == "running";
Expand Down
16 changes: 16 additions & 0 deletions Proxmox Desktop/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Windows;
using MaterialDesignThemes.Wpf;
using ProxmoxDesktop.Config;
using ProxmoxDesktop.Services;

namespace ProxmoxDesktop;
Expand All @@ -9,9 +11,23 @@ protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
NotificationService.Enable();
ApplySavedTheme();
new Views.LoginWindow().Show();
}

private static void ApplySavedTheme()
{
try
{
var cfg = new ConfigurationService().Config;
var helper = new PaletteHelper();
var theme = helper.GetTheme();
theme.SetBaseTheme(cfg.IsDarkTheme ? BaseTheme.Dark : BaseTheme.Light);
helper.SetTheme(theme);
}
catch { /* fall back to the default theme from App.xaml */ }
}

protected override void OnExit(ExitEventArgs e)
{
NotificationService.Disable();
Expand Down
35 changes: 30 additions & 5 deletions Proxmox Desktop/Controls/MachineCard.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:ProxmoxDesktop.Converters">
<UserControl.Resources>
<converters:StatusToBrushConverter x:Key="StatusToBrush"/>
<converters:BytesToReadableConverter x:Key="BytesToReadable"/>
<converters:CpuPercentConverter x:Key="CpuPercent"/>
<BooleanToVisibilityConverter x:Key="BoolToVisibility"/>
<converters:StatusToBrushConverter x:Key="StatusToBrush"/>
<converters:BytesToReadableConverter x:Key="BytesToReadable"/>
<converters:CpuPercentConverter x:Key="CpuPercent"/>
<converters:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisibility"/>
<converters:CollectionToVisibilityConverter x:Key="CollectionToVisibility"/>
<BooleanToVisibilityConverter x:Key="BoolToVisibility"/>
<Style x:Key="ActionBtn" TargetType="Button" BasedOn="{StaticResource MaterialDesignIconForegroundButton}">
<Setter Property="Width" Value="32"/>
<Setter Property="Height" Value="32"/>
Expand Down Expand Up @@ -82,6 +84,29 @@
FontSize="10" FontWeight="Bold" Foreground="White"/>
</Border>

<!-- Tags (click to filter) -->
<ItemsControl ItemsSource="{Binding Machine.TagList, RelativeSource={RelativeSource AncestorType=UserControl}}"
Visibility="{Binding Machine.TagList, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={StaticResource CollectionToVisibility}}"
HorizontalAlignment="Center" Margin="0,0,0,8">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><WrapPanel HorizontalAlignment="Center"/></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Command="{Binding FilterTagCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Style="{StaticResource MaterialDesignFlatButton}"
Padding="6,0" Height="20" MinWidth="0" Margin="2,0"
md:ButtonAssist.CornerRadius="10" ToolTip="Filter by this tag">
<StackPanel Orientation="Horizontal">
<md:PackIcon Kind="Tag" Width="10" Height="10" VerticalAlignment="Center" Margin="0,0,3,0"/>
<TextBlock Text="{Binding}" FontSize="10"/>
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

<!-- CPU bar -->
<Grid Margin="0,0,0,6">
<Grid.ColumnDefinitions>
Expand Down Expand Up @@ -131,7 +156,7 @@
</Button>
<Button Style="{StaticResource ActionBtn}" ToolTip="Start"
Command="{Binding StartCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
Visibility="{Binding Machine.IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={StaticResource BoolToVisibility}, ConverterParameter=inverse}">
Visibility="{Binding Machine.IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={StaticResource InverseBoolToVisibility}}">
<md:PackIcon Kind="Play" Width="16" Height="16" Foreground="#4CAF50"/>
</Button>
<Button Style="{StaticResource ActionBtn}" ToolTip="Shutdown"
Expand Down
2 changes: 2 additions & 0 deletions Proxmox Desktop/Controls/MachineCard.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public partial class MachineCard : UserControl

public string MachineIcon => Machine?.IsLxc == true ? "/Assets/lxc.png" : "/Assets/vm.png";

public IRelayCommand<string> FilterTagCommand => new RelayCommand<string>(tag => { if (!string.IsNullOrEmpty(tag)) VM?.FilterByTagCommand.Execute(tag); });

public IRelayCommand OpenNoVncCommand => new RelayCommand(() => VM?.OpenConsoleCommand.Execute(new ConsoleArgs(Machine, "novnc")));
public IRelayCommand OpenXtermCommand => new RelayCommand(() => VM?.OpenConsoleCommand.Execute(new ConsoleArgs(Machine, "xtermjs")));
public IRelayCommand OpenSpiceCommand => new RelayCommand(() => VM?.OpenSpiceCommand.Execute(Machine));
Expand Down
31 changes: 31 additions & 0 deletions Proxmox Desktop/Converters/Converters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,34 @@ public object Convert(object? value, Type t, object? p, CultureInfo l)
=> value is true ? Visibility.Collapsed : Visibility.Visible;
public object ConvertBack(object? v, Type t, object? p, CultureInfo l) => throw new NotImplementedException();
}

/// <summary>Visible when the bound collection has at least one item, otherwise Collapsed.</summary>
public sealed class CollectionToVisibilityConverter : IValueConverter
{
public object Convert(object? value, Type t, object? p, CultureInfo l)
{
if (value is System.Collections.IEnumerable e)
{
foreach (var _ in e) return Visibility.Visible;
return Visibility.Collapsed;
}
return value is null ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object? v, Type t, object? p, CultureInfo l) => throw new NotImplementedException();
}

/// <summary>
/// Maps a bool to a <see cref="GridLength"/> — true → ConverterParameter px (default 320),
/// false → 0. Used to collapse the activity panel column.
/// </summary>
public sealed class BoolToGridLengthConverter : IValueConverter
{
public object Convert(object? value, Type t, object? p, CultureInfo l)
{
var width = 320.0;
if (p is string s && double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var w))
width = w;
return value is true ? new GridLength(width) : new GridLength(0);
}
public object ConvertBack(object? v, Type t, object? p, CultureInfo l) => throw new NotImplementedException();
}
4 changes: 2 additions & 2 deletions Proxmox Desktop/ProxmoxDesktop.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
<UseWPF>true</UseWPF>
<RootNamespace>ProxmoxDesktop</RootNamespace>
<AssemblyName>ProxmoxDesktop</AssemblyName>
<AssemblyVersion>2.1.0.0</AssemblyVersion>
<InformationalVersion>2.1.0</InformationalVersion>
<AssemblyVersion>2.2.0.0</AssemblyVersion>
<InformationalVersion>2.2.0</InformationalVersion>
<Platforms>x64</Platforms>
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
Expand Down
64 changes: 64 additions & 0 deletions Proxmox Desktop/Services/ActivityLogService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Media;
using MaterialDesignThemes.Wpf;

namespace ProxmoxDesktop.Services;

public enum ActivityLevel { Info, Success, Warning, Error }

/// <summary>A single, immutable line in the activity log (UI-facing).</summary>
public sealed record ActivityEntry(DateTime Time, PackIconKind Icon, Brush Color, string Title, string? Detail)
{
public string TimeText => Time.ToString("HH:mm:ss");
}

/// <summary>
/// In-memory, bounded activity log. New entries are inserted at the top and the
/// collection is always mutated on the UI thread so it can be bound directly.
/// </summary>
public sealed class ActivityLogService
{
private const int MaxEntries = 200;

public ObservableCollection<ActivityEntry> Entries { get; } = [];

public void Info(string title, string? detail = null) => Add(ActivityLevel.Info, title, detail);
public void Success(string title, string? detail = null) => Add(ActivityLevel.Success, title, detail);
public void Warning(string title, string? detail = null) => Add(ActivityLevel.Warning, title, detail);
public void Error(string title, string? detail = null) => Add(ActivityLevel.Error, title, detail);

public void Add(ActivityLevel level, string title, string? detail = null)
{
var (icon, color) = level switch
{
ActivityLevel.Success => (PackIconKind.CheckCircle, Rgb(76, 175, 80)),
ActivityLevel.Warning => (PackIconKind.AlertCircle, Rgb(255, 152, 0)),
ActivityLevel.Error => (PackIconKind.CloseCircle, Rgb(244, 67, 54)),
_ => (PackIconKind.InformationOutline, Rgb(33, 150, 243)),
};

var entry = new ActivityEntry(DateTime.Now, icon, color, title, detail);
OnUi(() =>
{
Entries.Insert(0, entry);
while (Entries.Count > MaxEntries) Entries.RemoveAt(Entries.Count - 1);
});
}

public void Clear() => OnUi(Entries.Clear);

private static void OnUi(Action action)
{
var app = Application.Current;
if (app is not null && !app.Dispatcher.CheckAccess()) app.Dispatcher.Invoke(action);
else action();
}

private static SolidColorBrush Rgb(byte r, byte g, byte b)
{
var brush = new SolidColorBrush(Color.FromRgb(r, g, b));
brush.Freeze();
return brush;
}
}
Loading
Loading