diff --git a/src/BiliLite.UWP/App.xaml.cs b/src/BiliLite.UWP/App.xaml.cs index 6f370b92..39a9b173 100644 --- a/src/BiliLite.UWP/App.xaml.cs +++ b/src/BiliLite.UWP/App.xaml.cs @@ -206,6 +206,7 @@ private async void InitBili() App.Current.Resources["ImageCornerRadius"] = new CornerRadius(SettingService.GetValue(SettingConstants.UI.IMAGE_CORNER_RADIUS, 0)); await AppHelper.SetRegions(); await InitDb(); + try { var downloadService = ServiceProvider.GetRequiredService(); @@ -217,6 +218,9 @@ private async void InitBili() logger.Error("初始化加载下载视频错误", ex); } VideoPlayHistoryHelper.LoadABPlayHistories(true); + + var pluginService = ServiceProvider.GetRequiredService(); + await pluginService.Start(); } private async Task InitDb() diff --git a/src/BiliLite.UWP/BiliLite.UWP.csproj b/src/BiliLite.UWP/BiliLite.UWP.csproj index 2b74db93..3e49bb43 100644 --- a/src/BiliLite.UWP/BiliLite.UWP.csproj +++ b/src/BiliLite.UWP/BiliLite.UWP.csproj @@ -268,9 +268,11 @@ + + @@ -278,6 +280,7 @@ + @@ -1549,6 +1552,9 @@ 0.17.0 + + 0.15.2 + 1.26.0 @@ -1575,6 +1581,7 @@ + diff --git a/src/BiliLite.UWP/Controls/PlayerControl.xaml.cs b/src/BiliLite.UWP/Controls/PlayerControl.xaml.cs index a1f2a02c..c47804c1 100644 --- a/src/BiliLite.UWP/Controls/PlayerControl.xaml.cs +++ b/src/BiliLite.UWP/Controls/PlayerControl.xaml.cs @@ -845,6 +845,7 @@ private async void DanmuTimer_Tick(object sender, object e) private void PositionTimer_Tick(object sender, object e) { + PluginCenter.BroadcastPosition(this, Player.Position); if (!m_autoSkipOpEdFlag) return; if (CurrentPlayItem == null) return; if (CurrentPlayItem.EpisodeSkip == null) return; @@ -1834,7 +1835,7 @@ double Brightness { _brightness = value; BrightnessShield.Opacity = value; - if(!lockBrightness) + if (!lockBrightness) SettingService.SetValue(SettingConstants.Player.PLAYER_BRIGHTNESS, _brightness); } } diff --git a/src/BiliLite.UWP/Controls/Settings/DevSettingsControl.xaml b/src/BiliLite.UWP/Controls/Settings/DevSettingsControl.xaml index ed02549f..2d8bf976 100644 --- a/src/BiliLite.UWP/Controls/Settings/DevSettingsControl.xaml +++ b/src/BiliLite.UWP/Controls/Settings/DevSettingsControl.xaml @@ -9,6 +9,7 @@ xmlns:controls1="using:Microsoft.UI.Xaml.Controls" xmlns:common="using:BiliLite.Models.Common" xmlns:font="using:FontAwesome5" + xmlns:plugins="using:BiliLite.ViewModels.Plugins" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> @@ -66,6 +67,12 @@ + + + + + + @@ -133,5 +140,45 @@ DisplayMemberPath="Name"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/BiliLite.UWP/Controls/Settings/DevSettingsControl.xaml.cs b/src/BiliLite.UWP/Controls/Settings/DevSettingsControl.xaml.cs index 9a139050..c500a79e 100644 --- a/src/BiliLite.UWP/Controls/Settings/DevSettingsControl.xaml.cs +++ b/src/BiliLite.UWP/Controls/Settings/DevSettingsControl.xaml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.ObjectModel; using System.Threading.Tasks; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -7,6 +8,13 @@ using BiliLite.Services; using Microsoft.Extensions.DependencyInjection; using Windows.ApplicationModel.Core; +using BiliLite.ViewModels.Settings; +using Windows.Storage.Pickers; +using Windows.Storage; +using BiliLite.ViewModels.Plugins; +using Microsoft.Toolkit.Uwp.Helpers; +using Newtonsoft.Json; +using IMapper = AutoMapper.IMapper; //https://go.microsoft.com/fwlink/?LinkId=234236 上介绍了“用户控件”项模板 @@ -15,9 +23,17 @@ namespace BiliLite.Controls.Settings public sealed partial class DevSettingsControl : UserControl { private static readonly ILogger _logger = GlobalLogger.FromCurrentType(); + private readonly DevSettingsControlViewModel m_viewModel; + private readonly PluginService m_pluginService; + private readonly IMapper m_mapper; public DevSettingsControl() { + m_viewModel = App.ServiceProvider.GetRequiredService(); + m_mapper = App.ServiceProvider.GetRequiredService(); + m_pluginService = App.ServiceProvider.GetRequiredService(); + m_viewModel.Plugins = + m_mapper.Map>(m_pluginService.GetPlugins()); this.InitializeComponent(); LoadDev(); } @@ -217,5 +233,30 @@ private async void BtnMigrateDb_OnClick(object sender, RoutedEventArgs e) var migrateService = App.ServiceProvider.GetRequiredService(); await migrateService.ExcuteAllMigrationScripts(); } + + private async void BtnSettingPlugin_OnClick(object sender, RoutedEventArgs e) + { + await PluginsDialog.ShowAsync(); + } + + private async void BtnImportPluginInfo_OnClick(object sender, RoutedEventArgs e) + { + var filePicker = new FileOpenPicker(); + filePicker.FileTypeFilter.Add(".json"); + var file = await filePicker.PickSingleFileAsync(); + if (file == null) return; + using var openFile = await file.OpenAsync(FileAccessMode.Read); + var text = await openFile.ReadTextAsync(); + var plugin = JsonConvert.DeserializeObject(text); + await m_pluginService.AddPlugin(plugin); + m_viewModel.AddPlugin(m_mapper.Map(plugin)); + } + + private async void BtnDeletePlugin_OnClick(object sender, RoutedEventArgs e) + { + if (!(sender is Button { DataContext: WebSocketPluginViewModel plugin })) return; + await m_pluginService.RemovePlugin(plugin.Name); + m_viewModel.RemovePlugin(plugin); + } } } diff --git a/src/BiliLite.UWP/Extensions/MapperExtensions.cs b/src/BiliLite.UWP/Extensions/MapperExtensions.cs index 818faf46..8c7df82f 100644 --- a/src/BiliLite.UWP/Extensions/MapperExtensions.cs +++ b/src/BiliLite.UWP/Extensions/MapperExtensions.cs @@ -28,6 +28,7 @@ using BiliLite.ViewModels.Comment; using BiliLite.ViewModels.Download; using BiliLite.ViewModels.Home; +using BiliLite.ViewModels.Plugins; using BiliLite.ViewModels.Season; using BiliLite.ViewModels.Settings; using BiliLite.ViewModels.User; @@ -46,6 +47,8 @@ public static IServiceCollection AddMapper(this IServiceCollection services) { var mapper = new Mapper(new MapperConfiguration(expression => { + expression.CreateMap() + .ReverseMap(); expression.CreateMap() .ReverseMap(); expression.CreateMap() diff --git a/src/BiliLite.UWP/Extensions/ViewModelExtensions.cs b/src/BiliLite.UWP/Extensions/ViewModelExtensions.cs index 0af4875f..8cd58aa5 100644 --- a/src/BiliLite.UWP/Extensions/ViewModelExtensions.cs +++ b/src/BiliLite.UWP/Extensions/ViewModelExtensions.cs @@ -51,6 +51,7 @@ public static IServiceCollection AddViewModels(this IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); return services; } diff --git a/src/BiliLite.UWP/Models/Common/SettingConstants.cs b/src/BiliLite.UWP/Models/Common/SettingConstants.cs index e8390fa7..18e3cf0b 100644 --- a/src/BiliLite.UWP/Models/Common/SettingConstants.cs +++ b/src/BiliLite.UWP/Models/Common/SettingConstants.cs @@ -1063,6 +1063,8 @@ public class Other public const string DEFAULT_REQUEST_BUILD = "75900200"; public const string SQL_DB_VERSION = "SqlDbVersion"; + + public const string PLUGIN_LIST = "PluginList"; } } } diff --git a/src/BiliLite.UWP/Pages/BasePage.cs b/src/BiliLite.UWP/Pages/BasePage.cs index 7d35cab3..75f91e19 100644 --- a/src/BiliLite.UWP/Pages/BasePage.cs +++ b/src/BiliLite.UWP/Pages/BasePage.cs @@ -86,6 +86,11 @@ public void StopHighRateSpeedPlay() Player.StopHighRateSpeedPlay(); } + public void SetPosition(double position) + { + Player.SetPosition(position); + } + public void PositionBack() { Player.PositionBack(); diff --git a/src/BiliLite.UWP/Services/PluginService.cs b/src/BiliLite.UWP/Services/PluginService.cs new file mode 100644 index 00000000..762ee7a2 --- /dev/null +++ b/src/BiliLite.UWP/Services/PluginService.cs @@ -0,0 +1,337 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Flurl.Http; +using Windows.System; +using WebSocket4Net; +using System.Timers; +using Windows.UI.Core; +using Windows.UI.Xaml.Controls; +using BiliLite.Extensions; +using BiliLite.Models.Common; +using BiliLite.Pages; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BiliLite.Services +{ + public class PluginService + { + private readonly SettingSqlService m_settingSqlService; + private List m_plugins; + private static readonly ILogger _logger = GlobalLogger.FromCurrentType(); + + public PluginService(SettingSqlService settingSqlService) + { + m_settingSqlService = settingSqlService; + m_plugins = m_settingSqlService.GetValue(SettingConstants.Other.PLUGIN_LIST, new List()); + } + + public async Task Start() + { + foreach (var plugin in m_plugins) + { + await plugin.Start(); + } + } + + public async Task Stop() + { + foreach (var plugin in m_plugins) + { + await plugin.Stop(); + } + } + + public List GetPlugins() + { + return m_plugins; + } + + public async Task AddPlugin(WebSocketPlugin plugin) + { + var oldPlugin = m_plugins.FirstOrDefault(x => x.Name == plugin.Name); + if (oldPlugin != null) + { + await oldPlugin.Stop(); + + oldPlugin.CheckUrl = plugin.CheckUrl; + oldPlugin.WakeProto = plugin.WakeProto; + oldPlugin.WebSocketUrl = plugin.WebSocketUrl; + + try + { + await oldPlugin.Start(); + } + catch (Exception ex) + { + _logger.Error("更新插件失败", ex); + throw ex; + } + + m_settingSqlService.SetValue(SettingConstants.Other.PLUGIN_LIST, m_plugins); + return; + } + try + { + await plugin.Start(); + } + catch (Exception ex) + { + _logger.Error("添加插件失败", ex); + throw ex; + } + + m_plugins.Add(plugin); + m_settingSqlService.SetValue(SettingConstants.Other.PLUGIN_LIST, m_plugins); + } + + public async Task RemovePlugin(string name) + { + var plugin = m_plugins.FirstOrDefault(x => x.Name == name); + if (plugin == null) return; + await plugin.Stop(); + m_plugins.Remove(plugin); + } + } + + public class WebSocketPlugin + { + private WebSocket m_webSocket; + private bool m_autoReconnect = true; + private int m_retryCount = 0; + private Timer m_clearRetryTimer; + + public WebSocketPlugin() + { + m_clearRetryTimer = new Timer(5000); + m_clearRetryTimer.AutoReset = true; + m_clearRetryTimer.Elapsed += ClearRetryTimer_Elapsed; + } + + /// + /// 插件的唤醒协议, 例如 bilibili:// + /// + public string WakeProto { get; set; } + + /// + /// 连接插件的WebSocket地址 + /// + public string WebSocketUrl { get; set; } + + /// + /// 检查插件是否已启动的Http地址 + /// + public string CheckUrl { get; set; } + + /// + /// 插件名称 + /// + public string Name { get; set; } + + public async Task Start() + { + if (!await CheckIsEnable()) + { + await WakePlugin(); + } + await Connect(); + PluginCenter.BroadcastEvent += OnBroadcastEvent; + } + + public async Task Stop() + { + PluginCenter.BroadcastEvent -= OnBroadcastEvent; + await DisConnect(); + } + + private void OnBroadcastEvent(object sender,object msg) + { + // 将msg序列化为json字符串,通过ws发送出去 + var json = JsonConvert.SerializeObject(msg); + if (m_webSocket.State == WebSocketState.Open) + { + try + { + m_webSocket.Send(json); + } + catch (Exception ex) + { + // 处理发送消息时可能出现的异常 + Debug.WriteLine($"Error sending message: {ex.Message}"); + } + } + else + { + // WebSocket连接不是打开状态,可能需要重连或记录错误 + Debug.WriteLine("WebSocket is not in the Open state."); + } + } + + private async Task Connect() + { + m_webSocket = new WebSocket(WebSocketUrl); + m_webSocket.Opened += OnWebSocketOpened; + m_webSocket.Closed += OnWebSocketClosed; + m_webSocket.MessageReceived += OnWebSocketMessageReceived; + m_webSocket.Error += OnWebSocketError; + await OpenConnect(); + } + + private async Task OpenConnect() + { + try + { + await m_webSocket.OpenAsync(); + } + catch (Exception ex) + { + await RetryConnect(); + } + } + + private async Task DisConnect() + { + if (m_webSocket != null && m_webSocket.State == WebSocketState.Open) + { + m_webSocket.Opened -= OnWebSocketOpened; + m_webSocket.Closed -= OnWebSocketClosed; + m_webSocket.MessageReceived -= OnWebSocketMessageReceived; + m_webSocket.Error -= OnWebSocketError; + m_autoReconnect = false; + await m_webSocket.CloseAsync(); + m_webSocket = null; + } + } + + private void ClearRetryTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + { + if (m_webSocket.State == WebSocketState.Open) + { + m_clearRetryTimer.Stop(); + m_retryCount = 0; + } + } + + private async Task RetryConnect() + { + var retryTime = 2000 * (m_retryCount); + + m_retryCount++; + m_clearRetryTimer.Start(); + + await Task.Delay(retryTime); + + await OpenConnect(); + } + + private void OnWebSocketOpened(object sender, EventArgs e) + { + // WebSocket 连接已打开 + } + + private async void OnWebSocketClosed(object sender, EventArgs e) + { + if (m_autoReconnect) + { + await RetryConnect(); + } + } + + private void OnWebSocketMessageReceived(object sender, MessageReceivedEventArgs e) + { + // 处理接收到的消息 + var msg = JsonConvert.DeserializeObject(e.Message); + + // TODO: 优化代码 + if (msg.Type == "action") + { + var mainPage = App.ServiceProvider.GetRequiredService(); + var mainPageObj = mainPage as Page; + mainPageObj.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + if (msg.Action == "ShowMessageToast") + { + // 解析参数 + var message = msg.Params["message"]?.ToString(); + var seconds = msg.Params["seconds"]?.ToObject() ?? 2; // 默认值为2秒 + + // 调用Notify.ShowMessageToast方法 + Notify.ShowMessageToast(message, seconds); + } + else if (msg.Action == "ExecuteAction") + { + var name = msg.Params["name"]?.ToString(); + var shortcutKeyService = App.ServiceProvider.GetRequiredService(); + shortcutKeyService.ExecuteAction(name); + } + else if (msg.Action == "SetPosition") + { + var position = msg.Params["position"]?.ToObject() ?? 0; + if (!(mainPage.CurrentPage is PlayPage page)) return; + page.SetPosition(position); + } + }); + } + } + + private void OnWebSocketError(object sender, SuperSocket.ClientEngine.ErrorEventArgs e) + { + // 处理 WebSocket 错误 + Debug.WriteLine($"WebSocket error: {e.Exception.Message}"); + } + + private async Task WakePlugin() + { + var success = await Launcher.LaunchUriAsync(new Uri(WakeProto)); + if (!success) + { + // 处理唤醒失败的情况 + throw new Exception("Failed to wake the plugin."); + } + // 等待一段时间让插件启动 + await Task.Delay(5000); + } + + private async Task CheckIsEnable() + { + try + { + var response = await $"{CheckUrl}".GetAsync(); + // 假设如果插件启动了,这个请求会返回200 OK + return response.StatusCode == 200; + } + catch (Exception) + { + // 如果请求失败,可能意味着插件没有启动 + return false; + } + } + } + + public static class PluginCenter + { + public static event EventHandler BroadcastEvent; + + public static void BroadcastPosition(object sender, double position) + { + BroadcastEvent?.Invoke(sender, new + { + @event = "PositionChanged", + position, + }); + } + } + + public class PluginMessage + { + public string Type { get; set; } + + public string Action { get; set; } + + public JObject Params { get; set; } + } +} diff --git a/src/BiliLite.UWP/Services/ShortcutKeyService.cs b/src/BiliLite.UWP/Services/ShortcutKeyService.cs index 3d047835..7584419b 100644 --- a/src/BiliLite.UWP/Services/ShortcutKeyService.cs +++ b/src/BiliLite.UWP/Services/ShortcutKeyService.cs @@ -193,6 +193,12 @@ public void HandleKeyUp(VirtualKey virtualKey) } } + public void ExecuteAction(string name) + { + var function = DefaultShortcuts.GetDefaultShortcutFunctions().FirstOrDefault(x => x.TypeName==name); + function?.Action(m_mainPage.CurrentPage); + } + public void SetDefault() { m_shortcutKeys = DefaultShortcuts.GetDefaultShortcutFunctions(); diff --git a/src/BiliLite.UWP/Startup.cs b/src/BiliLite.UWP/Startup.cs index b574436a..ce163e64 100644 --- a/src/BiliLite.UWP/Startup.cs +++ b/src/BiliLite.UWP/Startup.cs @@ -11,6 +11,7 @@ public void ConfigureServices(HostBuilderContext context, IServiceCollection ser { services.AddDbContext(); services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); services.AddMapper(); diff --git a/src/BiliLite.UWP/ViewModels/Plugins/WebSocketPluginViewModel.cs b/src/BiliLite.UWP/ViewModels/Plugins/WebSocketPluginViewModel.cs new file mode 100644 index 00000000..32c7d223 --- /dev/null +++ b/src/BiliLite.UWP/ViewModels/Plugins/WebSocketPluginViewModel.cs @@ -0,0 +1,27 @@ +using BiliLite.ViewModels.Common; + +namespace BiliLite.ViewModels.Plugins +{ + public class WebSocketPluginViewModel : BaseViewModel + { + /// + /// 插件的唤醒协议, 例如 bilibili:// + /// + public string WakeProto { get; set; } + + /// + /// 连接插件的WebSocket地址 + /// + public string WebSocketUrl { get; set; } + + /// + /// 检查插件是否已启动的Http地址 + /// + public string CheckUrl { get; set; } + + /// + /// 插件名称 + /// + public string Name { get; set; } + } +} diff --git a/src/BiliLite.UWP/ViewModels/Settings/DevSettingsControlViewModel.cs b/src/BiliLite.UWP/ViewModels/Settings/DevSettingsControlViewModel.cs new file mode 100644 index 00000000..7d4f9037 --- /dev/null +++ b/src/BiliLite.UWP/ViewModels/Settings/DevSettingsControlViewModel.cs @@ -0,0 +1,39 @@ +using System.Collections.ObjectModel; +using System.Linq; +using BiliLite.ViewModels.Common; +using BiliLite.ViewModels.Plugins; +using PropertyChanged; + +namespace BiliLite.ViewModels.Settings +{ + public class DevSettingsControlViewModel : BaseViewModel + { + public ObservableCollection Plugins { get; set; } = + new ObservableCollection(); + + [DependsOn(nameof(Plugins))] + public bool ShowPluginList => Plugins.Any (); + + public void AddPlugin(WebSocketPluginViewModel plugin) + { + var oldPlugin = Plugins.FirstOrDefault(x => x.Name == plugin.Name); + if (oldPlugin != null) + { + oldPlugin.CheckUrl = plugin.CheckUrl; + oldPlugin.WakeProto = plugin.WakeProto; + oldPlugin.WebSocketUrl = plugin.WebSocketUrl; + } + else + { + Plugins.Add(plugin); + Set(nameof(ShowPluginList)); + } + } + + public void RemovePlugin(WebSocketPluginViewModel plugin) + { + Plugins.Remove(plugin); + Set(nameof(ShowPluginList)); + } + } +}