Compare commits

...

31 Commits

Author SHA1 Message Date
Zhe Fang
3947050d6f fix: lyrics cache dir was not created when first launch 2026-01-07 07:12:50 -05:00
Zhe Fang
707d85bc75 fix: database create issue 2026-01-07 06:58:02 -05:00
Zhe Fang
78bafb8508 fix: sponsor date 2026-01-06 22:45:09 -05:00
Zhe Fang
b4d24c5570 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-06 22:43:07 -05:00
Zhe Fang
83c9f9806d chores: update sponsors 2026-01-06 22:43:06 -05:00
Zhe Fang
adde74afb0 Update SPONSORS.md 2026-01-06 22:39:10 -05:00
Zhe Fang
67b4d4e409 chores: delete unused pre-defined path 2026-01-06 21:43:30 -05:00
Zhe Fang
8d7fbe63c5 fix: amll-ttml-db lyrics searching issue (due to metadata matching calculator error) 2026-01-06 16:43:16 -05:00
Zhe Fang
5037b92913 chores: change lyrics cache system from .json based to database based 2026-01-06 11:17:06 -05:00
Zhe Fang
c1ee7a6779 feat: add bg support for lrc (extended) 2026-01-05 19:43:41 -05:00
Zhe Fang
7ddfd1118b feat: support bg lyrics 2026-01-05 19:32:30 -05:00
Zhe Fang
97f20decf2 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-05 09:54:59 -05:00
Zhe Fang
81eb4e1c96 chores: update patron name 2026-01-05 09:54:57 -05:00
Zhe Fang
00ee4a051c Update SPONSORS.md 2026-01-05 09:17:36 -05:00
Zhe Fang
a13bb6e8e4 chores: bump to 1.2.243.0 2026-01-04 16:43:02 -05:00
Zhe Fang
0b436c1ea9 chores: rollback UpdateLyrics 2026-01-04 16:21:33 -05:00
Zhe Fang
5d332fdfc6 fix: media sessions record issue 2026-01-04 16:02:11 -05:00
Zhe Fang
572d2cd8ba fix 2026-01-04 14:54:15 -05:00
Zhe Fang
1e5a95c55e chores: bump to v1.2.240.0 2026-01-04 12:05:53 -05:00
Zhe Fang
18ce6d3a57 chores: improve thanks list 2026-01-04 11:39:47 -05:00
Zhe Fang
427aed6857 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-04 09:51:53 -05:00
Zhe Fang
ebfa484a2e fix: GSMTC 2026-01-04 09:51:51 -05:00
Zhe Fang
3ef9d81bea Update README.CN.md 2026-01-04 06:45:25 -05:00
Zhe Fang
e999d07834 Update README.md 2026-01-04 06:43:52 -05:00
Zhe Fang
838b8de94f Update README.md 2026-01-04 06:43:20 -05:00
Zhe Fang
b3059dbeb1 chores: improve GSMTC service 2026-01-03 22:02:08 -05:00
Zhe Fang
6fea88a6a1 fix: stats dashboard ui 2026-01-03 17:27:47 -05:00
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
101 changed files with 2911 additions and 2337 deletions

View File

@@ -12,7 +12,7 @@
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.2.236.0" />
Version="1.2.245.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,47 +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" />
<converter:DoubleToDecimalConverter x:Key="DoubleToDecimalConverter" />
<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 -->
@@ -110,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" />
@@ -118,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" />
@@ -132,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}"
@@ -359,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,15 @@ 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.LyricsCacheService;
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.SongSearchMapService;
using BetterLyrics.WinUI3.Services.TranslationService;
using BetterLyrics.WinUI3.Services.TransliterationService;
using BetterLyrics.WinUI3.ViewModels;
@@ -19,7 +22,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;
@@ -126,13 +128,25 @@ namespace BetterLyrics.WinUI3
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
// 初始化数据库
await EnsureDatabasesAsync();
await InitDatabasesAsync();
var settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
// Migrate MappedSongSearchQueries
var songSearchMapService = Ioc.Default.GetRequiredService<ISongSearchMapService>();
var obsoleteSongSearchMap = settingsService.AppSettings.MappedSongSearchQueries;
if (obsoleteSongSearchMap.Count > 0)
{
foreach (var item in obsoleteSongSearchMap)
{
await songSearchMapService.SaveMappingAsync(item);
}
obsoleteSongSearchMap.Clear();
}
// Start scan tasks in background
var fileSystemService = Ioc.Default.GetRequiredService<IFileSystemService>();
// 开始后台扫描任务
foreach (var item in settingsService.AppSettings.LocalMediaFolders)
{
if (item.LastSyncTime == null)
@@ -142,10 +156,10 @@ namespace BetterLyrics.WinUI3
}
fileSystemService.StartAllFolderTimers();
// 初始化托盘
// Init system tray
m_window = WindowHook.OpenOrShowWindow<SystemTrayWindow>();
// 根据设置打开歌词窗口
// Open lyrics window if set
if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow)
{
var defaultStatus = settingsService.AppSettings.WindowBoundsRecords.Where(x => x.IsDefault);
@@ -162,97 +176,39 @@ namespace BetterLyrics.WinUI3
}
}
// 根据设置自动打开主界面
// Open music gallery if set
if (settingsService.AppSettings.MusicGallerySettings.AutoOpen)
{
WindowHook.OpenOrShowWindow<MusicGalleryWindow>();
}
}
private async Task EnsureDatabasesAsync()
private async Task InitDatabasesAsync()
{
// Init databases
var playHistoryFactory = Ioc.Default.GetRequiredService<IDbContextFactory<PlayHistoryDbContext>>();
var fileCacheFactory = Ioc.Default.GetRequiredService<IDbContextFactory<FilesIndexDbContext>>();
var songSearchMapFactory = Ioc.Default.GetRequiredService<IDbContextFactory<SongSearchMapDbContext>>();
var filesIndexFactory = Ioc.Default.GetRequiredService<IDbContextFactory<FilesIndexDbContext>>();
var lyricsCacheFactory = Ioc.Default.GetRequiredService<IDbContextFactory<LyricsCacheDbContext>>();
await SafeInitDatabaseAsync(
"PlayHistory",
PathHelper.PlayHistoryPath,
async () =>
{
using var db = await playHistoryFactory.CreateDbContextAsync();
await db.Database.EnsureCreatedAsync();
},
isCritical: true
);
await SafeInitDatabaseAsync(
"FileCache",
PathHelper.FilesIndexPath,
async () =>
{
using var db = await fileCacheFactory.CreateDbContextAsync();
await db.Database.EnsureCreatedAsync();
},
isCritical: false
);
}
private async Task SafeInitDatabaseAsync(string dbName, string dbPath, Func<Task> initAction, bool isCritical)
{
try
using (var playHistoryDb = await playHistoryFactory.CreateDbContextAsync())
{
await initAction();
await playHistoryDb.Database.EnsureCreatedAsync();
}
catch (Exception ex)
using (var songSearchMapDb = await songSearchMapFactory.CreateDbContextAsync())
{
System.Diagnostics.Debug.WriteLine($"[DB Error] {dbName} init failed: {ex.Message}");
try
{
if (File.Exists(dbPath))
{
// 尝试清理连接池
SqliteConnection.ClearAllPools();
if (isCritical)
{
var backupPath = dbPath + ".bak_" + DateTime.Now.ToString("yyyyMMddHHmmss");
File.Move(dbPath, backupPath, true);
await ShowErrorDialogAsync("Database Recovery", $"Database {dbName} is damaged, the old database has been backed up to {backupPath}, and the program will create a new database.");
}
else
{
File.Delete(dbPath);
}
}
await initAction();
System.Diagnostics.Debug.WriteLine($"[DB Info] {dbName} recovered successfully.");
}
catch (Exception fatalEx)
{
System.Diagnostics.Debug.WriteLine($"[] : {fatalEx.Message}");
await ShowErrorDialogAsync("Fatal Error", $"{dbName} recovery failed, please delete the file at {dbPath} and try again by restarting the program. ({fatalEx.Message})");
}
await songSearchMapDb.Database.EnsureCreatedAsync();
}
}
private async Task ShowErrorDialogAsync(string title, string content)
{
// 这里假设 m_window 已经存在。如果没有显示主窗口,这个弹窗可能无法显示。
// 在 App 启动极早期的错误,可能需要退化为 Log 或者 System.Diagnostics.Process.Start 打开记事本报错
if (m_window != null)
using (var filesIndexDb = await filesIndexFactory.CreateDbContextAsync())
{
m_window.DispatcherQueue.TryEnqueue(async () =>
{
var dialog = new Microsoft.UI.Xaml.Controls.ContentDialog
{
Title = title,
Content = content,
CloseButtonText = "OK",
XamlRoot = m_window.Content?.XamlRoot // 确保 Content 不为空
};
if (dialog.XamlRoot != null) await dialog.ShowAsync();
});
await filesIndexDb.Database.EnsureCreatedAsync();
}
using (var lyricsCacheDb = await lyricsCacheFactory.CreateDbContextAsync())
{
await lyricsCacheDb.Database.EnsureCreatedAsync();
}
}
@@ -260,6 +216,7 @@ namespace BetterLyrics.WinUI3
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Is(Serilog.Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", Serilog.Events.LogEventLevel.Error)
.WriteTo.File(PathHelper.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger();
@@ -268,6 +225,8 @@ namespace BetterLyrics.WinUI3
// 数据库工厂
.AddDbContextFactory<PlayHistoryDbContext>(options => options.UseSqlite($"Data Source={PathHelper.PlayHistoryPath}"))
.AddDbContextFactory<FilesIndexDbContext>(options => options.UseSqlite($"Data Source={PathHelper.FilesIndexPath}"))
.AddDbContextFactory<LyricsCacheDbContext>(options => options.UseSqlite($"Data Source={PathHelper.LyricsCachePath}"))
.AddDbContextFactory<SongSearchMapDbContext>(options => options.UseSqlite($"Data Source={PathHelper.SongSearchMapPath}"))
// 日志
.AddLogging(loggingBuilder =>
@@ -278,7 +237,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>()
@@ -288,6 +248,8 @@ namespace BetterLyrics.WinUI3
.AddSingleton<ILocalizationService, LocalizationService>()
.AddSingleton<IFileSystemService, FileSystemService>()
.AddSingleton<IPlayHistoryService, PlayHistoryService>()
.AddSingleton<ILyricsCacheService, LyricsCacheService>()
.AddSingleton<ISongSearchMapService, SongSearchMapService>()
// ViewModels
.AddSingleton<AppSettingsControlViewModel>()
@@ -304,6 +266,7 @@ namespace BetterLyrics.WinUI3
.AddSingleton<AboutControlViewModel>()
.AddSingleton<MusicGalleryWindowViewModel>()
.AddSingleton<StatsDashboardControlViewModel>()
.AddSingleton<PlayQueueViewModel>()
.AddTransient<NowPlayingWindowViewModel>()
.AddTransient<NowPlayingPageViewModel>()

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

View File

@@ -42,13 +42,17 @@
<None Remove="Controls\LyricsWindowSwitchControl.xaml" />
<None Remove="Controls\MediaSettingsControl.xaml" />
<None Remove="Controls\NowPlayingBar.xaml" />
<None Remove="Controls\PatronControl.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" />
@@ -243,6 +247,9 @@
<Content Update="Assets\Question.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\RevolvingHearts.gif">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\SaltPlayerForWindows.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@@ -259,6 +266,26 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PatronControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</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 +436,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

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

View File

@@ -57,12 +57,12 @@
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal">
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="GitHub" NavigateUri="{x:Bind const:Link.BetterLyricsGitHub}" />
<HyperlinkButton x:Uid="UserGuide" NavigateUri="{x:Bind const:Link.UserGuide}" />
<HyperlinkButton x:Uid="PrivacyPolicy" NavigateUri="{x:Bind const:Link.PrivacyPolicy}" />
<HyperlinkButton x:Uid="TermsOfService" NavigateUri="{x:Bind const:Link.TermsOfService}" />
</StackPanel>
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
@@ -70,18 +70,18 @@
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageFeedback" />
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal">
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="QQ 反馈交流群" NavigateUri="{x:Bind const:Link.QQGroup}" />
<HyperlinkButton Content="Discord" NavigateUri="{x:Bind const:Link.Discord}" />
<HyperlinkButton Content="Telegram" NavigateUri="{x:Bind const:Link.Telegram}" />
</StackPanel>
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageDonation" />
<StackPanel Margin="-12,0,0,0" Orientation="Horizontal">
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="Buy Me a Coffee" NavigateUri="{x:Bind const:Link.BuyMeACoffee}" />
<HyperlinkButton Content="PayPal" NavigateUri="{x:Bind const:Link.PayPal}" />
<HyperlinkButton
@@ -117,7 +117,7 @@
</HyperlinkButton.ContextFlyout>
</HyperlinkButton>
<HyperlinkButton Content="爱发电" NavigateUri="{x:Bind const:Link.Afdian}" />
</StackPanel>
</dev:WrapPanel>
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@@ -136,15 +136,6 @@
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageThanksList">
<Button
Click="Patron_Click"
Content="{ui:FontIcon FontSize=16,
FontFamily={StaticResource IconFontFamily},
Glyph=&#xE7FD;}"
Style="{StaticResource AccentButtonStyle}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
<dev:SettingsExpander.ItemsFooter>
<InfoBar
@@ -157,6 +148,107 @@
</dev:SettingsExpander.ItemsFooter>
</dev:SettingsExpander>
<dev:SettingsExpander x:Uid="SettingsPageThanksList">
<dev:SettingsExpander.HeaderIcon>
<ImageIcon Source="ms-appx:///Assets/RevolvingHearts.gif" />
</dev:SettingsExpander.HeaderIcon>
<dev:SettingsExpander.Items>
<!-- 贡献者 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<RichTextBlock>
<Paragraph>
<Run x:Uid="SetingsPageContributors" />
<Run Text="-" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Code" />
</Paragraph>
</RichTextBlock>
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="jayfunc" NavigateUri="https://github.com/jayfunc" />
<HyperlinkButton Content="Raspberry-Monster" NavigateUri="https://github.com/Raspberry-Monster" />
<HyperlinkButton Content="ZHider" NavigateUri="https://github.com/ZHider" />
<HyperlinkButton Content="kusutori" NavigateUri="https://github.com/kusutori" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<!-- 贡献者 (Translator) -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<RichTextBlock>
<Paragraph>
<Run x:Uid="SetingsPageContributors" />
<Run Text="-" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Translator" />
</Paragraph>
</RichTextBlock>
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="borcolasky" NavigateUri="https://crowdin.com/profile/borcolasky" />
<HyperlinkButton Content="SuHeAndZl" NavigateUri="https://crowdin.com/profile/SuHeAndZl" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<!-- 赞助 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SettingsPagePatrons" />
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<uc:PatronControl Date="Jan 3, 2026" PatronName="**轩" />
<uc:PatronControl Date="Dec 13, 2025" PatronName="&lt;Anonymous&gt;" />
<uc:PatronControl Date="Dec 3, 2025" PatronName="YE" />
<uc:PatronControl Date="Dec 2, 2025" PatronName="&lt;Anonymous&gt;" />
<uc:PatronControl Date="Nov 23, 2025" PatronName="**玄" />
<uc:PatronControl Date="Nov 21, 2025" PatronName="**智" />
<uc:PatronControl Date="Nov 17, 2025" PatronName="SuHeAndZl" />
<uc:PatronControl Date="Nov 2, 2025" PatronName="借过" />
<uc:PatronControl Date="Aug 28, 2025" PatronName="**华" />
<TextBlock
x:Uid="SettingsPageUserWhoPurchased"
Margin="12,8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
<!-- 特别鸣谢 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageSpecialThanks" />
<TextBlock
x:Uid="SettingsPageYouNowUsing"
Margin="0,8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel>
</dev:SettingsCard>
<!-- 代码参考 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageDeps" />
<HyperlinkButton Margin="-12,0,0,0" NavigateUri="https://github.com/jayfunc/BetterLyrics/network/dependencies">
<TextBlock x:Uid="SetingsPageDeps" />
</HyperlinkButton>
</StackPanel>
</dev:SettingsCard>
<!-- UI/UX 参考 -->
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageUIUXRef" />
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<HyperlinkButton Content="refined-now-playing-netease" NavigateUri="https://github.com/solstice23/refined-now-playing-netease" />
<HyperlinkButton Content="Lyricify" NavigateUri="https://github.com/WXRIW/Lyricify-App" />
<HyperlinkButton Content="椒盐音乐 Salt Player" NavigateUri="https://moriafly.com/program/salt-player" />
<HyperlinkButton Content="MyToolBar" NavigateUri="https://github.com/TwilightLemon/MyToolBar" />
</dev:WrapPanel>
</StackPanel>
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageMockMusicPlaying">
<HyperlinkButton x:Uid="SettingsPagePlayingMockMusicButton" NavigateUri="https://soundcloud.com/carlyraejepsen/cut-to-the-feeling" />
</dev:SettingsCard>
@@ -209,194 +301,28 @@
Value="{x:Bind ViewModel.AppSettings.AdvancedSettings.FPS, Mode=TwoWay}" />
</dev:SettingsCard>
<RichTextBlock
Margin="0,16,0,0"
HorizontalAlignment="Center"
HorizontalTextAlignment="Center"
LineHeight="28">
<Paragraph FontWeight="Bold">
<Run Text="{x:Bind const:App.AppName}" />
</Paragraph>
<Paragraph>
<Run Text="An elegant and deeply customizable lyrics visualizer &amp; versatile music player" />
</Paragraph>
<Paragraph>
<Run Text="Proudly built by" />
<Hyperlink NavigateUri="{x:Bind const:Link.AuthorGitHub}">
<Run Text="{x:Bind const:App.AppAuthor}" />
</Hyperlink>
</Paragraph>
</RichTextBlock>
</StackPanel>
</Grid>
</ScrollViewer>
<Grid
x:Name="CreditsReel"
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}"
Opacity="0"
SizeChanged="CreditsReel_SizeChanged"
Tapped="CreditsReel_Tapped"
Visibility="Collapsed">
<Grid.OpacityTransition>
<ScalarTransition />
</Grid.OpacityTransition>
<ScrollViewer
x:Name="CreditsReelScrollViewer"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollMode="Disabled">
<RichTextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
HorizontalTextAlignment="Center"
LineHeight="28"
PointerEntered="RichTextBlock_PointerEntered"
PointerExited="RichTextBlock_PointerExited">
<Paragraph x:Name="CreditsReelHeader" />
<!-- 贡献者 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageContributors" />
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/jayfunc">
<Run Text="jayfunc" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Raspberry-Monster">
<Run Text="Raspberry-Monster" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/ZHider">
<Run Text="ZHider" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/kusutori">
<Run Text="kusutori" />
</Hyperlink>
</Paragraph>
<!-- 赞助 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SettingsPagePatrons" />
</Paragraph>
<Paragraph>
<Run Text="YE" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Dec 3, 2025" />
</Paragraph>
<Paragraph>
<Run Text="**玄" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 23, 2025" />
</Paragraph>
<Paragraph>
<Run Text="**智" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 21, 2025" />
</Paragraph>
<Paragraph>
<Run Text="*鹤" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 17, 2025" />
</Paragraph>
<Paragraph>
<Run Text="借过" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Nov 2, 2025" />
</Paragraph>
<Paragraph>
<Run Text="**华" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Aug 28, 2025" />
</Paragraph>
<Paragraph>
<Run x:Uid="SettingsPageUserWhoPurchased" />
</Paragraph>
<!-- 特别鸣谢 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageSpecialThanks" />
</Paragraph>
<Paragraph>
<Run x:Uid="SettingsPageYouNowUsing" />
</Paragraph>
<!-- 代码参考 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageDeps" />
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://gist.github.com/mcworkaholic/82fbf203e3f1043bbe534b5b2974c0ce">
<Run Text="Get album artwork from ITunes (with Python3 or C#)" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://stackoverflow.com/a/32013610/11048731">
<Run Text="FullyObservableCollection" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Storyteller-Studios/Impressionist">
<Run Text="Impressionist" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Storyteller-Studios/ColorThief.WinUI3">
<Run Text="ColorThief.WinUI3" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/Johnwikix/SpectrumVisualization">
<Run Text="SpectrumVisualization" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.shadertoy.com/view/Mdt3Df">
<Run Text="Snow (as shown in sweden)" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://www.shadertoy.com/view/lllSR2">
<Run Text="w10" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/mo-jinran/Taskbar-Lyrics">
<Run Text="Taskbar-Lyrics" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/jayfunc/BetterLyrics/network/dependencies">
<Run Text="..." />
</Hyperlink>
</Paragraph>
<!-- UI/UX 设计参考 -->
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run x:Uid="SetingsPageUIUXRef" />
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/solstice23/refined-now-playing-netease">
<Run Text="refined-now-playing-netease" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/WXRIW/Lyricify-App">
<Run Text="Lyricify" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://moriafly.com/program/salt-player">
<Run Text="椒盐音乐 Salt Player" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="https://github.com/TwilightLemon/MyToolBar">
<Run Text="MyToolBar" />
</Hyperlink>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="">
<Run Text="" />
</Hyperlink>
</Paragraph>
<Paragraph Margin="0,20,0,0" FontWeight="Bold">
<Run Text="{x:Bind const:App.AppName}" />
</Paragraph>
<Paragraph>
<Run Text="Proudly built by" />
<Hyperlink NavigateUri="{x:Bind const:Link.AuthorGitHub}">
<Run Text="{x:Bind const:App.AppAuthor}" />
</Hyperlink>
</Paragraph>
<Paragraph x:Name="CreditsReelFooter" />
</RichTextBlock>
</ScrollViewer>
</Grid>
</Grid>
</UserControl>

View File

@@ -11,7 +11,6 @@ namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class AboutControl : UserControl
{
private bool _isCreditsScrolling = false;
public AboutControlViewModel ViewModel => (AboutControlViewModel)DataContext;
public AboutControl()
@@ -20,47 +19,6 @@ namespace BetterLyrics.WinUI3.Controls
DataContext = Ioc.Default.GetRequiredService<AboutControlViewModel>();
}
private async void Patron_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
CompositionTarget.Rendering += CompositionTarget_Rendering;
CreditsReel.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
CreditsReel.Opacity = 1;
_isCreditsScrolling = true;
}
private void CompositionTarget_Rendering(object? sender, object e)
{
if (_isCreditsScrolling)
{
CreditsReelScrollViewer.ChangeView(null, CreditsReelScrollViewer.VerticalOffset + 0.5, null);
}
}
private async void CreditsReel_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
CreditsReel.Opacity = 0;
await Task.Delay(Constants.Time.AnimationDuration);
CreditsReel.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
CompositionTarget.Rendering -= CompositionTarget_Rendering;
CreditsReelScrollViewer.ChangeView(null, 0, null);
}
private void CreditsReel_SizeChanged(object sender, Microsoft.UI.Xaml.SizeChangedEventArgs e)
{
CreditsReelHeader.LineHeight = e.NewSize.Height;
CreditsReelFooter.LineHeight = e.NewSize.Height / 2;
}
private void RichTextBlock_PointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
_isCreditsScrolling = false;
}
private void RichTextBlock_PointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
_isCreditsScrolling = true;
}
private void WeChat_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
WeChatFlyout.ShowAt(WeChatButton);

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;
@@ -25,13 +25,14 @@ using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Storage.Streams;
using Windows.UI;
using System.Numerics;
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class LyricsCanvas : UserControl,
IRecipient<PropertyChangedMessage<TimeSpan>>,
IRecipient<PropertyChangedMessage<LyricsData?>>,
IRecipient<PropertyChangedMessage<SongInfo?>>,
IRecipient<PropertyChangedMessage<SongInfo>>,
IRecipient<PropertyChangedMessage<int>>,
IRecipient<PropertyChangedMessage<double>>,
IRecipient<PropertyChangedMessage<bool>>,
@@ -41,7 +42,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 +99,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 +344,7 @@ namespace BetterLyrics.WinUI3.Controls
var lyricsStyle = _lyricsWindowStatus.LyricsStyleSettings;
var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings;
double songDuration = _mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0;
double songDuration = _gsmtcService.CurrentSongInfo.DurationMs;
bool isForceWordByWord = _settingsService.AppSettings.GeneralSettings.IsForceWordByWordEffect;
Color overlayColor;
@@ -400,6 +399,7 @@ namespace BetterLyrics.WinUI3.Controls
strokeColor: _albumArtThemeColors.StrokeFontColor,
bgColor: _albumArtThemeColors.BgFontColor,
fgColor: _albumArtThemeColors.FgFontColor,
currentProgressMs: _songPositionWithOffset.TotalMilliseconds,
getPlaybackState: (lineIndex) =>
{
if (_renderLyricsLines == null) return new LinePlaybackState();
@@ -435,19 +435,19 @@ namespace BetterLyrics.WinUI3.Controls
);
}
#if DEBUG
//args.DrawingSession.DrawText(
// $"Lyrics render start pos: ({(int)_renderLyricsStartX}, {(int)_renderLyricsStartY})\n" +
// $"Lyrics render size: [{(int)_renderLyricsWidth} x {(int)_renderLyricsHeight}]\n" +
// $"Lyrics actual height: {LyricsLayoutManager.CalculateActualHeight(_renderLyricsLines)}\n" +
// $"Playing line (idx): {_playingLineIndex}\n" +
// $"Mouse hovering line (idx): {_mouseHoverLineIndex}\n" +
// $"Visible lines range (idx): [{_visibleRange.Start}, {_visibleRange.End}]\n" +
// $"Total line count: {LyricsLayoutManager.CalculateMaxRange(_renderLyricsLines).End + 1}\n" +
// $"Played: {_songPosition} / {TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0)}\n" +
// $"Y offset: {_canvasYScrollTransition.Value}\n" +
// $"User scroll offset: {_mouseYScrollTransition.Value}",
// new Vector2(0, 0), Colors.Red);
#if DEBUG && false
args.DrawingSession.DrawText(
$"Lyrics render start pos: ({(int)_renderLyricsStartX}, {(int)_renderLyricsStartY})\n" +
$"Lyrics render size: [{(int)_renderLyricsWidth} x {(int)_renderLyricsHeight}]\n" +
$"Lyrics actual height: {LyricsLayoutManager.CalculateActualHeight(_renderLyricsLines)}\n" +
$"Playing line (idx): {_playingLineIndex}\n" +
$"Mouse hovering line (idx): {_mouseHoverLineIndex}\n" +
$"Visible lines range (idx): [{_visibleRange.Start}, {_visibleRange.End}]\n" +
$"Total line count: {LyricsLayoutManager.CalculateMaxRange(_renderLyricsLines).End + 1}\n" +
$"Played: {_songPosition} / {TimeSpan.FromMilliseconds(_gsmtcService.CurrentSongInfo.DurationMs)}\n" +
$"Y offset: {_canvasYScrollTransition.Value}\n" +
$"User scroll offset: {_mouseYScrollTransition.Value}",
new Vector2(0, 0), Colors.Red);
#endif
}
@@ -459,7 +459,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;
@@ -477,7 +477,7 @@ namespace BetterLyrics.WinUI3.Controls
#region UpdatePlayingLineIndex
int newPlayingIndex = _synchronizer.GetCurrentLineIndex(_songPositionWithOffset.TotalMilliseconds, lyricsData);
int newPlayingIndex = _synchronizer.GetCurrentLineIndex(_songPositionWithOffset.TotalMilliseconds, _renderLyricsLines);
bool isPlayingLineChanged = newPlayingIndex != _playingLineIndex;
_playingLineIndex = newPlayingIndex;
@@ -538,7 +538,8 @@ namespace BetterLyrics.WinUI3.Controls
_isMouseScrolling,
_isLayoutChanged,
isPlayingLineChanged,
_isMouseScrollingChanged
_isMouseScrollingChanged,
_songPositionWithOffset.TotalMilliseconds
);
_isMouseScrollingChanged = false;
@@ -654,25 +655,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()
var lines = _gsmtcService.CurrentLyricsData?.LyricsLines.Select(x => new RenderLyricsLine()
{
LyricsSyllables = x.LyricsSyllables,
StartMs = x.StartMs,
@@ -681,11 +679,16 @@ namespace BetterLyrics.WinUI3.Controls
OriginalText = x.OriginalText,
TranslatedText = x.TranslatedText
}).ToList();
if (lines != null)
{
LyricsLayoutManager.CalculateLanes(lines);
}
_renderLyricsLines = lines;
}
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 +698,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 +724,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;
@@ -738,11 +734,11 @@ namespace BetterLyrics.WinUI3.Controls
}
}
public void Receive(PropertyChangedMessage<SongInfo?> message)
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,13 +891,14 @@ 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

@@ -97,19 +97,6 @@
Style="{StaticResource GhostButtonStyle}" />
</Grid>
<RichTextBlock
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap">
<Paragraph>
<Run Text="*" />
<Run x:Uid="ArtistsSplitHint" />
</Paragraph>
<Paragraph>
<Run Text="; , / " />
</Paragraph>
</RichTextBlock>
</StackPanel>
</Grid>
@@ -169,7 +156,7 @@
<Grid Grid.Column="1">
<ListView ItemsSource="{x:Bind ViewModel.LyricsSearchResults, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedLyricsSearchResult, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsSearchResult">
<DataTemplate x:DataType="models:LyricsCacheItem">
<ListViewItem IsEnabled="{x:Bind IsFound}">
<StackPanel Padding="0,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}">
<local:PropertyRow
@@ -180,17 +167,13 @@
<!-- Lyrics search result -->
<StackPanel Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind Artist, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageAlbum" Value="{x:Bind Album, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsPageMatchPercentage"
Unit="%"
Value="{x:Bind MatchPercentage, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsPageCachePath"
Link="{x:Bind SelfPath, TargetNullValue=N/A, Mode=OneWay}"
ToolTipService.ToolTip="{x:Bind SelfPath, TargetNullValue=N/A, Mode=OneWay}" />
</StackPanel>
<!-- NOT FOUND -->
<TextBlock

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"
@@ -59,16 +60,16 @@
</interactivity:Interaction.Behaviors>
<Grid VerticalAlignment="Center" CornerRadius="4">
<local:ImageSwitcher
x:Name="AlbumArtImageSwitcher"
Width="36"
Height="36" />
Height="36"
Source="{x:Bind ViewModel.GSMTCService.AlbumArtBitmapImage, Mode=OneWay}" />
</Grid>
<StackPanel VerticalAlignment="Center">
<TextBlock x:Name="TitleTextBlock" />
<TextBlock Text="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, Mode=OneWay}" />
<TextBlock
x:Name="ArtistsTextBlock"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Artist, Mode=OneWay}" />
</StackPanel>
</StackPanel>
@@ -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,10 +420,11 @@
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}" />
ThumbToolTipValueConverter="{StaticResource SecondsToFormattedTimeConverter}"
Value="{x:Bind ViewModel.GSMTCService.CurrentPosition.TotalSeconds, Mode=OneWay}" />
<Grid
x:Name="TimelineSliderLyricsLineInfo"

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;
@@ -21,15 +21,13 @@ using BetterLyrics.WinUI3.Extensions;
namespace BetterLyrics.WinUI3.Controls;
public sealed partial class NowPlayingBar : UserControl,
IRecipient<PropertyChangedMessage<SongInfo?>>,
IRecipient<PropertyChangedMessage<BitmapImage?>>,
IRecipient<PropertyChangedMessage<TimeSpan>>
public sealed partial class NowPlayingBar : UserControl
{
public NowPlayingBarViewModel ViewModel => (NowPlayingBarViewModel)DataContext;
public event EventHandler? SongInfoTapped;
public event EventHandler? TimeTapped;
public event EventHandler? PlayQueueButtonClick;
public bool ShowTime
{
@@ -64,6 +62,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 +83,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); }
@@ -109,8 +107,6 @@ public sealed partial class NowPlayingBar : UserControl,
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<NowPlayingBarViewModel>();
WeakReferenceMessenger.Default.RegisterAll(this);
}
private static void OnDependencyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -206,7 +202,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,45 +299,11 @@ 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)
{
PlaybackOrder = PlaybackOrder.GetNext();
}
public void Receive(PropertyChangedMessage<SongInfo?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentSongInfo))
{
TitleTextBlock.Text = message.NewValue?.Title;
ArtistsTextBlock.Text = message.NewValue?.DisplayArtists;
}
}
}
public void Receive(PropertyChangedMessage<BitmapImage?> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.AlbumArtBitmapImage))
{
AlbumArtImageSwitcher.Source = message.NewValue;
}
}
}
public void Receive(PropertyChangedMessage<TimeSpan> message)
{
if (message.Sender is IMediaSessionsService)
{
if (message.PropertyName == nameof(IMediaSessionsService.CurrentPosition))
{
TimelineSlider.Value = message.NewValue.TotalSeconds;
}
}
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.PatronControl"
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:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Margin="12,8">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind PatronName, Mode=OneWay}" />
<TextBlock Foreground="{ThemeResource TextFillColorTertiaryBrush}" Text="{x:Bind Date, Mode=OneWay}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,46 @@
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 PatronControl : UserControl
{
public string PatronName
{
get { return (string)GetValue(PatronNameProperty); }
set { SetValue(PatronNameProperty, value); }
}
public static readonly DependencyProperty PatronNameProperty =
DependencyProperty.Register(nameof(PatronName), typeof(string), typeof(PatronControl), new PropertyMetadata(""));
public string Date
{
get { return (string)GetValue(DateProperty); }
set { SetValue(DateProperty, value); }
}
public static readonly DependencyProperty DateProperty =
DependencyProperty.Register(nameof(Date), typeof(string), typeof(PatronControl), new PropertyMetadata(""));
public PatronControl()
{
InitializeComponent();
}
}
}

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.Artist, 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,22 @@
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.Artist, 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}" />
<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}" />
Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
</StackPanel>
</Expander>

View File

@@ -26,49 +26,82 @@
</Style>
</UserControl.Resources>
<Grid Margin="0,20,0,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="36,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ProgressBar
Grid.Row="0"
Background="Transparent"
IsIndeterminate="{x:Bind ViewModel.IsLoading, Mode=OneWay}"
Visibility="{x:Bind ViewModel.IsLoading, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
<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 Grid.Row="1" Visibility="{x:Bind ViewModel.GSMTCService.IsScrobbled, Mode=OneWay, Converter={StaticResource BoolNegationToVisibilityConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<InfoBar
x:Uid="StatsDashboardControlRecording"
Grid.Row="0"
IsClosable="False"
IsOpen="True"
Message="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, Mode=OneWay}" />
<ProgressBar
Grid.Row="1"
Background="Transparent"
Maximum="{x:Bind ViewModel.GSMTCService.TargetScrobbledDuration.TotalSeconds, Mode=OneWay}"
ShowPaused="{x:Bind ViewModel.GSMTCService.CurrentIsPlaying, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"
Value="{x:Bind ViewModel.GSMTCService.ScrobbledDuration.TotalSeconds, Mode=OneWay}" />
</Grid>
<ScrollViewer Grid.Row="1" Padding="36,0">
<controls:WrapPanel
Grid.Row="2"
Margin="36,36,36,12"
HorizontalSpacing="12"
Orientation="Horizontal"
VerticalSpacing="12">
<ComboBox
x:Uid="StatsDashboardControlTimeRange"
Header="Time Range"
SelectedIndex="{x:Bind ViewModel.SelectedTimeRange, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="StatsDashboardControlToday" />
<ComboBoxItem x:Uid="StatsDashboardControlThisWeek" />
<ComboBoxItem x:Uid="StatsDashboardControlThisMonth" />
<ComboBoxItem x:Uid="StatsDashboardControlThisQuarter" />
<ComboBoxItem x:Uid="StatsDashboardControlThisYear" />
<ComboBoxItem x:Uid="StatsDashboardControlCustom" />
</ComboBox>
<CalendarDatePicker
x:Uid="StatsDashboardControlStart"
Date="{x:Bind ViewModel.CustomStartDate, Mode=TwoWay}"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}" />
<TimePicker
VerticalAlignment="Bottom"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}"
Time="{x:Bind ViewModel.CustomStartTime, Mode=TwoWay}" />
<CalendarDatePicker
x:Uid="StatsDashboardControlEnd"
Date="{x:Bind ViewModel.CustomEndDate, Mode=TwoWay}"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}" />
<TimePicker
VerticalAlignment="Bottom"
IsEnabled="{x:Bind ViewModel.IsCustomRangeSelected, Mode=OneWay}"
Time="{x:Bind ViewModel.CustomEndTime, Mode=TwoWay}" />
<Button
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.RefreshDataCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=16,
Glyph=&#xE72C;}" />
</controls:WrapPanel>
<ScrollViewer Grid.Row="3" Padding="36,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
@@ -235,157 +268,143 @@
<!-- Top artists -->
<Border Grid.Column="0" Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition MinHeight="250" />
</Grid.RowDefinitions>
<StackPanel>
<TextBlock
x:Uid="StatsDashboardControlTopArtists"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<lvc:CartesianChart
Grid.Row="1"
Margin="0,8,0,0"
Background="Transparent">
<ItemsControl ItemsSource="{x:Bind ViewModel.TopArtists, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="statsmodels:ArtistPlayCount">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Spacing="10">
<PersonPicture
Width="32"
Height="32"
DisplayName="{x:Bind Artist}" />
<TextBlock
VerticalAlignment="Center"
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind Artist}" />
</StackPanel>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
x:Uid="StatsDashboardControlTrackCountText"
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<lvc:CartesianChart.XAxes>
<lvc:AxesCollection>
<lvc:XamlAxis x:Uid="StatsDashboardControlTrackCountAxis" NameTextSize="{StaticResource BodyTextBlockFontSize}" />
</lvc:AxesCollection>
</lvc:CartesianChart.XAxes>
<lvc:CartesianChart.YAxes>
<lvc:AxesCollection>
<lvc:XamlAxis IsInverted="True" IsVisible="False" />
</lvc:AxesCollection>
</lvc:CartesianChart.YAxes>
<lvc:CartesianChart.Series>
<lvc:SeriesCollection>
<lvc:XamlRowSeries
Padding="4"
DataLabelsFormatter="{x:Bind ViewModel.ArtistsLabelsFormatter}"
DataLabelsSize="{StaticResource BodyTextBlockFontSize}"
Rx="4"
Ry="4"
ShowDataLabels="True"
Values="{x:Bind ViewModel.ArtistSeriesValues, Mode=OneWay}" />
</lvc:SeriesCollection>
</lvc:CartesianChart.Series>
</lvc:CartesianChart>
</Grid>
</StackPanel>
</Border>
<!-- 播放源分布 -->
<!-- Top tracks -->
<Border
Grid.Column="1"
Margin="0,0,0,12"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel>
<TextBlock
x:Uid="StatsDashboardControlSources"
x:Uid="StatsDashboardControlTopSongs"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<ItemsControl ItemsSource="{x:Bind ViewModel.TopSongs, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="statsmodels:SongPlayCount">
<Grid Margin="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
Width="40"
Height="40"
Margin="0,0,12,0"
Background="{ThemeResource LayerFillColorAltBrush}"
CornerRadius="4">
<FontIcon
FontSize="16"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Glyph="&#xE8D6;" />
</Grid>
<lvc:PieChart
Grid.Row="1"
MinHeight="160"
Background="Transparent"
LegendPosition="Right"
LegendTextSize="{StaticResource BodyTextBlockFontSize}"
Series="{x:Bind ViewModel.SourceSeries, Mode=OneWay}"
TooltipPosition="Center" />
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Style="{ThemeResource BodyStrongTextBlockStyle}" Text="{x:Bind Title}" />
<TextBlock
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}"
Style="{ThemeResource CaptionTextBlockStyle}"
Text="{x:Bind Artist}" />
</StackPanel>
</Grid>
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
FontWeight="SemiBold">
<Run Text="{x:Bind PlayCount}" />
<Run
x:Uid="StatsDashboardControlTrackCountText"
FontSize="10"
FontWeight="Normal"
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" />
</TextBlock>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</Grid>
<!-- Top tracks -->
<!-- 播放源分布 -->
<Border
Grid.Row="3"
Margin="0,0,0,20"
Style="{StaticResource StatsCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock
x:Uid="StatsDashboardControlTopSongs"
Margin="0,0,0,18"
x:Uid="StatsDashboardControlSources"
Margin="0,0,0,12"
Style="{ThemeResource SubtitleTextBlockStyle}" />
<dev:OpacityMaskView Grid.Row="1" Margin="-20,0">
<dev:OpacityMaskView.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Offset="0" Color="Transparent" />
<GradientStop Offset="0.02" Color="#FFFFFFFF" />
<GradientStop Offset="0.98" Color="#FFFFFFFF" />
<GradientStop Offset="1" Color="Transparent" />
</LinearGradientBrush>
</dev:OpacityMaskView.OpacityMask>
<ListView
Padding="20,0"
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
ItemsSource="{x:Bind ViewModel.TopSongs, Mode=OneWay}"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.HorizontalScrollMode="Enabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollMode="Disabled"
SelectionMode="None">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate x:DataType="statsmodels:SongPlayCount">
<Grid
MinWidth="200"
Margin="6,0"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="4">
<StackPanel Margin="20">
<TextBlock
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind Title}"
TextWrapping="Wrap" />
<RichTextBlock>
<Paragraph>
<Run Text="{x:Bind Artist}" />
<Run Text="·" />
<Run Text="{x:Bind PlayCount}" />
<Run x:Uid="StatsDashboardControlTrackCountText" />
</Paragraph>
</RichTextBlock>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</dev:OpacityMaskView>
<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>
</Grid>
</ScrollViewer>
<Button
Grid.Row="1"
<!--<Button
Grid.Row="2"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.GenerateTestDataCommand}"
Content="Generate test data"
Visibility="Collapsed" />
Content="Generate test data" />-->
</Grid>
</UserControl>

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

@@ -8,21 +8,6 @@ namespace BetterLyrics.WinUI3.Extensions
{
extension(LyricsSearchProvider provider)
{
public string GetCacheDirectory() => provider switch
{
LyricsSearchProvider.LrcLib => PathHelper.LrcLibLyricsCacheDirectory,
LyricsSearchProvider.QQ => PathHelper.QQLyricsCacheDirectory,
LyricsSearchProvider.Netease => PathHelper.NeteaseLyricsCacheDirectory,
LyricsSearchProvider.Kugou => PathHelper.KugouLyricsCacheDirectory,
LyricsSearchProvider.AmllTtmlDb => PathHelper.AmllTtmlDbLyricsCacheDirectory,
LyricsSearchProvider.AppleMusic => PathHelper.AppleMusicCacheDirectory,
LyricsSearchProvider.LocalMusicFile => PathHelper.LocalMusicCacheDirectory,
LyricsSearchProvider.LocalLrcFile => PathHelper.LocalLrcCacheDirectory,
LyricsSearchProvider.LocalEslrcFile => PathHelper.LocalEslrcCacheDirectory,
LyricsSearchProvider.LocalTtmlFile => PathHelper.LocalTtmlCacheDirectory,
_ => throw new ArgumentOutOfRangeException(nameof(provider)),
};
public LyricsFormat GetLyricsFormat() => provider switch
{
LyricsSearchProvider.LrcLib => LyricsFormat.Lrc,

View File

@@ -1,15 +1,18 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
namespace BetterLyrics.WinUI3.Extensions
{
public static class SongInfoExtensions
{
public static SongInfo Placeholder => new SongInfo
public static SongInfo Placeholder => new()
{
Title = "N/A",
Album = "N/A",
Artists = ["N/A"],
Artist = "N/A",
};
extension(SongInfo songInfo)
@@ -20,9 +23,9 @@ namespace BetterLyrics.WinUI3.Extensions
return songInfo;
}
public SongInfo WithArtist(string[] value)
public SongInfo WithArtist(string value)
{
songInfo.Artists = value;
songInfo.Artist = value;
return songInfo;
}
@@ -39,14 +42,31 @@ namespace BetterLyrics.WinUI3.Extensions
return new PlayHistoryItem
{
Title = songInfo.Title,
Artist = songInfo.DisplayArtists,
Artist = songInfo.Artist,
Album = songInfo.Album,
PlayerId = songInfo.PlayerId ?? "N/A",
TotalDurationMs = songInfo.DurationMs,
DurationPlayedMs = actualPlayedMs,
StartedAt = DateTime.Now.AddMilliseconds(-actualPlayedMs)
StartedAt = DateTime.FromBinary(songInfo.StartedAt)
};
}
public string GetCacheKey()
{
string title = songInfo.Title?.Trim() ?? "";
string album = songInfo.Album?.Trim() ?? "";
string artists = songInfo.Artist?.Trim() ?? "";
long seconds = (long)Math.Round(songInfo.Duration);
string durationPart = seconds.ToString(CultureInfo.InvariantCulture);
string rawKey = $"{title}|{artists}|{album}|{durationPart}";
using var sha256 = SHA256.Create();
byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(rawKey));
return Convert.ToHexString(bytes);
}
}
}
}

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

@@ -53,22 +53,6 @@ namespace BetterLyrics.WinUI3.Helper
return sb.ToString();
}
public static LyricsSearchResult? ReadLyricsCache(SongInfo songInfo, LyricsSearchProvider lyricsSearchProvider)
{
var cacheFilePath = Path.Combine(
lyricsSearchProvider.GetCacheDirectory(),
SanitizeFileName($"{songInfo.ToFileName()}.json"));
if (File.Exists(cacheFilePath))
{
var json = File.ReadAllText(cacheFilePath);
var data = System.Text.Json.JsonSerializer.Deserialize(json, SourceGenerationContext.Default.LyricsSearchResult);
data?.SelfPath = cacheFilePath;
return data;
}
return null;
}
public static byte[]? ReadAlbumArtCache(string album, string artist, string format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}"));
@@ -79,19 +63,9 @@ namespace BetterLyrics.WinUI3.Helper
return null;
}
public static void WriteLyricsCache(SongInfo songInfo, LyricsSearchResult lyricsSearchResult)
{
var cacheFilePath = Path.Combine(
lyricsSearchResult.Provider.GetCacheDirectory(),
SanitizeFileName($"{songInfo.ToFileName()}.json"));
lyricsSearchResult.SelfPath = cacheFilePath;
var json = System.Text.Json.JsonSerializer.Serialize(lyricsSearchResult, SourceGenerationContext.Default.LyricsSearchResult);
File.WriteAllText(cacheFilePath, json);
}
public static void WriteAlbumArtCache(SongInfo songInfo, byte[] img, string format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{songInfo.DisplayArtists} - {songInfo.Album}{format}"));
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{songInfo.Artist} - {songInfo.Album}{format}"));
File.WriteAllBytes(cacheFilePath, img);
}

View File

@@ -17,7 +17,7 @@ namespace BetterLyrics.WinUI3.Helper
// JaroWinkler 适合短字符串匹配
private static readonly JaroWinkler _algo = new();
public static int CalculateScore(SongInfo local, LyricsSearchResult remote)
public static int CalculateScore(SongInfo local, LyricsCacheItem remote)
{
if (local == null || remote == null) return 0;
@@ -29,7 +29,7 @@ namespace BetterLyrics.WinUI3.Helper
if (localHasMetadata && remoteHasMetadata)
{
double titleScore = GetStringSimilarity(local.Title, remote.Title);
double artistScore = GetArtistSimilarity(local.Artists, remote.Artists);
double artistScore = GetStringSimilarity(local.Artist, remote.Artist);
double albumScore = GetStringSimilarity(local.Album, remote.Album);
double durationScore = GetDurationSimilarity(local.DurationMs, remote.Duration);
@@ -41,12 +41,12 @@ namespace BetterLyrics.WinUI3.Helper
else
{
string? localQuery = localHasMetadata
? $"{local.Title} {string.Join(" ", local.Artists ?? [])}"
? $"{local.Title} {local.Artist}"
: Path.GetFileNameWithoutExtension(local.LinkedFileName);
string remoteQuery = remoteHasMetadata
? $"{remote.Title} {string.Join(" ", remote.Artists ?? [])}"
: Path.GetFileNameWithoutExtension(remote.Reference);
string? remoteQuery = remoteHasMetadata
? $"{remote.Title} {remote.Artist}"
: null;
string fp1 = CreateSortedFingerprint(localQuery);
string fp2 = CreateSortedFingerprint(remoteQuery);

View File

@@ -40,16 +40,6 @@ namespace BetterLyrics.WinUI3.Helper
public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt");
public static string LyricsCacheDirectory => Path.Combine(CacheFolder, "lyrics");
public static string LrcLibLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "lrclib");
public static string NeteaseLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "netease");
public static string QQLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "qq");
public static string KugouLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "kugou");
public static string AmllTtmlDbLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "amll-ttml-db");
public static string AppleMusicCacheDirectory => Path.Combine(LyricsCacheDirectory, "apple-music");
public static string LocalMusicCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-music");
public static string LocalLrcCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-lrc");
public static string LocalEslrcCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-eslrc");
public static string LocalTtmlCacheDirectory => Path.Combine(LyricsCacheDirectory, "local-ttml");
public static string AmllTtmlDbIndexPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-index.jsonl");
public static string AmllTtmlDbLastUpdatedPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-last-updated.txt");
@@ -60,26 +50,17 @@ namespace BetterLyrics.WinUI3.Helper
public static string PlayQueuePath => Path.Combine(LocalFolder, "play-queue.m3u");
public static string PlayHistoryPath => Path.Combine(LocalFolder, "play-history.db");
public static string FilesIndexPath => Path.Combine(LocalFolder, "files-index.db");
public static string SongSearchMapPath => Path.Combine(LocalFolder, "song-search-map.db");
public static string LyricsCachePath => Path.Combine(LyricsCacheDirectory, "lyrics-cache.db");
public static void EnsureDirectories()
{
Directory.CreateDirectory(SettingsDirectory);
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LrcLibLyricsCacheDirectory);
Directory.CreateDirectory(QQLyricsCacheDirectory);
Directory.CreateDirectory(KugouLyricsCacheDirectory);
Directory.CreateDirectory(NeteaseLyricsCacheDirectory);
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
Directory.CreateDirectory(AppleMusicCacheDirectory);
Directory.CreateDirectory(LocalMusicCacheDirectory);
Directory.CreateDirectory(LocalLrcCacheDirectory);
Directory.CreateDirectory(LocalEslrcCacheDirectory);
Directory.CreateDirectory(LocalTtmlCacheDirectory);
Directory.CreateDirectory(LyricsCacheDirectory);
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
Directory.CreateDirectory(LocalAlbumArtCacheDirectory);
}
}
}

View File

@@ -17,7 +17,7 @@ namespace BetterLyrics.WinUI3.Logic
IList<RenderLyricsLine>? lines,
int startIndex,
int endIndex,
int playingLineIndex,
int primaryPlayingLineIndex,
double canvasHeight,
double targetYScrollOffset,
double playingLineTopOffsetFactor,
@@ -29,13 +29,14 @@ namespace BetterLyrics.WinUI3.Logic
TimeSpan elapsedTime,
bool isMouseScrolling,
bool isLayoutChanged,
bool isPlayingLineChanged,
bool isMouseScrollingChanged
bool isPrimaryPlayingLineChanged,
bool isMouseScrollingChanged,
double currentProgressMs
)
{
if (lines == null) return;
var currentPlayingLine = lines.ElementAtOrDefault(playingLineIndex);
var currentPlayingLine = lines.ElementAtOrDefault(primaryPlayingLineIndex);
if (currentPlayingLine == null) return;
var phoneticOpacity = lyricsStyle.PhoneticLyricsOpacity / 100.0;
@@ -47,13 +48,17 @@ namespace BetterLyrics.WinUI3.Logic
var line = lines.ElementAtOrDefault(i);
if (line == null) continue;
if (isLayoutChanged || isPlayingLineChanged || isMouseScrollingChanged)
bool isSecondaryLinePlaying = currentProgressMs >= line.StartMs && currentProgressMs <= line.EndMs;
if (i == primaryPlayingLineIndex) isSecondaryLinePlaying = true;
bool isSecondaryLinePlayingChanged = line.IsPlayingLastFrame != isSecondaryLinePlaying;
line.IsPlayingLastFrame = isSecondaryLinePlaying;
if (isLayoutChanged || isPrimaryPlayingLineChanged || isMouseScrollingChanged || isSecondaryLinePlayingChanged)
{
int lineCountDelta = i - playingLineIndex;
int absLineCountDelta = Math.Abs(lineCountDelta);
int lineCountDelta = i - primaryPlayingLineIndex;
double distanceFromPlayingLine = Math.Abs(line.OriginalPosition.Y - currentPlayingLine.OriginalPosition.Y);
double distanceFactor = 0;
double distanceFactor;
if (lineCountDelta < 0)
{
distanceFactor = Math.Clamp(distanceFromPlayingLine / (canvasHeight * playingLineTopOffsetFactor), 0, 1);
@@ -88,45 +93,53 @@ namespace BetterLyrics.WinUI3.Logic
line.BlurAmountTransition.SetDuration(yScrollDuration);
line.BlurAmountTransition.SetDelay(yScrollDelay);
line.BlurAmountTransition.StartTransition(isMouseScrolling ? 0 : (lyricsEffect.IsLyricsBlurEffectEnabled ? (5 * distanceFactor) : 0));
line.BlurAmountTransition.StartTransition(
(isMouseScrolling || isSecondaryLinePlaying) ? 0 :
(lyricsEffect.IsLyricsBlurEffectEnabled ? (5 * distanceFactor) : 0));
line.ScaleTransition.SetDuration(yScrollDuration);
line.ScaleTransition.SetDelay(yScrollDelay);
line.ScaleTransition.StartTransition(
lyricsEffect.IsLyricsOutOfSightEffectEnabled ?
isSecondaryLinePlaying ? _highlightedScale :
(lyricsEffect.IsLyricsOutOfSightEffectEnabled ?
(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale)) :
_highlightedScale);
_highlightedScale));
line.PhoneticOpacityTransition.SetDuration(yScrollDuration);
line.PhoneticOpacityTransition.SetDelay(yScrollDelay);
line.PhoneticOpacityTransition.StartTransition(
isSecondaryLinePlaying ? phoneticOpacity :
CalculateTargetOpacity(phoneticOpacity, phoneticOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
// 原文不透明度(已播放)
line.PlayedOriginalOpacityTransition.SetDuration(yScrollDuration);
line.PlayedOriginalOpacityTransition.SetDelay(yScrollDelay);
line.PlayedOriginalOpacityTransition.StartTransition(
isSecondaryLinePlaying ? 1.0 :
CalculateTargetOpacity(originalOpacity, 1.0, distanceFactor, isMouseScrolling, lyricsEffect));
// 原文不透明度(未播放)
line.UnplayedOriginalOpacityTransition.SetDuration(yScrollDuration);
line.UnplayedOriginalOpacityTransition.SetDelay(yScrollDelay);
line.UnplayedOriginalOpacityTransition.StartTransition(
isSecondaryLinePlaying ? originalOpacity :
CalculateTargetOpacity(originalOpacity, originalOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
line.TranslatedOpacityTransition.SetDuration(yScrollDuration);
line.TranslatedOpacityTransition.SetDelay(yScrollDelay);
line.TranslatedOpacityTransition.StartTransition(
isSecondaryLinePlaying ? translatedOpacity :
CalculateTargetOpacity(translatedOpacity, translatedOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
line.ColorTransition.SetDuration(yScrollDuration);
line.ColorTransition.SetDelay(yScrollDelay);
line.ColorTransition.StartTransition(absLineCountDelta == 0 ? fgColor : bgColor);
line.ColorTransition.StartTransition(isSecondaryLinePlaying ? fgColor : bgColor);
line.AngleTransition.SetEasingType(canvasYScrollTransition.EasingType);
line.AngleTransition.SetDuration(yScrollDuration);
line.AngleTransition.SetDelay(yScrollDelay);
line.AngleTransition.StartTransition(
(lyricsEffect.IsFanLyricsEnabled && !isMouseScrolling) ?
Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > playingLineIndex ? 1 : -1) :
Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > primaryPlayingLineIndex ? 1 : -1) :
0);
line.YOffsetTransition.SetEasingType(canvasYScrollTransition.EasingType);

View File

@@ -187,6 +187,37 @@ namespace BetterLyrics.WinUI3.Logic
return lines.Last().BottomRightPosition.Y;
}
public static void CalculateLanes(IList<RenderLyricsLine>? lines, int toleranceMs = 50)
{
if (lines == null) return;
var lanesEndMs = new List<int> { 0 };
foreach (var line in lines)
{
var start = line.StartMs;
var end = line.EndMs;
int assignedLane = -1;
for (int i = 0; i < lanesEndMs.Count; i++)
{
if (lanesEndMs[i] <= start + toleranceMs)
{
assignedLane = i;
break;
}
}
if (assignedLane == -1)
{
assignedLane = lanesEndMs.Count;
lanesEndMs.Add(0);
}
lanesEndMs[assignedLane] = end ?? 0;
line.LaneIndex = assignedLane;
}
}
public static int FindMouseHoverLineIndex(
IList<RenderLyricsLine>? lines,
bool isMouseInLyricsArea,

View File

@@ -13,30 +13,53 @@ namespace BetterLyrics.WinUI3.Logic
_lastFoundIndex = 0;
}
public int GetCurrentLineIndex(double currentTimeMs, LyricsData? lyricsData)
public int GetCurrentLineIndex(double currentTimeMs, IList<RenderLyricsLine>? lines)
{
if (lyricsData == null || lyricsData.LyricsLines.Count == 0) return 0;
var lines = lyricsData.LyricsLines;
if (lines == null || lines.Count == 0) return 0;
// Cache hit
if (IsTimeInLine(currentTimeMs, lines, _lastFoundIndex)) return _lastFoundIndex;
if (_lastFoundIndex + 1 < lines.Count && IsTimeInLine(currentTimeMs, lines, _lastFoundIndex + 1))
if (_lastFoundIndex >= 0 && _lastFoundIndex < lines.Count)
{
_lastFoundIndex++;
return _lastFoundIndex;
var lastLine = lines[_lastFoundIndex];
if (lastLine.LaneIndex == 0 && IsTimeInLine(currentTimeMs, lines, _lastFoundIndex))
{
return _lastFoundIndex;
}
}
// Cache miss
int bestCandidateIndex = -1;
int bestCandidateLane = int.MaxValue;
for (int i = 0; i < lines.Count; i++)
{
if (IsTimeInLine(currentTimeMs, lines, i))
{
_lastFoundIndex = i;
return i;
var currentLine = lines[i];
int currentLane = currentLine.LaneIndex;
if (currentLane == 0)
{
_lastFoundIndex = i;
return i;
}
if (currentLane < bestCandidateLane)
{
bestCandidateIndex = i;
bestCandidateLane = currentLane;
}
}
else if (lines[i].StartMs > currentTimeMs + 1000)
{
break;
}
}
// Default
if (bestCandidateIndex != -1)
{
_lastFoundIndex = bestCandidateIndex;
return bestCandidateIndex;
}
return Math.Min(_lastFoundIndex, lines.Count - 1);
}
@@ -140,7 +163,7 @@ namespace BetterLyrics.WinUI3.Logic
return state;
}
private bool IsTimeInLine(double time, IList<LyricsLine> lines, int index)
private bool IsTimeInLine(double time, IList<RenderLyricsLine> lines, int index)
{
if (index < 0 || index >= lines.Count) return false;
var line = lines[index];

View File

@@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Db
{
public partial class LyricsCacheDbContext : DbContext
{
public LyricsCacheDbContext(DbContextOptions<LyricsCacheDbContext> options) : base(options) { }
public DbSet<LyricsCacheItem> LyricsCache { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Db
{
public partial class SongSearchMapDbContext : DbContext
{
public DbSet<MappedSongSearchQuery> SongSearchMap { get; set; }
public SongSearchMapDbContext(DbContextOptions<SongSearchMapDbContext> options) : base(options) { }
}
}

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 { }
@@ -144,7 +129,7 @@ namespace BetterLyrics.WinUI3.Models
this.Uri = entity.Uri;
this.Title = entity.Title;
this.Artist = entity.Artists;
this.Artist = entity.Artist;
this.Album = entity.Album;
this.Year = entity.Year;
this.Bitrate = entity.Bitrate;

View File

@@ -10,25 +10,19 @@ namespace BetterLyrics.WinUI3.Models
[Index(nameof(Uri), IsUnique = true)] // 唯一索引
public class FilesIndexItem
{
[Key] // 主键
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] // 明确指定为自增 (Identity)
public int Id { get; set; }
[Key][DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
// 关联到 MediaFolder.Id
// 注意:作为索引列,必须限制长度,否则 SQL Server 会报错 (索引最大900字节)
[MaxLength(450)]
public string MediaFolderId { get; set; }
[MaxLength(450)] public string MediaFolderId { get; set; }
// 存储父文件夹的标准 URI
// 允许为空
[MaxLength(450)]
public string? ParentUri { get; set; }
[MaxLength(450)] public string? ParentUri { get; set; }
// 唯一索引列
// 必须限制长度。450字符 * 2字节/字符 = 900字节 (正好卡在 SQL Server 限制内)
[Required]
[MaxLength(450)]
public string Uri { get; set; }
[Required][MaxLength(450)] public string Uri { get; set; }
public string FileName { get; set; } = "";
@@ -41,7 +35,7 @@ namespace BetterLyrics.WinUI3.Models
// 下面的元数据字段通常不需要索引,可以使用 MaxLength 稍微优化空间,
// 或者直接留空(默认为 nvarchar(max)
public string Title { get; set; } = "";
public string Artists { get; set; } = "";
[Column("Artists")] public string Artist { get; set; } = "";
public string Album { get; set; } = "";
public int? Year { get; set; }
public int Bitrate { get; set; }
@@ -49,11 +43,9 @@ namespace BetterLyrics.WinUI3.Models
public int BitDepth { get; set; }
public int Duration { get; set; }
[MaxLength(50)] // 格式名称通常很短,限制一下是个好习惯
public string AudioFormatName { get; set; } = "";
[MaxLength(50)] public string AudioFormatName { get; set; } = "";
[MaxLength(20)]
public string AudioFormatShortName { get; set; } = "";
[MaxLength(20)] public string AudioFormatShortName { get; set; } = "";
public string Encoder { get; set; } = "";

View File

@@ -1,14 +1,24 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
using NTextCat.Commons;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace BetterLyrics.WinUI3.Models
{
public partial class LyricsSearchResult : ObservableObject, ICloneable
[Table("LyricsCache")]
// 建立联合索引,确保同一个 Provider 下,同一个 Hash 只有一条记录
[Index(nameof(CacheKey), nameof(Provider), IsUnique = false)]
public partial class LyricsCacheItem : ObservableObject, ICloneable
{
[Key][DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
[MaxLength(64)][Required] public string CacheKey { get; set; }
public LyricsSearchProvider Provider { get; set; }
[ObservableProperty] public partial TranslationSearchProvider? TranslationProvider { get; set; }
[ObservableProperty] public partial TransliterationSearchProvider? TransliterationProvider { get; set; }
@@ -25,33 +35,38 @@ namespace BetterLyrics.WinUI3.Models
/// </summary>
public string? Transliteration { get; set; }
[MaxLength(255)]
public string? Title { get; set; }
public string[]? Artists { get; set; }
[MaxLength(255)]
public string? Artist { get; set; }
[MaxLength(255)]
public string? Album { get; set; }
public double? Duration { get; set; }
[ObservableProperty] public partial int MatchPercentage { get; set; } = -1;
[ObservableProperty] public partial string Reference { get; set; } = "about:blank";
public string? SelfPath { get; set; }
[NotMapped][JsonIgnore] public bool IsFound => !string.IsNullOrEmpty(Raw);
[JsonIgnore] public bool IsFound => !string.IsNullOrEmpty(Raw);
[JsonIgnore] public LyricsSearchProvider? ProviderIfFound => IsFound ? Provider : null;
[JsonIgnore] public string? DisplayArtists => Artists?.Join("; ");
[NotMapped][JsonIgnore] public LyricsSearchProvider? ProviderIfFound => IsFound ? Provider : null;
public object Clone()
{
return new LyricsSearchResult()
return new LyricsCacheItem()
{
Album = this.Album,
Duration = this.Duration,
Provider = this.Provider,
TranslationProvider = this.TranslationProvider,
TransliterationProvider = this.TransliterationProvider,
Raw = this.Raw,
Translation = this.Translation,
Transliteration = this.Transliteration,
Title = this.Title,
Artists = this.Artists,
Artist = this.Artist,
Album = this.Album,
Duration = this.Duration,
MatchPercentage = this.MatchPercentage,
Provider = this.Provider,
Reference = this.Reference
};
}
@@ -59,7 +74,7 @@ namespace BetterLyrics.WinUI3.Models
public void CopyFromSongInfo(SongInfo songInfo)
{
Title = songInfo.Title;
Artists = songInfo.Artists;
Artist = songInfo.Artist;
Album = songInfo.Album;
}
}

View File

@@ -1,10 +1,18 @@
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BetterLyrics.WinUI3.Models
{
public partial class MappedSongSearchQuery : ObservableRecipient
[Table("SongSearchMap")]
[Index(nameof(OriginalTitle), nameof(OriginalArtist), nameof(OriginalAlbum))]
public partial class MappedSongSearchQuery : ObservableRecipient, ICloneable
{
[Key][DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string OriginalTitle { get; set; } = string.Empty;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string OriginalArtist { get; set; } = string.Empty;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string OriginalAlbum { get; set; } = string.Empty;
@@ -17,7 +25,7 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsSearchProvider? LyricsSearchProvider { get; set; }
public MappedSongSearchQuery Clone()
public object Clone()
{
return new MappedSongSearchQuery
{

View File

@@ -57,6 +57,16 @@ namespace BetterLyrics.WinUI3.Models
public CanvasGeometry? TranslatedCanvasGeometry { get; private set; }
public CanvasGeometry? PhoneticCanvasGeometry { get; private set; }
/// <summary>
/// 轨道索引 (0 = 主轨道, 1 = 第一副轨道, etc.)
/// 用于布局计算时的堆叠逻辑
/// </summary>
public int LaneIndex { get; set; } = 0;
/// <summary>
/// 是否为背景人声/和声
/// </summary>
public bool IsPlayingLastFrame { get; set; } = false;
public RenderLyricsLine()
{
AngleTransition = new(

View File

@@ -1,5 +1,6 @@
using BetterLyrics.WinUI3.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
namespace BetterLyrics.WinUI3.Models.Settings
{
@@ -14,7 +15,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MediaFolder> LocalMediaFolders { get; set; } = [];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MediaSourceProviderInfo> MediaSourceProvidersInfo { get; set; } = [];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MappedSongSearchQuery> MappedSongSearchQueries { get; set; } = [];
[Obsolete][ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<MappedSongSearchQuery> MappedSongSearchQueries { get; set; } = [];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<LyricsWindowStatus> WindowBoundsRecords { get; set; } = [];
[ObservableProperty][NotifyPropertyChangedRecipients] public partial FullyObservableCollection<SongsTabInfo> StarredPlaylists { get; set; } = [];

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

@@ -6,13 +6,13 @@ using System;
namespace BetterLyrics.WinUI3.Models
{
public partial class SongInfo : ObservableObject, ICloneable
public partial class SongInfo : ObservableRecipient, ICloneable
{
[ObservableProperty]
public partial string Album { get; set; }
[ObservableProperty]
public partial string[] Artists { get; set; }
public partial string Artist { get; set; }
[ObservableProperty]
public partial double DurationMs { get; set; }
@@ -26,12 +26,12 @@ namespace BetterLyrics.WinUI3.Models
[ObservableProperty]
public partial string? SongId { get; set; } = null;
[ObservableProperty] public partial long StartedAt { get; set; } = DateTime.Now.ToBinary();
public string? LinkedFileName { get; set; } = null;
public double Duration => DurationMs / 1000;
public string DisplayArtists => Artists.Join(ATL.Settings.DisplayValueSeparator.ToString());
public SongInfo() { }
public object Clone()
@@ -39,12 +39,13 @@ namespace BetterLyrics.WinUI3.Models
return new SongInfo()
{
Title = this.Title,
Artists = this.Artists,
Artist = this.Artist,
Album = this.Album,
DurationMs = this.DurationMs,
PlayerId = this.PlayerId,
SongId = this.SongId,
LinkedFileName = this.LinkedFileName,
StartedAt = this.StartedAt,
};
}
@@ -52,7 +53,7 @@ namespace BetterLyrics.WinUI3.Models
{
return
$"Title: {Title}, " +
$"Artist: {DisplayArtists}, " +
$"Artist: {Artist}, " +
$"Album: {Album}, " +
$"Duration: {Duration} sec, " +
$"Plauer ID: {PlayerId}, " +
@@ -62,7 +63,7 @@ namespace BetterLyrics.WinUI3.Models
public string ToFileName()
{
return $"{DisplayArtists} - {Title} - {Album} - {Duration}";
return $"{Artist} - {Title} - {Album} - {Duration}";
}
}
}

View File

@@ -40,11 +40,24 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
startIndex += text.Length;
}
if (syllables.Count > 1)
int lineEndMs = 0;
if (syllables.Count > 0)
{
var lastSyllable = syllables[syllables.Count - 1];
if (string.IsNullOrWhiteSpace(lastSyllable.Text))
{
lineEndMs = lastSyllable.StartMs;
syllables.RemoveAt(syllables.Count - 1);
}
}
if (syllables.Count > 0)
{
lrcLines.Add(new LyricsLine
{
StartMs = syllables[0].StartMs,
EndMs = lineEndMs,
OriginalText = string.Concat(syllables.Select(s => s.Text)),
LyricsSyllables = syllables
});
@@ -56,17 +69,19 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
var bracketMatches = bracketRegex.Matches(line);
string content = line;
int? lineStartTime = null;
int lineStartMs;
if (bracketMatches.Count > 0)
{
var match = bracketMatches[0];
int min = int.Parse(match.Groups[1].Value);
int sec = int.Parse(match.Groups[2].Value);
int ms = int.Parse(match.Groups[4].Value.PadRight(3, '0'));
lineStartTime = min * 60_000 + sec * 1000 + ms;
lineStartMs = min * 60_000 + sec * 1000 + ms;
content = bracketRegex!.Replace(line, "").Trim();
if (content == "//") content = "";
lrcLines.Add(new LyricsLine { StartMs = lineStartTime.Value, OriginalText = content });
lrcLines.Add(new LyricsLine { StartMs = lineStartMs, OriginalText = content });
}
}
}

View File

@@ -2,12 +2,15 @@
using BetterLyrics.WinUI3.Models;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
public partial class LyricsParser
{
private readonly XNamespace _ttml = "http://www.w3.org/ns/ttml#metadata";
private void ParseTtml(string raw)
{
try
@@ -19,120 +22,146 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
var xdoc = XDocument.Parse(raw, LoadOptions.PreserveWhitespace);
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
if (body == null) return;
var ps = body.Descendants().Where(e => e.Name.LocalName == "p");
foreach (var p in ps)
{
// 句级时间
string? pBegin = p.Attribute("begin")?.Value;
string? pEnd = p.Attribute("end")?.Value;
int pStartMs = ParseTtmlTime(pBegin);
int pEndMs = ParseTtmlTime(pEnd);
ParseTtmlSegment(
container: p,
originalDest: originalLines,
transDest: translationLines,
romanDest: romanLines,
isBackground: false
);
// 只获取一级span
var spans = p.Elements()
.Where(s => s.Name.LocalName == "span")
.ToList();
var bgSpans = p.Elements().Where(s => s.Attribute(_ttml + "role")?.Value == "x-bg");
var originalTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == null)
.ToList();
// 处理原文span后的空白
for (int i = 0; i < originalTextSpans.Count; i++)
foreach (var bgSpan in bgSpans)
{
var span = originalTextSpans[i];
var nextNode = span.NodesAfterSelf().FirstOrDefault();
if (nextNode is XText textNode)
{
span.Value += textNode.Value;
}
// 把 span 当作一个容器,再调一次通用解析方法
ParseTtmlSegment(
container: bgSpan,
originalDest: originalLines,
transDest: translationLines,
romanDest: romanLines,
isBackground: true
);
}
// 拼接空白字符后的原文
string originalText = string.Concat(originalTextSpans.Select(s => s.Value));
var originalCharTimings = new List<LyricsSyllable>();
int originalStartIndex = 0;
foreach (var span in originalTextSpans)
{
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
originalCharTimings.Add(new LyricsSyllable
{
StartMs = sStartMs,
EndMs = sEndMs,
StartIndex = originalStartIndex,
Text = span.Value
});
originalStartIndex += span.Value.Length;
}
if (originalTextSpans.Count == 0)
{
originalText = p.Value;
}
originalLines.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
OriginalText = originalText,
LyricsSyllables = originalCharTimings,
});
// 解析 x-role
ParseTtmlXRole(spans, translationLines, "x-translation", pStartMs, pEndMs);
ParseTtmlXRole(spans, romanLines, "x-roman", pStartMs, pEndMs);
}
_lyricsDataArr.Add(new LyricsData(originalLines));
if (translationLines.Count > 0)
{
_lyricsDataArr.Add(new LyricsData(translationLines));
}
if (romanLines.Count > 0)
{
_lyricsDataArr.Add(new LyricsData(romanLines) { LanguageCode = PhoneticHelper.RomanCode });
}
}
catch
{
// 解析失败,忽略
}
catch { }
}
private void ParseTtmlXRole(List<XElement> sourceSpans, List<LyricsLine> saveLyricsLines, string xRole, int pStartMs, int? pEndMs)
private void ParseTtmlSegment(
XElement container,
List<LyricsLine> originalDest,
List<LyricsLine> transDest,
List<LyricsLine> romanDest,
bool isBackground)
{
var textSpans = sourceSpans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == xRole)
int containerStartMs = ParseTtmlTime(container.Attribute("begin")?.Value);
int containerEndMs = ParseTtmlTime(container.Attribute("end")?.Value);
var contentSpans = container.Elements()
.Where(s => s.Name.LocalName == "span")
.Where(s =>
{
var role = s.Attribute(_ttml + "role")?.Value;
return role == null;
})
.ToList();
string text = string.Concat(textSpans.Select(s => s.Value));
var charTimings = new List<LyricsSyllable>();
int startIndex = 0;
foreach (var span in textSpans)
for (int i = 0; i < contentSpans.Count; i++)
{
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
charTimings.Add(new LyricsSyllable
var span = contentSpans[i];
var nextNode = span.NodesAfterSelf().FirstOrDefault();
if (nextNode is XText textNode)
{
span.Value += textNode.Value;
}
}
var syllables = new List<LyricsSyllable>();
int startIndex = 0;
var sbText = new System.Text.StringBuilder();
foreach (var span in contentSpans)
{
int sStartMs = ParseTtmlTime(span.Attribute("begin")?.Value);
int sEndMs = ParseTtmlTime(span.Attribute("end")?.Value);
string text = span.Value;
syllables.Add(new LyricsSyllable
{
StartMs = sStartMs,
EndMs = sEndMs,
StartIndex = startIndex,
Text = span.Value
Text = text
});
startIndex += span.Value.Length;
sbText.Append(text);
startIndex += text.Length;
}
if (textSpans.Count > 0)
string fullOriginalText = sbText.ToString();
if (contentSpans.Count == 0)
{
saveLyricsLines.Add(new LyricsLine
fullOriginalText = container.Value;
}
originalDest.Add(new LyricsLine
{
StartMs = containerStartMs,
EndMs = containerEndMs,
OriginalText = fullOriginalText,
LyricsSyllables = syllables
});
var transSpan = container.Elements()
.FirstOrDefault(s => s.Attribute(_ttml + "role")?.Value == "x-translation");
AddAuxiliaryLine(transDest, transSpan, containerStartMs, containerEndMs);
var romanSpan = container.Elements()
.FirstOrDefault(s => s.Attribute(_ttml + "role")?.Value == "x-roman");
AddAuxiliaryLine(romanDest, romanSpan, containerStartMs, containerEndMs);
}
private void AddAuxiliaryLine(List<LyricsLine> destList, XElement? span, int startMs, int endMs)
{
if (span != null)
{
string text = span.Value;
destList.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
OriginalText = text,
LyricsSyllables = charTimings,
StartMs = startMs,
EndMs = endMs,
OriginalText = text
});
}
else
{
destList.Add(new LyricsLine
{
StartMs = startMs,
EndMs = endMs,
OriginalText = ""
});
}
}

View File

@@ -30,7 +30,7 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
}
public List<LyricsData> Parse(LyricsSearchResult? lyricsSearchResult)
public List<LyricsData> Parse(LyricsCacheItem? lyricsSearchResult)
{
_logger.LogInformation("LyricsParser.Parse");
_lyricsDataArr = [];
@@ -75,7 +75,7 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
ITranslationService translationService,
ITransliterationService transliterationService,
TranslationSettings settings,
LyricsSearchResult? lyricsSearchResult,
LyricsCacheItem? lyricsSearchResult,
CancellationToken token
)
{
@@ -180,7 +180,7 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
return (main, transliterationSearchProvider, translationSearchProvider);
}
private void LoadTranslation(LyricsSearchResult? lyricsSearchResult)
private void LoadTranslation(LyricsCacheItem? lyricsSearchResult)
{
if (!string.IsNullOrWhiteSpace(lyricsSearchResult?.Translation))
{
@@ -197,7 +197,7 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
}
}
private void LoadTransliteration(LyricsSearchResult? lyricsSearchResult)
private void LoadTransliteration(LyricsCacheItem? lyricsSearchResult)
{
if (!string.IsNullOrWhiteSpace(lyricsSearchResult?.Transliteration))
{

View File

@@ -111,14 +111,14 @@ namespace BetterLyrics.WinUI3.Providers
return null;
}
public async Task<LyricsSearchResult> SearchSongInfoAsync(Models.SongInfo songInfo)
public async Task<LyricsCacheItem> SearchSongInfoAsync(Models.SongInfo songInfo)
{
LyricsSearchResult lyricsSearchResult = new()
LyricsCacheItem lyricsSearchResult = new()
{
Provider = Enums.LyricsSearchProvider.AppleMusic
};
var query = $"{songInfo.DisplayArtists} {songInfo.Title}";
var query = $"{songInfo.Artist} {songInfo.Title}";
var apiUrl = $"https://amp-api.music.apple.com/v1/catalog/{_storefront}/search";
var url = apiUrl + $"?term={WebUtility.UrlEncode(query)}&types=songs&limit=1&l={_language}";
var resp = await _client.GetStringAsync(url);
@@ -133,7 +133,7 @@ namespace BetterLyrics.WinUI3.Providers
var attr = song.GetProperty("attributes");
lyricsSearchResult.Title = attr.GetProperty("name").ToString();
lyricsSearchResult.Artists = attr.GetProperty("artistName").ToString().SplitByCommonSplitter();
lyricsSearchResult.Artist = attr.GetProperty("artistName").ToString();
lyricsSearchResult.Album = attr.GetProperty("albumName").ToString();
lyricsSearchResult.Duration = attr.GetProperty("durationInMillis").GetInt32() / 1000.0;

View File

@@ -41,6 +41,7 @@ namespace BetterLyrics.WinUI3.Renderer
Color strokeColor,
Color bgColor,
Color fgColor,
double currentProgressMs,
Func<int, LinePlaybackState> getPlaybackState)
{
using (var opacityLayer = ds.CreateLayer((float)lyricsOpacity))
@@ -70,6 +71,7 @@ namespace BetterLyrics.WinUI3.Renderer
strokeColor,
bgColor,
fgColor,
currentProgressMs,
getPlaybackState);
}
@@ -101,6 +103,7 @@ namespace BetterLyrics.WinUI3.Renderer
strokeColor,
bgColor,
fgColor,
currentProgressMs,
getPlaybackState);
}
}
@@ -125,6 +128,7 @@ namespace BetterLyrics.WinUI3.Renderer
Color strokeColor,
Color bgColor,
Color fgColor,
double currentProgressMs,
Func<int, LinePlaybackState> getPlaybackState)
{
if (lines == null) return;
@@ -162,10 +166,12 @@ namespace BetterLyrics.WinUI3.Renderer
using (var textOnlyLayer = RenderBaseTextLayer(control, line, styleSettings.LyricsFontStrokeWidth, strokeColor, line.ColorTransition.Value))
{
if (i == playingLineIndex)
bool isPlaying = currentProgressMs >= line.StartMs && currentProgressMs <= line.EndMs;
if (i == playingLineIndex) isPlaying = true;
if (isPlaying)
{
var state = getPlaybackState(i);
_playingRenderer.Draw(control, ds, textOnlyLayer, line, state, bgColor, fgColor, effectSettings);
}
else

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

@@ -2,6 +2,7 @@
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Text;
using System;
using System.Numerics;
using Windows.Foundation;
@@ -28,15 +29,7 @@ namespace BetterLyrics.WinUI3.Renderer
if (line.OriginalCanvasTextLayout != null)
{
double opacity;
if (line.PlayedOriginalOpacityTransition.StartValue > line.UnplayedOriginalOpacityTransition.StartValue)
{
opacity = line.PlayedOriginalOpacityTransition.Value;
}
else
{
opacity = line.UnplayedOriginalOpacityTransition.Value;
}
double opacity = Math.Max(line.PlayedOriginalOpacityTransition.Value, line.UnplayedOriginalOpacityTransition.Value);
DrawPart(ds, textOnlyLayer,
line.OriginalCanvasTextLayout,
line.OriginalPosition,

View File

@@ -12,7 +12,7 @@ namespace BetterLyrics.WinUI3.Serialization
[JsonSerializable(typeof(CutletDockerResponse))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(AppSettings))]
[JsonSerializable(typeof(LyricsSearchResult))]
[JsonSerializable(typeof(LyricsCacheItem))]
[JsonSourceGenerationOptions(WriteIndented = true)]
internal partial class SourceGenerationContext : JsonSerializerContext { }
}

View File

@@ -97,11 +97,11 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
var ext = Path.GetExtension(item.FileName).ToLower();
if (!FileHelper.MusicExtensions.Contains(ext)) continue;
bool isMetadataMatch = (item.Title == songInfo.Title && item.Artists == songInfo.DisplayArtists);
bool isMetadataMatch = (item.Title == songInfo.Title && item.Artist == songInfo.Artist);
bool isFilenameMatch = StringHelper.IsSwitchableNormalizedMatch(
Path.GetFileNameWithoutExtension(item.FileName),
songInfo.DisplayArtists,
songInfo.Artist,
songInfo.Title
);
@@ -138,7 +138,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
try
{
string format = ".jpg";
var cachedAlbumArt = FileHelper.ReadAlbumArtCache(songInfo.DisplayArtists, songInfo.Album, format, PathHelper.iTunesAlbumArtCacheDirectory);
var cachedAlbumArt = FileHelper.ReadAlbumArtCache(songInfo.Artist, songInfo.Album, format, PathHelper.iTunesAlbumArtCacheDirectory);
if (cachedAlbumArt != null)
{
@@ -146,7 +146,7 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService
}
// Build the iTunes API URL
string url = $"{Constants.iTunes.QueryPrefix}term=" + WebUtility.UrlEncode($"{songInfo.Artists} {songInfo.Album}").Replace("%20", "+") + "&country=" + countryCode + "&entity=album&media=music&limit=1";
string url = $"{Constants.iTunes.QueryPrefix}term=" + WebUtility.UrlEncode($"{songInfo.Artist} {songInfo.Album}").Replace("%20", "+") + "&country=" + countryCode + "&entity=album&media=music&limit=1";
// Make a request to the API
using HttpResponseMessage response = await _iTunesHttpClinet.GetAsync(url);

View File

@@ -33,7 +33,7 @@ namespace BetterLyrics.WinUI3.Services.DiscordService
SmallImageKey = "logo"
},
Details = songInfo.Title,
State = songInfo.DisplayArtists,
State = songInfo.Artist,
Timestamps = Timestamps.FromTimeSpan(songInfo.Duration)
});
}

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();
// 当前正在执行的扫描任务字典
@@ -192,7 +190,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
.Where(x => x.Id == entity.Id) // 优先用 Id
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Title, entity.Title)
.SetProperty(p => p.Artists, entity.Artists)
.SetProperty(p => p.Artist, entity.Artist)
.SetProperty(p => p.Album, entity.Album)
.SetProperty(p => p.Year, entity.Year)
.SetProperty(p => p.Bitrate, entity.Bitrate)
@@ -387,7 +385,7 @@ namespace BetterLyrics.WinUI3.Services.FileSystemService
string? artPath = await SaveAlbumArtToDiskAsync(track);
item.Title = track.Title;
item.Artists = track.Artist;
item.Artist = track.Artist;
item.Album = track.Album;
item.Year = track.Year;
item.Bitrate = track.Bitrate;
@@ -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();
@@ -42,15 +42,13 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
_logger.LogInformation("RefreshArtAlbum");
if (CurrentSongInfo == null)
IBuffer? buffer = null;
if (CurrentSongInfo != SongInfoExtensions.Placeholder)
{
_logger.LogWarning("CurrentSongInfo == null");
return;
buffer = await Task.Run(async () => await _albumArtSearchService.SearchAsync(CurrentSongInfo, _SMTCAlbumArtBuffer, token), token);
if (token.IsCancellationRequested) return;
}
IBuffer? buffer = await Task.Run(async () => await _albumArtSearchService.SearchAsync(CurrentSongInfo, _SMTCAlbumArtBuffer, token), token);
if (token.IsCancellationRequested) return;
if (buffer == null)
{
using var placeHolderStream = await ImageHelper.GetAlbumArtPlaceholderAsync();

View File

@@ -3,19 +3,21 @@ using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Parsers.LyricsParser;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.WinUI;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
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();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsData? CurrentLyricsData { get; private set; }
[ObservableProperty] public partial LyricsSearchResult? CurrentLyricsSearchResult { get; private set; }
[ObservableProperty] public partial LyricsCacheItem? CurrentLyricsSearchResult { get; private set; }
private async Task RefreshLyricsAsync(CancellationToken token)
{
@@ -24,15 +26,10 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
CurrentLyricsSearchResult = null;
CurrentLyricsData = LyricsData.GetLoadingPlaceholder();
if (CurrentSongInfo != null)
if (CurrentSongInfo != SongInfoExtensions.Placeholder)
{
CurrentLyricsSearchResult = await Task.Run(async () => await _lyrcsSearchService.SearchSmartlyAsync(
CurrentSongInfo,
true,
CurrentMediaSourceProviderInfo?.LyricsSearchType,
token),
token);
if (token.IsCancellationRequested) return;
CurrentSongInfo, true, CurrentMediaSourceProviderInfo?.LyricsSearchType, token), token);
if (CurrentLyricsSearchResult != null)
{
@@ -40,14 +37,14 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
(CurrentLyricsData, CurrentLyricsSearchResult.TransliterationProvider, CurrentLyricsSearchResult.TranslationProvider) =
await Task.Run(async () => await lyricsParser.Parse(
_translationService,
_transliterationService,
_settingsService.AppSettings.TranslationSettings,
CurrentLyricsSearchResult,
token),
token);
_translationService, _transliterationService, _settingsService.AppSettings.TranslationSettings, CurrentLyricsSearchResult, token), token);
}
}
if (CurrentLyricsSearchResult == null)
{
CurrentLyricsData = LyricsData.GetNotfoundPlaceholder();
}
}
public async void UpdateLyrics()

View File

@@ -26,6 +26,7 @@ using CommunityToolkit.WinUI;
using EvtSource;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Diagnostics;
@@ -38,9 +39,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>>,
@@ -50,6 +51,8 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private readonly MediaManager _mediaManager = new();
private IBuffer? _SMTCAlbumArtBuffer = null;
private MediaManager.MediaSession? _currentDesiredSession = null;
private readonly IAlbumArtSearchService _albumArtSearchService;
private readonly ILyricsSearchService _lyrcsSearchService;
private readonly ITranslationService _translationService;
@@ -58,22 +61,25 @@ 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;
private readonly DispatcherQueueTimer? _onMediaPropsChangedTimer;
private readonly DispatcherTimer _scrobbleTimer;
private readonly Stopwatch _scrobbleStopwatch = new();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsScrobbled { get; set; } = false;
[ObservableProperty] public partial TimeSpan ScrobbledDuration { get; set; } = TimeSpan.Zero;
[ObservableProperty] public partial TimeSpan TargetScrobbledDuration { get; set; } = TimeSpan.Zero;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool CurrentIsPlaying { get; private set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial TimeSpan CurrentPosition { get; private set; } = TimeSpan.Zero;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial SongInfo? CurrentSongInfo { get; private set; } = SongInfoExtensions.Placeholder;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial SongInfo CurrentSongInfo { get; private set; } = SongInfoExtensions.Placeholder;
[ObservableProperty] public partial MediaSourceProviderInfo? CurrentMediaSourceProviderInfo { get; set; }
public MediaSessionsService(
public GSMTCService(
ISettingsService settingsService,
IAlbumArtSearchService albumArtSearchService,
ILyricsSearchService lyricsSearchService,
@@ -82,7 +88,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
ITransliterationService transliterationService,
IPlayHistoryService playHistoryService,
ILastFMService lastFMService,
ILogger<MediaSessionsService> logger)
ILogger<GSMTCService> logger)
{
_settingsService = settingsService;
_albumArtSearchService = albumArtSearchService;
@@ -94,26 +100,52 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_lastFMService = lastFMService;
_logger = logger;
_scrobbleTimer = new();
_scrobbleTimer.Interval = TimeSpan.FromSeconds(1);
_scrobbleTimer.Tick += ScrobbleTimer_Tick;
_onMediaPropsChangedTimer = _dispatcherQueue.CreateTimer();
_settingsService.AppSettings.MediaSourceProvidersInfo.ItemPropertyChanged += MediaSourceProvidersInfo_ItemPropertyChanged;
_settingsService.AppSettings.LocalMediaFolders.CollectionChanged += LocalMediaFolders_CollectionChanged;
_settingsService.AppSettings.MappedSongSearchQueries.CollectionChanged += MappedSongSearchQueries_CollectionChanged;
_settingsService.AppSettings.MappedSongSearchQueries.ItemPropertyChanged += MappedSongSearchQueries_ItemPropertyChanged;
InitMediaManager();
}
private void MappedSongSearchQueries_ItemPropertyChanged(object? sender, ItemPropertyChangedEventArgs e)
private void ScrobbleTimer_Tick(object? sender, object e)
{
UpdateLyrics();
}
if (!IsScrobbled)
{
if (!string.IsNullOrWhiteSpace(CurrentSongInfo.Title) && CurrentSongInfo.Title != "N/A")
{
ScrobbledDuration += _scrobbleTimer.Interval;
if (ScrobbledDuration >= TargetScrobbledDuration)
{
// 写入本地播放记录
var playHistoryItem = CurrentSongInfo.ToPlayHistoryItem(ScrobbledDuration.TotalMilliseconds);
if (playHistoryItem != null)
{
// 后台
_ = Task.Run(async () =>
{
await _playHistoryService.AddLogAsync(playHistoryItem);
});
_logger.LogInformation("ScrobbleTimer_Tick: {} scrobbled", CurrentSongInfo.Title);
}
// 写入 Last.fm 播放记录
var isLastFMEnabled = CurrentMediaSourceProviderInfo?.IsLastFMTrackEnabled ?? false;
if (isLastFMEnabled)
{
// 后台
_ = Task.Run(() => _lastFMService.TrackAsync(CurrentSongInfo));
}
private void MappedSongSearchQueries_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UpdateLyrics();
IsScrobbled = true;
ScrobbledDuration = TimeSpan.Zero;
}
}
}
}
private void LocalMediaFolders_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
@@ -143,10 +175,9 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
}
}
private MediaSourceProviderInfo? GetCurrentMediaSourceProviderInfo()
private MediaSourceProviderInfo? GetCurrentDesiredMediaSourceProviderInfo()
{
var desiredSession = GetCurrentSession();
return _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == desiredSession?.Id);
return _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == _currentDesiredSession?.Id);
}
private bool IsMediaSourceEnabled(string id)
@@ -176,6 +207,10 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private void InitMediaManager()
{
_mediaManager.Start();
_mediaManager.CurrentMediaSessions.ToList().ForEach(x => RecordMediaSession(x.Value.Id));
_mediaManager.OnAnySessionOpened += MediaManager_OnAnySessionOpened;
_mediaManager.OnAnySessionClosed += MediaManager_OnAnySessionClosed;
_mediaManager.OnFocusedSessionChanged += MediaManager_OnFocusedSessionChanged;
@@ -183,221 +218,157 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
_mediaManager.OnAnyPlaybackStateChanged += MediaManager_OnAnyPlaybackStateChanged;
_mediaManager.OnAnyTimelinePropertyChanged += MediaManager_OnAnyTimelinePropertyChanged;
_mediaManager.Start();
MediaManager_OnFocusedSessionChanged(null);
_mediaManager.CurrentMediaSessions.ToList().ForEach(x => RecordMediaSourceProviderInfo(x.Value));
OnDesiredSessionChanged(true);
}
private async void MediaManager_OnFocusedSessionChanged(MediaManager.MediaSession? mediaSession)
private void OnDesiredSessionChanged(bool firstTime = false)
{
if (!_mediaManager.IsStarted) return;
await SendFocusedMessagesAsync();
}
private void MediaManager_OnAnyTimelinePropertyChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionTimelineProperties? timelineProperties)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
var desiredSession = GetCurrentDesiredSession();
if (firstTime || desiredSession != _currentDesiredSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
_currentDesiredSession = desiredSession;
if (_currentDesiredSession == null)
{
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
return;
}
var desiredSession = GetCurrentSession();
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
{
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
SendNullMessages();
}
else
{
if (IsMediaSourceTimelineSyncEnabled(mediaSession.Id))
{
CurrentPosition = timelineProperties?.Position ?? TimeSpan.Zero;
CurrentSongInfo?.DurationMs = timelineProperties?.EndTime.TotalMilliseconds ?? 0;
}
_ = SendFocusedMessagesAsync();
}
}
}
private void MediaManager_OnFocusedSessionChanged(MediaManager.MediaSession? mediaSession)
{
OnDesiredSessionChanged();
}
private void MediaManager_OnAnyTimelinePropertyChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionTimelineProperties timelineProperties)
{
_dispatcherQueue.TryEnqueue(() =>
{
if (mediaSession != _currentDesiredSession) return;
CurrentPosition = timelineProperties.Position;
CurrentSongInfo.DurationMs = timelineProperties.EndTime.TotalMilliseconds;
UpdateTargetScrobbledDuration();
if (CurrentPosition.TotalSeconds == 0)
{
IsScrobbled = false;
ScrobbledDuration = TimeSpan.Zero;
}
});
}
private void MediaManager_OnAnyPlaybackStateChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionPlaybackInfo? playbackInfo)
private void MediaManager_OnAnyPlaybackStateChanged(MediaManager.MediaSession mediaSession, GlobalSystemMediaTransportControlsSessionPlaybackInfo playbackInfo)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, (() =>
_dispatcherQueue.TryEnqueue(() =>
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null)
{
CurrentIsPlaying = false;
return;
}
if (mediaSession != _currentDesiredSession) return;
var desiredSession = GetCurrentSession();
if (mediaSession != desiredSession) return;
if (!IsMediaSourceEnabled(mediaSession.Id))
CurrentIsPlaying = playbackInfo.PlaybackStatus switch
{
CurrentIsPlaying = false;
}
else
{
CurrentIsPlaying = playbackInfo?.PlaybackStatus switch
{
GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => true,
_ => false,
};
}
GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => true,
_ => false,
};
if (CurrentIsPlaying)
{
_scrobbleStopwatch.Start();
_scrobbleTimer.Start();
}
else
{
_scrobbleStopwatch.Stop();
_scrobbleTimer.Stop();
}
}));
});
}
private void MediaManager_OnAnyMediaPropertyChanged(MediaManager.MediaSession? mediaSession, GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProperties)
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)
if (mediaSession != _currentDesiredSession) return;
string sessionId = mediaSession.Id;
var currentMediaSourceProviderInfo = GetCurrentDesiredMediaSourceProviderInfo();
if (currentMediaSourceProviderInfo?.ResetPositionOffsetOnSongChanged == true)
{
CurrentSongInfo = SongInfoExtensions.Placeholder;
currentMediaSourceProviderInfo?.PositionOffset = 0;
}
string? sessionId = mediaSession?.Id;
string fixedTitle = mediaProperties.Title;
string fixedArtist = mediaProperties.Artist;
string fixedAlbum = mediaProperties.AlbumTitle;
string? songId = null;
var desiredSession = GetCurrentSession();
if (mediaSession != desiredSession) return;
if (sessionId != null && !IsMediaSourceEnabled(sessionId))
if (PlayerIdHelper.IsAppleMusic(sessionId))
{
CurrentSongInfo = SongInfoExtensions.Placeholder;
fixedArtist = mediaProperties.Artist.Split(" — ").First();
fixedAlbum = mediaProperties.Artist.Split(" — ").Last();
fixedAlbum = fixedAlbum.Replace(" - Single", "");
fixedAlbum = fixedAlbum.Replace(" - EP", "");
}
else if (PlayerIdHelper.IsNeteaseFamily(sessionId))
{
songId = mediaProperties.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.NetEaseCloudMusicTrackID))?
.Replace(ExtendedGenreFiled.NetEaseCloudMusicTrackID, "");
}
else if (sessionId == PlayerId.QQMusic)
{
songId = mediaProperties.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.QQMusicTrackID))?
.Replace(ExtendedGenreFiled.QQMusicTrackID, "");
}
if (PlayerIdHelper.IsLXMusic(sessionId))
{
StopSSE();
}
var linkedFileName = mediaProperties.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.FileName))?
.Replace(ExtendedGenreFiled.FileName, "");
_SMTCAlbumArtBuffer = null;
CurrentSongInfo = new()
{
Title = fixedTitle,
Artist = fixedArtist,
Album = fixedAlbum,
DurationMs = mediaSession.ControlSession.GetTimelineProperties().EndTime.TotalMilliseconds,
PlayerId = sessionId,
SongId = songId,
LinkedFileName = linkedFileName,
StartedAt = DateTime.Now.ToBinary(),
};
UpdateTargetScrobbledDuration();
IsScrobbled = false;
ScrobbledDuration = TimeSpan.Zero;
if (PlayerIdHelper.IsLXMusic(sessionId))
{
StartSSE();
}
else
{
var currentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
if (currentMediaSourceProviderInfo?.ResetPositionOffsetOnSongChanged == true)
{
currentMediaSourceProviderInfo?.PositionOffset = 0;
}
StopSSE();
}
string? fixedArtist = mediaProperties?.Artist;
string? fixedAlbum = mediaProperties?.AlbumTitle;
string? songId = null;
if (PlayerIdHelper.IsAppleMusic(sessionId))
{
fixedArtist = mediaProperties?.Artist.Split(" — ").FirstOrDefault();
fixedAlbum = mediaProperties?.Artist.Split(" — ").LastOrDefault();
fixedAlbum = fixedAlbum?.Replace(" - Single", "");
fixedAlbum = fixedAlbum?.Replace(" - EP", "");
}
else if (PlayerIdHelper.IsNeteaseFamily(sessionId))
{
songId = mediaProperties?.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.NetEaseCloudMusicTrackID))?
.Replace(ExtendedGenreFiled.NetEaseCloudMusicTrackID, "");
}
else if (sessionId == PlayerId.QQMusic)
{
songId = mediaProperties?.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.QQMusicTrackID))?
.Replace(ExtendedGenreFiled.QQMusicTrackID, "");
}
var linkedFileName = mediaProperties?.Genres
.FirstOrDefault(x => x.StartsWith(ExtendedGenreFiled.FileName))?
.Replace(ExtendedGenreFiled.FileName, "");
// 写入播放记录
if (CurrentSongInfo != null && CurrentSongInfo.Title != "N/A")
{
// 必须捕获一个副本给异步任务,因为 CurrentSongInfo 马上就要变了
var lastSong = CurrentSongInfo;
// 当前秒表时间 >= 上一首总时长 / 2
if (lastSong.DurationMs > 0 &&
_scrobbleStopwatch.Elapsed.TotalMilliseconds >= (lastSong.DurationMs / 2))
{
// 写入本地播放记录
var playHistoryItem = lastSong.ToPlayHistoryItem(_scrobbleStopwatch.Elapsed.TotalMilliseconds);
if (playHistoryItem != null)
{
// 后台
_ = Task.Run(() => _playHistoryService.AddLogAsync(playHistoryItem));
_logger.LogInformation($"[Scrobble] 结算成功: {lastSong.Title}");
}
// 写入 Last.fm 播放记录
var isLastFMEnabled = CurrentMediaSourceProviderInfo?.IsLastFMTrackEnabled ?? false;
if (isLastFMEnabled)
{
// 后台
_ = Task.Run(() => _lastFMService.TrackAsync(lastSong));
}
}
}
_scrobbleStopwatch.Restart();
CurrentSongInfo = new SongInfo
{
Title = mediaProperties?.Title ?? "N/A",
Artists = fixedArtist?.SplitByCommonSplitter() ?? ["N/A"],
Album = fixedAlbum ?? "N/A",
DurationMs = mediaSession?.ControlSession?.GetTimelineProperties().EndTime.TotalMilliseconds ?? 0,
PlayerId = sessionId,
SongId = songId,
LinkedFileName = linkedFileName
};
if (PlayerIdHelper.IsLXMusic(sessionId))
{
StartSSE();
}
else
{
StopSSE();
}
if (PlayerIdHelper.IsLXMusic(sessionId) && _lxMusicAlbumArtBytes != null)
{
_SMTCAlbumArtBuffer = _lxMusicAlbumArtBytes.AsBuffer();
}
else if (mediaProperties?.Thumbnail is IRandomAccessStreamReference streamReference)
{
_SMTCAlbumArtBuffer = await ImageHelper.ToBufferAsync(streamReference);
}
else
{
_SMTCAlbumArtBuffer = null;
}
if (PlayerIdHelper.IsLXMusic(sessionId) && _lxMusicAlbumArtBytes != null)
{
_SMTCAlbumArtBuffer = _lxMusicAlbumArtBytes.AsBuffer();
}
else if (mediaProperties.Thumbnail is IRandomAccessStreamReference streamReference)
{
_SMTCAlbumArtBuffer = await ImageHelper.ToBufferAsync(streamReference);
}
else
{
_SMTCAlbumArtBuffer = null;
}
_logger.LogInformation("MediaManager_OnAnyMediaPropertyChanged {SongInfo}", CurrentSongInfo);
CurrentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
CurrentMediaSourceProviderInfo = GetCurrentDesiredMediaSourceProviderInfo();
UpdateAlbumArt();
UpdateLyrics();
@@ -410,81 +381,65 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
private void MediaManager_OnAnySessionClosed(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
if (_mediaManager.CurrentMediaSessions.Count == 0)
{
SendNullMessages();
}
OnDesiredSessionChanged();
}
private void MediaManager_OnAnySessionOpened(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
RecordMediaSourceProviderInfo(mediaSession);
SendFocusedMessagesAsync().ConfigureAwait(false);
var id = mediaSession.Id;
_dispatcherQueue.TryEnqueue(() =>
{
RecordMediaSession(id);
OnDesiredSessionChanged();
});
}
private MediaManager.MediaSession? GetCurrentSession()
private void RecordMediaSession(string id)
{
var found = _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == id);
if (found == null)
{
_settingsService.AppSettings.MediaSourceProvidersInfo.Add(new MediaSourceProviderInfo(id, _settingsService.AppSettings.GeneralSettings.ListenOnNewPlaybackSource));
}
}
private MediaManager.MediaSession? GetCurrentDesiredSession()
{
var focusedSession = _mediaManager.GetFocusedSession();
if (focusedSession == null)
{
return null;
}
if (IsMediaSourceEnabled(focusedSession.Id))
if (focusedSession != null && IsMediaSourceEnabled(focusedSession.Id))
{
return focusedSession;
}
else
foreach (var session in _mediaManager.CurrentMediaSessions.Values)
{
foreach (var session in _mediaManager.CurrentMediaSessions.Values)
if (IsMediaSourceEnabled(session.Id))
{
if (IsMediaSourceEnabled(session.Id))
{
return session;
}
return session;
}
}
return null;
}
private void RecordMediaSourceProviderInfo(MediaManager.MediaSession mediaSession)
{
if (!_mediaManager.IsStarted) return;
if (mediaSession == null) return;
var id = mediaSession?.Id;
if (string.IsNullOrEmpty(id)) return;
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
var found = _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == id);
if (found == null)
{
_settingsService.AppSettings.MediaSourceProvidersInfo.Add(new MediaSourceProviderInfo(id, _settingsService.AppSettings.GeneralSettings.ListenOnNewPlaybackSource));
}
});
}
private void SendNullMessages()
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, (() =>
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
CurrentSongInfo = SongInfoExtensions.Placeholder;
CurrentIsPlaying = false;
CurrentMediaSourceProviderInfo = GetCurrentMediaSourceProviderInfo();
_scrobbleStopwatch.Reset();
CurrentPosition = TimeSpan.Zero;
UpdateAlbumArt();
UpdateLyrics();
_scrobbleTimer.Stop();
_discordService.Disable();
UpdateCurrentMediaSourceProviderInfoPositionOffset();
}));
});
}
private void UpdateCurrentMediaSourceProviderInfoPositionOffset()
@@ -508,21 +463,39 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
}
}
private void UpdateTargetScrobbledDuration()
{
TargetScrobbledDuration = TimeSpan.FromSeconds(CurrentSongInfo.Duration == 0 ? 30 : CurrentSongInfo.Duration / 2);
}
private async Task SendFocusedMessagesAsync()
{
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps = null;
var desiredSession = GetCurrentSession();
if (_currentDesiredSession == null)
{
SendNullMessages();
return;
}
try
{
mediaProps = await desiredSession?.ControlSession?.TryGetMediaPropertiesAsync();
}
catch (Exception) { }
var mediaProps = await _currentDesiredSession.ControlSession?.TryGetMediaPropertiesAsync();
var timelineProps = _currentDesiredSession.ControlSession?.GetTimelineProperties();
var playbackInfo = _currentDesiredSession.ControlSession?.GetPlaybackInfo();
MediaManager_OnAnyTimelinePropertyChanged(desiredSession, desiredSession?.ControlSession?.GetTimelineProperties());
MediaManager_OnAnyMediaPropertyChanged(desiredSession, mediaProps);
MediaManager_OnAnyPlaybackStateChanged(desiredSession, desiredSession?.ControlSession?.GetPlaybackInfo());
if (mediaProps == null || timelineProps == null || playbackInfo == null)
{
SendNullMessages();
return;
}
MediaManager_OnAnyTimelinePropertyChanged(_currentDesiredSession, timelineProps);
MediaManager_OnAnyMediaPropertyChanged(_currentDesiredSession, mediaProps);
MediaManager_OnAnyPlaybackStateChanged(_currentDesiredSession, playbackInfo);
}
catch (Exception)
{
SendNullMessages();
}
}
private void StartSSE()
@@ -573,7 +546,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
if (PlayerIdHelper.IsLXMusic(CurrentSongInfo?.PlayerId))
if (PlayerIdHelper.IsLXMusic(CurrentSongInfo.PlayerId))
{
var data = JsonSerializer.Deserialize(e.Message, Serialization.SourceGenerationContext.Default.JsonElement);
if (data.ValueKind == JsonValueKind.Number)
@@ -584,11 +557,11 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
}
else if (e.Event == "duration")
{
CurrentSongInfo?.DurationMs = data.GetDouble() * 1000;
CurrentSongInfo.DurationMs = data.GetDouble() * 1000;
UpdateDiscordPresence();
}
if (IsMediaSourceTimelineSyncEnabled(CurrentSongInfo?.PlayerId))
if (IsMediaSourceTimelineSyncEnabled(CurrentSongInfo.PlayerId))
{
CurrentPosition = TimeSpan.FromSeconds(_lxMusicPositionSeconds);
}
@@ -620,47 +593,27 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
public async Task PlayAsync()
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TryPlayAsync();
}
await _currentDesiredSession?.ControlSession?.TryPlayAsync();
}
public async Task PauseAsync()
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TryPauseAsync();
}
await _currentDesiredSession?.ControlSession?.TryPauseAsync();
}
public async Task PreviousAsync()
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TrySkipPreviousAsync();
}
await _currentDesiredSession?.ControlSession?.TrySkipPreviousAsync();
}
public async Task NextAsync()
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TrySkipNextAsync();
}
await _currentDesiredSession?.ControlSession?.TrySkipNextAsync();
}
public async Task ChangePosition(double seconds)
{
var desiredSession = GetCurrentSession();
if (desiredSession != null)
{
await desiredSession.ControlSession?.TryChangePlaybackPositionAsync(TimeSpan.FromSeconds(seconds).Ticks);
}
await _currentDesiredSession?.ControlSession?.TryChangePlaybackPositionAsync(TimeSpan.FromSeconds(seconds).Ticks);
}
public async Task ChangeLyricsLine(int index)
@@ -683,7 +636,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
if (WindowHook.GetWindowHandle<NowPlayingWindow>() is IntPtr hwnd)
{
TaskbarList.SetProgressValue(hwnd, (ulong)value.TotalSeconds, (ulong)(CurrentSongInfo?.Duration ?? value.TotalSeconds));
TaskbarList.SetProgressValue(hwnd, (ulong)value.TotalSeconds, (ulong)(CurrentSongInfo.Duration));
}
}
@@ -693,7 +646,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
if (message.PropertyName == nameof(MediaSourceProviderInfo.IsEnabled))
{
MediaManager_OnFocusedSessionChanged(null);
OnDesiredSessionChanged();
}
}
else if (message.Sender is TranslationSettings)
@@ -723,7 +676,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
{
if (message.PropertyName == nameof(MusicGallerySettings.LyricsWindowStatus.IsOpened))
{
MediaManager_OnFocusedSessionChanged(null);
OnDesiredSessionChanged();
}
}
else if (message.Sender is MediaFolder)

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();
@@ -23,8 +26,12 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
MediaSourceProviderInfo? CurrentMediaSourceProviderInfo { get; }
bool IsScrobbled { get; }
TimeSpan ScrobbledDuration { get; }
TimeSpan TargetScrobbledDuration { get; }
bool CurrentIsPlaying { get; }
SongInfo? CurrentSongInfo { get; }
SongInfo CurrentSongInfo { get; }
TimeSpan CurrentPosition { get; }
LyricsData? CurrentLyricsData { get; }
@@ -33,6 +40,6 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService
AlbumArtThemeColors CalculateAlbumArtThemeColors(LyricsWindowStatus lyricsWindowStatus, Color backdropAccentColor);
LyricsSearchResult? CurrentLyricsSearchResult { get; }
LyricsCacheItem? CurrentLyricsSearchResult { get; }
}
}

View File

@@ -133,7 +133,7 @@ namespace BetterLyrics.WinUI3.Services.LastFMService
await _client.Track.ScrobbleAsync(new Hqub.Lastfm.Entities.Scrobble
{
Track = songInfo.Title,
Artist = songInfo.DisplayArtists,
Artist = songInfo.Artist,
Date = DateTime.Now,
});
}

View File

@@ -0,0 +1,15 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.LyricsCacheService
{
public interface ILyricsCacheService
{
Task<LyricsCacheItem?> GetLyricsAsync(SongInfo songInfo, LyricsSearchProvider provider);
Task SaveLyricsAsync(SongInfo songInfo, LyricsCacheItem result);
}
}

View File

@@ -0,0 +1,66 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Db;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.LyricsCacheService
{
public class LyricsCacheService : ILyricsCacheService
{
private readonly IDbContextFactory<LyricsCacheDbContext> _contextFactory;
public LyricsCacheService(IDbContextFactory<LyricsCacheDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
/// <summary>
/// Read cache from DB
/// </summary>
public async Task<LyricsCacheItem?> GetLyricsAsync(SongInfo songInfo, LyricsSearchProvider provider)
{
using var context = await _contextFactory.CreateDbContextAsync();
string key = songInfo.GetCacheKey();
var existingItem = await context.LyricsCache
.FirstOrDefaultAsync(x => x.CacheKey == key && x.Provider == provider);
return existingItem;
}
/// <summary>
/// Write cache to DB
/// </summary>
public async Task SaveLyricsAsync(SongInfo songInfo, LyricsCacheItem result)
{
using var context = await _contextFactory.CreateDbContextAsync();
string key = songInfo.GetCacheKey();
var existingItem = await context.LyricsCache
.FirstOrDefaultAsync(x => x.CacheKey == key && x.Provider == result.Provider);
if (existingItem == null)
{
var newItem = (LyricsCacheItem)result.Clone();
newItem.CacheKey = key;
await context.LyricsCache.AddAsync(newItem);
}
else
{
// No need to handle this case
return;
}
await context.SaveChangesAsync();
}
}
}

View File

@@ -10,8 +10,8 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
public interface ILyricsSearchService
{
Task<LyricsSearchResult?> SearchSmartlyAsync(SongInfo songInfo, bool checkCache, LyricsSearchType? lyricsSearchType, CancellationToken token);
Task<LyricsCacheItem?> SearchSmartlyAsync(SongInfo songInfo, bool checkCache, LyricsSearchType? lyricsSearchType, CancellationToken token);
Task<List<LyricsSearchResult>> SearchAllAsync(SongInfo songInfo, bool checkCache, CancellationToken token);
Task<List<LyricsCacheItem>> SearchAllAsync(SongInfo songInfo, bool checkCache, CancellationToken token);
}
}

View File

@@ -6,7 +6,9 @@ using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Providers;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.LyricsCacheService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SongSearchMapService;
using Lyricify.Lyrics.Helpers;
using Lyricify.Lyrics.Searchers;
using Microsoft.Extensions.Logging;
@@ -30,12 +32,22 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
private readonly ISettingsService _settingsService;
private readonly IFileSystemService _fileSystemService;
private readonly ILyricsCacheService _lyricsCacheService;
private readonly ISongSearchMapService _songSearchMapService;
private readonly ILogger _logger;
public LyricsSearchService(ISettingsService settingsService, IFileSystemService fileSystemService, ILogger<LyricsSearchService> logger)
public LyricsSearchService(
ISettingsService settingsService,
IFileSystemService fileSystemService,
ILyricsCacheService lyricsCacheService,
ISongSearchMapService songSearchMapService,
ILogger<LyricsSearchService> logger
)
{
_settingsService = settingsService;
_fileSystemService = fileSystemService;
_lyricsCacheService = lyricsCacheService;
_songSearchMapService = songSearchMapService;
_logger = logger;
_lrcLibHttpClient = new();
@@ -91,32 +103,30 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
}
public async Task<LyricsSearchResult?> SearchSmartlyAsync(SongInfo songInfo, bool checkCache, LyricsSearchType? lyricsSearchType, CancellationToken token)
public async Task<LyricsCacheItem?> SearchSmartlyAsync(SongInfo songInfo, bool checkCache, LyricsSearchType? lyricsSearchType, CancellationToken token)
{
if (lyricsSearchType == null)
{
return null;
}
var lyricsSearchResult = new LyricsSearchResult();
var lyricsSearchResult = new LyricsCacheItem();
//lyricsSearchResult.Raw = File.ReadAllText("C:\\Users\\Zhe\\Desktop\\星河回响 (Tech Demo).lrc");
//return lyricsSearchResult;
string overridenTitle = songInfo.Title;
string[] overridenArtists = songInfo.Artists;
string overridenArtist = songInfo.Artist;
string overridenAlbum = songInfo.Album;
_logger.LogInformation("SearchSmartlyAsync {SongInfo}", songInfo);
// 先检查该曲目是否已被用户映射
var found = _settingsService.AppSettings.MappedSongSearchQueries
.FirstOrDefault(x =>
x.OriginalTitle == overridenTitle &&
x.OriginalArtist == overridenArtists.Join(ATL.Settings.DisplayValueSeparator.ToString()) &&
x.OriginalAlbum == overridenAlbum);
var found = await _songSearchMapService.GetMappingAsync(overridenTitle, overridenArtist, overridenAlbum);
if (found != null)
{
overridenTitle = found.MappedTitle;
overridenArtists = found.MappedArtist.Split(ATL.Settings.DisplayValueSeparator);
overridenArtist = found.MappedArtist;
overridenAlbum = found.MappedAlbum;
_logger.LogInformation("Found mapped song search query: {MappedSongSearchQuery}", found);
@@ -125,7 +135,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (pureMusic)
{
lyricsSearchResult.Title = overridenTitle;
lyricsSearchResult.Artists = overridenArtists;
lyricsSearchResult.Artist = overridenArtist;
lyricsSearchResult.Album = overridenAlbum;
lyricsSearchResult.Raw = "[99:00.000]🎶🎶🎶";
return lyricsSearchResult;
@@ -137,13 +147,13 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return await SearchSingleAsync(
((SongInfo)songInfo.Clone())
.WithTitle(overridenTitle)
.WithArtist(overridenArtists)
.WithArtist(overridenArtist)
.WithAlbum(overridenAlbum),
targetProvider.Value, checkCache, token);
}
}
List<LyricsSearchResult> lyricsSearchResults = [];
List<LyricsCacheItem> lyricsSearchResults = [];
var mediaSourceProviderInfo = _settingsService.AppSettings.MediaSourceProvidersInfo.FirstOrDefault(x => x.Provider == songInfo.PlayerId);
if (mediaSourceProviderInfo != null)
@@ -159,7 +169,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
lyricsSearchResult = await SearchSingleAsync(
((SongInfo)songInfo.Clone())
.WithTitle(overridenTitle)
.WithArtist(overridenArtists)
.WithArtist(overridenArtist)
.WithAlbum(overridenAlbum),
provider.Provider, checkCache, token);
@@ -176,7 +186,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
case LyricsSearchType.Sequential:
return lyricsSearchResult;
case LyricsSearchType.BestMatch:
lyricsSearchResults.Add((LyricsSearchResult)lyricsSearchResult.Clone());
lyricsSearchResults.Add((LyricsCacheItem)lyricsSearchResult.Clone());
break;
default:
break;
@@ -193,10 +203,10 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
};
}
public async Task<List<LyricsSearchResult>> SearchAllAsync(SongInfo songInfo, bool checkCache, CancellationToken token)
public async Task<List<LyricsCacheItem>> SearchAllAsync(SongInfo songInfo, bool checkCache, CancellationToken token)
{
_logger.LogInformation("SearchAllAsync {SongInfo}", songInfo);
var results = new List<LyricsSearchResult>();
var results = new List<LyricsCacheItem>();
foreach (var provider in Enum.GetValues<LyricsSearchProvider>())
{
var searchResult = await SearchSingleAsync(songInfo, provider, checkCache, token);
@@ -205,9 +215,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return results;
}
private async Task<LyricsSearchResult> SearchSingleAsync(SongInfo songInfo, LyricsSearchProvider provider, bool checkCache, CancellationToken token)
private async Task<LyricsCacheItem> SearchSingleAsync(SongInfo songInfo, LyricsSearchProvider provider, bool checkCache, CancellationToken token)
{
var lyricsSearchResult = new LyricsSearchResult
var lyricsSearchResult = new LyricsCacheItem
{
Provider = provider,
};
@@ -217,7 +227,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
// Check cache first if allowed
if (checkCache && provider.IsRemote())
{
var cached = FileHelper.ReadLyricsCache(songInfo, provider);
var cached = await _lyricsCacheService.GetLyricsAsync(songInfo, provider);
if (cached != null)
{
lyricsSearchResult = cached;
@@ -269,20 +279,20 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (provider.IsRemote())
{
FileHelper.WriteLyricsCache(songInfo, lyricsSearchResult);
await _lyricsCacheService.SaveLyricsAsync(songInfo, lyricsSearchResult);
}
return lyricsSearchResult;
}
private async Task<LyricsSearchResult> SearchFile(SongInfo songInfo, LyricsFormat format)
private async Task<LyricsCacheItem> SearchFile(SongInfo songInfo, LyricsFormat format)
{
int maxScore = 0;
FilesIndexItem? bestFileEntity = null;
MediaFolder? bestFolderConfig = null;
var lyricsSearchResult = new LyricsSearchResult();
var lyricsSearchResult = new LyricsCacheItem();
if (format.ToLyricsSearchProvider() is LyricsSearchProvider lyricsSearchProvider)
{
lyricsSearchResult.Provider = lyricsSearchProvider;
@@ -305,7 +315,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
if (item.FileName.EndsWith(targetExt, StringComparison.OrdinalIgnoreCase))
{
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult { Reference = item.FileName });
int score = MetadataComparer.CalculateScore(songInfo, new LyricsCacheItem { Reference = item.FileName });
if (score > maxScore)
{
@@ -328,9 +338,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return lyricsSearchResult;
}
private async Task<LyricsSearchResult> SearchEmbedded(SongInfo songInfo)
private async Task<LyricsCacheItem> SearchEmbedded(SongInfo songInfo)
{
var lyricsSearchResult = new LyricsSearchResult
var lyricsSearchResult = new LyricsCacheItem
{
Provider = LyricsSearchProvider.LocalMusicFile,
};
@@ -352,10 +362,10 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
if (string.IsNullOrEmpty(item.EmbeddedLyrics)) continue;
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
int score = MetadataComparer.CalculateScore(songInfo, new LyricsCacheItem
{
Title = item.Title,
Artists = item.Artists?.Split(ATL.Settings.DisplayValueSeparator),
Artist = item.Artist,
Album = item.Album,
Duration = item.Duration
});
@@ -370,7 +380,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (bestFile != null && maxScore > 0)
{
lyricsSearchResult.Title = bestFile.Title;
lyricsSearchResult.Artists = bestFile.Artists?.Split(ATL.Settings.DisplayValueSeparator);
lyricsSearchResult.Artist = bestFile.Artist;
lyricsSearchResult.Album = bestFile.Album;
lyricsSearchResult.Duration = bestFile.Duration;
@@ -382,9 +392,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return lyricsSearchResult;
}
private async Task<LyricsSearchResult> SearchAmllTtmlDbAsync(SongInfo songInfo)
private async Task<LyricsCacheItem> SearchAmllTtmlDbAsync(SongInfo songInfo)
{
var lyricsSearchResult = new LyricsSearchResult
var lyricsSearchResult = new LyricsCacheItem
{
Provider = LyricsSearchProvider.AmllTtmlDb,
};
@@ -398,7 +408,6 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
}
int bestScore = 0;
string? rawLyricFile = null;
await foreach (var line in File.ReadLinesAsync(PathHelper.AmllTtmlDbIndexPath))
{
@@ -412,7 +421,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
continue;
string? title = null;
string[]? artists = null;
string? artist = null;
string? album = null;
foreach (var meta in metadataArr.EnumerateArray())
@@ -424,34 +433,32 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (key == "musicName" && valueArr.GetArrayLength() > 0)
title = valueArr[0].GetString();
if (key == "artists" && valueArr.GetArrayLength() > 0)
artists = valueArr.EnumerateArray().Select(x => x.GetString() ?? "").ToArray();
artist = string.Join(" / ", valueArr.EnumerateArray());
if (key == "album" && valueArr.GetArrayLength() > 0)
album = valueArr[0].GetString();
}
int score = MetadataComparer.CalculateScore(songInfo, new LyricsSearchResult
int score = MetadataComparer.CalculateScore(songInfo, new LyricsCacheItem
{
Title = title,
Artists = artists,
Artist = artist,
Album = album,
});
if (score > bestScore)
if (score > lyricsSearchResult.MatchPercentage)
{
if (root.TryGetProperty("rawLyricFile", out var rawLyricFileProp))
{
rawLyricFile = rawLyricFileProp.GetString();
lyricsSearchResult.Title = title;
lyricsSearchResult.Artists = artists;
lyricsSearchResult.Artist = artist;
lyricsSearchResult.Album = album;
bestScore = score;
lyricsSearchResult.MatchPercentage = score;
}
}
}
catch { }
}
lyricsSearchResult.MatchPercentage = MetadataComparer.CalculateScore(songInfo, lyricsSearchResult);
if (string.IsNullOrWhiteSpace(rawLyricFile))
{
return lyricsSearchResult;
@@ -478,9 +485,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return lyricsSearchResult;
}
private async Task<LyricsSearchResult> SearchLrcLibAsync(SongInfo songInfo)
private async Task<LyricsCacheItem> SearchLrcLibAsync(SongInfo songInfo)
{
var lyricsSearchResult = new LyricsSearchResult
var lyricsSearchResult = new LyricsCacheItem
{
Provider = LyricsSearchProvider.LrcLib,
};
@@ -489,7 +496,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
var url =
$"https://lrclib.net/api/search?" +
$"track_name={Uri.EscapeDataString(songInfo.Title)}&" +
$"artist_name={Uri.EscapeDataString(songInfo.DisplayArtists)}&" +
$"artist_name={Uri.EscapeDataString(songInfo.Artist)}&" +
$"&album_name={Uri.EscapeDataString(songInfo.Album)}" +
$"&durationMs={Uri.EscapeDataString(songInfo.DurationMs.ToString())}";
@@ -524,7 +531,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
lyricsSearchResult.Raw = original;
lyricsSearchResult.Title = searchedTitle;
lyricsSearchResult.Artists = searchedArtist?.SplitByCommonSplitter();
lyricsSearchResult.Artist = searchedArtist;
lyricsSearchResult.Album = searchedAlbum;
lyricsSearchResult.Duration = searchedDuration;
@@ -535,9 +542,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return lyricsSearchResult;
}
private static async Task<LyricsSearchResult> SearchQQNeteaseKugouAsync(SongInfo songInfo, Searchers searcher)
private static async Task<LyricsCacheItem> SearchQQNeteaseKugouAsync(SongInfo songInfo, Searchers searcher)
{
var lyricsSearchResult = new LyricsSearchResult();
var lyricsSearchResult = new LyricsCacheItem();
switch (searcher)
{
@@ -560,11 +567,11 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
if (songInfo.SongId != null && searcher == Searchers.Netease && PlayerIdHelper.IsNeteaseFamily(songInfo.PlayerId))
{
result = new NeteaseSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId);
result = new NeteaseSearchResult(songInfo.Title, [songInfo.Artist], songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId);
}
else if (songInfo.SongId != null && searcher == Searchers.QQMusic && songInfo.PlayerId == Constants.PlayerId.QQMusic)
{
result = new QQMusicSearchResult(songInfo.Title, songInfo.Artists, songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId, "");
result = new QQMusicSearchResult(songInfo.Title, [songInfo.Artist], songInfo.Album, [], (int)songInfo.DurationMs, songInfo.SongId, "");
}
else
{
@@ -572,8 +579,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
DurationMs = (int)songInfo.DurationMs,
Album = songInfo.Album,
AlbumArtists = songInfo.Artists.ToList(),
Artists = songInfo.Artists.ToList(),
Artist = songInfo.Artist,
Title = songInfo.Title,
}, searcher, Lyricify.Lyrics.Searchers.Helpers.CompareHelper.MatchType.NoMatch);
}
@@ -633,7 +639,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
}
lyricsSearchResult.Title = result?.Title;
lyricsSearchResult.Artists = result?.Artists;
lyricsSearchResult.Artist = string.Join(" / ", result?.Artists ?? []);
lyricsSearchResult.Album = result?.Album;
lyricsSearchResult.Duration = result?.DurationMs / 1000;
@@ -642,9 +648,9 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return lyricsSearchResult;
}
private async Task<LyricsSearchResult> SearchAppleMusicAsync(SongInfo songInfo)
private async Task<LyricsCacheItem> SearchAppleMusicAsync(SongInfo songInfo)
{
LyricsSearchResult lyricsSearchResult = new()
LyricsCacheItem lyricsSearchResult = new()
{
Provider = LyricsSearchProvider.AppleMusic
};

View File

@@ -2,6 +2,7 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Db;
using BetterLyrics.WinUI3.Models.Stats;
using BetterLyrics.WinUI3.ViewModels;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;

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

@@ -0,0 +1,15 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.SongSearchMapService
{
public interface ISongSearchMapService
{
Task SaveMappingAsync(MappedSongSearchQuery mapping);
Task<MappedSongSearchQuery?> GetMappingAsync(string title, string artist, string album);
Task DeleteMappingAsync(MappedSongSearchQuery mapping);
}
}

View File

@@ -0,0 +1,78 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Db;
using Microsoft.EntityFrameworkCore;
using System;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.SongSearchMapService
{
public class SongSearchMapService : ISongSearchMapService
{
private readonly IDbContextFactory<SongSearchMapDbContext> _contextFactory;
public SongSearchMapService(IDbContextFactory<SongSearchMapDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task SaveMappingAsync(MappedSongSearchQuery mapping)
{
using var context = await _contextFactory.CreateDbContextAsync();
var existing = await context.SongSearchMap
.FirstOrDefaultAsync(x =>
x.OriginalTitle == mapping.OriginalTitle &&
x.OriginalArtist == mapping.OriginalArtist &&
x.OriginalAlbum == mapping.OriginalAlbum);
if (existing != null)
{
existing.MappedTitle = mapping.MappedTitle;
existing.MappedArtist = mapping.MappedArtist;
existing.MappedAlbum = mapping.MappedAlbum;
existing.IsMarkedAsPureMusic = mapping.IsMarkedAsPureMusic;
existing.LyricsSearchProvider = mapping.LyricsSearchProvider;
context.SongSearchMap.Update(existing);
}
else
{
var newItem = (MappedSongSearchQuery)mapping.Clone();
await context.SongSearchMap.AddAsync(newItem);
}
await context.SaveChangesAsync();
}
public async Task<MappedSongSearchQuery?> GetMappingAsync(string title, string artist, string album)
{
using var context = await _contextFactory.CreateDbContextAsync();
return await context.SongSearchMap
.AsNoTracking()
.FirstOrDefaultAsync(x =>
x.OriginalTitle == title &&
x.OriginalArtist == artist &&
x.OriginalAlbum == album);
}
public async Task DeleteMappingAsync(MappedSongSearchQuery mapping)
{
using var context = await _contextFactory.CreateDbContextAsync();
var target = await context.SongSearchMap
.FirstOrDefaultAsync(x =>
x.OriginalTitle == mapping.OriginalTitle &&
x.OriginalArtist == mapping.OriginalArtist &&
x.OriginalAlbum == mapping.OriginalAlbum);
if (target != null)
{
context.SongSearchMap.Remove(target);
await context.SaveChangesAsync();
}
}
}
}

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>الأكثر نشاطاً</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>سكروبلينج...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>المصادر</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Aktivste</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Scrobbling...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Quellen</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Most Active</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Scrobbling...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sources</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Más activos</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Scrobbling...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Fuentes</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Les plus actifs</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Scrobbling...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sources d'information</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>सर्वाधिक सक्रिय</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>रिकॉर्डिंग...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>स्रोत</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Paling Aktif</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Menggelinding...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sumber</value>
</data>

View File

@@ -514,7 +514,7 @@
<value>タイトル</value>
</data>
<data name="MusicGalleryPageSortType.Text" xml:space="preserve">
<value>ソートタイプ</value>
<value>並べ替えタイプ</value>
</data>
<data name="MusicGalleryPageStopTrack.Text" xml:space="preserve">
<value>停止</value>
@@ -544,7 +544,7 @@
<value>狭い表示モード</value>
</data>
<data name="PictureInPictureMode" xml:space="preserve">
<value>ピクチャー イン ピクチャー モード</value>
<value>ピクチャーインピクチャーモード</value>
</data>
<data name="Pinyin" xml:space="preserve">
<value>ピンイン (中国語)</value>
@@ -775,7 +775,7 @@
<value>Discord Presence で視聴ステータスを表示</value>
</data>
<data name="SettingsPageDisplayTypeSwitcher.Header" xml:space="preserve">
<value>レイアウト</value>
<value>レイアウトモード</value>
</data>
<data name="SettingsPageDockedMode.Text" xml:space="preserve">
<value>ドッキングモード</value>
@@ -1159,7 +1159,7 @@
<value>テストミュージックを再生</value>
</data>
<data name="SettingsPageMultiNowPlayingWindows.Header" xml:space="preserve">
<value>マルチウィンドウ モード</value>
<value>マルチウィンドウモード</value>
</data>
<data name="SettingsPageMusicGallery.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>
@@ -1468,7 +1468,7 @@
<value>標準モード</value>
</data>
<data name="StatsDashboardControlActivityByHour.Text" xml:space="preserve">
<value>時間帯別アクティビティ</value>
<value>アクティブ時間帯</value>
</data>
<data name="StatsDashboardControlCustom.Content" xml:space="preserve">
<value>カスタム</value>
@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>最多アクティブ</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>再生記録中...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>再生ソース</value>
</data>
@@ -1504,16 +1507,16 @@
<value>期間</value>
</data>
<data name="StatsDashboardControlTimes" xml:space="preserve">
<value>タイムズ</value>
<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>
@@ -1522,10 +1525,10 @@
<value>総再生時間</value>
</data>
<data name="StatsDashboardControlTrackCountAxis.AxisName" xml:space="preserve">
<value>タイムズ</value>
<value></value>
</data>
<data name="StatsDashboardControlTrackCountText.Text" xml:space="preserve">
<value>タイムズ</value>
<value></value>
</data>
<data name="StatsDashboardControlTracksPlayed.Text" xml:space="preserve">
<value>再生された曲の数</value>
@@ -1555,7 +1558,7 @@
<value>歌詞ウィンドウスイッチャー</value>
</data>
<data name="TaskbarMode" xml:space="preserve">
<value>タスクバー モード</value>
<value>タスクバーモード</value>
</data>
<data name="TermsOfService.Content" xml:space="preserve">
<value>利用規約</value>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>가장 활동적인</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>스크러블...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>출처</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Paling Aktif</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Rakaman...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Sumber</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Mais activos</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>A fazer barulho...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Fontes</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Самые активные</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Скроблинг...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Источники</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>กิจกรรมล่าสุด</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>กำลังส่งข้อมูล...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>แหล่งข้อมูล</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>Hoạt động nhiều nhất</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>Đang ghi lại...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>Nguồn</value>
</data>

View File

@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>最活跃时段</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>记录中...</value>
</data>
<data name="StatsDashboardControlSources.Text" xml:space="preserve">
<value>播放源</value>
</data>

View File

@@ -193,7 +193,7 @@
<value>已偵測到根目錄路徑。全磁碟索引可能包含大量非媒體檔案,導致掃描時間過長。建議指定特定的子目錄。</value>
</data>
<data name="FileSystemServiceWaitingForScan" xml:space="preserve">
<value>等待掃描...</value>
<value>準備掃描...</value>
</data>
<data name="FullscreenMode" xml:space="preserve">
<value>全螢幕模式</value>
@@ -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>
@@ -1482,6 +1482,9 @@
<data name="StatsDashboardControlMostActive.Text" xml:space="preserve">
<value>最活躍</value>
</data>
<data name="StatsDashboardControlRecording.Title" xml:space="preserve">
<value>記錄中...</value>
</data>
<data name="StatsDashboardControlSources.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

@@ -92,14 +92,7 @@ namespace BetterLyrics.WinUI3.ViewModels
private void ClearCacheFiles()
{
DirectoryHelper.DeleteAllFiles(PathHelper.LogDirectory);
DirectoryHelper.DeleteAllFiles(PathHelper.LyricsCacheDirectory);
DirectoryHelper.DeleteAllFiles(PathHelper.AmllTtmlDbLyricsCacheDirectory);
DirectoryHelper.DeleteAllFiles(PathHelper.KugouLyricsCacheDirectory);
DirectoryHelper.DeleteAllFiles(PathHelper.LrcLibLyricsCacheDirectory);
DirectoryHelper.DeleteAllFiles(PathHelper.NeteaseLyricsCacheDirectory);
DirectoryHelper.DeleteAllFiles(PathHelper.QQLyricsCacheDirectory);
DirectoryHelper.DeleteAllFiles(PathHelper.iTunesAlbumArtCacheDirectory);
ToastHelper.ShowToast("ActionCompleted", null, InfoBarSeverity.Success);

View File

@@ -3,9 +3,10 @@ using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Parsers.LyricsParser;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SongSearchMapService;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
@@ -17,11 +18,12 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class LyricsSearchControlViewModel : BaseViewModel,
IRecipient<PropertyChangedMessage<SongInfo?>>
IRecipient<PropertyChangedMessage<SongInfo>>
{
private readonly ILyricsSearchService _lyricsSearchService;
private readonly IMediaSessionsService _mediaSessionsService;
private readonly IGSMTCService _gsmtcService;
private readonly ISettingsService _settingsService;
private readonly ISongSearchMapService _songSearchMapService;
private LatestOnlyTaskRunner _lyricsSearchRunner = new();
@@ -29,10 +31,10 @@ namespace BetterLyrics.WinUI3.ViewModels
public partial AppSettings AppSettings { get; set; }
[ObservableProperty]
public partial ObservableCollection<LyricsSearchResult> LyricsSearchResults { get; set; } = [];
public partial ObservableCollection<LyricsCacheItem> LyricsSearchResults { get; set; } = [];
[ObservableProperty]
public partial LyricsSearchResult? SelectedLyricsSearchResult { get; set; }
public partial LyricsCacheItem? SelectedLyricsSearchResult { get; set; }
[ObservableProperty]
public partial ObservableCollection<LyricsData>? LyricsDataArr { get; set; }
@@ -43,66 +45,60 @@ 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,
ISongSearchMapService songSearchMapService
)
{
_lyricsSearchService = lyricsSearchService;
_mediaSessionsService = mediaSessionsService;
_gsmtcService = gsmtcService;
_settingsService = settingsService;
_songSearchMapService = songSearchMapService;
AppSettings = _settingsService.AppSettings;
InitMappedSongSearchQuery();
_ = InitMappedSongSearchQueryAsync();
}
private void InitMappedSongSearchQuery()
private async Task InitMappedSongSearchQueryAsync()
{
LyricsSearchResults.Clear();
LyricsDataArr = null;
if (_mediaSessionsService.CurrentSongInfo != null)
if (_gsmtcService.CurrentSongInfo != null)
{
var found = GetMappedSongSearchQueryFromSettings();
var found = await _songSearchMapService.GetMappingAsync(
_gsmtcService.CurrentSongInfo.Title,
_gsmtcService.CurrentSongInfo.Artist,
_gsmtcService.CurrentSongInfo.Album);
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.Artist,
OriginalAlbum = _gsmtcService.CurrentSongInfo.Album,
MappedTitle = _gsmtcService.CurrentSongInfo.Title,
MappedArtist = _gsmtcService.CurrentSongInfo.Artist,
MappedAlbum = _gsmtcService.CurrentSongInfo.Album,
};
}
else
{
MappedSongSearchQuery = found.Clone();
MappedSongSearchQuery = (MappedSongSearchQuery)found.Clone();
}
}
}
private MappedSongSearchQuery? GetMappedSongSearchQueryFromSettings()
{
if (_mediaSessionsService.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);
return found;
}
public void PlayLyricsLine(LyricsLine? value)
{
if (value?.StartMs == null)
{
return;
}
_mediaSessionsService.ChangePosition(value.StartMs / 1000.0);
_gsmtcService.ChangePosition(value.StartMs / 1000.0);
}
[RelayCommand]
@@ -121,9 +117,9 @@ namespace BetterLyrics.WinUI3.ViewModels
LyricsSearchResults = [..await Task.Run(async () =>
{
var result = await _lyricsSearchService.SearchAllAsync(
((SongInfo?)_mediaSessionsService.CurrentSongInfo?.Clone() ?? new())
((SongInfo)_gsmtcService.CurrentSongInfo.Clone())
.WithTitle(MappedSongSearchQuery.MappedTitle)
.WithArtist(MappedSongSearchQuery.MappedArtist.SplitByCommonSplitter())
.WithArtist(MappedSongSearchQuery.MappedArtist)
.WithAlbum(MappedSongSearchQuery.MappedAlbum),
!_settingsService.AppSettings.GeneralSettings.IgnoreCacheWhenSearching,
token);
@@ -134,32 +130,27 @@ namespace BetterLyrics.WinUI3.ViewModels
}
[RelayCommand]
private void Save()
private async Task SaveAsync()
{
if (MappedSongSearchQuery == null)
{
return;
}
var existing = GetMappedSongSearchQueryFromSettings();
if (existing != null)
{
AppSettings.MappedSongSearchQueries.Remove(existing);
}
AppSettings.MappedSongSearchQueries.Add(MappedSongSearchQuery);
MappedSongSearchQuery = MappedSongSearchQuery.Clone();
await _songSearchMapService.SaveMappingAsync(MappedSongSearchQuery);
MappedSongSearchQuery = (MappedSongSearchQuery)MappedSongSearchQuery.Clone();
_gsmtcService.UpdateLyrics();
}
[RelayCommand]
private void Reset()
private async Task ResetAsync()
{
var existing = GetMappedSongSearchQueryFromSettings();
if (existing != null)
{
AppSettings.MappedSongSearchQueries.Remove(existing);
}
InitMappedSongSearchQuery();
if (MappedSongSearchQuery == null) return;
await _songSearchMapService.DeleteMappingAsync(MappedSongSearchQuery);
await InitMappedSongSearchQueryAsync();
SelectedLyricsSearchResult = null;
_gsmtcService.UpdateLyrics();
}
[RelayCommand]
@@ -180,7 +171,7 @@ namespace BetterLyrics.WinUI3.ViewModels
MappedSongSearchQuery?.MappedAlbum = MappedSongSearchQuery?.OriginalAlbum ?? string.Empty;
}
partial void OnSelectedLyricsSearchResultChanged(LyricsSearchResult? value)
partial void OnSelectedLyricsSearchResultChanged(LyricsCacheItem? value)
{
MappedSongSearchQuery?.LyricsSearchProvider = value?.Provider;
if (value?.Raw != null)
@@ -194,13 +185,13 @@ namespace BetterLyrics.WinUI3.ViewModels
}
}
public void Receive(PropertyChangedMessage<SongInfo?> message)
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();
_ = InitMappedSongSearchQueryAsync();
}
}
}

View File

@@ -9,6 +9,7 @@ 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;
@@ -43,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
@@ -60,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);
@@ -99,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)
@@ -147,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()
{
}
@@ -344,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
{
@@ -460,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();
@@ -641,7 +356,7 @@ namespace BetterLyrics.WinUI3.ViewModels
[RelayCommand]
private async Task StopTrackAsync()
{
await PlayTrackAtAsync(-1);
await SMTCService.PlayTrackAtAsync(-1);
}
[RelayCommand]
@@ -652,6 +367,15 @@ namespace BetterLyrics.WinUI3.ViewModels
settingsPageViewModel.NavViewSelectedItemTag = "MediaLib";
}
[RelayCommand]
private void ToggleSongListViewSelectionMode()
{
SongListViewSelectionMode =
SongListViewSelectionMode == ListViewSelectionMode.Single ?
ListViewSelectionMode.Multiple :
ListViewSelectionMode.Single;
}
public void Receive(PropertyChangedMessage<DateTime?> message)
{
if (message.Sender is MediaFolder)

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

@@ -3,16 +3,21 @@ using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Stats;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using CommunityToolkit.WinUI;
using LiveChartsCore;
using LiveChartsCore.Kernel;
using LiveChartsCore.Kernel.Sketches;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.Themes;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using SkiaSharp;
using SkiaSharp.Views.Windows;
@@ -25,7 +30,7 @@ using System.Xml.Linq;
namespace BetterLyrics.WinUI3.ViewModels
{
public partial class StatsDashboardControlViewModel : ObservableObject
public partial class StatsDashboardControlViewModel : BaseViewModel, IRecipient<PropertyChangedMessage<bool>>
{
private readonly IPlayHistoryService _playHistoryService;
private readonly ILocalizationService _localizationService;
@@ -33,13 +38,19 @@ namespace BetterLyrics.WinUI3.ViewModels
private string _localizedTimesValue;
[ObservableProperty] public partial bool IsLoading { get; set; }
private readonly DispatcherQueueTimer _timer;
[ObservableProperty] public partial IGSMTCService GSMTCService { get; set; }
[ObservableProperty] public partial bool IsLoading { get; set; } = false;
// 时间筛选
[ObservableProperty] public partial StatsRange SelectedTimeRange { get; set; }
[ObservableProperty] public partial bool IsCustomRangeSelected { get; set; }
[ObservableProperty] public partial DateTimeOffset CustomStartDate { get; set; }
[ObservableProperty] public partial DateTimeOffset CustomEndDate { get; set; } = DateTimeOffset.Now;
[ObservableProperty] public partial StatsRange SelectedTimeRange { get; set; } = StatsRange.Today;
[ObservableProperty] public partial bool IsCustomRangeSelected { get; set; } = false;
[ObservableProperty] public partial DateTimeOffset? CustomStartDate { get; set; } = DateTime.Now;
[ObservableProperty] public partial DateTimeOffset? CustomEndDate { get; set; } = DateTime.Now;
[ObservableProperty] public partial TimeSpan CustomStartTime { get; set; } = TimeSpan.Zero;
[ObservableProperty] public partial TimeSpan CustomEndTime { get; set; } = TimeSpan.Zero;
// 顶部基础数据
[ObservableProperty] public partial TimeSpan TotalDuration { get; set; }
@@ -54,8 +65,6 @@ namespace BetterLyrics.WinUI3.ViewModels
// 歌手
[ObservableProperty] public partial ObservableCollection<ArtistPlayCount> TopArtists { get; set; } = new();
[ObservableProperty] public partial ObservableCollection<int> ArtistSeriesValues { get; set; } = new();
public Func<ChartPoint, string> ArtistsLabelsFormatter { get; set; }
// 播放源
[ObservableProperty] public partial ObservableCollection<ISeries> SourceSeries { get; set; } = new();
@@ -63,35 +72,33 @@ namespace BetterLyrics.WinUI3.ViewModels
// 歌曲
[ObservableProperty] public partial ObservableCollection<SongPlayCount> TopSongs { get; set; } = new();
public StatsDashboardControlViewModel(IPlayHistoryService playHistoryService, ILocalizationService localizationService, IAlbumArtSearchService albumArtSearchService)
public StatsDashboardControlViewModel(
IPlayHistoryService playHistoryService,
ILocalizationService localizationService,
IAlbumArtSearchService albumArtSearchService,
IGSMTCService gsmtcService)
{
_playHistoryService = playHistoryService;
_localizationService = localizationService;
_albumArtSearchService = albumArtSearchService;
GSMTCService = gsmtcService;
_localizedTimesValue = _localizationService.GetLocalizedString("StatsDashboardControlTimes");
ArtistsLabelsFormatter = (point) =>
{
return TopArtists.ElementAtOrDefault(point.Index)?.Artist ?? "N/A";
};
_timer = _dispatcherQueue.CreateTimer();
SelectedTimeRange = StatsRange.Today;
CustomStartDate = DateTimeOffset.Now.AddDays(-7);
CustomEndDate = DateTimeOffset.Now;
UpdateDateRange();
}
async partial void OnSelectedTimeRangeChanged(StatsRange value)
partial void OnSelectedTimeRangeChanged(StatsRange value)
{
IsCustomRangeSelected = value == StatsRange.Custom;
if (!IsCustomRangeSelected)
{
await LoadDataAsync();
}
UpdateDateRange();
}
async partial void OnCustomEndDateChanged(DateTimeOffset value) => await LoadDataAsync();
async partial void OnCustomStartDateChanged(DateTimeOffset value) => await LoadDataAsync();
partial void OnCustomEndDateChanged(DateTimeOffset? value) => LoadData();
partial void OnCustomStartDateChanged(DateTimeOffset? value) => LoadData();
partial void OnCustomStartTimeChanged(TimeSpan value) => LoadData();
partial void OnCustomEndTimeChanged(TimeSpan value) => LoadData();
private void ProcessHourlyStats(List<PlayHistoryItem> logs)
{
@@ -117,16 +124,7 @@ namespace BetterLyrics.WinUI3.ViewModels
HourlySeriesValues = [.. hourCounts];
}
private void ProcessArtistStats(List<ArtistPlayCount> artists)
{
if (artists == null || !artists.Any())
{
ArtistSeriesValues = new();
return;
}
ArtistSeriesValues = [.. artists.Select(x => x.PlayCount)];
}
private void UpdatePlayerStats(List<PlayerStats> stats)
{
SourceSeries = new();
@@ -152,26 +150,39 @@ namespace BetterLyrics.WinUI3.ViewModels
})];
}
private (DateTime Start, DateTime End) CalculateDateRange()
private (DateTime? Start, DateTime? End) CalculateDateRange()
{
// 如果是自定义,直接返回 Picker 的值 (转为 UTC)
if (IsCustomRangeSelected)
{
return (CustomStartDate.UtcDateTime, CustomEndDate.UtcDateTime);
}
if (CustomStartDate == null || CustomEndDate == null) return (null, null);
return (
new DateTime(
DateOnly.FromDateTime(CustomStartDate.Value.LocalDateTime),
TimeOnly.FromTimeSpan(CustomStartTime),
DateTimeKind.Local)
.ToUniversalTime(),
new DateTime(
DateOnly.FromDateTime(CustomEndDate.Value.LocalDateTime),
TimeOnly.FromTimeSpan(CustomEndTime),
DateTimeKind.Local)
.ToUniversalTime()
);
}
private void UpdateDateRange()
{
DateTime nowLocal = DateTime.Now;
DateTime startLocal = nowLocal.Date;
switch (SelectedTimeRange)
{
case StatsRange.Today:
startLocal = nowLocal.Date.AddDays(-1);
return (startLocal.ToUniversalTime(), nowLocal.Date.ToUniversalTime());
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));
startLocal = new DateTime(startLocal.Year, startLocal.Month, startLocal.Day);
break;
case StatsRange.ThisMonth:
startLocal = new DateTime(nowLocal.Year, nowLocal.Month, 1);
@@ -185,57 +196,96 @@ namespace BetterLyrics.WinUI3.ViewModels
break;
}
return (startLocal.ToUniversalTime(), nowLocal.ToUniversalTime());
CustomStartDate = startLocal.Date;
CustomEndDate = nowLocal.Date;
CustomStartTime = startLocal.TimeOfDay;
CustomEndTime = nowLocal.TimeOfDay;
}
[RelayCommand]
public async Task LoadDataAsync()
private void RefreshData()
{
if (IsLoading) return;
IsLoading = true;
try
if (IsCustomRangeSelected)
{
var (start, end) = CalculateDateRange();
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);
await Task.WhenAll(durationTask, logsTask, topSongsTask, topArtistsTask, playersTask);
TotalDuration = await durationTask;
var logs = await logsTask;
TotalTracksPlayed = logs.Count;
TopSongs = [.. await topSongsTask];
var pStats = await playersTask;
UpdatePlayerStats(pStats);
TopArtists = [.. await topArtistsTask];
ProcessArtistStats(TopArtists.ToList());
ProcessHourlyStats(logs);
LoadData();
}
catch (Exception ex)
else
{
System.Diagnostics.Debug.WriteLine($"Error loading stats: {ex.Message}");
UpdateDateRange();
}
finally
}
[RelayCommand]
public void LoadData()
{
_timer.Debounce(async () =>
{
IsLoading = false;
}
if (IsLoading) return;
IsLoading = true;
try
{
await Task.Delay(Constants.Time.WaitingDuration);
var (start, end) = CalculateDateRange();
if (start == null || end == null)
{
start = end = DateTime.Now.ToUniversalTime();
}
var durationTask = _playHistoryService.GetTotalListeningDurationAsync(start.Value, end.Value);
var logsTask = _playHistoryService.GetLogsByDateRangeAsync(start.Value, end.Value);
var topSongsTask = _playHistoryService.GetTopSongsAsync(start.Value, end.Value, 10);
var topArtistsTask = _playHistoryService.GetTopArtistsAsync(start.Value, end.Value, 10);
var playersTask = _playHistoryService.GetPlayerDistributionAsync(start.Value, end.Value);
await Task.WhenAll(durationTask, logsTask, topSongsTask, topArtistsTask, playersTask);
TotalDuration = await durationTask;
var logs = await logsTask;
TotalTracksPlayed = logs.Count;
TopSongs = [.. await topSongsTask];
var pStats = await playersTask;
UpdatePlayerStats(pStats);
TopArtists = [.. await topArtistsTask];
ProcessHourlyStats(logs);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error loading stats: {ex.Message}");
}
finally
{
IsLoading = false;
}
}, Constants.Time.DebounceTimeout);
}
[RelayCommand]
private async Task GenerateTestDataAsync()
{
await _playHistoryService.GenerateTestDataAsync(1000);
await LoadDataAsync(); // 生成完刷新
LoadData(); // 生成完刷新
}
public void Receive(PropertyChangedMessage<bool> message)
{
if (message.Sender is IGSMTCService)
{
if (message.PropertyName == nameof(IGSMTCService.IsScrobbled))
{
if (message.NewValue == true)
{
RefreshData();
}
}
}
}
}
}

View File

@@ -169,7 +169,12 @@
x:Uid="MusicGalleryPageFolder"
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}" />
<Button
Command="{x:Bind ViewModel.OpenMediaSettingsCommand}"
Content="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
FontSize=12,
Glyph=&#xE713;}"
Style="{StaticResource GhostButtonStyle}" />
</StackPanel>
<TreeView
@@ -199,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="*" />
@@ -244,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>
@@ -252,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}}">
@@ -413,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>
@@ -433,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}" />
@@ -445,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"
@@ -462,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);
}
}
@@ -229,7 +150,10 @@ namespace BetterLyrics.WinUI3.Views
private void SelectAllCheckBox_Checked(object sender, RoutedEventArgs e)
{
SongListView.SelectAll();
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;
@@ -27,14 +27,14 @@ using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Views
{
public sealed partial class NowPlayingPage : Page,
IRecipient<PropertyChangedMessage<SongInfo?>>,
IRecipient<PropertyChangedMessage<SongInfo>>,
IRecipient<PropertyChangedMessage<LyricsLayoutOrientation>>,
IRecipient<PropertyChangedMessage<LyricsDisplayType>>,
IRecipient<PropertyChangedMessage<int>>,
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.Artist, artistsFontSize);
RenderTextBlock(AlbumTextBlock, _gsmtcService.CurrentSongInfo.Album, albumFontSize);
}
private void UpdateSongInfoOpacity()
@@ -139,6 +139,15 @@ namespace BetterLyrics.WinUI3.Views
}
}
private async void RefreshSongInfo()
{
SongInfoStackPanel.Opacity = 0;
await Task.Delay(Constants.Time.AnimationDuration);
RenderSongInfo();
SongInfoStackPanel.Opacity = 1;
UpdateSongInfoOpacity();
}
// ==== AlbumArt
private void UpdateAlbumArtOpacity()
{
@@ -555,7 +564,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)
@@ -575,17 +584,13 @@ namespace BetterLyrics.WinUI3.Views
// ====
public async void Receive(PropertyChangedMessage<SongInfo?> message)
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);
RenderSongInfo();
SongInfoStackPanel.Opacity = 1;
UpdateSongInfoOpacity();
RefreshSongInfo();
}
}
}
@@ -686,5 +691,6 @@ namespace BetterLyrics.WinUI3.Views
}
}
}
}

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();
});
}

View File

@@ -34,7 +34,7 @@
| :---: | :---: | :---: |
| <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) |
**交流群:** [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)
**交流群:** [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) | [QQ 频道BetterLyrics4U](https://pd.qq.com/s/1u1ntkyzr?b=9) | [Discord](https://discord.gg/5yAQPnyCKv) | [Telegram](https://t.me/+svhSLZ7awPsxNGY1)
</div>

View File

@@ -34,7 +34,7 @@
| :---: | :---: | :---: |
| <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)
**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) | [QQ Channel (BetterLyrics4U)](https://pd.qq.com/s/1u1ntkyzr?b=9) | [Discord](https://discord.gg/5yAQPnyCKv) | [Telegram](https://t.me/+svhSLZ7awPsxNGY1)
</div>

Some files were not shown because too many files have changed in this diff Show More