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);