Compare commits

...

42 Commits

Author SHA1 Message Date
Zhe Fang
abca9ae5fb fix: auto-play in music gallery wont show song title and artist when first play 2026-01-03 12:50:33 -05:00
Zhe Fang
a062897e1a fix: music gallery play issue (and improve ui/ux) 2026-01-03 12:14:54 -05:00
Zhe Fang
8b4748df1b fix: stats dashboard data selection issue 2026-01-02 12:05:10 -05:00
Zhe Fang
1df5ea6bab fix: playback realtime info encoded cache path 2026-01-02 08:27:03 -05:00
Zhe Fang
c576635af2 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-01 21:49:33 -05:00
Zhe Fang
c8590202ec chores: improve music stats 2026-01-01 21:49:31 -05:00
Zhe Fang
2dc8b1283f Update README.CN.md 2026-01-01 12:38:31 -05:00
Zhe Fang
c482edea0f Update README.md 2026-01-01 12:36:39 -05:00
Zhe Fang
315722252c Update README.md 2026-01-01 12:32:01 -05:00
Zhe Fang
32ba453264 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 16:30:16 -05:00
Zhe Fang
d4902329bb chores: update readme 2025-12-31 16:30:14 -05:00
Zhe Fang
83aee8948b Update README.CN.md 2025-12-31 16:13:48 -05:00
Zhe Fang
1f9fab3228 Update README.CN.md 2025-12-31 16:10:42 -05:00
Zhe Fang
7a3a659dfc Update README.CN.md 2025-12-31 16:09:17 -05:00
Zhe Fang
a14afd3eb5 Update README.md 2025-12-31 16:01:37 -05:00
Zhe Fang
c2af7f3186 Update README.CN.md 2025-12-31 16:00:16 -05:00
Zhe Fang
cd026dd2bf Update README.CN.md 2025-12-31 15:56:08 -05:00
Zhe Fang
4bc1a9975d Update README.md 2025-12-31 15:52:05 -05:00
Zhe Fang
07eecf0930 Update README.md 2025-12-31 15:49:22 -05:00
Zhe Fang
35fba5abb0 chores 2025-12-31 15:44:58 -05:00
Zhe Fang
03ef231a3f chores 2025-12-31 15:39:44 -05:00
Zhe Fang
f41879f4e5 chores: add pic 2025-12-31 15:38:18 -05:00
Zhe Fang
bda7510ed6 chores: update readme 2025-12-31 14:28:54 -05:00
Zhe Fang
5ec8c7c61f chores: bump to 1.2.236 2025-12-31 13:32:45 -05:00
Zhe Fang
7e6bd9dade fix: _scrobbleStopwatch 2025-12-31 10:55:56 -05:00
Zhe Fang
56244cb793 fix: scrollable timer 2025-12-31 10:47:43 -05:00
Zhe Fang
cb5f70ab55 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 10:12:29 -05:00
Zhe Fang
3cc018bb1f chores: bump to 1.2.234.0 2025-12-31 10:12:28 -05:00
Zhe Fang
c517d2b008 Update README.md 2025-12-31 10:08:58 -05:00
Zhe Fang
e79f2a0223 Update README.CN.md 2025-12-31 10:07:53 -05:00
Zhe Fang
39122b9147 Update README.CN.md 2025-12-31 10:07:01 -05:00
Zhe Fang
accbdc1806 Update README.md 2025-12-31 10:06:01 -05:00
Zhe Fang
de014d1ad7 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 09:52:12 -05:00
Zhe Fang
cc2ce5f8cf chores: i18n 2025-12-31 09:52:10 -05:00
Zhe Fang
2a2d80436e Update README.CN.md 2025-12-31 09:34:29 -05:00
Zhe Fang
ce3f79f35c Update README.md 2025-12-31 09:33:01 -05:00
Zhe Fang
12e6000cb3 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 08:43:44 -05:00
Zhe Fang
c1dc684411 fix: scrobble timer is still running when music is paused 2025-12-31 08:43:43 -05:00
Zhe Fang
69ea2cb495 Update README.md 2025-12-31 08:21:33 -05:00
Zhe Fang
e2ee03c4be Update README.CN.md 2025-12-31 08:21:03 -05:00
Zhe Fang
c6fe33d6ae Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2025-12-31 08:12:12 -05:00
Zhe Fang
7744e145fa chores: i18n 2025-12-31 08:12:10 -05:00
79 changed files with 2773 additions and 1807 deletions

View File

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

View File

@@ -3,8 +3,6 @@
x:Class="BetterLyrics.WinUI3.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converter="using:BetterLyrics.WinUI3.Converter"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:globalization="using:Windows.Globalization"
xmlns:local="using:BetterLyrics.WinUI3"
xmlns:media="using:CommunityToolkit.WinUI.Media">
@@ -13,8 +11,14 @@
<ResourceDictionary.MergedDictionaries>
<!-- Merged dictionaries here -->
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/Segmented/Segmented.xaml" />
<ResourceDictionary Source="ms-appx:///DevWinUI.Controls/Themes/Generic.xaml" />
<ResourceDictionary Source="/Styles/Converters.xaml" />
<ResourceDictionary Source="/Styles/InteractiveListViewHeaderStyle.xaml" />
<ResourceDictionary Source="/Styles/GhostSliderStyle.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Theme -->
@@ -42,46 +46,6 @@
<ExponentialEase x:Key="EaseOut" EasingMode="EaseOut" />
<ExponentialEase x:Key="EaseIn" EasingMode="EaseIn" />
<!-- Converter -->
<converter:EnumToIntConverter x:Key="EnumToIntConverter" />
<converter:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converter:MatchedLocalFilesPathToVisibilityConverter x:Key="MatchedLocalFilesPathToVisibilityConverter" />
<converter:IntToCornerRadius x:Key="IntToCornerRadius" />
<converter:CornerRadiusToDoubleConverter x:Key="CornerRadiusToDoubleConverter" />
<converter:LyricsSearchProviderToDisplayNameConverter x:Key="LyricsSearchProviderToDisplayNameConverter" />
<converter:TranslationSearchProviderToDisplayNameConverter x:Key="TranslationSearchProviderToDisplayNameConverter" />
<converter:TransliterationSearchProviderToDisplayNameConverter x:Key="TransliterationSearchProviderToDisplayNameConverter" />
<converter:AlbumArtSearchProviderToDisplayNameConverter x:Key="AlbumArtSearchProviderToDisplayNameConverter" />
<converter:SecondsToFormattedTimeConverter x:Key="SecondsToFormattedTimeConverter" />
<converter:MillisecondsToFormattedTimeConverter x:Key="MillisecondsToFormattedTimeConverter" />
<converter:FPSToTimeSpanConverter x:Key="FPSToTimeSpanConverter" />
<converter:ShortcutToStringConverter x:Key="ShortcutToStringConverter" />
<converter:BoolNegationToVisibilityConverter x:Key="BoolNegationToVisibilityConverter" />
<converter:BoolToOpacityConverter x:Key="BoolToOpacityConverter" />
<converter:BoolToPartialOpacityConverter x:Key="BoolToPartialOpacityConverter" />
<converter:BoolNegationToOpacityConverter x:Key="BoolNegationToOpacityConverter" />
<converter:RectToMarginConverter x:Key="RectToMarginConverter" />
<converter:LanguageCodeToDisplayedNameConverter x:Key="LanguageCodeToDisplayedNameConverter" />
<converter:ByteArrayToImageConverter x:Key="ByteArrayToImageConverter" />
<converter:DisplayLanguageCodeToIndexConverter x:Key="DisplayLanguageCodeToIndexConverter" />
<converter:PathToParentFolderConverter x:Key="PathToParentFolderConverter" />
<converter:IntToBoolConverter x:Key="IntToBoolConverter" />
<converter:IndexToDisplayConverter x:Key="IndexToDisplayConverter" />
<converter:IntToDoubleConverter x:Key="IntToDoubleConverter" />
<converter:MillisecondsToSecondsConverter x:Key="MillisecondsToSecondsConverter" />
<converter:PictureInfosToImageSourceConverter x:Key="PictureInfosToImageSourceConverter" />
<converter:LyricsFontWeightToFontWeightConverter x:Key="LyricsFontWeightToFontWeightConverter" />
<converter:TextAlignmentTypeToHorizontalAlignmentConverter x:Key="TextAlignmentTypeToHorizontalAlignmentConverter" />
<converter:LyricsLayoutOrientationToOrientationConverter x:Key="LyricsLayoutOrientationToOrientationConverter" />
<converter:LyricsLayoutOrientationNegationToOrientationConverter x:Key="LyricsLayoutOrientationNegationToOrientationConverter" />
<converter:FileSourceTypeToIconConverter x:Key="FileSourceTypeToIconConverter" />
<converter:PathToImageConverter x:Key="PathToImageConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<converters:CollectionVisibilityConverter x:Key="CollectionVisibilityConverter" />
<x:Double x:Key="SettingsCardSpacing">4</x:Double>
<!-- Style -->
@@ -109,7 +73,7 @@
</Style>
<Style
x:Key="TitleBarToggleButtonStyle"
BasedOn="{StaticResource ToggleButtonRevealStyle}"
BasedOn="{StaticResource DefaultToggleButtonStyle}"
TargetType="ToggleButton">
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="CornerRadius" Value="4" />
@@ -117,7 +81,10 @@
<Setter Property="Padding" Value="14,6,14,9" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostToggleButtonStyle" TargetType="ToggleButton">
<Style
x:Key="GhostToggleButtonStyle"
BasedOn="{StaticResource DefaultToggleButtonStyle}"
TargetType="ToggleButton">
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
@@ -131,190 +98,6 @@
<Setter Property="CornerRadius" Value="6" />
</Style>
<Style x:Key="GhostSliderStyle" TargetType="Slider">
<Setter Property="Background" Value="{ThemeResource ControlStrokeColorOnAccentDefaultBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource SliderBorderThemeThickness}" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="ManipulationMode" Value="None" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-7,0,-7,0" />
<Setter Property="IsFocusEngagementEnabled" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Slider">
<Grid Margin="{TemplateBinding Padding}">
<Grid.Resources>
<Style x:Key="SliderThumbStyle" TargetType="Thumb">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="0,1,1,0" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter
x:Name="HeaderContentPresenter"
Grid.Row="0"
Margin="{ThemeResource SliderTopHeaderMargin}"
x:DeferLoadStrategy="Lazy"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontWeight="{ThemeResource SliderHeaderThemeFontWeight}"
Foreground="{ThemeResource SliderHeaderForeground}"
TextWrapping="Wrap"
Visibility="Collapsed" />
<Grid
x:Name="SliderContainer"
Grid.Row="1"
Background="{ThemeResource SliderContainerBackground}"
Control.IsTemplateFocusTarget="True">
<Grid x:Name="HorizontalTemplate" MinHeight="{ThemeResource SliderHorizontalHeight}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="{ThemeResource SliderPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{ThemeResource SliderPostContentMargin}" />
</Grid.RowDefinitions>
<Rectangle
x:Name="HorizontalTrackRect"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="HorizontalDecreaseRect"
Grid.Row="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="TopTickBar"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,0,4"
VerticalAlignment="Bottom"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="HorizontalInlineTickBar"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="BottomTickBar"
Grid.Row="2"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,4,0,0"
VerticalAlignment="Top"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="HorizontalThumb"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
Width="2"
Height="2"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-14,-6,-14,-6"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
<Grid
x:Name="VerticalTemplate"
MinWidth="{ThemeResource SliderVerticalWidth}"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{ThemeResource SliderPreContentMargin}" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="{ThemeResource SliderPostContentMargin}" />
</Grid.ColumnDefinitions>
<Rectangle
x:Name="VerticalTrackRect"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="VerticalDecreaseRect"
Grid.Row="2"
Grid.Column="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="LeftTickBar"
Grid.RowSpan="3"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,4,0"
HorizontalAlignment="Right"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="VerticalInlineTickBar"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="RightTickBar"
Grid.RowSpan="3"
Grid.Column="2"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="4,0,0,0"
HorizontalAlignment="Left"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="VerticalThumb"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Width="24"
Height="8"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-6,-14,-6,-14"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="ListViewStretchedItemContainerStyle"
BasedOn="{StaticResource DefaultListViewItemStyle}"
@@ -358,10 +141,6 @@
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden" />
</Style>
<StaticResource x:Key="ToggleButtonBackgroundChecked" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPointerOver" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPressed" ResourceKey="TextFillColorPrimaryBrush" />
<!-- Dimensions -->
<!-- Fonts -->

View File

@@ -4,12 +4,13 @@ using BetterLyrics.WinUI3.Models.Db;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.DiscordService;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SMTCService;
using BetterLyrics.WinUI3.Services.TranslationService;
using BetterLyrics.WinUI3.Services.TransliterationService;
using BetterLyrics.WinUI3.ViewModels;
@@ -19,7 +20,6 @@ using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching; // 关键:用于线程调度
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle; // 关键App生命周期管理
using Serilog;
@@ -278,7 +278,8 @@ namespace BetterLyrics.WinUI3
// Services
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IMediaSessionsService, MediaSessionsService>()
.AddSingleton<ISMTCService, SMTCService>()
.AddSingleton<IGSMTCService, GSMTCService>()
.AddSingleton<IAlbumArtSearchService, AlbumArtSearchService>()
.AddSingleton<ILyricsSearchService, LyricsSearchService>()
.AddSingleton<ITranslationService, TranslationService>()
@@ -304,6 +305,7 @@ namespace BetterLyrics.WinUI3
.AddSingleton<AboutControlViewModel>()
.AddSingleton<MusicGalleryWindowViewModel>()
.AddSingleton<StatsDashboardControlViewModel>()
.AddSingleton<PlayQueueViewModel>()
.AddTransient<NowPlayingWindowViewModel>()
.AddTransient<NowPlayingPageViewModel>()

View File

@@ -43,12 +43,15 @@
<None Remove="Controls\MediaSettingsControl.xaml" />
<None Remove="Controls\NowPlayingBar.xaml" />
<None Remove="Controls\PlaybackSettingsControl.xaml" />
<None Remove="Controls\PlayQueue.xaml" />
<None Remove="Controls\PropertyRow.xaml" />
<None Remove="Controls\RemoteServerConfigControl.xaml" />
<None Remove="Controls\ShortcutTextBox.xaml" />
<None Remove="Controls\StatsDashboardControl.xaml" />
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Controls\WindowSettingsControl.xaml" />
<None Remove="Styles\GhostSliderStyle.xaml" />
<None Remove="Styles\InteractiveListViewHeaderStyle.xaml" />
<None Remove="Views\LyricsSearchWindow.xaml" />
<None Remove="Views\LyricsWindowSwitchWindow.xaml" />
<None Remove="Views\MusicGalleryPage.xaml" />
@@ -86,8 +89,8 @@
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.4.1" />
<PackageReference Include="Hqub.Last.fm" Version="2.5.1" />
<PackageReference Include="Interop.UIAutomationClient" Version="10.19041.0" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.WinUI" Version="2.0.0-rc6.1" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
@@ -259,6 +262,21 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\GhostSliderStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\Converters.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PlayQueue.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\StatsDashboardControl.xaml">
<Generator>MSBuild:Compile</Generator>
@@ -409,6 +427,11 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\InteractiveListViewHeaderStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>

View File

@@ -8,7 +8,7 @@ using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Renderer;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
@@ -41,7 +41,7 @@ namespace BetterLyrics.WinUI3.Controls
IRecipient<PropertyChangedMessage<IRandomAccessStream?>>
{
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
private readonly IGSMTCService _gsmtcService = Ioc.Default.GetRequiredService<IGSMTCService>();
private readonly LyricsRenderer _lyricsRenderer = new();
private readonly FluidBackgroundRenderer _fluidRenderer = new();
@@ -98,8 +98,6 @@ namespace BetterLyrics.WinUI3.Controls
private TimeSpan _songPositionWithOffset;
private TimeSpan _songPosition; // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1>
private TimeSpan _totalPlayedTime; // <20><>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ػ<EFBFBD><D8BB>ظ<EFBFBD><D8B8><EFBFBD><EFBFBD>ŵ<EFBFBD>ʱ<EFBFBD>
private bool _isLastFMTracked = false;
private double _renderLyricsStartX = 0;
private double _renderLyricsStartY = 0;
@@ -345,7 +343,7 @@ namespace BetterLyrics.WinUI3.Controls
var lyricsStyle = _lyricsWindowStatus.LyricsStyleSettings;
var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings;
double songDuration = _mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0;
double songDuration = _gsmtcService.CurrentSongInfo?.DurationMs ?? 0;
bool isForceWordByWord = _settingsService.AppSettings.GeneralSettings.IsForceWordByWordEffect;
Color overlayColor;
@@ -459,7 +457,7 @@ namespace BetterLyrics.WinUI3.Controls
var lyricsBg = _lyricsWindowStatus.LyricsBackgroundSettings;
var lyricsStyle = _lyricsWindowStatus.LyricsStyleSettings;
var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings;
var lyricsData = _mediaSessionsService.CurrentLyricsData;
var lyricsData = _gsmtcService.CurrentLyricsData;
TimeSpan elapsedTime = args.Timing.ElapsedTime;
@@ -654,25 +652,22 @@ namespace BetterLyrics.WinUI3.Controls
private void UpdatePlaybackState(TimeSpan elapsedTime)
{
if (_mediaSessionsService.CurrentIsPlaying)
if (_gsmtcService.CurrentIsPlaying)
{
_songPosition += elapsedTime;
_totalPlayedTime += elapsedTime;
_songPositionWithOffset = _songPosition + TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0);
_songPositionWithOffset = _songPosition + TimeSpan.FromMilliseconds(_gsmtcService.CurrentMediaSourceProviderInfo?.PositionOffset ?? 0);
}
}
private void ResetPlaybackState()
{
_songPosition = TimeSpan.Zero;
_totalPlayedTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
private void UpdateRenderLyricsLines()
{
_renderLyricsLines = null;
_renderLyricsLines = _mediaSessionsService.CurrentLyricsData?.LyricsLines.Select(x => new RenderLyricsLine()
_renderLyricsLines = _gsmtcService.CurrentLyricsData?.LyricsLines.Select(x => new RenderLyricsLine()
{
LyricsSyllables = x.LyricsSyllables,
StartMs = x.StartMs,
@@ -685,7 +680,7 @@ namespace BetterLyrics.WinUI3.Controls
private async Task ReloadCoverBackgroundResourcesAsync()
{
if (_mediaSessionsService.AlbumArtBitmapStream is IRandomAccessStream stream)
if (_gsmtcService.AlbumArtBitmapStream is IRandomAccessStream stream)
{
stream.Seek(0);
CanvasBitmap bitmap = await CanvasBitmap.LoadAsync(Canvas, stream);
@@ -695,26 +690,19 @@ namespace BetterLyrics.WinUI3.Controls
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition))
if (message.PropertyName == nameof(IGSMTCService.CurrentPosition))
{
var realPosition = message.NewValue;
var diff = Math.Abs(_songPosition.TotalMilliseconds - realPosition.TotalMilliseconds);
var timelineSyncThreshold = _mediaSessionsService.CurrentMediaSourceProviderInfo?.TimelineSyncThreshold ?? 0;
var timelineSyncThreshold = _gsmtcService.CurrentMediaSourceProviderInfo?.TimelineSyncThreshold ?? 0;
// ƫ<><C6AB> or seek
if (diff >= timelineSyncThreshold)
{
_songPosition = realPosition;
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˿<EFBFBD>ͷ<EFBFBD><CDB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD> LastFM ͳ<><CDB3>״̬
if (_songPosition.TotalSeconds <= 1)
{
_totalPlayedTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
}
// <20>϶<EFBFBD><CFB6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȴ<EFBFBD><C8B4><EFBFBD><EFBFBD><EFBFBD>
@@ -728,9 +716,9 @@ namespace BetterLyrics.WinUI3.Controls
public void Receive(PropertyChangedMessage<LyricsData?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentLyricsData))
if (message.PropertyName == nameof(IGSMTCService.CurrentLyricsData))
{
UpdateRenderLyricsLines();
_isLayoutChanged = true;
@@ -740,9 +728,9 @@ namespace BetterLyrics.WinUI3.Controls
public void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
if (message.PropertyName == nameof(IGSMTCService.CurrentSongInfo))
{
ResetPlaybackState();
}
@@ -895,9 +883,9 @@ namespace BetterLyrics.WinUI3.Controls
public void Receive(PropertyChangedMessage<IRandomAccessStream?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapStream))
if (message.PropertyName == nameof(IGSMTCService.AlbumArtBitmapStream))
{
_ = ReloadCoverBackgroundResourcesAsync();
}

View File

@@ -124,7 +124,14 @@
</ListView.ItemTemplate>
</ListView>
<dev:SettingsCard Style="{StaticResource DefaultSettingsExpanderItemStyle}">
<StackPanel
Margin="0,6,0,0"
HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="6">
<Button Command="{x:Bind ViewModel.OpenMusicGalleryWindowCommand}">
<TextBlock x:Uid="SystemTrayMusicGallery" />
</Button>
<DropDownButton x:Uid="SettingsPageAddFolderButton">
<DropDownButton.Flyout>
<MenuFlyout>
@@ -169,7 +176,7 @@
</MenuFlyout>
</DropDownButton.Flyout>
</DropDownButton>
</dev:SettingsCard>
</StackPanel>
</StackPanel>
</Grid>

View File

@@ -4,6 +4,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dev="using:DevWinUI"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -176,13 +177,13 @@
Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}"
Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}"
Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
@@ -196,13 +197,13 @@
Style="{StaticResource GhostButtonStyle}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}"
Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.MediaSessionsService.CurrentIsPlaying, Mode=OneWay}"
Binding="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
@@ -215,12 +216,16 @@
Glyph=&#xE623;}"
Style="{StaticResource GhostButtonStyle}" />
<!-- 播放队列按钮 -->
<ToggleButton
<Button
Click="PlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE8FD;}"
Style="{StaticResource GhostToggleButtonStyle}"
Visibility="{x:Bind ShowPlayingQueueButton, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind ShowPlayingQueueButton, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPagePlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</Grid>
@@ -231,6 +236,19 @@
Orientation="Horizontal"
Spacing="3">
<!-- Stop media session -->
<Button
Command="{x:Bind ViewModel.StopTrackCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE71A;}"
Style="{StaticResource GhostButtonStyle}"
Visibility="{x:Bind ShowStopButton, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageStopTrack" />
</ToolTipService.ToolTip>
</Button>
<!-- Volume -->
<Button Click="VolumeButton_Click" Style="{StaticResource GhostButtonStyle}">
<Grid>
@@ -402,7 +420,7 @@
Margin="0,-14,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Maximum="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DurationMs, Mode=OneWay, Converter={StaticResource MillisecondsToSecondsConverter}}"
Maximum="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DurationMs, Mode=OneWay, Converter={StaticResource MillisecondsToSecondsConverter}}"
Minimum="0"
Style="{StaticResource GhostSliderStyle}"
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}" />

View File

@@ -2,7 +2,7 @@ using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
@@ -30,6 +30,7 @@ public sealed partial class NowPlayingBar : UserControl,
public event EventHandler? SongInfoTapped;
public event EventHandler? TimeTapped;
public event EventHandler? PlayQueueButtonClick;
public bool ShowTime
{
@@ -64,6 +65,15 @@ public sealed partial class NowPlayingBar : UserControl,
set { SetValue(ShowPlaybackOrderButtonProperty, value); }
}
public static readonly DependencyProperty ShowStopButtonProperty =
DependencyProperty.Register(nameof(ShowStopButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool ShowStopButton
{
get { return (bool)GetValue(ShowStopButtonProperty); }
set { SetValue(ShowStopButtonProperty, value); }
}
public static readonly DependencyProperty ShowPlaybackOrderButtonProperty =
DependencyProperty.Register(nameof(ShowPlaybackOrderButton), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
@@ -76,15 +86,6 @@ public sealed partial class NowPlayingBar : UserControl,
public static readonly DependencyProperty PlaybackOrderProperty =
DependencyProperty.Register(nameof(PlaybackOrder), typeof(PlaybackOrder), typeof(NowPlayingBar), new PropertyMetadata(PlaybackOrder.RepeatAll));
public bool IsPlayingQueueOpened
{
get { return (bool)GetValue(IsPlayingQueueOpenedProperty); }
set { SetValue(IsPlayingQueueOpenedProperty, value); }
}
public static readonly DependencyProperty IsPlayingQueueOpenedProperty =
DependencyProperty.Register(nameof(IsPlayingQueueOpened), typeof(bool), typeof(NowPlayingBar), new PropertyMetadata(false));
public bool IsCompactMode
{
get { return (bool)GetValue(IsCompactModeProperty); }
@@ -206,7 +207,7 @@ public sealed partial class NowPlayingBar : UserControl,
var grid = (Grid)sender;
var pos = e.GetCurrentPoint(grid).Position;
var ratio = pos.X / grid.ActualWidth;
ViewModel.MediaSessionsService.ChangePosition(TimelineSlider.Maximum * ratio);
ViewModel.GSMTCService.ChangePosition(TimelineSlider.Maximum * ratio);
}
private void TimelineSliderOverlay_PointerMoved(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
@@ -303,7 +304,7 @@ public sealed partial class NowPlayingBar : UserControl,
private void PlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
IsPlayingQueueOpened = !IsPlayingQueueOpened;
PlayQueueButtonClick?.Invoke(sender, EventArgs.Empty);
}
private void PlaybackOrderButton_Click(object sender, RoutedEventArgs e)
@@ -313,9 +314,9 @@ public sealed partial class NowPlayingBar : UserControl,
public void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
if (message.PropertyName == nameof(IGSMTCService.CurrentSongInfo))
{
TitleTextBlock.Text = message.NewValue?.Title;
ArtistsTextBlock.Text = message.NewValue?.DisplayArtists;
@@ -324,9 +325,9 @@ public sealed partial class NowPlayingBar : UserControl,
}
public void Receive(PropertyChangedMessage<BitmapImage?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapImage))
if (message.PropertyName == nameof(IGSMTCService.AlbumArtBitmapImage))
{
AlbumArtImageSwitcher.Source = message.NewValue;
}
@@ -335,9 +336,9 @@ public sealed partial class NowPlayingBar : UserControl,
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition))
if (message.PropertyName == nameof(IGSMTCService.CurrentPosition))
{
TimelineSlider.Value = message.NewValue.TotalSeconds;
}

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.PlayQueue"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid>
<Grid.TranslationTransition>
<Vector3Transition />
</Grid.TranslationTransition>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="12,12,12,0">
<TextBlock
x:Uid="MusicGalleryPagePlayingQueue"
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}" />
</Grid>
<Grid Grid.Row="1" Margin="12,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex, Mode=OneWay, Converter={StaticResource IndexToDisplayConverter}}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="/" />
<TextBlock Text="{x:Bind ViewModel.SMTCService.TrackPlayingQueue.Count, Mode=OneWay}" />
</StackPanel>
<!-- Scroll to playing item -->
<Button
Grid.Column="1"
HorizontalAlignment="Right"
Click="ScrollToPlayingItemButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE7B7;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageScrollToPlayingItem" />
</ToolTipService.ToolTip>
</Button>
<!-- Empty play queue -->
<Button
Grid.Column="2"
HorizontalAlignment="Right"
Click="EmptyPlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageEmptyPlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</Grid>
<NavigationViewItemSeparator Grid.Row="2" />
<ListView
x:Name="PlayingQueueListView"
Grid.Row="3"
ItemsSource="{x:Bind ViewModel.SMTCService.TrackPlayingQueue, Mode=OneWay}"
SelectedIndex="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Padding="0,6">
<Grid Tapped="PlayingQueueListVireItemGrid_Tapped">
<StackPanel Margin="0,0,36,0">
<TextBlock Text="{Binding Track.Title}" TextWrapping="Wrap" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{Binding Track.Artist}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
<Grid HorizontalAlignment="Right">
<Button
VerticalAlignment="Center"
Click="RemoveFromPlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageRemoveFromPlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</Grid>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid Grid.Row="3">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SMTCService.TrackPlayingQueue.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SMTCService.TrackPlayingQueue.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<Image MaxWidth="100" Source="/Assets/EmptyBox.png" />
<TextBlock
x:Uid="MusicGalleryPagePlayingQueueEmpty"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,103 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class PlayQueue : UserControl, IRecipient<PropertyChangedMessage<int>>
{
public PlayQueueViewModel ViewModel => (PlayQueueViewModel)DataContext;
public PlayQueue()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<PlayQueueViewModel>();
WeakReferenceMessenger.Default.RegisterAll(this);
}
private void ScrollToPlayingItem()
{
if (PlayingQueueListView == null) return;
var targetItem = ViewModel.SMTCService.TrackPlayingQueue
.ElementAtOrDefault(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
if (targetItem == null) return;
PlayingQueueListView.ScrollIntoView(targetItem);
}
private void ScrollToPlayingItemButton_Click(object sender, RoutedEventArgs e)
{
ScrollToPlayingItem();
}
private async void PlayingQueueListVireItemGrid_Tapped(object sender, TappedRoutedEventArgs e)
{
var item = (PlayQueueItem)((FrameworkElement)sender).DataContext;
await ViewModel.SMTCService.PlayTrackAsync(item);
}
private async void RemoveFromPlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
bool playNext = false;
var item = (PlayQueueItem)((FrameworkElement)sender).DataContext;
int index = ViewModel.SMTCService.TrackPlayingQueue.IndexOf(item);
if (item == PlayingQueueListView.SelectedItem)
{
playNext = true;
}
ViewModel.SMTCService.TrackPlayingQueue.Remove(item);
if (playNext)
{
if (ViewModel.SMTCService.TrackPlayingQueue.Count == 0)
{
index = -1;
}
else if (index >= ViewModel.SMTCService.TrackPlayingQueue.Count)
{
index = ViewModel.SMTCService.TrackPlayingQueue.Count - 1;
}
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = index;
await ViewModel.SMTCService.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
}
}
private async void EmptyPlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
ViewModel.SMTCService.TrackPlayingQueue.Clear();
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = -1;
await ViewModel.SMTCService.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
}
public void Receive(PropertyChangedMessage<int> message)
{
if (message.Sender is MusicGallerySettings)
{
if (message.PropertyName == nameof(MusicGallerySettings.PlayQueueIndex))
{
ScrollToPlayingItem();
}
}
}
}
}

