diff --git a/src/App/App.csproj b/src/App/App.csproj index 45b2b1bb..86c23a7e 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -87,6 +87,7 @@ + @@ -281,6 +282,9 @@ Never + + MSBuild:Compile + MSBuild:Compile diff --git a/src/App/Controls/Base/BiliPlayerOverlay/BiliPlayerOverlay.Methods.cs b/src/App/Controls/Base/BiliPlayerOverlay/BiliPlayerOverlay.Methods.cs index 0dd43b2b..fbc83abf 100644 --- a/src/App/Controls/Base/BiliPlayerOverlay/BiliPlayerOverlay.Methods.cs +++ b/src/App/Controls/Base/BiliPlayerOverlay/BiliPlayerOverlay.Methods.cs @@ -134,7 +134,7 @@ private void HideTempMessage() private void HandleTransportAutoHide() { - if (_transportStayTime > 1.2) + if (_transportStayTime > 1.2 && ViewModel.Player != null) { _transportStayTime = 0; var isManual = SettingsToolkit.ReadLocalSetting(SettingNames.IsPlayerControlModeManual, false); @@ -163,6 +163,7 @@ private void HandleCursorAutoHide() && !ViewModel.IsShowMediaTransport && IsPointerStay && !_rootSplitView.IsPaneOpen + && ViewModel.Player != null && ViewModel.Player.Status == PlayerStatus.Playing) { ProtectedCursor.Dispose(); diff --git a/src/App/Controls/Comment/CommentBox.xaml b/src/App/Controls/Comment/CommentBox.xaml index 4287b4b4..741eba6e 100644 --- a/src/App/Controls/Comment/CommentBox.xaml +++ b/src/App/Controls/Comment/CommentBox.xaml @@ -8,11 +8,12 @@ xmlns:ext="using:Bili.Copilot.App.Extensions" xmlns:local="using:Bili.Copilot.App.Controls.Comment" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:modules="using:Bili.Copilot.App.Controls.Modules" mc:Ignorable="d"> @@ -51,34 +52,51 @@ - - + QuerySubmitted="OnReplySubmitted" + Text="{x:Bind Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> + + + + + + + diff --git a/src/App/Controls/Comment/CommentBox.xaml.cs b/src/App/Controls/Comment/CommentBox.xaml.cs index 9dc91950..96d90747 100644 --- a/src/App/Controls/Comment/CommentBox.xaml.cs +++ b/src/App/Controls/Comment/CommentBox.xaml.cs @@ -73,4 +73,22 @@ public ICommand ResetSelectedCommand get => (ICommand)GetValue(ResetSelectedCommandProperty); set => SetValue(ResetSelectedCommandProperty, value); } + + private void OnReplySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) + { + if (string.IsNullOrEmpty(Text)) + { + return; + } + + SendCommand?.Execute(Text); + } + + private void OnItemClick(object sender, string e) + => Text += e; + + private void OnFlyoutClosed(object sender, object e) + { + ReplyBox.Focus(FocusState.Programmatic); + } } diff --git a/src/App/Controls/Modules/EmotePanel.xaml b/src/App/Controls/Modules/EmotePanel.xaml new file mode 100644 index 00000000..093e2c7e --- /dev/null +++ b/src/App/Controls/Modules/EmotePanel.xaml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Controls/Modules/EmotePanel.xaml.cs b/src/App/Controls/Modules/EmotePanel.xaml.cs new file mode 100644 index 00000000..1609d1b9 --- /dev/null +++ b/src/App/Controls/Modules/EmotePanel.xaml.cs @@ -0,0 +1,55 @@ +// Copyright (c) Bili Copilot. All rights reserved. + +using Bili.Copilot.App.Controls.Base; +using Bili.Copilot.ViewModels.Components; +using Bili.Copilot.ViewModels.Items; + +namespace Bili.Copilot.App.Controls.Modules; + +/// +/// 表情模块. +/// +public sealed partial class EmotePanel : EmotePanelBase +{ + /// + /// Initializes a new instance of the class. + /// + public EmotePanel() + { + InitializeComponent(); + ViewModel = EmoteModuleViewModel.Instance; + Loaded += OnLoaded; + } + + /// + /// 点击表情. + /// + public event EventHandler ItemClick; + + private void OnLoaded(object sender, RoutedEventArgs e) + { + ViewModel.InitializeCommand.Execute(default); + } + + private void OnItemClick(object sender, RoutedEventArgs e) + { + var key = (sender as FrameworkElement).Tag as string; + ItemClick?.Invoke(this, key); + } + + private void OnPackageClick(object sender, RoutedEventArgs e) + { + var data = (sender as FrameworkElement).DataContext as EmotePackageViewModel; + if (data != ViewModel.Current) + { + ViewModel.SelectPackageCommand.Execute(data); + } + } +} + +/// +/// 的基类. +/// +public abstract class EmotePanelBase : ReactiveUserControl +{ +} diff --git a/src/App/Resources/zh-Hans/Resources.resw b/src/App/Resources/zh-Hans/Resources.resw index 65e0b4d9..eef0ad07 100644 --- a/src/App/Resources/zh-Hans/Resources.resw +++ b/src/App/Resources/zh-Hans/Resources.resw @@ -2223,4 +2223,13 @@ UGC优先级:分P > 播放列表 > 合集视频 > 关联视频 (如 自动=控制器自行显示/隐藏,手动=用户控制显示/隐藏 + + 表情 + + + 请求表情失败 + + + 无法获取表情包,请稍后再试 + \ No newline at end of file diff --git a/src/App/Resources/zh-Hant/Resources.resw b/src/App/Resources/zh-Hant/Resources.resw index d044dcce..4aa7a74a 100644 --- a/src/App/Resources/zh-Hant/Resources.resw +++ b/src/App/Resources/zh-Hant/Resources.resw @@ -2217,4 +2217,13 @@ UGC優先級:分P > 播放列表 > 合集視頻 > 關聯視頻 (如 自動=控制器自行顯示/隱藏,手動=使用者控制顯示/隱藏 + + 表情 + + + 請求表情失敗 + + + 無法獲取表情包,請稍後再試 + \ No newline at end of file diff --git a/src/Libs/Libs.Adapter/CommunityAdapter.cs b/src/Libs/Libs.Adapter/CommunityAdapter.cs index 88bdc493..57527a06 100644 --- a/src/Libs/Libs.Adapter/CommunityAdapter.cs +++ b/src/Libs/Libs.Adapter/CommunityAdapter.cs @@ -5,8 +5,10 @@ using System.Linq; using Bili.Copilot.Libs.Toolkit; using Bili.Copilot.Models.BiliBili; +using Bili.Copilot.Models.BiliBili.Others; using Bili.Copilot.Models.Constants.App; using Bili.Copilot.Models.Constants.Community; +using Bili.Copilot.Models.Data.Appearance; using Bili.Copilot.Models.Data.Community; using Bilibili.App.Archive.V1; using Bilibili.App.Card.V1; @@ -763,4 +765,30 @@ public static TripleInformation ConvertToTripleInformation(TripleResult result, /// . public static EpisodeInteractionInformation ConvertToEpisodeInteractionInformation(EpisodeInteraction interaction) => new(interaction.IsLike == 1, interaction.CoinNumber > 0, interaction.IsFavorite == 1); + + /// + /// 将 转换成 . + /// + /// 表情包. + /// . + public static EmotePackage ConvertToEmotePackage(BiliEmotePackage package) + { + var p = new EmotePackage(); + p.Name = package.Text; + p.Icon = ImageAdapter.ConvertToImage(package.Url); + p.Images = new List(); + foreach (var item in package.Emotes) + { + var e = new Emote(); + e.Key = item.Text; + if (item.Url.StartsWith("http")) + { + e.Image = ImageAdapter.ConvertToImage(item.Url); + } + + p.Images.Add(e); + } + + return p; + } } diff --git a/src/Libs/Libs.Provider/CommunityProvider/CommunityProvider.cs b/src/Libs/Libs.Provider/CommunityProvider/CommunityProvider.cs index 4c3464d5..ea34a65b 100644 --- a/src/Libs/Libs.Provider/CommunityProvider/CommunityProvider.cs +++ b/src/Libs/Libs.Provider/CommunityProvider/CommunityProvider.cs @@ -2,12 +2,15 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Bili.Copilot.Libs.Adapter; using Bili.Copilot.Models.BiliBili; +using Bili.Copilot.Models.BiliBili.Others; using Bili.Copilot.Models.Constants.Authorize; using Bili.Copilot.Models.Constants.Bili; +using Bili.Copilot.Models.Data.Appearance; using Bili.Copilot.Models.Data.Community; using Bili.Copilot.Models.Data.Dynamic; using Bilibili.App.Dynamic.V2; @@ -157,6 +160,22 @@ public static async Task MarkUserDynamicReadAsync(string userId, string offset, response.EnsureSuccessStatusCode(); } + /// + /// 获取表情包列表. + /// + /// 表情包列表. + public static async Task> GetEmotePackagesAsync() + { + var queryParameters = new Dictionary + { + { "business", "reply" }, + }; + var request = await HttpProvider.GetRequestMessageAsync(HttpMethod.Get, Community.Emotes, queryParameters); + var response = await HttpProvider.Instance.SendAsync(request); + var data = await HttpProvider.ParseAsync>(response); + return data.Data.Packages.Select(CommunityAdapter.ConvertToEmotePackage).ToList(); + } + /// /// 获取单层评论详情列表. /// diff --git a/src/Models/Models.App/Constants/ApiConstants.cs b/src/Models/Models.App/Constants/ApiConstants.cs index d6f4f02b..01145410 100644 --- a/src/Models/Models.App/Constants/ApiConstants.cs +++ b/src/Models/Models.App/Constants/ApiConstants.cs @@ -649,6 +649,11 @@ public static class Community /// 点赞/取消点赞动态. /// public const string LikeDynamic = _grpcBase + "/bilibili.main.dynamic.feed.v1.Feed/DynamicThumb"; + + /// + /// 表情包. + /// + public const string Emotes = _apiBase + "/x/emote/user/panel/web"; } } #pragma warning restore SA1600 // Elements should be documented diff --git a/src/Models/Models.BiliBili/Others/EmoteResponse.cs b/src/Models/Models.BiliBili/Others/EmoteResponse.cs new file mode 100644 index 00000000..d3a81f3d --- /dev/null +++ b/src/Models/Models.BiliBili/Others/EmoteResponse.cs @@ -0,0 +1,76 @@ +// Copyright (c) Bili Copilot. All rights reserved. + +namespace Bili.Copilot.Models.BiliBili.Others; + +/// +/// 表情包响应. +/// +public class EmoteResponse +{ + /// + /// 表情包集合. + /// + [JsonPropertyName("packages")] + public List Packages { get; set; } +} + +/// +/// 表情包. +/// +public class BiliEmotePackage +{ + /// + /// 标识符. + /// + [JsonPropertyName("id")] + public int Id { get; set; } + + /// + /// 对应文本. + /// + [JsonPropertyName("text")] + public string Text { get; set; } + + /// + /// 图标地址. + /// + [JsonPropertyName("url")] + public string Url { get; set; } + + /// + /// 表情集合. + /// + [JsonPropertyName("emote")] + public List Emotes { get; set; } +} + +/// +/// 表情. +/// +public class BiliEmote +{ + /// + /// 标识符. + /// + [JsonPropertyName("id")] + public int Id { get; set; } + + /// + /// 表情包标识符. + /// + [JsonPropertyName("package_id")] + public int PackageId { get; set; } + + /// + /// 文本. + /// + [JsonPropertyName("text")] + public string Text { get; set; } + + /// + /// 图标地址. + /// + [JsonPropertyName("url")] + public string Url { get; set; } +} + diff --git a/src/Models/Models.Data/Appearance/Emote.cs b/src/Models/Models.Data/Appearance/Emote.cs new file mode 100644 index 00000000..3eae3af8 --- /dev/null +++ b/src/Models/Models.Data/Appearance/Emote.cs @@ -0,0 +1,25 @@ +// Copyright (c) Bili Copilot. All rights reserved. + +namespace Bili.Copilot.Models.Data.Appearance; + +/// +/// 表情. +/// +public sealed class Emote +{ + /// + /// 替代文本. + /// + public string Key { get; set; } + + /// + /// 图片信息. + /// + public Image Image { get; set; } + + /// + public override bool Equals(object obj) => obj is Emote emote && Key == emote.Key; + + /// + public override int GetHashCode() => HashCode.Combine(Key); +} diff --git a/src/Models/Models.Data/Appearance/EmotePackage.cs b/src/Models/Models.Data/Appearance/EmotePackage.cs new file mode 100644 index 00000000..feb1ed2f --- /dev/null +++ b/src/Models/Models.Data/Appearance/EmotePackage.cs @@ -0,0 +1,24 @@ +// Copyright (c) Bili Copilot. All rights reserved. + +namespace Bili.Copilot.Models.Data.Appearance; + +/// +/// 表情包. +/// +public sealed class EmotePackage +{ + /// + /// 名称. + /// + public string Name { get; set; } + + /// + /// 图标. + /// + public Image Icon { get; set; } + + /// + /// 表情列表. + /// + public List Images { get; set; } +} diff --git a/src/ViewModels/Components/EmoteModuleViewModel.cs b/src/ViewModels/Components/EmoteModuleViewModel.cs new file mode 100644 index 00000000..506ba0ef --- /dev/null +++ b/src/ViewModels/Components/EmoteModuleViewModel.cs @@ -0,0 +1,79 @@ +// Copyright (c) Bili Copilot. All rights reserved. + +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using Bili.Copilot.Libs.Provider; +using Bili.Copilot.ViewModels.Items; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace Bili.Copilot.ViewModels.Components; + +/// +/// 表情模块视图模型. +/// +public sealed partial class EmoteModuleViewModel : ViewModelBase +{ + [ObservableProperty] + private bool _isLoading; + + [ObservableProperty] + private EmotePackageViewModel _current; + + [ObservableProperty] + private bool _isError; + + private EmoteModuleViewModel() + { + Packages = new ObservableCollection(); + + AttachIsRunningToAsyncCommand(p => IsLoading = p, InitializeCommand); + AttachExceptionHandlerToAsyncCommand( + ex => + { + LogException(ex); + IsError = true; + }, + InitializeCommand); + } + + /// + /// 实例. + /// + public static EmoteModuleViewModel Instance { get; } = new EmoteModuleViewModel(); + + /// + /// 表情包集合. + /// + public ObservableCollection Packages { get; } + + [RelayCommand] + private async Task InitializeAsync() + { + if (Packages.Count > 0) + { + return; + } + + IsError = false; + var packages = await CommunityProvider.GetEmotePackagesAsync(); + foreach (var item in packages) + { + Packages.Add(new EmotePackageViewModel(item)); + } + + SelectPackageCommand.Execute(Packages.First()); + } + + [RelayCommand] + private void SelectPackage(EmotePackageViewModel vm) + { + foreach (var item in Packages) + { + item.IsSelected = vm.Equals(item); + } + + Current = vm; + } +} diff --git a/src/ViewModels/Items/EmotePackageViewModel.cs b/src/ViewModels/Items/EmotePackageViewModel.cs new file mode 100644 index 00000000..dacb2470 --- /dev/null +++ b/src/ViewModels/Items/EmotePackageViewModel.cs @@ -0,0 +1,19 @@ +// Copyright (c) Bili Copilot. All rights reserved. + +using Bili.Copilot.Models.Data.Appearance; + +namespace Bili.Copilot.ViewModels.Items; + +/// +/// 表情包视图模型. +/// +public sealed partial class EmotePackageViewModel : SelectableViewModel +{ + /// + /// Initializes a new instance of the class. + /// + public EmotePackageViewModel(EmotePackage data) + : base(data) + { + } +} diff --git a/src/ViewModels/Views/MessageDetailViewModel/MessageDetailViewModel.cs b/src/ViewModels/Views/MessageDetailViewModel/MessageDetailViewModel.cs index c1aa492e..6a957460 100644 --- a/src/ViewModels/Views/MessageDetailViewModel/MessageDetailViewModel.cs +++ b/src/ViewModels/Views/MessageDetailViewModel/MessageDetailViewModel.cs @@ -33,7 +33,6 @@ private MessageDetailViewModel() }; InitializeMessageCount(); - SelectTypeCommand.Execute(MessageTypes.FirstOrDefault(p => p.Count > 0) ?? MessageTypes.First()); AccountViewModel.Instance.PropertyChanged += OnAccountViewModelPropertyChanged; } @@ -54,15 +53,14 @@ protected override void BeforeReload() /// protected override async Task GetDataAsync() { - if (_caches.Count == 0) + if (_isEnd) { - CurrentType = MessageTypes.First(); - CurrentType.IsSelected = true; + return; } - if (_isEnd) + if (CurrentType == default) { - return; + SelectTypeCommand.Execute(MessageTypes.First()); } var view = await AccountProvider.Instance.GetMyMessagesAsync(CurrentType.Type);