feat: implement full download capability
This needs some cleanup, but is otherwise fully functional.
This commit is contained in:
1 parent
9328fc98fe
commit
71ab0fe1f4
13 files changed
+246
-9
No files matched your search
@@ -8,6 +8,7 @@ using System.Linq;
|
|||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using PantherByte.Services;
|
using PantherByte.Services;
|
||||||
|
using PantherByte.Services.Interfaces;
|
||||||
using PantherByte.ViewModels;
|
using PantherByte.ViewModels;
|
||||||
using PantherByte.Views;
|
using PantherByte.Views;
|
||||||
|
|
||||||
@@ -30,9 +31,7 @@ public partial class App : Application {
|
|||||||
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
|
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
|
||||||
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
|
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
|
||||||
DisableAvaloniaDataAnnotationValidation();
|
DisableAvaloniaDataAnnotationValidation();
|
||||||
desktop.MainWindow = new MainWindow {
|
desktop.MainWindow = new MainWindow(new MainWindowViewModel());
|
||||||
DataContext = new MainWindowViewModel(),
|
|
||||||
};
|
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
services.AddSingleton<IFileSaveService>(x => new FileService(desktop.MainWindow));
|
services.AddSingleton<IFileSaveService>(x => new FileService(desktop.MainWindow));
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||||
|
using PantherByte.ViewModels;
|
||||||
|
|
||||||
|
namespace PantherByte.Messages;
|
||||||
|
|
||||||
|
public class OpenProgressDialogMessage(ProcessStartInfo info) : AsyncRequestMessage<ProgressWindowViewModel?> {
|
||||||
|
public readonly ProcessStartInfo Info = info;
|
||||||
|
};
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Models\" />
|
<Folder Include="Models\" />
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
|
<Folder Include="Programs\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -28,4 +29,13 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
|
||||||
<PackageReference Include="ReactiveUI" Version="21.0.1" />
|
<PackageReference Include="ReactiveUI" Version="21.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="Programs\yt-dlp-linux">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Programs\yt-dlp-win64.exe">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
Executable
BIN
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,7 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
|
using PantherByte.Services.Interfaces;
|
||||||
|
|
||||||
namespace PantherByte.Services;
|
namespace PantherByte.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using System.Collections.Generic;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
namespace PantherByte;
|
namespace PantherByte.Services.Interfaces;
|
||||||
|
|
||||||
public interface IFileSaveService {
|
public interface IFileSaveService {
|
||||||
public Task<IStorageFolder?> ChooseFolderAsync();
|
public Task<IStorageFolder?> ChooseFolderAsync();
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using DynamicData;
|
||||||
|
using PantherByte.Messages;
|
||||||
using PantherByte.Services;
|
using PantherByte.Services;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
using PantherByte.Services.Interfaces;
|
||||||
|
|
||||||
namespace PantherByte.ViewModels;
|
namespace PantherByte.ViewModels;
|
||||||
|
|
||||||
@@ -12,6 +18,7 @@ public partial class MainWindowViewModel : ViewModelBase {
|
|||||||
private string _link = string.Empty;
|
private string _link = string.Empty;
|
||||||
private string _saveLocation = string.Empty;
|
private string _saveLocation = string.Empty;
|
||||||
private string _format = "mp3";
|
private string _format = "mp3";
|
||||||
|
private bool _isDownloading = false;
|
||||||
private bool _canDownload = true;
|
private bool _canDownload = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -24,7 +31,7 @@ public partial class MainWindowViewModel : ViewModelBase {
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The format to download the video in. Currently supported options are MP3, MP4, and WAV. This will become the
|
/// 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 <see cref="GenerateCmdStringAsync"/>.
|
/// argument passed to the "-t" argument in <see cref="GenerateCmdArgsAsync"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Format {
|
public string Format {
|
||||||
get => _format;
|
get => _format;
|
||||||
@@ -40,6 +47,15 @@ public partial class MainWindowViewModel : ViewModelBase {
|
|||||||
set => this.RaiseAndSetIfChanged(ref _saveLocation, value);
|
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>
|
/// <summary>
|
||||||
/// Indicates if the file can be downloaded. If not, the download button is disabled.
|
/// Indicates if the file can be downloaded. If not, the download button is disabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -109,7 +125,17 @@ public partial class MainWindowViewModel : ViewModelBase {
|
|||||||
// The command string format to use: yt-dlp <link> -t <mp3|mp4|wav> -P <path> --no-playlist
|
// 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.
|
// yt-dlp requires usage of ffmpeg as well.
|
||||||
// Code will be executed when the button is clicked.
|
// Code will be executed when the button is clicked.
|
||||||
return await Task.Run(() => new List<string>([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<string>([fileName, Link, "-t", Format, "-P", SaveLocation, "--no-playlist"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -122,9 +148,27 @@ public partial class MainWindowViewModel : ViewModelBase {
|
|||||||
var args = await GenerateCmdArgsAsync();
|
var args = await GenerateCmdArgsAsync();
|
||||||
// Run the command.
|
// Run the command.
|
||||||
try {
|
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) {
|
} catch (Exception ex) {
|
||||||
Console.WriteLine(ex.ToString());
|
Console.WriteLine(ex.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
TransparencyLevelHint="AcrylicBlur"
|
TransparencyLevelHint="AcrylicBlur"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
SizeToContent="WidthAndHeight"
|
SizeToContent="WidthAndHeight"
|
||||||
CanMaximize="False">
|
CanMaximize="False"
|
||||||
|
WindowStartupLocation="CenterScreen">
|
||||||
|
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<!-- This only sets the DataContext for the previewer in an IDE,
|
<!-- This only sets the DataContext for the previewer in an IDE,
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using PantherByte.Messages;
|
||||||
|
using PantherByte.ViewModels;
|
||||||
|
|
||||||
namespace PantherByte.Views;
|
namespace PantherByte.Views;
|
||||||
|
|
||||||
public partial class MainWindow : Window {
|
public partial class MainWindow : Window {
|
||||||
public MainWindow() {
|
public MainWindow(MainWindowViewModel viewModel) {
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
DataContext = viewModel;
|
||||||
|
|
||||||
|
WeakReferenceMessenger.Default.Register<MainWindow, OpenProgressDialogMessage>(this,
|
||||||
|
static (w, m) => {
|
||||||
|
var dialog = new ProgressWindow {
|
||||||
|
DataContext = new ProgressWindowViewModel(m.Info)
|
||||||
|
};
|
||||||
|
m.Reply(dialog.ShowDialog<ProgressWindowViewModel?>(w));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<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="700" d:DesignHeight="450"
|
||||||
|
x:Class="PantherByte.Views.ProgressWindow"
|
||||||
|
x:DataType="vm:ProgressWindowViewModel"
|
||||||
|
Title="Loading..."
|
||||||
|
TransparencyLevelHint="AcrylicBlur"
|
||||||
|
SizeToContent="WidthAndHeight"
|
||||||
|
CanMaximize="False"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
Width="700"
|
||||||
|
Spacing="10">
|
||||||
|
|
||||||
|
<StackPanel Spacing="10" Margin="10">
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Text="{Binding StatusMessage}"/>
|
||||||
|
<ProgressBar
|
||||||
|
IsIndeterminate="{Binding InProgress}"
|
||||||
|
Foreground="{Binding BarColor}"
|
||||||
|
Maximum="100"
|
||||||
|
Value="100"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Expander VerticalAlignment="Top" HorizontalAlignment="Stretch"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderBrush="Transparent"
|
||||||
|
BorderThickness="1"
|
||||||
|
Header="More information..."
|
||||||
|
Margin="5">
|
||||||
|
<Grid RowDefinitions="*" ColumnDefinitions="*">
|
||||||
|
<ScrollViewer Grid.Row="0"
|
||||||
|
Height="100"
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
VerticalScrollBarVisibility="Auto">
|
||||||
|
<TextBlock Text="{Binding ProcessStdOut}"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Expander>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using PantherByte.ViewModels;
|
||||||
|
|
||||||
|
namespace PantherByte.Views;
|
||||||
|
|
||||||
|
public partial class ProgressWindow : Window {
|
||||||
|
public ProgressWindow() {
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
// This feels a little fragile, but we'll leave it for now.
|
||||||
|
Opened += async (_, _) => {
|
||||||
|
if (DataContext is not ProgressWindowViewModel vm)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await vm.RunProcessAsync();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user