diff --git a/DanmakuPlayer.sln.DotSettings b/DanmakuPlayer.sln.DotSettings index 98c58d1..5d2291d 100644 --- a/DanmakuPlayer.sln.DotSettings +++ b/DanmakuPlayer.sln.DotSettings @@ -1,3 +1,4 @@  True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/DanmakuPlayer/App.xaml.cs b/DanmakuPlayer/App.xaml.cs index aa5ce13..8a74af2 100644 --- a/DanmakuPlayer/App.xaml.cs +++ b/DanmakuPlayer/App.xaml.cs @@ -1,3 +1,4 @@ +using System; using DanmakuPlayer.Services; using Microsoft.UI.Xaml; using WinUI3Utilities; @@ -8,6 +9,7 @@ public partial class App : Application { public App() { + Environment.SetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "--remote-debugging-port=9222"); InitializeComponent(); CurrentContext.Title = nameof(DanmakuPlayer); AppContext.Initialize(); diff --git a/DanmakuPlayer/AppConfig.cs b/DanmakuPlayer/AppConfig.cs index 6ff871f..9b335f6 100644 --- a/DanmakuPlayer/AppConfig.cs +++ b/DanmakuPlayer/AppConfig.cs @@ -22,8 +22,8 @@ public partial record AppConfig /// /// 前景色 /// - /// default: 0xFFA9A9A9 - public uint Foreground { get; set; } = 0xFFA9A9A9; + /// default: 0xFFFFFFFF + public uint Foreground { get; set; } = 0xFFFFFFFF; #endregion @@ -39,7 +39,7 @@ public partial record AppConfig /// 倍速(times) /// /// default: 1 ∈ [0.5, 2] - public double PlaySpeed { get; set; } = 1; + public double PlaybackRate { get; set; } = 1; /// /// 帧率(second) @@ -70,7 +70,7 @@ public partial record AppConfig /// /// 方便计算使用float [AttributeIgnore(typeof(SettingsViewModelAttribute<>), typeof(GenerateConstructorAttribute), typeof(AppContextAttribute<>))] - public float DanmakuActualDuration => (float)(DanmakuDuration * PlaySpeed); + public float DanmakuActualDuration => (float)(DanmakuDuration * PlaybackRate); /// /// 弹幕透明度 @@ -212,6 +212,22 @@ public partial record AppConfig #endregion + #region 网页设置 + + /// + /// 使用WebView2 + /// + /// default: + public bool EnableWebView2 { get; set; } = true; + + /// + /// 使用WebView2 + /// + /// default: + public bool LockWebView2 { get; set; } = true; + + #endregion + public AppConfig() { diff --git a/DanmakuPlayer/AppContext.cs b/DanmakuPlayer/AppContext.cs index 9b1d17d..0a77503 100644 --- a/DanmakuPlayer/AppContext.cs +++ b/DanmakuPlayer/AppContext.cs @@ -21,9 +21,7 @@ public static void Initialize() ? new() : appConfigurations; } - public static void SetDefaultAppConfig() => AppConfig = new(); - - public static AppConfig AppConfig { get; private set; } = null!; + public static AppConfig AppConfig { get; set; } = null!; public static BackgroundPanel BackgroundPanel { get; set; } = null!; diff --git a/DanmakuPlayer/DanmakuPlayer.csproj b/DanmakuPlayer/DanmakuPlayer.csproj index c9a0bbb..fb8d2f3 100644 --- a/DanmakuPlayer/DanmakuPlayer.csproj +++ b/DanmakuPlayer/DanmakuPlayer.csproj @@ -20,9 +20,11 @@ + + MSBuild:Compile @@ -48,6 +50,7 @@ + @@ -102,6 +105,11 @@ + + + MSBuild:Compile + + + + + + + + + + + + + - + + + + + - - - - + Maximum="{x:Bind Vm.TotalTime, Mode=OneWay}" + Minimum="0" + StepFrequency="0.1" + ThumbToolTipValueConverter="{StaticResource DoubleToTimeTextConverter}" + Value="{x:Bind Vm.Time, Mode=TwoWay}" /> + + + + + + + + + + + + + + + + + + Visibility="{x:Bind Vm.NavigateEditingTime, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"> + Visibility="{x:Bind Vm.EditingTime, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + diff --git a/DanmakuPlayer/Views/Controls/BackgroundPanel.xaml.cs b/DanmakuPlayer/Views/Controls/BackgroundPanel.xaml.cs index 6772f50..cc93cce 100644 --- a/DanmakuPlayer/Views/Controls/BackgroundPanel.xaml.cs +++ b/DanmakuPlayer/Views/Controls/BackgroundPanel.xaml.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; +using Windows.UI; using DanmakuPlayer.Models; using DanmakuPlayer.Resources; using DanmakuPlayer.Services; @@ -27,14 +29,18 @@ namespace DanmakuPlayer.Views.Controls; public sealed partial class BackgroundPanel : SwapChainPanel { - public void RaiseForegroundChanged() => _vm.RaiseForegroundChanged(); +#pragma warning disable CA1822, IDE0079 // 将成员标记为 static + [SuppressMessage("ReSharper", "MemberCanBeMadeStatic.Local")] + private Color TransparentColor => Color.FromArgb(0xff / 2, 0, 0, 0); +#pragma warning restore CA1822, IDE0079 - private readonly RootViewModel _vm = new(); + public RootViewModel Vm { get; } = new(); public BackgroundPanel() { AppContext.BackgroundPanel = this; InitializeComponent(); + LockWebView2Button.Icon = new FontIcon { Glyph = Vm.LockWebView2 ? "\uE785" : "\uE72E" }; DragMoveAndResizeHelper.RootPanel = this; AppContext.DanmakuCanvas = DanmakuCanvas; @@ -65,8 +71,11 @@ private async Task LoadDanmaku(Func>> acti _cancellationTokenSource.Dispose(); _cancellationTokenSource = new(); - _vm.TotalTime = 0; - _vm.Time = 0; + if (!WebView.HasVideo) + { + Vm.TotalTime = 0; + Vm.Time = 0; + } DanmakuHelper.ClearPool(); try @@ -75,7 +84,7 @@ private async Task LoadDanmaku(Func>> acti RootTeachingTip.ShowAndHide(string.Format(MainPanelResources.ObtainedAndFiltrating, tempPool.Count), TeachingTipSeverity.Information, Emoticon.Okay); - DanmakuHelper.Pool = await _filter.Filtrate(tempPool, _vm.AppConfig, _cancellationTokenSource.Token); + DanmakuHelper.Pool = await _filter.Filtrate(tempPool, Vm.AppConfig, _cancellationTokenSource.Token); var filtrateRate = tempPool.Count is 0 ? 0 : DanmakuHelper.Pool.Length * 100 / tempPool.Count; RootTeachingTip.ShowAndHide(string.Format(MainPanelResources.FiltratedAndRendering, DanmakuHelper.Pool.Length, filtrateRate), TeachingTipSeverity.Information, Emoticon.Okay); @@ -83,7 +92,8 @@ private async Task LoadDanmaku(Func>> acti var renderedCount = await DanmakuHelper.Render(DanmakuCanvas, RenderType.RenderInit, _cancellationTokenSource.Token); var renderRate = DanmakuHelper.Pool.Length is 0 ? 0 : renderedCount * 100 / DanmakuHelper.Pool.Length; var totalRate = tempPool.Count is 0 ? 0 : renderedCount * 100 / tempPool.Count; - _vm.TotalTime = (DanmakuHelper.Pool.Length is 0 ? 0 : DanmakuHelper.Pool[^1].Time) + _vm.AppConfig.DanmakuActualDuration; + if (!WebView.HasVideo) + Vm.TotalTime = (DanmakuHelper.Pool.Length is 0 ? 0 : DanmakuHelper.Pool[^1].Time) + Vm.AppConfig.DanmakuActualDuration; RootTeachingTip.ShowAndHide(string.Format(MainPanelResources.DanmakuReady, DanmakuHelper.Pool.Length, filtrateRate, renderRate, totalRate), TeachingTipSeverity.Ok, Emoticon.Okay); } @@ -97,9 +107,7 @@ private async Task LoadDanmaku(Func>> acti RootTeachingTip.ShowAndHide(Emoticon.Depressed + " " + MainPanelResources.ExceptionThrown, TeachingTipSeverity.Error, e.Message); } - if (BannerTextBlock is not null) - _ = Children.Remove(BannerTextBlock); - _vm.StartPlaying = true; + Vm.StartPlaying = true; } public async void ReloadDanmaku(RenderType renderType) @@ -125,26 +133,43 @@ public async void ReloadDanmaku(RenderType renderType) TryResume(); } + public void ResetProvider() => ReloadDanmaku(RenderType.ReloadProvider); + + public void DanmakuFontChanged() => ReloadDanmaku(RenderType.ReloadFormats); + #region 播放及暂停 private DateTime _lastTime; + private int _tickCount; + private void TimerTick(object? sender, object e) { var now = DateTime.Now; - if (_vm.Time < _vm.TotalTime) + if (Vm.Time < Vm.TotalTime) { - if (_vm.IsPlaying) + if (Vm.IsPlaying) { - _vm.ActualTime += (now - _lastTime).TotalSeconds; + Vm.ActualTime += (now - _lastTime).TotalSeconds; DanmakuCanvas.Invalidate(); } } else { Pause(); - _vm.Time = 0; + Vm.Time = 0; } + + if (WebView.HasVideo) + { + ++_tickCount; + if (_tickCount is 10) + { + _tickCount = 0; + UpdateTime(); + } + } + _lastTime = now; } @@ -152,7 +177,7 @@ private void TimerTick(object? sender, object e) private void TryPause() { - _needResume = _vm.IsPlaying; + _needResume = Vm.IsPlaying; Pause(); } @@ -163,17 +188,38 @@ private void TryResume() _needResume = false; } - private void Resume() + private async void Resume() { _lastTime = DateTime.Now; DanmakuHelper.RenderType = RenderType.RenderAlways; - _vm.IsPlaying = true; + Vm.IsPlaying = true; + if (WebView.HasVideo) + { + await WebView.PlayAsync(); + UpdateTime(); + } } - private void Pause() + private async void Pause() { DanmakuHelper.RenderType = RenderType.RenderOnce; - _vm.IsPlaying = false; + Vm.IsPlaying = false; + if (WebView.HasVideo) + { + await WebView.PauseAsync(); + UpdateTime(); + } + } + + private async void UpdateTime(double? time = null) + { + Vm.Time = time ?? await WebView.CurrentTimeAsync(); + } + + public async void TrySetPlaybackRate() + { + if (WebView.HasVideo) + await WebView.SetPlaybackRateAsync(Vm.AppConfig.PlaybackRate); } #endregion @@ -184,17 +230,9 @@ private void Pause() #region SwapChainPanel事件 - private void RootDoubleTapped(object sender, DoubleTappedRoutedEventArgs e) + private void MaximizeRestoreTapped(object sender, RoutedEventArgs e) { - switch (CurrentContext.OverlappedPresenter.State) - { - case OverlappedPresenterState.Maximized: - CurrentContext.OverlappedPresenter.Restore(); - break; - case OverlappedPresenterState.Restored: - CurrentContext.OverlappedPresenter.Maximize(); - break; - } + Vm.IsMaximized = !Vm.IsMaximized; } private void RootSizeChanged(object sender, SizeChangedEventArgs e) @@ -211,44 +249,45 @@ private void RootUnloaded(object sender, RoutedEventArgs e) _cancellationTokenSource.Dispose(); } - #region 快进快退快捷键 + #endregion - private void RewindInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs e) - { - if (_vm.Time - _vm.AppConfig.PlayFastForward < 0) - _vm.Time = 0; - else - _vm.Time -= _vm.AppConfig.PlayFastForward; - } + #region Title区按钮 - private void FastForwardInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs e) + private void CloseTapped(object sender, TappedRoutedEventArgs e) => CurrentContext.App.Exit(); + + private void FrontTapped(object sender, TappedRoutedEventArgs e) { - if (_vm.Time + _vm.AppConfig.PlayFastForward > _vm.TotalTime) - _vm.Time = 0; - else - _vm.Time += _vm.AppConfig.PlayFastForward; + Vm.TopMost = !CurrentContext.OverlappedPresenter.IsAlwaysOnTop; + RootTeachingTip.ShowAndHide( + Vm.TopMost ? MainPanelResources.TopMostOn : MainPanelResources.TopMostOff, + TeachingTipSeverity.Information, + Emoticon.Okay); } - #endregion + private async void SettingTapped(object sender, IWinRTObject e) => await DialogSetting.ShowAsync(); #endregion - #region Title区按钮 + #region 导航区按钮 - private void CloseTapped(object sender, TappedRoutedEventArgs e) => CurrentContext.App.Exit(); + private async void GoBackTapped(object sender, TappedRoutedEventArgs e) + { + await WebView.GoBackAsync(); + } - private void FrontTapped(object sender, TappedRoutedEventArgs e) + private async void AddressBoxOnQuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) { - _vm.TopMost = !CurrentContext.OverlappedPresenter.IsAlwaysOnTop; - if (_vm.TopMost) - RootTeachingTip.ShowAndHide(MainPanelResources.TopMostOn, TeachingTipSeverity.Information, - Emoticon.Okay); - else - RootTeachingTip.ShowAndHide(MainPanelResources.TopMostOff, TeachingTipSeverity.Information, - Emoticon.Okay); + await WebView.GotoAsync(sender.Text); } - private async void SettingTapped(object sender, IWinRTObject e) => await DialogSetting.ShowAsync(); + private void WebViewOnPageLoaded(WebView2ForVideo sender, EventArgs e) + { + if (sender.HasVideo) + { + Vm.TotalTime = sender.Duration; + TrySetPlaybackRate(); + } + } #endregion @@ -302,58 +341,133 @@ await LoadDanmaku(async token => private void PauseResumeTapped(object sender, IWinRTObject e) { - if (_vm.IsPlaying) + if (Vm.IsPlaying) Pause(); else Resume(); } - private void DanmakuCanvasCreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs e) => DanmakuHelper.Current = new(sender, _vm.AppConfig); + private async void RewindTapped(object sender, IWinRTObject e) + { + if (WebView.HasVideo) + UpdateTime(await WebView.IncreaseCurrentTimeAsync(-Vm.AppConfig.PlayFastForward)); + else if (Vm.Time - Vm.AppConfig.PlayFastForward < 0) + Vm.Time = 0; + else + Vm.Time -= Vm.AppConfig.PlayFastForward; + } + + private async void FastForwardTapped(object sender, IWinRTObject e) + { + if (WebView.HasVideo) + UpdateTime(await WebView.IncreaseCurrentTimeAsync(Vm.AppConfig.PlayFastForward)); + else if (Vm.Time + Vm.AppConfig.PlayFastForward > Vm.TotalTime) + Vm.Time = 0; + else + Vm.Time += Vm.AppConfig.PlayFastForward; + } + + private void DanmakuCanvasCreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs e) => DanmakuHelper.Current = new(sender, Vm.AppConfig); + + private void DanmakuCanvasDraw(CanvasControl sender, CanvasDrawEventArgs e) => DanmakuHelper.Rendering(sender, e, (float)Vm.Time, Vm.AppConfig); - private void DanmakuCanvasDraw(CanvasControl sender, CanvasDrawEventArgs e) => DanmakuHelper.Rendering(sender, e, (float)_vm.Time, _vm.AppConfig); + // TODO: Time Slider // private void TimePointerPressed(object sender, PointerRoutedEventArgs e) => TryPause(); // private void TimePointerReleased(object sender, PointerRoutedEventArgs e) => TryResume(); + // private void SliderOnManipulationCompleted(object sender, PointerRoutedEventArgs pointerRoutedEventArgs) + // { + // Debug.WriteLine("ManipulationCompleted"); + // WebView.SetCurrentTimeAsync(Vm.ActualTime); + // WebView.SetPlaybackRateAsync(3); + // } + #endregion - #region 区域显隐触发 + #region WebView视频控制 - private void ImportPointerEntered(object sender, PointerRoutedEventArgs e) => _vm.PointerInImportArea = true; + private void PlaybackRateOnTapped(object sender, TappedRoutedEventArgs e) + { + Vm.AppConfig.PlaybackRate = double.Parse(sender.To().Text); + DispatcherTimerHelper.ResetTimerInterval(); + ResetProvider(); + TrySetPlaybackRate(); + AppContext.SaveConfiguration(Vm.AppConfig); + } - private void ImportPointerExited(object sender, PointerRoutedEventArgs e) => _vm.PointerInImportArea = false; + private async void MuteOnTapped(object sender, TappedRoutedEventArgs e) + { + if (!WebView.HasVideo) + return; + Vm.Mute = await WebView.MutedFlipAsync(); + } - private void TitlePointerEntered(object sender, PointerRoutedEventArgs e) => _vm.PointerInTitleArea = true; + private double Volume + { + get => WebView.HasVideo ? WebView.VolumeAsync().GetAwaiter().GetResult() * 100 : 0; + set + { + // TODO: Volume + return; + if (WebView.HasVideo) + WebView.SetVolumeAsync(value / 100).GetAwaiter().GetResult(); + } + } - private void TitlePointerExited(object sender, PointerRoutedEventArgs e) => _vm.PointerInTitleArea = false; + private void LockWebView2OnTapped(object sender, TappedRoutedEventArgs e) + { + if (!WebView.HasVideo) + return; + Vm.LockWebView2 = !Vm.LockWebView2; + } - private void ControlPointerEntered(object sender, PointerRoutedEventArgs e) => _vm.PointerInControlArea = true; + private async void FullScreenOnTapped(object sender, TappedRoutedEventArgs e) + { + if (!WebView.HasVideo) + return; + var button = sender.To(); + if (await WebView.FullScreenAsync()) + { + await WebView.ExitFullScreenAsync(); + button.Icon.To().Symbol = Symbol.FullScreen; + } + else + { + await WebView.RequestFullScreenAsync(); + button.Icon.To().Symbol = Symbol.BackToWindow; + } + } - private void ControlPointerExited(object sender, PointerRoutedEventArgs e) => _vm.PointerInControlArea = false; + #endregion #region 进度条时间输入 private void TimeTextTapped(object sender, TappedRoutedEventArgs e) { - TimeText.Text = DoubleToTimeTextConverter.ToTime(_vm.Time); - _vm.EditingTime = true; + TimeText.Text = DoubleToTimeTextConverter.ToTime(Vm.Time); + Vm.EditingTime = true; } - private void TimeTextLostFocus(object sender, RoutedEventArgs e) => _vm.EditingTime = false; + private void TimeTextLostFocus(object sender, RoutedEventArgs e) => Vm.EditingTime = false; - private void TimeTextInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs e) + private async void TimeTextInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs e) { if (TimeSpan.TryParse(TimeText.Text/*.ReplaceLineEndings("")*/, out var result)) - _vm.Time = Math.Max(Math.Min(TimeText.Text.Count(c => c is ':') switch + { + Vm.Time = Math.Max(Math.Min(TimeText.Text.Count(c => c is ':') switch { 0 => result.TotalDays, 1 => result.TotalMinutes, 2 => result.TotalSeconds, _ => 1 - }, _vm.TotalTime), 0); + }, Vm.TotalTime), 0); + if (WebView.HasVideo) + await WebView.SetCurrentTimeAsync(Vm.Time); + } - _vm.EditingTime = false; + Vm.EditingTime = false; } private void TimeTextIsEditing(object sender, DependencyPropertyChangedEventArgs e) @@ -369,6 +483,4 @@ private void TimeTextIsEditing(object sender, DependencyPropertyChangedEventArgs #endregion #endregion - - #endregion } diff --git a/DanmakuPlayer/Views/Controls/CollapsibleArea.cs b/DanmakuPlayer/Views/Controls/CollapsibleArea.cs new file mode 100644 index 0000000..e5e4a50 --- /dev/null +++ b/DanmakuPlayer/Views/Controls/CollapsibleArea.cs @@ -0,0 +1,38 @@ +using CommunityToolkit.WinUI.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using WinUI3Utilities; + +namespace DanmakuPlayer.Views.Controls; + +public sealed class CollapsibleArea : ContentControl +{ + public CollapsibleArea() + { + DefaultStyleKey = typeof(CollapsibleArea); + Loaded += (sender, _) => + { + var that = sender.To(); + that.Content.To().Visibility = Visibility.Collapsed; + if (that.FindDescendant() is { } border) + { + border.PointerEntered += OnPointerEntered; + border.PointerExited += OnPointerExited; + } + }; + } + + + public void OnPointerEntered(object sender, PointerRoutedEventArgs e) + { + base.OnPointerEntered(e); + sender.To().Child.To().Visibility = Visibility.Visible; + } + + public void OnPointerExited(object sender, PointerRoutedEventArgs e) + { + base.OnPointerExited(e); + sender.To().Child.To().Visibility = Visibility.Collapsed; + } +} diff --git a/DanmakuPlayer/Views/Controls/CollapsibleArea.xaml b/DanmakuPlayer/Views/Controls/CollapsibleArea.xaml new file mode 100644 index 0000000..1da9321 --- /dev/null +++ b/DanmakuPlayer/Views/Controls/CollapsibleArea.xaml @@ -0,0 +1,20 @@ + + + + diff --git a/DanmakuPlayer/Views/Controls/SettingsDialog.xaml b/DanmakuPlayer/Views/Controls/SettingsDialog.xaml index e0a6fa4..336bfe0 100644 --- a/DanmakuPlayer/Views/Controls/SettingsDialog.xaml +++ b/DanmakuPlayer/Views/Controls/SettingsDialog.xaml @@ -67,7 +67,7 @@ - + @@ -76,7 +76,6 @@ @@ -89,7 +88,7 @@ - + @@ -105,15 +104,17 @@ TickPlacement="Outside" Value="{x:Bind Vm.PlayFastForward, Mode=TwoWay}" /> - + + + + + Value="{x:Bind Vm.PlaybackRate, Mode=TwoWay}" /> @@ -125,7 +126,6 @@ StepFrequency="1" TickFrequency="20" TickPlacement="Outside" - ValueChanged="ResetTimer" Value="{x:Bind Vm.PlayFramePerSecond, Mode=TwoWay}" /> @@ -137,7 +137,6 @@ StepFrequency="1" TickFrequency="1" TickPlacement="Outside" - ValueChanged="ResetProvider" Value="{x:Bind Vm.DanmakuDuration, Mode=TwoWay}" /> @@ -150,14 +149,10 @@ StepFrequency="0.1" TickFrequency="0.2" TickPlacement="Outside" - ValueChanged="ResetProvider" Value="{x:Bind Vm.DanmakuOpacity, Mode=TwoWay}" /> - + @@ -282,7 +276,7 @@ - + @@ -297,7 +291,6 @@ ThumbToolTipValueConverter="{StaticResource CountLimitConverter}" TickFrequency="20" TickPlacement="Outside" - ValueChanged="ResetProvider" Value="{x:Bind Vm.DanmakuCountRollLimit, Mode=TwoWay}" /> @@ -308,7 +301,6 @@ ThumbToolTipValueConverter="{StaticResource CountLimitConverter}" TickFrequency="20" TickPlacement="Outside" - ValueChanged="ResetProvider" Value="{x:Bind Vm.DanmakuCountBottomLimit, Mode=TwoWay}" /> @@ -319,7 +311,6 @@ ThumbToolTipValueConverter="{StaticResource CountLimitConverter}" TickFrequency="20" TickPlacement="Outside" - ValueChanged="ResetProvider" Value="{x:Bind Vm.DanmakuCountTopLimit, Mode=TwoWay}" /> @@ -330,14 +321,26 @@ ThumbToolTipValueConverter="{StaticResource CountLimitConverter}" TickFrequency="20" TickPlacement="Outside" - ValueChanged="ResetProvider" Value="{x:Bind Vm.DanmakuCountInverseLimit, Mode=TwoWay}" /> - + + + + + + + + + + + + + + diff --git a/DanmakuPlayer/Views/Controls/SettingsDialog.xaml.cs b/DanmakuPlayer/Views/Controls/SettingsDialog.xaml.cs index e30326d..6862c87 100644 --- a/DanmakuPlayer/Views/Controls/SettingsDialog.xaml.cs +++ b/DanmakuPlayer/Views/Controls/SettingsDialog.xaml.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -9,6 +10,7 @@ using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; using WinUI3Utilities; @@ -17,26 +19,27 @@ namespace DanmakuPlayer.Views.Controls; [INotifyPropertyChanged] -[DependencyProperty("TopMost")] public sealed partial class SettingsDialog : UserControl { - [ObservableProperty] private SettingViewModel _vm = new(); + [ObservableProperty] private SettingViewModel _vm = null!; public SettingsDialog() => InitializeComponent(); - public async Task ShowAsync() => await Content.To().ShowAsync(); + public async Task ShowAsync() + { + Vm = new(); + _ = await Content.To().ShowAsync(); + } #region 事件处理 private void NavigateUriTapped(object sender, TappedRoutedEventArgs e) { - using var process = new Process + using var process = new Process(); + process.StartInfo = new() { - StartInfo = new() - { - FileName = sender.GetTag(), - UseShellExecute = true - } + FileName = sender.GetTag(), + UseShellExecute = true }; _ = process.Start(); } @@ -63,29 +66,16 @@ private void ThemeChanged(object sender, SelectionChangedEventArgs e) }; } - private void ForegroundColorChanged(ColorPicker sender, ColorChangedEventArgs e) => Parent.To().RaiseForegroundChanged(); - - private void ResetTimer(object sender, RoutedEventArgs e) => DispatcherTimerHelper.ResetTimerInterval(); - - private void ResetProvider(object sender, RoutedEventArgs e) => AppContext.BackgroundPanel.ReloadDanmaku(RenderType.ReloadProvider); - - private void ResetTimerAndProvider(object sender, RoutedEventArgs e) - { - ResetTimer(sender, e); - ResetProvider(sender, e); - } - - private void DanmakuFontChanged(object sender, SelectionChangedEventArgs e) => AppContext.BackgroundPanel.ReloadDanmaku(RenderType.ReloadFormats); - private void SetDefaultAppConfigClick(ContentDialog sender, ContentDialogButtonClickEventArgs e) { e.Cancel = true; - AppContext.SetDefaultAppConfig(); + Vm.AppConfig = new(); OnPropertyChanged(nameof(Vm)); } private void AddRegexPattern(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs e) { + // TODO: Localization if (string.IsNullOrEmpty(sender.Text)) { RegexErrorInfoBar.Severity = InfoBarSeverity.Warning; @@ -93,6 +83,7 @@ private void AddRegexPattern(AutoSuggestBox sender, AutoSuggestBoxQuerySubmitted RegexErrorInfoBar.IsOpen = true; return; } + if (Vm.PatternsCollection.Contains(sender.Text)) { RegexErrorInfoBar.Severity = InfoBarSeverity.Warning; @@ -100,6 +91,7 @@ private void AddRegexPattern(AutoSuggestBox sender, AutoSuggestBoxQuerySubmitted RegexErrorInfoBar.IsOpen = true; return; } + try { _ = new Regex(sender.Text); @@ -111,6 +103,7 @@ private void AddRegexPattern(AutoSuggestBox sender, AutoSuggestBoxQuerySubmitted RegexErrorInfoBar.IsOpen = true; return; } + RegexErrorInfoBar.IsOpen = false; Vm.PatternsCollection.Add(sender.Text); } @@ -126,12 +119,66 @@ private void RegexPatternChanged(AutoSuggestBox sender, AutoSuggestBoxTextChange ErrorBorder.BorderBrush = new SolidColorBrush(Colors.Red); return; } + ErrorBorder.BorderBrush = new SolidColorBrush(Colors.Transparent); } - private void RemoveTapped(object sender, TappedRoutedEventArgs e) => Vm.PatternsCollection.Remove(sender.GetTag()); + private void RemoveTapped(object sender, TappedRoutedEventArgs e) => + Vm.PatternsCollection.Remove(sender.GetTag()); - private void CloseClick(ContentDialog sender, ContentDialogButtonClickEventArgs e) => AppContext.SaveConfiguration(Vm.AppConfig); + private void CloseClick(ContentDialog sender, ContentDialogButtonClickEventArgs e) + { + var copy = AppContext.AppConfig; + AppContext.AppConfig = Vm.AppConfig; + CompareChanges(copy, Vm.AppConfig); + AppContext.SaveConfiguration(Vm.AppConfig); + } #endregion + + /// + /// 等事件在属性变化前触发, + /// 所以不能使用这些事件, 而是在保证属性变化后再去操作。此方法在关闭时调用 + /// + /// + /// +#pragma warning disable IDE0079 // 请删除不必要的忽略 + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")] +#pragma warning restore IDE0079 // 请删除不必要的忽略 + private void CompareChanges(AppConfig before, AppConfig after) + { + var backgroundPanel = Parent.To(); + if (before.PlaybackRate != after.PlaybackRate) + { + DispatcherTimerHelper.ResetTimerInterval(); + backgroundPanel.ResetProvider(); + backgroundPanel.TrySetPlaybackRate(); + } + else + { + if (before.RenderBefore != after.RenderBefore + || before.DanmakuDuration != after.DanmakuDuration + || before.DanmakuOpacity != after.DanmakuOpacity + || before.DanmakuScale != after.DanmakuScale + || before.DanmakuEnableOverlap != after.DanmakuEnableOverlap + || before.DanmakuCountRollLimit != after.DanmakuCountRollLimit + || before.DanmakuCountBottomLimit != after.DanmakuCountBottomLimit + || before.DanmakuCountTopLimit != after.DanmakuCountTopLimit + || before.DanmakuCountInverseLimit != after.DanmakuCountInverseLimit + || before.DanmakuCountM7Enable != after.DanmakuCountM7Enable) + backgroundPanel.ResetProvider(); + if (before.PlayFramePerSecond != after.PlayFramePerSecond) + DispatcherTimerHelper.ResetTimerInterval(); + } + if (before.DanmakuFont != after.DanmakuFont) + backgroundPanel.DanmakuFontChanged(); + if (before.Foreground != after.Foreground) + backgroundPanel.Vm.RaisePropertyChanged(nameof(AppConfig.Foreground)); + if (before.EnableWebView2 != after.EnableWebView2) + backgroundPanel.Vm.RaisePropertyChanged(nameof(AppConfig.EnableWebView2)); + if (before.LockWebView2 != after.LockWebView2) + backgroundPanel.Vm.RaisePropertyChanged(nameof(AppConfig.LockWebView2)); + if (before.TopMost != after.TopMost) + backgroundPanel.Vm.TopMost = after.TopMost; // 需要setter中设置OverlappedPresenter.IsAlwaysOnTop,所以不能直接用RaisePropertyChanged + } } diff --git a/DanmakuPlayer/Views/Controls/WebView2ForVideo.xaml b/DanmakuPlayer/Views/Controls/WebView2ForVideo.xaml new file mode 100644 index 0000000..81e6f32 --- /dev/null +++ b/DanmakuPlayer/Views/Controls/WebView2ForVideo.xaml @@ -0,0 +1,13 @@ + + + diff --git a/DanmakuPlayer/Views/Controls/WebView2ForVideo.xaml.cs b/DanmakuPlayer/Views/Controls/WebView2ForVideo.xaml.cs new file mode 100644 index 0000000..ab9f97c --- /dev/null +++ b/DanmakuPlayer/Views/Controls/WebView2ForVideo.xaml.cs @@ -0,0 +1,216 @@ +using System; +using System.Threading.Tasks; +using Windows.Foundation; +using Microsoft.Playwright; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using WinUI3Utilities; +using WinUI3Utilities.Attributes; + +namespace DanmakuPlayer.Views.Controls; + +[DependencyProperty("Duration")] +[DependencyProperty("CanGoForward")] +[DependencyProperty("CanGoBack")] +[DependencyProperty("HasVideo")] +public sealed partial class WebView2ForVideo : UserControl +{ + private WebView2 WebView2 => Content.To(); + private IPlaywright Pw { get; set; } = null!; + private IBrowser Browser { get; set; } = null!; + private IPage Page { get; set; } = null!; + private ILocator? Video { get; set; } + + public event TypedEventHandler? PageLoaded; + + public WebView2ForVideo() + { + InitializeComponent(); + Loaded += OnLoaded; + Unloaded += OnUnloaded; + } + + private async void OnLoaded(object sender, RoutedEventArgs e) + { + await WebView2.EnsureCoreWebView2Async(); + Pw = await Playwright.CreateAsync(); + Browser = await Pw.Chromium.ConnectOverCDPAsync("http://localhost:9222"); + Page = Browser.Contexts[0].Pages[0]; + } + + private async void OnUnloaded(object sender, RoutedEventArgs e) + { + await Browser.DisposeAsync(); + Pw.Dispose(); + } + + public async Task GotoAsync(string url) + { + try + { + _ = await Page.GotoAsync(url, new() { WaitUntil = WaitUntilState.Load }); + } + catch (PlaywrightException) // 网址错误不跳转 + { + return; + } + + Video = Page.Locator("video").First; + HasVideo = Video is not null; + if (HasVideo) + try + { + Duration = await DurationAsync(); + } + catch (ArgumentException) // 可能出现.NET不支持无穷浮点数异常 + { + try + { + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + Duration = await DurationAsync(); + } + catch (TimeoutException) + { + Duration = await DurationAsync(); + } + catch (ArgumentException) + { + Duration = 0; + } + } + PageLoaded?.Invoke(this, EventArgs.Empty); + } + + public async Task GoBackAsync() => await Page.GoBackAsync(); + + public async Task GoForwardAsync() => await Page.GoForwardAsync(); + + private async void WebView2Tapped(object sender, PointerRoutedEventArgs e) + { + var properties = e.GetCurrentPoint(sender.To()).Properties; + if (properties.IsXButton1Pressed) + { + if (CanGoBack) + await GoBackAsync(); + } + else if (properties.IsXButton2Pressed) + { + if (CanGoForward) + await GoForwardAsync(); + } + } + + #region JavaScript Property + + #region CurrentTime + + public async Task IncreaseCurrentTimeAsync(double second) + { + var duration = await Video!.EvaluateAsync($"video => video.currentTime += {second}"); + return duration!.Value.GetDouble(); + } + + public async Task SetCurrentTimeAsync(double second) + { + _ = await Video!.EvaluateAsync($"video => video.currentTime = {second}"); + } + + public async Task CurrentTimeAsync() + { + var duration = await Video!.EvaluateAsync("video => video.currentTime")!; + return duration!.Value.GetDouble(); + } + + #endregion + + #region Volume + + public async Task IncreaseVolumeAsync(double volume) + { + var duration = await Video!.EvaluateAsync($"video => video.volume += {volume}"); + return duration!.Value.GetDouble(); + } + + public async Task SetVolumeAsync(double volume) + { + _ = await Video!.EvaluateAsync($"video => video.volume = {volume}"); + } + + public async Task VolumeAsync() + { + var duration = await Video!.EvaluateAsync("video => video.volume")!; + return duration!.Value.GetDouble(); + } + + #endregion + + #region Muted + + public async Task MutedFlipAsync() + { + var duration = await Video!.EvaluateAsync("video => video.muted = !video.muted"); + return duration!.Value.GetBoolean(); + } + + public async Task SetMutedAsync(bool muted) + { + _ = await Video!.EvaluateAsync("video => video.muted = " + (muted ? "true" : "false")); + } + + public async Task MutedAsync() + { + var duration = await Video!.EvaluateAsync("video => video.muted"); + return duration!.Value.GetBoolean(); + } + + #endregion + + private async Task DurationAsync() + { + var duration = await Video!.EvaluateAsync("video => video.duration"); + return duration!.Value.GetDouble(); + } + + public async Task PlayAsync() + { + _ = await Video!.EvaluateAsync("video => video.play()"); + } + + public async Task PauseAsync() + { + _ = await Video!.EvaluateAsync("video => video.pause()"); + } + + public async Task PlaybackRateAsync() + { + _ = await Video!.EvaluateAsync("video => video.playbackRate"); + } + + public async Task SetPlaybackRateAsync(double playbackRate) + { + _ = await Video!.EvaluateAsync($"video => video.playbackRate = {playbackRate}"); + } + + #region FullScreen + + public async Task FullScreenAsync() + { + var fullScreen = await Video!.EvaluateAsync("video => window.document.fullscreenElement"); + return fullScreen.HasValue; + } + + public async Task RequestFullScreenAsync() + { + _ = await Video!.EvaluateAsync("video => video.requestFullscreen()"); + } + + public async Task ExitFullScreenAsync() + { + _ = await Video!.EvaluateAsync("video => window.document.exitFullscreen()"); + } + + #endregion + + #endregion +} diff --git a/DanmakuPlayer/Views/Converters/BoolToChromeMaximizeRestoreFontIconConverter.cs b/DanmakuPlayer/Views/Converters/BoolToChromeMaximizeRestoreFontIconConverter.cs new file mode 100644 index 0000000..78f8466 --- /dev/null +++ b/DanmakuPlayer/Views/Converters/BoolToChromeMaximizeRestoreFontIconConverter.cs @@ -0,0 +1,10 @@ +using Microsoft.UI.Xaml.Controls; + +namespace DanmakuPlayer.Views.Converters; + +public class BoolToChromeMaximizeRestoreFontIconConverter : BoolToIconConverter +{ + protected override object TrueValue => new FontIcon { Glyph = "\uE923" }; // Restore + + protected override object FalseValue => new FontIcon { Glyph = "\uE922" }; // Maximize +} diff --git a/DanmakuPlayer/Views/Converters/BoolToIconConverter.cs b/DanmakuPlayer/Views/Converters/BoolToIconConverter.cs new file mode 100644 index 0000000..df1567a --- /dev/null +++ b/DanmakuPlayer/Views/Converters/BoolToIconConverter.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using WinUI3Utilities; + +namespace DanmakuPlayer.Views.Converters; + +public abstract class BoolToIconConverter : IValueConverter +{ + protected abstract object TrueValue { get; } + + protected abstract object FalseValue { get; } + + public object Convert(object value, Type targetType, object parameter, string language) => value.To() ? TrueValue : FalseValue; + + public object ConvertBack(object value, Type targetType, object parameter, string language) => ThrowHelper.InvalidCast(); +} diff --git a/DanmakuPlayer/Views/Converters/BoolToLockUnLockFontIconConverter.cs b/DanmakuPlayer/Views/Converters/BoolToLockUnLockFontIconConverter.cs new file mode 100644 index 0000000..8e9a9d9 --- /dev/null +++ b/DanmakuPlayer/Views/Converters/BoolToLockUnLockFontIconConverter.cs @@ -0,0 +1,10 @@ +using Microsoft.UI.Xaml.Controls; + +namespace DanmakuPlayer.Views.Converters; + +public class BoolToLockUnLockFontIconConverter : BoolToIconConverter +{ + protected override object TrueValue => new FontIcon { Glyph = "\uE785" }; // UnLock + + protected override object FalseValue => new FontIcon { Glyph = "\uE72E" }; // Lock +} diff --git a/DanmakuPlayer/Views/Converters/BoolToMuteVolumeSymbolConverter.cs b/DanmakuPlayer/Views/Converters/BoolToMuteVolumeSymbolConverter.cs new file mode 100644 index 0000000..8ea59e9 --- /dev/null +++ b/DanmakuPlayer/Views/Converters/BoolToMuteVolumeSymbolConverter.cs @@ -0,0 +1,10 @@ +using Microsoft.UI.Xaml.Controls; + +namespace DanmakuPlayer.Views.Converters; + +public class BoolToMuteVolumeSymbolConverter : BoolToIconConverter +{ + protected override object TrueValue => Symbol.Volume; + + protected override object FalseValue => Symbol.Mute; +} diff --git a/DanmakuPlayer/Views/Converters/BoolToPinUnPinSymbolConverter.cs b/DanmakuPlayer/Views/Converters/BoolToPinUnPinSymbolConverter.cs index e627e39..46ad57a 100644 --- a/DanmakuPlayer/Views/Converters/BoolToPinUnPinSymbolConverter.cs +++ b/DanmakuPlayer/Views/Converters/BoolToPinUnPinSymbolConverter.cs @@ -1,13 +1,10 @@ -using System; using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Data; -using WinUI3Utilities; namespace DanmakuPlayer.Views.Converters; -public class BoolToPinUnPinSymbolConverter : IValueConverter +public class BoolToPinUnPinSymbolConverter : BoolToIconConverter { - public object Convert(object value, Type targetType, object parameter, string language) => value.To() ? Symbol.UnPin : Symbol.Pin; + protected override object TrueValue => Symbol.Pin; - public object ConvertBack(object value, Type targetType, object parameter, string language) => ThrowHelper.InvalidCast(); + protected override object FalseValue => Symbol.UnPin; } diff --git a/DanmakuPlayer/Views/Converters/BoolToPlayPauseSymbolConverter.cs b/DanmakuPlayer/Views/Converters/BoolToPlayPauseSymbolConverter.cs index dfe5624..7b4b6c0 100644 --- a/DanmakuPlayer/Views/Converters/BoolToPlayPauseSymbolConverter.cs +++ b/DanmakuPlayer/Views/Converters/BoolToPlayPauseSymbolConverter.cs @@ -1,13 +1,10 @@ -using System; using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Data; -using WinUI3Utilities; namespace DanmakuPlayer.Views.Converters; -public class BoolToPlayPauseSymbolConverter : IValueConverter +public class BoolToPlayPauseSymbolConverter : BoolToIconConverter { - public object Convert(object value, Type targetType, object parameter, string language) => ((bool)value) ? Symbol.Pause : Symbol.Play; + protected override object TrueValue => Symbol.Pause; - public object ConvertBack(object value, Type targetType, object parameter, string language) => ThrowHelper.InvalidCast(); + protected override object FalseValue => Symbol.Play; } diff --git a/DanmakuPlayer/Views/ViewModels/RootViewModel.cs b/DanmakuPlayer/Views/ViewModels/RootViewModel.cs index c56dc6e..05da372 100644 --- a/DanmakuPlayer/Views/ViewModels/RootViewModel.cs +++ b/DanmakuPlayer/Views/ViewModels/RootViewModel.cs @@ -1,39 +1,41 @@ using CommunityToolkit.Mvvm.ComponentModel; using DanmakuPlayer.Services; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml.Controls; using WinUI3Utilities; namespace DanmakuPlayer.Views.ViewModels; public partial class RootViewModel : ObservableObject { - public void RaiseForegroundChanged() => OnPropertyChanged(nameof(Foreground)); + public void RaisePropertyChanged(string propertyName) => OnPropertyChanged(propertyName); - public uint Foreground - { - get => AppConfig.Foreground; - set => SetProperty(AppConfig.Foreground, value, AppConfig, (setting, value) => setting.Foreground = value); - } + public uint Foreground => AppConfig.Foreground; - [ObservableProperty] private bool _startPlaying; + public bool EnableWebView2 => AppConfig.EnableWebView2; - [ObservableProperty] - [NotifyPropertyChangedFor(nameof(Time))] - private double _actualTime; + [ObservableProperty] private bool _mute; - public double Time + public bool LockWebView2 { - get => ActualTime * AppConfig.PlaySpeed; - set => ActualTime = value / AppConfig.PlaySpeed; + get => AppConfig.LockWebView2; + set + { + if (value == AppConfig.LockWebView2) + return; + AppConfig.LockWebView2 = value; + OnPropertyChanged(); + AppContext.SaveConfiguration(AppConfig); + } } - [ObservableProperty] private double _totalTime; - public bool TopMost { get => AppConfig.TopMost; set { - if (value == AppConfig.TopMost) + if (value == AppConfig.TopMost && + value == CurrentContext.OverlappedPresenter.IsAlwaysOnTop) return; CurrentContext.OverlappedPresenter.IsAlwaysOnTop = AppConfig.TopMost = value; OnPropertyChanged(); @@ -41,11 +43,40 @@ public bool TopMost } } - [ObservableProperty] private bool _pointerInTitleArea; + public bool IsMaximized + { + get => CurrentContext.OverlappedPresenter.State is OverlappedPresenterState.Maximized; + set + { + if (value == IsMaximized) + return; + if (value) + CurrentContext.OverlappedPresenter.Maximize(); + else + CurrentContext.OverlappedPresenter.Restore(); + OnPropertyChanged(); + } + } + + [ObservableProperty] private bool _startPlaying; + + /// + /// 现实时间 + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(Time))] + private double _actualTime; - [ObservableProperty] private bool _pointerInImportArea; + /// + /// 进度条时间 + /// + public double Time + { + get => ActualTime * AppConfig.PlaybackRate; + set => ActualTime = value / AppConfig.PlaybackRate; + } - [ObservableProperty] private bool _pointerInControlArea; + [ObservableProperty] private double _totalTime; [ObservableProperty] [NotifyPropertyChangedFor(nameof(NavigateEditingTime))] diff --git a/DanmakuPlayer/Views/ViewModels/SettingViewModel.cs b/DanmakuPlayer/Views/ViewModels/SettingViewModel.cs index 2968aa7..2338fa0 100644 --- a/DanmakuPlayer/Views/ViewModels/SettingViewModel.cs +++ b/DanmakuPlayer/Views/ViewModels/SettingViewModel.cs @@ -10,13 +10,12 @@ public partial class SettingViewModel : ObservableObject { public SettingViewModel() { - PatternsCollection = JsonSerializer.Deserialize>(RegexPatterns) ?? new ObservableCollection(); - PatternsCollection.CollectionChanged += (_, _) => RegexPatterns = JsonSerializer.Serialize(PatternsCollection); + AppConfig = AppContext.AppConfig with { }; + PatternsCollection = JsonSerializer.Deserialize>(AppConfig.RegexPatterns) ?? new ObservableCollection(); + PatternsCollection.CollectionChanged += (_, _) => AppConfig.RegexPatterns = JsonSerializer.Serialize(PatternsCollection); } public ObservableCollection PatternsCollection { get; } -#pragma warning disable CA1822 // 将成员标记为 static - public AppConfig AppConfig => AppContext.AppConfig; -#pragma warning restore CA1822 // 将成员标记为 static + public AppConfig AppConfig { get; set; } } diff --git a/README.md b/README.md index 631f97f..529031c 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,10 @@ B站视频[【炮姐/AMV】我永远都会守护在你的身边!](https://www. ![m7](https://github.com/Poker-sang/DanmakuPlayer/blob/master/readme/m7.png) +### 与背后播放器同步 + +![m7](https://github.com/Poker-sang/DanmakuPlayer/blob/master/readme/webview2.png) + ## 使用说明 ⚠️:指实现比较困难的功能 @@ -65,7 +69,15 @@ B站视频[【炮姐/AMV】我永远都会守护在你的身边!](https://www. * [x] 输入进度条 -* [ ] ⚠️ 和背后播放器同步 +* [x] 和背后网页播放器同步 + +* [x] 支持同时调整软件和网页视频的倍速、进度条、播放暂停等 + +* [ ] ⚠️ 支持网页视频跨域 + +* [ ] ⚠️ 支持网页视频调整音量 + +* [ ] ⚠️ 有网页时,支持**拖动**进度条 ### 弹幕 @@ -119,7 +131,7 @@ B站视频[【炮姐/AMV】我永远都会守护在你的身边!](https://www. 项目地址:[GitHub](https://github.com/Poker-sang/DanmakuPlayer) -版本:3.40 +版本:3.50 ## 联系方式 @@ -129,4 +141,4 @@ B站视频[【炮姐/AMV】我永远都会守护在你的身边!](https://www. QQ:[2639914082](http://wpa.qq.com/msgrd?v=3&uin=2639914082&site=qq&menu=yes) -2023.5.13 +2023.8.28 diff --git a/readme/webview2.png b/readme/webview2.png new file mode 100644 index 0000000..1390ea2 Binary files /dev/null and b/readme/webview2.png differ