diff --git a/App.axaml b/App.axaml index 29c910c..970ad4b 100644 --- a/App.axaml +++ b/App.axaml @@ -3,14 +3,14 @@ x:Class="SupportCompanion.App" x:DataType="vm:MainWindowViewModel" Name="Support Companion" - xmlns:local="using:SupportCompanion" + xmlns:common="using:SupportCompanion.Common" xmlns:sukiUi="clr-namespace:SukiUI;assembly=SukiUI" xmlns:vm="clr-namespace:SupportCompanion.ViewModels" xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" RequestedThemeVariant="Default"> - + diff --git a/App.axaml.cs b/App.axaml.cs index 57373d2..6432003 100644 --- a/App.axaml.cs +++ b/App.axaml.cs @@ -6,6 +6,8 @@ using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; using ObjCRuntime; +using SukiUI.Dialogs; +using SukiUI.Toasts; using SupportCompanion.Helpers; using SupportCompanion.Models; using SupportCompanion.Services; @@ -27,6 +29,7 @@ public App() public override void Initialize() { AvaloniaXamlLoader.Load(this); + RegisterAppServices(); var prefs = new AppConfigHelper(); try { @@ -76,7 +79,7 @@ private async Task InitializeCultureAsync() public override async void OnFrameworkInitializationCompleted() { - RegisterAppServices(); + //RegisterAppServices(); await InitializeCultureAsync(); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) @@ -161,6 +164,8 @@ private void RegisterAppServices() serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/CHANGELOG.md b/CHANGELOG.md index d630d98..28420b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.0] - 2024-11-06 +### Changed +- Avalonia and SukiUI has been updated. +- As part of the SukiUI update, the SukiHost has been updated to use the new scalable style of hosts. +- Mainwindow height has been slightly increased. +- Uninstall script updated with a check for root and use of Apple best practices. #50 thanks @pboushy + +### Fixed +- When a app or system update notification was clicked, the command was not run resulting in nothing happening. +- ToolTips were not being shown. + ## [1.3.0] - 2024-09-19 ### Added - A new mode for the app called `SystemProfilerApps` which allows for the app to display applications installed under `/Applications` and their version numbers as well as Architecture. This mode is useful for admins who want to see what applications are installed on the device and their version numbers. To enable this mode, set `SystemProfilerApps` to `true` in the configuration. Example configuration: diff --git a/Common/ViewLocator.cs b/Common/ViewLocator.cs new file mode 100644 index 0000000..f26b09d --- /dev/null +++ b/Common/ViewLocator.cs @@ -0,0 +1,40 @@ +using System.ComponentModel; +using Avalonia.Controls; +using Avalonia.Controls.Templates; + +namespace SupportCompanion.Common; + +public class ViewLocator : IDataTemplate +{ + private readonly Dictionary _controlCache = new(); + + public Control Build(object? data) + { + if (data is null) + return new TextBlock { Text = "Data is null." }; + + var fullName = data.GetType().FullName; + + if (string.IsNullOrWhiteSpace(fullName)) + return new TextBlock { Text = "Type has no name, or name is empty." }; + + var name = fullName.Replace("ViewModel", "View"); + var type = Type.GetType(name); + if (type is null) + return new TextBlock { Text = $"No View For {name}." }; + + if (!_controlCache.TryGetValue(data, out var res)) + { + res ??= (Control)Activator.CreateInstance(type)!; + _controlCache[data] = res; + } + + res.DataContext = data; + return res; + } + + public bool Match(object? data) + { + return data is INotifyPropertyChanged; + } +} \ No newline at end of file diff --git a/Info.plist b/Info.plist index 55bbc1f..2d2195e 100644 --- a/Info.plist +++ b/Info.plist @@ -22,7 +22,7 @@ CFBundleVersion - 1.3.0 + 1.4.0 LSMinimumSystemVersion 10.15 CFBundleExecutable @@ -32,7 +32,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.0 + 1.4.0 NSHighResolutionCapable LSUIElement diff --git a/Program.cs b/Program.cs index 15cc95f..dc26dc9 100644 --- a/Program.cs +++ b/Program.cs @@ -8,7 +8,7 @@ internal sealed class Program // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. - [STAThread] + //[STAThread] public static void Main(string[] args) { BuildAvaloniaApp() @@ -25,7 +25,12 @@ public static AppBuilder BuildAvaloniaApp() .UseReactiveUI() .With(new MacOSPlatformOptions { - ShowInDock = false, DisableDefaultApplicationMenuItems = true + ShowInDock = false, + DisableDefaultApplicationMenuItems = true + }) + .With(new AvaloniaNativePlatformOptions + { + RenderingMode = new[] { AvaloniaNativeRenderingMode.OpenGl, AvaloniaNativeRenderingMode.Metal } }); } } \ No newline at end of file diff --git a/Services/ActionsService.cs b/Services/ActionsService.cs index 37614f7..477dd4e 100644 --- a/Services/ActionsService.cs +++ b/Services/ActionsService.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using System.Diagnostics; -using SukiUI.Controls; +using Avalonia.Controls.Notifications; +using SukiUI.Toasts; using SupportCompanion.Helpers; using SupportCompanion.Interfaces; @@ -12,11 +13,14 @@ public class ActionsService : IActions private const string OpenMmcUpdates = "open munki://updates.html"; private readonly LoggerService _logger; - public ActionsService(LoggerService loggerService) + public ActionsService(LoggerService loggerService, ISukiToastManager toastManager) { _logger = loggerService; + ToastManager = toastManager; } + public ISukiToastManager ToastManager { get; } + public async Task KillAgent() { var startInfo = new ProcessStartInfo @@ -37,10 +41,20 @@ public async Task KillAgent() if (process.ExitCode != 0) { _logger.Log("ActionsService:KillAgent", $"Failed to kill agent: {error}", 2); - await SukiHost.ShowToast("Kill Agent", "Failed to kill agent"); + ToastManager.CreateSimpleInfoToast() + .WithTitle("Kill Agent") + .OfType(NotificationType.Error) + .WithContent("Failed to kill agent") + .Queue(); + } + else + { + ToastManager.CreateSimpleInfoToast() + .WithTitle("Kill Agent") + .OfType(NotificationType.Success) + .WithContent("Agent successfully killed") + .Queue(); } - - await SukiHost.ShowToast("Kill Agent", "Agent successfully killed"); } public async Task Reboot() @@ -63,7 +77,11 @@ public async Task Reboot() if (process.ExitCode != 0) { _logger.Log("ActionsService:Reboot", $"Reboot failed: {error}", 2); - await SukiHost.ShowToast("Reboot", "Reboot failed"); + ToastManager.CreateSimpleInfoToast() + .WithTitle("Reboot") + .OfType(NotificationType.Error) + .WithContent("Reboot failed") + .Queue(); } } diff --git a/Services/NotificationService.cs b/Services/NotificationService.cs index 46328ab..3396814 100644 --- a/Services/NotificationService.cs +++ b/Services/NotificationService.cs @@ -7,11 +7,10 @@ public class NotificationService : INotification { private readonly LoggerService _logger; - public NotificationService(LoggerService loggerService) + public NotificationService(LoggerService loggerService) : this() { _logger = loggerService; } - public NotificationService() { NSUserNotificationCenter.DefaultUserNotificationCenter.DidActivateNotification += (sender, args) => diff --git a/SupportCompanion.csproj b/SupportCompanion.csproj index 990790e..3fab036 100644 --- a/SupportCompanion.csproj +++ b/SupportCompanion.csproj @@ -27,19 +27,19 @@ - - - + + + - - + + - - - + + + - + diff --git a/ViewModels/ActionsViewModel.cs b/ViewModels/ActionsViewModel.cs index 5d9e997..7a7fca2 100644 --- a/ViewModels/ActionsViewModel.cs +++ b/ViewModels/ActionsViewModel.cs @@ -3,10 +3,12 @@ using System.Text.Json; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Notifications; using Avalonia.Platform.Storage; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; -using SukiUI.Controls; +using SukiUI.Dialogs; +using SukiUI.Toasts; using SupportCompanion.Interfaces; using SupportCompanion.Services; @@ -24,10 +26,13 @@ public partial class ActionsViewModel : ObservableObject, IWindowStateAware [ObservableProperty] private bool _hasUpdates; [ObservableProperty] private string _updateCount = "0"; - public ActionsViewModel(ActionsService actionsService, LoggerService loggerService) + public ActionsViewModel(ActionsService actionsService, LoggerService loggerService, + ISukiDialogManager dialogManager, ISukiToastManager toastManager) { _actionsService = actionsService; _logger = loggerService; + ToastManager = toastManager; + DialogManager = dialogManager; HideSupportButton = !App.Config.HiddenActions.Contains("Support"); HideMmcButton = !App.Config.HiddenActions.Contains("ManagedSoftwareCenter") && App.Config.MunkiMode; HideChangePasswordButton = !App.Config.HiddenActions.Contains("ChangePassword"); @@ -38,6 +43,9 @@ public ActionsViewModel(ActionsService actionsService, LoggerService loggerServi if (!App.Config.HiddenWidgets.Contains("Actions")) Dispatcher.UIThread.Post(InitializeAsync); } + public ISukiToastManager ToastManager { get; } + public ISukiDialogManager DialogManager { get; } + public bool HideSupportButton { get; private set; } public bool HideChangePasswordButton { get; private set; } public bool HideMmcButton { get; private set; } @@ -134,8 +142,11 @@ public async Task OpenChangePasswordPage() // Do we have a network connection? if (!await CheckForInternetConnection()) { - await SukiHost.ShowToast("Change Password", - "No network connection"); + ToastManager.CreateSimpleInfoToast() + .WithTitle("Change Password") + .OfType(NotificationType.Warning) + .WithContent("No network connection") + .Queue(); return; } @@ -158,19 +169,30 @@ await SukiHost.ShowToast("Change Password", // open the SSO extension with the realm name await _actionsService.RunCommandWithoutOutput($"/usr/bin/app-sso -c {realmName}"); else - await SukiHost.ShowToast("Change Password", - $"Cannot reach {realmName}, make sure to connect to VPN or corporate network."); + ToastManager.CreateSimpleInfoToast() + .WithTitle("Change Password") + .OfType(NotificationType.Warning) + .WithContent($"Cannot reach {realmName}, make sure to connect to VPN or corporate network.") + .Queue(); } catch (Exception) { - await SukiHost.ShowToast("Change Password", - "Change request failed for unknown reason"); + //await SukiHost.ShowToast("Change Password", + // "Change request failed for unknown reason"); + ToastManager.CreateSimpleInfoToast() + .WithTitle("Change Password") + .OfType(NotificationType.Error) + .WithContent("Change request failed for unknown reason") + .Queue(); } } else { - await SukiHost.ShowToast("Change Password", - "Change password mode not configured"); + ToastManager.CreateSimpleInfoToast() + .WithTitle("Change Password") + .OfType(NotificationType.Warning) + .WithContent("Change password mode not configured") + .Queue(); } } } @@ -187,8 +209,11 @@ public async Task GatherLogs() // Check if the zip command was successful if (!File.Exists(archivePath)) { - await SukiHost.ShowToast("Gather Logs", - "Failed to gather logs"); + ToastManager.CreateSimpleInfoToast() + .WithTitle("Gather Logs") + .OfType(NotificationType.Error) + .WithContent("Failed to gather logs") + .Queue(); return; } @@ -216,20 +241,29 @@ await SukiHost.ShowToast("Gather Logs", File.Delete(archivePath); // Delete the source file after successful copy - await SukiHost.ShowToast("Gather Logs", - "Logs saved successfully"); + ToastManager.CreateSimpleInfoToast() + .WithTitle("Gather Logs") + .OfType(NotificationType.Success) + .WithContent("Logs saved successfully") + .Queue(); } else { - await SukiHost.ShowToast("Gather Logs", - "Logs not saved"); + ToastManager.CreateSimpleInfoToast() + .WithTitle("Gather Logs") + .OfType(NotificationType.Warning) + .WithContent("Logs not saved") + .Queue(); } } } public void ShowSupportInfoDialog() { - SukiHost.ShowDialog(new SupportDialogViewModel(), allowBackgroundClose: true); + DialogManager.CreateDialog() + .WithViewModel(dialog => new SupportDialogViewModel()) + .Dismiss().ByClickingBackground() + .TryShow(); } private void CleanUp() diff --git a/ViewModels/DeviceWidgetViewModel.cs b/ViewModels/DeviceWidgetViewModel.cs index 1d648d5..5c32232 100644 --- a/ViewModels/DeviceWidgetViewModel.cs +++ b/ViewModels/DeviceWidgetViewModel.cs @@ -1,7 +1,8 @@ using System.Net; +using Avalonia.Controls.Notifications; using Avalonia.Threading; using ReactiveUI; -using SukiUI.Controls; +using SukiUI.Toasts; using SupportCompanion.Assets; using SupportCompanion.Helpers; using SupportCompanion.Interfaces; @@ -15,19 +16,22 @@ public class DeviceWidgetViewModel : ViewModelBase, IWindowStateAware private readonly ClipboardService _clipboard; private readonly IOKitService _iioKit; private readonly SystemInfoService _systemInfo; - + private readonly ISukiToastManager _toastManager; private DeviceInfoModel? _deviceInfo; public DeviceWidgetViewModel(SystemInfoService systemInfo, - ClipboardService clipboard, IOKitService iioKit) + ClipboardService clipboard, IOKitService iioKit, ISukiToastManager toastManager) { _iioKit = iioKit; _systemInfo = systemInfo; _clipboard = clipboard; + ToastManager = toastManager; DeviceInfo = new DeviceInfoModel(); Dispatcher.UIThread.Post(InitializeAsync); } + public ISukiToastManager ToastManager { get; } + public DeviceInfoModel? DeviceInfo { get => _deviceInfo; @@ -89,11 +93,19 @@ public async Task CopyToClipboard() try { await _clipboard.SetClipboardTextAsync(systemInfo); - await SukiHost.ShowToast("Copy System Info", "System Info successfully copied"); + ToastManager.CreateSimpleInfoToast() + .WithTitle("Copy System Info") + .OfType(NotificationType.Success) + .WithContent("System Info successfully copied") + .Queue(); } catch (Exception e) { - await SukiHost.ShowToast("Copy System Info", "Failed to copy System Info"); + ToastManager.CreateSimpleInfoToast() + .WithTitle("Copy System Info") + .OfType(NotificationType.Error) + .WithContent("Failed to copy System Info") + .Queue(); } } diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 767ed2f..7dbba60 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -7,6 +7,8 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; +using SukiUI.Dialogs; +using SukiUI.Toasts; using SupportCompanion.Services; using SupportCompanion.Views; @@ -21,10 +23,13 @@ public partial class MainWindowViewModel : ObservableObject [ObservableProperty] private string _nativeMenuQuitAppText; [ObservableProperty] private string _nativeMenuSystemUpdatesText; - public MainWindowViewModel(ActionsService actionsService, LoggerService loggerService) + public MainWindowViewModel(ActionsService actionsService, LoggerService loggerService, + ISukiToastManager toastManager, ISukiDialogManager dialogManager) { _actionsService = actionsService; _logger = loggerService; + ToastManager = toastManager; + DialogManager = dialogManager; ShowHeader = !string.IsNullOrEmpty(App.Config.BrandName); SelfServiceVisible = App.Config.Actions.Count > 0; BrandName = App.Config.BrandName; @@ -62,6 +67,9 @@ public MainWindowViewModel(ActionsService actionsService, LoggerService loggerSe ShowMenuToggle = App.Config.ShowMenuToggle; } + public ISukiToastManager ToastManager { get; } + public ISukiDialogManager DialogManager { get; } + public bool SelfServiceVisible { get; set; } public bool ShowHeader { get; private set; } diff --git a/ViewModels/SelfServiceViewModel.cs b/ViewModels/SelfServiceViewModel.cs index 7f8b8ca..d92685f 100644 --- a/ViewModels/SelfServiceViewModel.cs +++ b/ViewModels/SelfServiceViewModel.cs @@ -1,8 +1,9 @@ using System.Collections.ObjectModel; +using Avalonia.Controls.Notifications; using Avalonia.Threading; using CommunityToolkit.Mvvm.Input; using ReactiveUI; -using SukiUI.Controls; +using SukiUI.Toasts; using SupportCompanion.Interfaces; using SupportCompanion.Models; using SupportCompanion.Services; @@ -16,14 +17,18 @@ public class SelfServiceViewModel : ViewModelBase, IWindowStateAware private readonly LoggerService _logger; private ActionsModel? _actionsList; - public SelfServiceViewModel(LoggerService loggerService, ActionsService actionsService) + public SelfServiceViewModel(LoggerService loggerService, ActionsService actionsService, + ISukiToastManager toastManager) { _logger = loggerService; _actionsService = actionsService; + ToastManager = toastManager; ActionsList = new ActionsModel { ConfigActions = new ObservableCollection() }; if (App.Config.Actions.Count > 0) Dispatcher.UIThread.Post(InitializeAsync); } + public ISukiToastManager ToastManager { get; } + public ActionsModel? ActionsList { get => _actionsList; @@ -55,7 +60,6 @@ private async Task GetActions() ActionsList?.ConfigActions.Clear(); foreach (var action in App.Config.Actions) - { if (action.Value.TryGetValue("Name", out var name) && action.Value.TryGetValue("Command", out var command)) { @@ -72,7 +76,6 @@ private async Task GetActions() Icon = icon }); } - } } private async Task RunCommand(string command) @@ -84,14 +87,24 @@ private async Task RunCommand(string command) { action.IsRunning = true; await _actionsService.RunCommandWithoutOutput(command); - await SukiHost.ShowToast("Self Service", "Command executed successfully!"); + //await SukiHost.ShowToast("Self Service", "Command executed successfully!"); + ToastManager.CreateSimpleInfoToast() + .WithTitle("Self Service") + .OfType(NotificationType.Success) + .WithContent("Command executed successfully!") + .Queue(); action.IsRunning = false; } catch (Exception e) { action.IsRunning = false; _logger.Log("SelfServiceViewModel", e.Message, 3); - await SukiHost.ShowToast("Self Service", "Command failed to execute!"); + //await SukiHost.ShowToast("Self Service", "Command failed to execute!"); + ToastManager.CreateSimpleInfoToast() + .WithTitle("Self Service") + .OfType(NotificationType.Error) + .WithContent("Command failed to execute!") + .Queue(); } } diff --git a/ViewModels/SupportDialogViewModel.cs b/ViewModels/SupportDialogViewModel.cs index c41ebc3..a31057c 100644 --- a/ViewModels/SupportDialogViewModel.cs +++ b/ViewModels/SupportDialogViewModel.cs @@ -1,10 +1,8 @@ using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using SukiUI.Controls; namespace SupportCompanion.ViewModels; -public partial class SupportDialogViewModel : ObservableObject +public class SupportDialogViewModel : ObservableObject { public SupportDialogViewModel() { @@ -14,10 +12,4 @@ public SupportDialogViewModel() public string SupportEmail { get; set; } public string SupportPhone { get; set; } - - [RelayCommand] - private static void CloseDialog() - { - SukiHost.CloseDialog(); - } } \ No newline at end of file diff --git a/ViewModels/TransparentWindowViewModel.cs b/ViewModels/TransparentWindowViewModel.cs index cf7b780..4ae4411 100644 --- a/ViewModels/TransparentWindowViewModel.cs +++ b/ViewModels/TransparentWindowViewModel.cs @@ -12,12 +12,12 @@ namespace SupportCompanion.ViewModels; public class TransparentWindowViewModel : ViewModelBase { + private readonly IOKitService _iioKit; private readonly LoggerService _logger; private readonly StorageService _storage; + private readonly SystemInfoService _systemInfo; private DeviceInfoModel? _deviceInfo; - private readonly IOKitService _iioKit; private StorageModel? _storageInfo; - private readonly SystemInfoService _systemInfo; private Timer? _timer; public TransparentWindowViewModel(IOKitService iioKit, SystemInfoService systemInfo, StorageService storage, diff --git a/Views/ActionsWidgetView.axaml b/Views/ActionsWidgetView.axaml index 68c860b..eb3746b 100644 --- a/Views/ActionsWidgetView.axaml +++ b/Views/ActionsWidgetView.axaml @@ -15,6 +15,10 @@ + + + + diff --git a/Views/ApplicationsView.axaml b/Views/ApplicationsView.axaml index 0599001..a3ca1bb 100644 --- a/Views/ApplicationsView.axaml +++ b/Views/ApplicationsView.axaml @@ -23,7 +23,7 @@ - + @@ -35,19 +35,19 @@ + ColumnWidth="350"> - + + IsVisible="{Binding ShowActionButton}" + Width="Auto">