fix: stats dashboard ui

This commit is contained in:
Zhe Fang
2026-01-03 17:27:47 -05:00
parent abca9ae5fb
commit 6fea88a6a1
5 changed files with 159 additions and 103 deletions

View File

@@ -12,7 +12,7 @@
<Identity <Identity
Name="37412.BetterLyrics" Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9" Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.2.237.0" /> Version="1.2.238.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/> <mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

View File

@@ -6,5 +6,6 @@ namespace BetterLyrics.WinUI3.Constants
{ {
public static readonly TimeSpan DebounceTimeout = TimeSpan.FromMilliseconds(250); public static readonly TimeSpan DebounceTimeout = TimeSpan.FromMilliseconds(250);
public static readonly TimeSpan AnimationDuration = TimeSpan.FromMilliseconds(350); public static readonly TimeSpan AnimationDuration = TimeSpan.FromMilliseconds(350);
public static readonly TimeSpan WaitingDuration = TimeSpan.FromMilliseconds(300);
} }
} }

View File

@@ -28,47 +28,59 @@
<Grid Margin="0,20,0,0"> <Grid Margin="0,20,0,0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="36,12"> <ProgressBar
<Grid.ColumnDefinitions> Grid.Row="0"
<ColumnDefinition Width="Auto" /> Background="Transparent"
<ColumnDefinition Width="*" /> IsIndeterminate="{x:Bind ViewModel.IsLoading, Mode=OneWay}" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Spacing="12"> <controls:WrapPanel
<ComboBox Grid.Row="1"
x:Uid="StatsDashboardControlTimeRange" Margin="36,12"
Header="Time Range" HorizontalSpacing="12"
SelectedIndex="{x:Bind ViewModel.SelectedTimeRange, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}"> Orientation="Horizontal"
<ComboBoxItem x:Uid="StatsDashboardControlToday" /> VerticalSpacing="12">
<ComboBoxItem x:Uid="StatsDashboardControlThisWeek" /> <ComboBox
<ComboBoxItem x:Uid="StatsDashboardControlThisMonth" /> x:Uid="StatsDashboardControlTimeRange"
<ComboBoxItem x:Uid="StatsDashboardControlThisQuarter" /> Header="Time Range"
<ComboBoxItem x:Uid="StatsDashboardControlThisYear" /> SelectedIndex="{x:Bind ViewModel.SelectedTimeRange, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="StatsDashboardControlCustom" /> <ComboBoxItem x:Uid="StatsDashboardControlToday" />
</ComboBox> <ComboBoxItem x:Uid="StatsDashboardControlThisWeek" />
<ComboBoxItem x:Uid="StatsDashboardControlThisMonth" />
<ComboBoxItem x:Uid="StatsDashboardControlThisQuarter" />
<ComboBoxItem x:Uid="StatsDashboardControlThisYear" />
<ComboBoxItem x:Uid="StatsDashboardControlCustom" />
</ComboBox>
<StackPanel <CalendarDatePicker
Margin="0,0,0,5" x:Uid="StatsDashboardControlStart"
VerticalAlignment="Bottom" Date="{x:Bind ViewModel.CustomStartDate, Mode=TwoWay}"
Orientation="Horizontal" IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}" />
Spacing="8" <TimePicker
Visibility="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"> VerticalAlignment="Bottom"
<CalendarDatePicker x:Uid="StatsDashboardControlStart" Date="{x:Bind ViewModel.CustomStartDate, Mode=TwoWay}" /> IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}"
<TextBlock Time="{x:Bind ViewModel.CustomStartTime, Mode=TwoWay}" />
Margin="0,26,0,0" <CalendarDatePicker
VerticalAlignment="Center" x:Uid="StatsDashboardControlEnd"
Text="-" /> Date="{x:Bind ViewModel.CustomEndDate, Mode=TwoWay}"
<CalendarDatePicker x:Uid="StatsDashboardControlEnd" Date="{x:Bind ViewModel.CustomEndDate, Mode=TwoWay}" /> IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}" />
</StackPanel> <TimePicker
</StackPanel> VerticalAlignment="Bottom"
</Grid> IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}"
Time="{x:Bind ViewModel.CustomEndTime, Mode=TwoWay}" />
<Button
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.RefreshDataCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE72C;}" />
</controls:WrapPanel>
<ScrollViewer Grid.Row="1" Padding="36,0"> <ScrollViewer Grid.Row="2" Padding="36,0">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition /> <RowDefinition />
@@ -367,11 +379,11 @@
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
<Button <!--<Button
Grid.Row="1" Grid.Row="2"
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.GenerateTestDataCommand}" Command="{x:Bind ViewModel.GenerateTestDataCommand}"
Content="Generate test data" /> Content="Generate test data" />-->
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -254,15 +254,15 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => true, GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => true,
_ => false, _ => false,
}; };
}
if (CurrentIsPlaying) if (CurrentIsPlaying)
{ {
_scrobbleStopwatch.Start(); _scrobbleStopwatch.Start();
} }
else else
{ {
_scrobbleStopwatch.Stop(); _scrobbleStopwatch.Stop();
}
} }
}); });
} }

