diff --git a/PantherByte/Messages/OpenNotificationDialogMessage.cs b/PantherByte/Messages/OpenNotificationDialogMessage.cs new file mode 100644 index 0000000..79a5384 --- /dev/null +++ b/PantherByte/Messages/OpenNotificationDialogMessage.cs @@ -0,0 +1,10 @@ +using CommunityToolkit.Mvvm.Messaging.Messages; +using PantherByte.ViewModels; + +namespace PantherByte.Messages; + +public class OpenNotificationDialogMessage(string title, string message) + : AsyncRequestMessage { + public readonly string Title = title; + public readonly string Message = message; +} \ No newline at end of file diff --git a/PantherByte/Messages/OpenProgressDialogMessage.cs b/PantherByte/Messages/OpenProgressDialogMessage.cs index 2552dc0..7ad295d 100644 --- a/PantherByte/Messages/OpenProgressDialogMessage.cs +++ b/PantherByte/Messages/OpenProgressDialogMessage.cs @@ -4,6 +4,6 @@ using PantherByte.ViewModels; namespace PantherByte.Messages; -public class OpenProgressDialogMessage(ProcessStartInfo info) : AsyncRequestMessage { - public readonly ProcessStartInfo Info = info; +public class OpenProgressDialogMessage(ProgressWindowViewModel vm) : AsyncRequestMessage { + public readonly ProgressWindowViewModel ViewModel = vm; }; \ No newline at end of file diff --git a/PantherByte/ViewModels/MainWindowViewModel.cs b/PantherByte/ViewModels/MainWindowViewModel.cs index b39d0dc..7f85357 100644 --- a/PantherByte/ViewModels/MainWindowViewModel.cs +++ b/PantherByte/ViewModels/MainWindowViewModel.cs @@ -4,6 +4,7 @@ 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; @@ -27,7 +28,7 @@ public partial class MainWindowViewModel : ViewModelBase { /// /// The link to the video to download. For now, only use YouTube links. /// - public string Link { + public string Link { get => _link; set => this.RaiseAndSetIfChanged(ref _link, value); } @@ -45,7 +46,7 @@ public partial class MainWindowViewModel : ViewModelBase { get => _availableFormats; set => this.RaiseAndSetIfChanged(ref _availableFormats, value); } - + /// /// The folder to save the files in. This will become the argument passed to the "-P" argument in /// . @@ -54,7 +55,7 @@ public partial class MainWindowViewModel : ViewModelBase { get => _saveLocation; set => this.RaiseAndSetIfChanged(ref _saveLocation, value); } - + /// /// Indicates if the file can be downloaded. If not, the download button is disabled. /// @@ -62,7 +63,7 @@ public partial class MainWindowViewModel : ViewModelBase { get => _canDownload; set => this.RaiseAndSetIfChanged(ref _canDownload, value); } - + /// /// Constructor. Mainly used for subscribing properties to the changes from other properties. /// @@ -78,7 +79,7 @@ public partial class MainWindowViewModel : ViewModelBase { ) .Subscribe(value => CanDownload = value); } - + /// /// Gets the save location for the generated file. We are only allowing users to download one file at a time for now. /// @@ -101,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()); } @@ -119,7 +124,6 @@ public partial class MainWindowViewModel : ViewModelBase { /// /// Generates the arguments used for the main command string. /// - [RelayCommand] private async Task> GenerateCmdArgsAsync() { // The command string format to use: yt-dlp -t -P --no-playlist // yt-dlp requires usage of ffmpeg as well. @@ -130,7 +134,8 @@ public partial class MainWindowViewModel : ViewModelBase { 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"; } @@ -140,14 +145,19 @@ public partial class MainWindowViewModel : ViewModelBase { // WAV files require special command arguments if (Format.Equals("wav")) { args.AddRange(["--extract-audio", "--audio-format", Format]); - } else { + } + else { args.AddRange(["-t", Format]); } - + return args; }); } + /// + /// Checks if ffmpeg is installed. + /// + /// True if ffmpeg is installed, or false if not. private async Task CheckFfmpegInstalled() { return await Task.Run(async () => { ProcessStartInfo ffmpegInfo = new("ffmpeg") { @@ -160,13 +170,13 @@ public partial class MainWindowViewModel : ViewModelBase { StartInfo = ffmpegInfo }; - bool result = true; + var result = true; try { process.Start(); await process.WaitForExitAsync(); } - catch (Exception ex) { + catch (Exception) { result = false; } @@ -175,7 +185,9 @@ public partial class MainWindowViewModel : ViewModelBase { } /// - /// Handles the download process of the file. Generates the command and runs it. + /// Handles the download process of the file. + /// + /// This command is also where the dependency check/installation is run from. /// [RelayCommand] private async Task DownloadFileAsync() { @@ -184,40 +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..]); - - // Check that ffmpeg is installed. - var temp = await CheckFfmpegInstalled(); - // if (!await CheckFfmpegInstalled()) { - 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 not null) { - Console.WriteLine($"Received result: {result}"); - } - // } - + // 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()); } } + /// + /// Begins the installation process for ffmpeg and displays a window. + /// + /// True if the installation was successful, or false if not. + private async Task 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; + } + /// /// 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. /// - [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)); } -} +} \ No newline at end of file diff --git a/PantherByte/ViewModels/NotificationDialogViewModel.cs b/PantherByte/ViewModels/NotificationDialogViewModel.cs new file mode 100644 index 0000000..4b10fa6 --- /dev/null +++ b/PantherByte/ViewModels/NotificationDialogViewModel.cs @@ -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); + } +} \ No newline at end of file diff --git a/PantherByte/ViewModels/ProgressWindowViewModel.cs b/PantherByte/ViewModels/ProgressWindowViewModel.cs index 0a541dc..e4c6fcc 100644 --- a/PantherByte/ViewModels/ProgressWindowViewModel.cs +++ b/PantherByte/ViewModels/ProgressWindowViewModel.cs @@ -14,7 +14,7 @@ public partial class ProgressWindowViewModel : ViewModelBase, IProgressWindowVie private bool _inProgress = false; private string _statusMessage = "Downloading video..."; private string _barColor = "CornflowerBlue"; - + public string ProcessStdOut { get => _processStdOut; set => this.RaiseAndSetIfChanged(ref _processStdOut, value); @@ -48,11 +48,11 @@ public partial class ProgressWindowViewModel : ViewModelBase, IProgressWindowVie public async Task RunProcessAsync() { Process downloadProcess = new(); downloadProcess.StartInfo = _processStartInfo; - + downloadProcess.EnableRaisingEvents = true; downloadProcess.OutputDataReceived += (_, e) => { if (string.IsNullOrWhiteSpace(e.Data)) return; - + Console.WriteLine(e.Data); ProcessStdOut += e.Data + "\n"; }; @@ -77,7 +77,7 @@ public partial class ProgressWindowViewModel : ViewModelBase, IProgressWindowVie 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; } @@ -90,4 +90,4 @@ public partial class ProgressWindowViewModel : ViewModelBase, IProgressWindowVie } } } -} +} \ No newline at end of file diff --git a/PantherByte/Views/MainWindow.axaml.cs b/PantherByte/Views/MainWindow.axaml.cs index cb4e647..0fe5ca2 100644 --- a/PantherByte/Views/MainWindow.axaml.cs +++ b/PantherByte/Views/MainWindow.axaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Threading; using CommunityToolkit.Mvvm.Messaging; using PantherByte.Messages; using PantherByte.ViewModels; @@ -12,17 +13,26 @@ public partial class MainWindow : Window { WeakReferenceMessenger.Default.Register(this, static (w, m) => { var dialog = new ProgressWindow { - DataContext = new ProgressWindowViewModel(m.Info) + DataContext = m.ViewModel }; m.Reply(dialog.ShowDialog(w)); }); WeakReferenceMessenger.Default.Register(this, - static (w, m) => { + static async (w, m) => { var dialog = new SimpleDialog { DataContext = m.ViewModel }; - m.Reply(dialog.ShowDialog(w)); + var response = dialog.ShowDialog(w); + m.Reply(response); + }); + + WeakReferenceMessenger.Default.Register(this, + static (w, m) => { + var dialog = new NotificationDialog { + DataContext = new NotificationDialogViewModel(m.Title, m.Message) + }; + m.Reply(dialog.ShowDialog(w)); }); } -} +} \ No newline at end of file diff --git a/PantherByte/Views/NotificationDialog.axaml b/PantherByte/Views/NotificationDialog.axaml new file mode 100644 index 0000000..fca2d00 --- /dev/null +++ b/PantherByte/Views/NotificationDialog.axaml @@ -0,0 +1,28 @@ + + + + + + + +