diff --git a/Proxmox Desktop/Api/ApiClient.cs b/Proxmox Desktop/Api/ApiClient.cs index 215c2b9..5f52d96 100644 --- a/Proxmox Desktop/Api/ApiClient.cs +++ b/Proxmox Desktop/Api/ApiClient.cs @@ -41,6 +41,9 @@ public ApiClient(ServerInfo info) public ApiClient(string server, string port, bool skipSsl) : this(new ServerInfo(server, int.Parse(port), skipSsl)) { } + /// Hostname/IP this client is connected to — used as the friendly server name. + public string Host => _http.BaseAddress!.Host; + // ─── Auth ───────────────────────────────────────────────────────────────────────── public async Task> GetRealmsAsync(CancellationToken ct = default) diff --git a/Proxmox Desktop/Api/Models/MachineData.cs b/Proxmox Desktop/Api/Models/MachineData.cs index c1c01b8..1a9e77e 100644 --- a/Proxmox Desktop/Api/Models/MachineData.cs +++ b/Proxmox Desktop/Api/Models/MachineData.cs @@ -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"; diff --git a/Proxmox Desktop/App.xaml.cs b/Proxmox Desktop/App.xaml.cs index 7d3c90d..06bd184 100644 --- a/Proxmox Desktop/App.xaml.cs +++ b/Proxmox Desktop/App.xaml.cs @@ -1,4 +1,6 @@ using System.Windows; +using MaterialDesignThemes.Wpf; +using ProxmoxDesktop.Config; using ProxmoxDesktop.Services; namespace ProxmoxDesktop; @@ -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(); diff --git a/Proxmox Desktop/Controls/MachineCard.xaml b/Proxmox Desktop/Controls/MachineCard.xaml index 2e4b7e5..199a44f 100644 --- a/Proxmox Desktop/Controls/MachineCard.xaml +++ b/Proxmox Desktop/Controls/MachineCard.xaml @@ -5,10 +5,12 @@ xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:converters="clr-namespace:ProxmoxDesktop.Converters"> - - - - + + + + + + + + + @@ -47,12 +63,25 @@ FontWeight="Medium" VerticalAlignment="Center"/> + + + + @@ -62,8 +91,8 @@ - - - + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - diff --git a/Proxmox Desktop/Views/MainWindow.xaml.cs b/Proxmox Desktop/Views/MainWindow.xaml.cs index 56cce82..b7cf3c6 100644 --- a/Proxmox Desktop/Views/MainWindow.xaml.cs +++ b/Proxmox Desktop/Views/MainWindow.xaml.cs @@ -1,7 +1,8 @@ using System.Windows; +using System.Windows.Input; using MaterialDesignThemes.Wpf; using ProxmoxDesktop.Api; -using ProxmoxDesktop.Api.Models; +using ProxmoxDesktop.Config; using ProxmoxDesktop.Console; using ProxmoxDesktop.ViewModels; @@ -12,18 +13,31 @@ public partial class MainWindow : Window public static MainWindow? Instance { get; private set; } public ISnackbarMessageQueue SnackbarService => MainSnackbar.MessageQueue!; - private readonly MainViewModel _vm; - private bool _isDark = true; + private readonly MainViewModel _vm; + private readonly ConfigurationService _config = new(); + private bool _isDark; public MainWindow(IApiClient api) { InitializeComponent(); - Instance = this; - _vm = new MainViewModel((ApiClient)api); + Instance = this; + + _isDark = _config.Config.IsDarkTheme; + ApplyTheme(); + + _vm = new MainViewModel((ApiClient)api, _config.Config.RefreshIntervalSeconds); DataContext = _vm; + _vm.OnLogout += () => { _vm.Dispose(); Instance = null; new LoginWindow().Show(); Close(); }; _vm.OnOpenConsole += (m, url) => new ConsoleWindow(m, url).Show(); _vm.OnOpenSpice += async cfg => await SpiceLauncher.LaunchAsync(cfg); + _vm.OnNotify += msg => Dispatcher.Invoke(() => SnackbarService.Enqueue(msg)); + _vm.OnRequestAddServer += () => + { + var dlg = new LoginWindow(connectedApi => _vm.AddConnection(connectedApi)) { Owner = this }; + dlg.ShowDialog(); + }; + Loaded += async (_, _) => await _vm.RefreshAsync(); Closed += (_, _) => { _vm.Dispose(); Instance = null; }; } @@ -31,10 +45,27 @@ public MainWindow(IApiClient api) private void OnThemeToggle(object sender, RoutedEventArgs e) { _isDark = !_isDark; + ApplyTheme(); + _config.Config.IsDarkTheme = _isDark; + _config.Save(); + } + + private void ApplyTheme() + { var helper = new PaletteHelper(); var theme = helper.GetTheme(); theme.SetBaseTheme(_isDark ? BaseTheme.Dark : BaseTheme.Light); helper.SetTheme(theme); ThemeIcon.Kind = _isDark ? PackIconKind.WeatherNight : PackIconKind.WeatherSunny; } + + private void OnWindowPreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.F && Keyboard.Modifiers == ModifierKeys.Control) + { + SearchBox.Focus(); + SearchBox.SelectAll(); + e.Handled = true; + } + } } diff --git a/README.md b/README.md index d5a7ff5..e87b9a6 100644 --- a/README.md +++ b/README.md @@ -29,16 +29,21 @@ Built with **WPF** and **Material Design**, using an **MVVM** architecture (Comm ## Features -- 🖥️ **VM/LXC Dashboard** — Grid view of all machines with real-time status, CPU%, RAM%, uptime +- 🖥️ **VM/LXC Dashboard** — Grid **or list** view of all machines with real-time status, CPU%, RAM%, uptime +- 🌐 **Multi-server** — Connect several Proxmox clusters at once and see them side by side - 📊 **Stats bar** — Total / Running / Stopped / VMs / LXC at a glance -- 🗂️ **Node sidebar** — Filter by node with a single click -- 🔍 **Search** — Filter by name, VMID or node +- 🗂️ **Server & node sidebar** — Filter by cluster or node with a single click +- 🏷️ **Proxmox tags** — Shown on each card, click a tag to filter +- 🧭 **Sorting** — By name, VMID, CPU, RAM, status or uptime +- 🕑 **Activity log** — Side panel recording power actions, state changes and errors +- 🔍 **Search** — Filter by name, VMID, node or server - 🖥️ **Integrated console** — NoVNC, xTermJS and SPICE (Virt-Viewer) - ⚡ **Power control** — Start / Shutdown / Reboot / Suspend / Hibernate / Force Stop / Reset - 🔐 **Dual authentication** — Classic login + TOTP, or Proxmox API Token - 🔔 **Windows notifications** — Native toast on VM state changes -- 🔄 **Auto-refresh** — Every 60 seconds, ticket renewed automatically every 90 min -- 🌙 **Dark / Light theme** — Toggle in one click +- ⌨️ **Keyboard shortcuts** — `F5` refresh · `Ctrl+F` search · `Esc` clear +- 🔄 **Auto-refresh** — Configurable interval, ticket renewed automatically every 90 min +- 🌙 **Dark / Light theme** — Toggle in one click, remembered between sessions --- @@ -199,10 +204,12 @@ Proxmox-Desktop/ - [x] Group by node - [x] Windows Toast notifications - [x] Dark / Light theme -- [ ] Multi-server support (multiple Proxmox clusters) -- [ ] List view in addition to grid view -- [ ] Proxmox tags displayed on cards -- [ ] Action history / logs +- [x] Multi-server support (multiple Proxmox clusters) +- [x] List view in addition to grid view +- [x] Proxmox tags displayed on cards +- [x] Action history / logs +- [ ] Bulk power actions on multiple machines +- [ ] Per-VM resource graphs (history) ---