add a simple ui to configure the broker

pull/9/head
sleevezipper 3 years ago
parent b40ff6d6ee
commit 8c2edd69ff

@ -0,0 +1,25 @@
# Created by https://www.toptal.com/developers/gitignore/api/vscode,dotnetcore
# Edit at https://www.toptal.com/developers/gitignore?templates=vscode,dotnetcore
### DotnetCore ###
# .NET Core build folders
bin/
obj/
# Common node modules locations
/node_modules
/wwwroot/node_modules
### vscode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# End of https://www.toptal.com/developers/gitignore/api/vscode,dotnetcore
# ignore logs
logs/

@ -0,0 +1,13 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:UserInterface"
x:Class="UserInterface.App">
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
</Application.Styles>
</Application>

@ -0,0 +1,29 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using UserInterface.ViewModels;
using UserInterface.Views;
namespace UserInterface
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
}
base.OnFrameworkInitializationCompleted();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

@ -0,0 +1,13 @@
namespace MangaReader.Avalonia.Platform
{
public interface ITrayIcon : System.IDisposable
{
System.Windows.Input.ICommand DoubleClickCommand { get; set; }
System.Windows.Input.ICommand BalloonClickedCommand { get; set; }
void SetIcon();
void ShowBalloon(string text, object state);
}
}

@ -0,0 +1,24 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Logging.Serilog;
using Avalonia.ReactiveUI;
using System;
namespace UserInterface
{
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.
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToDebug()
.UseReactiveUI();
}
}

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\netcoreapp3.1\publish\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<PublishSingleFile>True</PublishSingleFile>
<PublishReadyToRun>False</PublishReadyToRun>
</PropertyGroup>
</Project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
</Project>

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\hass-workstation-logo.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.9.12" />
<PackageReference Include="Avalonia.Desktop" Version="0.9.12" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.9.12" />
<PackageReference Include="Avalonia.Win32" Version="0.9.12" />
<PackageReference Include="JKang.IpcServiceFramework.Client.NamedPipe" Version="3.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\hass-workstation-service\hass-workstation-service.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Views\BrokerSettings\BrokerSettings.axaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
</ItemGroup>
</Project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<_LastSelectedProfileId>C:\Users\Maurits\Documents\Repo\hass-desktop-service\UserInterface\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
</PropertyGroup>
</Project>