View File

@@ -7,12 +7,14 @@ using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.PlayHistoryService; using BetterLyrics.WinUI3.Services.PlayHistoryService;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI;
using LiveChartsCore; using LiveChartsCore;
using LiveChartsCore.Kernel; using LiveChartsCore.Kernel;
using LiveChartsCore.Kernel.Sketches; using LiveChartsCore.Kernel.Sketches;
using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting; using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.Themes; using LiveChartsCore.Themes;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using SkiaSharp; using SkiaSharp;
using SkiaSharp.Views.Windows; using SkiaSharp.Views.Windows;
@@ -25,7 +27,7 @@ using System.Xml.Linq;
namespace BetterLyrics.WinUI3.ViewModels namespace BetterLyrics.WinUI3.ViewModels
{ {
public partial class StatsDashboardControlViewModel : ObservableObject public partial class StatsDashboardControlViewModel : BaseViewModel
{ {
private readonly IPlayHistoryService _playHistoryService; private readonly IPlayHistoryService _playHistoryService;
private readonly ILocalizationService _localizationService; private readonly ILocalizationService _localizationService;
@@ -33,13 +35,17 @@ namespace BetterLyrics.WinUI3.ViewModels
private string _localizedTimesValue; private string _localizedTimesValue;
[ObservableProperty] public partial bool IsLoading { get; set; } private readonly DispatcherQueueTimer _timer;
[ObservableProperty] public partial bool IsLoading { get; set; } = false;
// 时间筛选 // 时间筛选
[ObservableProperty] public partial StatsRange SelectedTimeRange { get; set; } [ObservableProperty] public partial StatsRange SelectedTimeRange { get; set; } = StatsRange.Today;
[ObservableProperty] public partial bool IsCustomRangeSelected { get; set; } [ObservableProperty] public partial bool IsCustomRangeSelected { get; set; } = false;
[ObservableProperty] public partial DateTimeOffset? CustomStartDate { get; set; } [ObservableProperty] public partial DateTimeOffset? CustomStartDate { get; set; } = DateTime.Now;
[ObservableProperty] public partial DateTimeOffset? CustomEndDate { get; set; } [ObservableProperty] public partial DateTimeOffset? CustomEndDate { get; set; } = DateTime.Now;
[ObservableProperty] public partial TimeSpan CustomStartTime { get; set; } = TimeSpan.Zero;
[ObservableProperty] public partial TimeSpan CustomEndTime { get; set; } = TimeSpan.Zero;
// 顶部基础数据 // 顶部基础数据
[ObservableProperty] public partial TimeSpan TotalDuration { get; set; } [ObservableProperty] public partial TimeSpan TotalDuration { get; set; }
@@ -69,19 +75,20 @@ namespace BetterLyrics.WinUI3.ViewModels
_localizedTimesValue = _localizationService.GetLocalizedString("StatsDashboardControlTimes"); _localizedTimesValue = _localizationService.GetLocalizedString("StatsDashboardControlTimes");
SelectedTimeRange = StatsRange.Today; _timer = _dispatcherQueue.CreateTimer();
CustomStartDate = DateTimeOffset.Now.AddDays(-7); UpdateDateRange();
CustomEndDate = DateTimeOffset.Now;
} }
async partial void OnSelectedTimeRangeChanged(StatsRange value) partial void OnSelectedTimeRangeChanged(StatsRange value)
{ {
IsCustomRangeSelected = value == StatsRange.Custom; IsCustomRangeSelected = value == StatsRange.Custom;
await LoadDataAsync(); UpdateDateRange();
} }
async partial void OnCustomEndDateChanged(DateTimeOffset? value) => await LoadDataAsync(); partial void OnCustomEndDateChanged(DateTimeOffset? value) => LoadData();
async partial void OnCustomStartDateChanged(DateTimeOffset? value) => await LoadDataAsync(); partial void OnCustomStartDateChanged(DateTimeOffset? value) => LoadData();
partial void OnCustomStartTimeChanged(TimeSpan value) => LoadData();
partial void OnCustomEndTimeChanged(TimeSpan value) => LoadData();
private void ProcessHourlyStats(List<PlayHistoryItem> logs) private void ProcessHourlyStats(List<PlayHistoryItem> logs)
{ {
@@ -135,11 +142,24 @@ namespace BetterLyrics.WinUI3.ViewModels
private (DateTime? Start, DateTime? End) CalculateDateRange() private (DateTime? Start, DateTime? End) CalculateDateRange()
{ {
if (IsCustomRangeSelected) if (CustomStartDate == null || CustomEndDate == null) return (null, null);
{
return (CustomStartDate?.UtcDateTime, CustomEndDate?.UtcDateTime);
}
return (
new DateTime(
DateOnly.FromDateTime(CustomStartDate.Value.LocalDateTime),
TimeOnly.FromTimeSpan(CustomStartTime),
DateTimeKind.Local)
.ToUniversalTime(),
new DateTime(
DateOnly.FromDateTime(CustomEndDate.Value.LocalDateTime),
TimeOnly.FromTimeSpan(CustomEndTime),
DateTimeKind.Local)
.ToUniversalTime()
);
}
private void UpdateDateRange()
{
DateTime nowLocal = DateTime.Now; DateTime nowLocal = DateTime.Now;
DateTime startLocal = nowLocal.Date; DateTime startLocal = nowLocal.Date;
@@ -152,6 +172,7 @@ namespace BetterLyrics.WinUI3.ViewModels
int dayOfWeek = (int)nowLocal.DayOfWeek; int dayOfWeek = (int)nowLocal.DayOfWeek;
if (dayOfWeek == 0) dayOfWeek = 7; if (dayOfWeek == 0) dayOfWeek = 7;
startLocal = nowLocal.Date.AddDays(-(dayOfWeek - 1)); startLocal = nowLocal.Date.AddDays(-(dayOfWeek - 1));
startLocal = new DateTime(startLocal.Year, startLocal.Month, startLocal.Day);
break; break;
case StatsRange.ThisMonth: case StatsRange.ThisMonth:
startLocal = new DateTime(nowLocal.Year, nowLocal.Month, 1); startLocal = new DateTime(nowLocal.Year, nowLocal.Month, 1);
@@ -165,60 +186,82 @@ namespace BetterLyrics.WinUI3.ViewModels
break; break;
} }
return (startLocal.ToUniversalTime(), nowLocal.ToUniversalTime()); CustomStartDate = startLocal.Date;
CustomEndDate = nowLocal.Date;
CustomStartTime = startLocal.TimeOfDay;
CustomEndTime = nowLocal.TimeOfDay;
} }
[RelayCommand] [RelayCommand]
public async Task LoadDataAsync() private void RefreshData()
{ {
if (IsLoading) return; if (IsCustomRangeSelected)
IsLoading = true;
try
{ {
var (start, end) = CalculateDateRange(); LoadData();
}
else
{
UpdateDateRange();
}
}
if (start == null || end == null) [RelayCommand]
public void LoadData()
{
_timer.Debounce(async () =>
{
if (IsLoading) return;
IsLoading = true;
try
{ {
start = end = DateTime.Now.ToUniversalTime(); await Task.Delay(Constants.Time.WaitingDuration);
var (start, end) = CalculateDateRange();
if (start == null || end == null)
{
start = end = DateTime.Now.ToUniversalTime();
}
var durationTask = _playHistoryService.GetTotalListeningDurationAsync(start.Value, end.Value);
var logsTask = _playHistoryService.GetLogsByDateRangeAsync(start.Value, end.Value);
var topSongsTask = _playHistoryService.GetTopSongsAsync(start.Value, end.Value, 10);
var topArtistsTask = _playHistoryService.GetTopArtistsAsync(start.Value, end.Value, 10);
var playersTask = _playHistoryService.GetPlayerDistributionAsync(start.Value, end.Value);
await Task.WhenAll(durationTask, logsTask, topSongsTask, topArtistsTask, playersTask);
TotalDuration = await durationTask;
var logs = await logsTask;
TotalTracksPlayed = logs.Count;
TopSongs = [.. await topSongsTask];
var pStats = await playersTask;
UpdatePlayerStats(pStats);
TopArtists = [.. await topArtistsTask];
ProcessHourlyStats(logs);
} }
catch (Exception ex)
var durationTask = _playHistoryService.GetTotalListeningDurationAsync(start.Value, end.Value); {
var logsTask = _playHistoryService.GetLogsByDateRangeAsync(start.Value, end.Value); System.Diagnostics.Debug.WriteLine($"Error loading stats: {ex.Message}");
var topSongsTask = _playHistoryService.GetTopSongsAsync(start.Value, end.Value, 10); }
var topArtistsTask = _playHistoryService.GetTopArtistsAsync(start.Value, end.Value, 10); finally
var playersTask = _playHistoryService.GetPlayerDistributionAsync(start.Value, end.Value); {
IsLoading = false;
await Task.WhenAll(durationTask, logsTask, topSongsTask, topArtistsTask, playersTask); }
}, Constants.Time.DebounceTimeout);
TotalDuration = await durationTask;
var logs = await logsTask;
TotalTracksPlayed = logs.Count;
TopSongs = [.. await topSongsTask];
var pStats = await playersTask;
UpdatePlayerStats(pStats);
TopArtists = [.. await topArtistsTask];
ProcessHourlyStats(logs);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error loading stats: {ex.Message}");
}
finally
{
IsLoading = false;
}
} }
[RelayCommand] [RelayCommand]
private async Task GenerateTestDataAsync() private async Task GenerateTestDataAsync()
{ {
await _playHistoryService.GenerateTestDataAsync(1000); await _playHistoryService.GenerateTestDataAsync(1000);
await LoadDataAsync(); // 生成完刷新 LoadData(); // 生成完刷新
} }
} }