Compare commits

..

83 Commits

Author SHA1 Message Date
Zhe Fang
f735170961 refactor: plugin project 2026-01-12 12:51:42 -05:00
Zhe Fang
a215abc41b refactor: Move plugin projects to Plugins folder 2026-01-12 11:41:25 -05:00
Zhe Fang
2ddd7d19ab refactor: Move plugin projects to Plugins folder 2026-01-12 11:41:10 -05:00
Zhe Fang
4d270df594 refactor: Move plugin projects to Plugins folder 2026-01-12 11:37:59 -05:00
Zhe Fang
7fdb7ed870 chores 2026-01-12 11:27:29 -05:00
Zhe Fang
aad648d078 feat: ai plugin 2026-01-12 11:26:44 -05:00
Zhe Fang
d93fad6f3b fix: logo 2026-01-12 10:20:25 -05:00
Zhe Fang
a6bdf92341 fix: utf-8 2026-01-12 10:10:45 -05:00
Zhe Fang
96229d4346 Update _config.yml 2026-01-12 10:02:45 -05:00
Zhe Fang
bea29bdf9f Create index.md 2026-01-12 10:02:27 -05:00
Zhe Fang
4ad4959c39 Create _config.yml 2026-01-12 09:57:24 -05:00
Zhe Fang
7457cb1139 Change interface from ILyricsProvider to ILyricsSearchPlugin
Updated interface implementation reference from ILyricsProvider to ILyricsSearchPlugin.
2026-01-11 22:36:36 -05:00
Zhe Fang
b6a6cd8659 Update language link from 'ÖÐÎÄ' to '中文' 2026-01-11 22:32:26 -05:00
Zhe Fang
6173dae1fa Update plugin development guide link in README.CN.md 2026-01-11 22:31:39 -05:00
Zhe Fang
b6aac2b968 fix: BetterLyrics.Plugins.Romaji not working as expected 2026-01-11 21:01:48 -05:00
Zhe Fang
290f84cea0 chores: update links in readme 2026-01-11 20:30:30 -05:00
Zhe Fang
3031d298d5 chores 2026-01-11 20:25:33 -05:00
Zhe Fang
ed74e96aeb chores 2026-01-11 20:22:31 -05:00
Zhe Fang
affa55b057 fix: README in lyrics-window-styles 2026-01-11 20:20:37 -05:00
Zhe Fang
305f5e2cac chores 2026-01-11 20:18:24 -05:00
Zhe Fang
b8d4fc4130 chores: refactor folder structure 2026-01-11 20:03:12 -05:00
Zhe Fang
da5fd90d44 chores: update readme 2026-01-11 19:53:10 -05:00
Zhe Fang
f4a6aa78b6 Add BetterLyrics Plugin Development Guide
This document provides a comprehensive guide for developing BetterLyrics plugins, detailing core mechanics, project setup, configuration, and development workflow.
2026-01-11 19:38:09 -05:00
Zhe Fang
52b81cd959 Refactor XML project file structure and properties 2026-01-11 19:34:20 -05:00
Zhe Fang
d79933d41e Create PLUGIN_DEV.CN.md 2026-01-11 19:30:37 -05:00
Zhe Fang
a82f1bdb53 chores: plug-in system has been initially completed. 2026-01-11 18:58:18 -05:00
Zhe Fang
2cac55b55e Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-11 09:47:29 -05:00
Zhe Fang
4efe897ab2 chores: remove unnecessary packages for RomajiConverter.Core 2026-01-11 09:47:27 -05:00
Zhe Fang
173f808a88 Merge pull request #188 from zxbmmmmmmmmm/dev
refactor: animations easing
2026-01-11 09:46:37 -05:00
Betta_Fish
e73c27c705 refactor: animations easing 2026-01-11 22:29:57 +08:00
Zhe Fang
3724ef5f7e chores: add README for BetterLyrics.Plugins.Romaji 2026-01-10 18:19:57 -05:00
Zhe Fang
7ca0ecde16 Delete .github/workflows/plugin-registry-check.yml 2026-01-10 18:08:37 -05:00
Zhe Fang
2e0700dba5 Delete Community directory 2026-01-10 18:08:16 -05:00
Zhe Fang
9ec12e3a08 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-10 18:08:00 -05:00
Zhe Fang
d042993eb7 feat: romaji plugin (without dict) 2026-01-10 18:07:57 -05:00
Zhe Fang
fea7367671 feat: romaji plugin (without dict) 2026-01-10 18:07:54 -05:00
Zhe Fang
2b3169e5f6 Update check-hash-collision.js 2026-01-10 13:32:08 -05:00
Zhe Fang
9d2e245a99 Rename releases-to-discord.yml to release-to-discord.yml 2026-01-10 13:25:17 -05:00
Zhe Fang
0513c3a128 Add GitHub Actions workflow for plugin registry check 2026-01-10 13:22:32 -05:00
Zhe Fang
5082c4c245 Create check-hash-collision.js 2026-01-10 13:20:59 -05:00
Zhe Fang
659b4d0e60 Create plugins-registry.json 2026-01-10 13:19:39 -05:00
Zhe Fang
85f67c2ec6 Merge pull request #186 from jayfunc/plugin
feat: plugin system
2026-01-10 12:43:34 -05:00
Zhe Fang
bdbeb391ea Merge branch 'dev' into plugin 2026-01-10 12:43:27 -05:00
Zhe Fang
3357e7aaf4 chores: bump to 256 2026-01-10 11:46:26 -05:00
Zhe Fang
e43461d624 feat: float and glow effect now can be adapted to auto word-by-word effect 2026-01-10 11:29:22 -05:00
Zhe Fang
3e1907ad8c fix: lyrics parser (line endtime) 2026-01-10 10:06:11 -05:00
Zhe Fang
74eeffc8a6 fix: fan shape lyrics effect 2026-01-10 09:45:23 -05:00
Zhe Fang
c32eb3b877 Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-10 07:38:18 -05:00
Zhe Fang
047e53b830 fix: lyrics parser 2026-01-10 07:38:17 -05:00
Zhe Fang
fdb7bd16f6 plugin 2026-01-10 06:21:57 -05:00
Zhe Fang
4e33444d8e Update README.md 2026-01-09 19:26:31 -05:00
Zhe Fang
8fc67711b6 Simplify language selection in README.CN.md
Removed redundant link from Chinese README.
2026-01-09 19:26:16 -05:00
Zhe Fang
28757d9880 chores: bump to 253 2026-01-09 18:09:45 -05:00
Zhe Fang
e5fb04f577 fix: ValueTransition init 2026-01-09 17:54:50 -05:00
Zhe Fang
9d03c8f688 chores: i18n 2026-01-09 17:39:35 -05:00
Zhe Fang
094fe7b7a1 fix: lyrics animation gpu usage 2026-01-09 17:31:07 -05:00
Zhe Fang
bc32a3f34c feat: add all time filter for stats 2026-01-09 15:27:00 -05:00
Zhe Fang
b23d3c280f fix: not found and loading lyrics placeholder was not displayed 2026-01-09 14:48:12 -05:00
Zhe Fang
2738d45b69 fix: fill Duration (via searching NCM) for amll-ttml-db lyrics source; improve matching system 2026-01-09 13:50:46 -05:00
Zhe Fang
77a9bb0a1b Merge branch 'dev' of https://github.com/jayfunc/BetterLyrics into dev 2026-01-08 21:41:57 -05:00
Zhe Fang
c07389acfb chores: add new sponsor to AboutControl 2026-01-08 21:41:56 -05:00
Zhe Fang
042229ae74 Add new sponsor to SPONSORS.md 2026-01-08 21:40:00 -05:00
Zhe Fang
caaf93cf27 chores: i18n 2026-01-08 21:33:37 -05:00
Zhe Fang
92e4b9468c feat: enhance ValueTransition (support keyframes) 2026-01-08 20:58:12 -05:00
Zhe Fang
6f60952d09 fix: lyrics cache was not updated when searching without enabling cache, lyrics animation issue 2026-01-08 16:21:44 -05:00
Zhe Fang
efc175668e chores: remove unused files 2026-01-07 21:03:16 -05:00
Zhe Fang
3bf0fbef5f chores: code cleanup 2026-01-07 20:49:20 -05:00
Zhe Fang
96b7835e8f chores: re-structure Models folder 2026-01-07 20:47:08 -05:00
Zhe Fang
a0b6511a53 chores: extra data update algo from draw method to update method 2026-01-07 20:08:28 -05:00
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
296 changed files with 8476 additions and 2973 deletions

3
.gitignore vendored
View File

