返回提交历史
Added
HaloPixelToolBox/HaloPixelToolBox.Core/Models/Display/HaloPixelColor.cs
+14
-0
Added
HaloPixelToolBox/HaloPixelToolBox.Core/Models/Lighting/AmbientLightBrightness.cs
+8
-0
Added
HaloPixelToolBox/HaloPixelToolBox.Core/Models/Lighting/AmbientLightEffect.cs
+11
-0
Added
HaloPixelToolBox/HaloPixelToolBox.Core/Models/Lighting/AmbientLightOptions.cs
+16
-0
Added
HaloPixelToolBox/HaloPixelToolBox.Core/Models/Scenes/PixelSceneUploadProgress.cs
+12
-0
Modified
HaloPixelToolBox/HaloPixelToolBox.Core/Utilities/HaloPixelDevice.cs
+24
-1
Modified
HaloPixelToolBox/HaloPixelToolBox.Core/Utilities/HidPacketBuilder.cs
+138
-22
Modified
HaloPixelToolBox/HaloPixelToolBox/Profiles/CrossVersionProfiles/CloudMusicLyricsProfile.cs
+13
-1
Modified
HaloPixelToolBox/HaloPixelToolBox/Profiles/CrossVersionProfiles/SpotifyLyricsProfile.cs
+12
-0
Modified
HaloPixelToolBox/HaloPixelToolBox/Utilities/SpotifyLyricsReader.cs
+60
-0
Modified
HaloPixelToolBox/HaloPixelToolBox/ViewModels/CloudMusicLyricsToolPageViewModel.cs
+199
-15
Modified
HaloPixelToolBox/HaloPixelToolBox/ViewModels/SpotifyLyricsToolPageViewModel.cs
+121
-14
Modified
HaloPixelToolBox/HaloPixelToolBox/Views/CloudMusicLyricsToolPage.xaml
+6
-0
Modified
HaloPixelToolBox/HaloPixelToolBox/Views/CloudMusicLyricsToolPage.xaml.cs
+6
-4
Modified
HaloPixelToolBox/HaloPixelToolBox/Views/SpotifyLyricsToolPage.xaml
+6
-0
Modified
HaloPixelToolBox/HaloPixelToolBox/Views/SpotifyLyricsToolPage.xaml.cs
+2
-0
XFEstudio/HaloPixelToolBox
add ambient RGB sync with album art
f8f953b
代码差异
16 个文件
+648
-57
@@ -0,0 +1,14 @@
1
namespace HaloPixelToolBox.Core.Models.Display;
2
3
/// <summary>
4
/// 字幕颜色配置。当前设备文本协议未公开颜色字段,先作为上层样式参数保留。
5
/// </summary>
6
public record HaloPixelColor(byte Red, byte Green, byte Blue)
7
{
8
public static HaloPixelColor White { get; } = new(255, 255, 255);
9
public static HaloPixelColor RedColor { get; } = new(255, 64, 64);
10
public static HaloPixelColor GreenColor { get; } = new(64, 255, 128);
11
public static HaloPixelColor BlueColor { get; } = new(64, 160, 255);
12
13
public string ToHex() => $"#{Red:X2}{Green:X2}{Blue:X2}";
14
}
@@ -0,0 +1,8 @@
1
namespace HaloPixelToolBox.Core.Models.Lighting;
2
3
public enum AmbientLightBrightness
4
{
5
Low = 1,
6
Medium = 2,
7
High = 3
8
}
@@ -0,0 +1,11 @@
1
namespace HaloPixelToolBox.Core.Models.Lighting;
2
3
public enum AmbientLightEffect
4
{
5
Breathing = 1,
6
ColorTide = 2,
7
Static = 3,
8
Ripple = 4,
9
Flow = 5,
10
Dynamic = 6
11
}
@@ -0,0 +1,16 @@
1
using HaloPixelToolBox.Core.Models.Display;
2
3
namespace HaloPixelToolBox.Core.Models.Lighting;
4
5
public sealed class AmbientLightOptions
6
{
7
public bool IsEnabled { get; set; } = true;
8
9
public AmbientLightEffect Effect { get; set; } = AmbientLightEffect.Static;
10
11
public HaloPixelColor Color { get; set; } = HaloPixelColor.White;
12
13
public AmbientLightBrightness Brightness { get; set; } = AmbientLightBrightness.High;
14
15
public byte Speed { get; set; } = 10;
16
}
@@ -0,0 +1,12 @@
1
namespace HaloPixelToolBox.Core.Models.Scenes;
2
3
/// <summary>
4
/// 像素屏场景资源上传进度。
5
/// 进度按已发送的 .bin 资源字节数计算,后续如果改为更底层的流式上传,可继续扩展传输阶段字段。
6
/// </summary>
7
public sealed record PixelSceneUploadProgress(long SentBytes, long TotalBytes, string Stage)
8
{
9
public double Ratio => TotalBytes <= 0 ? 0 : Math.Clamp(SentBytes / (double)TotalBytes, 0, 1);
10
11
public double Percent => Ratio * 100;
12
}
@@ -1,4 +1,6 @@
1
using HaloPixelToolBox.Core.Models;
1
using HaloPixelToolBox.Core.Models;
2
using HaloPixelToolBox.Core.Models.Lighting;
3
using HaloPixelToolBox.Core.Models.Display;
2
4
using HidSharp;
3
5
using XFEExtension.NetCore.StringExtension;
4
6
@@ -97,4 +99,25 @@ public partial class HaloPixelDevice
97
99
catch { }
98
100
}
99
101
}
102
103
public void SetAmbientLight(AmbientLightOptions options)
104
{
105
using var stream = CurrentDevice?.Open();
106
stream?.Write(HidPacketBuilder.BuildAmbientLight(options));
107
stream?.Close();
108
}
109
110
public void SetAmbientLightEnabled(bool enabled)
111
{
112
using var stream = CurrentDevice?.Open();
113
stream?.Write(HidPacketBuilder.BuildAmbientLightPower(enabled));
114
stream?.Close();
115
}
116
117
public void SetPixelScreenColor(HaloPixelColor color)
118
{
119
using var stream = CurrentDevice?.Open();
120
stream?.Write(HidPacketBuilder.BuildPixelScreenColor(color));
121
stream?.Close();
122
}
100
123
}
@@ -1,4 +1,6 @@
1
using HaloPixelToolBox.Core.Models;
1
using HaloPixelToolBox.Core.Models;
2
using HaloPixelToolBox.Core.Models.Lighting;
3
using HaloPixelToolBox.Core.Models.Display;
2
4
using System.Text;
3
5
4
6
namespace HaloPixelToolBox.Core.Utilities;
@@ -55,35 +57,75 @@ public class HidPacketBuilder
55
57
return Build(list);
56
58
}
57
59
60
/// <summary>
61
/// 转换布局枚举到对应的 HID 包字节数组
62
public static (byte R, byte G, byte B) CurrentColor { get; set; } = (0xf0, 0xb4, 0xc8);
63
58
64
/// <summary>
59
65
/// 转换布局枚举到对应的 HID 包字节数组
60
66
/// </summary>
61
67
/// <param name="haloPixelTextLayout"></param>
62
68
/// <returns></returns>
63
public static byte[] ConvertLayout(HaloPixelTextLayout haloPixelTextLayout) => haloPixelTextLayout switch
69
public static byte[] ConvertLayout(HaloPixelTextLayout haloPixelTextLayout)
64
70
{
65
HaloPixelTextLayout.Left => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x01, 0xf0, 0xb4, 0xc8, 0x00, 0x02, 0x00, 0x00, 0xff, 0xfc, 0x00],
66
HaloPixelTextLayout.Center => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x01, 0xf0, 0xb4, 0xc8, 0x00, 0x02, 0x00, 0x01, 0xff, 0xfd, 0x00],
67
HaloPixelTextLayout.Right => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x01, 0xf0, 0xb4, 0xc8, 0x00, 0x02, 0x00, 0x02, 0xff, 0xfe, 0x00],
68
HaloPixelTextLayout.Stretch => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x01, 0xf0, 0xb4, 0xc8, 0x00, 0x02, 0x00, 0x03, 0xff, 0xff, 0x00],
69
HaloPixelTextLayout.ScrollLeftToRight => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x01, 0xf0, 0xb4, 0xc8, 0x00, 0x02, 0x01, 0x00, 0xff, 0xfd, 0x00],
70
HaloPixelTextLayout.ScrollRightToLeft => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x01, 0xf0, 0xb4, 0xc8, 0x00, 0x02, 0x01, 0x01, 0xff, 0xfe, 0x00],
71
_ => [],
72
};
71
byte[] payload = haloPixelTextLayout switch
72
{
73
HaloPixelTextLayout.Left => [0x01, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x02, 0x00, 0x00, 0xff],
74
HaloPixelTextLayout.Center => [0x01, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x02, 0x00, 0x01, 0xff],
75
HaloPixelTextLayout.Right => [0x01, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x02, 0x00, 0x02, 0xff],
76
HaloPixelTextLayout.Stretch => [0x01, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x02, 0x00, 0x03, 0xff],
77
HaloPixelTextLayout.ScrollLeftToRight => [0x01, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x02, 0x01, 0x00, 0xff],
78
HaloPixelTextLayout.ScrollRightToLeft => [0x01, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x02, 0x01, 0x01, 0xff],
79
_ => [],
80
};
81
82
if (payload.Length == 0) return [];
83
84
var list = new List<byte> { 0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09 };
85
list.AddRange(payload);
86
87
int acc = 124;
88
foreach (var b in payload)
89
{
90
acc += b + 2;
91
}
92
list.Add((byte)(acc % 256));
93
list.Add(0x00);
73
94
74
public static byte[] ConvertUIModel(HaloPixelUIModel haloPixelUIModel) => haloPixelUIModel switch
95
return Build(list);
96
}
97
98
public static byte[] ConvertUIModel(HaloPixelUIModel haloPixelUIModel)
75
99
{
76
HaloPixelUIModel.Clock => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x00, 0xff, 0xff, 0xfb, 0x00],
77
HaloPixelUIModel.Game => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x01, 0xff, 0xff, 0xfc, 0x00],
78
HaloPixelUIModel.Work => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x02, 0xff, 0xff, 0xfd, 0x00],
79
HaloPixelUIModel.Read => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x03, 0xff, 0xff, 0xfe, 0x00],
80
HaloPixelUIModel.Cats => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x04, 0xff, 0xff, 0xff, 0x00],
81
HaloPixelUIModel.Dogs => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x05, 0xff, 0xff, 0x00, 0x00],
82
HaloPixelUIModel.Memes => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x06, 0xff, 0xff, 0x01, 0x00],
83
HaloPixelUIModel.Cyber => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x07, 0xff, 0xff, 0x02, 0x00],
84
HaloPixelUIModel.Waves => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x08, 0xff, 0xff, 0x03, 0x00],
85
_ => [],
86
};
100
byte[] payload = haloPixelUIModel switch
101
{
102
HaloPixelUIModel.Clock => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x00, 0xff, 0xff],
103
HaloPixelUIModel.Game => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x01, 0xff, 0xff],
104
HaloPixelUIModel.Work => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x02, 0xff, 0xff],
105
HaloPixelUIModel.Read => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x03, 0xff, 0xff],
106
HaloPixelUIModel.Cats => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x04, 0xff, 0xff],
107
HaloPixelUIModel.Dogs => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x05, 0xff, 0xff],
108
HaloPixelUIModel.Memes => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x06, 0xff, 0xff],
109
HaloPixelUIModel.Cyber => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x07, 0xff, 0xff],
110
HaloPixelUIModel.Waves => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x08, 0xff, 0xff],
111
_ => [],
112
};
113
114
if (payload.Length == 0) return [];
115
116
var list = new List<byte> { 0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09 };
117
list.AddRange(payload);
118
119
int acc = 124;
120
foreach (var b in payload)
121
{
122
acc += b + 2;
123
}
124
list.Add((byte)(acc % 256));
125
list.Add(0x00);
126
127
return Build(list);
128
}
87
129
88
130
/// <summary>
89
131
/// 构造 HID 协议包
@@ -122,4 +164,78 @@ public class HidPacketBuilder
122
164
{
123
165
return BitConverter.ToString(packet).Replace("-", "").ToLower();
124
166
}
167
168
/// <summary>
169
/// 构造像素屏主题颜色包。
170
/// 协议来自 LiLyric 的 RGBController.set_pixel_color:2E AA EC EF 00 04 03 + RGB + checksum。
171
/// </summary>
172
public static byte[] BuildPixelScreenColor(HaloPixelColor color)
173
=> BuildEdifierPacket(0xef, [0x03, color.Red, color.Green, color.Blue]);
174
175
/// <summary>
176
/// 构造氛围灯效果包。
177
/// 协议来自 LiLyric 的 RGBController._set_light_color:2E AA EC 6B 00 07 13 + effect + RGB + brightness + speed + checksum。
178
/// </summary>
179
public static byte[] BuildAmbientLight(AmbientLightOptions options)
180
{
181
var speed = Math.Clamp(options.Speed, (byte)1, (byte)10);
182
byte[] payload =
183
[
184
0x13,
185
(byte)options.Effect,
186
options.Color.Red,
187
options.Color.Green,
188
options.Color.Blue,
189
ConvertAmbientBrightness(options.Brightness),
190
speed
191
];
192
193
return BuildEdifierPacket(0x6b, payload);
194
}
195
196
/// <summary>
197
/// 构造氛围灯开关包。
198
/// 协议来自 TempoHub 的 mood_lighting_splice/set_device_light:B 字段 1=开启、0=关闭。
199
/// </summary>
200
public static byte[] BuildAmbientLightPower(bool enabled)
201
{
202
byte mode = enabled ? (byte)0x01 : (byte)0x00;
203
return BuildEdifierPacket(0x6b, [0x00, 0x00, 0x00, 0x00, mode, 0xff, 0xff]);
204
}
205
206
private static byte ConvertAmbientBrightness(AmbientLightBrightness brightness) => brightness switch
207
{
208
AmbientLightBrightness.Low => 0x14,
209
AmbientLightBrightness.Medium => 0x28,
210
AmbientLightBrightness.High => 0x3c,
211
_ => 0x3c
212
};
213
214
/// <summary>
215
/// 构造通用 Edifier HID 包:2E AA EC + 指令号 + payload 长度 + payload + 校验。
216
/// </summary>
217
public static byte[] BuildEdifierPacket(byte commandIndex, IReadOnlyCollection<byte> payload)
218
{
219
var packet = new List<byte>(6 + payload.Count + 1)
220
{
221
0x2e,
222
0xaa,
223
0xec,
224
commandIndex,
225
(byte)((payload.Count >> 8) & 0xff),
226
(byte)(payload.Count & 0xff)
227
};
228
packet.AddRange(payload);
229
packet.Add(CalculateEdifierChecksum(packet));
230
return Build(packet);
231
}
232
233
private static byte CalculateEdifierChecksum(IReadOnlyList<byte> packetWithoutChecksum)
234
{
235
var sum = 0;
236
for (var index = 1; index < packetWithoutChecksum.Count; index++)
237
sum += packetWithoutChecksum[index];
238
239
return (byte)(sum & 0xff);
240
}
125
241
}
@@ -1,4 +1,4 @@
1
using HaloPixelToolBox.Core.Models;
1
using HaloPixelToolBox.Core.Models;
2
2
using XFEExtension.NetCore.AutoConfig;
3
3
using XFEExtension.NetCore.WinUIHelper.Utilities.Helper;
4
4
@@ -42,4 +42,16 @@ public partial class CloudMusicLyricsProfile : XFEProfile
42
42
/// </summary>
43
43
[ProfileProperty]
44
44
private HaloPixelUIModel defaultHaloPixelUIModel = HaloPixelUIModel.Clock;
45
46
/// <summary>
47
/// 是否开启屏幕颜色同步
48
/// </summary>
49
[ProfileProperty]
50
private bool enableScreenColorSync = false;
51
52
/// <summary>
53
/// 是否开启氛围灯颜色同步
54
/// </summary>
55
[ProfileProperty]
56
private bool enableAmbientColorSync = false;
45
57
}
@@ -37,4 +37,16 @@ public partial class SpotifyLyricsProfile : XFEProfile
37
37
/// </summary>
38
38
[ProfileProperty]
39
39
private HaloPixelUIModel defaultHaloPixelUIModel = HaloPixelUIModel.Clock;
40
41
/// <summary>
42
/// 是否开启屏幕颜色同步
43
/// </summary>
44
[ProfileProperty]
45
private bool enableScreenColorSync = false;
46
47
/// <summary>
48
/// 是否开启氛围灯颜色同步
49
/// </summary>
50
[ProfileProperty]
51
private bool enableAmbientColorSync = false;
40
52
}
@@ -119,6 +119,48 @@ public class SpotifyLyricsReader
119
119
private DateTimeOffset _songStartTime = DateTimeOffset.Now;
120
120
private bool _isSmtcSynced = false;
121
121
122
public (byte R, byte G, byte B)? CurrentAlbumColor { get; private set; }
123
124
public static async Task<(byte R, byte G, byte B)?> GetDominantColorAsync(Windows.Storage.Streams.IRandomAccessStreamReference thumbnail)
125
{
126
try
127
{
128
using var stream = await thumbnail.OpenReadAsync();
129
var decoder = await Windows.Graphics.Imaging.BitmapDecoder.CreateAsync(stream);
130
var transform = new Windows.Graphics.Imaging.BitmapTransform
131
{
132
ScaledWidth = 16,
133
ScaledHeight = 16,
134
InterpolationMode = Windows.Graphics.Imaging.BitmapInterpolationMode.Linear
135
};
136
var pixelData = await decoder.GetPixelDataAsync(
137
Windows.Graphics.Imaging.BitmapPixelFormat.Rgba8,
138
Windows.Graphics.Imaging.BitmapAlphaMode.Ignore,
139
transform,
140
Windows.Graphics.Imaging.ExifOrientationMode.IgnoreExifOrientation,
141
Windows.Graphics.Imaging.ColorManagementMode.ColorManageToSRgb
142
);
143
byte[] pixels = pixelData.DetachPixelData();
144
long rSum = 0, gSum = 0, bSum = 0;
145
int pixelCount = pixels.Length / 4;
146
for (int i = 0; i < pixels.Length; i += 4)
147
{
148
rSum += pixels[i];
149
gSum += pixels[i + 1];
150
bSum += pixels[i + 2];
151
}
152
if (pixelCount > 0)
153
{
154
return ((byte)(rSum / pixelCount), (byte)(gSum / pixelCount), (byte)(bSum / pixelCount));
155
}
156
}
157
catch (Exception ex)
158
{
159
Console.WriteLine($"[ERROR] GetDominantColorAsync failed: {ex.Message}\n{ex.StackTrace}");
160
}
161
return null;
162
}
163
122
164
public string CurrentTitle => _currentTitle;
123
165
public string CurrentArtist => _currentArtist;
124
166
@@ -312,6 +354,24 @@ public class SpotifyLyricsReader
312
354
_currentDuration = timeline?.EndTime.TotalSeconds ?? 0;
313
355
_isSmtcSynced = true;
314
356
Console.WriteLine($"SMTC Sync Achieved for: {_currentArtist} - {_currentTitle} (Duration: {_currentDuration}s)");
357
358
if (media.Thumbnail != null)
359
{
360
var color = await GetDominantColorAsync(media.Thumbnail);
361
if (color.HasValue)
362
{
363
CurrentAlbumColor = color.Value;
364
Console.WriteLine($"Extracted album dominant color: RGB({color.Value.R}, {color.Value.G}, {color.Value.B})");
365
}
366
else
367
{
368
Console.WriteLine("GetDominantColorAsync returned null for the thumbnail.");
369
}
370
}
371
else
372
{
373
Console.WriteLine("media.Thumbnail is null for this track.");
374
}
315
375
}
316
376
else
317
377
{
@@ -1,6 +1,7 @@
1
using CommunityToolkit.Mvvm.ComponentModel;
1
using CommunityToolkit.Mvvm.ComponentModel;
2
2
using HaloPixelToolBox.Core.Utilities;
3
3
using HaloPixelToolBox.Profiles.CrossVersionProfiles;
4
using HaloPixelToolBox.Utilities;
4
5
using System.Diagnostics;
5
6
using XFEExtension.NetCore.StringExtension;
6
7
using XFEExtension.NetCore.WinUIHelper.Implements;
@@ -26,6 +27,10 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
26
27
[ObservableProperty]
27
28
private int switchBackTimeout = CloudMusicLyricsProfile.SwitchBackTimeout;
28
29
[ObservableProperty]
30
private bool enableScreenColorSync = CloudMusicLyricsProfile.EnableScreenColorSync;
31
[ObservableProperty]
32
private bool enableAmbientColorSync = CloudMusicLyricsProfile.EnableAmbientColorSync;
33
[ObservableProperty]
29
34
private string cloudMusicVersion = string.Empty;
30
35
[ObservableProperty]
31
36
private string supportedVersion = CloudMusicLyricsReader.VersionResolverDictionary.Keys.FirstOrDefault() ?? string.Empty;
@@ -53,7 +58,19 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
53
58
}
54
59
}
55
60
56
partial void OnEnableCloudMusicLyricsChanged(bool value) => CloudMusicLyricsProfile.EnableCloudMusicLyrics = value;
61
private bool _forceRefresh;
62
63
public void OnNavigatedTo()
64
{
65
_forceRefresh = true;
66
}
67
68
partial void OnEnableCloudMusicLyricsChanged(bool value)
69
{
70
CloudMusicLyricsProfile.EnableCloudMusicLyrics = value;
71
if (value)
72
_forceRefresh = true;
73
}
57
74
58
75
partial void OnUseInputedAddressChanged(bool value)
59
76
{
@@ -70,9 +87,94 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
70
87
partial void OnSwitchBackWhenPauseChanged(bool value) => CloudMusicLyricsProfile.SwitchBackWhenPause = value;
71
88
72
89
partial void OnSwitchBackTimeoutChanged(int value) => CloudMusicLyricsProfile.SwitchBackTimeout = value;
90
partial void OnEnableScreenColorSyncChanged(bool value)
91
{
92
CloudMusicLyricsProfile.EnableScreenColorSync = value;
93
_forceRefresh = true;
94
}
95
partial void OnEnableAmbientColorSyncChanged(bool value)
96
{
97
CloudMusicLyricsProfile.EnableAmbientColorSync = value;
98
_forceRefresh = true;
99
}
100
101
private Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager? _smtcManager;
102
private Windows.Media.Control.GlobalSystemMediaTransportControlsSession? _cloudMusicSession;
103
public (byte R, byte G, byte B)? CurrentAlbumColor { get; private set; }
104
105
private async Task InitSmtcAsync()
106
{
107
try
108
{
109
_smtcManager = await Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
110
UpdateCloudMusicSession();
111
_smtcManager.SessionsChanged += (s, e) => UpdateCloudMusicSession();
112
}
113
catch (Exception ex)
114
{
115
Console.WriteLine($"SMTC init failed: {ex.Message}");
116
}
117
}
118
119
private void UpdateCloudMusicSession()
120
{
121
if (_smtcManager == null) return;
122
var sessions = _smtcManager.GetSessions();
123
Windows.Media.Control.GlobalSystemMediaTransportControlsSession? targetSession = null;
124
foreach (var s in sessions)
125
{
126
if (s.SourceAppUserModelId.Contains("cloudmusic", StringComparison.OrdinalIgnoreCase))
127
{
128
targetSession = s;
129
break;
130
}
131
}
132
133
if (targetSession != _cloudMusicSession)
134
{
135
if (_cloudMusicSession != null)
136
{
137
_cloudMusicSession.MediaPropertiesChanged -= OnSmtcMediaPropertiesChanged;
138
}
139
_cloudMusicSession = targetSession;
140
if (_cloudMusicSession != null)
141
{
142
_cloudMusicSession.MediaPropertiesChanged += OnSmtcMediaPropertiesChanged;
143
_ = UpdateAlbumColorAsync();
144
}
145
}
146
}
147
148
private void OnSmtcMediaPropertiesChanged(Windows.Media.Control.GlobalSystemMediaTransportControlsSession sender, Windows.Media.Control.MediaPropertiesChangedEventArgs args)
149
{
150
_ = UpdateAlbumColorAsync();
151
}
152
153
private async Task UpdateAlbumColorAsync()
154
{
155
if (_cloudMusicSession == null) return;
156
try
157
{
158
var media = await _cloudMusicSession.TryGetMediaPropertiesAsync();
159
if (media?.Thumbnail != null)
160
{
161
var color = await SpotifyLyricsReader.GetDominantColorAsync(media.Thumbnail);
162
if (color.HasValue)
163
{
164
CurrentAlbumColor = color.Value;
165
Console.WriteLine($"CloudMusic extracted album color: RGB({color.Value.R}, {color.Value.G}, {color.Value.B})");
166
}
167
}
168
}
169
catch (Exception ex)
170
{
171
Console.WriteLine($"Failed to update CloudMusic album color: {ex.Message}");
172
}
173
}
73
174
74
175
public CloudMusicLyricsToolPageViewModel()
75
176
{
177
_ = InitSmtcAsync();
76
178
Console.WriteLine("初始化网易云歌词读取器");
77
179
Reader = new CloudMusicLyricsReader
78
180
{
@@ -149,7 +251,7 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
149
251
Console.WriteLine("等待花再设备...");
150
252
while (!DeviceReady)
151
253
await Task.Delay(500);
152
if (DeviceReady)
254
if (DeviceReady && EnableCloudMusicLyrics)
153
255
{
154
256
Console.WriteLine("花再设备已就绪,显示启动信息");
155
257
Device.SetTextLayout(Core.Models.HaloPixelTextLayout.Center);
@@ -168,28 +270,83 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
168
270
Console.WriteLine("[DEBUG]设备均在线,准备进入主循环");
169
271
string lastRead = string.Empty;
170
272
bool scrolled = false;
273
(byte R, byte G, byte B) lastScreenColor = (0, 0, 0);
274
(byte R, byte G, byte B) lastAmbientColor = (0, 0, 0);
171
275
while (true)
172
276
{
173
277
try
174
278
{
175
279
if (!DeviceReady || !CloudMusicReady || !EnableCloudMusicLyrics)
176
280
break;
177
if (Reader.TryReadLyrics(out var lyrics) && lastRead != lyrics)
281
282
var albumColor = CurrentAlbumColor;
283
284
// 1. Determine screen text color
285
(byte R, byte G, byte B) screenColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
286
if (EnableScreenColorSync && albumColor.HasValue)
287
{
288
screenColor = albumColor.Value;
289
}
290
291
// 2. Determine ambient backlight color
292
(byte R, byte G, byte B) ambientColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
293
if (EnableAmbientColorSync && albumColor.HasValue)
178
294
{
179
Console.WriteLine($"已读取到歌词:{lyrics}");
180
lastRead = lyrics;
181
isClockUI = false;
182
time = 0;
183
if (scrolled)
295
ambientColor = albumColor.Value;
296
}
297
298
bool colorChanged = lastScreenColor != screenColor || lastAmbientColor != ambientColor;
299
bool lyricsChanged = Reader.TryReadLyrics(out var lyrics) && lastRead != lyrics;
300
301
if (lyricsChanged || colorChanged || _forceRefresh)
302
{
303
_forceRefresh = false;
304
lastScreenColor = screenColor;
305
lastAmbientColor = ambientColor;
306
HidPacketBuilder.CurrentColor = screenColor;
307
308
try
184
309
{
185
Device.ShowText(string.Empty);
186
await Task.Delay(100);
187
scrolled = false;
310
Device.SetAmbientLight(new Core.Models.Lighting.AmbientLightOptions
311
{
312
Effect = Core.Models.Lighting.AmbientLightEffect.Static,
313
Color = new Core.Models.Display.HaloPixelColor(ambientColor.R, ambientColor.G, ambientColor.B),
314
Brightness = Core.Models.Lighting.AmbientLightBrightness.High,
315
Speed = 5
316
});
188
317
}
318
catch (Exception ex)
319
{
320
Console.WriteLine($"[ERROR] SetAmbientLight failed: {ex.Message}");
321
}
322
323
try
324
{
325
Device.SetPixelScreenColor(new Core.Models.Display.HaloPixelColor(screenColor.R, screenColor.G, screenColor.B));
326
}
327
catch (Exception ex)
328
{
329
Console.WriteLine($"[ERROR] SetPixelScreenColor failed: {ex.Message}");
330
}
331
332
if (lyricsChanged)
333
{
334
Console.WriteLine($"已读取到歌词:{lyrics}");
335
lastRead = lyrics;
336
isClockUI = false;
337
time = 0;
338
if (scrolled)
339
{
340
Device.ShowText(string.Empty);
341
await Task.Delay(100);
342
scrolled = false;
343
}
344
}
345
189
346
Device.SetTextLayout(CloudMusicLyricsProfile.DefaultHaloPixelTextLayout);
190
Device.ShowText(lyrics);
191
Debug.WriteLine(lyrics.DisplayLength());
192
if (lyrics.DisplayLength() > 30)
347
Device.ShowText(lastRead);
348
349
if (lyricsChanged && lastRead.DisplayLength() > 30)
193
350
{
194
351
scrolled = true;
195
352
await Task.Delay(500);
@@ -201,6 +358,33 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
201
358
if (!isClockUI && time >= CloudMusicLyricsProfile.SwitchBackTimeout * 1000)
202
359
{
203
360
isClockUI = true;
361
(byte R, byte G, byte B) finalScreenColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
362
if (EnableScreenColorSync && CurrentAlbumColor.HasValue)
363
{
364
finalScreenColor = CurrentAlbumColor.Value;
365
}
366
(byte R, byte G, byte B) finalAmbientColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
367
if (EnableAmbientColorSync && CurrentAlbumColor.HasValue)
368
{
369
finalAmbientColor = CurrentAlbumColor.Value;
370
}
371
HidPacketBuilder.CurrentColor = finalScreenColor;
372
try
373
{
374
Device.SetAmbientLight(new Core.Models.Lighting.AmbientLightOptions
375
{
376
Effect = Core.Models.Lighting.AmbientLightEffect.Static,
377
Color = new Core.Models.Display.HaloPixelColor(finalAmbientColor.R, finalAmbientColor.G, finalAmbientColor.B),
378
Brightness = Core.Models.Lighting.AmbientLightBrightness.High,
379
Speed = 5
380
});
381
}
382
catch {}
383
try
384
{
385
Device.SetPixelScreenColor(new Core.Models.Display.HaloPixelColor(finalScreenColor.R, finalScreenColor.G, finalScreenColor.B));
386
}
387
catch {}
204
388
Device.SetUIModel(CloudMusicLyricsProfile.DefaultHaloPixelUIModel);
205
389
Console.WriteLine("已切换至时钟界面");
206
390
}
@@ -25,6 +25,10 @@ public partial class SpotifyLyricsToolPageViewModel : ServiceBaseViewModelBase<s
25
25
[ObservableProperty]
26
26
private int switchBackTimeout = SpotifyLyricsProfile.SwitchBackTimeout;
27
27
[ObservableProperty]
28
private bool enableScreenColorSync = SpotifyLyricsProfile.EnableScreenColorSync;
29
[ObservableProperty]
30
private bool enableAmbientColorSync = SpotifyLyricsProfile.EnableAmbientColorSync;
31
[ObservableProperty]
28
32
private string spotifyTrackInfo = "未检测到播放中的歌曲";
29
33
30
34
public HaloPixelDevice Device { get; set; } = new();
@@ -32,9 +36,31 @@ public partial class SpotifyLyricsToolPageViewModel : ServiceBaseViewModelBase<s
32
36
33
37
public ISettingService SettingService { get; } = ServiceManager.GetService<ISettingService>();
34
38
35
partial void OnEnableSpotifyLyricsChanged(bool value) => SpotifyLyricsProfile.EnableSpotifyLyrics = value;
39
private bool _forceRefresh;
40
41
public void OnNavigatedTo()
42
{
43
_forceRefresh = true;
44
}
45
46
partial void OnEnableSpotifyLyricsChanged(bool value)
47
{
48
SpotifyLyricsProfile.EnableSpotifyLyrics = value;
49
if (value)
50
_forceRefresh = true;
51
}
36
52
partial void OnSwitchBackWhenPauseChanged(bool value) => SpotifyLyricsProfile.SwitchBackWhenPause = value;
37
53
partial void OnSwitchBackTimeoutChanged(int value) => SpotifyLyricsProfile.SwitchBackTimeout = value;
54
partial void OnEnableScreenColorSyncChanged(bool value)
55
{
56
SpotifyLyricsProfile.EnableScreenColorSync = value;
57
_forceRefresh = true;
58
}
59
partial void OnEnableAmbientColorSyncChanged(bool value)
60
{
61
SpotifyLyricsProfile.EnableAmbientColorSync = value;
62
_forceRefresh = true;
63
}
38
64
39
65
public SpotifyLyricsToolPageViewModel()
40
66
{
@@ -96,7 +122,7 @@ public partial class SpotifyLyricsToolPageViewModel : ServiceBaseViewModelBase<s
96
122
while (!DeviceReady)
97
123
await Task.Delay(500);
98
124
99
if (DeviceReady)
125
if (DeviceReady && EnableSpotifyLyrics)
100
126
{
101
127
Console.WriteLine("花再设备已就绪,显示启动信息");
102
128
Device.SetTextLayout(Core.Models.HaloPixelTextLayout.Center);
@@ -115,6 +141,8 @@ public partial class SpotifyLyricsToolPageViewModel : ServiceBaseViewModelBase<s
115
141
Console.WriteLine("[DEBUG]设备均在线,准备进入主循环");
116
142
string lastRead = string.Empty;
117
143
bool scrolled = false;
144
(byte R, byte G, byte B) lastScreenColor = (0, 0, 0);
145
(byte R, byte G, byte B) lastAmbientColor = (0, 0, 0);
118
146
while (true)
119
147
{
120
148
try
@@ -128,22 +156,74 @@ public partial class SpotifyLyricsToolPageViewModel : ServiceBaseViewModelBase<s
128
156
SpotifyTrackInfo = !string.IsNullOrEmpty(Reader.CurrentTitle) ? $"{Reader.CurrentArtist} - {Reader.CurrentTitle}" : "无播放中的歌曲";
129
157
});
130
158
131
if (Reader.TryReadLyrics(out var lyrics) && lastRead != lyrics)
159
var albumColor = Reader.CurrentAlbumColor;
160
161
// 1. Determine screen text color
162
(byte R, byte G, byte B) screenColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
163
if (EnableScreenColorSync && albumColor.HasValue)
164
{
165
screenColor = albumColor.Value;
166
}
167
168
// 2. Determine ambient backlight color
169
(byte R, byte G, byte B) ambientColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
170
if (EnableAmbientColorSync && albumColor.HasValue)
171
{
172
ambientColor = albumColor.Value;
173
}
174
175
bool colorChanged = lastScreenColor != screenColor || lastAmbientColor != ambientColor;
176
bool lyricsChanged = Reader.TryReadLyrics(out var lyrics) && lastRead != lyrics;
177
178
if (lyricsChanged || colorChanged || _forceRefresh)
132
179
{
133
Console.WriteLine($"已读取到歌词:{lyrics}");
134
lastRead = lyrics;
135
isClockUI = false;
136
time = 0;
137
if (scrolled)
180
_forceRefresh = false;
181
lastScreenColor = screenColor;
182
lastAmbientColor = ambientColor;
183
HidPacketBuilder.CurrentColor = screenColor;
184
185
try
186
{
187
Device.SetAmbientLight(new Core.Models.Lighting.AmbientLightOptions
188
{
189
Effect = Core.Models.Lighting.AmbientLightEffect.Static,
190
Color = new Core.Models.Display.HaloPixelColor(ambientColor.R, ambientColor.G, ambientColor.B),
191
Brightness = Core.Models.Lighting.AmbientLightBrightness.High,
192
Speed = 5
193
});
194
}
195
catch (Exception ex)
196
{
197
Console.WriteLine($"[ERROR] SetAmbientLight failed: {ex.Message}");
198
}
199
200
try
201
{
202
Device.SetPixelScreenColor(new Core.Models.Display.HaloPixelColor(screenColor.R, screenColor.G, screenColor.B));
203
}
204
catch (Exception ex)
205
{
206
Console.WriteLine($"[ERROR] SetPixelScreenColor failed: {ex.Message}");
207
}
208
209
if (lyricsChanged)
138
210
{
139
Device.ShowText(string.Empty);
140
await Task.Delay(100);
141
scrolled = false;
211
Console.WriteLine($"已读取到歌词:{lyrics}");
212
lastRead = lyrics;
213
isClockUI = false;
214
time = 0;
215
if (scrolled)
216
{
217
Device.ShowText(string.Empty);
218
await Task.Delay(100);
219
scrolled = false;
220
}
142
221
}
222
143
223
Device.SetTextLayout(SpotifyLyricsProfile.DefaultHaloPixelTextLayout);
144
Device.ShowText(lyrics);
145
Debug.WriteLine(lyrics.DisplayLength());
146
if (lyrics.DisplayLength() > 30)
224
Device.ShowText(lastRead);
225
226
if (lyricsChanged && lastRead.DisplayLength() > 30)
147
227
{
148
228
scrolled = true;
149
229
await Task.Delay(500);
@@ -155,6 +235,33 @@ public partial class SpotifyLyricsToolPageViewModel : ServiceBaseViewModelBase<s
155
235
if (!isClockUI && time >= SpotifyLyricsProfile.SwitchBackTimeout * 1000)
156
236
{
157
237
isClockUI = true;
238
(byte R, byte G, byte B) finalScreenColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
239
if (EnableScreenColorSync && Reader.CurrentAlbumColor.HasValue)
240
{
241
finalScreenColor = Reader.CurrentAlbumColor.Value;
242
}
243
(byte R, byte G, byte B) finalAmbientColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
244
if (EnableAmbientColorSync && Reader.CurrentAlbumColor.HasValue)
245
{
246
finalAmbientColor = Reader.CurrentAlbumColor.Value;
247
}
248
HidPacketBuilder.CurrentColor = finalScreenColor;
249
try
250
{
251
Device.SetAmbientLight(new Core.Models.Lighting.AmbientLightOptions
252
{
253
Effect = Core.Models.Lighting.AmbientLightEffect.Static,
254
Color = new Core.Models.Display.HaloPixelColor(finalAmbientColor.R, finalAmbientColor.G, finalAmbientColor.B),
255
Brightness = Core.Models.Lighting.AmbientLightBrightness.High,
256
Speed = 5
257
});
258
}
259
catch {}
260
try
261
{
262
Device.SetPixelScreenColor(new Core.Models.Display.HaloPixelColor(finalScreenColor.R, finalScreenColor.G, finalScreenColor.B));
263
}
264
catch {}
158
265
Device.SetUIModel(SpotifyLyricsProfile.DefaultHaloPixelUIModel);
159
266
Console.WriteLine("已切换至时钟界面");
160
267
}