View File

@@ -314,8 +314,8 @@
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPagePlaybackSource" Value="{x:Bind ViewModel.MediaSessionsService.CurrentMediaSourceProviderInfo.DisplayName, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPagePlaybackSourceID" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.PlayerId, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPagePlaybackSource" Value="{x:Bind ViewModel.GSMTCService.CurrentMediaSourceProviderInfo.DisplayName, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPagePlaybackSourceID" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.PlayerId, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel>
</Expander>
@@ -325,10 +325,10 @@
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.MediaSessionsService.CurrentSongInfo.DurationMs, TargetNullValue=N/A, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DurationMs, TargetNullValue=N/A, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" />
</StackPanel>
</Expander>
@@ -338,26 +338,26 @@
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsPageLanguageCode" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsData.LanguageCode, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsPageLanguageCode" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsData.LanguageCode, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
<local:PropertyRow
x:Uid="LyricsPageLyricsProviderPrefix"
Link="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.Reference, TargetNullValue=N/A, Mode=OneWay}"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.ProviderIfFound, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTransliterationProviderPrefix" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.TransliterationProvider, Mode=OneWay, Converter={StaticResource TransliterationSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTranslationProviderPrefix" Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.TranslationProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}" />
Link="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Reference, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}"
ToolTipService.ToolTip="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Reference, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}"
Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.ProviderIfFound, Mode=OneWay, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTransliterationProviderPrefix" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.TransliterationProvider, Mode=OneWay, Converter={StaticResource TransliterationSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow x:Uid="LyricsPageTranslationProviderPrefix" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.TranslationProvider, Mode=OneWay, Converter={StaticResource TranslationSearchProviderToDisplayNameConverter}}" />
<local:PropertyRow
x:Uid="LyricsPageMatchPercentage"
Unit="%"
Value="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsPageCachePath"
Link="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind ViewModel.MediaSessionsService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay}" />
Link="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}"
ToolTipService.ToolTip="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}" />
</StackPanel>
</Expander>

View File

@@ -3,9 +3,12 @@
x:Class="BetterLyrics.WinUI3.Controls.StatsDashboardControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:BetterLyrics.WinUI3.Converter"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dev="using:DevWinUI"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.WinUI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:statsmodels="using:BetterLyrics.WinUI3.Models.Stats"
@@ -14,7 +17,7 @@
<UserControl.Resources>
<Style x:Key="StatsCardStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource LayerFillColorDefaultBrush}" />
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="8" />
@@ -23,38 +26,65 @@
</Style>
</UserControl.Resources>
<Grid Margin="0,24,0,0">
<Grid Margin="0,20,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="20,0">
<Pivot x:Name="TimeRangePivot" SelectionChanged="Pivot_SelectionChanged">
<PivotItem x:Uid="StatsDashboardControlToday" Tag="Day" />
<PivotItem x:Uid="StatsDashboardControlThisWeek" Tag="Week" />
<PivotItem x:Uid="StatsDashboardControlThisMonth" Tag="Month" />
<PivotItem x:Uid="StatsDashboardControlThisQuarter" Tag="Quarter" />
<PivotItem x:Uid="StatsDashboardControlThisYear" Tag="Year" />
</Pivot>
<Grid Grid.Row="0" Margin="36,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<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>
<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>
<ScrollViewer Grid.Row="1" Padding="20,0">
<ScrollViewer Grid.Row="1" Padding="36,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,20,0,0">
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 总播放时长 -->
<Border Grid.Column="0" Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
@@ -71,7 +101,7 @@
<TextBlock
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind ViewModel.TotalDuration.TotalHours, Mode=OneWay}" />
Text="{x:Bind ViewModel.TotalDuration.TotalHours, Mode=OneWay, Converter={StaticResource DoubleToDecimalConverter}}" />
<TextBlock
Margin="0,0,0,2"
VerticalAlignment="Bottom"
@@ -83,7 +113,6 @@
</StackPanel>
</Border>
<!-- 总播放歌曲次数 -->
<Border Grid.Column="1" Style="{StaticResource StatsCardStyle}">
<StackPanel>
<StackPanel
@@ -100,7 +129,6 @@
</StackPanel>
</Border>
<!-- Top source -->
<Border
Grid.Column="2"
Margin="0,0,0,12"
@@ -121,25 +149,99 @@
</Border>
</Grid>
<Grid Grid.Row="1">
<!-- Activity by hour -->
<Border
Grid.Row="1"
Margin="0,0,0,12"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel
Margin="0,0,0,12"
Orientation="Horizontal"
Spacing="8">
<TextBlock x:Uid="StatsDashboardControlActivityByHour" Style="{ThemeResource SubtitleTextBlockStyle}" />
</StackPanel>
<Grid Grid.Row="1" Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1.5*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock
x:Uid="StatsDashboardControlMostActive"
FontSize="12"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" />
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.PeakHourText, Mode=OneWay}" />
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock
x:Uid="StatsDashboardControlLeastActive"
FontSize="12"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" />
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.QuietHourText, Mode=OneWay}" />
</StackPanel>
</Grid>
<lvc:CartesianChart
Grid.Row="2"
Height="180"
Margin="0,8,0,0"
Background="Transparent"
TooltipPosition="Top">
<lvc:CartesianChart.XAxes>
<lvc:AxesCollection>
<lvc:XamlAxis Labels="{x:Bind ViewModel.HourlyXAxisLabels, Mode=OneWay}" TextSize="{StaticResource BodyTextBlockFontSize}" />
</lvc:AxesCollection>
</lvc:CartesianChart.XAxes>
<lvc:CartesianChart.YAxes>
<lvc:AxesCollection>
<lvc:XamlAxis
x:Uid="StatsDashboardControlTrackCountAxis"
NameTextSize="{StaticResource BodyTextBlockFontSize}"
ShowSeparatorLines="False"
TextSize="{StaticResource BodyTextBlockFontSize}" />
</lvc:AxesCollection>
</lvc:CartesianChart.YAxes>
<lvc:CartesianChart.Series>
<lvc:SeriesCollection>
<lvc:XamlColumnSeries
x:Name="HourlySeries"
Rx="4"
Ry="4"
Values="{x:Bind ViewModel.HourlySeriesValues, Mode=OneWay}" />
</lvc:SeriesCollection>
</lvc:CartesianChart.Series>
</lvc:CartesianChart>
</Grid>
</Border>
<!-- Top artists and sources -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Top artists -->
<Border Grid.Column="0" Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel>
<TextBlock
x:Uid="StatsDashboardControlTopArtists"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind ViewModel.TopArtists, Mode=OneWay}">
<ItemsControl ItemsSource="{x:Bind ViewModel.TopArtists, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="statsmodels:ArtistPlayCount">
<Grid Margin="0,4">
@@ -163,85 +265,31 @@
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
x:Uid="StatsDashboardControlTrackCountText"
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</StackPanel>
</Border>
<!-- Top sources -->
<!-- Top tracks -->
<Border
Grid.Column="1"
Margin="0,0,0,12"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="StatsDashboardControlSources"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ItemsControl Grid.Row="1" ItemsSource="{x:Bind ViewModel.PlayerStats, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="models:PlayerStatDisplayItem">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
FontSize="13"
Style="{ThemeResource BodyTextBlockStyle}"
Text="{x:Bind PlayerName}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
</Grid>
<!-- Top song -->
<Border
Grid.Row="2"
Margin="0,0,0,20"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel>
<TextBlock
x:Uid="StatsDashboardControlTopSongs"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ListView
Grid.Row="1"
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
ItemsSource="{x:Bind ViewModel.TopSongs, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemTemplate>
<ItemsControl ItemsSource="{x:Bind ViewModel.TopSongs, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="statsmodels:SongPlayCount">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
@@ -275,15 +323,44 @@
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
x:Uid="StatsDashboardControlTrackCountText"
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Text="plays" />
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" />
</TextBlock>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</Grid>
<!-- 播放源分布 -->
<Border
Grid.Row="3"
Margin="0,0,0,20"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="StatsDashboardControlSources"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<lvc:PieChart
Grid.Row="1"
MinHeight="250"
Background="Transparent"
LegendPosition="Bottom"
LegendTextSize="{StaticResource BodyTextBlockFontSize}"
Series="{x:Bind ViewModel.SourceSeries, Mode=OneWay}"
TooltipPosition="Center" />
</Grid>
</Border>
@@ -295,8 +372,6 @@
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.GenerateTestDataCommand}"
Content="Generate test data"
Visibility="Collapsed" />
Content="Generate test data" />
</Grid>
</UserControl>

View File

@@ -29,30 +29,6 @@ public sealed partial class StatsDashboardControl : UserControl
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<StatsDashboardControlViewModel>();
this.Loaded += StatsDashboardControl_Loaded;
}
private async void StatsDashboardControl_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
await ViewModel.LoadDataAsync(StatsRange.Day);
}
private async void Pivot_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ViewModel == null) return;
if (TimeRangePivot.SelectedItem is PivotItem item && item.Tag is string tag)
{
var range = tag switch
{
"Day" => StatsRange.Day,
"Week" => StatsRange.Week,
"Month" => StatsRange.Month,
"Quarter" => StatsRange.Quarter,
"Year" => StatsRange.Year,
_ => StatsRange.Day
};
await ViewModel.LoadDataAsync(range);
}
}
}

View File

@@ -0,0 +1,33 @@
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Converter
{
public partial class DoubleToDecimalConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value == null) return string.Empty;
if (double.TryParse(value.ToString(), out double number))
{
int decimalPlaces = 2;
if (parameter != null && int.TryParse(parameter.ToString(), out int parsedParams))
{
decimalPlaces = parsedParams;
}
return number.ToString($"F{decimalPlaces}");
}
return value.ToString() ?? "";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,25 @@
using BetterLyrics.WinUI3.Extensions;
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Converter
{
public partial class UriStringToDecodedAbsoluteUri : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is string uriString)
{
return uriString.ToDecodedAbsoluteUri();
}
return "";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -6,10 +6,11 @@ namespace BetterLyrics.WinUI3.Enums
{
public enum StatsRange
{
Day,
Week,
Month,
Quarter,
Year
Today,
ThisWeek,
ThisMonth,
ThisQuarter,
ThisYear,
Custom
}
}

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Enums;
using System;
using System.Linq;
namespace BetterLyrics.WinUI3.Extensions
@@ -75,6 +76,18 @@ namespace BetterLyrics.WinUI3.Extensions
return null;
}
}
public string ToDecodedAbsoluteUri()
{
if (string.IsNullOrEmpty(str)) return "";
try
{
var u = new Uri(str);
return u.IsFile ? u.LocalPath : System.Net.WebUtility.UrlDecode(u.AbsoluteUri);
}
catch { return str; }
}
}
}
}

View File

@@ -4,6 +4,7 @@ using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Hooks;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
@@ -15,6 +16,16 @@ namespace BetterLyrics.WinUI3.Helper
{
public static class ColorHelper
{
public static Color GetSystemAccentColor()
{
if (Application.Current.Resources.TryGetValue("SystemAccentColor", out var resource) &&
resource is Color uiColor)
{
return uiColor;
}
return Color.FromArgb(255, 0, 120, 215);
}
public static ElementTheme GetElementThemeFromBackgroundColor(Color backgroundColor)
{
// 计算亮度YIQ公式

View File

@@ -1,4 +1,5 @@
using ColorThiefDotNet;
using CommunityToolkit.WinUI.Helpers;
using Impressionist.Abstractions;
using Impressionist.Implementations;
using System;
@@ -50,7 +51,29 @@ namespace BetterLyrics.WinUI3.Helper
return paletteResult;
}
public static async Task<Dictionary<Vector3, int>> GetPixelColor(BitmapDecoder bitmapDecoder)
public static List<Windows.UI.Color> GenerateChartColors(Windows.UI.Color baseColor, int count)
{
List<Windows.UI.Color> results = [];
var baseHsl = baseColor.ToHsl();
double baseHue = baseHsl.H;
double baseSaturation = baseHsl.S;
double baseBrightness = baseHsl.L;
double step = 360.0 / count;
for (int i = 0; i < count; i++)
{
double newHue = (baseHue + (step * i)) % 360;
Windows.UI.Color newColor = CommunityToolkit.WinUI.Helpers.ColorHelper.FromHsl(newHue, baseSaturation, baseBrightness);
results.Add(newColor);
}
return results;
}
private static async Task<Dictionary<Vector3, int>> GetPixelColor(BitmapDecoder bitmapDecoder)
{
var pixelDataProvider = await bitmapDecoder.GetPixelDataAsync();
var pixels = pixelDataProvider.DetachPixelData();

View File

@@ -81,5 +81,6 @@ namespace BetterLyrics.WinUI3.Helper
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
Directory.CreateDirectory(LocalAlbumArtCacheDirectory);
}
}
}

View File