@ -0,0 +1,32 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using System;
using UserInterface.ViewModels;
namespace UserInterface
{
public class ViewLocator : IDataTemplate
{
public bool SupportsRecycling => false;
public IControl Build(object data)
{
var name = data.GetType().FullName.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type);
}
else
{
return new TextBlock { Text = "Not Found: " + name };
}
}
public bool Match(object data)
{
return data is ViewModelBase;
}
}
}

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace UserInterface.ViewModels
{
public class BrokerSettingsViewModel : ViewModelBase
{
public string Host { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace UserInterface.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
public string Greeting => "Welcome to Avalonia!";
}
}

@ -0,0 +1,11 @@
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Text;
namespace UserInterface.ViewModels
{
public class ViewModelBase : ReactiveObject
{
}
}

@ -0,0 +1,17 @@
<UserControl xmlns="https://github.com/avaloniaui"
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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="UserInterface.Views.BrokerSettings">
<StackPanel Margin="30" HorizontalAlignment="Left">
<ContentControl FontSize="18" FontWeight="Bold">Mqtt broker</ContentControl>
<ContentControl FontSize="18" Margin="0 30 0 10">IP or hostname</ContentControl>
<TextBox Text="{Binding Host}" HorizontalAlignment="Left" Width="100" Watermark="192.168.1.200"/>
<ContentControl FontSize="18" Margin="0 30 0 10">Username</ContentControl>
<TextBox Text="{Binding Username}" Width="200"/>
<ContentControl FontSize="18" Margin="0 30 0 10">Password</ContentControl>
<TextBox Text="{Binding Password}" Width="200" PasswordChar="•"/>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Configure">Save</Button>
</StackPanel>
</UserControl>

@ -0,0 +1,56 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using hass_workstation_service.Communication.NamedPipe;
using JKang.IpcServiceFramework.Client;
using System.Threading.Tasks;
using Avalonia.Interactivity;
using System.Reactive.Linq;
using UserInterface.ViewModels;
using System.Security;
namespace UserInterface.Views
{
public class BrokerSettings : UserControl
{
private readonly IIpcClient<ServiceContractInterfaces> client;
private string _host { get; set; }
private string _username { get; set; }
private string _password { get; set; }
public BrokerSettings()
{
DataContext = new BrokerSettingsViewModel();
this.InitializeComponent();
// register IPC clients
ServiceProvider serviceProvider = new ServiceCollection()
.AddNamedPipeIpcClient<ServiceContractInterfaces>("client1", pipeName: "pipeinternal")
.BuildServiceProvider();
// resolve IPC client factory
IIpcClientFactory<ServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<ServiceContractInterfaces>>();
// create client
this.client = clientFactory.CreateClient("client1");
}
public void Ping(object sender, RoutedEventArgs args) {
var result = this.client.InvokeAsync(x => x.Ping("ping")).Result;
}
public void Configure(object sender, RoutedEventArgs args)
{
var model = (BrokerSettingsViewModel)this.DataContext;
var result = this.client.InvokeAsync(x => x.WriteMqttBrokerSettings(model.Host, model.Username, model.Password));
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

@ -0,0 +1,20 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:UserInterface.ViewModels;assembly=UserInterface"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:UserInterface.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="UserInterface.Views.MainWindow"
Icon="/Assets/hass-workstation-logo.ico"
Title="UserInterface">
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<views:BrokerSettings/>
<!--<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>-->
</Window>

@ -0,0 +1,22 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace UserInterface.Views
{
public class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

@ -0,0 +1,61 @@
namespace MangaReader.Avalonia.Platform.Win.Interop
{
/// <summary>
/// Flags that define the icon that is shown on a balloon
/// tooltip.
/// </summary>
public enum BalloonFlags
{
/// <summary>
/// No icon is displayed.
/// </summary>
None = 0x00,
/// <summary>
/// An information icon is displayed.
/// </summary>
Info = 0x01,
/// <summary>
/// A warning icon is displayed.
/// </summary>
Warning = 0x02,
/// <summary>
/// An error icon is displayed.
/// </summary>
Error = 0x03,
/// <summary>
/// Windows XP Service Pack 2 (SP2) and later.
/// Use a custom icon as the title icon.
/// </summary>
User = 0x04,
/// <summary>
/// Windows XP (Shell32.dll version 6.0) and later.
/// Do not play the associated sound. Applies only to balloon ToolTips.
/// </summary>
NoSound = 0x10,
/// <summary>
/// Windows Vista (Shell32.dll version 6.0.6) and later. The large version
/// of the icon should be used as the balloon icon. This corresponds to the
/// icon with dimensions SM_CXICON x SM_CYICON. If this flag is not set,
/// the icon with dimensions XM_CXSMICON x SM_CYSMICON is used.<br/>
/// - This flag can be used with all stock icons.<br/>
/// - Applications that use older customized icons (NIIF_USER with hIcon) must
/// provide a new SM_CXICON x SM_CYICON version in the tray icon (hIcon). These
/// icons are scaled down when they are displayed in the System Tray or
/// System Control Area (SCA).<br/>
/// - New customized icons (NIIF_USER with hBalloonIcon) must supply an
/// SM_CXICON x SM_CYICON version in the supplied icon (hBalloonIcon).
/// </summary>
LargeIcon = 0x20,
/// <summary>
/// Windows 7 and later.
/// </summary>
RespectQuietTime = 0x80
}
}

@ -0,0 +1,70 @@
using System;
namespace MangaReader.Avalonia.Platform.Win.Interop
{
/// <summary>
/// Indicates which members of a <see cref="NotifyIconData"/> structure
/// were set, and thus contain valid data or provide additional information
/// to the ToolTip as to how it should display.
/// </summary>
[Flags]
public enum IconDataMembers
{
/// <summary>
/// The message ID is set.
/// </summary>
Message = 0x01,
/// <summary>
/// The notification icon is set.
/// </summary>
Icon = 0x02,
/// <summary>
/// The tooltip is set.
/// </summary>
Tip = 0x04,
/// <summary>
/// State information (<see cref="IconState"/>) is set. This
/// applies to both <see cref="NotifyIconData.IconState"/> and
/// <see cref="NotifyIconData.StateMask"/>.
/// </summary>
State = 0x08,
/// <summary>
/// The balloon ToolTip is set. Accordingly, the following
/// members are set: <see cref="NotifyIconData.BalloonText"/>,
/// <see cref="NotifyIconData.BalloonTitle"/>, <see cref="NotifyIconData.BalloonFlags"/>,
/// and <see cref="NotifyIconData.VersionOrTimeout"/>.
/// </summary>
Info = 0x10,
// Internal identifier is set. Reserved, thus commented out.
//Guid = 0x20,
/// <summary>
/// Windows Vista (Shell32.dll version 6.0.6) and later. If the ToolTip
/// cannot be displayed immediately, discard it.<br/>
/// Use this flag for ToolTips that represent real-time information which
/// would be meaningless or misleading if displayed at a later time.
/// For example, a message that states "Your telephone is ringing."<br/>
/// This modifies and must be combined with the <see cref="Info"/> flag.
/// </summary>
Realtime = 0x40,
/// <summary>
/// Windows Vista (Shell32.dll version 6.0.6) and later.
/// Use the standard ToolTip. Normally, when uVersion is set
/// to NOTIFYICON_VERSION_4, the standard ToolTip is replaced
/// by the application-drawn pop-up user interface (UI).
/// If the application wants to show the standard tooltip
/// in that case, regardless of whether the on-hover UI is showing,
/// it can specify NIF_SHOWTIP to indicate the standard tooltip
/// should still be shown.<br/>
/// Note that the NIF_SHOWTIP flag is effective until the next call
/// to Shell_NotifyIcon.
/// </summary>
UseLegacyToolTips = 0x80
}
}

@ -0,0 +1,22 @@
namespace MangaReader.Avalonia.Platform.Win.Interop
{
/// <summary>
/// The state of the icon - can be set to
/// hide the icon.
/// </summary>
public enum IconState
{
/// <summary>
/// The icon is visible.
/// </summary>
Visible = 0x00,
/// <summary>
/// Hide the icon.
/// </summary>
Hidden = 0x01,
// The icon is shared - currently not supported, thus commented out.
//Shared = 0x02
}
}

@ -0,0 +1,54 @@
namespace MangaReader.Avalonia.Platform.Win.Interop
{
/// <summary>
/// Event flags for clicked events.
/// </summary>
public enum MouseEvent
{
/// <summary>
/// The mouse was moved withing the
/// taskbar icon's area.
/// </summary>
MouseMove,
/// <summary>
/// The right mouse button was clicked.
/// </summary>
IconRightMouseDown,
/// <summary>
/// The left mouse button was clicked.
/// </summary>
IconLeftMouseDown,
/// <summary>
/// The right mouse button was released.
/// </summary>
IconRightMouseUp,
/// <summary>
/// The left mouse button was released.
/// </summary>
IconLeftMouseUp,
/// <summary>
/// The middle mouse button was clicked.
/// </summary>
IconMiddleMouseDown,
/// <summary>
/// The middle mouse button was released.
/// </summary>
IconMiddleMouseUp,
/// <summary>
/// The taskbar icon was double clicked.
/// </summary>
IconDoubleClick,
/// <summary>
/// The balloon tip was clicked.
/// </summary>
BalloonToolTipClicked
}
}

@ -0,0 +1,41 @@
namespace MangaReader.Avalonia.Platform.Win.Interop
{
/// <summary>
/// Main operations performed on the
/// <see cref="WinApi.Shell_NotifyIcon"/> function.
/// </summary>
public enum NotifyCommand
{
/// <summary>
/// The taskbar icon is being created.
/// </summary>
Add = 0x00,
/// <summary>
/// The settings of the taskbar icon are being updated.
/// </summary>
Modify = 0x01,
/// <summary>
/// The taskbar icon is deleted.
/// </summary>
Delete = 0x02,
/// <summary>
/// Focus is returned to the taskbar icon. Currently not in use.
/// </summary>
SetFocus = 0x03,
/// <summary>
/// Shell32.dll version 5.0 and later only. Instructs the taskbar
/// to behave according to the version number specified in the
/// uVersion member of the structure pointed to by lpdata.
/// This message allows you to specify whether you want the version
/// 5.0 behavior found on Microsoft Windows 2000 systems, or the
/// behavior found on earlier Shell versions. The default value for
/// uVersion is zero, indicating that the original Windows 95 notify
/// icon behavior should be used.
/// </summary>
SetVersion = 0x04
}
}

@ -0,0 +1,154 @@
using System;
using System.Runtime.InteropServices;
namespace MangaReader.Avalonia.Platform.Win.Interop
{
/// <summary>
/// A struct that is submitted in order to configure
/// the taskbar icon. Provides various members that
/// can be configured partially, according to the
/// values of the <see cref="IconDataMembers"/>
/// that were defined.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct NotifyIconData
{
/// <summary>
/// Size of this structure, in bytes.
/// </summary>
public uint cbSize;
/// <summary>
/// Handle to the window that receives notification messages associated with an icon in the
/// taskbar status area. The Shell uses hWnd and uID to identify which icon to operate on
/// when Shell_NotifyIcon is invoked.
/// </summary>
public IntPtr WindowHandle;
/// <summary>
/// Application-defined identifier of the taskbar icon. The Shell uses hWnd and uID to identify
/// which icon to operate on when Shell_NotifyIcon is invoked. You can have multiple icons
/// associated with a single hWnd by assigning each a different uID. This feature, however
/// is currently not used.
/// </summary>
public uint TaskbarIconId;
/// <summary>
/// Flags that indicate which of the other members contain valid data. This member can be
/// a combination of the NIF_XXX constants.
/// </summary>
public IconDataMembers ValidMembers;
/// <summary>
/// Application-defined message identifier. The system uses this identifier to send
/// notifications to the window identified in hWnd.
/// </summary>
public uint CallbackMessageId;
/// <summary>
/// A handle to the icon that should be displayed. Just
/// <c>Icon.Handle</c>.
/// </summary>
public IntPtr IconHandle;
/// <summary>
/// String with the text for a standard ToolTip. It can have a maximum of 64 characters including
/// the terminating NULL. For Version 5.0 and later, szTip can have a maximum of
/// 128 characters, including the terminating NULL.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string ToolTipText;
/// <summary>
/// State of the icon. Remember to also set the <see cref="StateMask"/>.
/// </summary>
public IconState IconState;
/// <summary>
/// A value that specifies which bits of the state member are retrieved or modified.
/// For example, setting this member to <see cref="Interop.IconState.Hidden"/>
/// causes only the item's hidden
/// state to be retrieved.
/// </summary>
public IconState StateMask;
/// <summary>
/// String with the text for a balloon ToolTip. It can have a maximum of 255 characters.
/// To remove the ToolTip, set the NIF_INFO flag in uFlags and set szInfo to an empty string.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string BalloonText;
/// <summary>
/// Mainly used to set the version when <see cref="WinApi.Shell_NotifyIcon"/> is invoked
/// with <see cref="NotifyCommand.SetVersion"/>. However, for legacy operations,
/// the same member is also used to set timeouts for balloon ToolTips.
/// </summary>
public uint VersionOrTimeout;
/// <summary>
/// String containing a title for a balloon ToolTip. This title appears in boldface
/// above the text. It can have a maximum of 63 characters.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string BalloonTitle;
/// <summary>
/// Adds an icon to a balloon ToolTip, which is placed to the left of the title. If the
/// <see cref="BalloonTitle"/> member is zero-length, the icon is not shown.
/// </summary>
public BalloonFlags BalloonFlags;
/// <summary>
/// Windows XP (Shell32.dll version 6.0) and later.<br/>
/// - Windows 7 and later: A registered GUID that identifies the icon.
/// This value overrides uID and is the recommended method of identifying the icon.<br/>
/// - Windows XP through Windows Vista: Reserved.
/// </summary>
public Guid TaskbarIconGuid;
/// <summary>
/// Windows Vista (Shell32.dll version 6.0.6) and later. The handle of a customized
/// balloon icon provided by the application that should be used independently
/// of the tray icon. If this member is non-NULL and the <see cref="Interop.BalloonFlags.User"/>
/// flag is set, this icon is used as the balloon icon.<br/>
/// If this member is NULL, the legacy behavior is carried out.
/// </summary>
public IntPtr CustomBalloonIconHandle;
/// <summary>
/// Creates a default data structure that provides
/// a hidden taskbar icon without the icon being set.
/// </summary>
/// <param name="handle"></param>
/// <returns>NotifyIconData</returns>
public static NotifyIconData CreateDefault(IntPtr handle)
{
var data = new NotifyIconData();
//use the current size
data.cbSize = (uint) Marshal.SizeOf(data);
data.WindowHandle = handle;
data.TaskbarIconId = 0x0;
data.CallbackMessageId = WindowMessageSink.CallbackMessageId;
data.VersionOrTimeout = (uint) NotifyIconVersion.Vista;
data.IconHandle = IntPtr.Zero;
//hide initially
data.IconState = IconState.Hidden;
data.StateMask = IconState.Hidden;
//set flags
data.ValidMembers = IconDataMembers.Message | IconDataMembers.Icon | IconDataMembers.Tip | IconDataMembers.UseLegacyToolTips;
//reset strings
data.ToolTipText = data.BalloonText = data.BalloonTitle = string.Empty;
return data;
}
}
}

@ -0,0 +1,15 @@
namespace MangaReader.Avalonia.Platform.Win.Interop
{
/// <summary>
/// The notify icon version that is used. The higher
/// the version, the more capabilities are available.
/// </summary>
public enum NotifyIconVersion
{
/// <summary>
/// Extended tooltip support, which is available for Vista and later.
/// Detailed information about what the different versions do, can be found <a href="https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shell_notifyicona">here</a>
/// </summary>
Vista = 0x4
}
}

@ -0,0 +1,59 @@
using System;
using System.Runtime.InteropServices;
namespace MangaReader.Avalonia.Platform.Win.Interop
{
/// <summary>
/// Win32 API imports.
/// </summary>
internal static class WinApi
{
private const string User32 = "user32.dll";
/// <summary>
/// Creates, updates or deletes the taskbar icon.
/// </summary>
[DllImport("shell32.Dll", CharSet = CharSet.Unicode)]
public static extern bool Shell_NotifyIcon(NotifyCommand cmd, [In] ref NotifyIconData data);
/// <summary>
/// Creates the helper window that receives messages from the taskar icon.
/// </summary>
[DllImport(User32, EntryPoint = "CreateWindowExW", SetLastError = true)]
public static extern IntPtr CreateWindowEx(int dwExStyle, [MarshalAs(UnmanagedType.LPWStr)] string lpClassName,
[MarshalAs(UnmanagedType.LPWStr)] string lpWindowName, int dwStyle, int x, int y,
int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance,
IntPtr lpParam);
/// <summary>
/// Processes a default windows procedure.
/// </summary>
[DllImport(User32)]
public static extern IntPtr DefWindowProc(IntPtr hWnd, uint msg, IntPtr wparam, IntPtr lparam);
/// <summary>
/// Registers the helper window class.
/// </summary>
[DllImport(User32, EntryPoint = "RegisterClassW", SetLastError = true)]
public static extern short RegisterClass(ref WindowClass lpWndClass);
/// <summary>
/// Registers a listener for a window message.
/// </summary>
/// <param name="lpString"></param>
/// <returns>uint</returns>
[DllImport(User32, EntryPoint = "RegisterWindowMessageW")]
public static extern uint RegisterWindowMessage([MarshalAs(UnmanagedType.LPWStr)] string lpString);
/// <summary>
/// Used to destroy the hidden helper window that receives messages from the
/// taskbar icon.
/// </summary>
/// <param name="hWnd"></param>
/// <returns>bool</returns>
[DllImport(User32, SetLastError = true)]
public static extern bool DestroyWindow(IntPtr hWnd);
}
}

@ -0,0 +1,35 @@
using System;
using System.Runtime.InteropServices;
namespace MangaReader.Avalonia.Platform.Win.Interop
{
/// <summary>
/// Callback delegate which is used by the Windows API to
/// submit window messages.
/// </summary>
public delegate IntPtr WindowProcedureHandler(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
/// <summary>
/// Win API WNDCLASS struct - represents a single window.
/// Used to receive window messages.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct WindowClass
{
#pragma warning disable 1591
public uint style;
public WindowProcedureHandler lpfnWndProc;
public int cbClsExtra;
public int cbWndExtra;
public IntPtr hInstance;
public IntPtr hIcon;
public IntPtr hCursor;
public IntPtr hbrBackground;
[MarshalAs(UnmanagedType.LPWStr)] public string lpszMenuName;
[MarshalAs(UnmanagedType.LPWStr)] public string lpszClassName;
#pragma warning restore 1591
}
}

@ -0,0 +1,357 @@
// hardcodet.net NotifyIcon for WPF
// Copyright (c) 2009 - 2013 Philipp Sumi
// Contact and Information: http://www.hardcodet.net
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the Code Project Open License (CPOL);
// either version 1.0 of the License, or (at your option) any later
// version.
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// THIS COPYRIGHT NOTICE MAY NOT BE REMOVED FROM THIS FILE
using System;
using System.ComponentModel;
using System.Diagnostics;
namespace MangaReader.Avalonia.Platform.Win.Interop
{
/// <summary>
/// Receives messages from the taskbar icon through
/// window messages of an underlying helper window.
/// </summary>
public class WindowMessageSink : IDisposable
{
#region members
/// <summary>
/// The ID of messages that are received from the the
/// taskbar icon.
/// </summary>
public const int CallbackMessageId = 0x400;
/// <summary>
/// The ID of the message that is being received if the
/// taskbar is (re)started.
/// </summary>
private uint taskbarRestartMessageId;
/// <summary>
/// Used to track whether a mouse-up event is just
/// the aftermath of a double-click and therefore needs
/// to be suppressed.
/// </summary>
private bool isDoubleClick;
/// <summary>
/// A delegate that processes messages of the hidden
/// native window that receives window messages. Storing
/// this reference makes sure we don't loose our reference
/// to the message window.
/// </summary>
private WindowProcedureHandler messageHandler;
/// <summary>
/// Window class ID.
/// </summary>
internal string WindowId { get; private set; }
/// <summary>
/// Handle for the message window.
/// </summary>
internal IntPtr MessageWindowHandle { get; private set; }
/// <summary>
/// The version of the underlying icon. Defines how
/// incoming messages are interpreted.
/// </summary>
public NotifyIconVersion Version { get; set; }
#endregion
#region events
/// <summary>
/// The custom tooltip should be closed or hidden.
/// </summary>
public event Action<bool> ChangeToolTipStateRequest;
/// <summary>
/// Fired in case the user clicked or moved within
/// the taskbar icon area.
/// </summary>
public event Action<MouseEvent> MouseEventReceived;
/// <summary>
/// Fired if a balloon ToolTip was either displayed
/// or closed (indicated by the boolean flag).
/// </summary>
public event Action<bool> BalloonToolTipChanged;
/// <summary>
/// Fired if the taskbar was created or restarted. Requires the taskbar
/// icon to be reset.
/// </summary>
public event Action TaskbarCreated;
#endregion
#region construction
/// <summary>
/// Creates a new message sink that receives message from
/// a given taskbar icon.
/// </summary>
/// <param name="version"></param>
public WindowMessageSink(NotifyIconVersion version)
{
Version = version;
CreateMessageWindow();
}
#endregion
#region CreateMessageWindow
/// <summary>
/// Creates the helper message window that is used
/// to receive messages from the taskbar icon.
/// </summary>
private void CreateMessageWindow()
{
//generate a unique ID for the window
WindowId = "WPFTaskbarIcon_" + Guid.NewGuid();
//register window message handler
messageHandler = OnWindowMessageReceived;
// Create a simple window class which is reference through
//the messageHandler delegate
WindowClass wc;
wc.style = 0;
wc.lpfnWndProc = messageHandler;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = IntPtr.Zero;
wc.hIcon = IntPtr.Zero;
wc.hCursor = IntPtr.Zero;
wc.hbrBackground = IntPtr.Zero;
wc.lpszMenuName = string.Empty;
wc.lpszClassName = WindowId;
// Register the window class
WinApi.RegisterClass(ref wc);
// Get the message used to indicate the taskbar has been restarted
// This is used to re-add icons when the taskbar restarts
taskbarRestartMessageId = WinApi.RegisterWindowMessage("TaskbarCreated");
// Create the message window
MessageWindowHandle = WinApi.CreateWindowEx(0, WindowId, "", 0, 0, 0, 1, 1, IntPtr.Zero, IntPtr.Zero,
IntPtr.Zero, IntPtr.Zero);
if (MessageWindowHandle == IntPtr.Zero)
{
throw new Win32Exception("Message window handle was not a valid pointer");
}
}
#endregion
#region Handle Window Messages
/// <summary>
/// Callback method that receives messages from the taskbar area.
/// </summary>
private IntPtr OnWindowMessageReceived(IntPtr hWnd, uint messageId, IntPtr wParam, IntPtr lParam)
{
if (messageId == taskbarRestartMessageId)
{
//recreate the icon if the taskbar was restarted (e.g. due to Win Explorer shutdown)
var listener = TaskbarCreated;
listener?.Invoke();
}
//forward message
ProcessWindowMessage(messageId, wParam, lParam);
// Pass the message to the default window procedure
return WinApi.DefWindowProc(hWnd, messageId, wParam, lParam);
}
/// <summary>
/// Processes incoming system messages.
/// </summary>
/// <param name="msg">Callback ID.</param>
/// <param name="wParam">If the version is <see cref="NotifyIconVersion.Vista"/>
/// or higher, this parameter can be used to resolve mouse coordinates.
/// Currently not in use.</param>
/// <param name="lParam">Provides information about the event.</param>
private void ProcessWindowMessage(uint msg, IntPtr wParam, IntPtr lParam)
{
if (msg != CallbackMessageId) return;
var message = (WindowsMessages) lParam.ToInt32();
switch (message)
{
case WindowsMessages.WM_CONTEXTMENU:
// TODO: Handle WM_CONTEXTMENU, see https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shell_notifyiconw
Debug.WriteLine("Unhandled WM_CONTEXTMENU");
break;
case WindowsMessages.WM_MOUSEMOVE:
MouseEventReceived?.Invoke(MouseEvent.MouseMove);
break;
case WindowsMessages.WM_LBUTTONDOWN:
MouseEventReceived?.Invoke(MouseEvent.IconLeftMouseDown);
break;
case WindowsMessages.WM_LBUTTONUP:
if (!isDoubleClick)
{
MouseEventReceived?.Invoke(MouseEvent.IconLeftMouseUp);
}
isDoubleClick = false;
break;
case WindowsMessages.WM_LBUTTONDBLCLK:
isDoubleClick = true;
MouseEventReceived?.Invoke(MouseEvent.IconDoubleClick);
break;
case WindowsMessages.WM_RBUTTONDOWN:
MouseEventReceived?.Invoke(MouseEvent.IconRightMouseDown);
break;
case WindowsMessages.WM_RBUTTONUP:
MouseEventReceived?.Invoke(MouseEvent.IconRightMouseUp);
break;
case WindowsMessages.WM_RBUTTONDBLCLK:
//double click with right mouse button - do not trigger event
break;
case WindowsMessages.WM_MBUTTONDOWN:
MouseEventReceived?.Invoke(MouseEvent.IconMiddleMouseDown);
break;
case WindowsMessages.WM_MBUTTONUP:
MouseEventReceived?.Invoke(MouseEvent.IconMiddleMouseUp);
break;
case WindowsMessages.WM_MBUTTONDBLCLK:
//double click with middle mouse button - do not trigger event
break;
case WindowsMessages.NIN_BALLOONSHOW:
BalloonToolTipChanged?.Invoke(true);
break;
case WindowsMessages.NIN_BALLOONHIDE:
case WindowsMessages.NIN_BALLOONTIMEOUT:
BalloonToolTipChanged?.Invoke(false);
break;
case WindowsMessages.NIN_BALLOONUSERCLICK:
MouseEventReceived?.Invoke(MouseEvent.BalloonToolTipClicked);
break;
case WindowsMessages.NIN_POPUPOPEN:
ChangeToolTipStateRequest?.Invoke(true);
break;
case WindowsMessages.NIN_POPUPCLOSE:
ChangeToolTipStateRequest?.Invoke(false);
break;
case WindowsMessages.NIN_SELECT:
// TODO: Handle NIN_SELECT see https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shell_notifyiconw
Debug.WriteLine("Unhandled NIN_SELECT");
break;
case WindowsMessages.NIN_KEYSELECT:
// TODO: Handle NIN_KEYSELECT see https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shell_notifyiconw
Debug.WriteLine("Unhandled NIN_KEYSELECT");
break;
default:
Debug.WriteLine("Unhandled NotifyIcon message ID: " + lParam);
break;
}
}
#endregion
#region Dispose
/// <summary>
/// Set to true as soon as <c>Dispose</c> has been invoked.
/// </summary>
public bool IsDisposed { get; private set; }
/// <summary>
/// Disposes the object.
/// </summary>
/// <remarks>This method is not virtual by design. Derived classes
/// should override <see cref="Dispose(bool)"/>.
/// </remarks>
public void Dispose()
{
Dispose(true);
// This object will be cleaned up by the Dispose method.
// Therefore, you should call GC.SuppressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
/// <summary>
/// This destructor will run only if the <see cref="Dispose()"/>
/// method does not get called. This gives this base class the
/// opportunity to finalize.
/// <para>
/// Important: Do not provide destructor in types derived from
/// this class.
/// </para>
/// </summary>
~WindowMessageSink()
{
Dispose(false);
}
/// <summary>
/// Removes the windows hook that receives window
/// messages and closes the underlying helper window.
/// </summary>
private void Dispose(bool disposing)
{
//don't do anything if the component is already disposed
if (IsDisposed) return;
IsDisposed = true;
//always destroy the unmanaged handle (even if called from the GC)
WinApi.DestroyWindow(MessageWindowHandle);
messageHandler = null;
}
#endregion
}
}

@ -0,0 +1,190 @@
// hardcodet.net NotifyIcon for WPF
// Copyright (c) 2009 - 2013 Philipp Sumi
// Contact and Information: http://www.hardcodet.net
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the Code Project Open License (CPOL);
// either version 1.0 of the License, or (at your option) any later
// version.
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// THIS COPYRIGHT NOTICE MAY NOT BE REMOVED FROM THIS FILE
// ReSharper disable InconsistentNaming
using System.Diagnostics.CodeAnalysis;
namespace MangaReader.Avalonia.Platform.Win.Interop
{
/// <summary>
/// This enum defines the windows messages we respond to.
/// See more on Windows messages <a href="https://docs.microsoft.com/en-us/windows/win32/learnwin32/window-messages">here</a>
/// </summary>
[SuppressMessage("ReSharper", "IdentifierTypo")]
public enum WindowsMessages : uint
{
/// <summary>
/// Notifies a window that the user clicked the right mouse button (right-clicked) in the window.
/// See <a href="https://docs.microsoft.com/en-us/windows/win32/menurc/wm-contextmenu">WM_CONTEXTMENU message</a>
///
/// In case of a notify icon:
/// If a user selects a notify icon's shortcut menu with the keyboard, the Shell now sends the associated application a WM_CONTEXTMENU message. Earlier versions send WM_RBUTTONDOWN and WM_RBUTTONUP messages.
/// See <a href="https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shell_notifyiconw">Shell_NotifyIcon function</a>
/// </summary>
WM_CONTEXTMENU = 0x007b,
/// <summary>
/// Posted to a window when the cursor moves.
/// If the mouse is not captured, the message is posted to the window that contains the cursor.
/// Otherwise, the message is posted to the window that has captured the mouse.
///
/// See <a href="https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mousemove">WM_MOUSEMOVE message</a>
/// </summary>
WM_MOUSEMOVE = 0x0200,
/// <summary>
/// Posted when the user presses the left mouse button while the cursor is in the client area of a window.
/// If the mouse is not captured, the message is posted to the window beneath the cursor.
/// Otherwise, the message is posted to the window that has captured the mouse.
///
/// See <a href="https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondown">WM_LBUTTONDOWN message</a>
/// </summary>
WM_LBUTTONDOWN = 0x0201,
/// <summary>
/// Posted when the user releases the left mouse button while the cursor is in the client area of a window.
/// If the mouse is not captured, the message is posted to the window beneath the cursor.
/// Otherwise, the message is posted to the window that has captured the mouse.
///
/// See <a href="https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttonup">WM_LBUTTONUP message</a>
/// </summary>
WM_LBUTTONUP = 0x0202,
/// <summary>
/// Posted when the user double-clicks the left mouse button while the cursor is in the client area of a window.
/// If the mouse is not captured, the message is posted to the window beneath the cursor.
/// Otherwise, the message is posted to the window that has captured the mouse.
///
/// See <a href="https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-lbuttondblclk">WM_LBUTTONDBLCLK message</a>
/// </summary>
WM_LBUTTONDBLCLK = 0x0203,
/// <summary>
/// Posted when the user presses the right mouse button while the cursor is in the client area of a window.
/// If the mouse is not captured, the message is posted to the window beneath the cursor.
/// Otherwise, the message is posted to the window that has captured the mouse.
///
/// See <a href="https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-rbuttondown">WM_RBUTTONDOWN message</a>
/// </summary>
WM_RBUTTONDOWN = 0x0204,
/// <summary>
/// Posted when the user releases the right mouse button while the cursor is in the client area of a window.
/// If the mouse is not captured, the message is posted to the window beneath the cursor.
/// Otherwise, the message is posted to the window that has captured the mouse.
///
/// See <a href="https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-rbuttonup">WM_RBUTTONUP message</a>
/// </summary>
WM_RBUTTONUP = 0x0205,
/// <summary>
/// Posted when the user double-clicks the right mouse button while the cursor is in the client area of a window.
/// If the mouse is not captured, the message is posted to the window beneath the cursor.
/// Otherwise, the message is posted to the window that has captured the mouse.
///
/// See <a href="https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-rbuttondblclk">WM_RBUTTONDBLCLK message</a>
/// </summary>
WM_RBUTTONDBLCLK = 0x0206,
/// <summary>
/// Posted when the user presses the middle mouse button while the cursor is in the client area of a window.
/// If the mouse is not captured, the message is posted to the window beneath the cursor.
/// Otherwise, the message is posted to the window that has captured the mouse.
///
/// See <a href="https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mbuttondown">WM_MBUTTONDOWN message</a>
/// </summary>
WM_MBUTTONDOWN = 0x0207,
/// <summary>
/// Posted when the user releases the middle mouse button while the cursor is in the client area of a window.
/// If the mouse is not captured, the message is posted to the window beneath the cursor.
/// Otherwise, the message is posted to the window that has captured the mouse.
///
/// See <a href="https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mbuttonup">WM_MBUTTONUP message</a>
/// </summary>
WM_MBUTTONUP = 0x0208,
/// <summary>
/// Posted when the user double-clicks the middle mouse button while the cursor is in the client area of a window.
/// If the mouse is not captured, the message is posted to the window beneath the cursor.
/// Otherwise, the message is posted to the window that has captured the mouse.
///
/// See <a href="https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mbuttondblclk">WM_MBUTTONDBLCLK message</a>
/// </summary>
WM_MBUTTONDBLCLK = 0x0209,
/// <summary>
/// Used to define private messages for use by private window classes, usually of the form WM_USER+x, where x is an integer value.
/// </summary>
WM_USER = 0x0400,
/// <summary>
/// This message is only send when using NOTIFYICON_VERSION_4, the Shell now sends the associated application an NIN_SELECT notification.
/// Send when a notify icon is activated with mouse or ENTER key.
/// Earlier versions send WM_RBUTTONDOWN and WM_RBUTTONUP messages.
/// </summary>
NIN_SELECT = WM_USER,
/// <summary>
/// This message is only send when using NOTIFYICON_VERSION_4, the Shell now sends the associated application an NIN_SELECT notification.
/// Send when a notify icon is activated with SPACEBAR or ENTER key.
/// Earlier versions send WM_RBUTTONDOWN and WM_RBUTTONUP messages.
/// </summary>
NIN_KEYSELECT = WM_USER + 1,
/// <summary>
/// Sent when the balloon is shown (balloons are queued).
/// </summary>
NIN_BALLOONSHOW = WM_USER + 2,
/// <summary>
/// Sent when the balloon disappears. For example, when the icon is deleted.
/// This message is not sent if the balloon is dismissed because of a timeout or if the user clicks the mouse.
///
/// As of Windows 7, NIN_BALLOONHIDE is also sent when a notification with the NIIF_RESPECT_QUIET_TIME flag set attempts to display during quiet time (a user's first hour on a new computer).
/// In that case, the balloon is never displayed at all.
/// </summary>
NIN_BALLOONHIDE = WM_USER + 3,
/// <summary>
/// Sent when the balloon is dismissed because of a timeout.
/// </summary>
NIN_BALLOONTIMEOUT = WM_USER + 4,
/// <summary>
/// Sent when the balloon is dismissed because the user clicked the mouse.
/// </summary>
NIN_BALLOONUSERCLICK = WM_USER + 5,
/// <summary>
/// Sent when the user hovers the cursor over an icon to indicate that the richer pop-up UI should be used in place of a standard textual tooltip.
/// </summary>
NIN_POPUPOPEN = WM_USER + 6,
/// <summary>
/// Sent when a cursor no longer hovers over an icon to indicate that the rich pop-up UI should be closed.
/// </summary>
NIN_POPUPCLOSE = WM_USER + 7
}
}

@ -0,0 +1,403 @@
// hardcodet.net NotifyIcon for WPF
// Copyright (c) 2009 - 2013 Philipp Sumi
// Contact and Information: http://www.hardcodet.net
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the Code Project Open License (CPOL);
// either version 1.0 of the License, or (at your option) any later
// version.
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// THIS COPYRIGHT NOTICE MAY NOT BE REMOVED FROM THIS FILE
using System;
using System.Drawing;
using System.Linq;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using MangaReader.Avalonia.Platform.Win.Interop;
namespace MangaReader.Avalonia.Platform.Win
{
/// <summary>
/// A WPF proxy to for a taskbar icon (NotifyIcon) that sits in the system's
/// taskbar notification area ("system tray").
/// </summary>
public class TaskBarIcon : IDisposable
{
private readonly object lockObject = new object();
#region Members
/// <summary>
/// Represents the current icon data.
/// </summary>
private NotifyIconData iconData;
/// <summary>
/// Receives messages from the taskbar icon.
/// </summary>
private readonly WindowMessageSink messageSink;
/// <summary>
/// Indicates whether the taskbar icon has been created or not.
/// </summary>
public bool IsTaskbarIconCreated { get; private set; }
public Icon Icon { get; }
public event EventHandler<MouseEvent> MouseEventHandler;
#endregion
#region Construction
/// <summary>
/// Initializes the taskbar icon and registers a message listener
/// in order to receive events from the taskbar area.
/// </summary>
public TaskBarIcon(Icon icon)
{
Icon = icon;
// using dummy sink in design mode
messageSink = new WindowMessageSink(NotifyIconVersion.Vista);
// init icon data structure
iconData = NotifyIconData.CreateDefault(messageSink.MessageWindowHandle);
iconData.IconHandle = Icon?.Handle ?? IntPtr.Zero;
iconData.ToolTipText = nameof(MangaReader);
// create the taskbar icon
CreateTaskbarIcon();
// register event listeners
messageSink.MouseEventReceived += OnMouseEvent;
messageSink.TaskbarCreated += OnTaskbarCreated;
// register listener in order to get notified when the application closes
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
lifetime.Exit += OnExit;
}
}
#endregion
#region Process Incoming Mouse Events
/// <summary>
/// Processes mouse events, which are bubbled
/// through the class' routed events, trigger
/// certain actions (e.g. show a popup), or
/// both.
/// </summary>
/// <param name="me">Event flag.</param>
private void OnMouseEvent(MouseEvent me)
{
if (IsDisposed)
return;
switch (me)
{
case MouseEvent.MouseMove:
// immediately return - there's nothing left to evaluate
return;
case MouseEvent.IconRightMouseDown:
case MouseEvent.IconLeftMouseDown:
case MouseEvent.IconRightMouseUp:
case MouseEvent.IconLeftMouseUp:
case MouseEvent.IconMiddleMouseDown:
case MouseEvent.IconMiddleMouseUp:
case MouseEvent.BalloonToolTipClicked:
case MouseEvent.IconDoubleClick:
MouseEventHandler?.Invoke(this, me);
break;
default:
throw new ArgumentOutOfRangeException(nameof(me), "Missing handler for mouse event flag: " + me);
}
}
#endregion
#region Balloon Tips
/// <summary>
/// Displays a balloon tip with the specified title,
/// text, and icon in the taskbar for the specified time period.
/// </summary>
/// <param name="title">The title to display on the balloon tip.</param>
/// <param name="message">The text to display on the balloon tip.</param>
/// <param name="symbol">A symbol that indicates the severity.</param>
public void ShowBalloonTip(string title, string message, BalloonFlags symbol)
{
lock (lockObject)
{
ShowBalloonTip(title, message, symbol, IntPtr.Zero);
}
}
/// <summary>
/// Invokes <see cref="WinApi.Shell_NotifyIcon"/> in order to display
/// a given balloon ToolTip.
/// </summary>
/// <param name="title">The title to display on the balloon tip.</param>
/// <param name="message">The text to display on the balloon tip.</param>
/// <param name="flags">Indicates what icon to use.</param>
/// <param name="balloonIconHandle">A handle to a custom icon, if any, or
/// <see cref="IntPtr.Zero"/>.</param>
private void ShowBalloonTip(string title, string message, BalloonFlags flags, IntPtr balloonIconHandle)
{
EnsureNotDisposed();
iconData.BalloonText = message ?? string.Empty;
iconData.BalloonTitle = title ?? string.Empty;
iconData.BalloonFlags = flags;
iconData.CustomBalloonIconHandle = balloonIconHandle;
WriteIconData(ref iconData, NotifyCommand.Modify, IconDataMembers.Info | IconDataMembers.Icon);
}
/// <summary>
/// Hides a balloon ToolTip, if any is displayed.
/// </summary>
public void HideBalloonTip()
{
EnsureNotDisposed();
// reset balloon by just setting the info to an empty string
iconData.BalloonText = iconData.BalloonTitle = string.Empty;
WriteIconData(ref iconData, NotifyCommand.Modify, IconDataMembers.Info);
}
#endregion
#region Create / Remove Taskbar Icon
/// <summary>
/// Recreates the taskbar icon if the whole taskbar was
/// recreated (e.g. because Explorer was shut down).
/// </summary>
private void OnTaskbarCreated()
{
IsTaskbarIconCreated = false;
CreateTaskbarIcon();
}
/// <summary>
/// Creates the taskbar icon. This message is invoked during initialization,
/// if the taskbar is restarted, and whenever the icon is displayed.
/// </summary>
private void CreateTaskbarIcon()
{
lock (lockObject)
{
if (IsTaskbarIconCreated)
{
return;
}
const IconDataMembers members = IconDataMembers.Message | IconDataMembers.Icon | IconDataMembers.Tip;
//write initial configuration
var status = WriteIconData(ref iconData, NotifyCommand.Add, members);
if (!status)
{
// couldn't create the icon - we can assume this is because explorer is not running (yet!)
// -> try a bit later again rather than throwing an exception. Typically, if the windows
// shell is being loaded later, this method is being re-invoked from OnTaskbarCreated
// (we could also retry after a delay, but that's currently YAGNI)
return;
}
messageSink.Version = (NotifyIconVersion)iconData.VersionOrTimeout;
IsTaskbarIconCreated = true;
}
}
/// <summary>
/// Closes the taskbar icon if required.
/// </summary>
private void RemoveTaskbarIcon()
{
lock (lockObject)
{
// make sure we didn't schedule a creation
if (!IsTaskbarIconCreated)
{
return;
}
WriteIconData(ref iconData, NotifyCommand.Delete, IconDataMembers.Message);
IsTaskbarIconCreated = false;
}
}
#endregion
#region Dispose / Exit
/// <summary>
/// Set to true as soon as <c>Dispose</c> has been invoked.
/// </summary>
public bool IsDisposed { get; private set; }
/// <summary>
/// Checks if the object has been disposed and
/// raises a <see cref="ObjectDisposedException"/> in case
/// the <see cref="IsDisposed"/> flag is true.
/// </summary>
private void EnsureNotDisposed()
{
if (IsDisposed)
throw new ObjectDisposedException(GetType().FullName);
}
/// <summary>
/// Disposes the class if the application exits.
/// </summary>
private void OnExit(object sender, EventArgs e)
{
Dispose();
}
/// <summary>
/// This destructor will run only if the <see cref="Dispose()"/>
/// method does not get called. This gives this base class the
/// opportunity to finalize.
/// <para>
/// Important: Do not provide destructor in types derived from this class.
/// </para>
/// </summary>
~TaskBarIcon()
{
Dispose(false);
}
/// <summary>
/// Disposes the object.
/// </summary>
/// <remarks>This method is not virtual by design. Derived classes
/// should override <see cref="Dispose(bool)"/>.
/// </remarks>
public void Dispose()
{
Dispose(true);
// This object will be cleaned up by the Dispose method.
// Therefore, you should call GC.SuppressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
GC.SuppressFinalize(this);
}
/// <summary>
/// Closes the tray and releases all resources.
/// </summary>
/// <summary>
/// <c>Dispose(bool disposing)</c> executes in two distinct scenarios.
/// If disposing equals <c>true</c>, the method has been called directly
/// or indirectly by a user's code. Managed and unmanaged resources
/// can be disposed.
/// </summary>
/// <param name="disposing">If disposing equals <c>false</c>, the method
/// has been called by the runtime from inside the finalizer and you
/// should not reference other objects. Only unmanaged resources can
/// be disposed.</param>
/// <remarks>Check the <see cref="IsDisposed"/> property to determine whether
/// the method has already been called.</remarks>
private void Dispose(bool disposing)
{
// don't do anything if the component is already disposed
if (IsDisposed || !disposing)
return;
lock (lockObject)
{
IsDisposed = true;
// de-register application event listener
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
lifetime.Exit -= OnExit;
}
// dispose message sink
messageSink.Dispose();
// remove icon
RemoveTaskbarIcon();
}
}
#endregion
#region WriteIconData
/// <summary>
/// Updates the taskbar icons with data provided by a given
/// <see cref="NotifyIconData"/> instance.
/// </summary>
/// <param name="data">Configuration settings for the NotifyIcon.</param>
/// <param name="command">Operation on the icon (e.g. delete the icon).</param>
/// <param name="flags">Defines which members of the <paramref name="data"/>
/// structure are set.</param>
/// <returns>True if the data was successfully written.</returns>
/// <remarks>See Shell_NotifyIcon documentation on MSDN for details.</remarks>
private bool WriteIconData(ref NotifyIconData data, NotifyCommand command, IconDataMembers flags)
{
data.ValidMembers |= flags;
lock (lockObject)
{
return WinApi.Shell_NotifyIcon(command, ref data);
}
}
#endregion
/// <summary>
/// Reads a given image resource into a WinForms icon.
/// </summary>
/// <param name="imageSource">Image source pointing to
/// an icon file (*.ico).</param>
/// <returns>An icon object that can be used with the
/// taskbar area.</returns>
public static Icon ToIcon(string imageSource)
{
if (imageSource == null)
return null;
var executingAssembly = System.Reflection.Assembly.GetExecutingAssembly();
if (executingAssembly.GetManifestResourceNames().Contains(imageSource))
{
var stream = executingAssembly.GetManifestResourceStream(imageSource);
return new Icon(stream);
}
return null;
}
}
}

@ -0,0 +1,62 @@
using System.Runtime.InteropServices;
using System.Windows.Input;
using MangaReader.Avalonia.Platform.Win.Interop;
namespace MangaReader.Avalonia.Platform.Win
{
public class WindowsTrayIcon : ITrayIcon
{
public ICommand DoubleClickCommand { get; set; }
public ICommand BalloonClickedCommand { get; set; }
private TaskBarIcon taskBarIcon;
private object lastBalloonState;
public void SetIcon()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var iconSource = "MangaReader.Avalonia.Assets.main.ico";
var icon = TaskBarIcon.ToIcon(iconSource);
taskBarIcon = new TaskBarIcon(icon);
taskBarIcon.MouseEventHandler += TaskBarIconOnMouseEventHandler;
}
}
public void ShowBalloon(string text, object state)
{
this.lastBalloonState = state;
taskBarIcon?.ShowBalloonTip(nameof(MangaReader), text, BalloonFlags.Info);
}
private void TaskBarIconOnMouseEventHandler(object sender, MouseEvent e)
{
if (e == MouseEvent.IconDoubleClick)
{
var command = this.DoubleClickCommand;
if (command != null && command.CanExecute(null))
{
command.Execute(null);
}
}
if (e == MouseEvent.BalloonToolTipClicked)
{
var command = this.BalloonClickedCommand;
if (command != null && command.CanExecute(lastBalloonState))
{
command.Execute(lastBalloonState);
}
}
}
public void Dispose()
{
if (taskBarIcon != null)
taskBarIcon.MouseEventHandler -= TaskBarIconOnMouseEventHandler;
taskBarIcon?.Dispose();
}
}
}

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
To use the Avalonia CI feed to get unstable packages, move this file to the root of your solution.
-->
<configuration>
<packageSources>
<add key="AvaloniaCI" value="https://www.myget.org/F/avalonia-ci/api/v2" />
</packageSources>
</configuration>

@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.30804.86
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "hass-workstation-service", "hass-workstation-service\hass-workstation-service.csproj", "{78EC7ACA-8826-4A0A-AA8E-664D03ACBE88}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserInterface", "UserInterface\UserInterface.csproj", "{8ECB6FEE-1AD2-40E3-897D-E75EDB637BB5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -21,6 +23,14 @@ Global
{78EC7ACA-8826-4A0A-AA8E-664D03ACBE88}.Release|Any CPU.Build.0 = Release|Any CPU
{78EC7ACA-8826-4A0A-AA8E-664D03ACBE88}.Release|x86.ActiveCfg = Release|Any CPU
{78EC7ACA-8826-4A0A-AA8E-664D03ACBE88}.Release|x86.Build.0 = Release|Any CPU
{8ECB6FEE-1AD2-40E3-897D-E75EDB637BB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8ECB6FEE-1AD2-40E3-897D-E75EDB637BB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8ECB6FEE-1AD2-40E3-897D-E75EDB637BB5}.Debug|x86.ActiveCfg = Debug|Any CPU
{8ECB6FEE-1AD2-40E3-897D-E75EDB637BB5}.Debug|x86.Build.0 = Debug|Any CPU
{8ECB6FEE-1AD2-40E3-897D-E75EDB637BB5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8ECB6FEE-1AD2-40E3-897D-E75EDB637BB5}.Release|Any CPU.Build.0 = Release|Any CPU
{8ECB6FEE-1AD2-40E3-897D-E75EDB637BB5}.Release|x86.ActiveCfg = Release|Any CPU
{8ECB6FEE-1AD2-40E3-897D-E75EDB637BB5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace hass_workstation_service.Communication.NamedPipe
{
public interface ServiceContractInterfaces
{
public string Ping(string str);
void WriteMqttBrokerSettings(string host, string username, string password);
}
}

@ -17,7 +17,7 @@ namespace hass_workstation_service.Communication
{
private readonly IMqttClient _mqttClient;
private readonly ILogger<MqttPublisher> _logger;
private readonly ConfigurationService _configurationService;
private readonly IConfigurationService _configurationService;
public DateTime LastConfigAnnounce { get; private set; }
public DeviceConfigModel DeviceConfigModel { get; private set; }
public bool IsConnected
@ -38,7 +38,7 @@ namespace hass_workstation_service.Communication
public MqttPublisher(
ILogger<MqttPublisher> logger,
DeviceConfigModel deviceConfigModel,
ConfigurationService configurationService)
IConfigurationService configurationService)
{
this._logger = logger;
@ -46,6 +46,7 @@ namespace hass_workstation_service.Communication
this._configurationService = configurationService;
var options = _configurationService.ReadMqttSettings().Result;
_configurationService.MqqtConfigChangedHandler = this.ReplaceMqttClient;
var factory = new MqttFactory();
this._mqttClient = factory.CreateMqttClient();
@ -54,17 +55,21 @@ namespace hass_workstation_service.Communication
// configure what happens on disconnect
this._mqttClient.UseDisconnectedHandler(async e =>
{
_logger.LogWarning("Disconnected from server");
await Task.Delay(TimeSpan.FromSeconds(5));
try
{
await this._mqttClient.ConnectAsync(options, CancellationToken.None);
}
catch (Exception ex)
if (e.ReasonCode != MQTTnet.Client.Disconnecting.MqttClientDisconnectReason.NormalDisconnection)
{
_logger.LogError(ex, "Reconnecting failed");
_logger.LogWarning("Disconnected from server");
await Task.Delay(TimeSpan.FromSeconds(5));
try
{
await this._mqttClient.ConnectAsync(options, CancellationToken.None);
}
catch (Exception ex)
{
_logger.LogError(ex, "Reconnecting failed");
}
}
});
}
@ -76,7 +81,7 @@ namespace hass_workstation_service.Communication
}
else
{
this._logger.LogInformation($"message dropped because mqtt not connected: {message}");
this._logger.LogInformation($"Message dropped because mqtt not connected: {message}");
}
}
@ -99,5 +104,24 @@ namespace hass_workstation_service.Communication
LastConfigAnnounce = DateTime.UtcNow;
}
}
public async void ReplaceMqttClient(IMqttClientOptions options)
{
this._logger.LogInformation($"Replacing Mqtt client with new config");
await _mqttClient.DisconnectAsync();
try
{
await _mqttClient.ConnectAsync(options);
}
catch (Exception ex)
{
Log.Logger.Error("Could not connect to broker: " + ex.Message);
}
finally
{
Log.Logger.Information("Connected to new broker");
}
}
}
}