@@ -408,3 +408,6 @@ FodyWeavers.xsd
/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package)_TemporaryKey.pfx /BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package)_TemporaryKey.pfx
/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Constants/LastFM.cs /BetterLyrics.WinUI3/BetterLyrics.WinUI3/Constants/LastFM.cs
/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Constants/Discord.cs /BetterLyrics.WinUI3/BetterLyrics.WinUI3/Constants/Discord.cs
/BetterLyrics.Plugins.Romaji/unidic
/Plugins/BetterLyrics.Plugins.Romaji/unidic
/Plugins/BetterLyrics.Plugins.Transliteration.Romaji/unidic

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,12 @@
using BetterLyrics.Core.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces.Features
{
public interface ILyricsSource
{
Task<LyricsSearchResult> GetLyricsAsync(string title, string artist, string album, double duration);
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces.Features
{
public interface ILyricsTranslator
{
Task<string?> GetTranslationAsync(string text, string targetLangCode);
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces.Features
{
public interface ILyricsTransliterator
{
Task<string?> GetTransliterationAsync(string text, string targetLangCode);
}
}

View File

@@ -0,0 +1,20 @@
using BetterLyrics.Core.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces
{
public interface IPlugin
{
string Id { get; }
string Name { get; }
string Description { get; }
string Author { get; }
string Version { get; }
DateTime LastUpdated { get; }
void OnLoad(IPluginContext context);
void OnUnload();
}
}

View File

@@ -0,0 +1,14 @@
using BetterLyrics.Core.Interfaces.Services;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces
{
public interface IPluginContext
{
string PluginDirectory { get; }
IAIService? AIService { get; }
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces.Services
{
public interface IAIService
{
Task<string> ChatAsync(string systemPrompt, string userPrompt);
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Models
{
public record LyricsSearchResult(
string? Title,
string? Artist,
string? Album,
double? Duration,
string Raw,
string? Translation = null,
string? Transliteration = null,
string? Reference = null);
}

View File

@@ -143,7 +143,7 @@
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
</ItemGroup> </ItemGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" /> <Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />

View File

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

View File

@@ -1,30 +1,31 @@
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models.Db; using BetterLyrics.WinUI3.Models.DbContext;
using BetterLyrics.WinUI3.Services.AlbumArtSearchService; using BetterLyrics.WinUI3.Services.AlbumArtSearchService;
using BetterLyrics.WinUI3.Services.DiscordService; using BetterLyrics.WinUI3.Services.DiscordService;
using BetterLyrics.WinUI3.Services.FileSystemService; using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.GSMTCService; using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.LastFMService; using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LocalizationService; using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.LyricsCacheService;
using BetterLyrics.WinUI3.Services.LyricsSearchService; using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.PlayHistoryService; using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.PluginService;
using BetterLyrics.WinUI3.Services.SettingsService; using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SMTCService; using BetterLyrics.WinUI3.Services.SMTCService;
using BetterLyrics.WinUI3.Services.SongSearchMapService;
using BetterLyrics.WinUI3.Services.TranslationService; using BetterLyrics.WinUI3.Services.TranslationService;
using BetterLyrics.WinUI3.Services.TransliterationService; using BetterLyrics.WinUI3.Services.TransliterationService;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views; using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle; // 关键App生命周期管理 using Microsoft.Windows.AppLifecycle;
using Serilog; using Serilog;
using System; using System;
using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@@ -126,13 +127,25 @@ namespace BetterLyrics.WinUI3
protected override async void OnLaunched(LaunchActivatedEventArgs args) protected override async void OnLaunched(LaunchActivatedEventArgs args)
{ {
// 初始化数据库 await InitDatabasesAsync();
await EnsureDatabasesAsync();
var settingsService = Ioc.Default.GetRequiredService<ISettingsService>(); 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>(); var fileSystemService = Ioc.Default.GetRequiredService<IFileSystemService>();
// 开始后台扫描任务
foreach (var item in settingsService.AppSettings.LocalMediaFolders) foreach (var item in settingsService.AppSettings.LocalMediaFolders)
{ {
if (item.LastSyncTime == null) if (item.LastSyncTime == null)
@@ -142,10 +155,14 @@ namespace BetterLyrics.WinUI3
} }
fileSystemService.StartAllFolderTimers(); fileSystemService.StartAllFolderTimers();
// 初始化托盘 // Ensure plugins
var pluginService = Ioc.Default.GetRequiredService<IPluginService>();
pluginService.LoadPlugins();
// Init system tray
m_window = WindowHook.OpenOrShowWindow<SystemTrayWindow>(); m_window = WindowHook.OpenOrShowWindow<SystemTrayWindow>();
// 根据设置打开歌词窗口 // Open lyrics window if set
if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow) if (settingsService.AppSettings.GeneralSettings.AutoStartLyricsWindow)
{ {
var defaultStatus = settingsService.AppSettings.WindowBoundsRecords.Where(x => x.IsDefault); var defaultStatus = settingsService.AppSettings.WindowBoundsRecords.Where(x => x.IsDefault);
@@ -162,97 +179,39 @@ namespace BetterLyrics.WinUI3
} }
} }
// 根据设置自动打开主界面 // Open music gallery if set
if (settingsService.AppSettings.MusicGallerySettings.AutoOpen) if (settingsService.AppSettings.MusicGallerySettings.AutoOpen)
{ {
WindowHook.OpenOrShowWindow<MusicGalleryWindow>(); WindowHook.OpenOrShowWindow<MusicGalleryWindow>();
} }
} }
private async Task EnsureDatabasesAsync() private async Task InitDatabasesAsync()
{ {
// Init databases
var playHistoryFactory = Ioc.Default.GetRequiredService<IDbContextFactory<PlayHistoryDbContext>>(); 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( using (var playHistoryDb = await playHistoryFactory.CreateDbContextAsync())
"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
{ {
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}"); await songSearchMapDb.Database.EnsureCreatedAsync();
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})");
}
} }
}
private async Task ShowErrorDialogAsync(string title, string content) using (var filesIndexDb = await filesIndexFactory.CreateDbContextAsync())
{
// 这里假设 m_window 已经存在。如果没有显示主窗口,这个弹窗可能无法显示。
// 在 App 启动极早期的错误,可能需要退化为 Log 或者 System.Diagnostics.Process.Start 打开记事本报错
if (m_window != null)
{ {
m_window.DispatcherQueue.TryEnqueue(async () => await filesIndexDb.Database.EnsureCreatedAsync();
{ }
var dialog = new Microsoft.UI.Xaml.Controls.ContentDialog
{ using (var lyricsCacheDb = await lyricsCacheFactory.CreateDbContextAsync())
Title = title, {
Content = content, await lyricsCacheDb.Database.EnsureCreatedAsync();
CloseButtonText = "OK",
XamlRoot = m_window.Content?.XamlRoot // 确保 Content 不为空
};
if (dialog.XamlRoot != null) await dialog.ShowAsync();
});
} }
} }
@@ -269,6 +228,8 @@ namespace BetterLyrics.WinUI3
// 数据库工厂 // 数据库工厂
.AddDbContextFactory<PlayHistoryDbContext>(options => options.UseSqlite($"Data Source={PathHelper.PlayHistoryPath}")) .AddDbContextFactory<PlayHistoryDbContext>(options => options.UseSqlite($"Data Source={PathHelper.PlayHistoryPath}"))
.AddDbContextFactory<FilesIndexDbContext>(options => options.UseSqlite($"Data Source={PathHelper.FilesIndexPath}")) .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 => .AddLogging(loggingBuilder =>
@@ -290,6 +251,9 @@ namespace BetterLyrics.WinUI3
.AddSingleton<ILocalizationService, LocalizationService>() .AddSingleton<ILocalizationService, LocalizationService>()
.AddSingleton<IFileSystemService, FileSystemService>() .AddSingleton<IFileSystemService, FileSystemService>()
.AddSingleton<IPlayHistoryService, PlayHistoryService>() .AddSingleton<IPlayHistoryService, PlayHistoryService>()
.AddSingleton<ILyricsCacheService, LyricsCacheService>()
.AddSingleton<ISongSearchMapService, SongSearchMapService>()
.AddSingleton<IPluginService, PluginService>()
// ViewModels // ViewModels
.AddSingleton<AppSettingsControlViewModel>() .AddSingleton<AppSettingsControlViewModel>()

View File

@@ -45,6 +45,7 @@
<None Remove="Controls\PatronControl.xaml" /> <None Remove="Controls\PatronControl.xaml" />
<None Remove="Controls\PlaybackSettingsControl.xaml" /> <None Remove="Controls\PlaybackSettingsControl.xaml" />
<None Remove="Controls\PlayQueue.xaml" /> <None Remove="Controls\PlayQueue.xaml" />
<None Remove="Controls\PluginManagerControl.xaml" />
<None Remove="Controls\PropertyRow.xaml" /> <None Remove="Controls\PropertyRow.xaml" />
<None Remove="Controls\RemoteServerConfigControl.xaml" /> <None Remove="Controls\RemoteServerConfigControl.xaml" />
<None Remove="Controls\ShortcutTextBox.xaml" /> <None Remove="Controls\ShortcutTextBox.xaml" />
@@ -82,7 +83,7 @@
<PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.2.251219" /> <PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.2.251219" />
<PackageReference Include="ComputeSharp.D2D1.WinUI" Version="3.2.0" /> <PackageReference Include="ComputeSharp.D2D1.WinUI" Version="3.2.0" />
<PackageReference Include="csharp-pinyin" Version="1.0.1" /> <PackageReference Include="csharp-pinyin" Version="1.0.1" />
<PackageReference Include="DevWinUI.Controls" Version="9.8.1" /> <PackageReference Include="DevWinUI.Controls" Version="9.9.2" />
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.6" /> <PackageReference Include="Dubya.WindowsMediaController" Version="2.5.6" />
<PackageReference Include="F23.StringSimilarity" Version="7.0.1" /> <PackageReference Include="F23.StringSimilarity" Version="7.0.1" />
<PackageReference Include="FlaUI.UIA3" Version="5.0.0" /> <PackageReference Include="FlaUI.UIA3" Version="5.0.0" />
@@ -96,10 +97,14 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" /> <PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
<PackageReference Include="NAudio.Wasapi" Version="2.2.1" /> <PackageReference Include="NAudio.Wasapi" Version="2.2.1" />
<PackageReference Include="NTextCat" Version="0.3.65" /> <PackageReference Include="NTextCat" Version="0.3.65" />
@@ -118,11 +123,12 @@
<PackageReference Include="VCollab.DiscordRichPresence" Version="1.7.0" /> <PackageReference Include="VCollab.DiscordRichPresence" Version="1.7.0" />
<PackageReference Include="WebDav.Client" Version="2.9.0" /> <PackageReference Include="WebDav.Client" Version="2.9.0" />
<PackageReference Include="WinUIEx" Version="2.9.0" /> <PackageReference Include="WinUIEx" Version="2.9.0" />
<PackageReference Include="z440.atl.core" Version="7.9.0" /> <PackageReference Include="z440.atl.core" Version="7.10.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" /> <ProjectReference Include="..\..\BetterLyrics.Core\BetterLyrics.Core.csproj" />
<ProjectReference Include="..\..\Impressionist\Impressionist\Impressionist.csproj" /> <ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" />
<ProjectReference Include="..\..\Impressionist\Impressionist\Impressionist.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml"> <Page Update="Rendering\InAppLyricsRenderer.xaml">
@@ -134,252 +140,245 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup> <ItemGroup>
<TrimmerRootAssembly Include="FlaUI.UIA3" /> <TrimmerRootDescriptor Include="PluginConfigs\**\*_TrimmerRoots.xml" />
<TrimmerRootAssembly Include="Interop.UIAutomationClient" /> <TrimmerRootDescriptor Include="Core_TrimmerRoots.xml" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Abstractions" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Relational" />
<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Sqlite" />
<TrimmerRootAssembly Include="NAudio.Wasapi" />
<TrimmerRootAssembly Include="TagLibSharp" />
<TrimmerRootAssembly Include="Vanara.PInvoke.DwmApi" />
<TrimmerRootAssembly Include="Vanara.PInvoke.Gdi32" />
<TrimmerRootAssembly Include="Vanara.PInvoke.Shell32" />
<TrimmerRootAssembly Include="Vanara.PInvoke.User32" />
<TrimmerRootAssembly Include="Vanara.Windows.Shell" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Update="Assets\AIMP.png"> <Content Update="Assets\AIMP.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\AlbumArtPlaceholder.png"> <Content Update="Assets\AlbumArtPlaceholder.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Alipay.jpg"> <Content Update="Assets\Alipay.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\AMLLPlayer.png"> <Content Update="Assets\AMLLPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\AppleMusic.png"> <Content Update="Assets\AppleMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Chrome.png"> <Content Update="Assets\Chrome.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Edge.png"> <Content Update="Assets\Edge.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\FluidEffect.bin"> <Content Update="Assets\FluidEffect.bin">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Empty.png"> <Content Update="Assets\Empty.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\EmptyBox.png"> <Content Update="Assets\EmptyBox.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\EmptyState.png"> <Content Update="Assets\EmptyState.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Folder.png"> <Content Update="Assets\Folder.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\foobar2000.png"> <Content Update="Assets\foobar2000.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\HyPlayer.png"> <Content Update="Assets\HyPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\iTunes.png"> <Content Update="Assets\iTunes.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\KugouMusic.png"> <Content Update="Assets\KugouMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\LastFM.png"> <Content Update="Assets\LastFM.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Leaf.png"> <Content Update="Assets\Leaf.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Listen1.png"> <Content Update="Assets\Listen1.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Logo.ico"> <Content Update="Assets\Logo.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Logo.png"> <Content Update="Assets\Logo.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\LXMusic.png"> <Content Update="Assets\LXMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\MediaPlayerWindows11.png"> <Content Update="Assets\MediaPlayerWindows11.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\MoeKoeMusic.png"> <Content Update="Assets\MoeKoeMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\MusicBee.png"> <Content Update="Assets\MusicBee.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\NetEaseCloudMusic.png"> <Content Update="Assets\NetEaseCloudMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\OriginalSoundHQPlayer.png"> <Content Update="Assets\OriginalSoundHQPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Page.png"> <Content Update="Assets\Page.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\PlanetMusic.png"> <Content Update="Assets\PlanetMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\PotPlayer.png"> <Content Update="Assets\PotPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\QQMusic.png"> <Content Update="Assets\QQMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Question.png"> <Content Update="Assets\Question.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\RevolvingHearts.gif"> <Content Update="Assets\RevolvingHearts.gif">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\SaltPlayerForWindows.png"> <Content Update="Assets\SaltPlayerForWindows.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Segoe Fluent Icons.ttf"> <Content Update="Assets\Segoe Fluent Icons.ttf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Spotify.png"> <Content Update="Assets\Spotify.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\WeChatReward.png"> <Content Update="Assets\WeChatReward.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Update="Assets\Wiki82.profile.xml"> <Content Update="Assets\Wiki82.profile.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\PatronControl.xaml"> <Page Update="Controls\PluginManagerControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Styles\GhostSliderStyle.xaml"> <Page Update="Controls\PatronControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Styles\Converters.xaml"> <Page Update="Styles\GhostSliderStyle.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\PlayQueue.xaml"> <Page Update="Styles\Converters.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\StatsDashboardControl.xaml"> <Page Update="Controls\PlayQueue.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\FontFamilyAutoSuggestBox.xaml"> <Page Update="Controls\StatsDashboardControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\LyricsStyleSettingsControl.xaml"> <Page Update="Controls\FontFamilyAutoSuggestBox.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Views\LyricsWindowSwitchWindow.xaml"> <Page Update="Controls\LyricsStyleSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\LyricsWindowSwitchControl.xaml"> <Page Update="Views\LyricsWindowSwitchWindow.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\DemoWindowGrid.xaml"> <Page Update="Controls\LyricsWindowSwitchControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\LyricsWindowSettingsControl.xaml"> <Page Update="Controls\DemoWindowGrid.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Views\LyricsSearchWindow.xaml"> <Page Update="Controls\LyricsWindowSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\LyricsSearchControl.xaml"> <Page Update="Views\LyricsSearchWindow.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\ShortcutTextBox.xaml"> <Page Update="Controls\LyricsSearchControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\ExtendedSlider.xaml"> <Page Update="Controls\ShortcutTextBox.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\LyricsEffectSettingsControl.xaml"> <Page Update="Controls\ExtendedSlider.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\MediaSettingsControl.xaml"> <Page Update="Controls\LyricsEffectSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\PlaybackSettingsControl.xaml"> <Page Update="Controls\MediaSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\AlbumArtAreaStyleSettingsControl.xaml"> <Page Update="Controls\PlaybackSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\LyricsBackgroundSettingsControl.xaml"> <Page Update="Controls\AlbumArtAreaStyleSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\AppSettingsControl.xaml"> <Page Update="Controls\LyricsBackgroundSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Views\MusicGalleryWindow.xaml"> <Page Update="Controls\AppSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Views\MusicGalleryPage.xaml"> <Page Update="Views\MusicGalleryWindow.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\MusicGalleryPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Views\SettingsWindow.xaml"> <Page Update="Views\SettingsWindow.xaml">
@@ -392,54 +391,57 @@
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\RemoteServerConfigControl.xaml"> <Page Update="Controls\RemoteServerConfigControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\NowPlayingBar.xaml"> <Page Update="Controls\NowPlayingBar.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Views\SystemTrayWindow.xaml"> <Page Update="Views\SystemTrayWindow.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\WindowSettingsControl.xaml"> <Page Update="Controls\WindowSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\AlbumArtAreaEffectSettingsControl.xaml"> <Page Update="Controls\AlbumArtAreaEffectSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\ImageSwitcher.xaml"> <Page Update="Controls\ImageSwitcher.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\ShadowImage.xaml"> <Page Update="Controls\ShadowImage.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\PropertyRow.xaml"> <Page Update="Controls\PropertyRow.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Controls\AboutControl.xaml"> <Page Update="Controls\AboutControl.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Styles\InteractiveListViewHeaderStyle.xaml"> <Page Update="Styles\InteractiveListViewHeaderStyle.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
</ItemGroup>
<ItemGroup>
<Folder Include="PluginConfigs\" />
</ItemGroup> </ItemGroup>
<!-- Publish Properties --> <!-- Publish Properties -->
<PropertyGroup> <PropertyGroup>

View File

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

View File

@@ -160,7 +160,8 @@
<RichTextBlock> <RichTextBlock>
<Paragraph> <Paragraph>
<Run x:Uid="SetingsPageContributors" /> <Run x:Uid="SetingsPageContributors" />
<Run Text="(Code)" /> <Run Text="-" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Code" />
</Paragraph> </Paragraph>
</RichTextBlock> </RichTextBlock>
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal"> <dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
@@ -178,7 +179,8 @@
<RichTextBlock> <RichTextBlock>
<Paragraph> <Paragraph>
<Run x:Uid="SetingsPageContributors" /> <Run x:Uid="SetingsPageContributors" />
<Run Text="(Translator)" /> <Run Text="-" />
<Run Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Translator" />
</Paragraph> </Paragraph>
</RichTextBlock> </RichTextBlock>
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal"> <dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
@@ -193,13 +195,20 @@
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<TextBlock x:Uid="SettingsPagePatrons" /> <TextBlock x:Uid="SettingsPagePatrons" />
<dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal"> <dev:WrapPanel Margin="-12,0,0,0" Orientation="Horizontal">
<uc:PatronControl Date="Jan 8, 2026" PatronName="Eureka-K_K" />
<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 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 23, 2025" PatronName="**玄" />
<uc:PatronControl Date="Nov 21, 2025" PatronName="**智" /> <uc:PatronControl Date="Nov 21, 2025" PatronName="**智" />
<uc:PatronControl Date="Nov 17, 2025" PatronName="*鹤" /> <uc:PatronControl Date="Nov 17, 2025" PatronName="SuHeAndZl" />
<uc:PatronControl Date="Nov 2, 2025" PatronName="借过" /> <uc:PatronControl Date="Nov 2, 2025" PatronName="借过" />
<uc:PatronControl Date="Aug 28, 2025" PatronName="**华" /> <uc:PatronControl Date="Aug 28, 2025" PatronName="**华" />
<TextBlock x:Uid="SettingsPageUserWhoPurchased" Margin="12,8" /> <TextBlock
x:Uid="SettingsPageUserWhoPurchased"
Margin="12,8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</dev:WrapPanel> </dev:WrapPanel>
</StackPanel> </StackPanel>
</dev:SettingsCard> </dev:SettingsCard>
@@ -208,7 +217,10 @@
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left"> <dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<TextBlock x:Uid="SetingsPageSpecialThanks" /> <TextBlock x:Uid="SetingsPageSpecialThanks" />
<TextBlock x:Uid="SettingsPageYouNowUsing" Margin="0,8" /> <TextBlock
x:Uid="SettingsPageYouNowUsing"
Margin="0,8"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</StackPanel> </StackPanel>
</dev:SettingsCard> </dev:SettingsCard>
@@ -271,12 +283,6 @@
</dev:SettingsExpander.ItemsHeader> </dev:SettingsExpander.ItemsHeader>
</dev:SettingsExpander> </dev:SettingsExpander>
<dev:SettingsCard x:Uid="SettingsPageSettingsPlayHistory" Visibility="Collapsed">
<StackPanel Orientation="Horizontal" Spacing="6">
<Button x:Uid="SettingsPageExportPlayHistoryButton" Command="{x:Bind ViewModel.ExportPlayHistoryCommand}" />
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageFixedTimeStep" Visibility="Collapsed"> <dev:SettingsCard x:Uid="SettingsPageFixedTimeStep" Visibility="Collapsed">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.AdvancedSettings.IsFixedTimeStep, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.AdvancedSettings.IsFixedTimeStep, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>

View File

@@ -1,8 +1,6 @@
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System.Threading.Tasks;
// To learn more about WinUI, the WinUI project structure, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // and more about our project templates, see: http://aka.ms/winui-project-info.

View File

@@ -1,5 +1,5 @@
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.SettingsService; using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Views; using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;

View File

@@ -5,9 +5,9 @@ using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Logic; using BetterLyrics.WinUI3.Logic;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.Models.Settings; using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Renderer; using BetterLyrics.WinUI3.Renderer;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.GSMTCService; using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.Services.SettingsService; using BetterLyrics.WinUI3.Services.SettingsService;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
@@ -58,42 +58,43 @@ namespace BetterLyrics.WinUI3.Controls
private readonly ValueTransition<Color> _immersiveBgColorTransition = new( private readonly ValueTransition<Color> _immersiveBgColorTransition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<double> _immersiveBgOpacityTransition = new( private readonly ValueTransition<double> _immersiveBgOpacityTransition = new(
initialValue: 1f, initialValue: 1f,
durationSeconds: 0.3f EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: 0.3f
); );
private readonly ValueTransition<Color> _accentColor1Transition = new( private readonly ValueTransition<Color> _accentColor1Transition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<Color> _accentColor2Transition = new( private readonly ValueTransition<Color> _accentColor2Transition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<Color> _accentColor3Transition = new( private readonly ValueTransition<Color> _accentColor3Transition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<Color> _accentColor4Transition = new( private readonly ValueTransition<Color> _accentColor4Transition = new(
initialValue: Colors.Transparent, initialValue: Colors.Transparent,
durationSeconds: 0.3f, defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to) interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
); );
private readonly ValueTransition<double> _canvasYScrollTransition = new( private readonly ValueTransition<double> _canvasYScrollTransition = new(
initialValue: 0f, initialValue: 0f,
durationSeconds: 0.3f, EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
easingType: EasingType.EaseInOutSine defaultTotalDuration: 0.3f
); );
private readonly ValueTransition<double> _mouseYScrollTransition = new( private readonly ValueTransition<double> _mouseYScrollTransition = new(
initialValue: 0f, initialValue: 0f,
durationSeconds: 0.3f, EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
easingType: EasingType.EaseInOutSine defaultTotalDuration: 0.3f
); );
private TimeSpan _songPositionWithOffset; private TimeSpan _songPositionWithOffset;
@@ -119,7 +120,7 @@ namespace BetterLyrics.WinUI3.Controls
private bool _isLayoutChanged = true; private bool _isLayoutChanged = true;
private bool _isMouseScrollingChanged = false; private bool _isMouseScrollingChanged = false;
private int _playingLineIndex; private int _primaryPlayingLineIndex;
private (int Start, int End) _visibleRange; private (int Start, int End) _visibleRange;
private double _canvasTargetScrollOffset; private double _canvasTargetScrollOffset;
@@ -292,7 +293,7 @@ namespace BetterLyrics.WinUI3.Controls
} }
else if (e.Property == MouseScrollOffsetProperty) else if (e.Property == MouseScrollOffsetProperty)
{ {
canvas._mouseYScrollTransition.StartTransition(Convert.ToDouble(e.NewValue)); canvas._mouseYScrollTransition.Start(Convert.ToDouble(e.NewValue));
} }
else if (e.Property == MousePositionProperty) else if (e.Property == MousePositionProperty)
{ {
@@ -318,11 +319,11 @@ namespace BetterLyrics.WinUI3.Controls
else if (e.Property == AlbumArtThemeColorsProperty) else if (e.Property == AlbumArtThemeColorsProperty)
{ {
var albumArtThemeColors = (AlbumArtThemeColors)e.NewValue; var albumArtThemeColors = (AlbumArtThemeColors)e.NewValue;
canvas._immersiveBgColorTransition.StartTransition(albumArtThemeColors.EnvColor); canvas._immersiveBgColorTransition.Start(albumArtThemeColors.EnvColor);
canvas._accentColor1Transition.StartTransition(albumArtThemeColors.AccentColor1); canvas._accentColor1Transition.Start(albumArtThemeColors.AccentColor1);
canvas._accentColor2Transition.StartTransition(albumArtThemeColors.AccentColor2); canvas._accentColor2Transition.Start(albumArtThemeColors.AccentColor2);
canvas._accentColor3Transition.StartTransition(albumArtThemeColors.AccentColor3); canvas._accentColor3Transition.Start(albumArtThemeColors.AccentColor3);
canvas._accentColor4Transition.StartTransition(albumArtThemeColors.AccentColor4); canvas._accentColor4Transition.Start(albumArtThemeColors.AccentColor4);
canvas._albumArtThemeColors = albumArtThemeColors; canvas._albumArtThemeColors = albumArtThemeColors;
canvas._isLayoutChanged = true; canvas._isLayoutChanged = true;
@@ -344,7 +345,6 @@ namespace BetterLyrics.WinUI3.Controls
var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings; var lyricsEffect = _lyricsWindowStatus.LyricsEffectSettings;
double songDuration = _gsmtcService.CurrentSongInfo.DurationMs; double songDuration = _gsmtcService.CurrentSongInfo.DurationMs;
bool isForceWordByWord = _settingsService.AppSettings.GeneralSettings.IsForceWordByWordEffect;
Color overlayColor; Color overlayColor;
double finalOpacity; double finalOpacity;
@@ -382,7 +382,6 @@ namespace BetterLyrics.WinUI3.Controls
control: sender, control: sender,
ds: args.DrawingSession, ds: args.DrawingSession,
lines: _renderLyricsLines, lines: _renderLyricsLines,
playingLineIndex: _playingLineIndex,
mouseHoverLineIndex: _mouseHoverLineIndex, mouseHoverLineIndex: _mouseHoverLineIndex,
isMousePressing: _isMousePressing, isMousePressing: _isMousePressing,
startVisibleIndex: _visibleRange.Start, startVisibleIndex: _visibleRange.Start,
@@ -398,6 +397,7 @@ namespace BetterLyrics.WinUI3.Controls
strokeColor: _albumArtThemeColors.StrokeFontColor, strokeColor: _albumArtThemeColors.StrokeFontColor,
bgColor: _albumArtThemeColors.BgFontColor, bgColor: _albumArtThemeColors.BgFontColor,
fgColor: _albumArtThemeColors.FgFontColor, fgColor: _albumArtThemeColors.FgFontColor,
currentProgressMs: _songPositionWithOffset.TotalMilliseconds,
getPlaybackState: (lineIndex) => getPlaybackState: (lineIndex) =>
{ {
if (_renderLyricsLines == null) return new LinePlaybackState(); if (_renderLyricsLines == null) return new LinePlaybackState();
@@ -410,9 +410,7 @@ namespace BetterLyrics.WinUI3.Controls
return _synchronizer.GetLinePlayingProgress( return _synchronizer.GetLinePlayingProgress(
_songPositionWithOffset.TotalMilliseconds, _songPositionWithOffset.TotalMilliseconds,
line, line,
nextLine, lyricsEffect.WordByWordEffectMode
songDuration,
isForceWordByWord
); );
} }
); );
@@ -433,19 +431,19 @@ namespace BetterLyrics.WinUI3.Controls
); );
} }
#if DEBUG #if DEBUG && false
//args.DrawingSession.DrawText( args.DrawingSession.DrawText(
// $"Lyrics render start pos: ({(int)_renderLyricsStartX}, {(int)_renderLyricsStartY})\n" + $"Lyrics render start pos: ({(int)_renderLyricsStartX}, {(int)_renderLyricsStartY})\n" +
// $"Lyrics render size: [{(int)_renderLyricsWidth} x {(int)_renderLyricsHeight}]\n" + $"Lyrics render size: [{(int)_renderLyricsWidth} x {(int)_renderLyricsHeight}]\n" +
// $"Lyrics actual height: {LyricsLayoutManager.CalculateActualHeight(_renderLyricsLines)}\n" + $"Lyrics actual height: {LyricsLayoutManager.CalculateActualHeight(_renderLyricsLines)}\n" +
// $"Playing line (idx): {_playingLineIndex}\n" + $"Playing line (idx): {_playingLineIndex}\n" +
// $"Mouse hovering line (idx): {_mouseHoverLineIndex}\n" + $"Mouse hovering line (idx): {_mouseHoverLineIndex}\n" +
// $"Visible lines range (idx): [{_visibleRange.Start}, {_visibleRange.End}]\n" + $"Visible lines range (idx): [{_visibleRange.Start}, {_visibleRange.End}]\n" +
// $"Total line count: {LyricsLayoutManager.CalculateMaxRange(_renderLyricsLines).End + 1}\n" + $"Total line count: {LyricsLayoutManager.CalculateMaxRange(_renderLyricsLines).End + 1}\n" +
// $"Played: {_songPosition} / {TimeSpan.FromMilliseconds(_mediaSessionsService.CurrentSongInfo?.DurationMs ?? 0)}\n" + $"Played: {_songPosition} / {TimeSpan.FromMilliseconds(_gsmtcService.CurrentSongInfo.DurationMs)}\n" +
// $"Y offset: {_canvasYScrollTransition.Value}\n" + $"Y offset: {_canvasYScrollTransition.Value}\n" +
// $"User scroll offset: {_mouseYScrollTransition.Value}", $"User scroll offset: {_mouseYScrollTransition.Value}",
// new Vector2(0, 0), Colors.Red); new Vector2(0, 0), Colors.Red);
#endif #endif
} }
@@ -475,22 +473,29 @@ namespace BetterLyrics.WinUI3.Controls
#region UpdatePlayingLineIndex #region UpdatePlayingLineIndex
int newPlayingIndex = _synchronizer.GetCurrentLineIndex(_songPositionWithOffset.TotalMilliseconds, lyricsData); int primaryPlayingIndex = _synchronizer.GetCurrentLineIndex(_songPositionWithOffset.TotalMilliseconds, _renderLyricsLines);
bool isPlayingLineChanged = newPlayingIndex != _playingLineIndex; bool isPrimaryPlayingLineChanged = primaryPlayingIndex != _primaryPlayingLineIndex;
_playingLineIndex = newPlayingIndex; _primaryPlayingLineIndex = primaryPlayingIndex;
#endregion #endregion
#region UpdateTargetScrollOffset #region UpdateTargetScrollOffset
if (isPlayingLineChanged || _isLayoutChanged) if (isPrimaryPlayingLineChanged || _isLayoutChanged)
{ {
var targetScroll = LyricsLayoutManager.CalculateTargetScrollOffset(_renderLyricsLines, _playingLineIndex); var targetScroll = LyricsLayoutManager.CalculateTargetScrollOffset(_renderLyricsLines, _primaryPlayingLineIndex);
if (targetScroll.HasValue) _canvasTargetScrollOffset = targetScroll.Value; if (targetScroll.HasValue) _canvasTargetScrollOffset = targetScroll.Value;
_canvasYScrollTransition.SetEasingType(lyricsEffect.LyricsScrollEasingType); if (_isLayoutChanged)
_canvasYScrollTransition.SetDuration(lyricsEffect.LyricsScrollDuration / 1000.0); {
_canvasYScrollTransition.StartTransition(_canvasTargetScrollOffset, _isLayoutChanged); _canvasYScrollTransition.JumpTo(_canvasTargetScrollOffset);
}
else
{
_canvasYScrollTransition.SetDurationMs(lyricsEffect.LyricsScrollDuration);
_canvasYScrollTransition.SetInterpolator(EasingHelper.GetInterpolatorByEasingType<double>(lyricsEffect.LyricsScrollEasingType, lyricsEffect.LyricsScrollEasingMode));
_canvasYScrollTransition.Start(_canvasTargetScrollOffset);
}
} }
_canvasYScrollTransition.Update(elapsedTime); _canvasYScrollTransition.Update(elapsedTime);
@@ -523,7 +528,7 @@ namespace BetterLyrics.WinUI3.Controls
_renderLyricsLines, _renderLyricsLines,
_isMouseScrolling ? maxRange.Start : _visibleRange.Start, _isMouseScrolling ? maxRange.Start : _visibleRange.Start,
_isMouseScrolling ? maxRange.End : _visibleRange.End, _isMouseScrolling ? maxRange.End : _visibleRange.End,
_playingLineIndex, _primaryPlayingLineIndex,
sender.Size.Height, sender.Size.Height,
_canvasTargetScrollOffset, _canvasTargetScrollOffset,
lyricsStyle.PlayingLineTopOffset / 100.0, lyricsStyle.PlayingLineTopOffset / 100.0,
@@ -535,8 +540,9 @@ namespace BetterLyrics.WinUI3.Controls
elapsedTime, elapsedTime,
_isMouseScrolling, _isMouseScrolling,
_isLayoutChanged, _isLayoutChanged,
isPlayingLineChanged, isPrimaryPlayingLineChanged,
_isMouseScrollingChanged _isMouseScrollingChanged,
_songPositionWithOffset.TotalMilliseconds
); );
_isMouseScrollingChanged = false; _isMouseScrollingChanged = false;
@@ -667,15 +673,12 @@ namespace BetterLyrics.WinUI3.Controls
private void UpdateRenderLyricsLines() private void UpdateRenderLyricsLines()
{ {
_renderLyricsLines = null; _renderLyricsLines = null;
_renderLyricsLines = _gsmtcService.CurrentLyricsData?.LyricsLines.Select(x => new RenderLyricsLine() var lines = _gsmtcService.CurrentLyricsData?.LyricsLines.Select(x => new RenderLyricsLine(x)).ToList();
if (lines != null)
{ {
LyricsSyllables = x.LyricsSyllables, LyricsLayoutManager.CalculateLanes(lines);
StartMs = x.StartMs, }
EndMs = x.EndMs, _renderLyricsLines = lines;
PhoneticText = x.PhoneticText,
OriginalText = x.OriginalText,
TranslatedText = x.TranslatedText
}).ToList();
} }
private async Task ReloadCoverBackgroundResourcesAsync() private async Task ReloadCoverBackgroundResourcesAsync()

