begin application build overhaul

This commit is contained in:
Nathan
2025-12-17 15:34:34 -07:00
parent 5cc6060323
commit 610cdb62a8
23 changed files with 2379 additions and 0 deletions

53
Views/MainWindow.axaml Normal file
View File

@@ -0,0 +1,53 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="UnifiedFarmLauncher.Views.MainWindow"
Title="Unified Farm Launcher"
Width="1000" Height="700"
MinWidth="800" MinHeight="600">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<!-- Toolbar -->
<StackPanel Orientation="Horizontal" Margin="5" Grid.Row="0">
<Button Name="AddWorkerButton" Content="Add Worker" Margin="5" Width="120"/>
<Button Name="EditWorkerButton" Content="Edit Worker" Margin="5" Width="120"/>
<Button Name="DeleteWorkerButton" Content="Delete Worker" Margin="5" Width="120"/>
<Separator Margin="10,0"/>
<Button Name="StartWorkerButton" Content="Start" Margin="5" Width="80"/>
<Button Name="StopWorkerButton" Content="Stop" Margin="5" Width="80"/>
<Button Name="AttachWorkerButton" Content="Attach" Margin="5" Width="80"/>
</StackPanel>
<!-- Worker Type Filter -->
<TabControl Name="WorkerTypeTabs" Grid.Row="1" Margin="5,0">
<TabItem Header="All Workers">
<TextBlock Text="All Workers" IsVisible="False"/>
</TabItem>
<TabItem Header="SheepIt">
<TextBlock Text="SheepIt" IsVisible="False"/>
</TabItem>
<TabItem Header="Flamenco">
<TextBlock Text="Flamenco" IsVisible="False"/>
</TabItem>
</TabControl>
<!-- Worker List -->
<DataGrid Name="WorkersGrid" Grid.Row="2" Margin="5"
AutoGenerateColumns="False"
IsReadOnly="True"
SelectionMode="Single"
GridLinesVisibility="All">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="150"/>
<DataGridCheckBoxColumn Header="Enabled" Binding="{Binding Enabled}" Width="80"/>
<DataGridTextColumn Header="SSH Host" Binding="{Binding Ssh.Host}" Width="150"/>
<DataGridTextColumn Header="SSH Port" Binding="{Binding Ssh.Port}" Width="80"/>
<DataGridTextColumn Header="Worker Types" Binding="{Binding WorkerTypes}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
<!-- Status Bar -->
<StatusBar Grid.Row="3">
<TextBlock Name="StatusText" Text="{Binding StatusText}" Margin="5"/>
</StatusBar>
</Grid>
</Window>

211
Views/MainWindow.axaml.cs Normal file
View File

