diff --git a/App.Avalonia/App.Avalonia.csproj b/App.Avalonia/App.Avalonia.csproj new file mode 100644 index 0000000..8260fbe --- /dev/null +++ b/App.Avalonia/App.Avalonia.csproj @@ -0,0 +1,41 @@ + + + WinExe + net8.0 + Coder.Desktop.App + Coder Desktop + enable + enable + preview + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/App.Avalonia/App.axaml b/App.Avalonia/App.axaml new file mode 100644 index 0000000..c94c40e --- /dev/null +++ b/App.Avalonia/App.axaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/App.Avalonia/App.axaml.cs b/App.Avalonia/App.axaml.cs new file mode 100644 index 0000000..1b800aa --- /dev/null +++ b/App.Avalonia/App.axaml.cs @@ -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(); + services.AddSingleton(); + services.AddSingleton(); + + _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(); + } +} diff --git a/App.Avalonia/Assets/coder.ico b/App.Avalonia/Assets/coder.ico new file mode 100644 index 0000000..b80bdc2 Binary files /dev/null and b/App.Avalonia/Assets/coder.ico differ diff --git a/App.Avalonia/Controls/ExpandChevron.axaml b/App.Avalonia/Controls/ExpandChevron.axaml new file mode 100644 index 0000000..6d7f51d --- /dev/null +++ b/App.Avalonia/Controls/ExpandChevron.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + diff --git a/App.Avalonia/Controls/ExpandChevron.axaml.cs b/App.Avalonia/Controls/ExpandChevron.axaml.cs new file mode 100644 index 0000000..81976a4 --- /dev/null +++ b/App.Avalonia/Controls/ExpandChevron.axaml.cs @@ -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 IsOpenProperty = + AvaloniaProperty.Register(nameof(IsOpen)); + + static ExpandChevron() + { + IsOpenProperty.Changed.AddClassHandler((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; + } + } +} diff --git a/App.Avalonia/Controls/ExpandContent.axaml b/App.Avalonia/Controls/ExpandContent.axaml new file mode 100644 index 0000000..b7fe4d0 --- /dev/null +++ b/App.Avalonia/Controls/ExpandContent.axaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/App.Avalonia/Controls/ExpandContent.axaml.cs b/App.Avalonia/Controls/ExpandContent.axaml.cs new file mode 100644 index 0000000..10f644a --- /dev/null +++ b/App.Avalonia/Controls/ExpandContent.axaml.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Metadata; + +namespace Coder.Desktop.App.Controls; + +public partial class ExpandContent : UserControl +{ + public static readonly StyledProperty IsOpenProperty = + AvaloniaProperty.Register(nameof(IsOpen)); + + static ExpandContent() + { + IsOpenProperty.Changed.AddClassHandler((x, e) => + { + if (e.NewValue is bool isOpen) + { + x.SetOpenState(isOpen, immediate: false); + } + }); + } + + private static readonly TimeSpan TransitionDuration = TimeSpan.FromMilliseconds(160); + + private CancellationTokenSource? _collapseCts; + + private TranslateTransform? _slideTransform; + + [Content] + public IList Children => CollapsiblePanel.Children; + + public bool IsOpen + { + get => GetValue(IsOpenProperty); + set => SetValue(IsOpenProperty, value); + } + + public ExpandContent() + { + InitializeComponent(); + + _slideTransform = CollapsiblePanel.RenderTransform as TranslateTransform; + + SetOpenState(IsOpen, immediate: true); + } + + private void SetOpenState(bool isOpen, bool immediate) + { + if (CollapsiblePanel is null || _slideTransform is null) + { + return; + } + + if (isOpen) + { + _collapseCts?.Cancel(); + CollapsiblePanel.IsVisible = true; + + CollapsiblePanel.Opacity = 1; + CollapsiblePanel.MaxHeight = 10000; + _slideTransform.Y = 0; + return; + } + + CollapsiblePanel.Opacity = 0; + CollapsiblePanel.MaxHeight = 0; + _slideTransform.Y = -16; + + if (immediate) + { + CollapsiblePanel.IsVisible = false; + return; + } + + ScheduleHideAfterCollapse(); + } + + private async void ScheduleHideAfterCollapse() + { + _collapseCts?.Cancel(); + _collapseCts = new CancellationTokenSource(); + var token = _collapseCts.Token; + + try + { + await Task.Delay(TransitionDuration, token); + } + catch (TaskCanceledException) + { + return; + } + + if (!token.IsCancellationRequested && !IsOpen) + { + CollapsiblePanel.IsVisible = false; + } + } +} diff --git a/App.Avalonia/Controls/HorizontalRule.axaml b/App.Avalonia/Controls/HorizontalRule.axaml new file mode 100644 index 0000000..5f3009e --- /dev/null +++ b/App.Avalonia/Controls/HorizontalRule.axaml @@ -0,0 +1,9 @@ + + + + diff --git a/App.Avalonia/Controls/HorizontalRule.axaml.cs b/App.Avalonia/Controls/HorizontalRule.axaml.cs new file mode 100644 index 0000000..31c2a12 --- /dev/null +++ b/App.Avalonia/Controls/HorizontalRule.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Coder.Desktop.App.Controls; + +public partial class HorizontalRule : UserControl +{ + public HorizontalRule() + { + InitializeComponent(); + } +} diff --git a/App.Avalonia/Converters/BoolToObjectConverter.cs b/App.Avalonia/Converters/BoolToObjectConverter.cs new file mode 100644 index 0000000..629562a --- /dev/null +++ b/App.Avalonia/Converters/BoolToObjectConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Coder.Desktop.App.Converters; + +public sealed class BoolToObjectConverter : IValueConverter +{ + public object? TrueValue { get; set; } = true; + + public object? FalseValue { get; set; } = true; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is true ? TrueValue : FalseValue; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/App.Avalonia/Converters/DependencyObjectSelector.cs b/App.Avalonia/Converters/DependencyObjectSelector.cs new file mode 100644 index 0000000..4f44ce7 --- /dev/null +++ b/App.Avalonia/Converters/DependencyObjectSelector.cs @@ -0,0 +1,152 @@ +using System; +using System.Linq; +using Avalonia; +using Avalonia.Collections; +using Avalonia.Media; +using Avalonia.Metadata; + +namespace Coder.Desktop.App.Converters; + +/// +/// An item in a . +/// +/// Key type. +/// Value type. +public class DependencyObjectSelectorItem : AvaloniaObject + where TK : IEquatable + where TV : class +{ + public static readonly StyledProperty KeyProperty = + AvaloniaProperty.Register, TK?>(nameof(Key)); + + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register, TV?>(nameof(Value)); + + public TK? Key + { + get => GetValue(KeyProperty); + set => SetValue(KeyProperty, value); + } + + public TV? Value + { + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } +} + +/// +/// Avalonia port of the WinUI DependencyObjectSelector. +/// +/// This is a simplified implementation intended for XAML resources where a view-model key +/// selects a value from a list. +/// +/// Key type. +/// Value type. +public class DependencyObjectSelector : AvaloniaObject + where TK : IEquatable + where TV : class +{ + public static readonly StyledProperty SelectedKeyProperty = + AvaloniaProperty.Register, TK?>(nameof(SelectedKey)); + + public static readonly DirectProperty, TV?> SelectedObjectProperty = + AvaloniaProperty.RegisterDirect, TV?>(nameof(SelectedObject), o => o.SelectedObject); + + private TV? _selectedObject; + private DependencyObjectSelectorItem? _selectedItem; + + public DependencyObjectSelector() + { + References = new AvaloniaList>(); + References.CollectionChanged += (_, __) => UpdateSelectedObject(); + + UpdateSelectedObject(); + } + + /// + /// The list of possible references. + /// + [Content] + public AvaloniaList> References { get; } + + /// + /// The key to select. + /// + public TK? SelectedKey + { + get => GetValue(SelectedKeyProperty); + set => SetValue(SelectedKeyProperty, value); + } + + /// + /// The selected value. + /// + public TV? SelectedObject + { + get => _selectedObject; + private set => SetAndRaise(SelectedObjectProperty, ref _selectedObject, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == SelectedKeyProperty) + { + UpdateSelectedObject(); + } + } + + private void UpdateSelectedObject() + { + VerifyReferencesUniqueKeys(); + + var item = References.FirstOrDefault(i => + (i.Key == null && SelectedKey == null) || + (i.Key != null && SelectedKey != null && i.Key.Equals(SelectedKey))) + ?? References.FirstOrDefault(i => i.Key == null); + + if (!ReferenceEquals(item, _selectedItem)) + { + if (_selectedItem != null) + { + _selectedItem.PropertyChanged -= SelectedItem_OnPropertyChanged; + } + + _selectedItem = item; + + if (_selectedItem != null) + { + _selectedItem.PropertyChanged += SelectedItem_OnPropertyChanged; + } + } + + SelectedObject = item?.Value; + } + + private void SelectedItem_OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == DependencyObjectSelectorItem.ValueProperty) + { + SelectedObject = _selectedItem?.Value; + } + } + + private void VerifyReferencesUniqueKeys() + { + var keys = References.Select(i => i.Key).Distinct().ToArray(); + if (keys.Length != References.Count) + { + throw new ArgumentException("DependencyObjectSelector keys must be unique."); + } + } +} + +public sealed class StringToBrushSelectorItem : DependencyObjectSelectorItem; + +public sealed class StringToBrushSelector : DependencyObjectSelector; + +public sealed class StringToStringSelectorItem : DependencyObjectSelectorItem; + +public sealed class StringToStringSelector : DependencyObjectSelector; diff --git a/App.Avalonia/Converters/FriendlyByteConverter.cs b/App.Avalonia/Converters/FriendlyByteConverter.cs new file mode 100644 index 0000000..9cef9e4 --- /dev/null +++ b/App.Avalonia/Converters/FriendlyByteConverter.cs @@ -0,0 +1,30 @@ +extern alias AppShared; + +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using SharedFriendlyByteConverter = AppShared::Coder.Desktop.App.Converters.FriendlyByteConverter; + +namespace Coder.Desktop.App.Converters; + +public sealed class FriendlyByteConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + var bytes = value switch + { + int i => i < 0 ? 0ul : (ulong)i, + uint ui => ui, + long l => l < 0 ? 0ul : (ulong)l, + ulong ul => ul, + _ => 0ul, + }; + + return SharedFriendlyByteConverter.FriendlyBytes(bytes); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/App.Avalonia/Converters/InverseBoolConverter.cs b/App.Avalonia/Converters/InverseBoolConverter.cs new file mode 100644 index 0000000..3d85c8d --- /dev/null +++ b/App.Avalonia/Converters/InverseBoolConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Coder.Desktop.App.Converters; + +public sealed class InverseBoolConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is false; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/App.Avalonia/Converters/VpnLifecycleToBoolConverter.cs b/App.Avalonia/Converters/VpnLifecycleToBoolConverter.cs new file mode 100644 index 0000000..2925aea --- /dev/null +++ b/App.Avalonia/Converters/VpnLifecycleToBoolConverter.cs @@ -0,0 +1,42 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Coder.Desktop.App.Models; + +namespace Coder.Desktop.App.Converters; + +public sealed class VpnLifecycleToBoolConverter : IValueConverter +{ + public bool Unknown { get; set; } = false; + + public bool Starting { get; set; } = false; + + public bool Started { get; set; } = false; + + public bool Stopping { get; set; } = false; + + public bool Stopped { get; set; } = false; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not VpnLifecycle lifecycle) + { + return Stopped; + } + + return lifecycle switch + { + VpnLifecycle.Unknown => Unknown, + VpnLifecycle.Starting => Starting, + VpnLifecycle.Started => Started, + VpnLifecycle.Stopping => Stopping, + VpnLifecycle.Stopped => Stopped, + _ => false, + }; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } +} diff --git a/App.Avalonia/Program.cs b/App.Avalonia/Program.cs new file mode 100644 index 0000000..d006f7f --- /dev/null +++ b/App.Avalonia/Program.cs @@ -0,0 +1,18 @@ +using Avalonia; + +namespace Coder.Desktop.App; + +public static class Program +{ + [STAThread] + public static void Main(string[] args) + { + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + } + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} diff --git a/App.Avalonia/Services/AvaloniaClipboardService.cs b/App.Avalonia/Services/AvaloniaClipboardService.cs new file mode 100644 index 0000000..7e30226 --- /dev/null +++ b/App.Avalonia/Services/AvaloniaClipboardService.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Input.Platform; +using Coder.Desktop.App.Services; + +namespace Coder.Desktop.App; + +public class AvaloniaClipboardService : IClipboardService +{ + public async Task SetTextAsync(string text) + { + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var clipboard = desktop.MainWindow?.Clipboard; + if (clipboard != null) + await clipboard.SetTextAsync(text); + } + } +} diff --git a/App.Avalonia/Services/AvaloniaDispatcher.cs b/App.Avalonia/Services/AvaloniaDispatcher.cs new file mode 100644 index 0000000..ebaa8f1 --- /dev/null +++ b/App.Avalonia/Services/AvaloniaDispatcher.cs @@ -0,0 +1,17 @@ +using Avalonia.Threading; +using IDispatcher = Coder.Desktop.App.Services.IDispatcher; + +namespace Coder.Desktop.App; + +public class AvaloniaDispatcher : IDispatcher +{ + public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); + + public void Post(Action action) + { + if (Dispatcher.UIThread.CheckAccess()) + action(); + else + Dispatcher.UIThread.Post(action); + } +} diff --git a/App.Avalonia/Services/ProcessLauncherService.cs b/App.Avalonia/Services/ProcessLauncherService.cs new file mode 100644 index 0000000..c3475e5 --- /dev/null +++ b/App.Avalonia/Services/ProcessLauncherService.cs @@ -0,0 +1,16 @@ +using System.Diagnostics; +using Coder.Desktop.App.Services; + +namespace Coder.Desktop.App; + +public class ProcessLauncherService : ILauncherService +{ + public Task LaunchUriAsync(Uri uri) + { + Process.Start(new ProcessStartInfo(uri.ToString()) + { + UseShellExecute = true + }); + return Task.CompletedTask; + } +} diff --git a/App.Avalonia/ViewModels/TrayIconViewModel.cs b/App.Avalonia/ViewModels/TrayIconViewModel.cs new file mode 100644 index 0000000..b0f68db --- /dev/null +++ b/App.Avalonia/ViewModels/TrayIconViewModel.cs @@ -0,0 +1,17 @@ +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace Coder.Desktop.App.ViewModels; + +public sealed class TrayIconViewModel : ObservableObject +{ + public IRelayCommand ShowWindowCommand { get; } + public IRelayCommand ExitCommand { get; } + + public TrayIconViewModel(Action showOrToggleWindow, Action exit) + { + ShowWindowCommand = new RelayCommand(showOrToggleWindow); + ExitCommand = new RelayCommand(exit); + } +} diff --git a/App.Avalonia/ViewModels/TrayWindowShellViewModel.cs b/App.Avalonia/ViewModels/TrayWindowShellViewModel.cs new file mode 100644 index 0000000..39dd195 --- /dev/null +++ b/App.Avalonia/ViewModels/TrayWindowShellViewModel.cs @@ -0,0 +1,44 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Coder.Desktop.App.ViewModels; + +public enum TrayWindowShellPage +{ + Loading, + Disconnected, + LoginRequired, + Main, +} + +/// +/// A light-weight shell ViewModel for the TrayWindow. +/// +/// The WinUI implementation performed page switching in code-behind based on +/// RPC + credential state. For Avalonia we keep the shell state in a VM so the +/// view can swap page content via a ContentControl. +/// +public sealed partial class TrayWindowShellViewModel : ObservableObject +{ + [ObservableProperty] + public partial TrayWindowShellPage Page { get; set; } = TrayWindowShellPage.Loading; + + /// + /// Height of the content host. We animate this value via Avalonia + /// transitions to approximate the WinUI height storyboard behavior. + /// + [ObservableProperty] + public partial double ContentHeight { get; set; } = 240; + + partial void OnPageChanged(TrayWindowShellPage value) + { + // These are placeholder sizes until real pages land. + ContentHeight = value switch + { + TrayWindowShellPage.Loading => 160, + TrayWindowShellPage.Disconnected => 220, + TrayWindowShellPage.LoginRequired => 240, + TrayWindowShellPage.Main => 420, + _ => 240, + }; + } +} diff --git a/App.Avalonia/Views/DirectoryPickerWindow.axaml b/App.Avalonia/Views/DirectoryPickerWindow.axaml new file mode 100644 index 0000000..fefbbd8 --- /dev/null +++ b/App.Avalonia/Views/DirectoryPickerWindow.axaml @@ -0,0 +1,12 @@ + + + + diff --git a/App.Avalonia/Views/DirectoryPickerWindow.axaml.cs b/App.Avalonia/Views/DirectoryPickerWindow.axaml.cs new file mode 100644 index 0000000..a6e3d34 --- /dev/null +++ b/App.Avalonia/Views/DirectoryPickerWindow.axaml.cs @@ -0,0 +1,58 @@ +using System; +using Avalonia.Controls; +using Coder.Desktop.App.ViewModels; + +namespace Coder.Desktop.App.Views; + +public partial class DirectoryPickerWindow : Window +{ + private DirectoryPickerViewModel? _vm; + + public DirectoryPickerWindow() + { + InitializeComponent(); + + DataContextChanged += (_, _) => AttachViewModel(); + Closed += (_, _) => DetachViewModel(); + + AttachViewModel(); + } + + public DirectoryPickerWindow(DirectoryPickerViewModel vm) : this() + { + DataContext = vm; + } + + private void AttachViewModel() + { + DetachViewModel(); + + _vm = DataContext as DirectoryPickerViewModel; + if (_vm is null) + return; + + _vm.PathSelected += VmOnPathSelected; + _vm.CloseRequested += VmOnCloseRequested; + } + + private void DetachViewModel() + { + if (_vm is null) + return; + + _vm.PathSelected -= VmOnPathSelected; + _vm.CloseRequested -= VmOnCloseRequested; + _vm = null; + } + + private void VmOnCloseRequested(object? sender, EventArgs e) + { + Close(); + } + + private void VmOnPathSelected(object? sender, string? path) + { + // ShowDialog will return the value passed to Close(T). + Close(path); + } +} diff --git a/App.Avalonia/Views/FileSyncListWindow.axaml b/App.Avalonia/Views/FileSyncListWindow.axaml new file mode 100644 index 0000000..2012e15 --- /dev/null +++ b/App.Avalonia/Views/FileSyncListWindow.axaml @@ -0,0 +1,11 @@ + + + + diff --git a/App.Avalonia/Views/FileSyncListWindow.axaml.cs b/App.Avalonia/Views/FileSyncListWindow.axaml.cs new file mode 100644 index 0000000..8a2a66b --- /dev/null +++ b/App.Avalonia/Views/FileSyncListWindow.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Coder.Desktop.App.Views; + +public partial class FileSyncListWindow : Window +{ + public FileSyncListWindow() + { + InitializeComponent(); + } +} diff --git a/App.Avalonia/Views/MainWindow.axaml b/App.Avalonia/Views/MainWindow.axaml new file mode 100644 index 0000000..1721bdd --- /dev/null +++ b/App.Avalonia/Views/MainWindow.axaml @@ -0,0 +1,10 @@ + + + diff --git a/App.Avalonia/Views/MainWindow.axaml.cs b/App.Avalonia/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..70fc59b --- /dev/null +++ b/App.Avalonia/Views/MainWindow.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Coder.Desktop.App.Views; + +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + } +} diff --git a/App.Avalonia/Views/MessageWindow.axaml b/App.Avalonia/Views/MessageWindow.axaml new file mode 100644 index 0000000..af2506a --- /dev/null +++ b/App.Avalonia/Views/MessageWindow.axaml @@ -0,0 +1,20 @@ + + + + +