@ -2,9 +2,11 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.IO.IsolatedStorage;
using System.Security;
using System.Text.Json;
using System.Threading.Tasks;
using hass_workstation_service.Communication;
using hass_workstation_service.Communication.NamedPipe;
using hass_workstation_service.Domain.Sensors;
using Microsoft.Extensions.Configuration;
using MQTTnet;
@ -14,9 +16,11 @@ using Serilog;
namespace hass_workstation_service.Data
{
public class ConfigurationService
public class ConfigurationService : ServiceContractInterfaces, IConfigurationService
{
public ICollection<AbstractSensor> ConfiguredSensors { get; private set; }
public Action<IMqttClientOptions> MqqtConfigChangedHandler { get; set; }
private readonly IsolatedStorageFile _fileStorage;
public ConfigurationService()
@ -64,8 +68,9 @@ namespace hass_workstation_service.Data
configuredBroker = await JsonSerializer.DeserializeAsync<ConfiguredMqttBroker>(stream);
}
stream.Close();
if (configuredBroker != null)
if (configuredBroker != null && configuredBroker.Host != null)
{
var mqttClientOptions = new MqttClientOptionsBuilder()
.WithTcpServer(configuredBroker.Host)
// .WithTls()
@ -110,5 +115,38 @@ namespace hass_workstation_service.Data
sensors.ForEach((sensor) => this.ConfiguredSensors.Add(sensor));
WriteSettings();
}
public async void WriteMqttBrokerSettings(string host, string username, string password)
{
IsolatedStorageFileStream stream = this._fileStorage.OpenFile("mqttbroker.json", FileMode.OpenOrCreate);
Log.Logger.Information($"writing configured mqttbroker to: {stream.Name}");
ConfiguredMqttBroker configuredBroker = new ConfiguredMqttBroker()
{
Host = host,
Username = username,
Password = password
};
await JsonSerializer.SerializeAsync(stream, configuredBroker);
stream.Close();
this.MqqtConfigChangedHandler.Invoke(await this.ReadMqttSettings());
}
/// <summary>
/// You can use this to check if the application responds.
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public string Ping(string str)
{
if (str == "ping")
{
return "pong";
}
return "what?";
}
}
}