@@ -0,0 +1,211 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Interactivity;
using UnifiedFarmLauncher.Models;
using UnifiedFarmLauncher.Services;
using UnifiedFarmLauncher.ViewModels;
using Avalonia.Controls.Primitives;
using MsBox.Avalonia;
using MsBox.Avalonia.Enums;
namespace UnifiedFarmLauncher.Views
{
public partial class MainWindow : Window
{
private readonly ConfigService _configService = new();
private readonly SshService _sshService = new();
private readonly WorkerControllerService _controllerService;
private readonly AttachService _attachService;
public MainWindow()
{
InitializeComponent();
_controllerService = new WorkerControllerService(_sshService, _configService);
_attachService = new AttachService(_sshService, _controllerService);
DataContext = new MainWindowViewModel();
SetupEventHandlers();
}
private void InitializeComponent()
{
Avalonia.Markup.Xaml.AvaloniaXamlLoader.Load(this);
}
private void SetupEventHandlers()
{
AddWorkerButton.Click += AddWorkerButton_Click;
EditWorkerButton.Click += EditWorkerButton_Click;
DeleteWorkerButton.Click += DeleteWorkerButton_Click;
StartWorkerButton.Click += StartWorkerButton_Click;
StopWorkerButton.Click += StopWorkerButton_Click;
AttachWorkerButton.Click += AttachWorkerButton_Click;
WorkerTypeTabs.SelectionChanged += WorkerTypeTabs_SelectionChanged;
WorkersGrid.SelectionChanged += WorkersGrid_SelectionChanged;
}
private async void AddWorkerButton_Click(object? sender, RoutedEventArgs e)
{
var dialog = new WorkerEditWindow();
if (await dialog.ShowDialogAsync(this))
{
((MainWindowViewModel)DataContext!).RefreshWorkers();
}
}
private async void EditWorkerButton_Click(object? sender, RoutedEventArgs e)
{
if (WorkersGrid.SelectedItem is WorkerConfig worker)
{
var dialog = new WorkerEditWindow(worker);
if (await dialog.ShowDialogAsync(this))
{
((MainWindowViewModel)DataContext!).RefreshWorkers();
}
}
}
private async void DeleteWorkerButton_Click(object? sender, RoutedEventArgs e)
{
if (WorkersGrid.SelectedItem is WorkerConfig worker)
{
var box = MessageBoxManager.GetMessageBoxStandard("Delete Worker",
$"Are you sure you want to delete worker '{worker.Name}'?",
ButtonEnum.YesNo, Icon.Warning);
var result = await box.ShowAsync();
if (result == ButtonResult.Yes)
{
_configService.DeleteWorker(worker.Id);
((MainWindowViewModel)DataContext!).RefreshWorkers();
}
}
}
private async void StartWorkerButton_Click(object? sender, RoutedEventArgs e)
{
if (WorkersGrid.SelectedItem is WorkerConfig worker)
{
try
{
string? workerType = null;
if (worker.WorkerTypes.SheepIt != null)
workerType = "sheepit";
else if (worker.WorkerTypes.Flamenco != null)
workerType = "flamenco";
if (workerType == null)
{
var box = MessageBoxManager.GetMessageBoxStandard("Error",
"Worker has no configured worker type.",
ButtonEnum.Ok, Icon.Error);
await box.ShowAsync();
return;
}
await _controllerService.StartWorkerAsync(worker, workerType);
var successBox = MessageBoxManager.GetMessageBoxStandard("Start Worker",
$"Worker '{worker.Name}' started successfully.",
ButtonEnum.Ok, Icon.Success);
await successBox.ShowAsync();
((MainWindowViewModel)DataContext!).RefreshWorkers();
}
catch (System.Exception ex)
{
var errorBox = MessageBoxManager.GetMessageBoxStandard("Error",
$"Failed to start worker: {ex.Message}",
ButtonEnum.Ok, Icon.Error);
await errorBox.ShowAsync();
}
}
}
private async void StopWorkerButton_Click(object? sender, RoutedEventArgs e)
{
if (WorkersGrid.SelectedItem is WorkerConfig worker)
{
try
{
string? workerType = null;
if (worker.WorkerTypes.SheepIt != null)
workerType = "sheepit";
else if (worker.WorkerTypes.Flamenco != null)
workerType = "flamenco";
if (workerType == null)
{
var box = MessageBoxManager.GetMessageBoxStandard("Error",
"Worker has no configured worker type.",
ButtonEnum.Ok, Icon.Error);
await box.ShowAsync();
return;
}
await _controllerService.StopWorkerAsync(worker, workerType);
var successBox = MessageBoxManager.GetMessageBoxStandard("Stop Worker",
$"Stop command sent to worker '{worker.Name}'.",
ButtonEnum.Ok, Icon.Info);
await successBox.ShowAsync();
}
catch (System.Exception ex)
{
var errorBox = MessageBoxManager.GetMessageBoxStandard("Error",
$"Failed to stop worker: {ex.Message}",
ButtonEnum.Ok, Icon.Error);
await errorBox.ShowAsync();
}
}
}
private async void AttachWorkerButton_Click(object? sender, RoutedEventArgs e)
{
if (WorkersGrid.SelectedItem is WorkerConfig worker)
{
try
{
string? workerType = null;
if (worker.WorkerTypes.SheepIt != null)
workerType = "sheepit";
else if (worker.WorkerTypes.Flamenco != null)
workerType = "flamenco";
if (workerType == null)
{
var box = MessageBoxManager.GetMessageBoxStandard("Error",
"Worker has no configured worker type.",
ButtonEnum.Ok, Icon.Error);
await box.ShowAsync();
return;
}
await _attachService.AttachToWorkerAsync(worker, workerType);
}
catch (System.Exception ex)
{
var errorBox = MessageBoxManager.GetMessageBoxStandard("Error",
$"Failed to attach to worker: {ex.Message}",
ButtonEnum.Ok, Icon.Error);
await errorBox.ShowAsync();
}
}
}
private void WorkerTypeTabs_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (WorkerTypeTabs.SelectedItem is TabItem tab)
{
var type = tab.Header?.ToString() ?? "All";
if (type == "All Workers") type = "All";
((MainWindowViewModel)DataContext!).SelectedWorkerType = type;
}
}
private void WorkersGrid_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (DataContext is MainWindowViewModel vm)
{
vm.SelectedWorker = WorkersGrid.SelectedItem as WorkerConfig;
}
}
}
}

