diff --git a/Directory.Build.props b/Directory.Build.props index 51d806364..aacec7b4d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - net8.0-windows + net8.0 999.9.9-dev Tyrrrz Copyright (C) Oleksii Holub diff --git a/YoutubeDownloader.sln b/YoutubeDownloader.sln index 76c4413b7..e0906555a 100644 --- a/YoutubeDownloader.sln +++ b/YoutubeDownloader.sln @@ -1,18 +1,19 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27428.2015 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.33920.267 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YoutubeDownloader", "YoutubeDownloader\YoutubeDownloader.csproj", "{AF6D645E-DDDD-4034-B644-D5328CC893C1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YoutubeDownloader", "YoutubeDownloader\YoutubeDownloader.csproj", "{AF6D645E-DDDD-4034-B644-D5328CC893C1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{131C2561-E5A1-43E8-BF38-40E2E23DB0A4}" ProjectSection(SolutionItems) = preProject + Changelog.md = Changelog.md + Directory.Build.props = Directory.Build.props License.txt = License.txt Readme.md = Readme.md - Directory.Build.props = Directory.Build.props EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YoutubeDownloader.Core", "YoutubeDownloader.Core\YoutubeDownloader.Core.csproj", "{5122A9DE-232C-4DA8-AD76-8B72AA377D5E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YoutubeDownloader.Core", "YoutubeDownloader.Core\YoutubeDownloader.Core.csproj", "{5122A9DE-232C-4DA8-AD76-8B72AA377D5E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/YoutubeDownloader/App.xaml b/YoutubeDownloader/App.xaml index 002332beb..146dce413 100644 --- a/YoutubeDownloader/App.xaml +++ b/YoutubeDownloader/App.xaml @@ -1,473 +1,109 @@ - - - - - - - - - - - - - - - #006400 - #FF8C00 - #8B0000 - - - - - - - - - - - + + + + - - + - + - + - - + - + - - + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + #006400 + #FF8C00 + #8B0000 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/YoutubeDownloader/App.xaml.cs b/YoutubeDownloader/App.xaml.cs index da5f87bdc..a946a4c33 100644 --- a/YoutubeDownloader/App.xaml.cs +++ b/YoutubeDownloader/App.xaml.cs @@ -1,8 +1,21 @@ -using System; +using System; +using System.Net; using System.Reflection; -using System.Windows.Media; -using MaterialDesignThemes.Wpf; -using YoutubeDownloader.Utils; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Input.Platform; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using Avalonia.Styling; +using AvaloniaWebView; +using Material.Styles.Themes; +using Microsoft.Extensions.DependencyInjection; +using PropertyChanged; +using YoutubeDownloader.Services; +using YoutubeDownloader.ViewModels; +using YoutubeDownloader.ViewModels.Framework; +using YoutubeDownloader.Views; +using YoutubeDownloader.Views.Framework; namespace YoutubeDownloader; @@ -10,7 +23,7 @@ public partial class App { private static Assembly Assembly { get; } = Assembly.GetExecutingAssembly(); - public static string Name { get; } = Assembly.GetName().Name!; + public static new string Name { get; } = Assembly.GetName().Name!; public static Version Version { get; } = Assembly.GetName().Version!; @@ -21,26 +34,27 @@ public partial class App public static string LatestReleaseUrl { get; } = ProjectUrl + "/releases/latest"; } -public partial class App +[DoNotNotify] +public partial class App : Application { + private readonly IServiceProvider _serviceProvider; + private static Theme LightTheme { get; } = - Theme.Create( - new MaterialDesignLightTheme(), - MediaColor.FromHex("#343838"), - MediaColor.FromHex("#F9A825") - ); + Theme.Create(Theme.Light, Color.Parse("#343838"), Color.Parse("#F9A825")); private static Theme DarkTheme { get; } = - Theme.Create( - new MaterialDesignDarkTheme(), - MediaColor.FromHex("#E8E8E8"), - MediaColor.FromHex("#F9A825") - ); + Theme.Create(Theme.Dark, Color.Parse("#E8E8E8"), Color.Parse("#F9A825")); + + public App() + { + _serviceProvider = ConfigureServices(); + } public static void SetLightTheme() { - var paletteHelper = new PaletteHelper(); - paletteHelper.SetTheme(LightTheme); + Current!.RequestedThemeVariant = ThemeVariant.Light; + var theme = Current.LocateMaterialTheme(); + theme.CurrentTheme = LightTheme; Current.Resources["SuccessBrush"] = new SolidColorBrush(Colors.DarkGreen); Current.Resources["CanceledBrush"] = new SolidColorBrush(Colors.DarkOrange); @@ -49,11 +63,61 @@ public static void SetLightTheme() public static void SetDarkTheme() { - var paletteHelper = new PaletteHelper(); - paletteHelper.SetTheme(DarkTheme); + Current!.RequestedThemeVariant = ThemeVariant.Dark; + var theme = Current.LocateMaterialTheme(); + theme.CurrentTheme = DarkTheme; Current.Resources["SuccessBrush"] = new SolidColorBrush(Colors.LightGreen); Current.Resources["CanceledBrush"] = new SolidColorBrush(Colors.Orange); Current.Resources["FailedBrush"] = new SolidColorBrush(Colors.OrangeRed); } + + public override void Initialize() + { + // Increase maximum concurrent connections + ServicePointManager.DefaultConnectionLimit = 20; + + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var rootViewModel = ActivatorUtilities.CreateInstance(_serviceProvider); + + desktop.MainWindow = new RootView { DataContext = rootViewModel, }; + } + + base.OnFrameworkInitializationCompleted(); + } + + public override void RegisterServices() + { + base.RegisterServices(); + + AvaloniaWebViewBuilder.Initialize(config => config.IsInPrivateModeEnabled = true); + } + + private ServiceProvider ConfigureServices() + { + var services = new ServiceCollection(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(sp => + sp.GetRequiredService().GetTopLevel()!.Clipboard! + ); + services.AddTransient(sp => Current!.ApplicationLifetime!); + services.AddTransient(_ => + (Current!.ApplicationLifetime! as IControlledApplicationLifetime)! + ); + + services.AddSingleton(_ => Current!.PlatformSettings!); + + return services.BuildServiceProvider(true); + } } diff --git a/YoutubeDownloader/Behaviors/MultiSelectionListBoxBehavior.cs b/YoutubeDownloader/Behaviors/MultiSelectionListBoxBehavior.cs deleted file mode 100644 index 44301c6b2..000000000 --- a/YoutubeDownloader/Behaviors/MultiSelectionListBoxBehavior.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Collections; -using System.Collections.Specialized; -using System.Linq; -using System.Windows; -using System.Windows.Controls; -using Microsoft.Xaml.Behaviors; - -namespace YoutubeDownloader.Behaviors; - -public class MultiSelectionListBoxBehavior : Behavior -{ - public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register( - nameof(SelectedItems), - typeof(IList), - typeof(MultiSelectionListBoxBehavior), - new FrameworkPropertyMetadata( - null, - FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, - OnSelectedItemsChanged - ) - ); - - private static void OnSelectedItemsChanged( - DependencyObject sender, - DependencyPropertyChangedEventArgs args - ) - { - var behavior = (MultiSelectionListBoxBehavior)sender; - if (behavior._modelHandled) - return; - - if (behavior.AssociatedObject is null) - return; - - behavior._modelHandled = true; - behavior.SelectItems(); - behavior._modelHandled = false; - } - - private bool _viewHandled; - private bool _modelHandled; - - public IList? SelectedItems - { - get => (IList?)GetValue(SelectedItemsProperty); - set => SetValue(SelectedItemsProperty, value); - } - - // Propagate selected items from the model to the view - private void SelectItems() - { - _viewHandled = true; - - AssociatedObject.SelectedItems.Clear(); - if (SelectedItems is not null) - { - foreach (var item in SelectedItems) - AssociatedObject.SelectedItems.Add(item); - } - - _viewHandled = false; - } - - // Propagate selected items from the view to the model - private void OnListBoxSelectionChanged(object? sender, SelectionChangedEventArgs args) - { - if (_viewHandled) - return; - if (AssociatedObject.Items.SourceCollection is null) - return; - - SelectedItems = AssociatedObject.SelectedItems.Cast().ToArray(); - } - - private void OnListBoxItemsChanged(object? sender, NotifyCollectionChangedEventArgs args) - { - if (_viewHandled) - return; - if (AssociatedObject.Items.SourceCollection is null) - return; - SelectItems(); - } - - protected override void OnAttached() - { - base.OnAttached(); - - AssociatedObject.SelectionChanged += OnListBoxSelectionChanged; - ((INotifyCollectionChanged)AssociatedObject.Items).CollectionChanged += - OnListBoxItemsChanged; - } - - protected override void OnDetaching() - { - base.OnDetaching(); - - if (AssociatedObject is not null) - { - AssociatedObject.SelectionChanged -= OnListBoxSelectionChanged; - ((INotifyCollectionChanged)AssociatedObject.Items).CollectionChanged -= - OnListBoxItemsChanged; - } - } -} diff --git a/YoutubeDownloader/Behaviors/VideoMultiSelectionListBoxBehavior.cs b/YoutubeDownloader/Behaviors/VideoMultiSelectionListBoxBehavior.cs deleted file mode 100644 index e098fdbbb..000000000 --- a/YoutubeDownloader/Behaviors/VideoMultiSelectionListBoxBehavior.cs +++ /dev/null @@ -1,5 +0,0 @@ -using YoutubeExplode.Videos; - -namespace YoutubeDownloader.Behaviors; - -public class VideoMultiSelectionListBoxBehavior : MultiSelectionListBoxBehavior; diff --git a/YoutubeDownloader/Bootstrapper.cs b/YoutubeDownloader/Bootstrapper.cs deleted file mode 100644 index 6ec94072e..000000000 --- a/YoutubeDownloader/Bootstrapper.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Net; -using Stylet; -using StyletIoC; -using YoutubeDownloader.Services; -using YoutubeDownloader.ViewModels; -using YoutubeDownloader.ViewModels.Framework; -#if !DEBUG -using System.Windows; -using System.Windows.Threading; -#endif - -namespace YoutubeDownloader; - -public class Bootstrapper : Bootstrapper -{ - protected override void OnStart() - { - base.OnStart(); - - // Set the default theme. - // Preferred theme will be set later, once the settings are loaded. - App.SetLightTheme(); - - // Increase maximum concurrent connections - ServicePointManager.DefaultConnectionLimit = 20; - } - - protected override void ConfigureIoC(IStyletIoCBuilder builder) - { - base.ConfigureIoC(builder); - - builder.Bind().ToSelf().InSingletonScope(); - builder.Bind().ToAbstractFactory(); - } - -#if !DEBUG - protected override void OnUnhandledException(DispatcherUnhandledExceptionEventArgs args) - { - base.OnUnhandledException(args); - - MessageBox.Show( - args.Exception.ToString(), - "Error occured", - MessageBoxButton.OK, - MessageBoxImage.Error - ); - } -#endif -} diff --git a/YoutubeDownloader/Converters/BoolToVisibilityConverter.cs b/YoutubeDownloader/Converters/BoolToVisibilityConverter.cs deleted file mode 100644 index abe7faf9a..000000000 --- a/YoutubeDownloader/Converters/BoolToVisibilityConverter.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Globalization; -using System.Windows; -using System.Windows.Data; - -namespace YoutubeDownloader.Converters; - -[ValueConversion(typeof(bool), typeof(Visibility))] -public partial class BoolToVisibilityConverter( - Visibility trueVisibility, - Visibility falseVisibility -) : IValueConverter -{ - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => - value is true ? trueVisibility : falseVisibility; - - public object ConvertBack( - object? value, - Type targetType, - object? parameter, - CultureInfo culture - ) => - value is Visibility visibility - ? visibility == trueVisibility - : throw new NotSupportedException(); -} - -public partial class BoolToVisibilityConverter -{ - public static BoolToVisibilityConverter VisibleOrCollapsed { get; } = - new(Visibility.Visible, Visibility.Collapsed); - - public static BoolToVisibilityConverter VisibleOrHidden { get; } = - new(Visibility.Visible, Visibility.Hidden); - - public static BoolToVisibilityConverter CollapsedOrVisible { get; } = - new(Visibility.Collapsed, Visibility.Visible); - - public static BoolToVisibilityConverter HiddenOrVisible { get; } = - new(Visibility.Hidden, Visibility.Visible); -} diff --git a/YoutubeDownloader/Converters/InverseBoolConverter.cs b/YoutubeDownloader/Converters/InverseBoolConverter.cs deleted file mode 100644 index f75db5d30..000000000 --- a/YoutubeDownloader/Converters/InverseBoolConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Globalization; -using System.Windows.Data; - -namespace YoutubeDownloader.Converters; - -[ValueConversion(typeof(bool), typeof(bool))] -public class InverseBoolConverter : IValueConverter -{ - public static InverseBoolConverter Instance { get; } = new(); - - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => - value is false; - - public object ConvertBack( - object? value, - Type targetType, - object? parameter, - CultureInfo culture - ) => value is false; -} diff --git a/YoutubeDownloader/Converters/IsEqualConverter.cs b/YoutubeDownloader/Converters/IsEqualConverter.cs new file mode 100644 index 000000000..a557c3227 --- /dev/null +++ b/YoutubeDownloader/Converters/IsEqualConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace YoutubeDownloader.Converters; + +public class IsEqualConverter : IValueConverter +{ + public bool Inverted { get; init; } = false; + + public static IsEqualConverter IsEqual { get; } = new IsEqualConverter(); + public static IsEqualConverter IsNotEqual { get; } = new IsEqualConverter { Inverted = true }; + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return EqualityComparer.Default.Equals(value, parameter) != Inverted; + } + + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture + ) + { + throw new NotSupportedException(); + } +} diff --git a/YoutubeDownloader/Converters/VideoQualityPreferenceToStringConverter.cs b/YoutubeDownloader/Converters/VideoQualityPreferenceToStringConverter.cs index fe045cfa2..fcb938d0d 100644 --- a/YoutubeDownloader/Converters/VideoQualityPreferenceToStringConverter.cs +++ b/YoutubeDownloader/Converters/VideoQualityPreferenceToStringConverter.cs @@ -1,11 +1,10 @@ using System; using System.Globalization; -using System.Windows.Data; +using Avalonia.Data.Converters; using YoutubeDownloader.Core.Downloading; namespace YoutubeDownloader.Converters; -[ValueConversion(typeof(VideoQualityPreference), typeof(string))] public class VideoQualityPreferenceToStringConverter : IValueConverter { public static VideoQualityPreferenceToStringConverter Instance { get; } = new(); diff --git a/YoutubeDownloader/Converters/VideoToHighestQualityThumbnailUrlConverter.cs b/YoutubeDownloader/Converters/VideoToHighestQualityThumbnailUrlConverter.cs index 3791eaf77..38b7e897c 100644 --- a/YoutubeDownloader/Converters/VideoToHighestQualityThumbnailUrlConverter.cs +++ b/YoutubeDownloader/Converters/VideoToHighestQualityThumbnailUrlConverter.cs @@ -1,12 +1,11 @@ using System; using System.Globalization; -using System.Windows.Data; +using Avalonia.Data.Converters; using YoutubeExplode.Common; using YoutubeExplode.Videos; namespace YoutubeDownloader.Converters; -[ValueConversion(typeof(IVideo), typeof(string))] public class VideoToHighestQualityThumbnailUrlConverter : IValueConverter { public static VideoToHighestQualityThumbnailUrlConverter Instance { get; } = new(); diff --git a/YoutubeDownloader/Converters/VideoToLowestQualityThumbnailUrlConverter.cs b/YoutubeDownloader/Converters/VideoToLowestQualityThumbnailUrlConverter.cs index 9720965f0..ed9b65220 100644 --- a/YoutubeDownloader/Converters/VideoToLowestQualityThumbnailUrlConverter.cs +++ b/YoutubeDownloader/Converters/VideoToLowestQualityThumbnailUrlConverter.cs @@ -1,12 +1,11 @@ using System; using System.Globalization; using System.Linq; -using System.Windows.Data; +using Avalonia.Data.Converters; using YoutubeExplode.Videos; namespace YoutubeDownloader.Converters; -[ValueConversion(typeof(IVideo), typeof(string))] public class VideoToLowestQualityThumbnailUrlConverter : IValueConverter { public static VideoToLowestQualityThumbnailUrlConverter Instance { get; } = new(); diff --git a/YoutubeDownloader/Program.cs b/YoutubeDownloader/Program.cs new file mode 100644 index 000000000..141003b50 --- /dev/null +++ b/YoutubeDownloader/Program.cs @@ -0,0 +1,27 @@ +using System; +using Avalonia; +using Avalonia.ReactiveUI; +using Avalonia.WebView.Desktop; + +namespace YoutubeDownloader; + +internal class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => + // prepare and run your App here + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() => + AppBuilder + .Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace() + .UseReactiveUI() + .UseDesktopWebView(); +} diff --git a/YoutubeDownloader/Services/SettingsService.cs b/YoutubeDownloader/Services/SettingsService.cs index eed220999..cbe76eab8 100644 --- a/YoutubeDownloader/Services/SettingsService.cs +++ b/YoutubeDownloader/Services/SettingsService.cs @@ -5,8 +5,8 @@ using System.Net; using System.Text.Json; using System.Text.Json.Serialization; +using Avalonia.Platform; using Cogwheel; -using Microsoft.Win32; using PropertyChanged; using YoutubeDownloader.Core.Downloading; using Container = YoutubeExplode.Videos.Streams.Container; @@ -64,20 +64,9 @@ public partial class SettingsService { private static bool IsDarkModeEnabledByDefault() { - try - { - return Registry - .CurrentUser.OpenSubKey( - "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", - false - ) - ?.GetValue("AppsUseLightTheme") - is 0; - } - catch - { - return false; - } + var platformColors = App.Current?.PlatformSettings?.GetColorValues(); + var isDark = platformColors?.ThemeVariant == PlatformThemeVariant.Dark; + return isDark; } } diff --git a/YoutubeDownloader/Utils/MediaColor.cs b/YoutubeDownloader/Utils/MediaColor.cs deleted file mode 100644 index 1c0612e92..000000000 --- a/YoutubeDownloader/Utils/MediaColor.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Windows.Media; - -namespace YoutubeDownloader.Utils; - -internal static class MediaColor -{ - public static Color FromHex(string hex) => (Color)ColorConverter.ConvertFromString(hex); -} diff --git a/YoutubeDownloader/ViewModels/Components/DashboardViewModel.cs b/YoutubeDownloader/ViewModels/Components/DashboardViewModel.cs index e8495e5ab..2c8769c19 100644 --- a/YoutubeDownloader/ViewModels/Components/DashboardViewModel.cs +++ b/YoutubeDownloader/ViewModels/Components/DashboardViewModel.cs @@ -1,10 +1,15 @@ using System; +using System.Collections.ObjectModel; using System.IO; using System.Linq; +using System.Reactive.Linq; using System.Threading.Tasks; +using Avalonia.Metadata; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.Input; using Gress; using Gress.Completable; -using Stylet; +using ReactiveUI; using YoutubeDownloader.Core.Downloading; using YoutubeDownloader.Core.Resolving; using YoutubeDownloader.Core.Tagging; @@ -16,7 +21,7 @@ namespace YoutubeDownloader.ViewModels.Components; -public class DashboardViewModel : PropertyChangedBase, IDisposable +public partial class DashboardViewModel : ViewModelBase, IDisposable { private readonly IViewModelFactory _viewModelFactory; private readonly DialogManager _dialogManager; @@ -33,7 +38,7 @@ public class DashboardViewModel : PropertyChangedBase, IDisposable public string? Query { get; set; } - public BindableCollection Downloads { get; } = []; + public ObservableCollection Downloads { get; } = []; public bool IsDownloadsAvailable => Downloads.Any(); @@ -49,27 +54,38 @@ SettingsService settingsService _progressMuxer = Progress.CreateMuxer().WithAutoReset(); - _settingsService.BindAndInvoke( - o => o.ParallelLimit, - (_, e) => _downloadSemaphore.MaxCount = e.NewValue - ); - - Progress.Bind( - o => o.Current, - (_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate) - ); - - Downloads.Bind(o => o.Count, (_, _) => NotifyOfPropertyChange(() => IsDownloadsAvailable)); + _settingsService + .WhenAnyValue(o => o.ParallelLimit) + .Subscribe(v => _downloadSemaphore.MaxCount = v); + Progress + .WhenAnyValue(o => o.Current) + .Subscribe(_ => OnPropertyChanged(nameof(IsProgressIndeterminate))); + Downloads + .WhenAnyValue(o => o.Count) + .Subscribe(_ => OnPropertyChanged(nameof(IsDownloadsAvailable))); + this.WhenAnyValue(o => o.Query) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => ProcessQueryCommand.NotifyCanExecuteChanged()); + this.WhenAnyValue(o => o.IsBusy) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => + { + ProcessQueryCommand.NotifyCanExecuteChanged(); + ShowAuthSetupCommand.NotifyCanExecuteChanged(); + ShowSettingsCommand.NotifyCanExecuteChanged(); + }); } public bool CanShowAuthSetup => !IsBusy; - public async void ShowAuthSetup() => + [RelayCommand(CanExecute = nameof(CanShowAuthSetup))] + public async Task ShowAuthSetupAsync() => await _dialogManager.ShowDialogAsync(_viewModelFactory.CreateAuthSetupViewModel()); public bool CanShowSettings => !IsBusy; - public async void ShowSettings() => + [RelayCommand(CanExecute = nameof(CanShowSettings))] + public async Task ShowSettingsAsync() => await _dialogManager.ShowDialogAsync(_viewModelFactory.CreateSettingsViewModel()); private void EnqueueDownload(DownloadViewModel download, int position = 0) @@ -154,10 +170,15 @@ ex is OperationCanceledException Downloads.Insert(position, download); } + [DependsOn(nameof(IsBusy))] + [DependsOn(nameof(Query))] public bool CanProcessQuery => !IsBusy && !string.IsNullOrWhiteSpace(Query); - public async void ProcessQuery() + [RelayCommand(CanExecute = nameof(CanProcessQuery))] + public async Task ProcessQueryAsync() { + Dispatcher.UIThread.CheckAccess(); + if (string.IsNullOrWhiteSpace(Query)) return; @@ -274,6 +295,7 @@ or DownloadStatus.Canceled } } + [RelayCommand] public void RestartDownload(DownloadViewModel download) { var position = Math.Max(0, Downloads.IndexOf(download)); diff --git a/YoutubeDownloader/ViewModels/Components/DownloadViewModel.cs b/YoutubeDownloader/ViewModels/Components/DownloadViewModel.cs index b70ff7279..774cc07c2 100644 --- a/YoutubeDownloader/ViewModels/Components/DownloadViewModel.cs +++ b/YoutubeDownloader/ViewModels/Components/DownloadViewModel.cs @@ -1,9 +1,13 @@ using System; using System.IO; +using System.Reactive.Linq; using System.Threading; -using System.Windows; +using System.Threading.Tasks; +using Avalonia.Input.Platform; +using CommunityToolkit.Mvvm.Input; using Gress; -using Stylet; +using PropertyChanged; +using ReactiveUI; using YoutubeDownloader.Core.Downloading; using YoutubeDownloader.Utils; using YoutubeDownloader.ViewModels.Dialogs; @@ -12,11 +16,11 @@ namespace YoutubeDownloader.ViewModels.Components; -public class DownloadViewModel : PropertyChangedBase, IDisposable +public partial class DownloadViewModel : ViewModelBase, IDisposable { private readonly IViewModelFactory _viewModelFactory; private readonly DialogManager _dialogManager; - + private readonly IClipboard _clipboard; private readonly CancellationTokenSource _cancellationTokenSource = new(); public IVideo? Video { get; set; } @@ -35,25 +39,45 @@ public class DownloadViewModel : PropertyChangedBase, IDisposable public CancellationToken CancellationToken => _cancellationTokenSource.Token; + [AlsoNotifyFor(nameof(IsRunning))] public DownloadStatus Status { get; set; } = DownloadStatus.Enqueued; + public bool IsRunning => Status is DownloadStatus.Started; + public bool IsCanceledOrFailed => Status is DownloadStatus.Canceled or DownloadStatus.Failed; public string? ErrorMessage { get; set; } - public DownloadViewModel(IViewModelFactory viewModelFactory, DialogManager dialogManager) + public DownloadViewModel( + IViewModelFactory viewModelFactory, + DialogManager dialogManager, + IClipboard clipboard + ) { _viewModelFactory = viewModelFactory; _dialogManager = dialogManager; - - Progress.Bind( - o => o.Current, - (_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate) - ); + _clipboard = clipboard; + + Progress + .WhenAnyValue(o => o.Current) + .Subscribe(_ => OnPropertyChanged(nameof(IsProgressIndeterminate))); + this.WhenAnyValue(o => o.Status) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => + { + OnPropertyChanged(nameof(IsRunning)); + CancelCommand.NotifyCanExecuteChanged(); + ShowFileCommand.NotifyCanExecuteChanged(); + OpenFileCommand.NotifyCanExecuteChanged(); + }); + this.WhenAnyValue(o => o.ErrorMessage) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(_ => CopyErrorMessageCommand.NotifyCanExecuteChanged()); } public bool CanCancel => Status is DownloadStatus.Enqueued or DownloadStatus.Started; + [RelayCommand(CanExecute = nameof(CanCancel))] public void Cancel() { if (!CanCancel) @@ -64,7 +88,8 @@ public void Cancel() public bool CanShowFile => Status == DownloadStatus.Completed; - public async void ShowFile() + [RelayCommand(CanExecute = nameof(CanShowFile))] + public async Task ShowFileAsync() { if (!CanShowFile) return; @@ -84,7 +109,8 @@ await _dialogManager.ShowDialogAsync( public bool CanOpenFile => Status == DownloadStatus.Completed; - public async void OpenFile() + [RelayCommand(CanExecute = nameof(CanOpenFile))] + public async Task OpenFileAsync() { if (!CanOpenFile) return; @@ -103,12 +129,13 @@ await _dialogManager.ShowDialogAsync( public bool CanCopyErrorMessage => !string.IsNullOrWhiteSpace(ErrorMessage); - public void CopyErrorMessage() + [RelayCommand(CanExecute = nameof(CanCopyErrorMessage))] + public async Task CopyErrorMessageAsync() { if (!CanCopyErrorMessage) return; - Clipboard.SetText(ErrorMessage!); + await _clipboard.SetTextAsync(ErrorMessage!); } public void Dispose() => _cancellationTokenSource.Dispose(); diff --git a/YoutubeDownloader/ViewModels/Dialogs/AuthSetupViewModel.cs b/YoutubeDownloader/ViewModels/Dialogs/AuthSetupViewModel.cs index b91fd45f5..79e5b1073 100644 --- a/YoutubeDownloader/ViewModels/Dialogs/AuthSetupViewModel.cs +++ b/YoutubeDownloader/ViewModels/Dialogs/AuthSetupViewModel.cs @@ -2,12 +2,17 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using Stylet; +using ReactiveUI; using YoutubeDownloader.Services; using YoutubeDownloader.ViewModels.Framework; namespace YoutubeDownloader.ViewModels.Dialogs; +public class AuthSetupViewDesignTimeViewModel() : AuthSetupViewModel(new SettingsService()) +{ + public new bool IsAuthenticated { get; set; } +} + public class AuthSetupViewModel : DialogScreen { private readonly SettingsService _settingsService; @@ -28,11 +33,11 @@ public AuthSetupViewModel(SettingsService settingsService) { _settingsService = settingsService; - _settingsService.BindAndInvoke( - o => o.LastAuthCookies, - (_, _) => NotifyOfPropertyChange(() => Cookies) - ); + _settingsService + .WhenAnyValue(o => o.LastAuthCookies) + .Subscribe(_ => OnPropertyChanged(nameof(Cookies))); - this.BindAndInvoke(o => o.Cookies, (_, _) => NotifyOfPropertyChange(() => IsAuthenticated)); + this.WhenAnyValue(o => o.Cookies) + .Subscribe(_ => OnPropertyChanged(nameof(IsAuthenticated))); } } diff --git a/YoutubeDownloader/ViewModels/Dialogs/DownloadMultipleSetupViewModel.cs b/YoutubeDownloader/ViewModels/Dialogs/DownloadMultipleSetupViewModel.cs index bd4b6a238..417458400 100644 --- a/YoutubeDownloader/ViewModels/Dialogs/DownloadMultipleSetupViewModel.cs +++ b/YoutubeDownloader/ViewModels/Dialogs/DownloadMultipleSetupViewModel.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Linq; -using System.Windows; +using System.Threading.Tasks; +using Avalonia.Input.Platform; +using CommunityToolkit.Mvvm.Input; using YoutubeDownloader.Core.Downloading; using YoutubeDownloader.Services; using YoutubeDownloader.Utils; @@ -13,17 +16,19 @@ namespace YoutubeDownloader.ViewModels.Dialogs; -public class DownloadMultipleSetupViewModel( +public partial class DownloadMultipleSetupViewModel( IViewModelFactory viewModelFactory, DialogManager dialogManager, - SettingsService settingsService + SettingsService settingsService, + IClipboard clipboard ) : DialogScreen> { public string? Title { get; set; } public IReadOnlyList? AvailableVideos { get; set; } - public IReadOnlyList? SelectedVideos { get; set; } + public ObservableCollection SelectedVideos { get; set; } = + new ObservableCollection(); public IReadOnlyList AvailableContainers { get; } = new[] { Container.Mp4, Container.WebM, Container.Mp3, new Container("ogg") }; @@ -36,19 +41,22 @@ SettingsService settingsService public VideoQualityPreference SelectedVideoQualityPreference { get; set; } = VideoQualityPreference.Highest; - public void OnViewLoaded() + protected override void OnViewLoaded() { SelectedContainer = settingsService.LastContainer; SelectedVideoQualityPreference = settingsService.LastVideoQualityPreference; + SelectedVideos.CollectionChanged += (_, _) => ConfirmCommand.NotifyCanExecuteChanged(); } - public void CopyTitle() => Clipboard.SetText(Title!); + [RelayCommand] + public async Task CopyTitleAsync() => await clipboard.SetTextAsync(Title!); - public bool CanConfirm => SelectedVideos!.Any(); + public bool CanConfirm => SelectedVideos.Any(); - public void Confirm() + [RelayCommand(CanExecute = nameof(CanConfirm))] + public async Task ConfirmAsync() { - var dirPath = dialogManager.PromptDirectoryPath(); + var dirPath = await dialogManager.PromptDirectoryPathAsync(); if (string.IsNullOrWhiteSpace(dirPath)) return; @@ -105,7 +113,9 @@ public static DownloadMultipleSetupViewModel CreateDownloadMultipleSetupViewMode viewModel.Title = title; viewModel.AvailableVideos = availableVideos; - viewModel.SelectedVideos = preselectVideos ? availableVideos : Array.Empty(); + viewModel.SelectedVideos = preselectVideos + ? new ObservableCollection(availableVideos) + : []; return viewModel; } diff --git a/YoutubeDownloader/ViewModels/Dialogs/DownloadSingleSetupViewModel.cs b/YoutubeDownloader/ViewModels/Dialogs/DownloadSingleSetupViewModel.cs index 64480f3a7..83cc77357 100644 --- a/YoutubeDownloader/ViewModels/Dialogs/DownloadSingleSetupViewModel.cs +++ b/YoutubeDownloader/ViewModels/Dialogs/DownloadSingleSetupViewModel.cs @@ -2,7 +2,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Windows; +using System.Threading.Tasks; +using Avalonia.Input.Platform; +using Avalonia.Platform.Storage; +using CommunityToolkit.Mvvm.Input; using YoutubeDownloader.Core.Downloading; using YoutubeDownloader.Services; using YoutubeDownloader.Utils; @@ -12,10 +15,11 @@ namespace YoutubeDownloader.ViewModels.Dialogs; -public class DownloadSingleSetupViewModel( +public partial class DownloadSingleSetupViewModel( IViewModelFactory viewModelFactory, DialogManager dialogManager, - SettingsService settingsService + SettingsService settingsService, + IClipboard clipboard ) : DialogScreen { public IVideo? Video { get; set; } @@ -24,21 +28,28 @@ SettingsService settingsService public VideoDownloadOption? SelectedDownloadOption { get; set; } - public void OnViewLoaded() + protected override void OnViewLoaded() { SelectedDownloadOption = AvailableDownloadOptions?.FirstOrDefault(o => o.Container == settingsService.LastContainer ); } - public void CopyTitle() => Clipboard.SetText(Video!.Title); + [RelayCommand] + public async Task CopyTitleAsync() => await clipboard.SetTextAsync(Video!.Title); - public void Confirm() + [RelayCommand] + public async Task ConfirmAsync() { var container = SelectedDownloadOption!.Container; - var filePath = dialogManager.PromptSaveFilePath( - $"{container.Name} file|*.{container.Name}", + var fileType = new FilePickerFileType($"{container.Name} file") + { + Patterns = new[] { $"*.{container.Name}" }, + }; + + var filePath = await dialogManager.PromptSaveFilePathAsync( + new[] { fileType }, FileNameTemplate.Apply(settingsService.FileNameTemplate, Video!, container) ); diff --git a/YoutubeDownloader/ViewModels/Dialogs/SettingsViewModel.cs b/YoutubeDownloader/ViewModels/Dialogs/SettingsViewModel.cs index 42601229c..ee7a5691a 100644 --- a/YoutubeDownloader/ViewModels/Dialogs/SettingsViewModel.cs +++ b/YoutubeDownloader/ViewModels/Dialogs/SettingsViewModel.cs @@ -4,6 +4,8 @@ namespace YoutubeDownloader.ViewModels.Dialogs; +public class SettingsDesignTimeViewModel() : SettingsViewModel(new SettingsService()) { } + public class SettingsViewModel(SettingsService settingsService) : DialogScreen { public bool IsAutoUpdateEnabled diff --git a/YoutubeDownloader/ViewModels/Framework/DialogManager.cs b/YoutubeDownloader/ViewModels/Framework/DialogManager.cs index 3716dfd44..20ed07416 100644 --- a/YoutubeDownloader/ViewModels/Framework/DialogManager.cs +++ b/YoutubeDownloader/ViewModels/Framework/DialogManager.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; -using MaterialDesignThemes.Wpf; -using Microsoft.Win32; -using Stylet; +using Avalonia.Platform.Storage; +using DialogHostAvalonia; +using YoutubeDownloader.Views.Framework; namespace YoutubeDownloader.ViewModels.Framework; @@ -38,7 +40,7 @@ void OnScreenClosed(object? closeSender, EventArgs closeArgs) await _dialogLock.WaitAsync(); try { - await DialogHost.Show(view, OnDialogOpened); + await DialogHost.Show(view!, OnDialogOpened); return dialogScreen.DialogResult; } finally @@ -47,27 +49,76 @@ void OnScreenClosed(object? closeSender, EventArgs closeArgs) } } - public string? PromptSaveFilePath(string filter = "All files|*.*", string defaultFilePath = "") + public async Task PromptSaveFilePathAsync( + IReadOnlyList? fileTypes = null, + string defaultFilePath = "" + ) { - var dialog = new SaveFileDialog + var topLevel = viewManager.GetTopLevel(); + + var storageProvider = topLevel?.StorageProvider; + if (storageProvider is null) + { + return null; + } + + var filePickResult = await storageProvider.SaveFilePickerAsync( + new() + { + FileTypeChoices = fileTypes, + SuggestedFileName = defaultFilePath, + DefaultExtension = Path.GetExtension(defaultFilePath).TrimStart('.') + } + ); + + if (filePickResult?.Path is Uri path) { - Filter = filter, - AddExtension = true, - FileName = defaultFilePath, - DefaultExt = Path.GetExtension(defaultFilePath) - }; + return path.LocalPath; + } - return dialog.ShowDialog() == true ? dialog.FileName : null; + return null; } - public string? PromptDirectoryPath(string defaultDirPath = "") + public async Task PromptDirectoryPathAsync(string defaultDirPath = "") { - var dialog = new OpenFolderDialog { InitialDirectory = defaultDirPath }; - return dialog.ShowDialog() == true ? dialog.FolderName : null; + var topLevel = viewManager.GetTopLevel(); + + var storageProvider = topLevel?.StorageProvider; + if (storageProvider is null) + { + return null; + } + + var startLocation = await GetStorageFolderAsync(storageProvider, defaultDirPath); + var folderPickResult = await storageProvider.OpenFolderPickerAsync( + new() { AllowMultiple = false, SuggestedStartLocation = startLocation } + ); + + if (folderPickResult.FirstOrDefault()?.Path is Uri path) + { + return path.LocalPath; + } + + return null; } public void Dispose() { _dialogLock.Dispose(); } + + private static async Task GetStorageFolderAsync( + IStorageProvider storageProvider, + string path + ) + { + if (string.IsNullOrEmpty(path)) + { + return null; + } + + var storageFolder = await storageProvider.TryGetFolderFromPathAsync(path); + + return storageFolder; + } } diff --git a/YoutubeDownloader/ViewModels/Framework/DialogScreen.cs b/YoutubeDownloader/ViewModels/Framework/DialogScreen.cs index cbbfd4349..3ca93eb53 100644 --- a/YoutubeDownloader/ViewModels/Framework/DialogScreen.cs +++ b/YoutubeDownloader/ViewModels/Framework/DialogScreen.cs @@ -1,14 +1,15 @@ using System; -using Stylet; +using CommunityToolkit.Mvvm.Input; namespace YoutubeDownloader.ViewModels.Framework; -public abstract class DialogScreen : PropertyChangedBase +public abstract partial class DialogScreen : ViewModelBase { public T? DialogResult { get; private set; } public event EventHandler? Closed; + [RelayCommand] public void Close(T? dialogResult = default) { DialogResult = dialogResult; diff --git a/YoutubeDownloader/ViewModels/Framework/SnackbarService.cs b/YoutubeDownloader/ViewModels/Framework/SnackbarService.cs new file mode 100644 index 000000000..0dd025cf9 --- /dev/null +++ b/YoutubeDownloader/ViewModels/Framework/SnackbarService.cs @@ -0,0 +1,49 @@ +using System; +using Avalonia.Threading; +using Material.Styles.Controls; +using Material.Styles.Models; + +namespace YoutubeDownloader.ViewModels.Framework; + +public class SnackbarService +{ + private readonly TimeSpan _defaultDuration; + + public SnackbarService(TimeSpan defaultDuration) + { + _defaultDuration = defaultDuration; + } + + /// + /// Posts to the default SnackBarHost + /// + public void Post(string message, TimeSpan? duration = null) + { + SnackbarHost.Post( + new SnackbarModel(message, duration ?? _defaultDuration), + null, + DispatcherPriority.Normal + ); + } + + /// + /// Posts to the default SnackBarHost + /// + public void Post( + string message, + string actionText, + Action actionHandler, + TimeSpan? duration = null + ) + { + SnackbarHost.Post( + new SnackbarModel( + message, + duration ?? _defaultDuration, + new SnackbarButtonModel { Text = actionText, Action = actionHandler } + ), + null, + DispatcherPriority.Normal + ); + } +} diff --git a/YoutubeDownloader/ViewModels/Framework/ViewModelBase.cs b/YoutubeDownloader/ViewModels/Framework/ViewModelBase.cs new file mode 100644 index 000000000..567fe256e --- /dev/null +++ b/YoutubeDownloader/ViewModels/Framework/ViewModelBase.cs @@ -0,0 +1,53 @@ +using System; +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace YoutubeDownloader.ViewModels.Framework; + +public partial class ViewModelBase : ObservableObject, IViewAware +{ + private Control? _view; + + protected virtual void OnViewLoaded() { } + + protected virtual void OnClose() { } + + void IViewAware.AttachView(Control view) + { + if (_view == view) + { + return; // Already attached + } + + _view = view; + + view.Loaded += OnViewLoaded; + view.Unloaded += OnViewUnloaded; + + if (view.IsLoaded) + { + OnViewLoaded(); + } + } + + private void OnViewUnloaded(object? sender, EventArgs e) + { + if (_view != null) + { + _view.Loaded -= OnViewLoaded; + _view.Unloaded -= OnViewUnloaded; + } + + OnClose(); + } + + private void OnViewLoaded(object? sender, EventArgs e) + { + OnViewLoaded(); + } +} + +public interface IViewAware +{ + void AttachView(Control control); +} diff --git a/YoutubeDownloader/ViewModels/Framework/ViewModelFactory.cs b/YoutubeDownloader/ViewModels/Framework/ViewModelFactory.cs new file mode 100644 index 000000000..71628e062 --- /dev/null +++ b/YoutubeDownloader/ViewModels/Framework/ViewModelFactory.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using YoutubeDownloader.ViewModels.Components; +using YoutubeDownloader.ViewModels.Dialogs; + +namespace YoutubeDownloader.ViewModels.Framework; + +public class ViewModelFactory(IServiceProvider serviceProvider) : IViewModelFactory +{ + public DashboardViewModel CreateDashboardViewModel() => GetViewModel(); + + public DownloadMultipleSetupViewModel CreateDownloadMultipleSetupViewModel() => + GetViewModel(); + + public AuthSetupViewModel CreateAuthSetupViewModel() => GetViewModel(); + + public DownloadSingleSetupViewModel CreateDownloadSingleSetupViewModel() => + GetViewModel(); + + public DownloadViewModel CreateDownloadViewModel() => GetViewModel(); + + public MessageBoxViewModel CreateMessageBoxViewModel() => GetViewModel(); + + public SettingsViewModel CreateSettingsViewModel() => GetViewModel(); + + private T GetViewModel() => + ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider); +} diff --git a/YoutubeDownloader/ViewModels/RootViewModel.cs b/YoutubeDownloader/ViewModels/RootViewModel.cs index 76e773fe8..4210842ab 100644 --- a/YoutubeDownloader/ViewModels/RootViewModel.cs +++ b/YoutubeDownloader/ViewModels/RootViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; -using MaterialDesignThemes.Wpf; -using Stylet; +using Avalonia.Controls.ApplicationLifetimes; using YoutubeDownloader.Services; using YoutubeDownloader.Utils; using YoutubeDownloader.ViewModels.Components; @@ -10,14 +9,17 @@ namespace YoutubeDownloader.ViewModels; -public class RootViewModel : Screen +public class RootViewModel : ViewModelBase { private readonly IViewModelFactory _viewModelFactory; private readonly DialogManager _dialogManager; private readonly SettingsService _settingsService; private readonly UpdateService _updateService; + private readonly IControlledApplicationLifetime _applicationLifetime; - public SnackbarMessageQueue Notifications { get; } = new(TimeSpan.FromSeconds(5)); + public string DisplayName { get; set; } + + public SnackbarService SnackbarService { get; set; } = new(TimeSpan.FromSeconds(5)); public DashboardViewModel Dashboard { get; } @@ -25,13 +27,15 @@ public RootViewModel( IViewModelFactory viewModelFactory, DialogManager dialogManager, SettingsService settingsService, - UpdateService updateService + UpdateService updateService, + IControlledApplicationLifetime applicationLifetime ) { _viewModelFactory = viewModelFactory; _dialogManager = dialogManager; _settingsService = settingsService; _updateService = updateService; + _applicationLifetime = applicationLifetime; Dashboard = _viewModelFactory.CreateDashboardViewModel(); @@ -70,27 +74,27 @@ private async Task CheckForUpdatesAsync() if (updateVersion is null) return; - Notifications.Enqueue($"Downloading update to {App.Name} v{updateVersion}..."); + SnackbarService.Post($"Downloading update to {App.Name} v{updateVersion}..."); await _updateService.PrepareUpdateAsync(updateVersion); - Notifications.Enqueue( + SnackbarService.Post( "Update has been downloaded and will be installed when you exit", "INSTALL NOW", () => { _updateService.FinalizeUpdate(true); - RequestClose(); + _applicationLifetime.Shutdown(); } ); } catch { // Failure to update shouldn't crash the application - Notifications.Enqueue("Failed to perform application update"); + SnackbarService.Post("Failed to perform application update"); } } - public async void OnViewFullyLoaded() + public async Task OnViewFullyLoadedAsync() { await ShowUkraineSupportMessageAsync(); await CheckForUpdatesAsync(); @@ -98,8 +102,6 @@ public async void OnViewFullyLoaded() protected override void OnViewLoaded() { - base.OnViewLoaded(); - _settingsService.Load(); // Sync the theme with settings @@ -118,7 +120,7 @@ _settingsService.LastAppVersion is not null && _settingsService.LastAppVersion != App.Version ) { - Notifications.Enqueue( + SnackbarService.Post( $"Successfully updated to {App.Name} v{App.VersionString}", "WHAT'S NEW", () => ProcessEx.StartShellExecute(App.LatestReleaseUrl) @@ -127,6 +129,8 @@ _settingsService.LastAppVersion is not null _settingsService.LastAppVersion = App.Version; _settingsService.Save(); } + + _ = OnViewFullyLoadedAsync(); } protected override void OnClose() diff --git a/YoutubeDownloader/Views/Components/DashboardView.xaml b/YoutubeDownloader/Views/Components/DashboardView.xaml index 0c26acec7..59a14d094 100644 --- a/YoutubeDownloader/Views/Components/DashboardView.xaml +++ b/YoutubeDownloader/Views/Components/DashboardView.xaml @@ -1,81 +1,68 @@ - - - - - - - + + - + - + - - - - - - - - - - - - - - - - - + Margin="8" + VerticalAlignment="Center" + Foreground="{DynamicResource PrimaryHueMidBrush}" + Kind="Search" /> + + + + + + @@ -83,14 +70,14 @@ Grid.Column="1" Margin="6" Padding="4" - Command="{s:Action ShowAuthSetup}" - Foreground="{DynamicResource MaterialDesignDarkForeground}" - Style="{DynamicResource MaterialDesignFlatButton}" - ToolTip="Authentication"> + Command="{Binding ShowAuthSetupCommand}" + Foreground="{DynamicResource MaterialDarkForegroundBrush}" + Theme="{StaticResource MaterialFlatButton}" + ToolTip.Tip="Authentication"> - @@ -101,14 +88,14 @@ Grid.Column="2" Margin="6" Padding="4" - Command="{s:Action ShowSettings}" - Foreground="{DynamicResource MaterialDesignDarkForeground}" - Style="{DynamicResource MaterialDesignFlatButton}" - ToolTip="Settings"> + Command="{Binding ShowSettingsCommand}" + Foreground="{DynamicResource MaterialDarkForegroundBrush}" + Theme="{StaticResource MaterialFlatButton}" + ToolTip.Tip="Settings"> - @@ -118,18 +105,18 @@ - + - + + IsVisible="{Binding IsDownloadsAvailable}"> - - + + - + - + - - - + + @@ -199,7 +180,7 @@ Width="48" Height="48" VerticalAlignment="Center" - Source="{Binding Video, Converter={x:Static converters:VideoToLowestQualityThumbnailUrlConverter.Instance}}" /> + asyncImageLoader:ImageLoader.Source="{Binding Video, Converter={x:Static converters:VideoToLowestQualityThumbnailUrlConverter.Instance}}" /> @@ -217,7 +198,7 @@ Foreground="{DynamicResource MaterialDesignBody}" Text="{Binding FileName}" TextTrimming="CharacterEllipsis" - ToolTip="{Binding FileName}" /> + ToolTip.Tip="{Binding FileName}" /> @@ -229,84 +210,57 @@ SortMemberPath="ProgressOperation.Progress"> - - - - - + - - - - + Value="{Binding Progress.Current.Fraction, Mode=OneWay}" + IsVisible="{Binding IsRunning}"/> - - + + + + - + + @@ -325,12 +279,11 @@