@ -7,6 +7,6 @@ namespace hass_workstation_service.Data
{
public string Host { get; set; }
public string Username { get; set; }
public SecureString Password { get; set; }
public string Password { get; set; }
}
}

@ -0,0 +1,24 @@
using hass_workstation_service.Communication;
using hass_workstation_service.Domain.Sensors;
using MQTTnet.Client.Options;
using System;
using System.Collections.Generic;
using System.Security;
using System.Threading.Tasks;
namespace hass_workstation_service.Data
{
public interface IConfigurationService
{
ICollection<AbstractSensor> ConfiguredSensors { get; }
Action<IMqttClientOptions> MqqtConfigChangedHandler { get; set; }
void AddConfiguredSensor(AbstractSensor sensor);
void AddConfiguredSensors(List<AbstractSensor> sensors);
string Ping(string str);
Task<IMqttClientOptions> ReadMqttSettings();
void ReadSensorSettings(MqttPublisher publisher);
void WriteMqttBrokerSettings(string host, string username, string password);
void WriteSettings();
}
}

@ -16,6 +16,8 @@ using System.IO.IsolatedStorage;
using System.Reflection;
using System.IO;
using Microsoft.Win32;
using JKang.IpcServiceFramework.Hosting;
using hass_workstation_service.Communication.NamedPipe;
namespace hass_workstation_service
{
@ -86,9 +88,15 @@ namespace hass_workstation_service
Sw_version = GetVersion()
};
services.AddSingleton(deviceConfig);
services.AddSingleton<ConfigurationService>();
ConfigurationService configurationService = new ConfigurationService();
services.AddSingleton<ServiceContractInterfaces>(configurationService);
services.AddSingleton<IConfigurationService>(configurationService);
services.AddSingleton<MqttPublisher>();
services.AddHostedService<Worker>();
}).ConfigureIpcHost(builder =>
{
// configure IPC endpoints
builder.AddNamedPipeEndpoint<ServiceContractInterfaces>(pipeName: "pipeinternal");
});
static internal string GetVersion()
{

@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ApplicationRevision>33</ApplicationRevision>
<ApplicationRevision>34</ApplicationRevision>
<ApplicationVersion>1.0.0.*</ApplicationVersion>
<BootstrapperEnabled>True</BootstrapperEnabled>
<Configuration>Release</Configuration>

@ -15,11 +15,11 @@ namespace hass_workstation_service
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly ConfigurationService _configurationService;
private readonly IConfigurationService _configurationService;
private readonly MqttPublisher _mqttPublisher;
public Worker(ILogger<Worker> logger,
ConfigurationService configuredSensorsService,
IConfigurationService configuredSensorsService,
MqttPublisher mqttPublisher)
{
_logger = logger;

@ -4,7 +4,7 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
<UserSecretsId>dotnet-hass_workstation_service-C65C2EBE-1977-4C24-AC6B-6921877E1390</UserSecretsId>
<RootNamespace>hass_workstation_service</RootNamespace>
<OutputType>WinExe</OutputType>
<OutputType>Exe</OutputType>
<Authors>Sleevezipper</Authors>
<RepositoryUrl>https://github.com/sleevezipper/hass-workstation-service</RepositoryUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
@ -30,7 +30,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.10" />
<PackageReference Include="JKang.IpcServiceFramework.Hosting.NamedPipe" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="MQTTnet" Version="3.0.13" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />

Loading…
Cancel
Save