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
Name="37412.BetterLyrics"
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"/>

View File

@@ -6,5 +6,6 @@ namespace BetterLyrics.WinUI3.Constants
{
public static readonly TimeSpan DebounceTimeout = TimeSpan.FromMilliseconds(250);
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.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="36,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ProgressBar
Grid.Row="0"
Background="Transparent"
IsIndeterminate="{x:Bind ViewModel.IsLoading, Mode=OneWay}" />
<StackPanel Orientation="Horizontal" Spacing="12">
<ComboBox
x:Uid="StatsDashboardControlTimeRange"
Header="Time Range"
SelectedIndex="{x:Bind ViewModel.SelectedTimeRange, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="StatsDashboardControlToday" />
<ComboBoxItem x:Uid="StatsDashboardControlThisWeek" />
<ComboBoxItem x:Uid="StatsDashboardControlThisMonth" />
<ComboBoxItem x:Uid="StatsDashboardControlThisQuarter" />
<ComboBoxItem x:Uid="StatsDashboardControlThisYear" />
<ComboBoxItem x:Uid="StatsDashboardControlCustom" />
</ComboBox>
<controls:WrapPanel
Grid.Row="1"
Margin="36,12"
HorizontalSpacing="12"
Orientation="Horizontal"
VerticalSpacing="12">
<ComboBox
x:Uid="StatsDashboardControlTimeRange"
Header="Time Range"
SelectedIndex="{x:Bind ViewModel.SelectedTimeRange, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="StatsDashboardControlToday" />
<ComboBoxItem x:Uid="StatsDashboardControlThisWeek" />
<ComboBoxItem x:Uid="StatsDashboardControlThisMonth" />
<ComboBoxItem x:Uid="StatsDashboardControlThisQuarter" />
<ComboBoxItem x:Uid="StatsDashboardControlThisYear" />
<ComboBoxItem x:Uid="StatsDashboardControlCustom" />
</ComboBox>
<StackPanel
Margin="0,0,0,5"
VerticalAlignment="Bottom"
Orientation="Horizontal"
Spacing="8"
Visibility="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<CalendarDatePicker x:Uid="StatsDashboardControlStart" Date="{x:Bind ViewModel.CustomStartDate, Mode=TwoWay}" />
<TextBlock
Margin="0,26,0,0"
VerticalAlignment="Center"
Text="-" />
<CalendarDatePicker x:Uid="StatsDashboardControlEnd" Date="{x:Bind ViewModel.CustomEndDate, Mode=TwoWay}" />
</StackPanel>
</StackPanel>
</Grid>
<CalendarDatePicker
x:Uid="StatsDashboardControlStart"
Date="{x:Bind ViewModel.CustomStartDate, Mode=TwoWay}"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}" />
<TimePicker
VerticalAlignment="Bottom"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}"
Time="{x:Bind ViewModel.CustomStartTime, Mode=TwoWay}" />
<CalendarDatePicker
x:Uid="StatsDashboardControlEnd"
Date="{x:Bind ViewModel.CustomEndDate, Mode=TwoWay}"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}" />
<TimePicker
VerticalAlignment="Bottom"
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.RowDefinitions>
<RowDefinition />
@@ -367,11 +379,11 @@
</Grid>
</ScrollViewer>
<Button
Grid.Row="1"
<!--<Button
Grid.Row="2"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.GenerateTestDataCommand}"
Content="Generate test data" />
Content="Generate test data" />-->
</Grid>
</UserControl>

View File

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

View File