@@ -10,20 +10,6 @@ namespace BetterLyrics.WinUI3.Models
{
public string Uri { get; private set; } = "";
public string DecodedAbsoluteUri
{
get
{
if (string.IsNullOrEmpty(Uri)) return "";
try
{
var u = new Uri(Uri);
return u.IsFile ? u.LocalPath : System.Net.WebUtility.UrlDecode(u.AbsoluteUri);
}
catch { return Uri; }
}
}
public string? RawLyrics { get; set; }
public string? LocalAlbumArtPath { get; set; }
public byte[]? AlbumArtByteArray { get; set; }
@@ -119,14 +105,13 @@ namespace BetterLyrics.WinUI3.Models
public ExtendedTrack() : base() { }
public ExtendedTrack(string uriString) : base()
public ExtendedTrack(string decodedUriString) : base()
{
Uri = uriString;
string atlPath = uriString;
string atlPath = decodedUriString;
try
{
var u = new Uri(uriString);
var u = new Uri(decodedUriString);
Uri = u.AbsoluteUri;
if (u.IsFile) atlPath = u.LocalPath;
}
catch { }

View File

@@ -1,5 +1,6 @@
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.ObjectModel;
namespace BetterLyrics.WinUI3.Models.Settings

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Stats
{
public class HourlyStatBar
{
public int Hour { get; set; }
public double NormalizedHeight { get; set; } // 0 - 100用于UI高度
public int RawCount { get; set; } // 实际播放数
public string Label { get; set; } // Tooltip: "09:00 - 15 plays"
}
}

View File

@@ -39,6 +39,8 @@ namespace BetterLyrics.WinUI3.Renderer
var blur = line.BlurAmountTransition.Value;
var bounds = line.PhoneticCanvasTextLayout.LayoutBounds;
if (double.IsNaN(opacity)) return;
var destRect = new Rect(
bounds.X + line.PhoneticPosition.X,
bounds.Y + line.PhoneticPosition.Y,
@@ -71,6 +73,8 @@ namespace BetterLyrics.WinUI3.Renderer
var blur = line.BlurAmountTransition.Value;
var bounds = line.TranslatedCanvasTextLayout.LayoutBounds;
if (double.IsNaN(opacity)) return;
var destRect = new Rect(
bounds.X + line.TranslatedPosition.X,
bounds.Y + line.TranslatedPosition.Y,

View File

@@ -31,8 +31,6 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
private readonly IDbContextFactory<FilesIndexDbContext> _contextFactory;
private bool _isInitialized = false;
// 定时器字典
private readonly ConcurrentDictionary<string, CancellationTokenSource> _folderTimerTokens = new();
// 当前正在执行的扫描任务字典
@@ -460,6 +458,17 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
}
}
public async Task<List<FilesIndexItem>> GetParsedFilesAsync()
{
using var context = await _contextFactory.CreateDbContextAsync();
// SQL: SELECT * FROM FileCache WHERE IsMetadataParsed = 1 AND MediaFolderId IN (...)
return await context.FilesIndex
.AsNoTracking()
.Where(x => x.IsMetadataParsed)
.ToListAsync();
}
public async Task<List<FilesIndexItem>> GetParsedFilesAsync(IEnumerable<string> enabledConfigIds)
{
if (enabledConfigIds == null || !enabledConfigIds.Any())

View File

@@ -50,7 +50,13 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
Task ScanMediaFolderAsync(MediaFolder folder, CancellationToken token = default);
/// <summary>
/// 从数据库拉取
/// 从数据库拉取全部已解析的数据
/// </summary>
/// <returns></returns>
Task<List<FilesIndexItem>> GetParsedFilesAsync();
/// <summary>
/// 从数据库拉取全部已解析的且其所属的 MediaFolder 在应用内处于开启状态的数据
/// </summary>
/// <param name="enabledConfigIds"></param>
/// <returns></returns>

View File

@@ -17,9 +17,9 @@ using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Windows.UI;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
namespace BetterLyrics.WinUI3.Services.GSMTCService
{
public partial class MediaSessionsService : IMediaSessionsService
public partial class GSMTCService : IGSMTCService
{
private readonly LatestOnlyTaskRunner _albumArtRefreshRunner = new();

View File

@@ -7,9 +7,9 @@ using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
namespace BetterLyrics.WinUI3.Services.GSMTCService
{
public partial class MediaSessionsService : IMediaSessionsService
public partial class GSMTCService : IGSMTCService
{
private LatestOnlyTaskRunner _refreshLyricsRunner = new();

View File

@@ -38,9 +38,9 @@ using Windows.Media.Control;
using Windows.Storage.Streams;
using WindowsMediaController;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
namespace BetterLyrics.WinUI3.Services.GSMTCService
{
public partial class MediaSessionsService : BaseViewModel, IMediaSessionsService,
public partial class GSMTCService : BaseViewModel, IGSMTCService,
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<string>>,
IRecipient<PropertyChangedMessage<ChineseRomanization>>,
@@ -58,7 +58,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private readonly IDiscordService _discordService;
private readonly IPlayHistoryService _playHistoryService;
private readonly ILastFMService _lastFMService;
private readonly ILogger<MediaSessionsService> _logger;
private readonly ILogger<GSMTCService> _logger;
private double _lxMusicPositionSeconds = 0;
private byte[]? _lxMusicAlbumArtBytes = null;
@@ -73,7 +73,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
[ObservableProperty] public partial MediaSourceProviderInfo? CurrentMediaSourceProviderInfo { get; set; }
public MediaSessionsService(
public GSMTCService(
ISettingsService settingsService,
IAlbumArtSearchService albumArtSearchService,
ILyricsSearchService lyricsSearchService,
@@ -82,7 +82,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
ITransliterationService transliterationService,
IPlayHistoryService playHistoryService,
ILastFMService lastFMService,
ILogger<MediaSessionsService> logger)
ILogger<GSMTCService> logger)
{
_settingsService = settingsService;
_albumArtSearchService = albumArtSearchService;
@@ -198,11 +198,12 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private void MediaManager_OnAnyTimelinePropertyChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionTimelineProperties? timelineProperties)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
_dispatcherQueue.TryEnqueue(() =>
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
{
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
return;
}
@@ -213,6 +214,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
if (!IsMediaSourceEnabled(mediaSession.Id))
{
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
}
else
@@ -228,7 +230,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private void MediaManager_OnAnyPlaybackStateChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionPlaybackInfo? playbackInfo)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, (() =>
_dispatcherQueue.TryEnqueue(() =>
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
@@ -239,7 +241,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
var desiredSession = GetCurrentSession();
//RecordMediaSourceProviderInfo(mediaSession);
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
@@ -254,14 +255,23 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_ => false,
};
}
}));
if (CurrentIsPlaying)
{
_scrobbleStopwatch.Start();
}
else
{
_scrobbleStopwatch.Stop();
}
});
}
private void MediaManager_OnAnyMediaPropertyChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProperties)
{
_onMediaPropsChangedTimer?.Debounce(() =>
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
_dispatcherQueue.TryEnqueue(async () =>
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
@@ -323,7 +333,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
.Replace(ExtendedGenreFiled.FileName, "");
// 写入播放记录
if (CurrentSongInfo != null && CurrentSongInfo.Title != "N/A")
if (CurrentSongInfo != null && !string.IsNullOrWhiteSpace(CurrentSongInfo.Title) && CurrentSongInfo.Title != "N/A")
{
// 必须捕获一个副本给异步任务,因为 CurrentSongInfo 马上就要变了
var lastSong = CurrentSongInfo;
@@ -409,13 +419,13 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
}
}
private void MediaManager_OnAnySessionOpened(MediaManager.MediaSession mediaSession)
private async void MediaManager_OnAnySessionOpened(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
RecordMediaSourceProviderInfo(mediaSession);
SendFocusedMessagesAsync().ConfigureAwait(false);
await SendFocusedMessagesAsync();
}
private MediaManager.MediaSession? GetCurrentSession()
@@ -469,6 +479,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
CurrentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
_discordService.Disable();

View File

@@ -8,9 +8,12 @@ using System.Threading.Tasks;
using Windows.Storage.Streams;
using Windows.UI;
namespace BetterLyrics.WinUI3.Services.MediaSessionsService
namespace BetterLyrics.WinUI3.Services.GSMTCService
{
public interface IMediaSessionsService : INotifyPropertyChanged
/// <summary>
/// Interface for GlobalSystemMediaTransportControlsSession Service
/// </summary>
public interface IGSMTCService : INotifyPropertyChanged
{
Task PlayAsync();
Task PauseAsync();

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Db;
using BetterLyrics.WinUI3.Models.Stats;
using Microsoft.EntityFrameworkCore;
@@ -99,7 +100,7 @@ namespace BetterLyrics.WinUI3.Services.PlayHistoryService
var totalMs = await context.PlayHistory
.Where(x => x.StartedAt >= start && x.StartedAt <= end)
.SumAsync(x => x.DurationPlayedMs); // 直接在数据库层面求和
.SumAsync(x => Math.Min(x.DurationPlayedMs, x.TotalDurationMs)); // 防止超过歌曲本身时长
return TimeSpan.FromMilliseconds(totalMs);
}
@@ -197,7 +198,14 @@ namespace BetterLyrics.WinUI3.Services.PlayHistoryService
("Summer", "Calvin Harris", "Motion"),
};
var playerIds = new[] { "Spotify", "Spotify", "Spotify", "MusicBee", "MusicBee", "QQMusic", "NeteaseCloudMusic", "AppleMusic" };
var playerIds = new[]
{
PlayerId.Spotify, PlayerId.Spotify, PlayerId.Spotify,
PlayerId.MusicBee, PlayerId.MusicBee,
PlayerId.QQMusic,
PlayerId.NetEaseCloudMusic,
PlayerId.AppleMusic,
};
var batchList = new List<PlayHistoryItem>();

View File

@@ -0,0 +1,21 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.SMTCService
{
/// <summary>
/// Interface for SystemMediaTransportControlsSession Service
/// </summary>
public interface ISMTCService
{
public ObservableCollection<PlayQueueItem> TrackPlayingQueue { get; set; }
public ExtendedTrack? PlayingTrack { get; set; }
Task PlayTrackAsync(PlayQueueItem? playQueueItem);
Task PlayTrackAtAsync(int index);
}
}

View File

@@ -0,0 +1,330 @@
using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Media;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Services.SMTCService
{
public partial class SMTCService : BaseViewModel, ISMTCService
{
private readonly MediaPlayer _mediaPlayer;
private readonly MediaTimelineController _timelineController;
private readonly SystemMediaTransportControls _smtc;
private IRandomAccessStream? _currentStream;
private Stream? _currentNetStream;
private IUnifiedFileSystem? _currentProvider;
private readonly ISettingsService _settingsService;
private readonly IFileSystemService _fileSystemService;
[ObservableProperty] public partial ObservableCollection<PlayQueueItem> TrackPlayingQueue { get; set; } = [];
[ObservableProperty] public partial ExtendedTrack? PlayingTrack { get; set; }
public SMTCService(ISettingsService settingsService, IFileSystemService fileSystemService)
{
_settingsService = settingsService;
_fileSystemService = fileSystemService;
_mediaPlayer = new MediaPlayer();
_mediaPlayer.MediaOpened += MediaPlayer_MediaOpened;
_mediaPlayer.MediaEnded += MediaPlayer_MediaEnded;
_mediaPlayer.CommandManager.IsEnabled = false;
_timelineController = _mediaPlayer.TimelineController = new();
_timelineController.PositionChanged += TimelineController_PositionChanged;
_smtc = _mediaPlayer.SystemMediaTransportControls;
_smtc.IsPlayEnabled = true;
_smtc.IsPauseEnabled = true;
_smtc.IsNextEnabled = true;
_smtc.IsPreviousEnabled = true;
_smtc.ButtonPressed += Smtc_ButtonPressed;
_smtc.PlaybackPositionChangeRequested += Smtc_PlaybackPositionChangeRequested;
_ = Task.Run(async () =>
{
var parsedFiles = await _fileSystemService.GetParsedFilesAsync();
var playQueue = _settingsService.AppSettings.MusicGallerySettings.PlayQueuePaths
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(x =>
{
var encodedUri = new Uri(x).AbsoluteUri;
return new PlayQueueItem(new ExtendedTrack(parsedFiles.FirstOrDefault(y => y.Uri == encodedUri)));
});
_dispatcherQueue.TryEnqueue(() =>
{
TrackPlayingQueue = [.. playQueue];
TrackPlayingQueue.CollectionChanged += TrackPlayingQueue_CollectionChanged;
});
});
}
private void Smtc_ButtonPressed(SystemMediaTransportControls sender, SystemMediaTransportControlsButtonPressedEventArgs args)
{
switch (args.Button)
{
case SystemMediaTransportControlsButton.Play:
_smtc.PlaybackStatus = MediaPlaybackStatus.Playing;
_timelineController.Resume();
break;
case SystemMediaTransportControlsButton.Pause:
_smtc.PlaybackStatus = MediaPlaybackStatus.Paused;
_timelineController.Pause();
break;
case SystemMediaTransportControlsButton.Next:
PlayNextTrack();
break;
case SystemMediaTransportControlsButton.Previous:
PlayPreviousTrack();
break;
}
}
private void Smtc_PlaybackPositionChangeRequested(SystemMediaTransportControls sender, PlaybackPositionChangeRequestedEventArgs args)
{
_timelineController.Position = args.RequestedPlaybackPosition;
}
private void MediaPlayer_MediaOpened(MediaPlayer sender, object args)
{
_timelineController.Start();
_smtc.PlaybackStatus = MediaPlaybackStatus.Playing;
}
private void MediaPlayer_MediaEnded(MediaPlayer sender, object args)
{
PlayNextTrack();
}
private void TimelineController_PositionChanged(MediaTimelineController sender, object args)
{
_smtc.UpdateTimelineProperties(new SystemMediaTransportControlsTimelineProperties()
{
Position = sender.Position,
EndTime = _mediaPlayer.PlaybackSession.NaturalDuration
});
}
private void TrackPlayingQueue_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
_settingsService.AppSettings.MusicGallerySettings.PlayQueuePaths = [.. TrackPlayingQueue.Select(x => x.Track.Uri.ToDecodedAbsoluteUri())];
}
private string GetMimeType(string path)
{
var ext = Path.GetExtension(path).ToLower();
return ext switch
{
".mp3" => "audio/mpeg",
".flac" => "audio/flac",
".wav" => "audio/wav",
".m4a" => "audio/mp4",
".aac" => "audio/aac",
".ogg" => "audio/ogg",
".wma" => "audio/x-ms-wma",
_ => "application/octet-stream"
};
}
private void PlayNextTrack()
{
var musicGallerySettings = _settingsService.AppSettings.MusicGallerySettings;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
switch (musicGallerySettings.PlaybackOrder)
{
case PlaybackOrder.RepeatAll:
if (musicGallerySettings.PlayQueueIndex < TrackPlayingQueue.Count - 1)
{
musicGallerySettings.PlayQueueIndex++;
}
else
{
musicGallerySettings.PlayQueueIndex = 0;
}
break;
case PlaybackOrder.RepeatOne:
//_timelineController.Position = TimeSpan.Zero;
break;
case PlaybackOrder.Shuffle:
if (TrackPlayingQueue.Count > 0)
{
musicGallerySettings.PlayQueueIndex = new Random().Next(0, TrackPlayingQueue.Count);
}
break;
default:
break;
}
await PlayTrackAtAsync(musicGallerySettings.PlayQueueIndex);
});
}
private void PlayPreviousTrack()
{
var musicGallerySettings = _settingsService.AppSettings.MusicGallerySettings;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
switch (musicGallerySettings.PlaybackOrder)
{
case PlaybackOrder.RepeatAll:
if (musicGallerySettings.PlayQueueIndex > 0)
{
musicGallerySettings.PlayQueueIndex--;
}
else
{
musicGallerySettings.PlayQueueIndex = TrackPlayingQueue.Count - 1;
}
break;
case PlaybackOrder.RepeatOne:
//_timelineController.Position = TimeSpan.Zero;
break;
case PlaybackOrder.Shuffle:
if (TrackPlayingQueue.Count > 0)
{
musicGallerySettings.PlayQueueIndex = new Random().Next(0, TrackPlayingQueue.Count);
}
break;
default:
break;
}
await PlayTrackAtAsync(musicGallerySettings.PlayQueueIndex);
});
}
public async Task PlayTrackAsync(PlayQueueItem? playQueueItem)
{
_timelineController.Pause();
_mediaPlayer.Source = null;
// 清理旧资源
_currentStream?.Dispose();
_currentNetStream?.Dispose();
_currentStream = null;
_currentNetStream = null;
if (playQueueItem == null)
{
_smtc.IsEnabled = false;
_smtc.DisplayUpdater.ClearAll();
}
else
{
PlayingTrack = playQueueItem.Track;
_smtc.IsEnabled = true;
try
{
var targetFolder = _settingsService.AppSettings.LocalMediaFolders.FirstOrDefault(f =>
{
var fUri = f.GetStandardUri().AbsoluteUri;
return PlayingTrack.Uri.StartsWith(fUri, StringComparison.OrdinalIgnoreCase);
});
if (targetFolder == null)
{
throw new FileNotFoundException(null, PlayingTrack.Uri.ToDecodedAbsoluteUri());
}
_currentProvider = targetFolder.CreateFileSystem();
if (_currentProvider == null) return;
await _currentProvider.ConnectAsync();
var fileCacheStub = new FilesIndexItem
{
Uri = PlayingTrack.Uri
};
var sourceStream = await _fileSystemService.OpenFileAsync(_currentProvider, fileCacheStub);
if (sourceStream == null)
{
throw new FileNotFoundException(null, fileCacheStub.Uri);
}
if (sourceStream.CanSeek)
{
_currentNetStream = sourceStream;
}
else
{
var memStream = new MemoryStream();
await sourceStream.CopyToAsync(memStream);
memStream.Position = 0;
sourceStream.Dispose();
_currentNetStream = memStream;
}
_currentStream = _currentNetStream.AsRandomAccessStream();
string contentType = GetMimeType(PlayingTrack.FileName);
var mediaSource = MediaSource.CreateFromStream(_currentStream, contentType);
_mediaPlayer.Source = mediaSource;
var updater = _smtc.DisplayUpdater;
updater.Type = MediaPlaybackType.Music;
updater.MusicProperties.Title = PlayingTrack.Title ?? PlayingTrack.FileName;
updater.MusicProperties.Artist = PlayingTrack.Artist ?? "";
updater.MusicProperties.AlbumTitle = PlayingTrack.Album ?? "";
updater.MusicProperties.Genres.Clear();
updater.MusicProperties.Genres.Add($"{ExtendedGenreFiled.FileName}{Path.GetFileNameWithoutExtension(PlayingTrack.FileName)}");
updater.AppMediaId = Package.Current.Id.FullName;
if (!string.IsNullOrEmpty(PlayingTrack.LocalAlbumArtPath) && File.Exists(PlayingTrack.LocalAlbumArtPath))
{
var storageFile = await StorageFile.GetFileFromPathAsync(PlayingTrack.LocalAlbumArtPath);
updater.Thumbnail = RandomAccessStreamReference.CreateFromFile(storageFile);
}
else
{
updater.Thumbnail = null;
}
updater.Update();
}
catch (Exception ex)
{
ToastHelper.ShowToast("Error", ex.Message, InfoBarSeverity.Error);
_timelineController.Pause();
}
}
}
public async Task PlayTrackAtAsync(int index)
{
await PlayTrackAsync(TrackPlayingQueue.ElementAtOrDefault(index));
}
}
}

View File

@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>الوضع القياسي</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>النشاط بالساعة</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>مخصص</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>النهاية</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>الأقل نشاطاً</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>الأكثر نشاطاً</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>المصادر</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>ابدأ</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>هذا الشهر</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>هذا الربع</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>هذا الأسبوع</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>هذا العام</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>النطاق الزمني</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>التايمز</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>اليوم</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1497,6 +1521,12 @@
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>المدة الإجمالية</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>التايمز</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>التايمز</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>المسارات التي تم تشغيلها</value>
</data>

View File

@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>Standard-Modus</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Aktivität nach Stunden</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Benutzerdefiniert</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>Ende</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Am wenigsten aktiv</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Aktivste</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Quellen</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Start</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>Dieser Monat</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>Dieses Quartal</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>Diese Woche</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>Dieses Jahr</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Zeitspanne</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Zeiten</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Heute</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1497,6 +1521,12 @@
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Gesamtdauer</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Zeiten</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Zeiten</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Gespielte Tracks</value>
</data>

View File

@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>Standard Mode</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Activity by Hour</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Custom</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>End</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Least Active</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Most Active</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sources</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Start</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>This Month</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>This Quarter</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>This Week</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>This Year</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Time Range</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Today</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1497,6 +1521,12 @@
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Total Duration</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Tracks Played</value>
</data>

View File

@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>Modo Estándar</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Actividad por horas</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>A medida</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>Fin</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Menos activo</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Más activos</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Fuentes</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Inicio</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>Este mes</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>Este trimestre</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>Esta semana</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>Este año</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Intervalo de tiempo</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Hoy</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1497,6 +1521,12 @@
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Duración total</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Pistas reproducidas</value>
</data>

View File

@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>Mode Standard</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Activité par heure</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Sur mesure</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>Fin</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Le moins actif</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Les plus actifs</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sources d'information</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Démarrage</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>Ce mois-ci</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>Ce trimestre</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>Cette semaine</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>Cette année</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Plage de temps</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Temps</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Aujourd'hui</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1497,6 +1521,12 @@
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Durée totale</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Temps</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Temps</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Pistes jouées</value>
</data>

View File

@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>मानक मोड</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>घंटे के हिसाब से गतिविधि</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>कस्टम</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>समाप्त</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>सबसे कम सक्रिय</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>सर्वाधिक सक्रिय</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>स्रोत</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>शुरू करें</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>इस महीने</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>इस तिमाही</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>इस सप्ताह</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>इस वर्ष</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>समय सीमा</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>दूसरे दर्जे का</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>आज</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1497,6 +1521,12 @@
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>कुल अवधि</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>दूसरे दर्जे का</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>दूसरे दर्जे का</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>बजाए गए ट्रैक</value>
</data>

View File

@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>Mode Standar</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Aktivitas per Jam</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Kustom</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>Akhir</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Paling Tidak Aktif</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Paling Aktif</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sumber</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Mulai</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>Bulan ini</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>Kuartal ini</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>Minggu Ini</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>Tahun ini</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Rentang Waktu</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Waktu</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Hari ini</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1497,6 +1521,12 @@
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Total Durasi</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Waktu</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Waktu</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Lagu yang Dimainkan</value>
</data>

View File

