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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions App.Avalonia/App.Avalonia.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Coder.Desktop.App</RootNamespace>
<AssemblyName>Coder Desktop</AssemblyName>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.3" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.3" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.3" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.3" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
</ItemGroup>

<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
<ProjectReference Include="..\Vpn.Windows\Vpn.Windows.csproj" />
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))">
<ProjectReference Include="..\App.Linux\App.Linux.csproj" />
<ProjectReference Include="..\Vpn.Linux\Vpn.Linux.csproj" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\App.Shared\App.Shared.csproj" Aliases="global,AppShared" />
</ItemGroup>

<ItemGroup>
<AvaloniaResource Include="Assets\\**" />
</ItemGroup>
</Project>
41 changes: 41 additions & 0 deletions App.Avalonia/App.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Coder.Desktop.App.Converters"
x:Class="Coder.Desktop.App.App"
RequestedThemeVariant="Default">

<!--
Note: pages are implemented in parallel tasks (T5/T6). We register
converters here so future pages can reference them via StaticResource.
-->
<Application.Resources>
<converters:InverseBoolConverter x:Key="InverseBoolConverter" />
<converters:BoolToObjectConverter x:Key="BoolToObjectConverter" />
<converters:FriendlyByteConverter x:Key="FriendlyByteConverter" />
<converters:VpnLifecycleToBoolConverter x:Key="VpnLifecycleToBoolConverter" />
<converters:StringToBrushSelector x:Key="StringToBrushSelector" />
<converters:StringToStringSelector x:Key="StringToStringSelector" />
</Application.Resources>

<Application.Styles>
<FluentTheme />
</Application.Styles>

<TrayIcon.Icons>
<TrayIcons>
<TrayIcon Icon="/Assets/coder.ico"
ToolTipText="Coder Desktop"
Command="{Binding ShowWindowCommand}">
<TrayIcon.Menu>
<NativeMenu>
<NativeMenuItem Header="Show" Command="{Binding ShowWindowCommand}" />
<NativeMenuItemSeparator />
<NativeMenuItem Header="Check for Updates" />
<NativeMenuItemSeparator />
<NativeMenuItem Header="Exit" Command="{Binding ExitCommand}" />
</NativeMenu>
</TrayIcon.Menu>
</TrayIcon>
</TrayIcons>
</TrayIcon.Icons>
</Application>
112 changes: 112 additions & 0 deletions App.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Coder.Desktop.App.Services;
using Coder.Desktop.App.ViewModels;
using Coder.Desktop.App.Views;
using Microsoft.Extensions.DependencyInjection;

namespace Coder.Desktop.App;

public partial class App : Application
{
private ServiceProvider? _services;
private TrayWindow? _trayWindow;
private TrayIconViewModel? _trayIconViewModel;

public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}

public override void OnFrameworkInitializationCompleted()
{
var services = new ServiceCollection();

// Register cross-platform services from App.Shared
services.AddSingleton<IDispatcher, AvaloniaDispatcher>();
services.AddSingleton<IClipboardService, AvaloniaClipboardService>();
services.AddSingleton<ILauncherService, ProcessLauncherService>();

_services = services.BuildServiceProvider();

if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;

// Create the tray popup window immediately so we can show/hide it
// quickly from the tray icon.
_trayWindow = new TrayWindow();

// Make TrayWindow the MainWindow so dialogs can use it as an owner.
desktop.MainWindow = _trayWindow;

// Keep it hidden on startup; the user will open it from the tray.
// (StartWithClassicDesktopLifetime will show MainWindow by default.)
_trayWindow.RequestHideOnFirstOpen();

_trayIconViewModel = new TrayIconViewModel(ToggleTrayWindow, () => desktop.Shutdown());
ConfigureTrayIcons(_trayIconViewModel);
}

base.OnFrameworkInitializationCompleted();
}

