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