mirror of
https://github.com/jayfunc/BetterLyrics.git
synced 2026-01-12 10:54:55 +08:00
fix: stats dashboard ui
This commit is contained in:
@@ -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"/>
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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=}" />
|
||||||
|
</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>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(); // 生成完刷新
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user