@@ -169,7 +169,7 @@
<value>LX Music サーバーに接続できません。「設定」-「再生ソース」-「LX Music」-「LX Music サーバー」に移動し、リンクが正しく入力されているか確認してください</value>
</data>
<data name="FileSystemServiceCleaningCache" xml:space="preserve">
<value>キャッシュクリーニング中...</value>
<value>キャッシュクリ中...</value>
</data>
<data name="FileSystemServiceConnectFailed" xml:space="preserve">
<value>接続に失敗しました</value>
@@ -181,19 +181,19 @@
<value>ファイルリストを取得しています...</value>
</data>
<data name="FileSystemServiceParsing" xml:space="preserve">
<value>Parsing...</value>
<value>解析中...</value>
</data>
<data name="FileSystemServicePrepareToClean" xml:space="preserve">
<value>キャッシュクリーン準備中</value>
<value>キャッシュクリ準備中...</value>
</data>
<data name="FileSystemServiceReady" xml:space="preserve">
<value>準備完了</value>
</data>
<data name="FileSystemServiceRootDirectoryWarning" xml:space="preserve">
<value>ルートディレクトリのパスが検出された。フルディスクインデックスにはメディア以外のファイルが多数含まれている可能性があり、スキャンに時間がかかりすぎる。特定のサブディレクトリを指定することを推奨する。</value>
<value>ルートディレクトリが指定されました。フルディスクインデックス作成には大量の非メディアファイルが含まれる可能性があり、スキャンに時間がかかる恐れがあります。特定のサブディレクトリを指定することをお勧めします。</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>スキャンを待っています...</value>
<value>スキャン準備中...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>全画面モード</value>
@@ -259,13 +259,13 @@
<value>歌詞の言語</value>
</data>
<data name="LyricsPageLyricsProviderPrefix.Header" xml:space="preserve">
<value>歌詞提供</value>
<value>歌詞提供</value>
</data>
<data name="LyricsPageLyricsSearch.Text" xml:space="preserve">
<value>歌詞検索</value>
</data>
<data name="LyricsPageLyricsSettings.Text" xml:space="preserve">
<value>歌詞ウィンドウのショートカット</value>
<value>歌詞ウィンドウのショートカット設定</value>
</data>
<data name="LyricsPageMatchPercentage.Header" xml:space="preserve">
<value>一致率</value>
@@ -289,7 +289,7 @@
<value>翻訳提供元</value>
</data>
<data name="LyricsPageTransliterationProviderPrefix.Header" xml:space="preserve">
<value>翻訳提供元</value>
<value>ルビ提供元</value>
</data>
<data name="LyricsParseError" xml:space="preserve">
<value>歌詞の解析に失敗しました</value>
@@ -307,7 +307,7 @@
<value>曲の長さ</value>
</data>
<data name="LyricsSearchControlHelp.Text" xml:space="preserve">
<value>* 変更は保存後すぐに反映され、マッピング情報とターゲットの歌詞を使用して後続の曲の歌詞が取得されます。 「Instrumental」としてマークすると、「Instrumental」プレースホルダ歌詞に直接戻ります。 リセットすると元のデータの検索が復元されます。</value>
<value>* 変更は保存後すぐに反映され、以降はマッピング情報に基づいた歌詞取得が行われます。「インストゥルメンタル」としてマークすると、専用のプレースホルダ歌詞に切り替わります。リセットを実行すると元のデータ取得状態に戻ります。</value>
</data>
<data name="LyricsSearchControlIgnoreCache.Header" xml:space="preserve">
<value>検索時にキャッシュを無視する</value>
@@ -316,7 +316,7 @@
<value>マッピング先</value>
</data>
<data name="LyricsSearchControlMarkAsPureMusic.Content" xml:space="preserve">
<value>インストとしてマーク</value>
<value>インストゥルメンタルとしてマーク</value>
</data>
<data name="LyricsSearchControlNotFound.Text" xml:space="preserve">
<value>見つかりません</value>
@@ -367,10 +367,10 @@
<value>さらにモードを追加するには「設定」に移動します</value>
</data>
<data name="LyricsWindowSwitchWindowTitle" xml:space="preserve">
<value>歌詞ウィンドウ切替</value>
<value>歌詞ウィンドウスイッチャー</value>
</data>
<data name="MainPageAlbumArtOnly.Content" xml:space="preserve">
<value>アルバムアートエリアのみ表示</value>
<value>アルバムアートのみ表示</value>
</data>
<data name="MainPageLyriscOnly.Content" xml:space="preserve">
<value>歌詞のみ表示</value>
@@ -412,13 +412,13 @@
<value>再生キューに追加</value>
</data>
<data name="MusicGalleryPageAllSongs" xml:space="preserve">
<value>すべてのミュージック</value>
<value>すべての</value>
</data>
<data name="MusicGalleryPageDataSync.Message" xml:space="preserve">
<value>メディアライブラリー同期中...</value>
<value>メディアライブラリー同期中...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>メディアライブラリの同期に問題があります</value>
<value>メディアライブラリの同期に問題が発生しました</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>キューをクリア</value>
@@ -460,7 +460,7 @@
<value>年</value>
</data>
<data name="MusicGalleryPageFileNotFound.Text" xml:space="preserve">
<value>メディアライブラリに曲が見つかりません</value>
<value>メディアライブラリに曲が見つかりません</value>
</data>
<data name="MusicGalleryPageFolder.Text" xml:space="preserve">
<value>フォルダ</value>
@@ -523,22 +523,22 @@
<value>ミュージックギャラリー - BetterLyrics</value>
</data>
<data name="MusicSettingsControlAutoSyncInterval.Header" xml:space="preserve">
<value>自動同期周波数</value>
<value>自動同期の頻度</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalDisabled.Content" xml:space="preserve">
<value>一切なし</value>
<value>なし</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryDay.Content" xml:space="preserve">
<value>毎日</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryFifteenMin.Content" xml:space="preserve">
<value>15分ごと</value>
<value>15分間隔</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEveryHour.Content" xml:space="preserve">
<value>1時間ごと</value>
<value>1時間間隔</value>
</data>
<data name="MusicSettingsControlAutoSyncIntervalEverySixHrs.Content" xml:space="preserve">
<value>6時間ごと</value>
<value>6時間間隔</value>
</data>
<data name="NarrowMode" xml:space="preserve">
<value>狭い表示モード</value>
@@ -556,13 +556,13 @@
<value>個人情報保護方針</value>
</data>
<data name="RemoteServerConfigControlBrowse.Content" xml:space="preserve">
<value>ブラウズ</value>
<value>参照</value>
</data>
<data name="RemoteServerConfigControlName.Header" xml:space="preserve">
<value>名称</value>
</data>
<data name="RemoteServerConfigControlName.PlaceholderText" xml:space="preserve">
<value>空のままにしておくと、自動的にデフォルトの名前が生成され。</value>
<value>空のままにすると、デフォルトの名前が自動生成されます。</value>
</data>
<data name="RemoteServerConfigControlPassword.Header" xml:space="preserve">
<value>パスワード</value>
@@ -571,7 +571,7 @@
<value>パス</value>
</data>
<data name="RemoteServerConfigControlPathNotExisted" xml:space="preserve">
<value>指定されたフォルダのパスが見つかりません</value>
<value>指定されたパスが見つかりません</value>
</data>
<data name="RemoteServerConfigControlPathRequired" xml:space="preserve">
<value>パスが必要</value>
@@ -613,7 +613,7 @@
<value>デザイン参考</value>
</data>
<data name="SettingsPage3DLyrics.Header" xml:space="preserve">
<value>[実験的] 深度エフェクト</value>
<value>[実験的] 奥行きのエフェクト</value>
</data>
<data name="SettingsPage3DLyricsDepth.Header" xml:space="preserve">
<value>深度</value>
@@ -640,7 +640,7 @@
<value>アルバムアートの高さ</value>
</data>
<data name="SettingsPageAlbumArtLayer.Header" xml:space="preserve">
<value>アルバムアートレイヤー</value>
<value>アルバムアートレイヤー</value>
</data>
<data name="SettingsPageAlbumArtSearchProvidersConfig.Text" xml:space="preserve">
<value>アルバムアートのソースを編集する</value>
@@ -688,10 +688,10 @@
<value>起動時にデフォルトの歌詞ウィンドウを自動的に開く</value>
</data>
<data name="SettingsPageAutoOpenMusicGalleryWindow.Header" xml:space="preserve">
<value>起動時にギャラリーウィンドウを開く</value>
<value>起動時にミュージックギャラリーウィンドウを開く</value>
</data>
<data name="SettingsPageAutoPlayWhenOpenMusicGalleryWindow.Header" xml:space="preserve">
<value>ギャラリーウィンドウを開いたときに自動的に再生を再開</value>
<value>ミュージックギャラリーを開いたときに自動的に再生を再開する</value>
</data>
<data name="SettingsPageAutoStart.Header" xml:space="preserve">
<value>自動起動</value>
@@ -703,7 +703,7 @@
<value>ぼかし量</value>
</data>
<data name="SettingsPageCache.Description" xml:space="preserve">
<value>ログファイル、オンライン歌詞キャッシュを含</value>
<value>ログファイル、オンライン歌詞キャッシュを含みます</value>
</data>
<data name="SettingsPageCache.Header" xml:space="preserve">
<value>キャッシュ</value>
@@ -712,13 +712,13 @@
<value>中央揃え</value>
</data>
<data name="SettingsPageCheckShortcut.Content" xml:space="preserve">
<value>キー割り当ての確認</value>
<value>ショートカットキー確認</value>
</data>
<data name="SettingsPageChinese.Header" xml:space="preserve">
<value>ピンインルビ</value>
</data>
<data name="SettingsPageChineseLyrics.Text" xml:space="preserve">
<value>中国語歌詞</value>
<value>中国語歌詞</value>
</data>
<data name="SettingsPageChinesePreference.Header" xml:space="preserve">
<value>簡体字から繁体字へ変換</value>
@@ -730,7 +730,7 @@
<value>クリア</value>
</data>
<data name="SettingsPageClearCache.Content" xml:space="preserve">
<value>キャッシュファイルクリア</value>
<value>キャッシュファイルクリア</value>
</data>
<data name="SettingsPageCloseStatus.Text" xml:space="preserve">
<value>閉じる</value>
@@ -859,7 +859,7 @@
<value>歌詞ウィンドウを閉じたときにアプリを終了する</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value>プレー履歴エクスポート</value>
<value>再生履歴エクスポート</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>設定をエクスポート</value>
@@ -874,19 +874,19 @@
<value>流体レイヤー</value>
</data>
<data name="SettingsPageFogLayer.Header" xml:space="preserve">
<value>霧レイヤー</value>
<value>霧レイヤー</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>システムに従う</value>
</data>
<data name="SettingsPageFontColor.Header" xml:space="preserve">
<value>フォント色</value>
<value>フォント色</value>
</data>
<data name="SettingsPageForceAlwaysOnTop.Description" xml:space="preserve">
<value>定期チェックで最前面表示を強制維持します</value>
</data>
<data name="SettingsPageForceAlwaysOnTop.Header" xml:space="preserve">
<value>常に上部に強制的に表示</value>
<value>常にトップに強制的に表示</value>
</data>
<data name="SettingsPageForceWordByWordEffect.Description" xml:space="preserve">
<value>現在の歌詞に文字単位の情報がない場合でも、文字単位の歌詞シミュレーションをします</value>
@@ -907,7 +907,7 @@
<value>このアプリの翻訳に協力する 🌏</value>
</data>
<data name="SettingsPageHideWindow.Description" xml:space="preserve">
<value>音楽の再生が停止した場合、自動的に歌詞ウィンドウを非表示/表示する</value>
<value>ミュージックの再生が停止した場合、自動的に歌詞ウィンドウを非表示/表示する</value>
</data>
<data name="SettingsPageHideWindow.Header" xml:space="preserve">
<value>ウィンドウの自動非表示/表示</value>
@@ -1021,7 +1021,7 @@
<value>現在の行の位置</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌詞エフェクト</value>
<value>歌詞エフェクト</value>
</data>
<data name="SettingsPageLyricsExtraBlack.Content" xml:space="preserve">
<value>超極太</value>
@@ -1099,7 +1099,7 @@
<value>ベストマッチ</value>
</data>
<data name="SettingsPageLyricsSearchProvidersConfig.Text" xml:space="preserve">
<value>歌詞の設定</value>
<value>歌詞ソースの設定</value>
</data>
<data name="SettingsPageLyricsSearchSequential.Content" xml:space="preserve">
<value>順次</value>
@@ -1108,7 +1108,7 @@
<value>順次:以下のリストの優先順位に従って検索し、最初に見つかった結果を返します。ベストマッチ:有効なすべてのソースを検索し、一致スコアが最も高い結果を自動的に選択します</value>
</data>
<data name="SettingsPageLyricsSearchType.Header" xml:space="preserve">
<value>歌詞検索方法</value>
<value>歌詞検索方法</value>
</data>
<data name="SettingsPageLyricsSemiBold.Content" xml:space="preserve">
<value>中太</value>
@@ -1117,7 +1117,7 @@
<value>準細</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>歌詞スタイル</value>
<value>歌詞スタイル</value>
</data>
<data name="SettingsPageLyricsThin.Content" xml:space="preserve">
<value>極細</value>
@@ -1138,16 +1138,16 @@
<value>歌詞ウィンドウマネージャー</value>
</data>
<data name="SettingsPageLyricsWindowSwitchHotKey.Header" xml:space="preserve">
<value>歌詞ウィンドウ状態切り替えショートカット</value>
<value>歌詞ウィンドウスイッチャーのショートカットキー</value>
</data>
<data name="SettingsPageMatchingThreshold.Description" xml:space="preserve">
<value>この値を調整すると、順次検索とベストマッチ検索の結果に影響しますが、手動歌詞検索インターフェイスの検索結果には影響しません</value>
</data>
<data name="SettingsPageMatchingThreshold.Header" xml:space="preserve">
<value>一致度の最低ライン</value>
<value>最小一致しきい値</value>
</data>
<data name="SettingsPageMediaLib.Content" xml:space="preserve">
<value>メディアライブラリ</value>
<value>メディアライブラリ</value>
</data>
<data name="SettingsPageMedianCut.Content" xml:space="preserve">
<value>保守的</value>
@@ -1156,7 +1156,7 @@
<value>この再生ソースを監視する</value>
</data>
<data name="SettingsPageMockMusicPlaying.Header" xml:space="preserve">
<value>テスト音楽を再生</value>
<value>テストミュージックを再生</value>
</data>
<data name="SettingsPageMultiNowPlayingWindows.Header" xml:space="preserve">
<value>マルチウィンドウ モード</value>
@@ -1171,10 +1171,10 @@
<value>ミュージック ギャラリーが開いているため、他の再生ソースを無視します</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>音楽または歌詞が含まれるフォルダを追加</value>
<value>ミュージックまたは歌詞が含まれるフォルダを追加</value>
</data>
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>ローカルメディアライブラリ</value>
<value>ローカルメディアライブラリ</value>
</data>
<data name="SettingsPageNarrowMode.Text" xml:space="preserve">
<value>狭い表示モード</value>
@@ -1213,10 +1213,10 @@
<value>スポンサー</value>
</data>
<data name="SettingsPagePhonetic.Text" xml:space="preserve">
<value>歌詞ルビ</value>
<value>歌詞ルビ</value>
</data>
<data name="SettingsPagePhoneticText.Header" xml:space="preserve">
<value>ルビのサイズ</value>
<value>ルビ</value>
</data>
<data name="SettingsPagePinToTaskbar.Header" xml:space="preserve">
<value>タスクバーにピン留め</value>
@@ -1243,13 +1243,13 @@
<value>"soundcloud.com" で "Cut to the Feeling" を再生</value>
</data>
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>再生/一時停止のショートカット</value>
<value>再生/一時停止のショートカットキー</value>
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>前の曲へのショートカット</value>
<value>次のトラックのショートカットキー</value>
</data>
<data name="SettingsPagePureLayer.Header" xml:space="preserve">
<value>単色レイヤー</value>
<value>単色レイヤー</value>
</data>
<data name="SettingsPageRealtimeStatus.Text" xml:space="preserve">
<value>リアルタイムステータス</value>
@@ -1315,13 +1315,13 @@
<value>設定マネージャー</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>プレーの歴史</value>
<value>再生履歴</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>オンライン共有センターを閲覧する</value>
</data>
<data name="SettingsPageShortcut.Text" xml:space="preserve">
<value>ショートカット</value>
<value>ショートカットキー</value>
</data>
<data name="SettingsPageShortcutRegFailInfo" xml:space="preserve">
<value>ホットキーの登録に失敗しました</value>
@@ -1336,7 +1336,7 @@
<value>アーティストを表示</value>
</data>
<data name="SettingsPageShowHideHotKey.Header" xml:space="preserve">
<value>歌詞ウィンドウの表示/非表示ショートカット</value>
<value>歌詞ウィンドウの表示/非表示ショートカットキー</value>
</data>
<data name="SettingsPageShowInSwitchers.Description" xml:space="preserve">
<value>例: Alt + Tab、タスクバー</value>
@@ -1351,7 +1351,7 @@
<value>スライド</value>
</data>
<data name="SettingsPageSnowFlakeLayer.Header" xml:space="preserve">
<value>雪レイヤー</value>
<value>雪レイヤー</value>
</data>
<data name="SettingsPageSongInfo.Text" xml:space="preserve">
<value>楽曲情報</value>
@@ -1393,7 +1393,7 @@
<value>スタートアップ</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>統計</value>
<value>統計データ</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>ミュージックギャラリーウィンドウを閉じたときに再生を停止する</value>
@@ -1435,7 +1435,7 @@
<value>訳文</value>
</data>
<data name="SettingsPageTranslation.Text" xml:space="preserve">
<value>歌詞翻訳</value>
<value>歌詞翻訳</value>
</data>
<data name="SettingsPageTranslationConfig.Header" xml:space="preserve">
<value>LibreTranslate サービス</value>
@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>標準モード</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>情報源</value>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>時間帯別アクティビティ</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>カスタム</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>終了</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>最少アクティブ</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>最多アクティブ</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>再生ソース</value>
</data>
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>開始</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>今月</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>今期</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>今週</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>今年</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>期間</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>タイムズ</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>今日</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1492,13 +1516,19 @@
<value>トップトラック</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>トップ・ソース</value>
<value>よく使う再生ソース</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>総所要時間</value>
<value>総再生時間</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>タイムズ</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>タイムズ</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>演奏曲目</value>
<value>再生された曲の数</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>終了</value>
@@ -1516,13 +1546,13 @@
<value>再起動</value>
</data>
<data name="SystemTraySearch.Text" xml:space="preserve">
<value>歌詞検索ウィンドウ</value>
<value>歌詞検索ウィンドウ</value>
</data>
<data name="SystemTraySettings.Text" xml:space="preserve">
<value>設定を開く</value>
</data>
<data name="SystemTraySwitch.Text" xml:space="preserve">
<value>歌詞ウィンドウ切り替え</value>
<value>歌詞ウィンドウスイッチャー</value>
</data>
<data name="TaskbarMode" xml:space="preserve">
<value>タスクバー モード</value>

View File

@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>표준 모드</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>시간별 활동</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>사용자 지정</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>종료</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>최소 활성</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>가장 활동적인</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>출처</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>시작</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>이번 달</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>이번 분기</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>이번 주</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>올해</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>시간 범위</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>시간</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>오늘</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1497,6 +1521,12 @@
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>총 기간</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>시간</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>시간</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>재생된 트랙</value>
</data>

View File

@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>Mod Standard</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Aktiviti mengikut Jam</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Tersuai</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>Penamat</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Kurang Aktif</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Paling Aktif</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sumber</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Mula</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>Bulan Ini</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<value>Suku Tahun Ini</value>
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>Suku ini</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>Minggu Ini</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>Tahun Ini</value>
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>Tahun ini</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Julat masa</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Kelas kedua</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Hari ini</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1497,6 +1521,12 @@
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Jumlah Tempoh</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Kelas kedua</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Kelas kedua</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Trek Dimainkan</value>
</data>

View File

@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>Modo Padrão</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Atividade por hora</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Personalizado</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>Fim</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Menos ativo</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Mais activos</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Fontes</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Início</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>Este mês</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>Este trimestre</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>Esta semana</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>Este ano</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Intervalo de tempo</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Tempos</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Hoje</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1497,6 +1521,12 @@
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Duração total</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Tempos</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Tempos</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Faixas reproduzidas</value>
</data>

View File

@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>Стандартный режим</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Активность по часам</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Пользовательское</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>Конец</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Наименее активный</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Самые активные</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Источники</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Начало</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>Этот месяц</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>Этот квартал</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>На этой неделе</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>В этом году</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Диапазон времени</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Сегодня</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1497,6 +1521,12 @@
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Общая продолжительность</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Times</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Воспроизведенные треки</value>
</data>

View File

@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>โหมดมาตรฐาน</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>กิจกรรมตามชั่วโมง</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>กำหนดเอง</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>สิ้นสุด</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>น้อยที่สุด</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>กิจกรรมล่าสุด</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>แหล่งข้อมูล</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>เริ่มต้น</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>เดือนนี้</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>ไตรมาสนี้</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>สัปดาห์นี้</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>ปีนี้</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>ช่วงเวลา</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>เวลา</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>วันนี้</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1497,6 +1521,12 @@
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>ระยะเวลาทั้งหมด</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>เวลา</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>เวลา</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>เพลงที่เล่น</value>
</data>

View File

@@ -1467,22 +1467,46 @@
<data name="StandardMode" xml:space="preserve">
<value>Chế độ tiêu chuẩn</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>Hoạt động theo giờ</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>Tùy chỉnh</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>Kết thúc</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>Hoạt động ít nhất</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Hoạt động nhiều nhất</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Nguồn</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>Bắt đầu</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>Tháng này</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>Quý này</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>Tuần này</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>Năm nay</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>Khoảng thời gian</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>Thời gian</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>Hôm nay</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
@@ -1497,6 +1521,12 @@
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>Thời gian tổng cộng</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>Thời gian</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>Thời gian</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>Các bài hát đã phát</value>
</data>

View File

@@ -59,7 +59,7 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
@@ -418,7 +418,7 @@
<value>正在同步媒体库...</value>
</data>
<data name="MusicGalleryPageDataSyncError.Message" xml:space="preserve">
<value>同步媒体库出现问题</value>
<value>媒体库同步出现问题</value>
</data>
<data name="MusicGalleryPageEmptyPlayingQueue.Text" xml:space="preserve">
<value>清除播放队列</value>
@@ -859,7 +859,7 @@
<value>关闭歌词窗口时退出程序</value>
</data>
<data name="SettingsPageExportPlayHistoryButton.Content" xml:space="preserve">
<value />
<value>导出播放记录</value>
</data>
<data name="SettingsPageExportSettingsButton.Content" xml:space="preserve">
<value>导出设置</value>
@@ -1314,6 +1314,9 @@
<data name="SettingsPageSettingsManager.Header" xml:space="preserve">
<value>设置管理器</value>
</data>
<data name="SettingsPageSettingsPlayHistory.Header" xml:space="preserve">
<value>播放记录</value>
</data>
<data name="SettingsPageShareHub.Content" xml:space="preserve">
<value>浏览在线资源共享中心</value>
</data>
@@ -1390,7 +1393,7 @@
<value>启动</value>
</data>
<data name="SettingsPageStats.Content" xml:space="preserve">
<value>统计数据</value>
<value>音乐报告</value>
</data>
<data name="SettingsPageStopTrackOnGalleryWindowClosed.Header" xml:space="preserve">
<value>关闭音乐库窗口时停止播放</value>
@@ -1464,38 +1467,68 @@
<data name="StandardMode" xml:space="preserve">
<value>标准模式</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>活跃时段</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>自定义</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>结束</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>最不活跃时段</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>最活跃时段</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>播放源</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>开始</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>本月</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>本季度</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>本周</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>本年度</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>时间范围</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>次</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>今日</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>最热爱的艺人</value>
<value>常听的歌手</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>最喜欢的歌曲</value>
<value>常听的曲目</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>常用的播放源</value>
<value>常用的播放源</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>总时长</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>次</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>次</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>播放的曲目</value>
<value>播放的曲目</value>
</data>
<data name="SystemTrayExit.Text" xml:space="preserve">
<value>退出程序</value>

View File

@@ -1467,36 +1467,66 @@
<data name="StandardMode" xml:space="preserve">
<value>標準模式</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>每小時的活動</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>自訂</value>
</data>
<data name="StatsDashboardControlEnd.Header" xml:space="preserve">
<value>結束</value>
</data>
<data name="StatsDashboardControlLeastActive.Text" xml:space="preserve">
<value>最不活躍</value>
</data>
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>最活躍</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>來源</value>
</data>
<data name="StatsDashboardControlThisMonth.Header" xml:space="preserve">
<data name="StatsDashboardControlStart.Header" xml:space="preserve">
<value>開始</value>
</data>
<data name="StatsDashboardControlThisMonth.Content" xml:space="preserve">
<value>本月</value>
</data>
<data name="StatsDashboardControlThisQuarter.Header" xml:space="preserve">
<data name="StatsDashboardControlThisQuarter.Content" xml:space="preserve">
<value>本季</value>
</data>
<data name="StatsDashboardControlThisWeek.Header" xml:space="preserve">
<data name="StatsDashboardControlThisWeek.Content" xml:space="preserve">
<value>本週</value>
</data>
<data name="StatsDashboardControlThisYear.Header" xml:space="preserve">
<value>年</value>
<data name="StatsDashboardControlThisYear.Content" xml:space="preserve">
<value>年</value>
</data>
<data name="StatsDashboardControlToday.Header" xml:space="preserve">
<value>今天</value>
<data name="StatsDashboardControlTimeRange.Header" xml:space="preserve">
<value>時間範圍</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>次</value>
</data>
<data name="StatsDashboardControlToday.Content" xml:space="preserve">
<value>今日</value>
</data>
<data name="StatsDashboardControlTopArtists.Text" xml:space="preserve">
<value>頂級藝術家</value>
<value>常聽的藝術家</value>
</data>
<data name="StatsDashboardControlTopSongs.Text" xml:space="preserve">
<value>熱門曲目</value>
<value>常聽的曲目</value>
</data>
<data name="StatsDashboardControlTopSource.Text" xml:space="preserve">
<value>頂端來源</value>
<value>常用的播放來源</value>
</data>
<data name="StatsDashboardControlTotalDuration.Text" xml:space="preserve">
<value>總時間</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>次</value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>次</value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>播放曲目</value>
</data>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:bwc="using:BetterLyrics.WinUI3.Converter"
xmlns:cwc="using:CommunityToolkit.WinUI.Converters">
<bwc:EnumToIntConverter x:Key="EnumToIntConverter" />
<bwc:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<bwc:MatchedLocalFilesPathToVisibilityConverter x:Key="MatchedLocalFilesPathToVisibilityConverter" />
<bwc:IntToCornerRadius x:Key="IntToCornerRadius" />
<bwc:CornerRadiusToDoubleConverter x:Key="CornerRadiusToDoubleConverter" />
<bwc:LyricsSearchProviderToDisplayNameConverter x:Key="LyricsSearchProviderToDisplayNameConverter" />
<bwc:TranslationSearchProviderToDisplayNameConverter x:Key="TranslationSearchProviderToDisplayNameConverter" />
<bwc:TransliterationSearchProviderToDisplayNameConverter x:Key="TransliterationSearchProviderToDisplayNameConverter" />
<bwc:AlbumArtSearchProviderToDisplayNameConverter x:Key="AlbumArtSearchProviderToDisplayNameConverter" />
<bwc:SecondsToFormattedTimeConverter x:Key="SecondsToFormattedTimeConverter" />
<bwc:MillisecondsToFormattedTimeConverter x:Key="MillisecondsToFormattedTimeConverter" />
<bwc:FPSToTimeSpanConverter x:Key="FPSToTimeSpanConverter" />
<bwc:ShortcutToStringConverter x:Key="ShortcutToStringConverter" />
<bwc:BoolNegationToVisibilityConverter x:Key="BoolNegationToVisibilityConverter" />
<bwc:BoolToOpacityConverter x:Key="BoolToOpacityConverter" />
<bwc:BoolToPartialOpacityConverter x:Key="BoolToPartialOpacityConverter" />
<bwc:BoolNegationToOpacityConverter x:Key="BoolNegationToOpacityConverter" />
<bwc:RectToMarginConverter x:Key="RectToMarginConverter" />
<bwc:LanguageCodeToDisplayedNameConverter x:Key="LanguageCodeToDisplayedNameConverter" />
<bwc:ByteArrayToImageConverter x:Key="ByteArrayToImageConverter" />
<bwc:DisplayLanguageCodeToIndexConverter x:Key="DisplayLanguageCodeToIndexConverter" />
<bwc:PathToParentFolderConverter x:Key="PathToParentFolderConverter" />
<bwc:IntToBoolConverter x:Key="IntToBoolConverter" />
<bwc:IndexToDisplayConverter x:Key="IndexToDisplayConverter" />
<bwc:IntToDoubleConverter x:Key="IntToDoubleConverter" />
<bwc:MillisecondsToSecondsConverter x:Key="MillisecondsToSecondsConverter" />
<bwc:PictureInfosToImageSourceConverter x:Key="PictureInfosToImageSourceConverter" />
<bwc:LyricsFontWeightToFontWeightConverter x:Key="LyricsFontWeightToFontWeightConverter" />
<bwc:TextAlignmentTypeToHorizontalAlignmentConverter x:Key="TextAlignmentTypeToHorizontalAlignmentConverter" />
<bwc:LyricsLayoutOrientationToOrientationConverter x:Key="LyricsLayoutOrientationToOrientationConverter" />
<bwc:LyricsLayoutOrientationNegationToOrientationConverter x:Key="LyricsLayoutOrientationNegationToOrientationConverter" />
<bwc:FileSourceTypeToIconConverter x:Key="FileSourceTypeToIconConverter" />
<bwc:PathToImageConverter x:Key="PathToImageConverter" />
<bwc:DoubleToDecimalConverter x:Key="DoubleToDecimalConverter" />
<bwc:UriStringToDecodedAbsoluteUri x:Key="UriStringToDecodedAbsoluteUri" />
<cwc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<cwc:BoolNegationConverter x:Key="BoolNegationConverter" />
<cwc:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<cwc:CollectionVisibilityConverter x:Key="CollectionVisibilityConverter" />
</ResourceDictionary>

View File

