diff --git a/PantherByte/App.axaml.cs b/PantherByte/App.axaml.cs index c152059..5d0e2a2 100644 --- a/PantherByte/App.axaml.cs +++ b/PantherByte/App.axaml.cs @@ -8,6 +8,7 @@ using System.Linq; using Avalonia.Markup.Xaml; using Microsoft.Extensions.DependencyInjection; using PantherByte.Services; +using PantherByte.Services.Interfaces; using PantherByte.ViewModels; using PantherByte.Views; @@ -30,9 +31,7 @@ public partial class App : Application { // Avoid duplicate validations from both Avalonia and the CommunityToolkit. // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins DisableAvaloniaDataAnnotationValidation(); - desktop.MainWindow = new MainWindow { - DataContext = new MainWindowViewModel(), - }; + desktop.MainWindow = new MainWindow(new MainWindowViewModel()); var services = new ServiceCollection(); services.AddSingleton(x => new FileService(desktop.MainWindow)); diff --git a/PantherByte/Messages/OpenProgressDialogMessage.cs b/PantherByte/Messages/OpenProgressDialogMessage.cs new file mode 100644 index 0000000..2552dc0 --- /dev/null +++ b/PantherByte/Messages/OpenProgressDialogMessage.cs @@ -0,0 +1,9 @@ +using System.Diagnostics; +using CommunityToolkit.Mvvm.Messaging.Messages; +using PantherByte.ViewModels; + +namespace PantherByte.Messages; + +public class OpenProgressDialogMessage(ProcessStartInfo info) : AsyncRequestMessage { + public readonly ProcessStartInfo Info = info; +}; \ No newline at end of file diff --git a/PantherByte/PantherByte.csproj b/PantherByte/PantherByte.csproj index 766b03c..5ead698 100644 --- a/PantherByte/PantherByte.csproj +++ b/PantherByte/PantherByte.csproj @@ -11,6 +11,7 @@ + @@ -28,4 +29,13 @@ + + + + PreserveNewest + + + PreserveNewest + + diff --git a/PantherByte/Programs/yt-dlp-linux b/PantherByte/Programs/yt-dlp-linux new file mode 100755 index 0000000..94af2c7 Binary files /dev/null and b/PantherByte/Programs/yt-dlp-linux differ diff --git a/PantherByte/Programs/yt-dlp-win64.exe b/PantherByte/Programs/yt-dlp-win64.exe new file mode 100644 index 0000000..e0d1bec Binary files /dev/null and b/PantherByte/Programs/yt-dlp-win64.exe differ diff --git a/PantherByte/Services/FileService.cs b/PantherByte/Services/FileService.cs index 866cf3e..e60fc83 100644 --- a/PantherByte/Services/FileService.cs +++ b/PantherByte/Services/FileService.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Platform.Storage; +using PantherByte.Services.Interfaces; namespace PantherByte.Services; diff --git a/PantherByte/Services/Interfaces/IFileService.cs b/PantherByte/Services/Interfaces/IFileService.cs index 6265970..373b8e9 100644 --- a/PantherByte/Services/Interfaces/IFileService.cs +++ b/PantherByte/Services/Interfaces/IFileService.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.Platform.Storage; -namespace PantherByte; +namespace PantherByte.Services.Interfaces; public interface IFileSaveService { public Task ChooseFolderAsync(); diff --git a/PantherByte/ViewModels/MainWindowViewModel.cs b/PantherByte/ViewModels/MainWindowViewModel.cs index d8aac77..0457dac 100644 --- a/PantherByte/ViewModels/MainWindowViewModel.cs +++ b/PantherByte/ViewModels/MainWindowViewModel.cs @@ -1,10 +1,16 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; using CommunityToolkit.Mvvm.Input; using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; +using DynamicData; +using PantherByte.Messages; using PantherByte.Services; using ReactiveUI; +using PantherByte.Services.Interfaces; namespace PantherByte.ViewModels; @@ -12,6 +18,7 @@ public partial class MainWindowViewModel : ViewModelBase { private string _link = string.Empty; private string _saveLocation = string.Empty; private string _format = "mp3"; + private bool _isDownloading = false; private bool _canDownload = true; /// @@ -24,7 +31,7 @@ public partial class MainWindowViewModel : ViewModelBase { /// /// The format to download the video in. Currently supported options are MP3, MP4, and WAV. This will become the - /// argument passed to the "-t" argument in . + /// argument passed to the "-t" argument in . /// public string Format { get => _format; @@ -40,6 +47,15 @@ public partial class MainWindowViewModel : ViewModelBase { set => this.RaiseAndSetIfChanged(ref _saveLocation, value); } + /// + /// 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. + /// + public bool IsDownloading { + get => _isDownloading; + set => this.RaiseAndSetIfChanged(ref _isDownloading, value); + } + /// /// Indicates if the file can be downloaded. If not, the download button is disabled. /// @@ -47,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. /// @@ -109,7 +125,17 @@ public partial class MainWindowViewModel : ViewModelBase { // The command string format to use: yt-dlp -t -P --no-playlist // yt-dlp requires usage of ffmpeg as well. // Code will be executed when the button is clicked. - return await Task.Run(() => new List([Link, "-t", Format, "-P", SaveLocation, "--no-playlist"])); + + var fileName = ""; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + Console.WriteLine("Using Windows program."); + fileName = "Programs/yt-dlp-win64.exe"; + } 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([fileName, Link, "-t", Format, "-P", SaveLocation, "--no-playlist"])); } /// @@ -122,9 +148,27 @@ public partial class MainWindowViewModel : ViewModelBase { var args = await GenerateCmdArgsAsync(); // Run the command. try { - System.Diagnostics.Process.Start("yt-dlp", args); + // The command that's run must work on at least Linux and Windows + ProcessStartInfo info = new() { + FileName = args[0], + RedirectStandardOutput = true, + RedirectStandardError = 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) { Console.WriteLine(ex.ToString()); } } + + /// + /// 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)); + } } diff --git a/PantherByte/ViewModels/ProgressWindowViewModel.cs b/PantherByte/ViewModels/ProgressWindowViewModel.cs new file mode 100644 index 0000000..9527156 --- /dev/null +++ b/PantherByte/ViewModels/ProgressWindowViewModel.cs @@ -0,0 +1,92 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using ReactiveUI; + +namespace PantherByte.ViewModels; + +public class ProgressWindowViewModel : ViewModelBase { + // 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; + private string _processStdOut = ""; + private bool _error = false; + private bool _inProgress = false; + private string _statusMessage = "Downloading video..."; + 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 ProgressWindowViewModel(ProcessStartInfo info) { + _processStartInfo = info; + Console.WriteLine("Created ProgressWindowViewModel instance"); + } + + public async Task RunProcessAsync() { + Process downloadProcess = new(); + downloadProcess.StartInfo = _processStartInfo; + + downloadProcess.OutputDataReceived += (_, e) => { + if (!string.IsNullOrWhiteSpace(e.Data)) { + Console.WriteLine(e.Data); + ProcessStdOut += e.Data + "\n"; + } + }; + + downloadProcess.ErrorDataReceived += (_, e) => { + if (string.IsNullOrWhiteSpace(e.Data)) return; + Console.WriteLine(e.Data); + ProcessStdOut += e.Data + "\n"; + Error = true; + StatusMessage = "Errors while downloading video."; + BarColor = "Red"; + }; + + Console.WriteLine("Download process is started."); + InProgress = true; + try { + downloadProcess.Start(); + downloadProcess.BeginOutputReadLine(); + downloadProcess.BeginErrorReadLine(); + await downloadProcess.WaitForExitAsync(); + } + catch (Exception e) { + Console.WriteLine(e.StackTrace); + ProcessStdOut += "Error: " + e.StackTrace + "\n"; + StatusMessage = "Error while downloading video."; + BarColor = "Red"; + Error = true; + } + finally { + InProgress = false; + + if (!Error) { + BarColor = "Green"; + StatusMessage = "Download finished!"; + } + } + } +} diff --git a/PantherByte/Views/MainWindow.axaml b/PantherByte/Views/MainWindow.axaml index 0dc4e32..5c61938 100644 --- a/PantherByte/Views/MainWindow.axaml +++ b/PantherByte/Views/MainWindow.axaml @@ -11,7 +11,8 @@ TransparencyLevelHint="AcrylicBlur" Background="Transparent" SizeToContent="WidthAndHeight" - CanMaximize="False"> + CanMaximize="False" + WindowStartupLocation="CenterScreen">