private void ConfigureTrayIcons(TrayIconViewModel trayIconViewModel)
{
// The tray icons are defined in App.axaml via the TrayIcon.Icons attached property.
var icons = TrayIcon.GetIcons(this);
if (icons is null)
return;

foreach (var trayIcon in icons)
{
// Ensure clicking the icon toggles the tray window.
trayIcon.Clicked -= TrayIconOnClicked;
trayIcon.Clicked += TrayIconOnClicked;

// Also set the command explicitly so bindings don't depend on a DataContext
// being available for tray icon objects.
trayIcon.Command = trayIconViewModel.ShowWindowCommand;

if (trayIcon.Menu is NativeMenu menu)
{
foreach (var item in menu.Items)
{
if (item is not NativeMenuItem nativeItem)
continue;

switch (nativeItem.Header?.ToString())
{
case "Show":
nativeItem.Command = trayIconViewModel.ShowWindowCommand;
break;
case "Exit":
nativeItem.Command = trayIconViewModel.ExitCommand;
break;
}
}
}
}
}

private void TrayIconOnClicked(object? sender, EventArgs e)
{
ToggleTrayWindow();
}

private void ToggleTrayWindow()
{
if (_trayWindow is null)
return;

if (_trayWindow.IsVisible)
{
_trayWindow.Hide();
return;
}

_trayWindow.ShowNearSystemTray();
}
}
Binary file added App.Avalonia/Assets/coder.ico
Binary file not shown.
27 changes: 27 additions & 0 deletions App.Avalonia/Controls/ExpandChevron.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Coder.Desktop.App.Controls.ExpandChevron">

<Grid>
<PathIcon x:Name="ChevronIcon"
Width="16"
Height="16"
Margin="0,0,8,0"
RenderTransformOrigin="0.5,0.5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=UserControl}}"
Data="M 6,3 L 10.5,8 L 6,13 L 7.4,14.4 L 13.8,8 L 7.4,1.6 Z">

<PathIcon.RenderTransform>
<RotateTransform Angle="0">
<RotateTransform.Transitions>
<Transitions>
<DoubleTransition Property="Angle" Duration="0:0:0.15" />
</Transitions>
</RotateTransform.Transitions>
</RotateTransform>
</PathIcon.RenderTransform>
</PathIcon>
</Grid>
</UserControl>
48 changes: 48 additions & 0 deletions App.Avalonia/Controls/ExpandChevron.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;

namespace Coder.Desktop.App.Controls;

public partial class ExpandChevron : UserControl
{
public static readonly StyledProperty<bool> IsOpenProperty =
AvaloniaProperty.Register<ExpandChevron, bool>(nameof(IsOpen));

static ExpandChevron()
{
IsOpenProperty.Changed.AddClassHandler<ExpandChevron>((x, e) =>
{
if (e.NewValue is bool isOpen)
{
x.UpdateChevronAngle(isOpen);
}
});
}

public bool IsOpen
{
get => GetValue(IsOpenProperty);
set => SetValue(IsOpenProperty, value);
}

public ExpandChevron()
{
InitializeComponent();

UpdateChevronAngle(IsOpen);
}

private void UpdateChevronAngle(bool isOpen)
{
if (ChevronIcon is null)
{
return;
}

if (ChevronIcon.RenderTransform is RotateTransform rotate)
{
rotate.Angle = isOpen ? 90 : 0;
}
}
}
28 changes: 28 additions & 0 deletions App.Avalonia/Controls/ExpandContent.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Coder.Desktop.App.Controls.ExpandContent">

<Grid x:Name="CollapsiblePanel"
Opacity="0"
IsVisible="False"
MaxHeight="0"
ClipToBounds="True">

<Grid.Transitions>
<Transitions>
<DoubleTransition Property="MaxHeight" Duration="0:0:0.16" />
<DoubleTransition Property="Opacity" Duration="0:0:0.16" />
</Transitions>
</Grid.Transitions>

<Grid.RenderTransform>
<TranslateTransform Y="-16">
<TranslateTransform.Transitions>
<Transitions>
<DoubleTransition Property="Y" Duration="0:0:0.16" />
</Transitions>
</TranslateTransform.Transitions>
</TranslateTransform>
</Grid.RenderTransform>
</Grid>
</UserControl>
Loading
Loading