@@ -0,0 +1,188 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="GhostSliderStyle" TargetType="Slider">
<Setter Property="Background" Value="{ThemeResource ControlStrokeColorOnAccentDefaultBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource SliderBorderThemeThickness}" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="ManipulationMode" Value="None" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-7,0,-7,0" />
<Setter Property="IsFocusEngagementEnabled" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Slider">
<Grid Margin="{TemplateBinding Padding}">
<Grid.Resources>
<Style x:Key="SliderThumbStyle" TargetType="Thumb">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="0,1,1,0" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter
x:Name="HeaderContentPresenter"
Grid.Row="0"
Margin="{ThemeResource SliderTopHeaderMargin}"
x:DeferLoadStrategy="Lazy"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontWeight="{ThemeResource SliderHeaderThemeFontWeight}"
Foreground="{ThemeResource SliderHeaderForeground}"
TextWrapping="Wrap"
Visibility="Collapsed" />
<Grid
x:Name="SliderContainer"
Grid.Row="1"
Background="{ThemeResource SliderContainerBackground}"
Control.IsTemplateFocusTarget="True">
<Grid x:Name="HorizontalTemplate" MinHeight="{ThemeResource SliderHorizontalHeight}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="{ThemeResource SliderPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{ThemeResource SliderPostContentMargin}" />
</Grid.RowDefinitions>
<Rectangle
x:Name="HorizontalTrackRect"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="HorizontalDecreaseRect"
Grid.Row="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="TopTickBar"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,0,4"
VerticalAlignment="Bottom"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="HorizontalInlineTickBar"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="BottomTickBar"
Grid.Row="2"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,4,0,0"
VerticalAlignment="Top"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="HorizontalThumb"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
Width="2"
Height="2"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-14,-6,-14,-6"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
<Grid
x:Name="VerticalTemplate"
MinWidth="{ThemeResource SliderVerticalWidth}"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{ThemeResource SliderPreContentMargin}" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="{ThemeResource SliderPostContentMargin}" />
</Grid.ColumnDefinitions>
<Rectangle
x:Name="VerticalTrackRect"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="VerticalDecreaseRect"
Grid.Row="2"
Grid.Column="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="LeftTickBar"
Grid.RowSpan="3"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,4,0"
HorizontalAlignment="Right"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="VerticalInlineTickBar"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="RightTickBar"
Grid.RowSpan="3"
Grid.Column="2"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="4,0,0,0"
HorizontalAlignment="Left"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="VerticalThumb"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Width="24"
Height="8"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-6,-14,-6,-14"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="InteractiveListViewHeaderStyle" TargetType="ListViewHeaderItem">
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ListViewHeaderItemThemeFontSize}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Margin" Value="0,0,0,4" />
<Setter Property="Padding" Value="12,8,12,8" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewHeaderItem">
<Grid
x:Name="RootGrid"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter
x:Name="ContentPresenter"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}" />
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:0.1" />
</VisualStateGroup.Transitions>
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource ListViewItemBackgroundPointerOver}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="{ThemeResource ListViewItemBackgroundPressed}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="ContentPresenter.Opacity" Value="{ThemeResource ListViewItemDisabledThemeOpacity}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -4,7 +4,7 @@ using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Parsers.LyricsParser;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -20,7 +20,7 @@ namespace BetterLyrics.WinUI3.ViewModels
IRecipient<PropertyChangedMessage<SongInfo?>>
{
private readonly ILyricsSearchService _lyricsSearchService;
private readonly IMediaSessionsService _mediaSessionsService;
private readonly IGSMTCService _gsmtcService;
private readonly ISettingsService _settingsService;
private LatestOnlyTaskRunner _lyricsSearchRunner = new();
@@ -43,10 +43,10 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial bool IsSearching { get; set; } = false;
public LyricsSearchControlViewModel(ILyricsSearchService lyricsSearchService, IMediaSessionsService mediaSessionsService, ISettingsService settingsService)
public LyricsSearchControlViewModel(ILyricsSearchService lyricsSearchService, IGSMTCService gsmtcService, ISettingsService settingsService)
{
_lyricsSearchService = lyricsSearchService;
_mediaSessionsService = mediaSessionsService;
_gsmtcService = gsmtcService;
_settingsService = settingsService;
AppSettings = _settingsService.AppSettings;
@@ -58,19 +58,19 @@ namespace BetterLyrics.WinUI3.ViewModels
{
LyricsSearchResults.Clear();
LyricsDataArr = null;
if (_mediaSessionsService.CurrentSongInfo != null)
if (_gsmtcService.CurrentSongInfo != null)
{
var found = GetMappedSongSearchQueryFromSettings();
if (found == null)
{
MappedSongSearchQuery = new MappedSongSearchQuery
{
OriginalTitle = _mediaSessionsService.CurrentSongInfo.Title,
OriginalArtist = _mediaSessionsService.CurrentSongInfo.DisplayArtists,
OriginalAlbum = _mediaSessionsService.CurrentSongInfo.Album,
MappedTitle = _mediaSessionsService.CurrentSongInfo.Title,
MappedArtist = _mediaSessionsService.CurrentSongInfo.DisplayArtists,
MappedAlbum = _mediaSessionsService.CurrentSongInfo.Album,
OriginalTitle = _gsmtcService.CurrentSongInfo.Title,
OriginalArtist = _gsmtcService.CurrentSongInfo.DisplayArtists,
OriginalAlbum = _gsmtcService.CurrentSongInfo.Album,
MappedTitle = _gsmtcService.CurrentSongInfo.Title,
MappedArtist = _gsmtcService.CurrentSongInfo.DisplayArtists,
MappedAlbum = _gsmtcService.CurrentSongInfo.Album,
};
}
else
@@ -82,16 +82,16 @@ namespace BetterLyrics.WinUI3.ViewModels
private MappedSongSearchQuery? GetMappedSongSearchQueryFromSettings()
{
if (_mediaSessionsService.CurrentSongInfo == null)
if (_gsmtcService.CurrentSongInfo == null)
{
return null;
}
var found = AppSettings.MappedSongSearchQueries
.FirstOrDefault(x =>
x.OriginalTitle == _mediaSessionsService.CurrentSongInfo.Title &&
x.OriginalArtist == _mediaSessionsService.CurrentSongInfo.DisplayArtists &&
x.OriginalAlbum == _mediaSessionsService.CurrentSongInfo.Album);
x.OriginalTitle == _gsmtcService.CurrentSongInfo.Title &&
x.OriginalArtist == _gsmtcService.CurrentSongInfo.DisplayArtists &&
x.OriginalAlbum == _gsmtcService.CurrentSongInfo.Album);
return found;
}
@@ -102,7 +102,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
return;
}
_mediaSessionsService.ChangePosition(value.StartMs / 1000.0);
_gsmtcService.ChangePosition(value.StartMs / 1000.0);
}
[RelayCommand]
@@ -121,7 +121,7 @@ namespace BetterLyrics.WinUI3.ViewModels
LyricsSearchResults = [..await Task.Run(async () =>
{
var result = await _lyricsSearchService.SearchAllAsync(
((SongInfo?)_mediaSessionsService.CurrentSongInfo?.Clone() ?? new())
((SongInfo?)_gsmtcService.CurrentSongInfo?.Clone() ?? new())
.WithTitle(MappedSongSearchQuery.MappedTitle)
.WithArtist(MappedSongSearchQuery.MappedArtist.SplitByCommonSplitter())
.WithAlbum(MappedSongSearchQuery.MappedAlbum),
@@ -196,9 +196,9 @@ namespace BetterLyrics.WinUI3.ViewModels
public void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
if (message.PropertyName == nameof(IGSMTCService.CurrentSongInfo))
{
InitMappedSongSearchQuery();
}

View File

@@ -235,5 +235,11 @@ namespace BetterLyrics.WinUI3.ViewModels
await dialog.ShowAsync();
}
[RelayCommand]
private void OpenMusicGalleryWindow()
{
WindowHook.OpenOrShowWindow<MusicGalleryWindow>();
}
}
}

View File

@@ -3,13 +3,16 @@ using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SMTCService;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
@@ -41,16 +44,10 @@ namespace BetterLyrics.WinUI3.ViewModels
private readonly ILocalizationService _localizationService;
private readonly IFileSystemService _fileSystemService;
private readonly MediaPlayer _mediaPlayer = new();
private readonly MediaTimelineController _timelineController = new();
private readonly SystemMediaTransportControls _smtc;
[ObservableProperty] public partial ISMTCService SMTCService { get; set; }
private readonly DispatcherQueueTimer _refreshSongsTimer;
private IRandomAccessStream? _currentStream;
private Stream? _currentNetStream;
private IUnifiedFileSystem? _currentProvider;
// All songs
private List<ExtendedTrack> _allTracks = [];
// Songs in current playlist or songs in current file tree
@@ -58,37 +55,22 @@ namespace BetterLyrics.WinUI3.ViewModels
// Filtered songs based on search query for current playlist
private List<ExtendedTrack> _filteredTracks = [];
[ObservableProperty]
public partial AppSettings AppSettings { get; set; }
[ObservableProperty] public partial AppSettings AppSettings { get; set; }
[ObservableProperty]
public partial bool IsLocalMediaNotFound { get; set; }
[ObservableProperty] public partial bool IsLocalMediaNotFound { get; set; }
/// <summary>
/// Grouped tracks after filtering and sorting for current playlist
/// </summary>
[ObservableProperty]
public partial ObservableCollection<GroupInfoList> GroupedTracks { get; set; } = [];
[ObservableProperty] public partial ObservableCollection<GroupInfoList> GroupedTracks { get; set; } = [];
[ObservableProperty]
public partial List<ExtendedTrack> SelectedTracks { get; set; } = [];
[ObservableProperty] public partial List<ExtendedTrack> SelectedTracks { get; set; } = [];
[ObservableProperty]
public partial int SelectedTracksTotalDuration { get; set; } = 0;
[ObservableProperty] public partial int SelectedTracksTotalDuration { get; set; } = 0;
[ObservableProperty]
public partial ObservableCollection<PlayQueueItem> TrackPlayingQueue { get; set; }
[ObservableProperty] public partial CommonSongProperty SongOrderType { get; set; } = CommonSongProperty.Title;
public PlayQueueItem? PlayingQueueItem => TrackPlayingQueue.ElementAtOrDefault(AppSettings.MusicGallerySettings.PlayQueueIndex);
[ObservableProperty]
public partial ExtendedTrack? PlayingTrack { get; set; } = null;
[ObservableProperty]
public partial CommonSongProperty SongOrderType { get; set; } = CommonSongProperty.Title;
[ObservableProperty]
public partial int SelectedSongsTabInfoIndex { get; set; } = 0;
[ObservableProperty] public partial int SelectedSongsTabInfoIndex { get; set; } = 0;
public SongsTabInfo? SelectedSongsTabInfo => AppSettings.StarredPlaylists.ElementAtOrDefault(SelectedSongsTabInfoIndex);
@@ -97,47 +79,32 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty] public partial ExtendedTrack TrackRightTapped { get; set; } = new();
[ObservableProperty]
public partial string SongSearchQuery { get; set; } = string.Empty;
[ObservableProperty] public partial string SongSearchQuery { get; set; } = string.Empty;
[ObservableProperty] public partial ListViewSelectionMode SongListViewSelectionMode { get; set; } = ListViewSelectionMode.Single;
public ObservableCollection<FolderNode> FolderRoots { get; } = new();
public MusicGalleryPageViewModel(
ISettingsService settingsService,
ILocalizationService localizationService,
IFileSystemService fileSystemService
IFileSystemService fileSystemService,
ISMTCService smtcService
)
{
_localizationService = localizationService;
_fileSystemService = fileSystemService;
SMTCService = smtcService;
_refreshSongsTimer = _dispatcherQueue.CreateTimer();
_settingsService = settingsService;
AppSettings = _settingsService.AppSettings;
TrackPlayingQueue = [.. AppSettings.MusicGallerySettings.PlayQueuePaths.Select(x => new PlayQueueItem(new ExtendedTrack(x)))];
TrackPlayingQueue.CollectionChanged += TrackPlayingQueue_CollectionChanged;
RefreshSongs();
_settingsService.AppSettings.LocalMediaFolders.CollectionChanged += LocalMediaFolders_CollectionChanged;
_settingsService.AppSettings.LocalMediaFolders.ItemPropertyChanged += LocalMediaFolders_ItemPropertyChanged;
_mediaPlayer.MediaOpened += MediaPlayer_MediaOpened;
_mediaPlayer.MediaEnded += MediaPlayer_MediaEnded;
_mediaPlayer.CommandManager.IsEnabled = false;
_timelineController = _mediaPlayer.TimelineController = new();
_timelineController.PositionChanged += TimelineController_PositionChanged;
_smtc = _mediaPlayer.SystemMediaTransportControls;
_smtc.IsPlayEnabled = true;
_smtc.IsPauseEnabled = true;
_smtc.IsNextEnabled = true;
_smtc.IsPreviousEnabled = true;
_smtc.ButtonPressed += Smtc_ButtonPressed;
_smtc.PlaybackPositionChangeRequested += Smtc_PlaybackPositionChangeRequested;
}
private void LocalMediaFolders_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
@@ -145,134 +112,11 @@ namespace BetterLyrics.WinUI3.ViewModels
IsDataSyncError = AppSettings.LocalMediaFolders.Any(x => x.StatusSeverity == InfoBarSeverity.Error);
}
private void TrackPlayingQueue_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
AppSettings.MusicGallerySettings.PlayQueuePaths = [.. TrackPlayingQueue.Select(x => x.Track.DecodedAbsoluteUri)];
}
private void LocalMediaFolders_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
RefreshSongs();
}
private void MediaPlayer_MediaEnded(MediaPlayer sender, object args)
{
PlayNextTrack();
}
public void PlayNextTrack()
{
switch (AppSettings.MusicGallerySettings.PlaybackOrder)
{
case PlaybackOrder.RepeatAll:
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
if (AppSettings.MusicGallerySettings.PlayQueueIndex < TrackPlayingQueue.Count - 1)
{
AppSettings.MusicGallerySettings.PlayQueueIndex++;
}
else
{
AppSettings.MusicGallerySettings.PlayQueueIndex = 0;
}
await PlayTrackAsync(PlayingQueueItem);
});
break;
case PlaybackOrder.RepeatOne:
_timelineController.Position = TimeSpan.Zero;
break;
case PlaybackOrder.Shuffle:
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
if (TrackPlayingQueue.Count > 0)
{
AppSettings.MusicGallerySettings.PlayQueueIndex = new Random().Next(0, TrackPlayingQueue.Count);
}
await PlayTrackAsync(PlayingQueueItem);
});
break;
default:
break;
}
}
private void PlayPreviousTrack()
{
switch (AppSettings.MusicGallerySettings.PlaybackOrder)
{
case PlaybackOrder.RepeatAll:
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
if (AppSettings.MusicGallerySettings.PlayQueueIndex > 0)
{
AppSettings.MusicGallerySettings.PlayQueueIndex--;
}
else
{
AppSettings.MusicGallerySettings.PlayQueueIndex = TrackPlayingQueue.Count - 1;
}
await PlayTrackAsync(PlayingQueueItem);
});
break;
case PlaybackOrder.RepeatOne:
_timelineController.Position = TimeSpan.Zero;
break;
case PlaybackOrder.Shuffle:
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
if (TrackPlayingQueue.Count > 0)
{
AppSettings.MusicGallerySettings.PlayQueueIndex = new Random().Next(0, TrackPlayingQueue.Count);
}
await PlayTrackAsync(PlayingQueueItem);
});
break;
default:
break;
}
}
private void Smtc_PlaybackPositionChangeRequested(SystemMediaTransportControls sender, PlaybackPositionChangeRequestedEventArgs args)
{
_timelineController.Position = args.RequestedPlaybackPosition;
}
private void MediaPlayer_MediaOpened(MediaPlayer sender, object args)
{
_timelineController.Start();
_smtc.PlaybackStatus = MediaPlaybackStatus.Playing;
}
private void TimelineController_PositionChanged(MediaTimelineController sender, object args)
{
_smtc.UpdateTimelineProperties(new SystemMediaTransportControlsTimelineProperties()
{
Position = sender.Position,
EndTime = _mediaPlayer.PlaybackSession.NaturalDuration
});
}
private void Smtc_ButtonPressed(SystemMediaTransportControls sender, SystemMediaTransportControlsButtonPressedEventArgs args)
{
switch (args.Button)
{
case SystemMediaTransportControlsButton.Play:
_smtc.PlaybackStatus = MediaPlaybackStatus.Playing;
_timelineController.Resume();
break;
case SystemMediaTransportControlsButton.Pause:
_smtc.PlaybackStatus = MediaPlaybackStatus.Paused;
_timelineController.Pause();
break;
case SystemMediaTransportControlsButton.Next:
PlayNextTrack();
break;
case SystemMediaTransportControlsButton.Previous:
PlayPreviousTrack();
break;
}
}
public void CancelRefreshSongs()
{
}
@@ -342,7 +186,7 @@ namespace BetterLyrics.WinUI3.ViewModels
if (File.Exists(path))
{
var m3uFileContent = File.ReadAllText(path);
_middleTracks = _allTracks.Where(t => m3uFileContent.Contains(t.DecodedAbsoluteUri)).ToList();
_middleTracks = _allTracks.Where(t => m3uFileContent.Contains(t.Uri.ToDecodedAbsoluteUri())).ToList();
}
else
{
@@ -458,133 +302,6 @@ namespace BetterLyrics.WinUI3.ViewModels
ApplyPlaylist();
}
public async Task PlayTrackAtAsync(int index)
{
await PlayTrackAsync(TrackPlayingQueue.ElementAtOrDefault(index));
}
public async Task PlayTrackAsync(PlayQueueItem? playQueueItem)
{
_timelineController.Pause();
_mediaPlayer.Source = null;
// 清理旧资源
_currentStream?.Dispose();
_currentNetStream?.Dispose();
_currentStream = null;
_currentNetStream = null;
if (playQueueItem == null)
{
_smtc.IsEnabled = false;
_smtc.DisplayUpdater.ClearAll();
}
else
{
PlayingTrack = playQueueItem.Track;
_smtc.IsEnabled = true;
try
{
var targetFolder = _settingsService.AppSettings.LocalMediaFolders.FirstOrDefault(f =>
{
var fUri = f.GetStandardUri().AbsoluteUri;
return PlayingTrack.Uri.StartsWith(fUri, StringComparison.OrdinalIgnoreCase);
});
if (targetFolder == null)
{
throw new FileNotFoundException(null, PlayingTrack.DecodedAbsoluteUri);
}
_currentProvider = targetFolder.CreateFileSystem();
if (_currentProvider == null) return;
await _currentProvider.ConnectAsync();
var fileCacheStub = new FilesIndexItem
{
Uri = PlayingTrack.Uri
};
var sourceStream = await _fileSystemService.OpenFileAsync(_currentProvider, fileCacheStub);
if (sourceStream == null)
{
throw new FileNotFoundException(null, fileCacheStub.Uri);
}
if (sourceStream.CanSeek)
{
_currentNetStream = sourceStream;
}
else
{
var memStream = new MemoryStream();
await sourceStream.CopyToAsync(memStream);
memStream.Position = 0;
sourceStream.Dispose();
_currentNetStream = memStream;
}
_currentStream = _currentNetStream.AsRandomAccessStream();
string contentType = GetMimeType(PlayingTrack.FileName);
var mediaSource = MediaSource.CreateFromStream(_currentStream, contentType);
_mediaPlayer.Source = mediaSource;
var updater = _smtc.DisplayUpdater;
updater.Type = MediaPlaybackType.Music;
updater.MusicProperties.Title = PlayingTrack.Title ?? PlayingTrack.FileName;
updater.MusicProperties.Artist = PlayingTrack.Artist ?? "";
updater.MusicProperties.AlbumTitle = PlayingTrack.Album ?? "";
updater.MusicProperties.Genres.Clear();
updater.MusicProperties.Genres.Add($"{ExtendedGenreFiled.FileName}{Path.GetFileNameWithoutExtension(PlayingTrack.FileName)}");
updater.AppMediaId = Package.Current.Id.FullName;
if (!string.IsNullOrEmpty(PlayingTrack.LocalAlbumArtPath) && File.Exists(PlayingTrack.LocalAlbumArtPath))
{
var storageFile = await StorageFile.GetFileFromPathAsync(PlayingTrack.LocalAlbumArtPath);
updater.Thumbnail = RandomAccessStreamReference.CreateFromFile(storageFile);
}
else
{
updater.Thumbnail = null;
}
updater.Update();
}
catch (Exception ex)
{
ToastHelper.ShowToast("Error", ex.Message, InfoBarSeverity.Error);
_timelineController.Pause();
}
}
}
private string GetMimeType(string path)
{
var ext = Path.GetExtension(path).ToLower();
return ext switch
{
".mp3" => "audio/mpeg",
".flac" => "audio/flac",
".wav" => "audio/wav",
".m4a" => "audio/mp4",
".aac" => "audio/aac",
".ogg" => "audio/ogg",
".wma" => "audio/x-ms-wma",
_ => "application/octet-stream"
};
}
partial void OnSongOrderTypeChanged(CommonSongProperty value)
{
ApplySongOrderType();
@@ -639,7 +356,24 @@ namespace BetterLyrics.WinUI3.ViewModels
[RelayCommand]
private async Task StopTrackAsync()
{
await PlayTrackAtAsync(-1);
await SMTCService.PlayTrackAtAsync(-1);
}
[RelayCommand]
private void OpenMediaSettings()
{
WindowHook.OpenOrShowWindow<SettingsWindow>();
var settingsPageViewModel = Ioc.Default.GetRequiredService<SettingsPageViewModel>();
settingsPageViewModel.NavViewSelectedItemTag = "MediaLib";
}
[RelayCommand]
private void ToggleSongListViewSelectionMode()
{
SongListViewSelectionMode =
SongListViewSelectionMode == ListViewSelectionMode.Single ?
ListViewSelectionMode.Multiple :
ListViewSelectionMode.Single;
}
public void Receive(PropertyChangedMessage<DateTime?> message)

View File

@@ -1,7 +1,8 @@
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.SMTCService;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -11,7 +12,9 @@ namespace BetterLyrics.WinUI3.ViewModels
{
public partial class NowPlayingBarViewModel : BaseViewModel
{
public IMediaSessionsService MediaSessionsService { get; private set; }
public IGSMTCService GSMTCService { get; private set; }
private readonly ISMTCService _smtcService;
[ObservableProperty]
public partial int Volume { get; set; }
@@ -31,9 +34,10 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial double BottomCommandFlyoutTriggerOpacity { get; set; }
public NowPlayingBarViewModel(IMediaSessionsService mediaSessionsService)
public NowPlayingBarViewModel(IGSMTCService mediaSessionsService, ISMTCService smtcService)
{
MediaSessionsService = mediaSessionsService;
GSMTCService = mediaSessionsService;
_smtcService = smtcService;
Volume = SystemVolumeHook.MasterVolume;
SystemVolumeHook.VolumeNotification += SystemVolumeHelper_VolumeNotification;
@@ -46,32 +50,38 @@ namespace BetterLyrics.WinUI3.ViewModels
partial void OnTimelineSliderThumbSecondsChanged(double value)
{
TimelineSliderThumbLyricsLine = MediaSessionsService.CurrentLyricsData?.GetLyricsLine(value);
TimelineSliderThumbLyricsLine = GSMTCService.CurrentLyricsData?.GetLyricsLine(value);
}
[RelayCommand]
private async Task PlaySongAsync()
{
await MediaSessionsService.PlayAsync();
await GSMTCService.PlayAsync();
}
[RelayCommand]
private async Task PauseSongAsync()
{
await MediaSessionsService.PauseAsync();
await GSMTCService.PauseAsync();
}
[RelayCommand]
private async Task PreviousSongAsync()
{
await MediaSessionsService.PreviousAsync();
await GSMTCService.PreviousAsync();
}
[RelayCommand]
private async Task NextSongAsync()
{
await MediaSessionsService.NextAsync();
await GSMTCService.NextAsync();
}
[RelayCommand]
private async Task StopTrackAsync()
{
await _smtcService.PlayTrackAtAsync(-1);
}
[RelayCommand]

View File

@@ -2,7 +2,7 @@
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.Input;
@@ -10,9 +10,9 @@ namespace BetterLyrics.WinUI3.ViewModels
{
public partial class NowPlayingPageViewModel : BaseViewModel
{
public IMediaSessionsService MediaSessionsService { get; private set; }
public IGSMTCService MediaSessionsService { get; private set; }
public NowPlayingPageViewModel(IMediaSessionsService mediaSessionsService)
public NowPlayingPageViewModel(IGSMTCService mediaSessionsService)
{
MediaSessionsService = mediaSessionsService;
}

View File

@@ -0,0 +1,27 @@
using BetterLyrics.WinUI3.Controls;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SMTCService;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class PlayQueueViewModel : BaseViewModel
{
private readonly ISettingsService _settingsService;
public ISMTCService SMTCService { get; set; }
[ObservableProperty] public partial AppSettings AppSettings { get; set; }
public PlayQueueViewModel(ISMTCService smtcService, ISettingsService settingsService)
{
_settingsService = settingsService;
SMTCService = smtcService;
AppSettings = _settingsService.AppSettings;
}
}
}

View File

@@ -2,7 +2,7 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.TranslationService;
@@ -20,7 +20,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
public partial class PlaybackSettingsControlViewModel : BaseViewModel
{
public IMediaSessionsService MediaSessionsService;
public IGSMTCService GSMTCService;
private readonly ITranslationService _translationService;
private readonly ILastFMService _lastFMService;
private readonly ISettingsService _settingsService;
@@ -55,12 +55,12 @@ namespace BetterLyrics.WinUI3.ViewModels
public PlaybackSettingsControlViewModel(
ISettingsService settingsService,
IMediaSessionsService mediaSessionsService,
IGSMTCService gsmtcService,
ITranslationService libreTranslationService,
ILastFMService lastFMService,
ITransliterationService transliterationService)
{
MediaSessionsService = mediaSessionsService;
GSMTCService = gsmtcService;
_settingsService = settingsService;
_translationService = libreTranslationService;
@@ -206,7 +206,7 @@ namespace BetterLyrics.WinUI3.ViewModels
{
PasswordVaultHelper.Delete(Constants.App.AppName, Constants.AppleMusic.MediaUserTokenKey);
PasswordVaultHelper.Save(Constants.App.AppName, Constants.AppleMusic.MediaUserTokenKey, AppleMusicMediaUserToken);
MediaSessionsService.UpdateLyrics();
GSMTCService.UpdateLyrics();
}
partial void OnSelectedTargetLanguageIndexChanged(int value)

View File

@@ -2,56 +2,192 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Stats;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LiveChartsCore;
using LiveChartsCore.Kernel;
using LiveChartsCore.Kernel.Sketches;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.Themes;
using Microsoft.UI.Xaml;
using SkiaSharp;
using SkiaSharp.Views.Windows;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class StatsDashboardControlViewModel : ObservableObject
{
private readonly IPlayHistoryService _playHistoryService;
private readonly ILocalizationService _localizationService;
private readonly IAlbumArtSearchService _albumArtSearchService;
public StatsDashboardControlViewModel(IPlayHistoryService playHistoryService)
{
_playHistoryService = playHistoryService;
}
private string _localizedTimesValue;
[ObservableProperty] public partial bool IsLoading { get; set; }
// 时间筛选
[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 TimeSpan TotalDuration { get; set; }
[ObservableProperty] public partial int TotalTracksPlayed { get; set; }
[ObservableProperty] public partial string TopPlayerName { get; set; } = "N/A";
public ObservableCollection<SongPlayCount> TopSongs { get; } = new();
public ObservableCollection<ArtistPlayCount> TopArtists { get; } = new();
// 时段分布
[ObservableProperty] public partial ObservableCollection<int> HourlySeriesValues { get; set; } = new();
[ObservableProperty] public partial ObservableCollection<string> HourlyXAxisLabels { get; set; } = [.. Enumerable.Range(0, 24).Select(x => $"{x:D2}:00")];
[ObservableProperty] public partial string PeakHourText { get; set; } = "--:--";
[ObservableProperty] public partial string QuietHourText { get; set; } = "--:--";
public ObservableCollection<PlayerStatDisplayItem> PlayerStats { get; } = new();
// 歌手
[ObservableProperty] public partial ObservableCollection<ArtistPlayCount> TopArtists { get; set; } = new();
// 播放源
[ObservableProperty] public partial ObservableCollection<ISeries> SourceSeries { get; set; } = new();
// 歌曲
[ObservableProperty] public partial ObservableCollection<SongPlayCount> TopSongs { get; set; } = new();
public StatsDashboardControlViewModel(IPlayHistoryService playHistoryService, ILocalizationService localizationService, IAlbumArtSearchService albumArtSearchService)
{
_playHistoryService = playHistoryService;
_localizationService = localizationService;
_albumArtSearchService = albumArtSearchService;
_localizedTimesValue = _localizationService.GetLocalizedString("StatsDashboardControlTimes");
SelectedTimeRange = StatsRange.Today;
CustomStartDate = DateTimeOffset.Now.AddDays(-7);
CustomEndDate = DateTimeOffset.Now;
}
async partial void OnSelectedTimeRangeChanged(StatsRange value)
{
IsCustomRangeSelected = value == StatsRange.Custom;
await LoadDataAsync();
}
async partial void OnCustomEndDateChanged(DateTimeOffset? value) => await LoadDataAsync();
async partial void OnCustomStartDateChanged(DateTimeOffset? value) => await LoadDataAsync();
private void ProcessHourlyStats(List<PlayHistoryItem> logs)
{
if (logs == null || !logs.Any())
{
PeakHourText = "--:--";
QuietHourText = "--:--";
HourlySeriesValues = new();
return;
}
var hourCounts = new int[24];
foreach (var log in logs)
{
hourCounts[log.StartedAt.ToLocalTime().Hour]++;
}
int peakHour = Array.IndexOf(hourCounts, hourCounts.Max());
PeakHourText = $"{peakHour:D2}:00 - {peakHour + 1:D2}:00";
int quietHour = Array.IndexOf(hourCounts, hourCounts.Min());
QuietHourText = $"{quietHour:D2}:00 - {quietHour + 1:D2}:00";
HourlySeriesValues = [.. hourCounts];
}
private void UpdatePlayerStats(List<PlayerStats> stats)
{
SourceSeries = new();
if (stats == null || stats.Count == 0)
{
TopPlayerName = "N/A";
return;
}
var topPlayer = stats.OrderByDescending(x => x.Count).FirstOrDefault();
TopPlayerName = PlayerIdHelper.GetDisplayName(topPlayer?.PlayerId) ?? "N/A";
var colors = PaletteHelper.GenerateChartColors(ColorHelper.GetSystemAccentColor(), stats.Count);
SourceSeries = [.. stats.OrderByDescending(x => x.Count).Select((x, i) => new PieSeries<int>
{
Values = [x.Count],
Name = PlayerIdHelper.GetDisplayName(x.PlayerId),
ToolTipLabelFormatter = point => $"{x.Count} {_localizedTimesValue}",
Pushout = 4, // 间隙
})];
}
private (DateTime? Start, DateTime? End) CalculateDateRange()
{
if (IsCustomRangeSelected)
{
return (CustomStartDate?.UtcDateTime, CustomEndDate?.UtcDateTime);
}
DateTime nowLocal = DateTime.Now;
DateTime startLocal = nowLocal.Date;
switch (SelectedTimeRange)
{
case StatsRange.Today:
startLocal = new DateTime(nowLocal.Year, nowLocal.Month, nowLocal.Day);
break;
case StatsRange.ThisWeek:
int dayOfWeek = (int)nowLocal.DayOfWeek;
if (dayOfWeek == 0) dayOfWeek = 7;
startLocal = nowLocal.Date.AddDays(-(dayOfWeek - 1));
break;
case StatsRange.ThisMonth:
startLocal = new DateTime(nowLocal.Year, nowLocal.Month, 1);
break;
case StatsRange.ThisQuarter:
int quarterStartMonth = (nowLocal.Month - 1) / 3 * 3 + 1;
startLocal = new DateTime(nowLocal.Year, quarterStartMonth, 1);
break;
case StatsRange.ThisYear:
startLocal = new DateTime(nowLocal.Year, 1, 1);
break;
}
return (startLocal.ToUniversalTime(), nowLocal.ToUniversalTime());
}
/// <summary>
/// 核心方法:根据选中的 Tab 加载数据
/// </summary>
[RelayCommand]
public async Task LoadDataAsync(StatsRange range)
public async Task LoadDataAsync()
{
if (IsLoading) return;
IsLoading = true;
try
{
var (start, end) = CalculateDateRange(range);
var (start, end) = CalculateDateRange();
var durationTask = _playHistoryService.GetTotalListeningDurationAsync(start, end);
var logsTask = _playHistoryService.GetLogsByDateRangeAsync(start, end);
var topSongsTask = _playHistoryService.GetTopSongsAsync(start, end, 10);
var topArtistsTask = _playHistoryService.GetTopArtistsAsync(start, end, 10);
var playersTask = _playHistoryService.GetPlayerDistributionAsync(start, end);
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);
@@ -59,13 +195,14 @@ namespace BetterLyrics.WinUI3.ViewModels
var logs = await logsTask;
TotalTracksPlayed = logs.Count;
TopSongs.Clear();
foreach (var item in await topSongsTask) TopSongs.Add(item);
TopSongs = [.. await topSongsTask];
TopArtists.Clear();
foreach (var item in await topArtistsTask) TopArtists.Add(item);
var pStats = await playersTask;
UpdatePlayerStats(pStats);
UpdatePlayerStats(await playersTask);
TopArtists = [.. await topArtistsTask];
ProcessHourlyStats(logs);
}
catch (Exception ex)
{
@@ -80,69 +217,9 @@ namespace BetterLyrics.WinUI3.ViewModels
[RelayCommand]
private async Task GenerateTestDataAsync()
{
await _playHistoryService.GenerateTestDataAsync(10000);
await _playHistoryService.GenerateTestDataAsync(1000);
await LoadDataAsync(); // 生成完刷新
}
/// <summary>
/// 将原始统计数据转换为带进度条宽度的 UI 数据
/// </summary>
private void UpdatePlayerStats(List<PlayerStats> stats)
{
PlayerStats.Clear();
if (stats == null || stats.Count == 0)
{
TopPlayerName = "N/A";
return;
}
double maxCount = stats.Max(x => x.Count);
if (maxCount == 0) maxCount = 1;
var topPlayer = stats.OrderByDescending(x => x.Count).FirstOrDefault();
TopPlayerName = PlayerIdHelper.GetDisplayName(topPlayer?.PlayerId) ?? "N/A";
foreach (var item in stats.OrderByDescending(x => x.Count))
{
PlayerStats.Add(new PlayerStatDisplayItem
{
PlayerId = item.PlayerId,
PlayCount = item.Count,
});
}
}
private (DateTime Start, DateTime End) CalculateDateRange(StatsRange range)
{
DateTime nowLocal = DateTime.Now;
DateTime startLocal = nowLocal.Date; // 默认为本地今天 00:00
switch (range)
{
case StatsRange.Day:
break;
case StatsRange.Week:
int dayOfWeek = (int)nowLocal.DayOfWeek;
if (dayOfWeek == 0) dayOfWeek = 7; // 处理周日
startLocal = nowLocal.Date.AddDays(-(dayOfWeek - 1));
break;
case StatsRange.Month:
startLocal = new DateTime(nowLocal.Year, nowLocal.Month, 1);
break;
case StatsRange.Quarter:
int quarterStartMonth = (nowLocal.Month - 1) / 3 * 3 + 1;
startLocal = new DateTime(nowLocal.Year, quarterStartMonth, 1);
break;
case StatsRange.Year:
startLocal = new DateTime(nowLocal.Year, 1, 1);
break;
}
// 数据库里的 StartedAt 是 UTC所以查询条件必须也是 UTC
DateTime startUtc = startLocal.ToUniversalTime();
DateTime endUtc = nowLocal.ToUniversalTime();
return (startUtc, endUtc);
}
}
}

View File

@@ -161,11 +161,21 @@
<NavigationViewItemSeparator Grid.Row="2" />
<StackPanel
Grid.Row="3"
Orientation="Horizontal"
Spacing="3">
<TextBlock
x:Uid="MusicGalleryPageFolder"
Grid.Row="3"
Margin="1,4,0,6"
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<Button
Command="{x:Bind ViewModel.OpenMediaSettingsCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE713;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
<TreeView
x:Name="FolderTreeView"
@@ -194,8 +204,12 @@
<controls:ContentSizer Grid.Column="1" TargetControl="{x:Bind LeftSidePanel}" />
<Grid x:Name="SongViewer" Grid.Column="2">
<Grid
x:Name="SongViewer"
Grid.Column="2"
RowSpacing="3">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" MinHeight="34" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
@@ -239,7 +253,10 @@
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoBitDepth" Value="{x:Bind ViewModel.TrackRightTapped.BitDepth, Mode=OneWay}" />
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoFormat" Value="{x:Bind ViewModel.TrackRightTapped.AudioFormatName, Mode=OneWay}" />
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoEncoder" Value="{x:Bind ViewModel.TrackRightTapped.Encoder, Mode=OneWay}" />
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoPath" Value="{x:Bind ViewModel.TrackRightTapped.DecodedAbsoluteUri, Mode=OneWay}" />
<uc:PropertyRow
x:Uid="MusicGalleryPageFileInfoPath"
Link="{x:Bind ViewModel.TrackRightTapped.Uri, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}"
Value="{x:Bind ViewModel.TrackRightTapped.Uri, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}" />
<uc:PropertyRow x:Uid="MusicGalleryPageFileInfoLyrics" Value="{x:Bind ViewModel.TrackRightTapped.RawLyrics, Mode=OneWay}" />
</StackPanel>
</Grid>
@@ -247,84 +264,105 @@
</Flyout>
</Grid.Tag>
<StackPanel Grid.Row="0" Spacing="6">
<AutoSuggestBox
x:Name="SongSearchBox"
x:Uid="MusicGalleryPageSongSearchBox"
Margin="0,0,128,0"
HorizontalAlignment="Stretch"
QueryIcon="Find"
Text="{x:Bind ViewModel.SongSearchQuery, Mode=TwoWay}" />
<Grid>
<StackPanel
HorizontalAlignment="Left"
Orientation="Horizontal"
Spacing="6">
<CheckBox
x:Name="SelectAllCheckBox"
MinWidth="20"
VerticalAlignment="Center"
Checked="SelectAllCheckBox_Checked"
Unchecked="SelectAllCheckBox_Unchecked"
Visibility="{Binding ElementName=SongListViewSelectionModeToggleButton, Path=IsChecked, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
<StackPanel VerticalAlignment="Center" Orientation="Horizontal">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.SelectedTracks.Count, Mode=OneWay}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="/" />
<TextBlock Text="{x:Bind GroupedTracksCVS.View.Count, Mode=OneWay}" />
</StackPanel>
<StackPanel VerticalAlignment="Center" Orientation="Horizontal">
<TextBlock Text="{x:Bind ViewModel.SelectedTracksTotalDuration, Mode=OneWay, Converter={StaticResource SecondsToFormattedTimeConverter}}" />
</StackPanel>
</StackPanel>
<StackPanel
HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="6">
<StackPanel Orientation="Horizontal" Spacing="12">
<TextBlock
x:Uid="MusicGalleryPageSortType"
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<controls:Segmented
x:Name="Segmented"
SelectedIndex="{x:Bind ViewModel.SongOrderType, Converter={StaticResource EnumToIntConverter}, Mode=TwoWay}"
SelectionMode="Single">
<controls:SegmentedItem x:Uid="MusicGalleryPageSortByTitle" Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEC4F;}" />
<controls:SegmentedItem x:Uid="MusicGalleryPageSortByAlbum" Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE93C;}" />
<controls:SegmentedItem x:Uid="MusicGalleryPageSortByArtist" Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xEFA9;}" />
<controls:SegmentedItem x:Uid="MusicGalleryPageSortByFolder" Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE8B7;}" />
</controls:Segmented>
</StackPanel>
</StackPanel>
</Grid>
</StackPanel>
<InfoBar
x:Uid="MusicGalleryPageDataSync"
Grid.Row="1"
Grid.Row="0"
IsClosable="False"
IsOpen="{x:Bind ViewModel.IsDataSyncing, Mode=OneWay}" />
<InfoBar
x:Uid="MusicGalleryPageDataSyncError"
Grid.Row="1"
Grid.Row="0"
IsClosable="False"
IsOpen="{x:Bind ViewModel.IsDataSyncError, Mode=OneWay}"
Severity="Error" />
<SemanticZoom Grid.Row="2">
<!-- 命令览 -->
<Grid Grid.Row="1" ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- 切换选择模式 -->
<ToggleButton
x:Name="SongListViewSelectionModeToggleButton"
Grid.Column="0"
Command="{x:Bind ViewModel.ToggleSongListViewSelectionModeCommand}"
Content="{ui:FontIcon FontSize=16,
FontFamily={StaticResource IconFontFamily},
Glyph=&#xE762;}"
Style="{StaticResource GhostToggleButtonStyle}" />
<!-- 为多选模式保留 -->
<Grid Grid.Column="1" Visibility="{Binding ElementName=SongListViewSelectionModeToggleButton, Path=IsChecked, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<CheckBox
x:Name="SelectAllCheckBox"
Grid.Column="0"
MinWidth="20"
VerticalAlignment="Center"
Checked="SelectAllCheckBox_Checked"
Unchecked="SelectAllCheckBox_Unchecked" />
<RichTextBlock Grid.Column="1" VerticalAlignment="Center">
<Paragraph>
<Run Text="{x:Bind ViewModel.SelectedTracks.Count, Mode=OneWay}" />
<Run Text="/" />
<Run Text="{x:Bind GroupedTracksCVS.View.Count, Mode=OneWay}" />
<Run Text="{x:Bind ViewModel.SelectedTracksTotalDuration, Mode=OneWay, Converter={StaticResource SecondsToFormattedTimeConverter}}" />
</Paragraph>
</RichTextBlock>
</Grid>
<AppBarSeparator Grid.Column="2" />
<!-- 排序选择 -->
<Grid Grid.Column="3" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock
x:Uid="MusicGalleryPageSortType"
Grid.Column="0"
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<ComboBox Grid.Column="1" SelectedIndex="{x:Bind ViewModel.SongOrderType, Converter={StaticResource EnumToIntConverter}, Mode=TwoWay}">
<ComboBoxItem x:Uid="MusicGalleryPageSortByTitle" />
<ComboBoxItem x:Uid="MusicGalleryPageSortByAlbum" />
<ComboBoxItem x:Uid="MusicGalleryPageSortByArtist" />
<ComboBoxItem x:Uid="MusicGalleryPageSortByFolder" />
</ComboBox>
</Grid>
<Grid Grid.Column="4">
<AutoSuggestBox
x:Name="SongSearchBox"
x:Uid="MusicGalleryPageSongSearchBox"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
QueryIcon="Find"
Text="{x:Bind ViewModel.SongSearchQuery, Mode=TwoWay}" />
</Grid>
</Grid>
<NavigationViewItemSeparator Grid.Row="2" />
<SemanticZoom Grid.Row="3">
<SemanticZoom.ZoomedInView>
<ListView
x:Name="SongListView"
ItemsSource="{x:Bind GroupedTracksCVS.View, Mode=OneWay}"
SelectionChanged="SongListView_SelectionChanged"
SelectionMode="Multiple">
SelectionMode="{x:Bind ViewModel.SongListViewSelectionMode, Mode=TwoWay}">
<ListView.ContextFlyout>
<MenuBarItemFlyout Opened="AddToMenuBarItemFlyout_Opened">
<MenuFlyoutSubItem x:Uid="MusicGalleryPageAddToPlayingQueue" IsEnabled="{x:Bind ViewModel.SelectedTracks.Count, Mode=OneWay, Converter={StaticResource IntToBoolConverter}}">
@@ -408,14 +446,12 @@
</ListView.ItemsPanel>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderContainerStyle>
<Style BasedOn="{StaticResource InteractiveListViewHeaderStyle}" TargetType="ListViewHeaderItem" />
</GroupStyle.HeaderContainerStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate x:DataType="models:GroupInfoList">
<Border AutomationProperties.AccessibilityView="Raw">
<TextBlock
AutomationProperties.AccessibilityView="Raw"
Style="{ThemeResource SubtitleTextBlockStyle}"
Text="{x:Bind}" />
</Border>
<TextBlock Text="{x:Bind}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
@@ -428,9 +464,7 @@
MaxWidth="500"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ItemsSource="{x:Bind GroupedTracksCVS.View.CollectionGroups, Mode=OneWay}"
ScrollViewer.IsHorizontalScrollChainingEnabled="False"
SelectionMode="None">
ItemsSource="{x:Bind GroupedTracksCVS.View.CollectionGroups, Mode=OneWay}">
<GridView.ItemTemplate>
<DataTemplate x:DataType="models:GroupInfoList">
<TextBlock Style="{ThemeResource TitleTextBlockStyle}" Text="{Binding}" />
@@ -440,7 +474,7 @@
</SemanticZoom.ZoomedOutView>
</SemanticZoom>
<Grid Grid.Row="2" Visibility="{x:Bind ViewModel.IsLocalMediaNotFound, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Grid Grid.Row="3" Visibility="{x:Bind ViewModel.IsLocalMediaNotFound, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
@@ -457,155 +491,5 @@
</Grid>
<Grid
x:Name="PlayQueue"
Width="300"
Margin="0,4,4,72"
Padding="12,16,12,0"
HorizontalAlignment="Right"
Background="{ThemeResource AcrylicBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource ControlElevationBorderBrush}"
BorderThickness="1"
CornerRadius="8"
Translation="310,0,0">
<Grid.TranslationTransition>
<Vector3Transition />
</Grid.TranslationTransition>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBlock
x:Uid="MusicGalleryPagePlayingQueue"
VerticalAlignment="Center"
Style="{StaticResource BodyStrongTextBlockStyle}" />
</Grid>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex, Mode=OneWay, Converter={StaticResource IndexToDisplayConverter}}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="/" />
<TextBlock Text="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}" />
</StackPanel>
<!-- Stop media session -->
<Button
Grid.Column="1"
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.StopTrackCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE71A;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageStopTrack" />
</ToolTipService.ToolTip>
</Button>
<!-- Scroll to playing item -->
<Button
Grid.Column="3"
HorizontalAlignment="Right"
Click="ScrollToPlayingItemButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE7B7;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageScrollToPlayingItem" />
</ToolTipService.ToolTip>
</Button>
<!-- Empty play queue -->
<Button
Grid.Column="4"
HorizontalAlignment="Right"
Click="EmptyPlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageEmptyPlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</Grid>
<ListView
x:Name="PlayingQueueListView"
Grid.Row="3"
ItemsSource="{x:Bind ViewModel.TrackPlayingQueue, Mode=OneWay}"
SelectedIndex="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Padding="0,6">
<Grid Tapped="PlayingQueueListVireItemGrid_Tapped">
<StackPanel Margin="0,0,36,0">
<TextBlock Text="{Binding Track.Title}" TextWrapping="Wrap" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{Binding Track.Artist}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
<Grid HorizontalAlignment="Right">
<Button
Click="RemoveFromPlayingQueueButton_Click"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE738;}"
Style="{StaticResource GhostButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="MusicGalleryPageRemoveFromPlayingQueue" />
</ToolTipService.ToolTip>
</Button>
</Grid>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid Grid.Row="3">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.TrackPlayingQueue.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12">
<Image MaxWidth="100" Source="/Assets/EmptyBox.png" />
<TextBlock
x:Uid="MusicGalleryPagePlayingQueueEmpty"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</Grid>
</Grid>
</Page>

View File

@@ -3,7 +3,7 @@ using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.SMTCService;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using DevWinUI;
@@ -14,6 +14,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -26,118 +27,38 @@ namespace BetterLyrics.WinUI3.Views
public sealed partial class MusicGalleryPage : Page
{
public MusicGalleryPageViewModel ViewModel => (MusicGalleryPageViewModel)DataContext;
public bool IsPlayingQueueOpened
{
get { return (bool)GetValue(IsPlayingQueueOpenedProperty); }
set { SetValue(IsPlayingQueueOpenedProperty, value); }
}
public static readonly DependencyProperty IsPlayingQueueOpenedProperty =
DependencyProperty.Register(nameof(IsPlayingQueueOpened), typeof(bool), typeof(MusicGalleryPage), new PropertyMetadata(false, OnDependencyPropertyChanged));
private readonly ISMTCService _smtcService = Ioc.Default.GetRequiredService<ISMTCService>();
public MusicGalleryPage()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<MusicGalleryPageViewModel>();
ViewModel.AppSettings.MusicGallerySettings.PropertyChanged += MusicGallerySettings_PropertyChanged;
}
private static void OnDependencyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MusicGalleryPage self)
{
if (e.Property == IsPlayingQueueOpenedProperty)
{
var newValue = (bool)e.NewValue;
self.PlayQueue.Translation = newValue ? new() : new(310, 0, 0);
}
}
}
private void ScrollToPlayingItem()
{
if (ViewModel.PlayingQueueItem == null) return;
if (PlayingQueueListView == null) return;
PlayingQueueListView.ScrollIntoView(ViewModel.PlayingQueueItem);
}
private void MusicGallerySettings_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MusicGallerySettings.PlayQueueIndex))
{
ScrollToPlayingItem();
}
}
private async void SongPathHyperlinkButton_Click(object sender, RoutedEventArgs e)
{
await LauncherHelper.SelectAndShowFile(((ExtendedTrack)((HyperlinkButton)sender).DataContext).DecodedAbsoluteUri);
}
private async void PlayingQueueListVireItemGrid_Tapped(object sender, TappedRoutedEventArgs e)
{
var item = (PlayQueueItem)((FrameworkElement)sender).DataContext;
await ViewModel.PlayTrackAsync(item);
PlayingQueueListView.ScrollIntoView(item);
}
private async void EmptyPlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
ViewModel.TrackPlayingQueue.Clear();
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = -1;
await ViewModel.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
}
private void ScrollToPlayingItemButton_Click(object sender, RoutedEventArgs e)
{
ScrollToPlayingItem();
}
private async void RemoveFromPlayingQueueButton_Click(object sender, RoutedEventArgs e)
{
bool playNext = false;
var item = (PlayQueueItem)((FrameworkElement)sender).DataContext;
int index = ViewModel.TrackPlayingQueue.IndexOf(item);
if (item == ViewModel.PlayingQueueItem)
{
playNext = true;
}
ViewModel.TrackPlayingQueue.Remove(item);
if (playNext)
{
if (ViewModel.TrackPlayingQueue.Count == 0)
{
index = -1;
}
else if (index >= ViewModel.TrackPlayingQueue.Count)
{
index = ViewModel.TrackPlayingQueue.Count - 1;
}
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = index;
await ViewModel.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
}
await LauncherHelper.SelectAndShowFile(((ExtendedTrack)((HyperlinkButton)sender).DataContext).Uri.ToDecodedAbsoluteUri());
}
private async void AddSongToQueueNextMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
bool startPlaying = ViewModel.TrackPlayingQueue.Count == 0;
ViewModel.TrackPlayingQueue.InsertRange(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex + 1, SongListView.SelectedItems.Cast<ExtendedTrack>().Select(x => new PlayQueueItem(x)));
bool startPlaying = _smtcService.TrackPlayingQueue.Count == 0;
_smtcService.TrackPlayingQueue.InsertRange(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex + 1, SongListView.SelectedItems.Cast<ExtendedTrack>().Select(x => new PlayQueueItem(x)));
if (startPlaying)
{
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex + 1;
await ViewModel.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
await _smtcService.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
}
}
private async void AddSongToQueueEndMenuFlyoutItem_Click(object sender, RoutedEventArgs e)
{
bool startPlaying = ViewModel.TrackPlayingQueue.Count == 0;
ViewModel.TrackPlayingQueue.AddRange(SongListView.SelectedItems.Cast<ExtendedTrack>().Select(x => new PlayQueueItem(x)));
bool startPlaying = _smtcService.TrackPlayingQueue.Count == 0;
_smtcService.TrackPlayingQueue.AddRange(SongListView.SelectedItems.Cast<ExtendedTrack>().Select(x => new PlayQueueItem(x)));
if (startPlaying)
{
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex + 1;
await ViewModel.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
await _smtcService.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
}
}
@@ -228,9 +149,12 @@ namespace BetterLyrics.WinUI3.Views
}
private void SelectAllCheckBox_Checked(object sender, RoutedEventArgs e)
{
if (ViewModel.SongListViewSelectionMode == ListViewSelectionMode.Multiple)
{
SongListView.SelectAll();
}
}
private void SelectAllCheckBox_Unchecked(object sender, RoutedEventArgs e)
{
@@ -243,12 +167,12 @@ namespace BetterLyrics.WinUI3.Views
var track = (ExtendedTrack)((FrameworkElement)sender).DataContext;
// Play all the songs
ViewModel.TrackPlayingQueue.Clear();
_smtcService.TrackPlayingQueue.Clear();
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = -1;
ViewModel.TrackPlayingQueue.InsertRange(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex + 1, displayedTracks.Select(x => new PlayQueueItem(x)));
_smtcService.TrackPlayingQueue.InsertRange(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex + 1, displayedTracks.Select(x => new PlayQueueItem(x)));
ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex = displayedTracks.ToList().IndexOf(track);
await ViewModel.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
await _smtcService.PlayTrackAtAsync(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
}
private void Page_Loaded(object sender, RoutedEventArgs e)
@@ -256,9 +180,12 @@ namespace BetterLyrics.WinUI3.Views
var settings = ViewModel.AppSettings.MusicGallerySettings;
if (settings.AutoPlay)
{
_ = ViewModel.PlayTrackAtAsync(settings.PlayQueueIndex);
Task.Run(async () =>
{
await Task.Delay(1000);
_ = _smtcService.PlayTrackAtAsync(settings.PlayQueueIndex);
});
}
ScrollToPlayingItem();
}
private void FolderTreeView_ItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args)
@@ -280,7 +207,7 @@ namespace BetterLyrics.WinUI3.Views
if (File.Exists(path))
{
var content = File.ReadAllText(path);
foreach (var item in ViewModel.SelectedTracks.Select(x => x.DecodedAbsoluteUri).ToList())
foreach (var item in ViewModel.SelectedTracks.Select(x => x.Uri.ToDecodedAbsoluteUri()).ToList())
{
if (!content.Contains(item))
{
@@ -316,5 +243,6 @@ namespace BetterLyrics.WinUI3.Views
}
}
}
}
}