View File

@@ -0,0 +1,90 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="UnifiedFarmLauncher.Views.WorkerEditWindow"
Title="Edit Worker"
Width="600" Height="700"
MinWidth="500" MinHeight="600">
<Grid RowDefinitions="Auto,*,Auto" Margin="10">
<!-- Tabs -->
<TabControl Grid.Row="1" Margin="0,10">
<!-- Basic Info Tab -->
<TabItem Header="Basic Info">
<StackPanel Margin="10" Spacing="10">
<TextBlock Text="Worker Name:"/>
<TextBox Name="NameTextBox" Text="{Binding Name}"/>
<CheckBox Name="EnabledCheckBox" Content="Enabled" IsChecked="{Binding Enabled}" Margin="0,10,0,0"/>
<TextBlock Text="SSH Host:" Margin="0,10,0,0"/>
<TextBox Name="SshHostTextBox" Text="{Binding SshHost}"/>
<TextBlock Text="SSH Port:" Margin="0,10,0,0"/>
<NumericUpDown Name="SshPortNumeric" Value="{Binding SshPort}" Minimum="1" Maximum="65535"/>
<TextBlock Text="SSH Args:" Margin="0,10,0,0"/>
<TextBox Name="SshArgsTextBox" Text="{Binding SshArgs}"/>
</StackPanel>
</TabItem>
<!-- SheepIt Tab -->
<TabItem Header="SheepIt">
<StackPanel Margin="10" Spacing="10">
<CheckBox Name="HasSheepItCheckBox" Content="Enable SheepIt Worker" IsChecked="{Binding HasSheepIt}" Margin="0,0,0,10"/>
<TextBlock Text="GPU:" IsVisible="{Binding HasSheepIt}"/>
<ComboBox Name="GpuComboBox" IsVisible="{Binding HasSheepIt}" SelectedItem="{Binding SheepItGpu}">
<ComboBox.Items>
<ComboBoxItem Content="OPTIX_0"/>
<ComboBoxItem Content="CUDA_0"/>
<ComboBoxItem Content="OPENCL_0"/>
</ComboBox.Items>
</ComboBox>
<TextBlock Text="Username:" IsVisible="{Binding HasSheepIt}" Margin="0,10,0,0"/>
<TextBox Name="SheepItUsernameTextBox" Text="{Binding SheepItUsername}" IsVisible="{Binding HasSheepIt}"/>
<TextBlock Text="Render Key:" IsVisible="{Binding HasSheepIt}" Margin="0,10,0,0"/>
<TextBox Name="SheepItRenderKeyTextBox" Text="{Binding SheepItRenderKey}" IsVisible="{Binding HasSheepIt}" PasswordChar="*"/>
</StackPanel>
</TabItem>
<!-- Flamenco Tab -->
<TabItem Header="Flamenco">
<StackPanel Margin="10" Spacing="10">
<CheckBox Name="HasFlamencoCheckBox" Content="Enable Flamenco Worker" IsChecked="{Binding HasFlamenco}" Margin="0,0,0,10"/>
<TextBlock Text="Worker Path:" IsVisible="{Binding HasFlamenco}"/>
<Grid IsVisible="{Binding HasFlamenco}" ColumnDefinitions="*,Auto">
<TextBox Name="FlamencoPathTextBox" Grid.Column="0" Text="{Binding FlamencoWorkerPath}" Margin="0,0,5,0"/>
<Button Name="BrowseFlamencoPathButton" Grid.Column="1" Content="Browse..." Width="80"/>
</Grid>
<TextBlock Text="Network Drives:" IsVisible="{Binding HasFlamenco}" Margin="0,10,0,0"/>
<Grid IsVisible="{Binding HasFlamenco}" RowDefinitions="*,Auto" Margin="0,5,0,0">
<ListBox Name="NetworkDrivesListBox" Grid.Row="0" ItemsSource="{Binding NetworkDrives}" MaxHeight="100"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,5,0,0">
<Button Name="AddDriveButton" Content="Add" Width="60" Margin="0,0,5,0"/>
<Button Name="RemoveDriveButton" Content="Remove" Width="60"/>
</StackPanel>
</Grid>
<TextBlock Text="Network Paths:" IsVisible="{Binding HasFlamenco}" Margin="0,10,0,0"/>
<Grid IsVisible="{Binding HasFlamenco}" RowDefinitions="*,Auto" Margin="0,5,0,0">
<ListBox Name="NetworkPathsListBox" Grid.Row="0" ItemsSource="{Binding NetworkPaths}" MaxHeight="100"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,5,0,0">
<Button Name="AddPathButton" Content="Add" Width="60" Margin="0,0,5,0"/>
<Button Name="RemovePathButton" Content="Remove" Width="60"/>
</StackPanel>
</Grid>
</StackPanel>
</TabItem>
</TabControl>
<!-- Buttons -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="2" Spacing="10" Margin="0,10,0,0">
<Button Name="OkButton" Content="OK" Width="80" IsDefault="True"/>
<Button Name="CancelButton" Content="Cancel" Width="80" IsCancel="True"/>
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,131 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using UnifiedFarmLauncher.Models;
using UnifiedFarmLauncher.Services;
using UnifiedFarmLauncher.ViewModels;
using MsBox.Avalonia;
using MsBox.Avalonia.Enums;
namespace UnifiedFarmLauncher.Views
{
public partial class WorkerEditWindow : Window
{
private readonly WorkerEditViewModel _viewModel;
private bool _result;
public WorkerEditWindow(WorkerConfig? worker = null)
{
InitializeComponent();
var configService = new ConfigService();
_viewModel = new WorkerEditViewModel(configService, worker);
DataContext = _viewModel;
SetupEventHandlers();
}
private void InitializeComponent()
{
Avalonia.Markup.Xaml.AvaloniaXamlLoader.Load(this);
}
private void SetupEventHandlers()
{
OkButton.Click += OkButton_Click;
CancelButton.Click += CancelButton_Click;
BrowseFlamencoPathButton.Click += BrowseFlamencoPathButton_Click;
AddDriveButton.Click += AddDriveButton_Click;
RemoveDriveButton.Click += RemoveDriveButton_Click;
AddPathButton.Click += AddPathButton_Click;
RemovePathButton.Click += RemovePathButton_Click;
}
private async void OkButton_Click(object? sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(_viewModel.Name))
{
var box = MessageBoxManager.GetMessageBoxStandard("Error",
"Worker name is required.",
ButtonEnum.Ok, Icon.Error);
await box.ShowAsync();
return;
}
try
{
_viewModel.Save();
_result = true;
Close();
}
catch (Exception ex)
{
var errorBox = MessageBoxManager.GetMessageBoxStandard("Error",
$"Failed to save worker: {ex.Message}",
ButtonEnum.Ok, Icon.Error);
await errorBox.ShowAsync();
}
}
private void CancelButton_Click(object? sender, RoutedEventArgs e)
{
_result = false;
Close();
}
private async void BrowseFlamencoPathButton_Click(object? sender, RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel?.StorageProvider.CanPickFolder == true)
{
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = "Select Flamenco Worker Path"
});
if (folders.Count > 0 && folders[0].TryGetLocalPath() is { } localPath)
{
_viewModel.FlamencoWorkerPath = localPath;
}
}
}
private async void AddDriveButton_Click(object? sender, RoutedEventArgs e)
{
// Simplified: use a simple input box
// In a full implementation, you'd use a proper input dialog
_viewModel.NetworkDrives.Add("A:");
}
private void RemoveDriveButton_Click(object? sender, RoutedEventArgs e)
{
if (NetworkDrivesListBox.SelectedItem is string drive)
{
_viewModel.NetworkDrives.Remove(drive);
}
}
private async void AddPathButton_Click(object? sender, RoutedEventArgs e)
{
// Simplified: use a simple input box
// In a full implementation, you'd use a proper input dialog
_viewModel.NetworkPaths.Add("\\\\SERVER\\share");
}
private void RemovePathButton_Click(object? sender, RoutedEventArgs e)
{
if (NetworkPathsListBox.SelectedItem is string path)
{
_viewModel.NetworkPaths.Remove(path);
}
}
public async Task<bool> ShowDialogAsync(Window parent)
{
await base.ShowDialog(parent);
return _result;
}
}
}