View File

@@ -23,6 +23,14 @@
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"
Text="Effect" /> Text="Effect" />
<dev:SettingsCard x:Uid="SettingsPageLyricsWordByWordEffectMode" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xF714;}">
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.WordByWordEffectMode, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageLyricsWordByWordEffectModeAuto" />
<ComboBoxItem x:Uid="SettingsPageLyricsWordByWordEffectModeNever" />
<ComboBoxItem x:Uid="SettingsPageLyricsWordByWordEffectModeAlways" />
</ComboBox>
</dev:SettingsCard>
<!-- 模糊效果 --> <!-- 模糊效果 -->
<dev:SettingsCard x:Uid="SettingsPageLyricsBlurEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE727;}"> <dev:SettingsCard x:Uid="SettingsPageLyricsBlurEffect" HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily}, Glyph=&#xE727;}">
<ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsBlurEffectEnabled, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind LyricsEffectSettings.IsLyricsBlurEffectEnabled, Mode=TwoWay}" />
@@ -108,6 +116,14 @@
Minimum="0" Minimum="0"
Value="{x:Bind LyricsEffectSettings.LyricsFloatAnimationAmount, Mode=TwoWay}" /> Value="{x:Bind LyricsEffectSettings.LyricsFloatAnimationAmount, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>
<dev:SettingsCard x:Uid="LyricsEffectSettingsControlAnimationDuration">
<local:ExtendedSlider
Default="450"
Maximum="2000"
Minimum="0"
Unit="ms"
Value="{x:Bind LyricsEffectSettings.LyricsFloatAnimationDuration, Mode=TwoWay}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items> </dev:SettingsExpander.Items>
</dev:SettingsExpander> </dev:SettingsExpander>
@@ -180,18 +196,25 @@
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.LyricsScrollEasingType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}"> <ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.LyricsScrollEasingType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageEasingTypeLinear" /> <ComboBoxItem x:Uid="SettingsPageEasingTypeLinear" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeSmoothStep" /> <ComboBoxItem x:Uid="SettingsPageEasingTypeSmoothStep" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutSine" /> <ComboBoxItem x:Uid="SettingsPageEasingTypeEaseSine" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutQuad" /> <ComboBoxItem x:Uid="SettingsPageEasingTypeEaseQuad" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutCubic" /> <ComboBoxItem x:Uid="SettingsPageEasingTypeEaseCubic" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutQuart" /> <ComboBoxItem x:Uid="SettingsPageEasingTypeEaseQuart" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutQuint" /> <ComboBoxItem x:Uid="SettingsPageEasingTypeEaseQuint" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutExpo" /> <ComboBoxItem x:Uid="SettingsPageEasingTypeEaseExpo" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutCirc" /> <ComboBoxItem x:Uid="SettingsPageEasingTypeEaseCirc" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutBack" /> <ComboBoxItem x:Uid="SettingsPageEasingTypeEaseBack" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutElastic" /> <ComboBoxItem x:Uid="SettingsPageEasingTypeEaseElastic" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutBounce" /> <ComboBoxItem x:Uid="SettingsPageEasingTypeEaseBounce" />
</ComboBox> </ComboBox>
<dev:SettingsExpander.Items> <dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageEasingMode">
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.LyricsScrollEasingMode, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageEasingModeIn" />
<ComboBoxItem x:Uid="SettingsPageEasingModeOut" />
<ComboBoxItem x:Uid="SettingsPageEasingModeInOut" />
</ComboBox>
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageScrollTopDuration"> <dev:SettingsCard x:Uid="SettingsPageScrollTopDuration">
<local:ExtendedSlider <local:ExtendedSlider
Default="500" Default="500"

View File

@@ -8,6 +8,7 @@
xmlns:dev="using:DevWinUI" xmlns:dev="using:DevWinUI"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Controls" xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:lyricsmodels="using:BetterLyrics.WinUI3.Models.Lyrics"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models" xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:ui="using:CommunityToolkit.WinUI" xmlns:ui="using:CommunityToolkit.WinUI"
@@ -97,19 +98,6 @@
Style="{StaticResource GhostButtonStyle}" /> Style="{StaticResource GhostButtonStyle}" />
</Grid> </Grid>
<RichTextBlock
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap">
<Paragraph>
<Run Text="*" />
<Run x:Uid="ArtistsSplitHint" />
</Paragraph>
<Paragraph>
<Run Text="; , / " />
</Paragraph>
</RichTextBlock>
</StackPanel> </StackPanel>
</Grid> </Grid>
@@ -169,7 +157,7 @@
<Grid Grid.Column="1"> <Grid Grid.Column="1">
<ListView ItemsSource="{x:Bind ViewModel.LyricsSearchResults, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedLyricsSearchResult, Mode=TwoWay}"> <ListView ItemsSource="{x:Bind ViewModel.LyricsSearchResults, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedLyricsSearchResult, Mode=TwoWay}">
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsSearchResult"> <DataTemplate x:DataType="models:LyricsCacheItem">
<ListViewItem IsEnabled="{x:Bind IsFound}"> <ListViewItem IsEnabled="{x:Bind IsFound}">
<StackPanel Padding="0,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}"> <StackPanel Padding="0,6" Opacity="{x:Bind IsFound, Converter={StaticResource BoolToPartialOpacityConverter}}">
<local:PropertyRow <local:PropertyRow
@@ -180,17 +168,13 @@
<!-- Lyrics search result --> <!-- Lyrics search result -->
<StackPanel Visibility="{x:Bind IsFound, Converter={StaticResource BoolToVisibilityConverter}}"> <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="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="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="LyricsSearchControlDurauion" Value="{x:Bind Duration, Converter={StaticResource SecondsToFormattedTimeConverter}, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow <local:PropertyRow
x:Uid="LyricsPageMatchPercentage" x:Uid="LyricsPageMatchPercentage"
Unit="%" Unit="%"
Value="{x:Bind MatchPercentage, Mode=OneWay}" /> 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> </StackPanel>
<!-- NOT FOUND --> <!-- NOT FOUND -->
<TextBlock <TextBlock
@@ -259,7 +243,7 @@
</interactivity:DataTriggerBehavior> </interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors> </interactivity:Interaction.Behaviors>
<Pivot.HeaderTemplate> <Pivot.HeaderTemplate>
<DataTemplate x:DataType="models:LyricsData"> <DataTemplate x:DataType="lyricsmodels:LyricsData">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind LanguageCode, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" /> <TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{x:Bind LanguageCode, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
<InfoBadge <InfoBadge
@@ -271,13 +255,13 @@
</DataTemplate> </DataTemplate>
</Pivot.HeaderTemplate> </Pivot.HeaderTemplate>
<Pivot.ItemTemplate> <Pivot.ItemTemplate>
<DataTemplate x:DataType="models:LyricsData"> <DataTemplate x:DataType="lyricsmodels:LyricsData">
<ListView <ListView
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}" ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
ItemsSource="{x:Bind LyricsLines, Mode=OneWay}" ItemsSource="{x:Bind LyricsLines, Mode=OneWay}"
SelectionMode="None"> SelectionMode="None">
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsLine"> <DataTemplate x:DataType="lyricsmodels:LyricsLine">
<Grid Margin="0,6" ColumnSpacing="6"> <Grid Margin="0,6" ColumnSpacing="6">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@@ -307,7 +291,7 @@
</interactivity:Interaction.Behaviors> </interactivity:Interaction.Behaviors>
</Button> </Button>
</Grid> </Grid>
<local:PropertyRow Grid.Column="1" Value="{x:Bind OriginalText, Mode=OneWay}" /> <local:PropertyRow Grid.Column="1" Value="{x:Bind PrimaryText, Mode=OneWay}" />
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ListView.ItemTemplate> </ListView.ItemTemplate>

View File

@@ -1,4 +1,4 @@
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;

View File

@@ -1,6 +1,6 @@
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Serialization; using BetterLyrics.WinUI3.Serialization;
using BetterLyrics.WinUI3.Services.SettingsService; using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;

View File

@@ -10,6 +10,7 @@
xmlns:local="using:BetterLyrics.WinUI3.Controls" xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models" xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:settingsmodels="using:BetterLyrics.WinUI3.Models.Settings"
xmlns:ui="using:CommunityToolkit.WinUI" xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -50,7 +51,7 @@
ItemsSource="{x:Bind ViewModel.AppSettings.LocalMediaFolders, Mode=OneWay}" ItemsSource="{x:Bind ViewModel.AppSettings.LocalMediaFolders, Mode=OneWay}"
SelectionMode="None"> SelectionMode="None">
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate x:DataType="models:MediaFolder"> <DataTemplate x:DataType="settingsmodels:MediaFolder">
<dev:SettingsExpander IsExpanded="True"> <dev:SettingsExpander IsExpanded="True">
<dev:SettingsExpander.HeaderIcon> <dev:SettingsExpander.HeaderIcon>

View File

@@ -1,11 +1,8 @@
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using System;
using System.Threading.Tasks;
using Windows.System;
// To learn more about WinUI, the WinUI project structure, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // and more about our project templates, see: http://aka.ms/winui-project-info.

View File

@@ -69,7 +69,7 @@
<TextBlock <TextBlock
FontSize="12" FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DisplayArtists, Mode=OneWay}" /> Text="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Artist, Mode=OneWay}" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
@@ -447,7 +447,7 @@
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind ViewModel.TimelineSliderThumbLyricsLine.StartMs, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" /> Text="{x:Bind ViewModel.TimelineSliderThumbLyricsLine.StartMs, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" />
<!-- TODO 原文翻译共同显示 --> <!-- TODO 原文翻译共同显示 -->
<TextBlock Margin="0,0,0,2" Text="{x:Bind ViewModel.TimelineSliderThumbLyricsLine.OriginalText, Mode=OneWay}" /> <TextBlock Margin="0,0,0,2" Text="{x:Bind ViewModel.TimelineSliderThumbLyricsLine.PrimaryText, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
<Grid <Grid

View File

@@ -1,20 +1,14 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.GSMTCService;
using BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views; using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Imaging;
using System; using System;
using System.Numerics; using System.Numerics;
using BetterLyrics.WinUI3.Extensions;
// To learn more about WinUI, the WinUI project structure, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // and more about our project templates, see: http://aka.ms/winui-project-info.

View File

@@ -10,8 +10,8 @@
<Grid Margin="12,8"> <Grid Margin="12,8">
<StackPanel Orientation="Horizontal" Spacing="6"> <StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="{x:Bind PatronName, Mode=OneWay}" /> <TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind PatronName, Mode=OneWay}" />
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind Date, Mode=OneWay}" /> <TextBlock Foreground="{ThemeResource TextFillColorTertiaryBrush}" Text="{x:Bind Date, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -1,17 +1,5 @@
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; 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, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // and more about our project templates, see: http://aka.ms/winui-project-info.

View File

@@ -6,18 +6,9 @@ using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages; using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; 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.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System; using System;
using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -41,7 +32,7 @@ namespace BetterLyrics.WinUI3.Controls
var targetItem = ViewModel.SMTCService.TrackPlayingQueue var targetItem = ViewModel.SMTCService.TrackPlayingQueue
.ElementAtOrDefault(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex); .ElementAtOrDefault(ViewModel.AppSettings.MusicGallerySettings.PlayQueueIndex);
if (targetItem == null) return; if (targetItem == null) return;
PlayingQueueListView.ScrollIntoView(targetItem); PlayingQueueListView.ScrollIntoView(targetItem);
} }

View File

@@ -13,6 +13,7 @@
xmlns:local="using:BetterLyrics.WinUI3.Controls" xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models" xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:settingsmodels="using:BetterLyrics.WinUI3.Models.Settings"
xmlns:ui="using:CommunityToolkit.WinUI" xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -60,7 +61,7 @@
ItemsSource="{x:Bind ViewModel.AppSettings.MediaSourceProvidersInfo, Mode=OneWay}" ItemsSource="{x:Bind ViewModel.AppSettings.MediaSourceProvidersInfo, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedMediaSourceProvider, Mode=TwoWay}"> SelectedItem="{x:Bind ViewModel.SelectedMediaSourceProvider, Mode=TwoWay}">
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>
<DataTemplate x:DataType="models:MediaSourceProviderInfo"> <DataTemplate x:DataType="settingsmodels:MediaSourceProviderInfo">
<Grid Padding="2,4" ColumnSpacing="12"> <Grid Padding="2,4" ColumnSpacing="12">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@@ -172,7 +173,7 @@
<ScalarTransition /> <ScalarTransition />
</ListView.OpacityTransition> </ListView.OpacityTransition>
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate x:DataType="models:AlbumArtSearchProviderInfo"> <DataTemplate x:DataType="settingsmodels:AlbumArtSearchProviderInfo">
<dev:SettingsCard Header="{Binding Provider, Converter={StaticResource AlbumArtSearchProviderToDisplayNameConverter}, Mode=OneWay}"> <dev:SettingsCard Header="{Binding Provider, Converter={StaticResource AlbumArtSearchProviderToDisplayNameConverter}, Mode=OneWay}">
<dev:SettingsCard.HeaderIcon> <dev:SettingsCard.HeaderIcon>
<FontIcon FontFamily="Segoe UI Symbol" Glyph="&#x283F;" /> <FontIcon FontFamily="Segoe UI Symbol" Glyph="&#x283F;" />
@@ -219,7 +220,7 @@
</Style> </Style>
</ListView.ItemContainerStyle> </ListView.ItemContainerStyle>
<ListView.ItemTemplate> <ListView.ItemTemplate>
<DataTemplate x:DataType="models:LyricsSearchProviderInfo"> <DataTemplate x:DataType="settingsmodels:LyricsSearchProviderInfo">
<Grid> <Grid>
<dev:SettingsExpander Header="{Binding Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}, Mode=OneWay}" IsExpanded="{Binding IsMatchingThresholdOverwritten, Mode=OneWay}"> <dev:SettingsExpander Header="{Binding Provider, Converter={StaticResource LyricsSearchProviderToDisplayNameConverter}, Mode=OneWay}" IsExpanded="{Binding IsMatchingThresholdOverwritten, Mode=OneWay}">
<dev:SettingsExpander.HeaderIcon> <dev:SettingsExpander.HeaderIcon>
@@ -227,6 +228,9 @@
</dev:SettingsExpander.HeaderIcon> </dev:SettingsExpander.HeaderIcon>
<ToggleSwitch IsOn="{Binding IsEnabled, Mode=TwoWay}" /> <ToggleSwitch IsOn="{Binding IsEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items> <dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="LyricsSearchControlIgnoreCache">
<CheckBox IsChecked="{Binding IgnoreCacheWhenSearching, Mode=TwoWay}" />
</dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageOverwriteMatchingThreshold"> <dev:SettingsCard x:Uid="SettingsPageOverwriteMatchingThreshold">
<ToggleSwitch IsOn="{Binding IsMatchingThresholdOverwritten, Mode=TwoWay}" /> <ToggleSwitch IsOn="{Binding IsMatchingThresholdOverwritten, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>
@@ -326,7 +330,7 @@
HorizontalContentAlignment="Left"> HorizontalContentAlignment="Left">
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="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="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}" /> <local:PropertyRow x:Uid="LyricsSearchControlDurauion" Value="{x:Bind ViewModel.GSMTCService.CurrentSongInfo.DurationMs, TargetNullValue=N/A, Converter={StaticResource MillisecondsToFormattedTimeConverter}, Mode=OneWay}" />
</StackPanel> </StackPanel>
@@ -339,7 +343,7 @@
HorizontalContentAlignment="Left"> HorizontalContentAlignment="Left">
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="SettingsPageSongTitle" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.Title, TargetNullValue=N/A, Mode=OneWay}" />
<local:PropertyRow x:Uid="SettingsPageArtist" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.DisplayArtists, TargetNullValue=N/A, Mode=OneWay}" /> <local:PropertyRow x:Uid="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="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="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="LyricsPageLanguageCode" Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsData.LanguageCode, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource LanguageCodeToDisplayedNameConverter}}" />
@@ -354,10 +358,6 @@
x:Uid="LyricsPageMatchPercentage" x:Uid="LyricsPageMatchPercentage"
Unit="%" Unit="%"
Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" /> Value="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.MatchPercentage, Mode=OneWay}" />
<local:PropertyRow
x:Uid="LyricsPageCachePath"
Link="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}"
ToolTipService.ToolTip="{x:Bind ViewModel.GSMTCService.CurrentLyricsSearchResult.SelfPath, TargetNullValue=N/A, Mode=OneWay, Converter={StaticResource UriStringToDecodedAbsoluteUri}}" />
</StackPanel> </StackPanel>
</Expander> </Expander>
@@ -365,10 +365,6 @@
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.ListenOnNewPlaybackSource, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.ListenOnNewPlaybackSource, Mode=TwoWay}" />
</dev:SettingsCard> </dev:SettingsCard>
<dev:SettingsCard x:Uid="SettingsPageForceWordByWordEffect">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.GeneralSettings.IsForceWordByWordEffect, Mode=TwoWay}" />
</dev:SettingsCard>
<!-- Lyrics translation --> <!-- Lyrics translation -->
<TextBlock x:Uid="SettingsPageTranslation" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" /> <TextBlock x:Uid="SettingsPageTranslation" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<dev:SettingsExpander x:Uid="LyricsPageTranslationEnabled" IsExpanded="True"> <dev:SettingsExpander x:Uid="LyricsPageTranslationEnabled" IsExpanded="True">
@@ -428,30 +424,6 @@
</dev:SettingsExpander> </dev:SettingsExpander>
<dev:SettingsExpander x:Uid="SettingsPageJapanese" IsExpanded="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=OneWay}"> <dev:SettingsExpander x:Uid="SettingsPageJapanese" IsExpanded="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=TwoWay}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard x:Uid="SettingsPageCutletDockerServer" IsEnabled="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=OneWay}">
<dev:SettingsCard.Description>
<HyperlinkButton Content="https://github.com/jayfunc/cutlet-docker" NavigateUri="https://github.com/jayfunc/cutlet-docker" />
</dev:SettingsCard.Description>
<Grid ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
x:Uid="CutletServerTextBox"
Grid.Column="0"
IsEnabled="{x:Bind ViewModel.IsCutletDockerServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}"
Text="{x:Bind ViewModel.AppSettings.TranslationSettings.CutletDockerServer, Mode=TwoWay}"
TextWrapping="Wrap" />
<Button
x:Uid="SettingsPageServerTestButton"
Grid.Column="1"
Command="{x:Bind ViewModel.CutletDockerServerTestCommand}"
IsEnabled="{x:Bind ViewModel.IsCutletDockerServerTesting, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}" />
</Grid>
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander> </dev:SettingsExpander>
<!-- 中文简体繁体偏好 --> <!-- 中文简体繁体偏好 -->

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.PluginManagerControl"
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:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:BetterLyrics.WinUI3.Models"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid Padding="36,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBlock x:Uid="PluginManagerControlTitle" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
</Grid>
<ListView
Grid.Row="1"
ItemContainerStyle="{StaticResource ListViewStretchedItemContainerStyle}"
ItemsSource="{x:Bind Plugins, Mode=OneWay}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:PluginDisplayModel">
<dev:SettingsExpander
Description="{x:Bind Description}"
Header="{x:Bind Name}"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE74C;}">
<TextBlock Text="{x:Bind Version}" />
<dev:SettingsExpander.Items>
<dev:SettingsCard HorizontalContentAlignment="Left" ContentAlignment="Left">
<StackPanel Spacing="6">
<local:PropertyRow Header="Author" Value="{x:Bind Author}" />
<local:PropertyRow Header="ID" Value="{x:Bind Id}" />
</StackPanel>
</dev:SettingsCard>
<dev:SettingsCard>
<Button
x:Uid="PluginManagerControlUninstall"
Click="OnUninstallClick"
Tag="{x:Bind Plugin}" />
</dev:SettingsCard>
</dev:SettingsExpander.Items>
</dev:SettingsExpander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="{x:Bind IsListEmpty, Mode=OneWay}">
<FontIcon
FontSize="48"
Glyph="&#xE74C;"
Opacity="0.3" />
<TextBlock
x:Uid="PluginManagerControlNoPluginsInstalled"
Margin="0,12,0,0"
Opacity="0.5" />
</StackPanel>
<StackPanel Grid.Row="2" Margin="0,6,0,20">
<Button
x:Uid="PluginManagerControlInstall"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Click="OnInstallPluginClick"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,149 @@
using BetterLyrics.Core.Interfaces;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.PluginService;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
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.Collections.ObjectModel;
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 PluginManagerControl : UserControl
{
public ObservableCollection<PluginDisplayModel> Plugins { get; } = new();
public Visibility IsListEmpty => Plugins.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
private readonly IPluginService _pluginService;
public PluginManagerControl()
{
this.InitializeComponent();
_pluginService = Ioc.Default.GetRequiredService<IPluginService>();
this.Loaded += (s, e) =>
{
RefreshPluginList();
};
}
private void RefreshPluginList()
{
Plugins.Clear();
var allPlugins = _pluginService.Plugins;
foreach (var plugin in allPlugins)
{
Plugins.Add(new PluginDisplayModel(plugin));
}
Bindings.Update();
}
private async void OnInstallPluginClick(object sender, RoutedEventArgs e)
{
var file = await Helper.PickerHelper.PickSingleFileAsync<SettingsWindow>([".zip"]);
if (file != null)
{
try
{
// 显示加载条...
// 3. 调用我们在上一步写的 InstallPlugin 方法
_pluginService.InstallPlugin(file.Path);
// 4. 重新加载所有插件 (这会触发热重载)
_pluginService.LoadPlugins();
// 5. 刷新界面
RefreshPluginList();
ShowTip("安装成功", $"插件 {file.Name} 已安装。");
}
catch (Exception ex)
{
ShowError("安装失败", ex.Message);
}
}
}
// 卸载按钮点击事件
private async void OnUninstallClick(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.Tag is IPlugin plugin)
{
// 二次确认对话框
ContentDialog deleteDialog = new ContentDialog
{
XamlRoot = this.XamlRoot,
Title = "卸载插件",
Content = $"确定要删除 \"{plugin.Name}\" 吗?此操作无法撤销。",
PrimaryButtonText = "删除",
CloseButtonText = "取消",
DefaultButton = ContentDialogButton.Close
};
var result = await deleteDialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
try
{
_pluginService.UninstallPlugin(plugin.Id);
// 暂时我们只能刷新列表演示
RefreshPluginList();
}
catch (Exception ex)
{
ShowError("卸载失败", ex.Message);
}
}
}
}
// 简单的弹窗辅助方法
private async void ShowTip(string title, string content)
{
ContentDialog dialog = new ContentDialog
{
XamlRoot = this.XamlRoot,
Title = title,
Content = content,
CloseButtonText = "关闭"
};
await dialog.ShowAsync();
}
private async void ShowError(string title, string content)
{
ContentDialog dialog = new ContentDialog
{
XamlRoot = this.XamlRoot,
Title = title,
Content = content,
CloseButtonText = "关闭"
};
await dialog.ShowAsync();
}
}
}