View File

@@ -16,7 +16,7 @@
Loaded="RootGrid_Loaded"
Unloaded="RootGrid_Unloaded">
<local:MusicGalleryPage x:Name="MusicGalleryPage" IsPlayingQueueOpened="{Binding ElementName=NowPlayingBar, Path=IsPlayingQueueOpened, Mode=OneWay}" />
<local:MusicGalleryPage x:Name="MusicGalleryPage" />
<local:NowPlayingPage
x:Name="NowPlayingPage"
@@ -32,15 +32,28 @@
x:Name="NowPlayingBar"
VerticalAlignment="Bottom"
IsAutoHideEnabled="False"
PlayQueueButtonClick="NowPlayingBar_PlayingQueueClick"
PlaybackOrder="{x:Bind ViewModel.AppSettings.MusicGallerySettings.PlaybackOrder, Mode=TwoWay}"
ShowPlaybackOrderButton="True"
ShowPlayingQueueButton="True"
ShowSongInfo="True"
ShowStopButton="True"
SongInfoTapped="NowPlayingBar_SongInfoTapped"
TimeTapped="NowPlayingBar_TimeTapped" />
<!-- Title bar -->
<StackPanel VerticalAlignment="Top" Orientation="Horizontal" />
TimeTapped="NowPlayingBar_TimeTapped">
<uc:NowPlayingBar.ContextFlyout>
<Flyout x:Name="PlayQueueFlyout" ShouldConstrainToRootBounds="False">
<Flyout.FlyoutPresenterStyle>
<Style BasedOn="{StaticResource FlyoutGhostStyle}" TargetType="FlyoutPresenter">
<Setter Property="MaxHeight" Value="600" />
</Style>
</Flyout.FlyoutPresenterStyle>
<uc:PlayQueue
x:Name="PlayQueue"
Width="300"
MaxHeight="600" />
</Flyout>
</uc:NowPlayingBar.ContextFlyout>
</uc:NowPlayingBar>
</Grid>