@@ -7,12 +7,14 @@ using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.WinUI;
using LiveChartsCore;
using LiveChartsCore.Kernel;
using LiveChartsCore.Kernel.Sketches;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.Themes;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using SkiaSharp;
using SkiaSharp.Views.Windows;
@@ -25,7 +27,7 @@ using System.Xml.Linq;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class StatsDashboardControlViewModel : ObservableObject
public partial class StatsDashboardControlViewModel : BaseViewModel
{
private readonly IPlayHistoryService _playHistoryService;
private readonly ILocalizationService _localizationService;
@@ -33,13 +35,17 @@ namespace BetterLyrics.WinUI3.ViewModels
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 bool IsCustomRangeSelected { get; set; }
[ObservableProperty] public partial DateTimeOffset? CustomStartDate { get; set; }
[ObservableProperty] public partial DateTimeOffset? CustomEndDate { get; set; }
[ObservableProperty] public partial StatsRange SelectedTimeRange { get; set; } = StatsRange.Today;
[ObservableProperty] public partial bool IsCustomRangeSelected { get; set; } = false;
[ObservableProperty] public partial DateTimeOffset? CustomStartDate { get; set; } = DateTime.Now;
[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; }
@@ -69,19 +75,20 @@ namespace BetterLyrics.WinUI3.ViewModels
_localizedTimesValue = _localizationService.GetLocalizedString("StatsDashboardControlTimes");
SelectedTimeRange = StatsRange.Today;
_timer = _dispatcherQueue.CreateTimer();
CustomStartDate = DateTimeOffset.Now.AddDays(-7);
CustomEndDate = DateTimeOffset.Now;
UpdateDateRange();
}
async partial void OnSelectedTimeRangeChanged(StatsRange value)
partial void OnSelectedTimeRangeChanged(StatsRange value)
{
IsCustomRangeSelected = value == StatsRange.Custom;
await LoadDataAsync();
UpdateDateRange();
}
async partial void OnCustomEndDateChanged(DateTimeOffset? value) => await LoadDataAsync();
async partial void OnCustomStartDateChanged(DateTimeOffset? value) => await LoadDataAsync();
partial void OnCustomEndDateChanged(DateTimeOffset? value) => LoadData();
partial void OnCustomStartDateChanged(DateTimeOffset? value) => LoadData();
partial void OnCustomStartTimeChanged(TimeSpan value) => LoadData();
partial void OnCustomEndTimeChanged(TimeSpan value) => LoadData();
private void ProcessHourlyStats(List<PlayHistoryItem> logs)
{
@@ -135,11 +142,24 @@ namespace BetterLyrics.WinUI3.ViewModels
private (DateTime? Start, DateTime? End) CalculateDateRange()
{
if (IsCustomRangeSelected)
{
return (CustomStartDate?.UtcDateTime, CustomEndDate?.UtcDateTime);
}
if (CustomStartDate == null || CustomEndDate == null) return (null, null);
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 startLocal = nowLocal.Date;
@@ -152,6 +172,7 @@ namespace BetterLyrics.WinUI3.ViewModels
int dayOfWeek = (int)nowLocal.DayOfWeek;
if (dayOfWeek == 0) dayOfWeek = 7;
startLocal = nowLocal.Date.AddDays(-(dayOfWeek - 1));
startLocal = new DateTime(startLocal.Year, startLocal.Month, startLocal.Day);
break;
case StatsRange.ThisMonth:
startLocal = new DateTime(nowLocal.Year, nowLocal.Month, 1);
@@ -165,60 +186,82 @@ namespace BetterLyrics.WinUI3.ViewModels
break;
}
return (startLocal.ToUniversalTime(), nowLocal.ToUniversalTime());
CustomStartDate = startLocal.Date;
CustomEndDate = nowLocal.Date;
CustomStartTime = startLocal.TimeOfDay;
CustomEndTime = nowLocal.TimeOfDay;
}
[RelayCommand]
public async Task LoadDataAsync()
private void RefreshData()
{
if (IsLoading) return;
IsLoading = true;
try
if (IsCustomRangeSelected)
{
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);
}
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)
{
System.Diagnostics.Debug.WriteLine($"Error loading stats: {ex.Message}");
}
finally
{
IsLoading = false;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error loading stats: {ex.Message}");
}
finally
{
IsLoading = false;
}
}, Constants.Time.DebounceTimeout);
}
[RelayCommand]
private async Task GenerateTestDataAsync()
{
await _playHistoryService.GenerateTestDataAsync(1000);
await LoadDataAsync(); // 生成完刷新
LoadData(); // 生成完刷新
}
}