View File

@@ -1,6 +1,6 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.LocalizationService; using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Views; using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
@@ -142,7 +142,7 @@ namespace BetterLyrics.WinUI3.Controls
{ {
ProgressBar.Visibility = visibility; ProgressBar.Visibility = visibility;
} }
private void CheckPathForWarning() private void CheckPathForWarning()
{ {
string? path = PathBox.Text?.Trim(); string? path = PathBox.Text?.Trim();

View File

@@ -74,6 +74,7 @@
<ComboBoxItem x:Uid="StatsDashboardControlThisMonth" /> <ComboBoxItem x:Uid="StatsDashboardControlThisMonth" />
<ComboBoxItem x:Uid="StatsDashboardControlThisQuarter" /> <ComboBoxItem x:Uid="StatsDashboardControlThisQuarter" />
<ComboBoxItem x:Uid="StatsDashboardControlThisYear" /> <ComboBoxItem x:Uid="StatsDashboardControlThisYear" />
<ComboBoxItem x:Uid="StatsDashboardControlAllTime" />
<ComboBoxItem x:Uid="StatsDashboardControlCustom" /> <ComboBoxItem x:Uid="StatsDashboardControlCustom" />
</ComboBox> </ComboBox>

View File

@@ -1,20 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
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 BetterLyrics.WinUI3.ViewModels; using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using BetterLyrics.WinUI3.Enums; using Microsoft.UI.Xaml.Controls;
// To learn more about WinUI, the WinUI project structure, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // and more about our project templates, see: http://aka.ms/winui-project-info.

View File

@@ -1,5 +1,4 @@
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings; using BetterLyrics.WinUI3.Models.Settings;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;

View File

@@ -1,7 +1,5 @@
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
using System; using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Converter namespace BetterLyrics.WinUI3.Converter
{ {

View File

@@ -2,9 +2,7 @@
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.UI.Xaml.Media.Imaging;
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text;
namespace BetterLyrics.WinUI3.Converter namespace BetterLyrics.WinUI3.Converter
{ {

View File

@@ -1,8 +1,6 @@
using BetterLyrics.WinUI3.Extensions; using BetterLyrics.WinUI3.Extensions;
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
using System; using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Converter namespace BetterLyrics.WinUI3.Converter
{ {

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<linker>
<assembly fullname="FlaUI.UIA3" preserve="all" />
<assembly fullname="Interop.UIAutomationClient" preserve="all" />
<assembly fullname="Microsoft.EntityFrameworkCore" preserve="all" />
<assembly fullname="Microsoft.EntityFrameworkCore.Abstractions" preserve="all" />
<assembly fullname="Microsoft.EntityFrameworkCore.Relational" preserve="all" />
<assembly fullname="Microsoft.EntityFrameworkCore.Sqlite" preserve="all" />
<assembly fullname="NAudio.Wasapi" preserve="all" />
<assembly fullname="TagLibSharp" preserve="all" />
<assembly fullname="Vanara.PInvoke.DwmApi" preserve="all" />
<assembly fullname="Vanara.PInvoke.Gdi32" preserve="all" />
<assembly fullname="Vanara.PInvoke.Shell32" preserve="all" />
<assembly fullname="Vanara.PInvoke.User32" preserve="all" />
<assembly fullname="Vanara.Windows.Shell" preserve="all" />
</linker>

View File

@@ -1,8 +1,4 @@
using System; namespace BetterLyrics.WinUI3.Enums
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Enums
{ {
public enum AutoScanInterval public enum AutoScanInterval
{ {

View File

@@ -0,0 +1,8 @@
namespace BetterLyrics.WinUI3.Enums;
public enum EaseMode
{
In,
Out,
InOut,
}

View File

@@ -6,15 +6,15 @@ namespace BetterLyrics.WinUI3.Enums
{ {
Linear, Linear,
SmoothStep, SmoothStep,
EaseInOutSine, Sine,
EaseInOutQuad, Quad,
EaseInOutCubic, Cubic,
EaseInOutQuart, Quart,
EaseInOutQuint, Quint,
EaseInOutExpo, Expo,
EaseInOutCirc, Circle,
EaseInOutBack, Back,
EaseInOutElastic, Elastic,
EaseInOutBounce, Bounce,
} }
} }

View File

@@ -1,14 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum Language
{
FollowSystem,
English,
SimplifiedChinese,
TraditionalChinese,
Japanese,
Korean,
}
}

View File

@@ -1,10 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum LocalSearchTargetProps
{
LyricsOnly,
LyricsAndAlbumArt,
}
}

View File

@@ -14,5 +14,6 @@ namespace BetterLyrics.WinUI3.Enums
LocalEslrcFile, LocalEslrcFile,
LocalTtmlFile, LocalTtmlFile,
AppleMusic, AppleMusic,
Plugin = 999,
} }
} }

View File

@@ -1,8 +0,0 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum SettingsStoreType
{
Container,
JSON
}
}

View File

@@ -1,6 +1,6 @@
namespace BetterLyrics.WinUI3.Enums namespace BetterLyrics.WinUI3.Enums
{ {
public enum ShortcutID public enum ShortcutId
{ {
LyricsWindowShowOrHide, LyricsWindowShowOrHide,
LyricsWindowSwitch, LyricsWindowSwitch,

View File

@@ -1,8 +1,4 @@
using System; namespace BetterLyrics.WinUI3.Enums
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Enums
{ {
public enum StatsRange public enum StatsRange
{ {
@@ -11,6 +7,7 @@ namespace BetterLyrics.WinUI3.Enums
ThisMonth, ThisMonth,
ThisQuarter, ThisQuarter,
ThisYear, ThisYear,
AllTime,
Custom Custom
} }
} }

View File

@@ -13,5 +13,6 @@
LocalEslrcFile, LocalEslrcFile,
LocalTtmlFile, LocalTtmlFile,
LibreTranslate, LibreTranslate,
Plugin = 999,
} }
} }

View File

@@ -13,6 +13,7 @@
LocalEslrcFile, LocalEslrcFile,
LocalTtmlFile, LocalTtmlFile,
BetterLyrics, BetterLyrics,
CutletDocker CutletDocker,
Plugin = 999,
} }
} }

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Enums
{
public enum WordByWordEffectMode
{
Auto,
Never,
Always,
}
}

View File

@@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.Events
{
public class AlbumArtChangedEventArgs(SoftwareBitmap? albumArtSwBitmap, List<Color> albumArtLightAccentColors, List<Color> albumArtDarkAccentColors) : EventArgs
{
public SoftwareBitmap? AlbumArtSwBitmap { get; set; } = albumArtSwBitmap;
public List<Color> AlbumArtLightAccentColors { get; set; } = albumArtLightAccentColors;
public List<Color> AlbumArtDarkAccentColors { get; set; } = albumArtDarkAccentColors;
}
}

View File

@@ -1,14 +0,0 @@
// 2025/6/23 by Zhe Fang
using System;
using System.IO;
namespace BetterLyrics.WinUI3.Events
{
public class LibChangedEventArgs(string folder, string filePath, WatcherChangeTypes changeType) : EventArgs
{
public WatcherChangeTypes ChangeType { get; } = changeType;
public string FilePath { get; } = filePath;
public string Folder { get; } = folder;
}
}

View File

@@ -1,10 +0,0 @@
using BetterLyrics.WinUI3.Models;
using System;
namespace BetterLyrics.WinUI3.Events
{
public class LyricsChangedEventArgs(LyricsData? lyricsData) : EventArgs
{
public LyricsData? LyricsData { get; } = lyricsData;
}
}

View File

@@ -1,11 +0,0 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
namespace BetterLyrics.WinUI3.Events
{
public class MediaSourceProvidersInfoEventArgs(List<MediaSourceProviderInfo> sessionIds) : EventArgs
{
public List<MediaSourceProviderInfo> MediaSourceProviersInfo { get; set; } = sessionIds;
}
}

View File

@@ -1,9 +1,9 @@
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.Services.LocalizationService;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
namespace BetterLyrics.WinUI3.Extensions namespace BetterLyrics.WinUI3.Extensions
{ {
@@ -19,14 +19,27 @@ namespace BetterLyrics.WinUI3.Extensions
new LyricsLine new LyricsLine
{ {
StartMs = 0, StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds, EndMs = (int)TimeSpan.FromSeconds(30).TotalMilliseconds,
OriginalText = "● ● ●", PrimaryText = "● ● ●",
PrimarySyllables = [new BaseLyrics { Text = "● ● ●", StartMs = 0, EndMs = (int)TimeSpan.FromSeconds(30).TotalMilliseconds }],
IsPrimaryHasRealSyllableInfo = true,
}, },
], ],
LanguageCode = "N/A", LanguageCode = "N/A",
}; };
} }
public static LyricsData GetNotfoundPlaceholder()
{
return new LyricsData([new LyricsLine
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
PrimaryText = "N/A",
PrimarySyllables = [new BaseLyrics { Text = "N/A", StartMs = 0, EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds }],
}]);
}
public void SetTranslatedText(LyricsData translationData, int toleranceMs = 50) public void SetTranslatedText(LyricsData translationData, int toleranceMs = 50)
{ {
foreach (var line in lyricsData.LyricsLines) foreach (var line in lyricsData.LyricsLines)
@@ -38,12 +51,12 @@ namespace BetterLyrics.WinUI3.Extensions
if (transLine != null) if (transLine != null)
{ {
// 此处 transLine.OriginalText 指翻译中的“原文”属性 // 此处 transLine.OriginalText 指翻译中的“原文”属性
line.TranslatedText = transLine.OriginalText; line.SecondaryText = transLine.PrimaryText;
} }
else else
{ {
// 没有匹配的翻译 // 没有匹配的翻译
line.TranslatedText = ""; line.SecondaryText = "";
} }
} }
} }
@@ -59,12 +72,12 @@ namespace BetterLyrics.WinUI3.Extensions
if (transLine != null) if (transLine != null)
{ {
// 此处 transLine.OriginalText 指音译中的“原文”属性 // 此处 transLine.OriginalText 指音译中的“原文”属性
line.PhoneticText = transLine.OriginalText; line.TertiaryText = transLine.PrimaryText;
} }
else else
{ {
// 没有匹配的音译 // 没有匹配的音译
line.PhoneticText = ""; line.TertiaryText = "";
} }
} }
} }
@@ -77,11 +90,11 @@ namespace BetterLyrics.WinUI3.Extensions
{ {
if (i >= translationArr.Count) if (i >= translationArr.Count)
{ {
line.TranslatedText = ""; // No translation available, keep empty line.SecondaryText = ""; // No translation available, keep empty
} }
else else
{ {
line.TranslatedText = translationArr[i]; line.SecondaryText = translationArr[i];
} }
i++; i++;
} }
@@ -95,11 +108,11 @@ namespace BetterLyrics.WinUI3.Extensions
{ {
if (i >= transliterationArr.Count) if (i >= transliterationArr.Count)
{ {
line.PhoneticText = ""; // No transliteration available, keep empty line.TertiaryText = ""; // No transliteration available, keep empty
} }
else else
{ {
line.PhoneticText = transliterationArr[i]; line.TertiaryText = transliterationArr[i];
} }
i++; i++;
} }

View File

@@ -1,6 +1,4 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using System;
namespace BetterLyrics.WinUI3.Extensions namespace BetterLyrics.WinUI3.Extensions
{ {
@@ -8,21 +6,6 @@ namespace BetterLyrics.WinUI3.Extensions
{ {
extension(LyricsSearchProvider provider) 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 public LyricsFormat GetLyricsFormat() => provider switch
{ {
LyricsSearchProvider.LrcLib => LyricsFormat.Lrc, LyricsSearchProvider.LrcLib => LyricsFormat.Lrc,

View File

@@ -1,6 +1,5 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings; using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services.LocalizationService; using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Views; using BetterLyrics.WinUI3.Views;

View File

@@ -1,5 +1,9 @@
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Entities;
using System; using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
namespace BetterLyrics.WinUI3.Extensions namespace BetterLyrics.WinUI3.Extensions
{ {
@@ -9,7 +13,7 @@ namespace BetterLyrics.WinUI3.Extensions
{ {
Title = "N/A", Title = "N/A",
Album = "N/A", Album = "N/A",
Artists = ["N/A"], Artist = "N/A",
}; };
extension(SongInfo songInfo) extension(SongInfo songInfo)
@@ -20,9 +24,9 @@ namespace BetterLyrics.WinUI3.Extensions
return songInfo; return songInfo;
} }
public SongInfo WithArtist(string[] value) public SongInfo WithArtist(string value)
{ {
songInfo.Artists = value; songInfo.Artist = value;
return songInfo; return songInfo;
} }
@@ -32,6 +36,12 @@ namespace BetterLyrics.WinUI3.Extensions
return songInfo; return songInfo;
} }
public SongInfo WithSongId(string value)
{
songInfo.SongId = value;
return songInfo;
}
public PlayHistoryItem? ToPlayHistoryItem(double actualPlayedMs) public PlayHistoryItem? ToPlayHistoryItem(double actualPlayedMs)
{ {
if (songInfo == null) return null; if (songInfo == null) return null;
@@ -39,7 +49,7 @@ namespace BetterLyrics.WinUI3.Extensions
return new PlayHistoryItem return new PlayHistoryItem
{ {
Title = songInfo.Title, Title = songInfo.Title,
Artist = songInfo.DisplayArtists, Artist = songInfo.Artist,
Album = songInfo.Album, Album = songInfo.Album,
PlayerId = songInfo.PlayerId ?? "N/A", PlayerId = songInfo.PlayerId ?? "N/A",
TotalDurationMs = songInfo.DurationMs, TotalDurationMs = songInfo.DurationMs,
@@ -47,6 +57,23 @@ namespace BetterLyrics.WinUI3.Extensions
StartedAt = DateTime.FromBinary(songInfo.StartedAt) 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

@@ -4,7 +4,6 @@ using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Hooks; using BetterLyrics.WinUI3.Hooks;
using Microsoft.UI; using Microsoft.UI;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using SkiaSharp;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing.Imaging; using System.Drawing.Imaging;

View File

@@ -56,6 +56,57 @@
} }
catch (Exception) { } catch (Exception) { }
} }
/// <summary>
/// https://learn.microsoft.com/zh-cn/dotnet/standard/io/how-to-copy-directories
/// </summary>
/// <param name="sourceDir"></param>
/// <param name="destinationDir"></param>
/// <param name="recursive"></param>
/// <exception cref="DirectoryNotFoundException"></exception>
public static void CopyDirectory(string sourceDir, string destinationDir, bool recursive)
{
// Get information about the source directory
var dir = new DirectoryInfo(sourceDir);
// Check if the source directory exists
if (!dir.Exists)
throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}");
// Cache directories before we start copying
DirectoryInfo[] dirs = dir.GetDirectories();
// Create the destination directory
Directory.CreateDirectory(destinationDir);
// Get the files in the source directory and copy to the destination directory
foreach (FileInfo file in dir.GetFiles())
{
string targetFilePath = Path.Combine(destinationDir, file.Name);
CopyLockedFile(file.FullName, targetFilePath);
}
// If recursive and copying subdirectories, recursively call this method
if (recursive)
{
foreach (DirectoryInfo subDir in dirs)
{
string newDestinationDir = Path.Combine(destinationDir, subDir.Name);
CopyDirectory(subDir.FullName, newDestinationDir, true);
}
}
}
private static void CopyLockedFile(string sourcePath, string targetPath)
{
using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var destStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write))
{
sourceStream.CopyTo(destStream);
}
}
} }
} }
} }

View File