View File

@@ -4,7 +4,7 @@ using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
@@ -14,6 +14,7 @@ using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Imaging;
using System.Threading.Tasks;
using static Vanara.PInvoke.AdvApi32;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -30,7 +31,7 @@ namespace BetterLyrics.WinUI3.Views
{
public MusicGalleryWindowViewModel ViewModel { get; private set; } = Ioc.Default.GetRequiredService<MusicGalleryWindowViewModel>();
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
private readonly IGSMTCService _gsmtcService = Ioc.Default.GetRequiredService<IGSMTCService>();
public MusicGalleryWindow()
{
@@ -47,7 +48,7 @@ namespace BetterLyrics.WinUI3.Views
private void UpdateAlbumArtThemeColors()
{
var result = _mediaSessionsService.CalculateAlbumArtThemeColors(
var result = _gsmtcService.CalculateAlbumArtThemeColors(
ViewModel.AppSettings.MusicGallerySettings.LyricsWindowStatus, Colors.Transparent);
NowPlayingPage.AlbumArtThemeColors = result;
@@ -69,9 +70,9 @@ namespace BetterLyrics.WinUI3.Views
public void Receive(PropertyChangedMessage<BitmapImage?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapImage))
if (message.PropertyName == nameof(IGSMTCService.AlbumArtBitmapImage))
{
UpdateAlbumArtThemeColors();
}
@@ -131,7 +132,14 @@ namespace BetterLyrics.WinUI3.Views
private void NowPlayingBar_PlayingQueueClick(object sender, System.EventArgs e)
{
MusicGalleryPage.IsPlayingQueueOpened = !MusicGalleryPage.IsPlayingQueueOpened;
if (PlayQueueFlyout.IsOpen)
{
PlayQueueFlyout.Hide();
}
else
{
PlayQueueFlyout.ShowAt(NowPlayingBar);
}
}
}
}

View File

@@ -6,7 +6,7 @@ using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
@@ -34,7 +34,7 @@ namespace BetterLyrics.WinUI3.Views
IRecipient<PropertyChangedMessage<bool>>,
IRecipient<PropertyChangedMessage<string>>
{
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
private readonly IGSMTCService _gsmtcService = Ioc.Default.GetRequiredService<IGSMTCService>();
private readonly DispatcherQueueTimer _layoutChangedTimer = App.Current.Resources.DispatcherQueue.CreateTimer();
private readonly DispatcherQueueTimer _scrollChangedTimer = App.Current.Resources.DispatcherQueue.CreateTimer();
@@ -114,9 +114,9 @@ namespace BetterLyrics.WinUI3.Views
var artistsFontSize = albumArtLayoutSettings.IsAutoSongInfoFontSize ? lyricsLayoutMetrics.ArtistNameSize : albumArtLayoutSettings.SongInfoFontSize * 0.8;
var albumFontSize = albumArtLayoutSettings.IsAutoSongInfoFontSize ? lyricsLayoutMetrics.AlbumNameSize : albumArtLayoutSettings.SongInfoFontSize * 0.8;
RenderTextBlock(TitleTextBlock, _mediaSessionsService.CurrentSongInfo?.Title, titleFontSize);
RenderTextBlock(ArtistsTextBlock, _mediaSessionsService.CurrentSongInfo?.DisplayArtists, artistsFontSize);
RenderTextBlock(AlbumTextBlock, _mediaSessionsService.CurrentSongInfo?.Album, albumFontSize);
RenderTextBlock(TitleTextBlock, _gsmtcService.CurrentSongInfo?.Title, titleFontSize);
RenderTextBlock(ArtistsTextBlock, _gsmtcService.CurrentSongInfo?.DisplayArtists, artistsFontSize);
RenderTextBlock(AlbumTextBlock, _gsmtcService.CurrentSongInfo?.Album, albumFontSize);
}
private void UpdateSongInfoOpacity()
@@ -555,7 +555,7 @@ namespace BetterLyrics.WinUI3.Views
private void LyricsScrollViewer_PointerReleased(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
LyricsCanvas.IsMousePressing = false;
_mediaSessionsService.ChangeLyricsLine(LyricsCanvas.CurrentHoveringLineIndex);
_gsmtcService.ChangeLyricsLine(LyricsCanvas.CurrentHoveringLineIndex);
}
private void LyricsScrollViewer_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
@@ -577,9 +577,9 @@ namespace BetterLyrics.WinUI3.Views
public async void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
if (message.PropertyName == nameof(IGSMTCService.CurrentSongInfo))
{
SongInfoStackPanel.Opacity = 0;
await Task.Delay(Constants.Time.AnimationDuration);

View File

@@ -5,7 +5,7 @@ using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
@@ -47,7 +47,7 @@ namespace BetterLyrics.WinUI3.Views
public LyricsWindowStatus LyricsWindowStatus { get; private set; }
public NowPlayingWindowViewModel ViewModel { get; private set; } = Ioc.Default.GetRequiredService<NowPlayingWindowViewModel>();
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
private readonly IGSMTCService _gsmtcService = Ioc.Default.GetRequiredService<IGSMTCService>();
private readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
public NowPlayingWindow(LyricsWindowStatus status)
@@ -141,7 +141,7 @@ namespace BetterLyrics.WinUI3.Views
private void UpdateAlbumArtThemeColors()
{
var result = _mediaSessionsService.CalculateAlbumArtThemeColors(LyricsWindowStatus, _backdropAccentColor);
var result = _gsmtcService.CalculateAlbumArtThemeColors(LyricsWindowStatus, _backdropAccentColor);
NowPlayingPage.AlbumArtThemeColors = result;
RootGrid.RequestedTheme = result.ThemeType;
@@ -231,7 +231,7 @@ namespace BetterLyrics.WinUI3.Views
private void OnAutoShowOrHideWindowChanged()
{
this.SetLyricsWindowVisibilityByPlayingStatus(_mediaSessionsService.CurrentIsPlaying, DispatcherQueue);
this.SetLyricsWindowVisibilityByPlayingStatus(_gsmtcService.CurrentIsPlaying, DispatcherQueue);
}
private void OnIsAdaptToEnvironmentChanged()
@@ -466,9 +466,9 @@ namespace BetterLyrics.WinUI3.Views
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentIsPlaying))
if (message.PropertyName == nameof(IGSMTCService.CurrentIsPlaying))
{
OnAutoShowOrHideWindowChanged();
}
@@ -520,9 +520,9 @@ namespace BetterLyrics.WinUI3.Views
public void Receive(PropertyChangedMessage<BitmapImage?> message)
{
if (message.Sender is IMediaSessionsService)
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapImage))
if (message.PropertyName == nameof(IGSMTCService.AlbumArtBitmapImage))
{
UpdateAlbumArtThemeColors();
}

View File

