Author SHA1 Message Date
alphafetus aa62e53013 style: reformatting pass 2025-10-03 22:36:59 -04:00
alphafetus 3c7cfd23ac feat: finish implementation of ffmpeg installation on linux
This requires testing on Windows before we can call it a full release.
2025-10-03 22:36:31 -04:00
alphafetus 4c9e22faf8 feat: add additional simple result dialog 2025-09-29 12:33:32 -04:00
Lucas Vas 3e8c316bc2 Merge pull request #1 from R15-Software-Dev/feature/format-options
Implement new download format options
2025-09-19 14:14:25 -04:00
alphafetus f9853efe1a chore: change default download format 2025-09-19 14:11:42 -04:00
alphafetus 55d04877f6 feat: add additional download formats
Formats are specified by the yt-dlp documentation: https://github.com/yt-dlp/yt-dlp#preset-aliases
2025-09-19 14:05:41 -04:00
18 changed files with 470 additions and 65 deletions

No files matched your search

@@ -0,0 +1,10 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using PantherByte.ViewModels;
namespace PantherByte.Messages;
public class OpenNotificationDialogMessage(string title, string message)
: AsyncRequestMessage<NotificationDialogViewModel?> {
public readonly string Title = title;
public readonly string Message = message;
}
@@ -4,6 +4,6 @@ using PantherByte.ViewModels;
namespace PantherByte.Messages;
public class OpenProgressDialogMessage(ProcessStartInfo info) : AsyncRequestMessage<ProgressWindowViewModel?> {
public readonly ProcessStartInfo Info = info;
public class OpenProgressDialogMessage(ProgressWindowViewModel vm) : AsyncRequestMessage<ProgressWindowViewModel?> {
public readonly ProgressWindowViewModel ViewModel = vm;
};
@@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using PantherByte.ViewModels;
namespace PantherByte.Messages;
public class OpenSimpleDialogMessage(SimpleDialogViewModel vm) : AsyncRequestMessage<SimpleDialogViewModel.SimpleDialogResult?> {
public SimpleDialogViewModel ViewModel = vm;
}
+8
View File
@@ -6,6 +6,14 @@
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<Version>0.1.0</Version>
<Title>PantherByte</Title>
<Authors>Region 15 Schools</Authors>
<PackageProjectUrl>https://github.com/R15-Software-Dev/PantherByte</PackageProjectUrl>
<RepositoryUrl>https://github.com/R15-Software-Dev/PantherByte</RepositoryUrl>
<Company>Region 15 Schools</Company>
<AssemblyVersion>0.0.1.0</AssemblyVersion>
<FileVersion>0.0.1.0</FileVersion>
</PropertyGroup>
<ItemGroup>
@@ -0,0 +1,105 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using ReactiveUI;
namespace PantherByte.ViewModels;
public partial class DependencyProgressViewModel : ViewModelBase, IProgressWindowViewModel {
private readonly ProcessStartInfo _processStartInfo;
private string _processStdOut = "";
private bool _error = false;
private bool _inProgress = false;
private string _statusMessage = "Downloading ffmpeg...";
private string _barColor = "CornflowerBlue";
public string ProcessStdOut {
get => _processStdOut;
set => this.RaiseAndSetIfChanged(ref _processStdOut, value);
}
public string StatusMessage {
get => _statusMessage;
set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
}
public bool Error {
get => _error;
set => this.RaiseAndSetIfChanged(ref _error, value);
}
public bool InProgress {
get => _inProgress;
set => this.RaiseAndSetIfChanged(ref _inProgress, value);
}
public string BarColor {
get => _barColor;
set => this.RaiseAndSetIfChanged(ref _barColor, value);
}
public DependencyProgressViewModel() {
_processStartInfo = new ProcessStartInfo() {
UseShellExecute = false
};
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
// Windows specific command.
_processStartInfo.FileName = "winget";
_processStartInfo.Arguments = "install ffmpeg";
_processStartInfo.Verb = "runas";
} else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
// Linux specific command.
_processStartInfo.FileName = "sudo";
_processStartInfo.Arguments = "apt install ffmpeg";
}
}
[RelayCommand]
public async Task RunProcessAsync() {
Process installProcess = new();
installProcess.StartInfo = _processStartInfo;
installProcess.EnableRaisingEvents = true;
installProcess.OutputDataReceived += (s, e) => {
if (string.IsNullOrWhiteSpace(e.Data)) return;
Console.WriteLine(e.Data);
ProcessStdOut += e.Data;
};
installProcess.ErrorDataReceived += (s, e) => {
if (string.IsNullOrWhiteSpace(e.Data)) return;
Console.WriteLine(e.Data);
ProcessStdOut += e.Data + "\n";
Error = true;
StatusMessage = "Error occurred while installing ffmpeg.";
BarColor = "Red";
};
InProgress = true;
try {
installProcess.Start();
Console.WriteLine("Installation process started.");
installProcess.BeginOutputReadLine();
installProcess.BeginErrorReadLine();
await installProcess.WaitForExitAsync();
}
catch (Exception ex) {
Console.WriteLine(ex.Message);
Error = true;
StatusMessage = "Error occurred while installing ffmpeg.";
BarColor = "Red";
ProcessStdOut += ex.Message;
}
finally {
InProgress = false;
if (!Error) {
BarColor = "Green";
StatusMessage = "Download finished!";
}
}
}
}
@@ -0,0 +1,18 @@
using System.ComponentModel;
using System.Threading.Tasks;
namespace PantherByte.ViewModels;
/// <summary>
/// Defines a series of properties and methods that are used by the <see cref="Views.ProgressWindow"/>.
/// It requires the implementation of <see cref="INotifyPropertyChanged"/> as well, so that it can
/// preserve the reactivity of the interface.
/// </summary>
public interface IProgressWindowViewModel : INotifyPropertyChanged {
public string ProcessStdOut { get; set; }
public string StatusMessage { get; set; }
public bool Error { get; set; }
public bool InProgress { get; set; }
public string BarColor { get; set; }
public Task RunProcessAsync();
}
+131 -24
View File
@@ -1,24 +1,28 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using CommunityToolkit.Mvvm.Input;
using System.Threading.Tasks;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.Messaging;
using DynamicData;
using PantherByte.Messages;
using PantherByte.Services;
using ReactiveUI;
using PantherByte.Services.Interfaces;
using PantherByte.Views;
namespace PantherByte.ViewModels;
public partial class MainWindowViewModel : ViewModelBase {
private string _link = string.Empty;
private string _saveLocation = string.Empty;
private string _format = "mp3";
private bool _isDownloading = false;
private string _format = "mp4";
private ObservableCollection<string> _availableFormats = ["mp4", "mp3", "wav", "aac", "mkv"];
private bool _canDownload = true;
/// <summary>
@@ -38,24 +42,20 @@ public partial class MainWindowViewModel : ViewModelBase {
set => this.RaiseAndSetIfChanged(ref _format, value);
}
public ObservableCollection<string> AvailableFormats {
get => _availableFormats;
set => this.RaiseAndSetIfChanged(ref _availableFormats, value);
}
/// <summary>
/// The folder to save the files in. This will become the argument passed to the "-P" argument in
/// <see cref="GenerateCmdStringAsync"/>.
/// <see cref="GenerateCmdArgsAsync"/>.
/// </summary>
public string SaveLocation {
get => _saveLocation;
set => this.RaiseAndSetIfChanged(ref _saveLocation, value);
}
/// <summary>
/// Indicates if the command is currently running. When the command is running, the application
/// should disable all inputs and/or open a blocking modal window.
/// </summary>
public bool IsDownloading {
get => _isDownloading;
set => this.RaiseAndSetIfChanged(ref _isDownloading, value);
}
/// <summary>
/// Indicates if the file can be downloaded. If not, the download button is disabled.
/// </summary>
@@ -102,16 +102,20 @@ public partial class MainWindowViewModel : ViewModelBase {
if (Directory.Exists(tempPath)) {
SaveLocation = tempPath;
Console.WriteLine("Successfully set the file path.");
} else {
}
else {
throw new DirectoryNotFoundException($"Directory does not exist: {tempPath}");
}
} else {
}
else {
throw new NullReferenceException("The resulting storage folder was null.");
}
} else {
}
else {
throw new NullReferenceException("Missing a FileSaveService instance");
}
} catch (Exception ex) {
}
catch (Exception ex) {
// Exceptions are caught locally for now, but will eventually require a modal error window.
Console.WriteLine(ex.ToString());
}
@@ -120,26 +124,70 @@ public partial class MainWindowViewModel : ViewModelBase {
/// <summary>
/// Generates the arguments used for the main command string.
/// </summary>
[RelayCommand]
private async Task<List<string>> GenerateCmdArgsAsync() {
// The command string format to use: yt-dlp <link> -t <mp3|mp4|wav> -P <path> --no-playlist
// yt-dlp requires usage of ffmpeg as well.
// Code will be executed when the button is clicked.
return await Task.Run(() => {
var fileName = "";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
Console.WriteLine("Using Windows program.");
fileName = "Programs/yt-dlp-win64.exe";
} else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
Console.WriteLine("Using Linux program. May require additional run permissions.");
fileName = "Programs/yt-dlp-linux";
}
return await Task.Run(() => new List<string>([fileName, Link, "-t", Format, "-P", SaveLocation, "--no-playlist"]));
List<string> args = [fileName, Link, "-P", SaveLocation, "--no-playlist"];
// WAV files require special command arguments
if (Format.Equals("wav")) {
args.AddRange(["--extract-audio", "--audio-format", Format]);
}
else {
args.AddRange(["-t", Format]);
}
return args;
});
}
/// <summary>
/// Handles the download process of the file. Generates the command and runs it.
/// Checks if ffmpeg is installed.
/// </summary>
/// <returns>True if ffmpeg is installed, or false if not.</returns>
private async Task<bool> CheckFfmpegInstalled() {
return await Task.Run(async () => {
ProcessStartInfo ffmpegInfo = new("ffmpeg") {
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
Process process = new() {
StartInfo = ffmpegInfo
};
var result = true;
try {
process.Start();
await process.WaitForExitAsync();
}
catch (Exception) {
result = false;
}
return result;
});
}
/// <summary>
/// Handles the download process of the file.
///
/// This command is also where the dependency check/installation is run from.
/// </summary>
[RelayCommand]
private async Task DownloadFileAsync() {
@@ -148,29 +196,88 @@ public partial class MainWindowViewModel : ViewModelBase {
var args = await GenerateCmdArgsAsync();
// Run the command.
try {
// ffmpeg installation check
var installed = await CheckFfmpegInstalled();
Console.WriteLine($"Is ffmpeg installed: {installed}");
if (!installed) {
SimpleDialogViewModel vm = new("Dependency ffmpeg was not found. Would you like to install it?",
"Dependency Install");
var result = await WeakReferenceMessenger.Default.Send(new OpenSimpleDialogMessage(vm));
if (result is SimpleDialogViewModel.SimpleDialogResult.Yes) {
// Install the program
installed = await InstallFfmpegAsync();
}
}
// This means installation has failed or the user canceled it.
if (!installed) return;
// The command that's run must work on at least Linux and Windows
ProcessStartInfo info = new() {
FileName = args[0],
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
CreateNoWindow = true,
};
info.ArgumentList.AddRange(args[1..]);
// This window isn't linked to the download process yet, but that is the plan.
await OpenProgressWindowAsync(info);
} catch (Exception ex) {
}
catch (Exception ex) {
Console.WriteLine(ex.ToString());
}
}
/// <summary>
/// Begins the installation process for ffmpeg and displays a window.
/// </summary>
/// <returns>True if the installation was successful, or false if not.</returns>
private async Task<bool> InstallFfmpegAsync() {
ProcessStartInfo info = new() {
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
var result = false;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
// Run: winget install "FFmpeg (Essentials Build)"
info.Verb = "runas";
info.FileName = "winget";
info.Arguments = "install \"FFmpeg (Essentials Build)\"";
ProgressWindowViewModel vm = new(info) {
StatusMessage = "Installing ffmpeg..."
};
await WeakReferenceMessenger.Default.Send(new OpenProgressDialogMessage(vm));
// If there was an error, return false. Otherwise, true.
result = !vm.Error;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
await WeakReferenceMessenger.Default.Send(new OpenNotificationDialogMessage(
"Linux Install Error",
"Linux users must install ffmpeg manually.\nThis may be developed further in the future."
));
// Since this requires a manual install (we can't run sudo nicely) we'll just return false,
// as we didn't actually install anything.
result = false;
}
return result;
}
/// <summary>
/// Opens a ProgressWindow as a modal dialog. I haven't decided if this window should run the command or if it
/// should simply display the progress of a command that's running here.
/// </summary>
[RelayCommand]
private async Task OpenProgressWindowAsync(ProcessStartInfo info) {
await WeakReferenceMessenger.Default.Send(new OpenProgressDialogMessage(info));
ProgressWindowViewModel vm = new(info);
await WeakReferenceMessenger.Default.Send(new OpenProgressDialogMessage(vm));
}
}
@@ -0,0 +1,12 @@
namespace PantherByte.ViewModels;
public class NotificationDialogViewModel : ViewModelBase {
public string Title { get; set; } = "Notification";
public string Message { get; set; } = "Testing message";
public NotificationDialogViewModel() { }
public NotificationDialogViewModel(string title, string message) {
(Title, Message) = (title, message);
}
}
@@ -5,7 +5,7 @@ using ReactiveUI;
namespace PantherByte.ViewModels;
public class ProgressWindowViewModel : ViewModelBase {
public partial class ProgressWindowViewModel : ViewModelBase, IProgressWindowViewModel {
// Pass variables between the MainWindow and this window, specifically what point the application
// is at in terms of downloading. We'll likely do this as a simple percentage of total progress.
private readonly ProcessStartInfo _processStartInfo;
@@ -49,11 +49,12 @@ public class ProgressWindowViewModel : ViewModelBase {
Process downloadProcess = new();
downloadProcess.StartInfo = _processStartInfo;
downloadProcess.EnableRaisingEvents = true;
downloadProcess.OutputDataReceived += (_, e) => {
if (!string.IsNullOrWhiteSpace(e.Data)) {
if (string.IsNullOrWhiteSpace(e.Data)) return;
Console.WriteLine(e.Data);
ProcessStdOut += e.Data + "\n";
}
};
downloadProcess.ErrorDataReceived += (_, e) => {
@@ -76,7 +77,7 @@ public class ProgressWindowViewModel : ViewModelBase {
catch (Exception e) {
Console.WriteLine(e.StackTrace);
ProcessStdOut += "Error: " + e.StackTrace + "\n";
StatusMessage = "Error while downloading video.";
StatusMessage = "Error while performing download.";
BarColor = "Red";
Error = true;
}
@@ -0,0 +1,14 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
namespace PantherByte.ViewModels;
public partial class SimpleDialogViewModel(string prompt, string title) : ViewModelBase {
public enum SimpleDialogResult {
Yes,
No
}
public string PromptMessage { get; set; } = prompt;
public string Title { get; set; } = title;
}
+22 -26
View File
@@ -33,52 +33,48 @@
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
<Grid
Margin="20 35 20 20"
ShowGridLines="False"
<StackPanel Margin="20 35 20 20"
Width="500"
RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto"
RowSpacing="10">
<TextBlock
Grid.Row="0"
Text="Youtube Video Downloads"
Spacing="10"
HorizontalAlignment="Stretch">
<TextBlock Text="Youtube Video Downloads"
FontSize="30"/>
<TextBlock
Grid.Row="1"
Text="Enter your video link and save location."
<TextBlock Text="Enter your video link and save location."
FontSize="20"></TextBlock>
<Border Grid.Row="2" BorderBrush="White" BorderThickness="0,0,0,1"/>
<StackPanel Grid.Row="3" Spacing="5">
<Border BorderBrush="White" BorderThickness="0,0,0,1"/>
<StackPanel Spacing="5">
<TextBlock Text="Save location:"/>
<Grid
ColumnDefinitions="*, 30"
<Grid ColumnDefinitions="*, 30"
ColumnSpacing="5">
<TextBox
Grid.Column="0"
<TextBox Grid.Column="0"
Text="{Binding SaveLocation}"
Watermark="Choose a folder..."/>
<Button
Grid.Column="1"
<Button Grid.Column="1"
Content=">"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Command="{Binding GetSaveLocationCommand}"/>
</Grid>
</StackPanel>
<StackPanel Grid.Row="4" Spacing="5">
<StackPanel Spacing="5">
<TextBlock Text="Video link:"/>
<TextBox
Text="{Binding Link}"
<TextBox Text="{Binding Link}"
Watermark="Paste link here..."/>
</StackPanel>
<Button
Grid.Row="5"
Content="Download"
<StackPanel Spacing="5"
HorizontalAlignment="Stretch">
<TextBlock Text="Format:"/>
<ComboBox HorizontalAlignment="Stretch"
ItemsSource="{Binding AvailableFormats}"
SelectedItem="{Binding Format, Mode=TwoWay}">
</ComboBox>
</StackPanel>
<Button Content="Download"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsEnabled="{Binding CanDownload}"
Command="{Binding DownloadFileCommand}"/>
</Grid>
</StackPanel>
</Panel>
</Window>
+19 -1
View File
@@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.Messaging;
using PantherByte.Messages;
using PantherByte.ViewModels;
@@ -12,9 +13,26 @@ public partial class MainWindow : Window {
WeakReferenceMessenger.Default.Register<MainWindow, OpenProgressDialogMessage>(this,
static (w, m) => {
var dialog = new ProgressWindow {
DataContext = new ProgressWindowViewModel(m.Info)
DataContext = m.ViewModel
};
m.Reply(dialog.ShowDialog<ProgressWindowViewModel?>(w));
});
WeakReferenceMessenger.Default.Register<MainWindow, OpenSimpleDialogMessage>(this,
static async (w, m) => {
var dialog = new SimpleDialog {
DataContext = m.ViewModel
};
var response = dialog.ShowDialog<SimpleDialogViewModel.SimpleDialogResult?>(w);
m.Reply(response);
});
WeakReferenceMessenger.Default.Register<MainWindow, OpenNotificationDialogMessage>(this,
static (w, m) => {
var dialog = new NotificationDialog {
DataContext = new NotificationDialogViewModel(m.Title, m.Message)
};
m.Reply(dialog.ShowDialog<NotificationDialogViewModel?>(w));
});
}
}
@@ -0,0 +1,28 @@
<Window 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"
xmlns:vm="using:PantherByte.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="PantherByte.Views.NotificationDialog"
x:DataType="vm:NotificationDialogViewModel"
Title="{Binding Title}"
WindowStartupLocation="CenterOwner"
SizeToContent="WidthAndHeight"
Background="Gray"
CanMaximize="False">
<Design.DataContext>
<vm:NotificationDialogViewModel />
</Design.DataContext>
<StackPanel Spacing="20" Margin="10" HorizontalAlignment="Stretch">
<TextBlock
HorizontalAlignment="Center"
Text="{Binding Message}" />
<Button HorizontalAlignment="Center"
HorizontalContentAlignment="Center"
Content="OK"
Padding="30 10"
Click="OkButton_Click" />
</StackPanel>
</Window>
@@ -0,0 +1,15 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace PantherByte.Views;
public partial class NotificationDialog : Window {
public NotificationDialog() {
InitializeComponent();
}
private void OkButton_Click(object? sender, RoutedEventArgs e) =>
Close(DataContext);
}
+5 -4
View File
@@ -19,18 +19,19 @@
<StackPanel Spacing="10" Margin="10">
<TextBlock
HorizontalAlignment="Center"
Text="{Binding StatusMessage}"/>
Text="{Binding StatusMessage}" />
<ProgressBar
IsIndeterminate="{Binding InProgress}"
Foreground="{Binding BarColor}"
Maximum="100"
Value="100"/>
Value="100" />
</StackPanel>
<Expander VerticalAlignment="Top" HorizontalAlignment="Stretch"
Margin="5"
BorderThickness="0"
Background="Transparent">
Background="Transparent"
x:Name="InfoExpander">
<Expander.Header>More information...</Expander.Header>
<Grid RowDefinitions="*" ColumnDefinitions="*">
<ScrollViewer Grid.Row="0"
@@ -40,7 +41,7 @@
VerticalScrollBarVisibility="Auto">
<TextBlock x:Name="StdOutText"
Text="{Binding ProcessStdOut}"
TextWrapping="Wrap"/>
TextWrapping="Wrap" />
</ScrollViewer>
</Grid>
</Expander>
+14 -4
View File
@@ -1,3 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using PantherByte.ViewModels;
@@ -7,14 +8,23 @@ public partial class ProgressWindow : Window {
public ProgressWindow() {
InitializeComponent();
// This feels a little fragile, but we'll leave it for now.
// Runs the specified process in the view model when the window is opened.
Opened += async (_, _) => {
if (DataContext is not ProgressWindowViewModel vm)
return;
if (DataContext is IProgressWindowViewModel vm)
await vm.RunProcessAsync();
};
// Automatically scrolls to the end of the logs when more information is added.
StdOutText.SizeChanged += (_, _) => ScrollView.ScrollToEnd();
// Automatically resizes the window when the user collapses the expander.
InfoExpander.Collapsing += (_, _) => {
if (Content is not Control content) return;
content.Measure(Size.Infinity);
var desired = content.DesiredSize;
Width = desired.Width;
Height = desired.Height;
};
}
}
+35
View File
@@ -0,0 +1,35 @@
<Window 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"
xmlns:vm="using:PantherByte.ViewModels"
x:DataType="vm:SimpleDialogViewModel"
mc:Ignorable="d"
x:Class="PantherByte.Views.SimpleDialog"
WindowStartupLocation="CenterOwner"
SizeToContent="WidthAndHeight"
Background="Gray"
Title="{Binding Title}"
CanMaximize="False">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:SimpleDialogViewModel/>
</Design.DataContext>
<StackPanel Spacing="20" Margin="10" HorizontalAlignment="Stretch">
<TextBlock Text="{Binding PromptMessage}" HorizontalAlignment="Center"/>
<Grid ColumnDefinitions="*, *" ColumnSpacing="10" Width="200" MaxWidth="300">
<Button Grid.Column="0"
Content="Yes"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Click="YesButtonClicked"/>
<Button Grid.Column="1"
Content="No"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Click="NoButtonClicked"/>
</Grid>
</StackPanel>
</Window>
+19
View File
@@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using PantherByte.ViewModels;
namespace PantherByte.Views;
public partial class SimpleDialog : Window {
public SimpleDialog() {
InitializeComponent();
}
public void YesButtonClicked(object sender, RoutedEventArgs e) =>
Close(SimpleDialogViewModel.SimpleDialogResult.Yes);
public void NoButtonClicked(object sender, RoutedEventArgs e) =>
Close(SimpleDialogViewModel.SimpleDialogResult.No);
}