@@ -1,114 +1,170 @@
// 2025/6/23 by Zhe Fang // 2025/6/23 by Zhe Fang
using System; using System;
using System.Numerics;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Helper namespace BetterLyrics.WinUI3.Helper
{ {
public class EasingHelper public class EasingHelper
{ {
public static double EaseInOutSine(double t) #region Interpolators
{
return -(Math.Cos(Math.PI * t) - 1f) / 2f;
}
public static double EaseInOutQuad(double t)
{
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
public static double EaseInOutCubic(double t) public static Func<T, T, double, T> GetInterpolatorByEasingType<T>(EasingType? type, EaseMode easingMode = EaseMode.Out)
where T : INumber<T>, IFloatingPointIeee754<T>
{ {
return t < 0.5f ? 4 * t * t * t : 1 - Math.Pow(-2 * t + 2, 3) / 2; return (start, end, progress) =>
}
public static double EaseInOutQuart(double t)
{
return t < 0.5f ? 8 * t * t * t * t : 1 - Math.Pow(-2 * t + 2, 4) / 2;
}
public static double EaseInOutQuint(double t)
{
return t < 0.5f ? 16 * t * t * t * t * t : 1 - Math.Pow(-2 * t + 2, 5) / 2;
}
public static double EaseInOutExpo(double t)
{
return t == 0
? 0
: t == 1
? 1
: t < 0.5 ? Math.Pow(2, 20 * t - 10) / 2
: (2 - Math.Pow(2, -20 * t + 10)) / 2;
}
public static double EaseInOutCirc(double t)
{
return t < 0.5f
? (1 - Math.Sqrt(1 - Math.Pow(2 * t, 2))) / 2
: (Math.Sqrt(1 - Math.Pow(-2 * t + 2, 2)) + 1) / 2;
}
public static double EaseInOutBack(double t)
{
double c1 = 1.70158f;
double c2 = c1 * 1.525f;
return t < 0.5
? (Math.Pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
: (Math.Pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
}
public static double EaseInOutElastic(double t)
{
if (t == 0 || t == 1) return t;
double p = 0.3f;
double s = p / 4;
return t < 0.5f
? -(Math.Pow(2, 20 * t - 10) * Math.Sin((20 * t - 11.125f) * (2 * Math.PI) / p)) / 2
: (Math.Pow(2, -20 * t + 10) * Math.Sin((20 * t - 11.125f) * (2 * Math.PI) / p)) / 2 + 1;
}
private static double EaseOutBounce(double t)
{
if (t < 4 / 11f)
{ {
return (121 * t * t) / 16f; Func<T, T> easeInFunc = type switch
{
EasingType.Sine => EaseInSine,
EasingType.Quad => EaseInQuad,
EasingType.Cubic => EaseInCubic,
EasingType.Quart => EaseInQuart,
EasingType.Quint => EaseInQuint,
EasingType.Expo => EaseInExpo,
EasingType.Circle => EaseInCircle,
EasingType.Back => EaseInBack,
EasingType.Elastic => EaseInElastic,
EasingType.Bounce => EaseInBounce,
EasingType.SmoothStep => SmoothStep,
EasingType.Linear => Linear,
_ => EaseInQuad,
};
double t = Ease(progress, easingMode, easeInFunc);
return start + ((end - start) * T.CreateChecked(t));
};
}
#endregion
public static double Ease<T>(double t, EaseMode mode, Func<T, T> easeIn)
where T : IFloatingPointIeee754<T>
{
t = Math.Clamp(t, 0.0, 1.0);
T tt = T.CreateChecked(t);
T half = T.CreateChecked(0.5);
T two = T.CreateChecked(2);
T tResult = mode switch
{
EaseMode.In => easeIn(tt),
EaseMode.Out => T.One - easeIn(T.One - tt),
EaseMode.InOut => tt < half
? easeIn(tt * two) / two
: T.One - (easeIn((T.One - tt) * two) / two),
_ => easeIn(tt),
};
return double.CreateChecked(tResult);
}
public static T EaseInSine<T>(T t) where T : IFloatingPointIeee754<T>
{
return T.One - T.Cos((t * T.Pi) / T.CreateChecked(2));
}
public static T EaseInQuad<T>(T t) where T : INumber<T>
{
return t * t;
}
public static T EaseInCubic<T>(T t) where T : INumber<T>
{
return t * t * t;
}
public static T EaseInQuart<T>(T t) where T : INumber<T>
{
return t * t * t * t;
}
public static T EaseInQuint<T>(T t) where T : INumber<T>
{
return t * t * t * t * t;
}
public static T EaseInExpo<T>(T t) where T : IFloatingPointIeee754<T>
{
if (t == T.Zero)
{
return T.Zero;
} }
else if (t < 8 / 11f)
return T.Pow(T.CreateChecked(2), (T.CreateChecked(10) * t) - T.CreateChecked(10));
}
public static T EaseInCircle<T>(T t) where T : IFloatingPointIeee754<T>
{
return T.One - T.Sqrt(T.One - (t * t));
}
public static T EaseInBack<T>(T t) where T : IFloatingPointIeee754<T>
{
T c1 = T.CreateChecked(1.70158);
T c3 = c1 + T.One;
return (c3 * t * t * t) - (c1 * t * t);
}
public static T EaseInElastic<T>(T t) where T : IFloatingPointIeee754<T>
{
if (t == T.Zero || t == T.One)
{ {
return (363 / 40f * t * t) - (99 / 10f * t) + 17 / 5f; return t;
} }
else if (t < 9 / 10f)
const double springiness = 6;
const double oscillations = 1;
double td = double.CreateChecked(t);
double expo = (Math.Exp(springiness * td) - 1.0) / (Math.Exp(springiness) - 1.0);
double result = 0.7 * expo * Math.Sin((Math.PI * 2.0 * oscillations + (Math.PI * 0.5)) * td);
return T.CreateChecked(result);
}
private static T EaseOutBounce<T>(T t) where T : IFloatingPointIeee754<T>
{
if (t < T.CreateChecked(4.0 / 11.0))
{ {
return (4356 / 361f * t * t) - (35442 / 1805f * t) + 16061 / 1805f; return (T.CreateChecked(121) * t * t) / T.CreateChecked(16);
}
else if (t < T.CreateChecked(8.0 / 11.0))
{
return ((T.CreateChecked(363.0 / 40.0) * t * t) - (T.CreateChecked(99.0 / 10.0) * t)) + T.CreateChecked(17.0 / 5.0);
}
else if (t < T.CreateChecked(9.0 / 10.0))
{
return ((T.CreateChecked(4356.0 / 361.0) * t * t) - (T.CreateChecked(35442.0 / 1805.0) * t)) + T.CreateChecked(16061.0 / 1805.0);
} }
else else
{ {
return (54 / 5f * t * t) - (513 / 25f * t) + 268 / 25f; return ((T.CreateChecked(54.0 / 5.0) * t * t) - (T.CreateChecked(513.0 / 25.0) * t)) + T.CreateChecked(268.0 / 25.0);
} }
} }
public static double EaseInOutBounce(double t) public static T EaseInBounce<T>(T t) where T : IFloatingPointIeee754<T>
{ {
if (t < 0.5f) return T.One - EaseOutBounce(T.One - t);
{
return (1 - EaseOutBounce(1 - 2 * t)) / 2;
}
else
{
return (1 + EaseOutBounce(2 * t - 1)) / 2;
}
} }
public static double SmoothStep(double t) public static T SmoothStep<T>(T t) where T : IFloatingPointIeee754<T>
{ {
return t * t * (3f - 2f * t); return t * t * (T.CreateChecked(3) - (T.CreateChecked(2) * t));
} }
public static double CubicBezier(double t, double p0, double p1, double p2, double p3) public static T CubicBezier<T>(T t, T p0, T p1, T p2, T p3) where T : IFloatingPointIeee754<T>
{ {
double u = 1 - t; T u = T.One - t;
return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3;
return (u * u * u * p0)
+ (T.CreateChecked(3) * u * u * t * p1)
+ (T.CreateChecked(3) * u * t * t * p2)
+ (t * t * t * p3);
} }
public static double Linear(double t) => t; public static T Linear<T>(T t) where T : INumber<T> => t;
} }
} }

View File

@@ -3,7 +3,6 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions; using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Serialization;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@@ -53,22 +52,6 @@ namespace BetterLyrics.WinUI3.Helper
return sb.ToString(); 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) public static byte[]? ReadAlbumArtCache(string album, string artist, string format, string cacheFolderPath)
{ {
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}")); var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}"));
@@ -79,19 +62,9 @@ namespace BetterLyrics.WinUI3.Helper
return null; 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) 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); File.WriteAllBytes(cacheFilePath, img);
} }

View File

@@ -1,8 +1,9 @@
using System; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using BetterLyrics.WinUI3.Models; using System.Linq;
public static class FolderTreeBuilder public static class FolderTreeBuilder
{ {

View File

@@ -3,7 +3,6 @@ using BetterLyrics.WinUI3.Services.LocalizationService;
using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.DependencyInjection;
using NTextCat; using NTextCat;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Windows.Globalization; using Windows.Globalization;

View File

@@ -1,4 +1,5 @@
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Entities;
using F23.StringSimilarity; using F23.StringSimilarity;
using System; using System;
using System.IO; using System.IO;
@@ -9,29 +10,39 @@ namespace BetterLyrics.WinUI3.Helper
{ {
public static partial class MetadataComparer public static partial class MetadataComparer
{ {
private const double WeightTitle = 0.40; private const double WeightTitle = 0.30;
private const double WeightArtist = 0.40; private const double WeightArtist = 0.30;
private const double WeightAlbum = 0.10; private const double WeightAlbum = 0.10;
private const double WeightDuration = 0.10; private const double WeightDuration = 0.30;
// JaroWinkler 适合短字符串匹配 // JaroWinkler 适合短字符串匹配
private static readonly JaroWinkler _algo = new(); private static readonly JaroWinkler _algo = new();
public static int CalculateScore(SongInfo local, LyricsSearchResult remote) public static int CalculateScore(SongInfo songInfo, LyricsCacheItem remote)
{ {
if (local == null || remote == null) return 0; return CalculateScore(songInfo, remote.Title, remote.Artist, remote.Album, remote.Duration);
}
public static int CalculateScore(SongInfo songInfo, FilesIndexItem local)
{
return CalculateScore(songInfo, local.Title, local.Artist, local.Album, local.Duration, local.FileName);
}
public static int CalculateScore(
SongInfo songInfo,
string? compareTitle, string? compareArtist, string? compareAlbum, double? compareDuration, string? compareFileName = null)
{
double totalScore = 0; double totalScore = 0;
bool localHasMetadata = !string.IsNullOrWhiteSpace(local.Title); bool localHasMetadata = !string.IsNullOrWhiteSpace(songInfo.Title);
bool remoteHasMetadata = !string.IsNullOrWhiteSpace(remote.Title); bool remoteHasMetadata = !string.IsNullOrWhiteSpace(compareTitle);
if (localHasMetadata && remoteHasMetadata) if (localHasMetadata && remoteHasMetadata)
{ {
double titleScore = GetStringSimilarity(local.Title, remote.Title); double titleScore = GetStringSimilarity(songInfo.Title, compareTitle);
double artistScore = GetArtistSimilarity(local.Artists, remote.Artists); double artistScore = GetStringSimilarity(songInfo.Artist, compareArtist);
double albumScore = GetStringSimilarity(local.Album, remote.Album); double albumScore = GetStringSimilarity(songInfo.Album, compareAlbum);
double durationScore = GetDurationSimilarity(local.DurationMs, remote.Duration); double durationScore = GetDurationSimilarity(songInfo.Duration, compareDuration);
totalScore = (titleScore * WeightTitle) + totalScore = (titleScore * WeightTitle) +
(artistScore * WeightArtist) + (artistScore * WeightArtist) +
@@ -41,12 +52,12 @@ namespace BetterLyrics.WinUI3.Helper
else else
{ {
string? localQuery = localHasMetadata string? localQuery = localHasMetadata
? $"{local.Title} {string.Join(" ", local.Artists ?? [])}" ? $"{songInfo.Title} {songInfo.Artist}"
: Path.GetFileNameWithoutExtension(local.LinkedFileName); : Path.GetFileNameWithoutExtension(songInfo.LinkedFileName);
string remoteQuery = remoteHasMetadata string? remoteQuery = remoteHasMetadata
? $"{remote.Title} {string.Join(" ", remote.Artists ?? [])}" ? $"{compareTitle} {compareArtist}"
: Path.GetFileNameWithoutExtension(remote.Reference); : Path.GetFileNameWithoutExtension(compareFileName);
string fp1 = CreateSortedFingerprint(localQuery); string fp1 = CreateSortedFingerprint(localQuery);
string fp2 = CreateSortedFingerprint(remoteQuery); string fp2 = CreateSortedFingerprint(remoteQuery);
@@ -83,19 +94,18 @@ namespace BetterLyrics.WinUI3.Helper
return _algo.Similarity(s1, s2); return _algo.Similarity(s1, s2);
} }
private static double GetDurationSimilarity(double localMs, double? remoteSeconds) private static double GetDurationSimilarity(double localSeconds, double? remoteSeconds)
{ {
if (remoteSeconds == null || remoteSeconds == 0) return 0.0; // 远程没有时长数据,不匹配 if (remoteSeconds == null || remoteSeconds == 0) return 0.0; // 远程没有时长数据,不匹配
double localSeconds = localMs / 1000.0;
double diff = Math.Abs(localSeconds - remoteSeconds.Value); double diff = Math.Abs(localSeconds - remoteSeconds.Value);
// 差距 <= 3100% 相似 // 差距 <= 1 100 % 相似
// 差距 >= 200% 相似 // 差距 >= 10 0 % 相似
// 中间线性插值 // 中间线性插值
const double PerfectTolerance = 3.0; const double PerfectTolerance = 1.0;
const double MaxTolerance = 20.0; const double MaxTolerance = 10.0;
if (diff <= PerfectTolerance) return 1.0; if (diff <= PerfectTolerance) return 1.0;
if (diff >= MaxTolerance) return 0.0; if (diff >= MaxTolerance) return 0.0;

View File

@@ -40,16 +40,6 @@ namespace BetterLyrics.WinUI3.Helper
public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt"); public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt");
public static string LyricsCacheDirectory => Path.Combine(CacheFolder, "lyrics"); 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 AmllTtmlDbIndexPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-index.jsonl");
public static string AmllTtmlDbLastUpdatedPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-last-updated.txt"); public static string AmllTtmlDbLastUpdatedPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-last-updated.txt");
@@ -60,24 +50,14 @@ namespace BetterLyrics.WinUI3.Helper
public static string PlayQueuePath => Path.Combine(LocalFolder, "play-queue.m3u"); public static string PlayQueuePath => Path.Combine(LocalFolder, "play-queue.m3u");
public static string PlayHistoryPath => Path.Combine(LocalFolder, "play-history.db"); public static string PlayHistoryPath => Path.Combine(LocalFolder, "play-history.db");
public static string FilesIndexPath => Path.Combine(LocalFolder, "files-index.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() public static void EnsureDirectories()
{ {
Directory.CreateDirectory(SettingsDirectory); Directory.CreateDirectory(SettingsDirectory);
Directory.CreateDirectory(LogDirectory); Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LyricsCacheDirectory);
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(iTunesAlbumArtCacheDirectory); Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
Directory.CreateDirectory(LocalAlbumArtCacheDirectory); Directory.CreateDirectory(LocalAlbumArtCacheDirectory);
} }

View File

@@ -42,19 +42,23 @@ namespace BetterLyrics.WinUI3.Helper
return file; return file;
} }
public static async Task<StorageFile?> PickSaveFileAsync<T>(IDictionary<string, IList<string>> fileTypeChoices) public static async Task<StorageFile?> PickSaveFileAsync<T>(IDictionary<string, IList<string>> fileTypeChoices, string? suggestedFileName = null)
{ {
var window = WindowHook.GetWindow<T>(); var window = WindowHook.GetWindow<T>();
return await PickSaveFileAsync(window, fileTypeChoices); return await PickSaveFileAsync(window, fileTypeChoices, suggestedFileName);
} }
public static async Task<StorageFile?> PickSaveFileAsync<T>(T? window, IDictionary<string, IList<string>> fileTypeChoices) public static async Task<StorageFile?> PickSaveFileAsync<T>(T? window, IDictionary<string, IList<string>> fileTypeChoices, string? suggestedFileName = null)
{ {
if (window == null) return null; if (window == null) return null;
var picker = new Windows.Storage.Pickers.FileSavePicker(); var picker = new Windows.Storage.Pickers.FileSavePicker();
picker.FileTypeChoices.AddRange(fileTypeChoices); picker.FileTypeChoices.AddRange(fileTypeChoices);
if (suggestedFileName != null)
{
picker.SuggestedFileName = suggestedFileName;
}
var hwnd = WindowNative.GetWindowHandle(window); var hwnd = WindowNative.GetWindowHandle(window);
InitializeWithWindow.Initialize(picker, hwnd); InitializeWithWindow.Initialize(picker, hwnd);

View File

@@ -1,153 +1,232 @@
// 2025/6/23 by Zhe Fang using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Enums;
using System; using System;
using System.Collections.Generic;
using static BetterLyrics.WinUI3.Helper.EasingHelper;
namespace BetterLyrics.WinUI3.Helper namespace BetterLyrics.WinUI3.Helper
{ {
public class ValueTransition<T> public class ValueTransition<T> where T : struct
where T : struct
{ {
// 状态变量
private T _currentValue; private T _currentValue;
private double _durationSeconds;
private double _delaySeconds;
private double _delayRemaining;
private EasingType? _easingType;
private Func<T, T, double, T> _interpolator;
private bool _isTransitioning;
private double _progress;
private T _startValue; private T _startValue;
private T _targetValue; private T _targetValue;
public double DurationSeconds => _durationSeconds; // 核心队列
public double DelaySeconds => _delaySeconds; private readonly Queue<Keyframe<T>> _keyframeQueue = new Queue<Keyframe<T>>();
public bool IsTransitioning => _isTransitioning; // 时间控制
private double _stepDuration; // 当前这一段的时长 (动态变化)
private double _totalDurationForAutoSplit; // 自动均分模式的总时长
private double _configuredDelaySeconds; // 配置的延迟时长
// 动画状态
private Func<T, T, double, T> _interpolator;
private bool _isTransitioning;
private double _progress; // 当前段的进度 (0.0 ~ 1.0)
// 公开属性
public T Value => _currentValue; public T Value => _currentValue;
public T StartValue => _startValue; public bool IsTransitioning => _isTransitioning;
public T TargetValue => _targetValue; public T TargetValue => _targetValue; // 获取当前段的目标值
public EasingType? EasingType => _easingType; public double DurationSeconds => _totalDurationForAutoSplit;
public double Progress => _progress;
public ValueTransition(T initialValue, double durationSeconds, Func<T, T, double, T>? interpolator = null, EasingType? easingType = null, double delaySeconds = 0) public Func<T, T, double, T> Interpolator => _interpolator;
public ValueTransition(T initialValue, Func<T, T, double, T>? interpolator, double defaultTotalDuration = 0.3)
{ {
_currentValue = initialValue; _currentValue = initialValue;
_startValue = initialValue; _startValue = initialValue;
_targetValue = initialValue; _targetValue = initialValue;
_durationSeconds = durationSeconds; _totalDurationForAutoSplit = defaultTotalDuration;
_delaySeconds = delaySeconds;
_delayRemaining = 0;
_progress = 1f;
_isTransitioning = false;
if (interpolator != null) if (interpolator != null)
{ {
_interpolator = interpolator; _interpolator = interpolator;
_easingType = null;
}
else if (easingType.HasValue)
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
}
else
{
_easingType = Enums.EasingType.EaseInOutQuad;
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
} }
} }
#region Configuration
public void SetDuration(double seconds) public void SetDuration(double seconds)
{ {
if (seconds < 0) if (seconds < 0) throw new ArgumentOutOfRangeException(nameof(seconds));
throw new ArgumentOutOfRangeException(nameof(seconds), "Duration must be positive."); _totalDurationForAutoSplit = seconds;
_durationSeconds = seconds;
} }
public void SetDurationMs(double millionSeconds) => SetDuration(millionSeconds / 1000.0);
/// <summary>
/// 设置启动延迟。
/// 原理:在动画队列最前方插入一个“数值不变”的关键帧。
/// </summary>
public void SetDelay(double seconds) public void SetDelay(double seconds)
{ {
_delaySeconds = seconds; _configuredDelaySeconds = seconds;
} }
private void JumpTo(T value) public void SetInterpolator(Func<T, T, double, T> interpolator)
{ {
_interpolator = interpolator;
}
#endregion
#region Control Methods
/// <summary>
/// 立即跳转到指定值(停止动画)
/// </summary>
public void JumpTo(T value)
{
_keyframeQueue.Clear();
_currentValue = value; _currentValue = value;
_startValue = value; _startValue = value;
_targetValue = value; _targetValue = value;
_progress = 1f;
_delayRemaining = 0;
_isTransitioning = false; _isTransitioning = false;
_progress = 0;
} }
public void Reset(T value) /// <summary>
/// 模式 A: 精确控制模式
/// 显式指定每一段的目标值和时长。
/// </summary>
public void Start(params Keyframe<T>[] keyframes)
{ {
_currentValue = value; if (keyframes == null || keyframes.Length == 0) return;
_startValue = value;
_targetValue = value;
_progress = 0f;
_delayRemaining = 0;
_isTransitioning = false;
}
public void StartTransition(T targetValue, bool jumpTo = false) PrepareStart();
{
if (jumpTo) // 1. 处理延迟 (插入静止帧)
if (_configuredDelaySeconds > 0)
{ {
JumpTo(targetValue); _keyframeQueue.Enqueue(new Keyframe<T>(_currentValue, _configuredDelaySeconds));
return;
} }
if (!targetValue.Equals(_currentValue)) // 2. 入队用户帧
foreach (var kf in keyframes)
{ {
_startValue = _currentValue; _keyframeQueue.Enqueue(kf);
_targetValue = targetValue;
_progress = 0f;
_delayRemaining = _delaySeconds;
_isTransitioning = true;
} }
MoveToNextSegment(firstStart: true);
} }
public static bool Equals(double x, double y, double tolerance) /// <summary>
/// 模式 B: 自动均分模式 (兼容旧写法)
/// 指定一串目标值,系统根据 SetDuration 的总时长平均分配。
/// </summary>
public void Start(params T[] values)
{ {
var diff = Math.Abs(x - y); if (values == null || values.Length == 0) return;
return diff <= tolerance || diff <= Math.Max(Math.Abs(x), Math.Abs(y)) * tolerance;
// 如果目标就是当前值且只有1帧直接跳过以省性能
if (values.Length == 1 && values[0].Equals(_currentValue) && _configuredDelaySeconds <= 0) return;
PrepareStart();
// 1. 处理延迟
if (_configuredDelaySeconds > 0)
{
_keyframeQueue.Enqueue(new Keyframe<T>(_currentValue, _configuredDelaySeconds));
}
// 2. 计算均分时长
double autoStepDuration = _totalDurationForAutoSplit / values.Length;
// 3. 入队生成帧
foreach (var val in values)
{
_keyframeQueue.Enqueue(new Keyframe<T>(val, autoStepDuration));
}
MoveToNextSegment(firstStart: true);
}
#endregion
#region Core Logic
private void PrepareStart()
{
_keyframeQueue.Clear();
_isTransitioning = true;
}
private void MoveToNextSegment(bool firstStart = false)
{
if (_keyframeQueue.Count > 0)
{
var kf = _keyframeQueue.Dequeue();
// 起点逻辑:如果是刚开始,起点是当前值;如果是中间切换,起点是上一段的终点
_startValue = firstStart ? _currentValue : _targetValue;
_targetValue = kf.Value;
_stepDuration = kf.Duration;
if (firstStart) _progress = 0f;
// 注意:非 firstStart 时不重置 _progress保留溢出值以平滑过渡
}
else
{
// 队列耗尽,动画结束
_currentValue = _targetValue;
_isTransitioning = false;
_progress = 1f;
}
} }
public void Update(TimeSpan elapsedTime) public void Update(TimeSpan elapsedTime)
{ {
if (!_isTransitioning) return; if (!_isTransitioning) return;
if (_delayRemaining > 0) double timeStep = elapsedTime.TotalSeconds;
{
double consume = Math.Min(_delayRemaining, elapsedTime.TotalSeconds);
_delayRemaining -= consume;
if (_delayRemaining > 0)
return;
elapsedTime = TimeSpan.FromSeconds(elapsedTime.TotalSeconds - consume);
}
if (_durationSeconds <= 0) // 使用 while 处理单帧时间过长跨越多段的情况
while (timeStep > 0 && _isTransitioning)
{ {
_progress = 1f; // 计算当前帧的步进比例
} // 极小值保护防止除以0
else double progressDelta = (_stepDuration > 0.000001) ? (timeStep / _stepDuration) : 1.0;
{
_progress += elapsedTime.TotalSeconds / _durationSeconds;
}
if (_progress >= 1f) if (_progress + progressDelta >= 1.0)
{ {
_progress = 1f; // === 当前段结束 ===
_currentValue = _targetValue;
_isTransitioning = false; // 1. 计算这一段实际消耗的时间
} double timeConsumed = (1.0 - _progress) * _stepDuration;
else
{ // 2. 剩余时间留给下一段
_currentValue = _interpolator(_startValue, _targetValue, _progress); timeStep -= timeConsumed;
// 3. 修正当前值到目标值
_progress = 1.0;
_currentValue = _targetValue;
// 4. 切换到下一段
MoveToNextSegment();
// 5. 如果还有下一段,进度归零
if (_isTransitioning) _progress = 0f;
}
else
{
// === 当前段进行中 ===
_progress += progressDelta;
timeStep = 0; // 时间耗尽
// 插值计算
_currentValue = _interpolator(_startValue, _targetValue, _progress);
}
} }
} }
private Func<T, T, double, T> GetInterpolatorByEasingType(EasingType? type) #endregion
#region Interpolators
public static Func<T, T, double, T> GetInterpolatorByEasingType(EasingType? type, EaseMode easingMode)
{ {
if (typeof(T) == typeof(double)) if (typeof(T) == typeof(double))
{ {
@@ -155,59 +234,32 @@ namespace BetterLyrics.WinUI3.Helper
{ {
double s = (double)(object)start; double s = (double)(object)start;
double e = (double)(object)end; double e = (double)(object)end;
double t = progress;
switch (type) Func<double, double> easeInFunc = type switch
{ {
case Enums.EasingType.EaseInOutSine: Enums.EasingType.Sine => EaseInSine,
t = EasingHelper.EaseInOutSine(t); Enums.EasingType.Quad => EaseInQuad,
break; Enums.EasingType.Cubic => EaseInCubic,
case Enums.EasingType.EaseInOutQuad: Enums.EasingType.Quart => EaseInQuart,
t = EasingHelper.EaseInOutQuad(t); Enums.EasingType.Quint => EaseInQuint,
break; Enums.EasingType.Expo => EaseInExpo,
case Enums.EasingType.EaseInOutCubic: Enums.EasingType.Circle => EaseInCircle,
t = EasingHelper.EaseInOutCubic(t); Enums.EasingType.Back => EaseInBack,
break; Enums.EasingType.Elastic => EaseInElastic,
case Enums.EasingType.EaseInOutQuart: Enums.EasingType.Bounce => EaseInBounce,
t = EasingHelper.EaseInOutQuart(t); Enums.EasingType.SmoothStep => SmoothStep,
break; Enums.EasingType.Linear => Linear,
case Enums.EasingType.EaseInOutQuint: _ => EaseInQuad,
t = EasingHelper.EaseInOutQuint(t); };
break; double t = Ease(progress, easingMode, easeInFunc);
case Enums.EasingType.EaseInOutExpo:
t = EasingHelper.EaseInOutExpo(t);
break;
case Enums.EasingType.EaseInOutCirc:
t = EasingHelper.EaseInOutCirc(t);
break;
case Enums.EasingType.EaseInOutBack:
t = EasingHelper.EaseInOutBack(t);
break;
case Enums.EasingType.EaseInOutElastic:
t = EasingHelper.EaseInOutElastic(t);
break;
case Enums.EasingType.EaseInOutBounce:
t = EasingHelper.EaseInOutBounce(t);
break;
case Enums.EasingType.SmoothStep:
t = EasingHelper.SmoothStep(t);
break;
case Enums.EasingType.Linear:
t = EasingHelper.Linear(t);
break;
default:
t = EasingHelper.EaseInOutQuad(t);
break;
}
return (T)(object)(s + (e - s) * t); return (T)(object)(s + (e - s) * t);
}; };
} }
throw new NotSupportedException($"Easing type {type} is not supported for type {typeof(T)}.");
throw new NotSupportedException($"Type {typeof(T)} is not supported.");
} }
public void SetEasingType(EasingType? easingType) #endregion
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(easingType);
}
} }
} }

View File

@@ -1,8 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper namespace BetterLyrics.WinUI3.Helper

View File

@@ -20,7 +20,7 @@ namespace BetterLyrics.WinUI3.Hooks
/// <param name="id"></param> /// <param name="id"></param>
/// <param name="keys"></param> /// <param name="keys"></param>
/// <param name="action"></param> /// <param name="action"></param>
private static void RegisterHotKey(Window window, ShortcutID id, List<string> keys, Action action) private static void RegisterHotKey(Window window, ShortcutId id, List<string> keys, Action action)
{ {
if (keys.Count == 0) return; if (keys.Count == 0) return;
@@ -58,7 +58,7 @@ namespace BetterLyrics.WinUI3.Hooks
} }
} }
private static void UnregisterHotKey(Window window, ShortcutID id) private static void UnregisterHotKey(Window window, ShortcutId id)
{ {
HWND hwnd = WindowNative.GetWindowHandle(window); HWND hwnd = WindowNative.GetWindowHandle(window);
User32.UnregisterHotKey(hwnd, (int)id); User32.UnregisterHotKey(hwnd, (int)id);
@@ -66,13 +66,13 @@ namespace BetterLyrics.WinUI3.Hooks
_keys.Remove((int)id); _keys.Remove((int)id);
} }
public static void UpdateHotKey(Window window, ShortcutID id, List<string> keys, Action action) public static void UpdateHotKey(Window window, ShortcutId id, List<string> keys, Action action)
{ {
UnregisterHotKey(window, id); UnregisterHotKey(window, id);
RegisterHotKey(window, id, keys, action); RegisterHotKey(window, id, keys, action);
} }
public static bool IsHotKeyRegistered(ShortcutID id) public static bool IsHotKeyRegistered(ShortcutId id)
{ {
return _actions.ContainsKey((int)id); return _actions.ContainsKey((int)id);
} }
@@ -82,7 +82,7 @@ namespace BetterLyrics.WinUI3.Hooks
return _keys.ContainsValue(keys); return _keys.ContainsValue(keys);
} }
public static bool TryInvokeAction(ShortcutID id) public static bool TryInvokeAction(ShortcutId id)
{ {
return TryInvokeAction((int)id); return TryInvokeAction((int)id);
} }

View File

@@ -7,7 +7,6 @@ using FlaUI.Core.EventHandlers;
using FlaUI.UIA3; using FlaUI.UIA3;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using System; using System;
using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Threading; using System.Threading;

View File

@@ -1,7 +1,7 @@
// 2025/6/23 by Zhe Fang // 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Views; using BetterLyrics.WinUI3.Views;
using CommunityToolkit.WinUI; using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;

View File

@@ -1,6 +1,8 @@
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.Models.Settings; using BetterLyrics.WinUI3.Models.Settings;
using DevWinUI;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -17,7 +19,7 @@ namespace BetterLyrics.WinUI3.Logic
IList<RenderLyricsLine>? lines, IList<RenderLyricsLine>? lines,
int startIndex, int startIndex,
int endIndex, int endIndex,
int playingLineIndex, int primaryPlayingLineIndex,
double canvasHeight, double canvasHeight,
double targetYScrollOffset, double targetYScrollOffset,
double playingLineTopOffsetFactor, double playingLineTopOffsetFactor,
@@ -29,38 +31,84 @@ namespace BetterLyrics.WinUI3.Logic
TimeSpan elapsedTime, TimeSpan elapsedTime,
bool isMouseScrolling, bool isMouseScrolling,
bool isLayoutChanged, bool isLayoutChanged,
bool isPlayingLineChanged, bool isPrimaryPlayingLineChanged,
bool isMouseScrollingChanged bool isMouseScrollingChanged,
double currentPositionMs
) )
{ {
if (lines == null) return; if (lines == null || lines.Count == 0) return;
var currentPlayingLine = lines.ElementAtOrDefault(playingLineIndex); if (primaryPlayingLineIndex < 0 || primaryPlayingLineIndex >= lines.Count) return;
if (currentPlayingLine == null) return; var primaryPlayingLine = lines[primaryPlayingLineIndex];
var phoneticOpacity = lyricsStyle.PhoneticLyricsOpacity / 100.0; var phoneticOpacity = lyricsStyle.PhoneticLyricsOpacity / 100.0;
var originalOpacity = lyricsStyle.OriginalLyricsOpacity / 100.0; var originalOpacity = lyricsStyle.OriginalLyricsOpacity / 100.0;
var translatedOpacity = lyricsStyle.TranslatedLyricsOpacity / 100.0; var translatedOpacity = lyricsStyle.TranslatedLyricsOpacity / 100.0;
for (int i = startIndex; i <= endIndex + 1; i++) double topHeightFactor = canvasHeight * playingLineTopOffsetFactor;
double bottomHeightFactor = canvasHeight * (1 - playingLineTopOffsetFactor);
double scrollTopDurationSec = lyricsEffect.LyricsScrollTopDuration / 1000.0;
double scrollTopDelaySec = lyricsEffect.LyricsScrollTopDelay / 1000.0;
double scrollBottomDurationSec = lyricsEffect.LyricsScrollBottomDuration / 1000.0;
double scrollBottomDelaySec = lyricsEffect.LyricsScrollBottomDelay / 1000.0;
double canvasTransDuration = canvasYScrollTransition.DurationSeconds;
bool isBlurEnabled = lyricsEffect.IsLyricsBlurEffectEnabled;
bool isOutOfSightEnabled = lyricsEffect.IsLyricsOutOfSightEffectEnabled;
bool isFanEnabled = lyricsEffect.IsFanLyricsEnabled;
double fanAngleRad = Math.PI * (lyricsEffect.FanLyricsAngle / 180.0);
bool isGlowEnabled = lyricsEffect.IsLyricsGlowEffectEnabled;
bool isFloatEnabled = lyricsEffect.IsLyricsFloatAnimationEnabled;
bool isScaleEnabled = lyricsEffect.IsLyricsScaleEffectEnabled;
int safeStart = Math.Max(0, startIndex);
int safeEnd = Math.Min(lines.Count - 1, endIndex + 1);
for (int i = safeStart; i <= safeEnd; i++)
{ {
var line = lines.ElementAtOrDefault(i); var line = lines[i];
if (line == null) continue; var lineHeight = line.PrimaryLineHeight;
if (lineHeight == null || lineHeight <= 0) continue;
if (isLayoutChanged || isPlayingLineChanged || isMouseScrollingChanged) bool isWordAnimationEnabled = lyricsEffect.WordByWordEffectMode switch
{ {
int lineCountDelta = i - playingLineIndex; Enums.WordByWordEffectMode.Auto => line.IsPrimaryHasRealSyllableInfo,
int absLineCountDelta = Math.Abs(lineCountDelta); Enums.WordByWordEffectMode.Always => true,
double distanceFromPlayingLine = Math.Abs(line.OriginalPosition.Y - currentPlayingLine.OriginalPosition.Y); Enums.WordByWordEffectMode.Never => false,
_ => line.IsPrimaryHasRealSyllableInfo
};
double distanceFactor = 0; double targetCharFloat = lyricsEffect.IsLyricsFloatAnimationAmountAutoAdjust
? lineHeight.Value * 0.1
: lyricsEffect.LyricsFloatAnimationAmount;
double targetCharGlow = lyricsEffect.IsLyricsGlowEffectAmountAutoAdjust
? lineHeight.Value * 0.2
: lyricsEffect.LyricsGlowEffectAmount;
double targetCharScale = lyricsEffect.IsLyricsScaleEffectAmountAutoAdjust
? 1.15
: lyricsEffect.LyricsScaleEffectAmount / 100.0;
var maxAnimationDurationMs = Math.Max(line.EndMs ?? 0 - currentPositionMs, 0);
bool isSecondaryLinePlaying = line.GetIsPlaying(currentPositionMs);
bool isSecondaryLinePlayingChanged = line.IsPlayingLastFrame != isSecondaryLinePlaying;
line.IsPlayingLastFrame = isSecondaryLinePlaying;
// 行动画
if (isLayoutChanged || isPrimaryPlayingLineChanged || isMouseScrollingChanged)
{
int lineCountDelta = i - primaryPlayingLineIndex;
double distanceFromPlayingLine = Math.Abs(line.PrimaryPosition.Y - primaryPlayingLine.PrimaryPosition.Y);
double distanceFactor;
if (lineCountDelta < 0) if (lineCountDelta < 0)
{ {
distanceFactor = Math.Clamp(distanceFromPlayingLine / (canvasHeight * playingLineTopOffsetFactor), 0, 1); distanceFactor = Math.Clamp(distanceFromPlayingLine / topHeightFactor, 0, 1);
} }
else else
{ {
distanceFactor = Math.Clamp(distanceFromPlayingLine / (canvasHeight * (1 - playingLineTopOffsetFactor)), 0, 1); distanceFactor = Math.Clamp(distanceFromPlayingLine / bottomHeightFactor, 0, 1);
} }
double yScrollDuration; double yScrollDuration;
@@ -69,83 +117,179 @@ namespace BetterLyrics.WinUI3.Logic
if (lineCountDelta < 0) if (lineCountDelta < 0)
{ {
yScrollDuration = yScrollDuration =
canvasYScrollTransition.DurationSeconds + canvasTransDuration +
distanceFactor * (lyricsEffect.LyricsScrollTopDuration / 1000.0 - canvasYScrollTransition.DurationSeconds); distanceFactor * (scrollTopDurationSec - canvasTransDuration);
yScrollDelay = distanceFactor * lyricsEffect.LyricsScrollTopDelay / 1000.0; yScrollDelay = distanceFactor * scrollTopDelaySec;
} }
else if (lineCountDelta == 0) else if (lineCountDelta == 0)
{ {
yScrollDuration = canvasYScrollTransition.DurationSeconds; yScrollDuration = canvasTransDuration;
yScrollDelay = 0; yScrollDelay = 0;
} }
else else
{ {
yScrollDuration = yScrollDuration =
canvasYScrollTransition.DurationSeconds + canvasTransDuration +
distanceFactor * (lyricsEffect.LyricsScrollBottomDuration / 1000.0 - canvasYScrollTransition.DurationSeconds); distanceFactor * (scrollBottomDurationSec - canvasTransDuration);
yScrollDelay = distanceFactor * lyricsEffect.LyricsScrollBottomDelay / 1000.0; yScrollDelay = distanceFactor * scrollBottomDelaySec;
} }
line.BlurAmountTransition.SetDuration(yScrollDuration); line.BlurAmountTransition.SetDuration(yScrollDuration);
line.BlurAmountTransition.SetDelay(yScrollDelay); line.BlurAmountTransition.SetDelay(yScrollDelay);
line.BlurAmountTransition.StartTransition(isMouseScrolling ? 0 : (lyricsEffect.IsLyricsBlurEffectEnabled ? (5 * distanceFactor) : 0)); line.BlurAmountTransition.Start(
(isMouseScrolling || isSecondaryLinePlaying) ? 0 :
(isBlurEnabled ? (5 * distanceFactor) : 0));
line.ScaleTransition.SetDuration(yScrollDuration); line.ScaleTransition.SetDuration(yScrollDuration);
line.ScaleTransition.SetDelay(yScrollDelay); line.ScaleTransition.SetDelay(yScrollDelay);
line.ScaleTransition.StartTransition( line.ScaleTransition.Start(
lyricsEffect.IsLyricsOutOfSightEffectEnabled ? isSecondaryLinePlaying ? _highlightedScale :
(isOutOfSightEnabled ?
(_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale)) : (_highlightedScale - distanceFactor * (_highlightedScale - _defaultScale)) :
_highlightedScale); _highlightedScale));
line.PhoneticOpacityTransition.SetDuration(yScrollDuration); line.PhoneticOpacityTransition.SetDuration(yScrollDuration);
line.PhoneticOpacityTransition.SetDelay(yScrollDelay); line.PhoneticOpacityTransition.SetDelay(yScrollDelay);
line.PhoneticOpacityTransition.StartTransition( line.PhoneticOpacityTransition.Start(
isSecondaryLinePlaying ? phoneticOpacity :
CalculateTargetOpacity(phoneticOpacity, phoneticOpacity, distanceFactor, isMouseScrolling, lyricsEffect)); CalculateTargetOpacity(phoneticOpacity, phoneticOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
// 原文不透明度(已播放)
line.PlayedOriginalOpacityTransition.SetDuration(yScrollDuration); line.PlayedOriginalOpacityTransition.SetDuration(yScrollDuration);
line.PlayedOriginalOpacityTransition.SetDelay(yScrollDelay); line.PlayedOriginalOpacityTransition.SetDelay(yScrollDelay);
line.PlayedOriginalOpacityTransition.StartTransition( line.PlayedOriginalOpacityTransition.Start(
isSecondaryLinePlaying ? 1.0 :
CalculateTargetOpacity(originalOpacity, 1.0, distanceFactor, isMouseScrolling, lyricsEffect)); CalculateTargetOpacity(originalOpacity, 1.0, distanceFactor, isMouseScrolling, lyricsEffect));
// 原文不透明度(未播放)
line.UnplayedOriginalOpacityTransition.SetDuration(yScrollDuration); line.UnplayedOriginalOpacityTransition.SetDuration(yScrollDuration);
line.UnplayedOriginalOpacityTransition.SetDelay(yScrollDelay); line.UnplayedOriginalOpacityTransition.SetDelay(yScrollDelay);
line.UnplayedOriginalOpacityTransition.StartTransition( line.UnplayedOriginalOpacityTransition.Start(
isSecondaryLinePlaying ? originalOpacity :
CalculateTargetOpacity(originalOpacity, originalOpacity, distanceFactor, isMouseScrolling, lyricsEffect)); CalculateTargetOpacity(originalOpacity, originalOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
line.TranslatedOpacityTransition.SetDuration(yScrollDuration); line.TranslatedOpacityTransition.SetDuration(yScrollDuration);
line.TranslatedOpacityTransition.SetDelay(yScrollDelay); line.TranslatedOpacityTransition.SetDelay(yScrollDelay);
line.TranslatedOpacityTransition.StartTransition( line.TranslatedOpacityTransition.Start(
isSecondaryLinePlaying ? translatedOpacity :
CalculateTargetOpacity(translatedOpacity, translatedOpacity, distanceFactor, isMouseScrolling, lyricsEffect)); CalculateTargetOpacity(translatedOpacity, translatedOpacity, distanceFactor, isMouseScrolling, lyricsEffect));
line.ColorTransition.SetDuration(yScrollDuration); line.ColorTransition.SetDuration(yScrollDuration);
line.ColorTransition.SetDelay(yScrollDelay); line.ColorTransition.SetDelay(yScrollDelay);
line.ColorTransition.StartTransition(absLineCountDelta == 0 ? fgColor : bgColor); line.ColorTransition.Start(isSecondaryLinePlaying ? fgColor : bgColor);
line.AngleTransition.SetEasingType(canvasYScrollTransition.EasingType); line.AngleTransition.SetInterpolator(canvasYScrollTransition.Interpolator);
line.AngleTransition.SetDuration(yScrollDuration); line.AngleTransition.SetDuration(yScrollDuration);
line.AngleTransition.SetDelay(yScrollDelay); line.AngleTransition.SetDelay(yScrollDelay);
line.AngleTransition.StartTransition( line.AngleTransition.Start(
(lyricsEffect.IsFanLyricsEnabled && !isMouseScrolling) ? (isFanEnabled && !isMouseScrolling) ?
Math.PI * (lyricsEffect.FanLyricsAngle / 180.0) * distanceFactor * (i > playingLineIndex ? 1 : -1) : fanAngleRad * distanceFactor * (i > primaryPlayingLineIndex ? 1 : -1) :
0); 0);
line.YOffsetTransition.SetEasingType(canvasYScrollTransition.EasingType); line.YOffsetTransition.SetInterpolator(canvasYScrollTransition.Interpolator);
line.YOffsetTransition.SetDuration(yScrollDuration); line.YOffsetTransition.SetDuration(yScrollDuration);
line.YOffsetTransition.SetDelay(yScrollDelay); line.YOffsetTransition.SetDelay(yScrollDelay);
// 设计之初是当 isLayoutChanged 为真时 jumpTo // 设计之初是当 isLayoutChanged 为真时 jumpTo
// 但考虑到动画视觉,强制使用动画 // 但考虑到动画视觉,强制使用动画
line.YOffsetTransition.StartTransition(targetYScrollOffset); line.YOffsetTransition.Start(targetYScrollOffset);
} }
line.AngleTransition.Update(elapsedTime); if (isWordAnimationEnabled)
line.ScaleTransition.Update(elapsedTime); {
line.BlurAmountTransition.Update(elapsedTime); if (isSecondaryLinePlayingChanged)
line.PhoneticOpacityTransition.Update(elapsedTime); {
line.PlayedOriginalOpacityTransition.Update(elapsedTime); // 辉光动画
line.UnplayedOriginalOpacityTransition.Update(elapsedTime); if (isGlowEnabled && lyricsEffect.LyricsGlowEffectScope == Enums.LyricsEffectScope.LineStartToCurrentChar
line.TranslatedOpacityTransition.Update(elapsedTime); && isSecondaryLinePlaying)
line.YOffsetTransition.Update(elapsedTime); {
line.ColorTransition.Update(elapsedTime); foreach (var renderChar in line.PrimaryRenderChars)
{
var stepInOutDuration = Math.Min(Time.AnimationDuration.TotalMilliseconds, maxAnimationDurationMs) / 2.0 / 1000.0;
var stepLastingDuration = Math.Max(maxAnimationDurationMs / 1000.0 - stepInOutDuration * 2, 0);
renderChar.GlowTransition.Start(
new Models.Keyframe<double>(targetCharGlow, stepInOutDuration),
new Models.Keyframe<double>(targetCharGlow, stepLastingDuration),
new Models.Keyframe<double>(0, stepInOutDuration)
);
}
}
// 浮动动画
if (isFloatEnabled)
{
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.FloatTransition.Start(isSecondaryLinePlaying ? targetCharFloat : 0);
}
}
}
// 字符动画
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.ProgressPlayed = renderChar.GetPlayProgress(currentPositionMs);
bool isCharPlaying = renderChar.GetIsPlaying(currentPositionMs);
bool isCharPlayingChanged = renderChar.IsPlayingLastFrame != isCharPlaying;
if (isCharPlayingChanged)
{
if (isFloatEnabled)
{
renderChar.FloatTransition.SetDurationMs(Math.Min(lyricsEffect.LyricsFloatAnimationDuration, maxAnimationDurationMs));
renderChar.FloatTransition.Start(0);
}
renderChar.IsPlayingLastFrame = isCharPlaying;
}
}
// 音节动画
foreach (var syllable in line.PrimaryRenderSyllables)
{
bool isSyllablePlaying = syllable.GetIsPlaying(currentPositionMs);
bool isSyllablePlayingChanged = syllable.IsPlayingLastFrame != isSyllablePlaying;
if (isSyllablePlayingChanged)
{
if (isScaleEnabled && isSyllablePlaying)
{
foreach (var renderChar in syllable.ChildrenRenderLyricsChars)
{
if (syllable.DurationMs >= lyricsEffect.LyricsScaleEffectLongSyllableDuration)
{
var stepDuration = Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0 / 1000.0;
renderChar.ScaleTransition.Start(
new Models.Keyframe<double>(targetCharScale, stepDuration),
new Models.Keyframe<double>(1.0, stepDuration)
);
}
}
}
if (isGlowEnabled && isSyllablePlaying && lyricsEffect.LyricsGlowEffectScope == Enums.LyricsEffectScope.LongDurationSyllable
&& syllable.DurationMs >= lyricsEffect.LyricsGlowEffectLongSyllableDuration)
{
foreach (var renderChar in syllable.ChildrenRenderLyricsChars)
{
var stepDuration = Math.Min(syllable.DurationMs, maxAnimationDurationMs) / 2.0 / 1000.0;
renderChar.GlowTransition.Start(
new Models.Keyframe<double>(targetCharGlow, stepDuration),
new Models.Keyframe<double>(0, stepDuration)
);
}
}
syllable.IsPlayingLastFrame = isSyllablePlaying;
}
}
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.Update(elapsedTime);
}
}
line.Update(elapsedTime);
} }
} }

View File

@@ -1,5 +1,5 @@
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Models.Lyrics;
using BetterLyrics.WinUI3.Models.Settings; using BetterLyrics.WinUI3.Models.Settings;
using Microsoft.Graphics.Canvas.UI.Xaml; using Microsoft.Graphics.Canvas.UI.Xaml;
using System; using System;
@@ -79,50 +79,52 @@ namespace BetterLyrics.WinUI3.Logic
// 左上角坐标 // 左上角坐标
line.TopLeftPosition = new Vector2(0, (float)currentY); line.TopLeftPosition = new Vector2(0, (float)currentY);
// 注音层 // 注音层
line.PhoneticPosition = line.TopLeftPosition; line.TertiaryPosition = line.TopLeftPosition;
if (line.PhoneticCanvasTextLayout != null) if (line.TertiaryTextLayout != null)
{ {
currentY += line.PhoneticCanvasTextLayout.LayoutBounds.Height; currentY += line.TertiaryTextLayout.LayoutBounds.Height;
// 间距 // 间距
currentY += (line.PhoneticCanvasTextLayout.LayoutBounds.Height / line.PhoneticCanvasTextLayout.LineCount) * 0.1; currentY += (line.TertiaryTextLayout.LayoutBounds.Height / line.TertiaryTextLayout.LineCount) * 0.1;
actualWidth = Math.Max(actualWidth, line.PhoneticCanvasTextLayout.LayoutBounds.Width); actualWidth = Math.Max(actualWidth, line.TertiaryTextLayout.LayoutBounds.Width);
} }
// 原文层 // 原文层
line.OriginalPosition = new Vector2(0, (float)currentY); line.PrimaryPosition = new Vector2(0, (float)currentY);
if (line.OriginalCanvasTextLayout != null) if (line.PrimaryTextLayout != null)
{ {
currentY += line.OriginalCanvasTextLayout.LayoutBounds.Height; currentY += line.PrimaryTextLayout.LayoutBounds.Height;
actualWidth = Math.Max(actualWidth, line.OriginalCanvasTextLayout.LayoutBounds.Width); actualWidth = Math.Max(actualWidth, line.PrimaryTextLayout.LayoutBounds.Width);
} }
// 翻译层 // 翻译层
if (line.TranslatedCanvasTextLayout != null) if (line.SecondaryTextLayout != null)
{ {
// 间距 // 间距
currentY += (line.TranslatedCanvasTextLayout.LayoutBounds.Height / line.TranslatedCanvasTextLayout.LineCount) * 0.1; currentY += (line.SecondaryTextLayout.LayoutBounds.Height / line.SecondaryTextLayout.LineCount) * 0.1;
} }
line.TranslatedPosition = new Vector2(0, (float)currentY); line.SecondaryPosition = new Vector2(0, (float)currentY);
if (line.TranslatedCanvasTextLayout != null) if (line.SecondaryTextLayout != null)
{ {
currentY += line.TranslatedCanvasTextLayout.LayoutBounds.Height; currentY += line.SecondaryTextLayout.LayoutBounds.Height;
actualWidth = Math.Max(actualWidth, line.TranslatedCanvasTextLayout.LayoutBounds.Width); actualWidth = Math.Max(actualWidth, line.SecondaryTextLayout.LayoutBounds.Width);
} }
// 右下角坐标 // 右下角坐标
line.BottomRightPosition = new Vector2(0 + (float)actualWidth, (float)currentY); line.BottomRightPosition = new Vector2(0 + (float)actualWidth, (float)currentY);
// 行间距 // 行间距
if (line.OriginalCanvasTextLayout != null) if (line.PrimaryTextLayout != null)
{ {
currentY += (line.OriginalCanvasTextLayout.LayoutBounds.Height / line.OriginalCanvasTextLayout.LineCount) * style.LyricsLineSpacingFactor; currentY += (line.PrimaryTextLayout.LayoutBounds.Height / line.PrimaryTextLayout.LineCount) * style.LyricsLineSpacingFactor;
} }
// 更新中心点 // 更新中心点
line.UpdateCenterPosition(lyricsWidth, style.LyricsAlignmentType); line.UpdateCenterPosition(lyricsWidth, style.LyricsAlignmentType);
line.RecreateRenderChars();
} }
} }
@@ -138,9 +140,9 @@ namespace BetterLyrics.WinUI3.Logic
var currentLine = lines.ElementAtOrDefault(playingLineIndex); var currentLine = lines.ElementAtOrDefault(playingLineIndex);
var firstLine = lines.FirstOrDefault(); var firstLine = lines.FirstOrDefault();
if (currentLine?.OriginalCanvasTextLayout == null || firstLine == null) return null; if (currentLine?.PrimaryTextLayout == null || firstLine == null) return null;
return -currentLine.OriginalPosition.Y + firstLine.OriginalPosition.Y return -currentLine.PrimaryPosition.Y + firstLine.PrimaryPosition.Y
- (currentLine.BottomRightPosition.Y - currentLine.TopLeftPosition.Y) / 2.0; - (currentLine.BottomRightPosition.Y - currentLine.TopLeftPosition.Y) / 2.0;
} }
@@ -187,6 +189,37 @@ namespace BetterLyrics.WinUI3.Logic
return lines.Last().BottomRightPosition.Y; 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( public static int FindMouseHoverLineIndex(
IList<RenderLyricsLine>? lines, IList<RenderLyricsLine>? lines,
bool isMouseInLyricsArea, bool isMouseInLyricsArea,
@@ -208,7 +241,7 @@ namespace BetterLyrics.WinUI3.Logic
{ {
int mid = (left + right) / 2; int mid = (left + right) / 2;
var line = lines[mid]; var line = lines[mid];
if (line.OriginalCanvasTextLayout == null) break; if (line.PrimaryTextLayout == null) break;
double value = offset + line.BottomRightPosition.Y; double value = offset + line.BottomRightPosition.Y;
if (value >= mousePosition.Y) { result = mid; right = mid - 1; } if (value >= mousePosition.Y) { result = mid; right = mid - 1; }
else { left = mid + 1; } else { left = mid + 1; }
@@ -234,7 +267,7 @@ namespace BetterLyrics.WinUI3.Logic
{ {
int mid = (left + right) / 2; int mid = (left + right) / 2;
var line = lines[mid]; var line = lines[mid];
if (line.OriginalCanvasTextLayout == null) break; if (line.PrimaryTextLayout == null) break;
double value = offset + line.BottomRightPosition.Y; double value = offset + line.BottomRightPosition.Y;
// 理论上说应该使用下面这一行来精确计算视野内的首个可见行,但是考虑到动画视觉效果,还是注释掉了 // 理论上说应该使用下面这一行来精确计算视野内的首个可见行,但是考虑到动画视觉效果,还是注释掉了
//if (value >= lyricsY) { result = mid; right = mid - 1; } //if (value >= lyricsY) { result = mid; right = mid - 1; }
@@ -251,7 +284,7 @@ namespace BetterLyrics.WinUI3.Logic
{ {
int mid = (left + right) / 2; int mid = (left + right) / 2;
var line = lines[mid]; var line = lines[mid];
if (line.OriginalCanvasTextLayout == null) break; if (line.PrimaryTextLayout == null) break;
double value = offset + line.BottomRightPosition.Y; double value = offset + line.BottomRightPosition.Y;
// 同理 // 同理
//if (value >= lyricsY + lyricsHeight) { result = mid; right = mid - 1; } //if (value >= lyricsY + lyricsHeight) { result = mid; right = mid - 1; }

View File

@@ -1,4 +1,6 @@
using BetterLyrics.WinUI3.Models; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Lyrics;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -13,48 +15,66 @@ namespace BetterLyrics.WinUI3.Logic
_lastFoundIndex = 0; _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; if (lines == null || lines.Count == 0) return 0;
var lines = lyricsData.LyricsLines;
// Cache hit if (_lastFoundIndex >= 0 && _lastFoundIndex < lines.Count)
if (IsTimeInLine(currentTimeMs, lines, _lastFoundIndex)) return _lastFoundIndex;
if (_lastFoundIndex + 1 < lines.Count && IsTimeInLine(currentTimeMs, lines, _lastFoundIndex + 1))
{ {
_lastFoundIndex++; var lastLine = lines[_lastFoundIndex];
return _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++) for (int i = 0; i < lines.Count; i++)
{ {
if (IsTimeInLine(currentTimeMs, lines, i)) if (IsTimeInLine(currentTimeMs, lines, i))
{ {
_lastFoundIndex = i; var currentLine = lines[i];
return 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); return Math.Min(_lastFoundIndex, lines.Count - 1);
} }
public LinePlaybackState GetLinePlayingProgress( public LinePlaybackState GetLinePlayingProgress(
double currentTimeMs, double currentTimeMs,
LyricsLine line, RenderLyricsLine line,
LyricsLine? nextLine, WordByWordEffectMode wordByWordEffectMode)
double songDurationMs,
bool isForceWordByWord)
{ {
var state = new LinePlaybackState { SyllableStartIndex = 0, SyllableLength = 0, SyllableProgress = 0 }; var state = new LinePlaybackState { SyllableStartIndex = 0, SyllableLength = 0, SyllableProgress = 0 };
if (line == null) return state; if (line == null) return state;
double lineEndMs; double lineEndMs = line.EndMs ?? 0;
if (line.EndMs != null) lineEndMs = line.EndMs.Value;
else if (nextLine != null) lineEndMs = nextLine.StartMs;
else lineEndMs = songDurationMs;
// 还没到 // 还没到
if (currentTimeMs < line.StartMs) return state; if (currentTimeMs < line.StartMs) return state;
@@ -63,42 +83,53 @@ namespace BetterLyrics.WinUI3.Logic
if (currentTimeMs > lineEndMs) if (currentTimeMs > lineEndMs)
{ {
state.SyllableProgress = 1f; state.SyllableProgress = 1f;
state.SyllableStartIndex = Math.Max(0, line.OriginalText.Length - 1); state.SyllableStartIndex = Math.Max(0, line.PrimaryText.Length - 1);
state.SyllableLength = 1; state.SyllableLength = 1;
return state; return state;
} }
// 逐字 switch (wordByWordEffectMode)
if (line.LyricsSyllables != null && line.LyricsSyllables.Count > 1)
{ {
return CalculateSyllableProgress(currentTimeMs, line, lineEndMs); case WordByWordEffectMode.Auto:
} if (line.IsPrimaryHasRealSyllableInfo)
{
// 强制逐字 return CalculateSyllableProgress(currentTimeMs, line, lineEndMs);
if (isForceWordByWord && line.OriginalText.Length > 0) }
{ else
return CalculateSimulatedProgress(currentTimeMs, line, lineEndMs); {
} state.SyllableStartIndex = line.PrimaryText.Length;
else state.SyllableProgress = 1f;
{ return state;
// 普通行 }
state.SyllableStartIndex = line.OriginalText.Length; case WordByWordEffectMode.Never:
state.SyllableProgress = 1f; state.SyllableStartIndex = line.PrimaryText.Length;
return state; state.SyllableProgress = 1f;
return state;
case WordByWordEffectMode.Always:
if (line.IsPrimaryHasRealSyllableInfo)
{
return CalculateSyllableProgress(currentTimeMs, line, lineEndMs);
}
else
{
return CalculateSimulatedProgress(currentTimeMs, line, lineEndMs);
}
default:
return state;
} }
} }
private LinePlaybackState CalculateSyllableProgress(double time, LyricsLine line, double lineEndMs) private LinePlaybackState CalculateSyllableProgress(double time, RenderLyricsLine line, double lineEndMs)
{ {
var state = new LinePlaybackState(); var state = new LinePlaybackState();
int count = line.LyricsSyllables.Count; int count = line.PrimaryRenderSyllables.Count;
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
var timing = line.LyricsSyllables[i]; var timing = line.PrimaryRenderSyllables[i];
var nextTiming = (i + 1 < count) ? line.LyricsSyllables[i + 1] : null; var nextTiming = (i + 1 < count) ? line.PrimaryRenderSyllables[i + 1] : null;
double timingEndMs = timing.EndMs ?? nextTiming?.StartMs ?? lineEndMs; double timingEndMs = timing.EndMs ?? 0;
// 在当前字范围内 // 在当前字范围内
if (time >= timing.StartMs && time <= timingEndMs) if (time >= timing.StartMs && time <= timingEndMs)
@@ -122,10 +153,10 @@ namespace BetterLyrics.WinUI3.Logic
return state; return state;
} }
private LinePlaybackState CalculateSimulatedProgress(double time, LyricsLine line, double lineEndMs) private LinePlaybackState CalculateSimulatedProgress(double time, RenderLyricsLine line, double lineEndMs)
{ {
var state = new LinePlaybackState(); var state = new LinePlaybackState();
int textLength = line.OriginalText.Length; int textLength = line.PrimaryText.Length;
double progress = (time - line.StartMs) / (lineEndMs - line.StartMs); double progress = (time - line.StartMs) / (lineEndMs - line.StartMs);
progress = Math.Clamp(progress, 0, 1); progress = Math.Clamp(progress, 0, 1);
@@ -140,7 +171,7 @@ namespace BetterLyrics.WinUI3.Logic
return state; 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; if (index < 0 || index >= lines.Count) return false;
var line = lines[index]; var line = lines[index];

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
using BetterLyrics.WinUI3.Models.Entities;
using Microsoft.EntityFrameworkCore;
namespace BetterLyrics.WinUI3.Models.DbContext
{
public partial class FilesIndexDbContext : Microsoft.EntityFrameworkCore.DbContext
{
public FilesIndexDbContext(DbContextOptions<FilesIndexDbContext> options) : base(options) { }
public DbSet<FilesIndexItem> FilesIndex { get; set; }
}
}

View File

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

View File

@@ -0,0 +1,12 @@
using BetterLyrics.WinUI3.Models.Entities;
using Microsoft.EntityFrameworkCore;
namespace BetterLyrics.WinUI3.Models.DbContext
{
public partial class PlayHistoryDbContext : Microsoft.EntityFrameworkCore.DbContext
{
public PlayHistoryDbContext(DbContextOptions<PlayHistoryDbContext> options) : base(options) { }
public DbSet<PlayHistoryItem> PlayHistory { get; set; }
}
}

View File

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

View File

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

View File

@@ -1,14 +1,22 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using NTextCat.Commons; using Microsoft.EntityFrameworkCore;
using System; using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace BetterLyrics.WinUI3.Models 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; } public LyricsSearchProvider Provider { get; set; }
[ObservableProperty] public partial TranslationSearchProvider? TranslationProvider { get; set; } [ObservableProperty] public partial TranslationSearchProvider? TranslationProvider { get; set; }
[ObservableProperty] public partial TransliterationSearchProvider? TransliterationProvider { get; set; } [ObservableProperty] public partial TransliterationSearchProvider? TransliterationProvider { get; set; }
@@ -25,33 +33,38 @@ namespace BetterLyrics.WinUI3.Models
/// </summary> /// </summary>
public string? Transliteration { get; set; } public string? Transliteration { get; set; }
[MaxLength(255)]
public string? Title { get; set; } 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 string? Album { get; set; }
public double? Duration { get; set; } public double? Duration { get; set; }
[ObservableProperty] public partial int MatchPercentage { get; set; } = -1; [ObservableProperty] public partial int MatchPercentage { get; set; } = -1;
[ObservableProperty] public partial string Reference { get; set; } = "about:blank"; [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); [NotMapped][JsonIgnore] public LyricsSearchProvider? ProviderIfFound => IsFound ? Provider : null;
[JsonIgnore] public LyricsSearchProvider? ProviderIfFound => IsFound ? Provider : null;
[JsonIgnore] public string? DisplayArtists => Artists?.Join("; ");
public object Clone() public object Clone()
{ {
return new LyricsSearchResult() return new LyricsCacheItem()
{ {
Album = this.Album, Provider = this.Provider,
Duration = this.Duration, TranslationProvider = this.TranslationProvider,
TransliterationProvider = this.TransliterationProvider,
Raw = this.Raw, Raw = this.Raw,
Translation = this.Translation, Translation = this.Translation,
Transliteration = this.Transliteration,
Title = this.Title, Title = this.Title,
Artists = this.Artists, Artist = this.Artist,
Album = this.Album,
Duration = this.Duration,
MatchPercentage = this.MatchPercentage, MatchPercentage = this.MatchPercentage,
Provider = this.Provider,
Reference = this.Reference Reference = this.Reference
}; };
} }
@@ -59,7 +72,7 @@ namespace BetterLyrics.WinUI3.Models
public void CopyFromSongInfo(SongInfo songInfo) public void CopyFromSongInfo(SongInfo songInfo)
{ {
Title = songInfo.Title; Title = songInfo.Title;
Artists = songInfo.Artists; Artist = songInfo.Artist;
Album = songInfo.Album; Album = songInfo.Album;
} }
} }

View File

@@ -1,10 +1,18 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.EntityFrameworkCore;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BetterLyrics.WinUI3.Models 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 OriginalTitle { get; set; } = string.Empty;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string OriginalArtist { get; set; } = string.Empty; [ObservableProperty][NotifyPropertyChangedRecipients] public partial string OriginalArtist { get; set; } = string.Empty;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string OriginalAlbum { 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; } [ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsSearchProvider? LyricsSearchProvider { get; set; }
public MappedSongSearchQuery Clone() public object Clone()
{ {
return new MappedSongSearchQuery return new MappedSongSearchQuery
{ {

View File

@@ -3,7 +3,7 @@ using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
namespace BetterLyrics.WinUI3.Models namespace BetterLyrics.WinUI3.Models.Entities
{ {
[Index(nameof(Title))] [Index(nameof(Title))]
[Index(nameof(Artist))] [Index(nameof(Artist))]

View File

@@ -1,5 +1,6 @@
using ATL; using ATL;
using BetterLyrics.WinUI3.Helper; using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models.Entities;
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -129,7 +130,7 @@ namespace BetterLyrics.WinUI3.Models
this.Uri = entity.Uri; this.Uri = entity.Uri;
this.Title = entity.Title; this.Title = entity.Title;
this.Artist = entity.Artists; this.Artist = entity.Artist;
this.Album = entity.Album; this.Album = entity.Album;
this.Year = entity.Year; this.Year = entity.Year;
this.Bitrate = entity.Bitrate; this.Bitrate = entity.Bitrate;

View File

@@ -1,9 +1,6 @@
using BetterLyrics.WinUI3.Enums; using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Text;
namespace BetterLyrics.WinUI3.Models namespace BetterLyrics.WinUI3.Models
{ {

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace BetterLyrics.WinUI3.Models namespace BetterLyrics.WinUI3.Models.Http
{ {
public class CutletDockerRequest public class CutletDockerRequest
{ {

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace BetterLyrics.WinUI3.Models namespace BetterLyrics.WinUI3.Models.Http
{ {
public class CutletDockerResponse public class CutletDockerResponse
{ {

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace BetterLyrics.WinUI3.Models namespace BetterLyrics.WinUI3.Models.Http
{ {
public class LibreTranslateResponse public class LibreTranslateResponse
{ {

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models
{
public struct Keyframe<T>
{
public T Value { get; }
public double Duration { get; }
public Keyframe(T value, double durationSeconds)
{
Value = value;
Duration = durationSeconds;
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Lyrics
{
public class BaseLyrics
{
public int StartMs { get; set; }
public int? EndMs { get; set; } = null;
public int DurationMs => Math.Max((EndMs ?? 0) - StartMs, 0);
public string Text { get; set; } = "";
public int Length => Text.Length;
public int StartIndex { get; set; }
public int EndIndex => StartIndex + Length - 1;
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models.Lyrics
{
public class BaseRenderLyrics : BaseLyrics
{
public bool IsPlayingLastFrame { get; set; } = false;
public BaseRenderLyrics(BaseLyrics baseLyrics)
{
this.Text = baseLyrics.Text;
this.StartMs = baseLyrics.StartMs;
this.EndMs = baseLyrics.EndMs;
this.StartIndex = baseLyrics.StartIndex;
}
public bool GetIsPlaying(double currentMs) => this.StartMs <= currentMs && currentMs < this.EndMs;
public double GetPlayProgress(double currentMs) => Math.Clamp((currentMs - this.StartMs) / this.DurationMs, 0, 1);
}
}

View File

@@ -5,12 +5,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace BetterLyrics.WinUI3.Models namespace BetterLyrics.WinUI3.Models.Lyrics
{ {
public class LyricsData public class LyricsData
{ {
private static readonly ILocalizationService _localizationService = Ioc.Default.GetRequiredService<ILocalizationService>();
public List<LyricsLine> LyricsLines { get; set; } = []; public List<LyricsLine> LyricsLines { get; set; } = [];
public string? LanguageCode public string? LanguageCode
{ {
@@ -18,7 +16,7 @@ namespace BetterLyrics.WinUI3.Models
set => field = value; set => field = value;
} }
public bool AutoGenerated { get; set; } = false; public bool AutoGenerated { get; set; } = false;
public string WrappedOriginalText => string.Join(StringHelper.NewLine, LyricsLines.Select(line => line.OriginalText)); public string WrappedOriginalText => string.Join(StringHelper.NewLine, LyricsLines.Select(line => line.PrimaryText));
public LyricsData() public LyricsData()
{ {
@@ -29,15 +27,5 @@ namespace BetterLyrics.WinUI3.Models
LyricsLines = lyricsLines; LyricsLines = lyricsLines;
} }
public static LyricsData GetNotfoundPlaceholder()
{
return new LyricsData([new LyricsLine
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
OriginalText = _localizationService.GetLocalizedString("LyricsNotFound"),
}]);
}
} }
} }

View File

@@ -0,0 +1,51 @@
// 2025/6/23 by Zhe Fang
using System.Collections.Generic;
using System.Linq;
namespace BetterLyrics.WinUI3.Models.Lyrics
{
public class LyricsLine : BaseLyrics
{
public List<BaseLyrics> PrimarySyllables { get; set; } = [];
public List<BaseLyrics> SecondarySyllables { get; set; } = [];
public List<BaseLyrics> TertiarySyllables { get; set; } = [];
public List<BaseLyrics> PrimaryChars { get; private set; } = [];
public List<BaseLyrics> SecondaryChars { get; private set; } = [];
public List<BaseLyrics> TertiaryChars { get; private set; } = [];
public string PrimaryText { get; set; } = "";
public string SecondaryText { get; set; } = "";
public string TertiaryText { get; set; } = "";
public new string Text => PrimaryText;
public new int StartIndex = 0;
public bool IsPrimaryHasRealSyllableInfo { get; set; } = false;
public LyricsLine()
{
for (int charStartIndex = 0; charStartIndex < PrimaryText.Length; charStartIndex++)
{
var syllable = PrimarySyllables.FirstOrDefault(x => x.StartIndex <= charStartIndex && charStartIndex <= x.EndIndex);
if (syllable == null) continue;
var avgCharDuration = syllable.DurationMs / syllable.Length;
if (avgCharDuration == 0) continue;
var charStartMs = syllable.StartMs + (charStartIndex - syllable.StartIndex) * avgCharDuration;
var charEndMs = charStartMs + avgCharDuration;
PrimaryChars.Add(new BaseLyrics
{
StartIndex = charStartIndex,
StartMs = charStartMs,
EndMs = charEndMs,
Text = PrimaryText[charStartIndex].ToString()
});
}
}
}
}

View File

@@ -0,0 +1,47 @@
using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Helper;
using System;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Models.Lyrics
{
public class RenderLyricsChar : BaseRenderLyrics
{
public Rect LayoutRect { get; private set; }
public ValueTransition<double> ScaleTransition { get; set; }
public ValueTransition<double> GlowTransition { get; set; }
public ValueTransition<double> FloatTransition { get; set; }
public double ProgressPlayed { get; set; } = 0; // 0~1
public RenderLyricsChar(BaseLyrics lyricsChars, Rect layoutRect) : base(lyricsChars)
{
ScaleTransition = new(
initialValue: 1.0,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: Time.AnimationDuration.TotalSeconds
);
GlowTransition = new(
initialValue: 0,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: Time.AnimationDuration.TotalSeconds
);
FloatTransition = new(
initialValue: 0,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: Time.LongAnimationDuration.TotalSeconds
);
LayoutRect = layoutRect;
}
public void Update(TimeSpan elapsedTime)
{
ScaleTransition.Update(elapsedTime);
GlowTransition.Update(elapsedTime);
FloatTransition.Update(elapsedTime);
}
}
}

View File

@@ -0,0 +1,305 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
using BetterLyrics.WinUI3.Helper;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Windows.UI;
namespace BetterLyrics.WinUI3.Models.Lyrics
{
public class RenderLyricsLine : BaseRenderLyrics
{
public List<RenderLyricsChar> PrimaryRenderChars { get; private set; } = [];
public List<RenderLyricsSyllable> PrimaryRenderSyllables { get; private set; }
public double AnimationDuration { get; set; } = 0.3;
public ValueTransition<double> AngleTransition { get; set; }
public ValueTransition<double> BlurAmountTransition { get; set; }
public ValueTransition<double> PhoneticOpacityTransition { get; set; }
public ValueTransition<double> PlayedOriginalOpacityTransition { get; set; }
public ValueTransition<double> UnplayedOriginalOpacityTransition { get; set; }
public ValueTransition<double> TranslatedOpacityTransition { get; set; }
public ValueTransition<double> ScaleTransition { get; set; }
public ValueTransition<double> YOffsetTransition { get; set; }
public ValueTransition<Color> ColorTransition { get; set; }
public CanvasTextLayout? PrimaryTextLayout { get; private set; }
public CanvasTextLayout? SecondaryTextLayout { get; private set; }
public CanvasTextLayout? TertiaryTextLayout { get; private set; }
/// <summary>
/// 原文坐标(相对于坐标原点)
/// </summary>
public Vector2 PrimaryPosition { get; set; }
/// <summary>
/// 译文坐标(相对于坐标原点)
/// </summary>
public Vector2 SecondaryPosition { get; set; }
/// <summary>
/// 注音坐标(相对于坐标原点)
/// </summary>
public Vector2 TertiaryPosition { get; set; }
/// <summary>
/// 顶部坐标(相对于坐标原点)
/// </summary>
public Vector2 TopLeftPosition { get; set; }
/// <summary>
/// 中心坐标(相对于坐标原点)
/// </summary>
public Vector2 CenterPosition { get; private set; }
/// <summary>
/// 底部坐标(相对于坐标原点)
/// </summary>
public Vector2 BottomRightPosition { get; set; }
public CanvasGeometry? PrimaryCanvasGeometry { get; private set; }
public CanvasGeometry? SecondaryCanvasGeometry { get; private set; }
public CanvasGeometry? TertiaryCanvasGeometry { get; private set; }
public string PrimaryText { get; set; } = "";
public string SecondaryText { get; set; } = "";
public string TertiaryText { get; set; } = "";
/// <summary>
/// 轨道索引 (0 = 主轨道, 1 = 第一副轨道, etc.)
/// 用于布局计算时的堆叠逻辑
/// </summary>
public int LaneIndex { get; set; } = 0;
public double? PrimaryLineHeight => PrimaryRenderChars.FirstOrDefault()?.LayoutRect.Height;
public bool IsPrimaryHasRealSyllableInfo { get; set; }
public RenderLyricsLine(LyricsLine lyricsLine) : base(lyricsLine)
{
AngleTransition = new(
initialValue: 0,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
BlurAmountTransition = new(
initialValue: 0,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
PhoneticOpacityTransition = new(
initialValue: 0,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
PlayedOriginalOpacityTransition = new(
initialValue: 0,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
UnplayedOriginalOpacityTransition = new(
initialValue: 0,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
TranslatedOpacityTransition = new(
initialValue: 0,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
ScaleTransition = new(
initialValue: 0,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
YOffsetTransition = new(
initialValue: 0,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
ColorTransition = new(
initialValue: Colors.Transparent,
defaultTotalDuration: 0.3f,
interpolator: (from, to, progress) => Helper.ColorHelper.GetInterpolatedColor(progress, from, to)
);
StartMs = lyricsLine.StartMs;
EndMs = lyricsLine.EndMs;
TertiaryText = lyricsLine.TertiaryText;
PrimaryText = lyricsLine.PrimaryText;
SecondaryText = lyricsLine.SecondaryText;
PrimaryRenderSyllables = lyricsLine.PrimarySyllables.Select(x => new RenderLyricsSyllable(x)).ToList();
IsPrimaryHasRealSyllableInfo = lyricsLine.IsPrimaryHasRealSyllableInfo;
}
public void UpdateCenterPosition(double maxWidth, TextAlignmentType type)
{
if (PrimaryTextLayout == null)
{
return;
}
double centerY = (TopLeftPosition.Y + BottomRightPosition.Y) / 2;
CenterPosition = type switch
{
TextAlignmentType.Left => new Vector2(0, (float)centerY),
TextAlignmentType.Center => new Vector2((float)(0 + maxWidth / 2.0), (float)centerY),
TextAlignmentType.Right => new Vector2((float)(0 + maxWidth), (float)centerY),
_ => throw new System.ArgumentOutOfRangeException(nameof(type), type, null),
};
}
public void DisposeTextLayout()
{
TertiaryTextLayout?.Dispose();
TertiaryTextLayout = null;
PrimaryTextLayout?.Dispose();
PrimaryTextLayout = null;
SecondaryTextLayout?.Dispose();
SecondaryTextLayout = null;
}
public void RecreateTextLayout(
ICanvasAnimatedControl control,
bool createPhonetic, bool createTranslated,
int phoneticTextFontSize, int originalTextFontSize, int translatedTextFontSize,
LyricsFontWeight fontWeight,
string fontFamilyCJK, string fontFamilyWestern,
double maxWidth, double maxHeight, TextAlignmentType type)
{
DisposeTextLayout();
if (createPhonetic && TertiaryText != "")
{
TertiaryTextLayout = new CanvasTextLayout(control, TertiaryText, new CanvasTextFormat
{
HorizontalAlignment = CanvasHorizontalAlignment.Left,
VerticalAlignment = CanvasVerticalAlignment.Top,
FontSize = phoneticTextFontSize,
FontWeight = fontWeight.ToFontWeight(),
}, (float)maxWidth, (float)maxHeight)
{
HorizontalAlignment = type.ToCanvasHorizontalAlignment(),
};
TertiaryTextLayout.SetFontFamily(TertiaryText, fontFamilyCJK, fontFamilyWestern);
}
PrimaryTextLayout = new CanvasTextLayout(control, PrimaryText, new CanvasTextFormat
{
HorizontalAlignment = CanvasHorizontalAlignment.Left,
VerticalAlignment = CanvasVerticalAlignment.Top,
FontSize = originalTextFontSize,
FontWeight = fontWeight.ToFontWeight(),
}, (float)maxWidth, (float)maxHeight)
{
HorizontalAlignment = type.ToCanvasHorizontalAlignment()
};
PrimaryTextLayout.SetFontFamily(PrimaryText, fontFamilyCJK, fontFamilyWestern);
if (createTranslated && SecondaryText != "")
{
SecondaryTextLayout = new CanvasTextLayout(control, SecondaryText, new CanvasTextFormat
{
HorizontalAlignment = CanvasHorizontalAlignment.Left,
VerticalAlignment = CanvasVerticalAlignment.Top,
FontSize = translatedTextFontSize,
FontWeight = fontWeight.ToFontWeight(),
}, (float)maxWidth, (float)maxHeight)
{
HorizontalAlignment = type.ToCanvasHorizontalAlignment()
};
SecondaryTextLayout.SetFontFamily(SecondaryText, fontFamilyCJK, fontFamilyWestern);
}
}
public void DisposeTextGeometry()
{
TertiaryCanvasGeometry?.Dispose();
TertiaryCanvasGeometry = null;
PrimaryCanvasGeometry?.Dispose();
PrimaryCanvasGeometry = null;
SecondaryCanvasGeometry?.Dispose();
SecondaryCanvasGeometry = null;
}
public void RecreateTextGeometry()
{
DisposeTextGeometry();
if (TertiaryTextLayout != null)
{
TertiaryCanvasGeometry = CanvasGeometry.CreateText(TertiaryTextLayout);
}
if (PrimaryTextLayout != null)
{
PrimaryCanvasGeometry = CanvasGeometry.CreateText(PrimaryTextLayout);
}
if (SecondaryTextLayout != null)
{
SecondaryCanvasGeometry = CanvasGeometry.CreateText(SecondaryTextLayout);
}
}
public void RecreateRenderChars()
{
PrimaryRenderChars.Clear();
if (PrimaryTextLayout == null) return;
foreach (var syllable in PrimaryRenderSyllables)
{
syllable.ChildrenRenderLyricsChars.Clear();
}
var textLength = PrimaryText.Length;
for (int startCharIndex = 0; startCharIndex < textLength; startCharIndex++)
{
var region = PrimaryTextLayout.GetCharacterRegions(startCharIndex, 1).FirstOrDefault();
var bounds = region.LayoutBounds;
var syllable = PrimaryRenderSyllables.FirstOrDefault(x => x.StartIndex <= startCharIndex && startCharIndex <= x.EndIndex);
if (syllable == null) continue;
var avgCharDuration = syllable.DurationMs / syllable.Length;
var charStartMs = syllable.StartMs + (startCharIndex - syllable.StartIndex) * avgCharDuration;
var charEndMs = charStartMs + avgCharDuration;
var renderLyricsChar = new RenderLyricsChar(new BaseLyrics
{
StartIndex = startCharIndex,
Text = PrimaryText[startCharIndex].ToString(),
StartMs = charStartMs,
EndMs = charEndMs,
}, bounds);
syllable.ChildrenRenderLyricsChars.Add(renderLyricsChar);
PrimaryRenderChars.Add(renderLyricsChar);
}
}
public void Update(TimeSpan elapsedTime)
{
AngleTransition.Update(elapsedTime);
ScaleTransition.Update(elapsedTime);
BlurAmountTransition.Update(elapsedTime);
PhoneticOpacityTransition.Update(elapsedTime);
PlayedOriginalOpacityTransition.Update(elapsedTime);
UnplayedOriginalOpacityTransition.Update(elapsedTime);
TranslatedOpacityTransition.Update(elapsedTime);
YOffsetTransition.Update(elapsedTime);
ColorTransition.Update(elapsedTime);
}
}
}

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