@@ -3,7 +3,7 @@ using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
@@ -24,7 +24,7 @@ namespace BetterLyrics.WinUI3.Views;
public sealed partial class SystemTrayWindow : Window, IRecipient<PropertyChangedMessage<List<string>>>
{
private ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly IMediaSessionsService _mediaSessionsService = Ioc.Default.GetRequiredService<IMediaSessionsService>();
private readonly IGSMTCService _gsmtcService = Ioc.Default.GetRequiredService<IGSMTCService>();
private WindowMessageMonitor _wmm;
@@ -92,13 +92,13 @@ public sealed partial class SystemTrayWindow : Window, IRecipient<PropertyChange
{
GlobalHotKeyHook.UpdateHotKey(this, ShortcutID.PlayOrPauseSong, _settingsService.AppSettings.GeneralSettings.PlayOrPauseShortcut, (() =>
{
if (_mediaSessionsService.CurrentIsPlaying)
if (_gsmtcService.CurrentIsPlaying)
{
_ = _mediaSessionsService.PauseAsync();
_ = _gsmtcService.PauseAsync();
}
else
{
_ = _mediaSessionsService.PlayAsync();
_ = _gsmtcService.PlayAsync();
}
}));
}
@@ -107,7 +107,7 @@ public sealed partial class SystemTrayWindow : Window, IRecipient<PropertyChange
{
GlobalHotKeyHook.UpdateHotKey(this, ShortcutID.PreviousSong, _settingsService.AppSettings.GeneralSettings.PreviousSongShortcut, () =>
{
_ = _mediaSessionsService.PreviousAsync();
_ = _gsmtcService.PreviousAsync();
});
}
@@ -115,7 +115,7 @@ public sealed partial class SystemTrayWindow : Window, IRecipient<PropertyChange
{
GlobalHotKeyHook.UpdateHotKey(this, ShortcutID.NextSong, _settingsService.AppSettings.GeneralSettings.NextSongShortcut, () =>
{
_ = _mediaSessionsService.NextAsync();
_ = _gsmtcService.NextAsync();
});
}

BIN
Promotion/banner_fade.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

View File

@@ -1,239 +1,185 @@
![](Promotion/banner.png)
[**中文**](README.CN.md) | [**English**](README.md)
<div align=center>
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="96">
</div>
<div align="center">
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="Logo" width="120">
<h2 align=center>
BetterLyrics
</h2>
<h1>BetterLyrics</h1>
<h4 align="center">
🤩 一款优雅且高度自定义的歌词/播放应用,基于 WinUI3/Win2D 构建
<h4>
🤩 一款优雅且高度自定义的歌词可视化与全能音乐播放应用 <br>
基于 WinUI3 / Win2D 构建
</h4>
<div align="center">
<div>
<img src="https://img.shields.io/badge/Language-C%23-purple" alt="C#">
<img src="https://img.shields.io/badge/Framework-WinUI%203-blue" alt="WinUI 3">
<img src="https://img.shields.io/badge/License-GPL_v3.0-blue" alt="License">
<a href="https://github.com/jayfunc/BetterLyrics/stargazers"><img src="https://img.shields.io/github/stars/jayfunc/BetterLyrics" alt="Stars"></a>
<a href="https://crowdin.com/project/betterlyrics"><img src="https://badges.crowdin.net/betterlyrics/localized.svg" alt="Crowdin"></a>
</div>
[使用指南](https://github.com/jayfunc/BetterLyrics/wiki/使用指南) | [隐私政策](PrivacyPolicy.CN.md) | [服务协议](TermsofService.CN.md)
<br>
<img src="Promotion/banner.png" alt="Banner" width="100%" style="border-radius: 10px;">
</div>
<div align=center>
<br>
![Static Badge](https://img.shields.io/badge/Language-C%23-purple)
![Static Badge](https://img.shields.io/badge/License-GPL_v3.0-blue)
![Static Badge](https://img.shields.io/badge/IDE-Visual%20Studio-purple)
![Static Badge](https://img.shields.io/badge/Framework-WinUI%203-blue)
</div>
<div align=center>
[![GitHub Repo stars](https://img.shields.io/github/stars/jayfunc/BetterLyrics)](https://github.com/jayfunc/BetterLyrics/stargazers)
[![Crowdin](https://badges.crowdin.net/betterlyrics/localized.svg)](https://crowdin.com/project/betterlyrics)
</div>
<div align=center>
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics)
</div>
## 🔥 精选推荐与社区
<div align="center">
<mark>**_💞 BetterLyrics 的发展离不开每一位贡献者、反馈者和用户的全力支持。_**</mark>
| HelloGitHub 推荐 | 少数派 SSPAI 推荐 | 🤖 AI 问答 |
| :---: | :---: | :---: |
| <a href="https://hellogithub.com/repository/jayfunc/BetterLyrics" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=d2af74f0aea146ad8e4b2086982f5777&claim_uid=SgtQs9c54C8wjnv" alt="HelloGitHub" height="40"></a> | [**阅读评测文章**](https://sspai.com/post/101028) | [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics) <br> [![Zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTk5QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics) |
<mark>**_项目持续活跃开发中可能会遇到未知问题。_**</mark>
**交流群:** [QQ 群 (1054700388)](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) | [Discord](https://discord.gg/5yAQPnyCKv) | [Telegram](https://t.me/+svhSLZ7awPsxNGY1)
</div>
## ✍️ 协助翻译
## 🧪 下载与安装
找不到你的语言?有更好的翻译?没关系!😆 访问 [此处](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file) 查看如何贡献翻译!
<div align="center">
## 🎉 该项目已被 HelloGithub 推荐!
| Microsoft Store (推荐) | 手动安装 |
| :---: | :---: |
| <a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/en-us%20dark.svg" width="160"/></a><br>无限期免费试用(功能与付费版一致) | [**📦 最新版本 (.zip)**](https://github.com/jayfunc/BetterLyrics/releases/latest)<br>[查看安装指南](https://www.cnblogs.com/jayfunc/p/19212078) |
<a href="https://hellogithub.com/repository/jayfunc/BetterLyrics" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=d2af74f0aea146ad8e4b2086982f5777&claim_uid=SgtQs9c54C8wjnv" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
[📖 用户指南](https://github.com/jayfunc/BetterLyrics/wiki/使用指南) | [🔒 隐私政策](PrivacyPolicy.CN.md) | [⚖️ 服务条款](TermsofService.CN.md)
## 🎉 该项目入选少数派推荐文章!
</div>
文章链接:[BetterLyrics - 一款专为 Windows 打造的沉浸式流畅歌词显示软件](https://sspai.com/post/101028)。
## 🌟 核心功能
## 🔈 反馈交流群
- 🎨 **绝美视觉与 UI**
- **优雅设计:** 基于 WinUI3 & Win2D 的流畅、高度个性化风格。
- **沉浸特效:** 支持流体背景、3D/扇形歌词、雪花粒子等多种效果。
- **深度定制:** 按需配置动画、字体和行为逻辑,打造你的专属播放器。
[QQ 群](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388) | [Discord Server](https://discord.gg/5yAQPnyCKv) | [Telegram Group](https://t.me/+svhSLZ7awPsxNGY1)
- 🎧 **多功能播放与连接**
- **内置播放器:** 支持播放 **本地硬盘** 文件或通过 **网络协议** (SMB, WebDAV) 流式播放。
- **外部集成:** 可视化来自 Spotify, Apple Music, 网易云音乐及 [其他多种播放器](https://github.com/jayfunc/BetterLyrics/wiki/使用指南#已知支持的音乐播放器配置指南) 的音乐。
## 🌟 特色功能
- 🌐 **强大的歌词系统**
- **离线翻译:** 注重隐私的本地机器翻译(支持 30+ 种语言)。
- **全面源支持:** 支持 .lrc (标准/增强), .eslrc, .ttml, 内嵌标签以及在线源QQ 音乐, 网易云, LRCLIB
- **Apple Music** 支持歌词获取(需配置 Token
- 🌠 **精美的用户界面**
- 流畅、高度自定义的样式、动画、动效
- 沉浸式流体背景
- 透视/扇形歌词
- 雪花效果
- 多种歌词滚动函数
- ...
- ↔️ **强大的歌词翻译**
- 本地机器翻译 (支持 30 多种语言)
- 自动读取本地音乐文件内嵌歌词
- 🧩 **多种歌词源**
- 💾 本地源
- 音乐文件 (内嵌歌词)
- [.lrc](<https://en.wikipedia.org/wiki/LRC_(file_format)>) 文件 (传统格式、增强格式)
- [.eslrc](https://github.com/ESLyric/release) 文件
- [.ttml](https://en.wikipedia.org/wiki/Timed_Text_Markup_Language) 文件
- ☁️ 在线源
- QQ 音乐
- 网易云音乐
- 酷狗音乐
- [amll-ttml-db](https://github.com/Steve-xmh/amll-ttml-db)
- [LRCLIB](https://lrclib.net/)
- <details><summary>⚠️ Apple Music (需要额外配置)</summary>
- 🪟 **全场景显示模式**
- **标准模式:** 全屏沉浸式体验。
- **停靠模式:** 贴附于屏幕边缘的精致侧边栏。
- **桌面悬浮:** 悬浮于所有应用之上的歌词挂件。
- 浏览器打开 Apple Music打开开发者工具。刷新网页回到开发者工具窗口筛选出 Fetch/XHR选择一个请求在请求标头中找到 media-user-token 并复制其值。
- 打开 BetterLyrics 转到播放源设置。在 Media-User-Token (for Apple Music) 中粘贴复制的值并点按右侧对勾
- 🧠 **智能行为**
- 音乐暂停时自动隐藏
- 🎶 **支持众多音乐播放器**
## 🖼️ 软件截图
- 点击 [此处](https://github.com/jayfunc/BetterLyrics/wiki/使用指南#已知支持的音乐播放器配置指南) 查看详细信息
<div align="center">
- 🪟 **多种显示模式**
- **标准模式**
- 标准的歌词窗口样式,沉浸式的音乐歌词体验。
- **停靠模式**
- 停靠在屏幕上/下边缘的轻量歌词窗口,工作休闲互不打扰。
- **桌面模式**
- 悬浮在所有应用上层,不能被选中,但能直击你的使用需求。
- **更多模式...**
- 等你来发现...
- 🧠 **智能化行为**
- 根据歌曲播放状态自动显隐歌词窗口
| 标准视图 | 侧边栏模式 |
| :---: | :---: |
| <img src="Screenshots/std.png" width="100%"> | <img src="Screenshots/narrow.png" width="100%"> |
## 🖼️ 屏幕截图
| 歌词视觉特效 | 多模式共存 |
| :---: | :---: |
| <img src="Screenshots/effect.png" width="100%"> | <img src="Screenshots/all-in-one.png" width="100%"> |
![](Screenshots/fs2.png)
![](Screenshots/std.png)
![](Screenshots/narrow.png)
![](Screenshots/Snipaste_2025-10-31_19-23-17.png)
![](Screenshots/Snipaste_2025-10-31_19-27-34.png)
![](Screenshots/dock.png)
![](Screenshots/desktop.png)
| 全屏模式 | 全屏模式 |
| :---: | :---: |
| <img src="Screenshots/fs3.png" width="100%"> | <img src="Screenshots/fs2.png" width="100%"> |
> ⚠️ 由于 GIF 格式帧率限制,效果仅作展示。请以实机效果为准。
| 音乐库 | 播放统计 |
| :---: | :---: |
| <img src="Screenshots/music-gallery.png" width="100%"> | <img src="Screenshots/stats.png" width="100%"> |
![](Screenshots/PixPin_2025-10-24_18-13-44.gif)
![](Screenshots/PixPin_2025-10-24_18-17-17.gif)
</div>
## 📹 演示
## 📹 演示视频
在 [哔哩哔哩](https://www.bilibili.com/video/BV1QRstz1EGt/) 上观看于 2025 年 10 月 21 日上传的演示视频
> 观看我们在 Bilibili 发布的演示视频(上传于 2025 年 10 月 21 日):[点击观看](https://www.bilibili.com/video/BV1QRstz1EGt/)
## 🧪 即刻体验
## ✍️ 贡献与构建
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
<img src="https://get.microsoft.com/images/zh-cn%20dark.svg" width="200"/>
</a>
**协助翻译:** 找不到你的语言?[点此开始翻译](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file)。
**无限期**免费试用版和付费版**无任何区别**
**从源码构建:**
> 构建前,请确保已替换 `Constants` 文件夹下的 `DiscordTemplate.cs` 和 `LastFM.cs`。
如果喜欢该软件,请考虑 [捐赠](#-捐赠) 或在 **Microsoft Store** 购买,感谢您的支持! 🥰
## 🤑 赞助与捐赠
无法从 Microsoft Store 下载?尝试以下方法:
- [从 Microsoft Store 外部下载](https://www.cnblogs.com/jayfunc/p/19212083)
- 转至 [最新发布页](https://github.com/jayfunc/BetterLyrics/releases/latest) 并从 `Assets`(资产)列表下载 `.zip` 文件。(安装方法参考 [此文档](https://www.cnblogs.com/jayfunc/p/19212078)。)
如果你喜欢 BetterLyrics请考虑支持它。你的支持有助于项目持续发展
## 🏗️ 构建
<div align="center">
在构建之前确保:
- 替换文件 `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\DiscordTemplate``BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\DiscordTemplate.cs`.
- 替换文件 `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\LastFMTemplate``BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\LastFM.cs`
| 网页平台 | 支付宝 (扫码) | 微信 (扫码) |
| :---: | :---: | :---: |
| [PayPal](https://paypal.me/zhefangpay)<br><br>[Buy Me a Coffee](https://buymeacoffee.com/founchoo)<br><br>[爱发电 (Afdian)](https://afdian.com/a/jayfunc) | <img src="Donate/Alipay.jpg" width="150"> | <img src="Donate/WeChatReward.png" width="150"> |
## 🤑 捐赠
**[查看完整赞助者名单 (Hall of Fame)](SPONSORS.md)**
如果你喜欢本应用,请考虑捐赠支持开发者。这将有助于本应用的长远发展。
</div>
通过以下途径捐赠:
- [PayPal](https://paypal.me/zhefangpay)
- [Buy Me a Coffee](https://buymeacoffee.com/founchoo)
- [爱发电](https://afdian.com/a/jayfunc)
- <details><summary>支付宝</summary>
## ⭐ Star 历史趋势
![](Donate/Alipay.jpg)
<div align="center">
<img src="https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date" width="100%">
</div>
</detais>
## 📄 许可与致谢
- <details><summary>微信</summary>
本项目采用 **GNU General Public License v3.0** 许可证。
![](Donate/WeChatReward.png)
<details>
<summary><b>💖 特别致谢、引用与灵感</b></summary>
<br>
</details>
**依赖与引用:**
本项目的持续发展离不开大家的支持。**[查看完整鸣谢名单](SPONSORS.md)**
## 📄 许可证
本项目采用 GNU 通用公共许可证 v3.0 授权。详情请参阅 [LICENSE](https://github.com/jayfunc/BetterLyrics/blob/dev/LICENSE) 文件。
## 💖 感谢
部分功能及代码引用或修改自公开资料库,包括但不限于下述开源项目/包、教程等,在此一并感谢。
| 项目/包 | 描述 |
| 项目/包 (Projects/Packages) | 描述 (Description) |
| :--- | :--- |
| [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) | 为 QQ、网易、酷狗在线歌词源提供歌词抓取、解密、解析等一系列方法 |
| [lrclib](https://github.com/tranxuanthang/lrclib) | LRCLIB 歌词 API |
| [Manzana-Apple-Music-Lyrics](https://github.com/dropcreations/Manzana-Apple-Music-Lyrics) | Apple Music 歌词抓取Python 实现) |
| [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet) | 从音乐文件提取图片 |
| [WinUIEx](https://github.com/dotMorten/WinUIEx) | 提供有关窗口的开箱即用的 Win32 API |
| [TagLib#](https://github.com/mono/taglib-sharp) | 读取音乐文件内嵌的原始歌词内容 |
| [Vanara](https://github.com/dahall/Vanara) | 提供开箱即用的 Win32 API |
| [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) | 离线翻译核心 |
| [Isolation](https://github.com/Storyteller-Studios/Isolation) | 动态流体背景 |
| [SpectrumVisualization](https://github.com/Johnwikix/SpectrumVisualization) | 频谱图 |
| [DevWinUI](https://github.com/ghost1372/DevWinUI) | WinUI3 提供众多开箱即用的功能 |
| ... | ... |
| [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet) | 用于从音乐文件中提取图片(封面) |
| [cutlet](https://github.com/polm/cutlet) | 提供将日语转换为罗马音 (Romaji) 的功能 |
| [DevWinUI](https://github.com/ghost1372/DevWinUI) | 为构建 WinUI 3 应用程序提供许多开箱即用的功能 |
| [Isolation](https://github.com/Storyteller-Studios/Isolation) | 动态流体背景的实现 |
| [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) | 提供离线歌词翻译功能 |
| [lrclib](https://github.com/tranxuanthang/lrclib) | LRCLIB 歌词 API 数据源 |
| [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) | 提供 QQ、网易云音乐和酷狗音乐源的歌词抓取、解密及解析功能 |
| [Manzana-Apple-Music-Lyrics](https://github.com/dropcreations/Manzana-Apple-Music-Lyrics) | 使用 Python 获取 Apple Music 歌词 |
| [SpectrumVisualization](https://github.com/Johnwikix/SpectrumVisualization) | 音频频谱可视化参考 |
| [TagLib#](https://github.com/mono/taglib-sharp) | 用于读取原始歌词内容(元数据) |
| [Vanara](https://github.com/dahall/Vanara) | Win32 API 封装库 |
| [WinUIEx](https://github.com/dotMorten/WinUIEx) | 提供访问与窗口管理相关的 Win32 API 的便捷方式 |
点按 [此处](https://github.com/jayfunc/BetterLyrics/network/dependencies) 查看所有依赖
查看 [完整依赖列表](https://github.com/jayfunc/BetterLyrics/network/dependencies)。
### 教程、博客等
<br>
- [Stackoverflow - How to animate Margin property in WPF](https://stackoverflow.com/a/21542882/11048731)
- [Bilibili -【WinUI3】SystemBackdropController定义云母、亚克力效果](https://www.bilibili.com/video/BV1PY4FevEkS)
- [cnblogs - .NET App 与 Windows 系统媒体控制(SMTC)交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D 中的游戏循环CanvasAnimatedControl](https://www.cnblogs.com/walterlv/p/10236395.html)
- [r2d2rigo/Win2D-Samples](https://github.com/r2d2rigo/Win2D-Samples/blob/master/IrisBlurWin2D/IrisBlurWin2D/MainPage.xaml.cs)
- [CommunityToolkit - 从入门到精通](https://mvvm.coldwind.top/)
## 💡 灵感来源
部分设计思路参考自下述插件/软件(不含间接或直接引用、修改的代码,仅作为设计思路指导方向)。
**💡 灵感来源:**
部分设计理念参考了以下插件/软件(仅作为设计思路参考,不涉及代码引用):
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
- [Lyricify-App](https://github.com/WXRIW/Lyricify-App)
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
- [MyToolBar](https://github.com/TwilightLemon/MyToolBar)
## ⭐ 星标记录
</details>
<div style="display: flex; justify-content: space-around; align-items: flex-start;">
<img src="https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date)](https://www.star-history.com/#jayfunc/BetterLyrics&Date" width="100%" >
## 💭 分享到社交媒体
<details>
<summary><b>点击展开</b></summary>
<br>
<div align="center">
<img src="https://socialify.git.ci/jayfunc/BetterLyrics/image?description=1&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light" width="48%">
<img src="https://opengraph.githubassets.com/<any_hash_number>/jayfunc/BetterLyrics" width="48%">
</div>
</details>
## 🤗 欢迎反馈问题、提交代码
<br>
如果发现 Bug 请在 Issues 内提出,同时也欢迎任何想法、建议。
## ⚠️ 免责声明
本项目按“原样”提供,不提供任何形式的担保。
所有歌词、字体、图标及其他第三方资源均为其各自版权所有者的财产。
本项目作者不主张对这些资源的所有权。
本项目为非商业用途,不得用于侵犯任何权利。
用户有责任确保其使用符合适用的法律和许可协议。
## 💭 社交媒体分享
![BetterLyrics](https://socialify.git.ci/jayfunc/BetterLyrics/image?description=1&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light)
![BetterLyrics](https://opengraph.githubassets.com/<any_hash_number>/jayfunc/BetterLyrics)
<div align="center">
<mark><i>本项目正处于积极开发阶段;可能会出现意外问题。</i></mark><br>
<sub>免责声明:本项目“按原样”提供。所有第三方资源归其各自所有者所有。</sub>
</div>

292
README.md
View File

@@ -1,247 +1,185 @@
![](Promotion/banner.png)
[**中文**](README.CN.md) | [**English**](README.md)
<div align=center>
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="" width="96">
</div>
<div align="center">
<img src="BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Logo.png" alt="Logo" width="120">
<h2 align=center>
BetterLyrics
</h2>
<h1>BetterLyrics</h1>
<h4 align="center">
🤩 An elegant and deeply customizable lyrics & player app, built with WinUI3/Win2D
<h4>
🤩 An elegant and deeply customizable lyrics visualizer & versatile music player <br>
Built with WinUI3 / Win2D
</h4>
<div align="center">
<div>
<img src="https://img.shields.io/badge/Language-C%23-purple" alt="C#">
<img src="https://img.shields.io/badge/Framework-WinUI%203-blue" alt="WinUI 3">
<img src="https://img.shields.io/badge/License-GPL_v3.0-blue" alt="License">
<a href="https://github.com/jayfunc/BetterLyrics/stargazers"><img src="https://img.shields.io/github/stars/jayfunc/BetterLyrics" alt="Stars"></a>
<a href="https://crowdin.com/project/betterlyrics"><img src="https://badges.crowdin.net/betterlyrics/localized.svg" alt="Crowdin"></a>
</div>
[User Guide](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide) | [Privacy Policy](PrivacyPolicy.md) | [Terms of Service](TermsofService.md)
<br>
<img src="Promotion/banner.png" alt="Banner" width="100%" style="border-radius: 10px;">
</div>
<div align=center>
<br>
![Static Badge](https://img.shields.io/badge/Language-C%23-purple)
![Static Badge](https://img.shields.io/badge/License-GPL_v3.0-blue)
![Static Badge](https://img.shields.io/badge/IDE-Visual%20Studio-purple)
![Static Badge](https://img.shields.io/badge/Framework-WinUI%203-blue)
</div>
<div align=center>
[![GitHub Repo stars](https://img.shields.io/github/stars/jayfunc/BetterLyrics)](https://github.com/jayfunc/BetterLyrics/stargazers)
[![Crowdin](https://badges.crowdin.net/betterlyrics/localized.svg)](https://crowdin.com/project/betterlyrics)
</div>
<div align=center>
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics)
</div>
## 🔥 Featured & Community
<div align="center">
<mark>**_💞 BetterLyrics is made possible by all its contributors, bug reporters and users._**</mark>
| Featured by HelloGitHub | Featured by SSPAI | 🤖 Ask AI |
| :---: | :---: | :---: |
| <a href="https://hellogithub.com/repository/jayfunc/BetterLyrics" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=d2af74f0aea146ad8e4b2086982f5777&claim_uid=SgtQs9c54C8wjnv" alt="HelloGitHub" height="40"></a> | [**Read the Review Article**](https://sspai.com/post/101028) | [![DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/jayfunc/BetterLyrics) <br> [![Zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTk5QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/jayfunc/BetterLyrics) |
**Chat Groups:** [QQ Group (1054700388)](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) | [Discord](https://discord.gg/5yAQPnyCKv) | [Telegram](https://t.me/+svhSLZ7awPsxNGY1)
</div>
## 🧪 Download & Install
<div align="center">
**_[中文版 README 请点按此处](https://github.com/jayfunc/BetterLyrics/blob/dev/README.CN.md)_**
| Microsoft Store (Recommended) | Manual Install |
| :---: | :---: |
| <a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/en-us%20dark.svg" width="160"/></a><br>Unlimited free trial (Same as paid) | [**📦 Latest Release (.zip)**](https://github.com/jayfunc/BetterLyrics/releases/latest)<br>See [Installation Guide](https://jayfunc.blog/blog/how-to-install-zip) |
<mark>**_This project is under active development; unexpected issues may occur._**</mark>
[📖 User Guide](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide) | [🔒 Privacy Policy](PrivacyPolicy.md) | [⚖️ Terms of Service](TermsofService.md)
</div>
## ✍️ Help us translate into your language
## 🌟 Highlighted Features
Cannot find your language? Or have better translations? Don't worry! Start translating and becoming one of the contributors! 😆 See [here](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file) for more info on how to contribute.
- 🎨 **Stunning Visuals & UI**
- **Elegant Design:** Smooth, highly personalized style powered by WinUI3 & Win2D.
- **Immersive Effects:** Fluid backgrounds, 3D/Fan-shaped lyrics, snowflake particles, and more.
- **Deep Customization:** Configure animations, fonts, and behaviors to your taste.
## 🎉 This project was recommended by HelloGithub!
- 🎧 **Versatile Playback & Connectivity**
- **Built-in Player:** Play from **Local Drives** or stream via **Network Protocols** (SMB, WebDAV).
- **External Integration:** Visualizes music from Spotify, Apple Music, NetEase, and [many others](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide#known-supported-music-players-configuration-guide).
<a href="https://hellogithub.com/repository/jayfunc/BetterLyrics" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=d2af74f0aea146ad8e4b2086982f5777&claim_uid=SgtQs9c54C8wjnv" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
- 🌐 **Advanced Lyrics System**
- **Offline Translation:** Privacy-focused local machine translation (30+ languages).
- **Comprehensive Sources:** .lrc (Standard/Enhanced), .eslrc, .ttml, embedded tags, and online sources (QQ Music, NetEase, LRCLIB).
- **Apple Music:** Supports lyrics fetching (Requires token configuration).
## 🎉 This project was featured by SSPAI!
- 🪟 **Display Modes for Every Scenario**
- **Standard:** Full immersive experience.
- **Docked:** A sleek bar attached to your screen edge.
- **Desktop Overlay:** Lyrics floating above all apps.
Check out the article: [BetterLyrics An immersive and smooth lyrics display tool designed for Windows](https://sspai.com/post/101028).
## 🔈 Feedback and chat group
[QQ 群](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388) | [Discord Server](https://discord.gg/5yAQPnyCKv) | [Telegram Group](https://t.me/+svhSLZ7awPsxNGY1)
## 🌟 Highlighted features
- 🌠 **Pleasing User Interface**
- Smooth and highly personalized style, animations and effects
- Immersive fluid background
- Perspective/fan-shaped lyrics
- Snowflake effect
- Multiple lyrics scrolling functions
- ... (and more)
- ↔️ **Strong Lyrics Translation**
- Offline machine translation (supporting 30+ languages)
- Auto-reading local lyrics files for embedded translation
- 🧩 **Various Lyrics Source**
- 💾 Local storage
- Music files (with embedded lyrics)
- [.lrc](<https://en.wikipedia.org/wiki/LRC_(file_format)>) files (with both core format and enhanced format)
- [.eslrc](https://github.com/ESLyric/release) files
- [.ttml](https://en.wikipedia.org/wiki/Timed_Text_Markup_Language) files
- ☁️ Online lyrics providers
- QQ 音乐
- 网易云音乐
- 酷狗音乐
- [amll-ttml-db](https://github.com/Steve-xmh/amll-ttml-db)
- [LRCLIB](https://lrclib.net/)
- <details><summary>⚠️ Apple Music (additional config needed)</summary>
- Open the Apple Music web app and the Developer Tools window. Refresh the page. Return to the Developer Tools window, select Fetch/XHR, select a request, find the Media-User-Token header in the request header, and copy its value.
- Open BetterLyrics and go to the Playback Source settings. Enter the copied value in the Media-User-Token (for Apple Music) setting and click the accept icon on the right-hand side.
- 🎶 **Multiple Music Players Supported**
- Check it out [here](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide#known-supported-music-players-configuration-guide) for detailed info
- 🪟 **Multiple Display Modes**
- **Standard Mode**
- Enjoy an immersive listening journey with rich lyrics, animations and beautifully dynamic backgrounds
- **Docked Mode**
- A smart animated lyrics bar docked to your screen edge
- **Desktop Mode**
- Enjoy immersive lyrics floating above your apps
- **And More...**
- Waiting for you to discover...
- 🧠 **Smart Behaviors**
- Auto hide when music paused
- Auto-hides when music pauses.
## 🖼️ Screenshots
![](Screenshots/fs2.png)
![](Screenshots/std.png)
![](Screenshots/narrow.png)
![](Screenshots/Snipaste_2025-10-31_19-23-17.png)
![](Screenshots/Snipaste_2025-10-31_19-27-34.png)
![](Screenshots/dock.png)
![](Screenshots/desktop.png)
<div align="center">
> ⚠️ Due to GIF format and frame rate limitations, the displayed effect is for preview only. Please refer to the actual device for the actual effect.
| Standard View | Narrow Mode |
| :---: | :---: |
| <img src="Screenshots/std.png" width="100%"> | <img src="Screenshots/narrow.png" width="100%"> |
![](Screenshots/PixPin_2025-10-24_18-13-44.gif)
![](Screenshots/PixPin_2025-10-24_18-17-17.gif)
| Lyrics Visual Effects | Coexisting Modes |
| :---: | :---: |
| <img src="Screenshots/effect.png" width="100%"> | <img src="Screenshots/all-in-one.png" width="100%"> |
| Fullscreen Mode | Fullscreen Mode |
| :---: | :---: |
| <img src="Screenshots/fs3.png" width="100%"> | <img src="Screenshots/fs2.png" width="100%"> |
| Music Gallery | Playback Statistics |
| :---: | :---: |
| <img src="Screenshots/music-gallery.png" width="100%"> | <img src="Screenshots/stats.png" width="100%"> |
</div>
## 📹 Demonstration
Watch our demo video (uploaded on 21 Oct 2025) on Bilibili [here](https://www.bilibili.com/video/BV1QRstz1EGt/).
> Watch our demo video (uploaded on 21 Oct 2025) on Bilibili [here](https://www.bilibili.com/video/BV1QRstz1EGt/).
## 🧪 Try it now
## ✍️ Contribute & Build
<a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
</a>
**Help us translate:** Cannot find your language? [Start translating here](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file).
**Unlimited** free trail or purchase (there is **no difference** between free and paid version).
If you find it useful, please consider [donating](#-donations) or purchasing it in **Microsoft Store**, I'll appreciate it! 🥰
Having trouble downloading and installing from the MS Store? Try the following options:
- [Download from outside Microsoft Store](https://jayfunc.blog/blog/download-from-outside-ms-store)
- Go to [latest release](https://github.com/jayfunc/BetterLyrics/releases/latest) and download `.zip` file from `Assets`. (See [this doc](https://jayfunc.blog/blog/how-to-install-zip) for how to install it.)
## 🏗️ Build
Before you build, make sure that you have already:
- Replaced `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\DiscordTemplate` with `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\DiscordTemplate.cs`.
- Replaced `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\LastFMTemplate` with `BetterLyrics\BetterLyrics.WinUI3\BetterLyrics.WinUI3\Constants\LastFM.cs`.
**Build from source:**
> Before building, ensure you have replaced `DiscordTemplate.cs` and `LastFM.cs` in the `Constants` folder.
## 🤑 Donations
If you like this project, please consider supporting it by donating. Your support will help keep the project alive and encourage further development.
If you like BetterLyrics, please consider supporting it. Your support helps keep the project alive!
You can donate via:
- [PayPal](https://paypal.me/zhefangpay)
- [Buy Me a Coffee](https://buymeacoffee.com/founchoo)
- [爱发电](https://afdian.com/a/jayfunc)
- <details><summary>支付宝</summary>
<div align="center">
![](Donate/Alipay.jpg)
| Web Platforms | Alipay (QR) | WeChat (QR) |
| :---: | :---: | :---: |
| [PayPal](https://paypal.me/zhefangpay)<br><br>[Buy Me a Coffee](https://buymeacoffee.com/founchoo)<br><br>[爱发电 (Afdian)](https://afdian.com/a/jayfunc) | <img src="Donate/Alipay.jpg" width="150"> | <img src="Donate/WeChatReward.png" width="150"> |
</detais>
**[View the full Hall of Fame (Sponsors)](SPONSORS.md)**
- <details><summary>微信</summary>
</div>
![](Donate/WeChatReward.png)
## ⭐ Star History
</details>
<div align="center">
<img src="https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date" width="100%">
</div>
This project is made possible by the generous support of our users. **[View the full Hall of Fame](SPONSORS.md)**
## 📄 License & Credits
## 📄 License
This project is licensed under the **GNU General Public License v3.0**.
This project is licensed under the GNU General Public License v3.0. See the [LICENSE](https://github.com/jayfunc/BetterLyrics/blob/dev/LICENSE) file for details.
<details>
<summary><b>💖 Special Thanks, Credits & Inspiration</b></summary>
<br>
## 💖 Many thanks to
Some functions and code are referenced or modified from public repositories, including but not limited to the following open source projects/packages, tutorials, etc., and we would like to express our gratitude to them here.
**Dependencies & References:**
| Projects/Packages | Description |
| :--- | :--- |
| [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) | Provide lyrics fetch, decryption, and parsing for QQ, Netease, and Kugou sources |
| [lrclib](https://github.com/tranxuanthang/lrclib) | LRCLIB lyrics API provider |
| [Manzana-Apple-Music-Lyrics](https://github.com/dropcreations/Manzana-Apple-Music-Lyrics) | Apple Music lyrics fetch using Python |
| [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet) | Used for extracting pictures from music files |
| [WinUIEx](https://github.com/dotMorten/WinUIEx) | Provide easy ways to access the Win32 API regarding windowing |
| [cutlet](https://github.com/polm/cutlet) | Provide the ability for converting Japanese to romaji |
| [DevWinUI](https://github.com/ghost1372/DevWinUI) | Provide many out-of-the-box features for building WinUI 3 applications |
| [Isolation](https://github.com/Storyteller-Studios/Isolation) | Dynamic fluid background implementation |
| [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) | Provide the ability for offline lyrics translation |
| [lrclib](https://github.com/tranxuanthang/lrclib) | LRCLIB lyrics API provider |
| [Lyricify-Lyrics-Helper](https://github.com/WXRIW/Lyricify-Lyrics-Helper) | Provide lyrics fetch, decryption, and parsing for QQ, Netease, and Kugou sources |
| [Manzana-Apple-Music-Lyrics](https://github.com/dropcreations/Manzana-Apple-Music-Lyrics) | Apple Music lyrics fetch using Python |
| [SpectrumVisualization](https://github.com/Johnwikix/SpectrumVisualization) | Audio visualization reference |
| [TagLib#](https://github.com/mono/taglib-sharp) | Used for reading the original lyrics content |
| [Vanara](https://github.com/dahall/Vanara) | Win32 API wrapper |
| [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) | Provide the ability for offline lyrics translation |
| [Isolation](https://github.com/Storyteller-Studios/Isolation) | Dynamic fluid background implementation |
| [SpectrumVisualization](https://github.com/Johnwikix/SpectrumVisualization) | Audio visualization reference |
| [DevWinUI](https://github.com/ghost1372/DevWinUI) | Provide many out-of-the-box features for building WinUI 3 applications |
| ... | ... |
| [WinUIEx](https://github.com/dotMorten/WinUIEx) | Provide easy ways to access the Win32 API regarding windowing |
See all the dependencies [here](https://github.com/jayfunc/BetterLyrics/network/dependencies).
See [dependencies](https://github.com/jayfunc/BetterLyrics/network/dependencies) for full list.
### Tutorials/Blogs/etc.
<br>
- [Stackoverflow - How to animate Margin property in WPF](https://stackoverflow.com/a/21542882/11048731)
- [Bilibili -【WinUI3】SystemBackdropController定义云母、亚克力效果](https://www.bilibili.com/video/BV1PY4FevEkS)
- [cnblogs - .NET App 与 Windows 系统媒体控制(SMTC)交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D 中的游戏循环CanvasAnimatedControl](https://www.cnblogs.com/walterlv/p/10236395.html)
- [r2d2rigo/Win2D-Samples](https://github.com/r2d2rigo/Win2D-Samples/blob/master/IrisBlurWin2D/IrisBlurWin2D/MainPage.xaml.cs)
- [CommunityToolkit - 从入门到精通](https://mvvm.coldwind.top/)
## 💡 Inspired by
Some design ideas are referenced from the following plugins/software (excluding code that is indirectly or directly referenced or modified, and is only used as a guide for design ideas).
**💡 Inspired by:**
Some design ideas are referenced from the following projects (design inspiration only):
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
- [Lyricify-App](https://github.com/WXRIW/Lyricify-App)
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
- [Salt Player](https://moriafly.com/program/salt-player)
- [MyToolBar](https://github.com/TwilightLemon/MyToolBar)
## ⭐ Star history
</details>
<div style="display: flex; justify-content: space-around; align-items: flex-start;">
<img src="https://api.star-history.com/svg?repos=jayfunc/BetterLyrics&type=Date)](https://www.star-history.com/#jayfunc/BetterLyrics&Date" width="100%" >
## 💭 Share on Social Media
<details>
<summary><b>Click to expand</b></summary>
<br>
<div align="center">
<img src="https://socialify.git.ci/jayfunc/BetterLyrics/image?description=1&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light" width="48%">
<img src="https://opengraph.githubassets.com/<any_hash_number>/jayfunc/BetterLyrics" width="48%">
</div>
</details>
## 🤗 Any issues and PRs are welcome
<br>
If you find a bug, please file it in issues, or if you have any ideas, feel free to share them here.
## ⚠️ Disclaimer
This project is provided "as is" without warranty of any kind.
All lyrics, fonts, icons, and other third-party resources are the property of their respective copyright holders.
The author of this project does not claim ownership of such resources.
This project is non-commercial and should not be used to infringe any rights.
Users are responsible for ensuring their own use complies with applicable laws and licenses.
## 💭 Share it on social media
![BetterLyrics](https://socialify.git.ci/jayfunc/BetterLyrics/image?description=1&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light)
![BetterLyrics](https://opengraph.githubassets.com/<any_hash_number>/jayfunc/BetterLyrics)
<div align="center">
<mark><i>This project is under active development; unexpected issues may occur.</i></mark><br>
<sub>Disclaimer: This project is provided "as is". All third-party resources belong to their respective owners.</sub>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 KiB

BIN
Screenshots/all-in-one.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 KiB

BIN
Screenshots/effect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
Screenshots/fs3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

BIN
Screenshots/stats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB