Compare commits

...

36 Commits

Author SHA1 Message Date
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
153 changed files with 4471 additions and 1319 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,13 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Interfaces\" />
</ItemGroup>
<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

@@ -1,7 +0,0 @@
namespace BetterLyrics.Core
{
public class Class1
{
}
}

View File

@@ -0,0 +1,12 @@
using BetterLyrics.Core.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.Core.Interfaces
{
public interface ILyricsSearchPlugin : IPlugin
{
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
{
public interface ILyricsTransliterationPlugin : IPlugin
{
Task<string?> GetTransliterationAsync(string text, string targetLangCode);
}
}

View File

@@ -0,0 +1,17 @@
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; }
void Initialize();
}
}

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

@@ -0,0 +1,135 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<Content Include="customizeDict.txt">
<PackagePath>contentFiles\any\any\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
<Content Include="unidic\AUTHORS">
<PackagePath>contentFiles\any\any\unidic\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
<Content Include="unidic\BSD">
<PackagePath>contentFiles\any\any\unidic\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
<Content Include="unidic\ChangeLog">
<PackagePath>contentFiles\any\any\unidic\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
<Content Include="unidic\char.bin">
<PackagePath>contentFiles\any\any\unidic\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
<Content Include="unidic\COPYING">
<PackagePath>contentFiles\any\any\unidic\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
<Content Include="unidic\dicrc">
<PackagePath>contentFiles\any\any\unidic\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
<Content Include="unidic\GPL">
<PackagePath>contentFiles\any\any\unidic\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
<Content Include="unidic\INSTALL">
<PackagePath>contentFiles\any\any\unidic\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
<Content Include="unidic\LGPL">
<PackagePath>contentFiles\any\any\unidic\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
<Content Include="unidic\matrix.bin">
<PackagePath>contentFiles\any\any\unidic\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
<Content Include="unidic\sys.dic">
<PackagePath>contentFiles\any\any\unidic\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
<Content Include="unidic\unidic-mecab.pdf">
<PackagePath>contentFiles\any\any\unidic\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
<Content Include="unidic\unk.dic">
<PackagePath>contentFiles\any\any\unidic\</PackagePath>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
<Pack>true</Pack>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BetterLyrics.Core\BetterLyrics.Core.csproj" />
<ProjectReference Include="..\RomajiConverter.Core\RomajiConverter.Core.csproj" />
</ItemGroup>
<Target Name="AutoExcludeSharedAssemblies" AfterTargets="ResolveAssemblyReferences">
<PropertyGroup>
<HostOutputDir>..\BetterLyrics.WinUI3\BetterLyrics.WinUI3\bin\x64\$(Configuration)\$(TargetFramework)\</HostOutputDir>
</PropertyGroup>
<Message Text="[Debug] Searching for Host Assemblies in: $(HostOutputDir)" Importance="High" />
<ItemGroup>
<FilesToCopy Include="@(ReferenceCopyLocalPaths)" />
<SharedFiles Include="@(FilesToCopy)"
Condition="Exists('$(HostOutputDir)%(Filename)%(Extension)')" />
<ReferenceCopyLocalPaths Remove="@(SharedFiles)" />
</ItemGroup>
<Message Text="[Smart Trim] Excluded shared assemblies:%0a@(SharedFiles->' -> %(Filename)%(Extension)', '%0a')" Importance="High" Condition="'@(SharedFiles)' != ''" />
</Target>
<Target Name="RunPluginAnalyzer" AfterTargets="Build">
<PropertyGroup>
<AnalyzerPath>..\PluginAnalyzer\bin\Debug\net10.0\PluginAnalyzer.exe</AnalyzerPath>
<ScanDir>$(TargetDir)</ScanDir>
<Ns>BetterLyrics.WinUI3</Ns>
<Prefix>$(ProjectName)</Prefix>
<OutputDir>..\BetterLyrics.WinUI3\BetterLyrics.WinUI3\PluginConfigs\</OutputDir>
</PropertyGroup>
<Message Text="[Analyzer] Delivering configs to Main App..." Importance="High" />
<Exec Command="&quot;$(AnalyzerPath)&quot; &quot;$(ScanDir)\&quot; &quot;$(Ns)&quot; &quot;$(Prefix)&quot; &quot;$(OutputDir)\&quot;" />
</Target>
</Project>

View File

@@ -0,0 +1,6 @@
## Before you build...
- Download `unidic-mecab-2.1.2_bin.zip` from https://clrd.ninjal.ac.jp/unidic_archive/cwj/2.1.2/
- Unzip it and copy the contents of the `dic` folder to `BetterLyrics.Plugins.Romaji\unidic`
See the original info at https://github.com/xyh20180101/RomajiConverter.WinUI/blob/main/doc/README-dev-en.md

View File

@@ -0,0 +1,34 @@
using BetterLyrics.Core.Interfaces;
using RomajiConverter.Core.Helpers;
using System.Reflection;
namespace BetterLyrics.Plugins.Romaji
{
public class RomajiPlugin : ILyricsTransliterationPlugin
{
public string Id => "jayfunc.romaji";
public string Name => "Romaji";
public string Description => "Convert Japanese lyrics to Romaji transliteration.";
public string Author => "jayfunc";
public void Initialize()
{
string? pluginPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
RomajiHelper.Init(pluginPath);
}
public Task<string?> GetTransliterationAsync(string text, string targetLangCode)
{
string? result = null;
if (targetLangCode == "ja-latin")
{
var lines = text.Split("\n");
result = string.Join("\n", lines.Select(p => string.Join(" ", RomajiHelper.ToRomaji(p).FirstOrDefault()?.Units.Select(q => q.Romaji) ?? [""])));
}
return Task.FromResult(result);
}
}
}

View File

@@ -0,0 +1 @@
私 わたし

View File

@@ -143,7 +143,7 @@
</ProjectReference>
</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" />
</ItemGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />

View File

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

View File

@@ -10,6 +10,7 @@ using BetterLyrics.WinUI3.Services.LocalizationService;
using BetterLyrics.WinUI3.Services.LyricsCacheService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.PlayHistoryService;
using BetterLyrics.WinUI3.Services.PluginService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SMTCService;
using BetterLyrics.WinUI3.Services.SongSearchMapService;
@@ -22,7 +23,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle; // 关键App生命周期管理
using Microsoft.Windows.AppLifecycle;
using Serilog;
using System;
using System.Linq;
@@ -154,6 +155,10 @@ namespace BetterLyrics.WinUI3
}
fileSystemService.StartAllFolderTimers();
// Ensure plugins
var pluginService = Ioc.Default.GetRequiredService<IPluginService>();
pluginService.LoadPlugins();
// Init system tray
m_window = WindowHook.OpenOrShowWindow<SystemTrayWindow>();
@@ -248,6 +253,7 @@ namespace BetterLyrics.WinUI3
.AddSingleton<IPlayHistoryService, PlayHistoryService>()
.AddSingleton<ILyricsCacheService, LyricsCacheService>()
.AddSingleton<ISongSearchMapService, SongSearchMapService>()
.AddSingleton<IPluginService, PluginService>()
// ViewModels
.AddSingleton<AppSettingsControlViewModel>()

View File

@@ -45,6 +45,7 @@
<None Remove="Controls\PatronControl.xaml" />
<None Remove="Controls\PlaybackSettingsControl.xaml" />
<None Remove="Controls\PlayQueue.xaml" />
<None Remove="Controls\PluginManagerControl.xaml" />
<None Remove="Controls\PropertyRow.xaml" />
<None Remove="Controls\RemoteServerConfigControl.xaml" />
<None Remove="Controls\ShortcutTextBox.xaml" />
@@ -82,7 +83,7 @@
<PackageReference Include="CommunityToolkit.WinUI.Triggers" Version="8.2.251219" />
<PackageReference Include="ComputeSharp.D2D1.WinUI" Version="3.2.0" />
<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="F23.StringSimilarity" Version="7.0.1" />
<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.Relational" 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.Logging" Version="10.0.1" />
<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="NAudio.Wasapi" Version="2.2.1" />
<PackageReference Include="NTextCat" Version="0.3.65" />
@@ -118,11 +123,12 @@
<PackageReference Include="VCollab.DiscordRichPresence" Version="1.7.0" />
<PackageReference Include="WebDav.Client" 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>
<ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" />
<ProjectReference Include="..\..\Impressionist\Impressionist\Impressionist.csproj" />
<ProjectReference Include="..\..\BetterLyrics.Core\BetterLyrics.Core.csproj" />
<ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" />
<ProjectReference Include="..\..\Impressionist\Impressionist\Impressionist.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml">
@@ -134,252 +140,245 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup>
<TrimmerRootAssembly Include="FlaUI.UIA3" />
<TrimmerRootAssembly Include="Interop.UIAutomationClient" />
<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" />
<TrimmerRootDescriptor Include="PluginConfigs\**\*_TrimmerRoots.xml" />
<TrimmerRootDescriptor Include="Core_TrimmerRoots.xml" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\AIMP.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AlbumArtPlaceholder.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Alipay.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AMLLPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AppleMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Chrome.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Edge.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\FluidEffect.bin">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Empty.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EmptyBox.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EmptyState.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Folder.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\foobar2000.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\HyPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\iTunes.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\KugouMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\LastFM.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Leaf.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Listen1.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Logo.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Logo.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\LXMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\MediaPlayerWindows11.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\MoeKoeMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\MusicBee.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\NetEaseCloudMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\OriginalSoundHQPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Page.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\PlanetMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\PotPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\QQMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Question.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\RevolvingHearts.gif">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\SaltPlayerForWindows.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Segoe Fluent Icons.ttf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Spotify.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\WeChatReward.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Wiki82.profile.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AIMP.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AlbumArtPlaceholder.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Alipay.jpg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AMLLPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\AppleMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Chrome.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Edge.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\FluidEffect.bin">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Empty.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EmptyBox.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EmptyState.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Folder.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\foobar2000.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\HyPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\iTunes.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\KugouMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\LastFM.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Leaf.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Listen1.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Logo.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Logo.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\LXMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\MediaPlayerWindows11.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\MoeKoeMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\MusicBee.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\NetEaseCloudMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\OriginalSoundHQPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Page.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\PlanetMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\PotPlayer.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\QQMusic.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Question.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\RevolvingHearts.gif">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\SaltPlayerForWindows.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Segoe Fluent Icons.ttf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Spotify.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\WeChatReward.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Wiki82.profile.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PatronControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\PluginManagerControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\GhostSliderStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\PatronControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\Converters.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\GhostSliderStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PlayQueue.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\Converters.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\StatsDashboardControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\PlayQueue.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\FontFamilyAutoSuggestBox.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\StatsDashboardControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsStyleSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\FontFamilyAutoSuggestBox.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\LyricsWindowSwitchWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\LyricsStyleSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsWindowSwitchControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Views\LyricsWindowSwitchWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\DemoWindowGrid.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\LyricsWindowSwitchControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsWindowSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\DemoWindowGrid.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\LyricsSearchWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\LyricsWindowSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsSearchControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Views\LyricsSearchWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\ShortcutTextBox.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\LyricsSearchControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\ExtendedSlider.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\ShortcutTextBox.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsEffectSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\ExtendedSlider.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\MediaSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\LyricsEffectSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PlaybackSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\MediaSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\AlbumArtAreaStyleSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\PlaybackSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\LyricsBackgroundSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\AlbumArtAreaStyleSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\AppSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\LyricsBackgroundSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\MusicGalleryWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\AppSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\MusicGalleryPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Views\MusicGalleryWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\MusicGalleryPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\SettingsWindow.xaml">
@@ -392,54 +391,57 @@
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\RemoteServerConfigControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\RemoteServerConfigControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\NowPlayingBar.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\NowPlayingBar.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\SystemTrayWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Views\SystemTrayWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\WindowSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\WindowSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\AlbumArtAreaEffectSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\AlbumArtAreaEffectSettingsControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\ImageSwitcher.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\ImageSwitcher.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\ShadowImage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\ShadowImage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PropertyRow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\PropertyRow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\AboutControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Controls\AboutControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\InteractiveListViewHeaderStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\InteractiveListViewHeaderStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Folder Include="PluginConfigs\" />
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>

View File

@@ -63,6 +63,7 @@ namespace BetterLyrics.WinUI3.Controls
);
private readonly ValueTransition<double> _immersiveBgOpacityTransition = new(
initialValue: 1f,
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: 0.3f
);
private readonly ValueTransition<Color> _accentColor1Transition = new(
@@ -87,13 +88,13 @@ namespace BetterLyrics.WinUI3.Controls
);
private readonly ValueTransition<double> _canvasYScrollTransition = new(
initialValue: 0f,
defaultTotalDuration: 0.3f,
defaultEasingType: EasingType.EaseInOutSine
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: 0.3f
);
private readonly ValueTransition<double> _mouseYScrollTransition = new(
initialValue: 0f,
defaultTotalDuration: 0.3f,
defaultEasingType: EasingType.EaseInOutSine
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: 0.3f
);
private TimeSpan _songPositionWithOffset;
@@ -492,7 +493,7 @@ namespace BetterLyrics.WinUI3.Controls
else
{
_canvasYScrollTransition.SetDurationMs(lyricsEffect.LyricsScrollDuration);
_canvasYScrollTransition.SetEasingType(lyricsEffect.LyricsScrollEasingType);
_canvasYScrollTransition.SetInterpolator(EasingHelper.GetInterpolatorByEasingType<double>(lyricsEffect.LyricsScrollEasingType, lyricsEffect.LyricsScrollEasingMode));
_canvasYScrollTransition.Start(_canvasTargetScrollOffset);
}
}

View File

@@ -196,18 +196,25 @@
<ComboBox SelectedIndex="{x:Bind LyricsEffectSettings.LyricsScrollEasingType, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
<ComboBoxItem x:Uid="SettingsPageEasingTypeLinear" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeSmoothStep" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutSine" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutQuad" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutCubic" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutQuart" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutQuint" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutExpo" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutCirc" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutBack" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutElastic" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseInOutBounce" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseSine" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseQuad" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseCubic" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseQuart" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseQuint" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseExpo" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseCirc" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseBack" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseElastic" />
<ComboBoxItem x:Uid="SettingsPageEasingTypeEaseBounce" />
</ComboBox>
<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">
<local:ExtendedSlider
Default="500"

View File

@@ -424,30 +424,6 @@
</dev:SettingsExpander>
<dev:SettingsExpander x:Uid="SettingsPageJapanese" IsExpanded="{x:Bind ViewModel.AppSettings.TranslationSettings.IsJapaneseRomanizationEnabled, Mode=OneWay}">
<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>
<!-- 中文简体繁体偏好 -->

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

@@ -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

@@ -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,
SmoothStep,
EaseInOutSine,
EaseInOutQuad,
EaseInOutCubic,
EaseInOutQuart,
EaseInOutQuint,
EaseInOutExpo,
EaseInOutCirc,
EaseInOutBack,
EaseInOutElastic,
EaseInOutBounce,
Sine,
Quad,
Cubic,
Quart,
Quint,
Expo,
Circle,
Back,
Elastic,
Bounce,
}
}

View File

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

View File

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

View File

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

View File

@@ -19,9 +19,10 @@ namespace BetterLyrics.WinUI3.Extensions
new LyricsLine
{
StartMs = 0,
EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds,
EndMs = (int)TimeSpan.FromSeconds(30).TotalMilliseconds,
PrimaryText = "● ● ●",
PrimarySyllables = [new BaseLyrics { Text = "● ● ●", StartMs = 0, EndMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds }],
PrimarySyllables = [new BaseLyrics { Text = "● ● ●", StartMs = 0, EndMs = (int)TimeSpan.FromSeconds(30).TotalMilliseconds }],
IsPrimaryHasRealSyllableInfo = true,
},
],
LanguageCode = "N/A",

View File

@@ -1,114 +1,170 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Numerics;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Helper
{
public class EasingHelper
{
public static double EaseInOutSine(double t)
{
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;
}
#region Interpolators
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;
}
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 (start, end, progress) =>
{
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
{
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 (1 - EaseOutBounce(1 - 2 * t)) / 2;
}
else
{
return (1 + EaseOutBounce(2 * t - 1)) / 2;
}
return T.One - EaseOutBounce(T.One - t);
}
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;
return u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3;
T u = T.One - t;
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

@@ -2,6 +2,7 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using static BetterLyrics.WinUI3.Helper.EasingHelper;
namespace BetterLyrics.WinUI3.Helper
{
@@ -21,7 +22,6 @@ namespace BetterLyrics.WinUI3.Helper
private double _configuredDelaySeconds; // 配置的延迟时长
// 动画状态
private Enums.EasingType? _easingType;
private Func<T, T, double, T> _interpolator;
private bool _isTransitioning;
private double _progress; // 当前段的进度 (0.0 ~ 1.0)
@@ -30,10 +30,11 @@ namespace BetterLyrics.WinUI3.Helper
public T Value => _currentValue;
public bool IsTransitioning => _isTransitioning;
public T TargetValue => _targetValue; // 获取当前段的目标值
public Enums.EasingType? EasingType => _easingType;
public double DurationSeconds => _totalDurationForAutoSplit;
public ValueTransition(T initialValue, double defaultTotalDuration = 0.3, EasingType? defaultEasingType = null, Func<T, T, double, T>? interpolator = null)
public Func<T, T, double, T> Interpolator => _interpolator;
public ValueTransition(T initialValue, Func<T, T, double, T>? interpolator, double defaultTotalDuration = 0.3)
{
_currentValue = initialValue;
_startValue = initialValue;
@@ -43,15 +44,6 @@ namespace BetterLyrics.WinUI3.Helper
if (interpolator != null)
{
_interpolator = interpolator;
_easingType = null;
}
else if (defaultEasingType != null)
{
SetEasingType(defaultEasingType);
}
else
{
SetEasingType(Enums.EasingType.EaseInOutQuad);
}
}
@@ -74,10 +66,9 @@ namespace BetterLyrics.WinUI3.Helper
_configuredDelaySeconds = seconds;
}
public void SetEasingType(Enums.EasingType? easingType)
public void SetInterpolator(Func<T, T, double, T> interpolator)
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(easingType);
_interpolator = interpolator;
}
#endregion
@@ -235,7 +226,7 @@ namespace BetterLyrics.WinUI3.Helper
#region Interpolators
private Func<T, T, double, T> GetInterpolatorByEasingType(Enums.EasingType? type)
public static Func<T, T, double, T> GetInterpolatorByEasingType(EasingType? type, EaseMode easingMode)
{
if (typeof(T) == typeof(double))
{
@@ -243,25 +234,24 @@ namespace BetterLyrics.WinUI3.Helper
{
double s = (double)(object)start;
double e = (double)(object)end;
double t = progress;
// 使用 EasingHelper (假设您的项目中已有此辅助类)
switch (type)
Func<double, double> easeInFunc = type switch
{
case Enums.EasingType.EaseInOutSine: t = EasingHelper.EaseInOutSine(t); break;
case Enums.EasingType.EaseInOutQuad: t = EasingHelper.EaseInOutQuad(t); break;
case Enums.EasingType.EaseInOutCubic: t = EasingHelper.EaseInOutCubic(t); break;
case Enums.EasingType.EaseInOutQuart: t = EasingHelper.EaseInOutQuart(t); break;
case Enums.EasingType.EaseInOutQuint: t = EasingHelper.EaseInOutQuint(t); break;
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;
}
Enums.EasingType.Sine => EaseInSine,
Enums.EasingType.Quad => EaseInQuad,
Enums.EasingType.Cubic => EaseInCubic,
Enums.EasingType.Quart => EaseInQuart,
Enums.EasingType.Quint => EaseInQuint,
Enums.EasingType.Expo => EaseInExpo,
Enums.EasingType.Circle => EaseInCircle,
Enums.EasingType.Back => EaseInBack,
Enums.EasingType.Elastic => EaseInElastic,
Enums.EasingType.Bounce => EaseInBounce,
Enums.EasingType.SmoothStep => SmoothStep,
Enums.EasingType.Linear => Linear,
_ => EaseInQuad,
};
double t = Ease(progress, easingMode, easeInFunc);
return (T)(object)(s + (e - s) * t);
};

View File

@@ -68,11 +68,17 @@ namespace BetterLyrics.WinUI3.Logic
for (int i = safeStart; i <= safeEnd; i++)
{
var line = lines[i];
var lineHeight = line.PrimaryLineHeight;
if (lineHeight == null || lineHeight <= 0) continue;
bool isWordAnimationEnabled = lyricsEffect.WordByWordEffectMode switch
{
Enums.WordByWordEffectMode.Auto => line.IsPrimaryHasRealSyllableInfo,
Enums.WordByWordEffectMode.Always => true,
Enums.WordByWordEffectMode.Never => false,
_ => line.IsPrimaryHasRealSyllableInfo
};
double targetCharFloat = lyricsEffect.IsLyricsFloatAnimationAmountAutoAdjust
? lineHeight.Value * 0.1
: lyricsEffect.LyricsFloatAnimationAmount;
@@ -83,7 +89,7 @@ namespace BetterLyrics.WinUI3.Logic
? 1.15
: lyricsEffect.LyricsScaleEffectAmount / 100.0;
var maxAnimationDurationMs = Math.Max(line.EndMs - currentPositionMs, 0);
var maxAnimationDurationMs = Math.Max(line.EndMs ?? 0 - currentPositionMs, 0);
bool isSecondaryLinePlaying = line.GetIsPlaying(currentPositionMs);
bool isSecondaryLinePlayingChanged = line.IsPlayingLastFrame != isSecondaryLinePlaying;
@@ -171,15 +177,15 @@ namespace BetterLyrics.WinUI3.Logic
line.ColorTransition.SetDelay(yScrollDelay);
line.ColorTransition.Start(isSecondaryLinePlaying ? fgColor : bgColor);
line.AngleTransition.SetEasingType(canvasYScrollTransition.EasingType);
line.AngleTransition.SetInterpolator(canvasYScrollTransition.Interpolator);
line.AngleTransition.SetDuration(yScrollDuration);
line.AngleTransition.SetDelay(yScrollDelay);
line.AngleTransition.Start(
(isFanEnabled && !isMouseScrolling) ?
Math.PI * (fanAngleRad / 180.0) * distanceFactor * (i > primaryPlayingLineIndex ? 1 : -1) :
fanAngleRad * distanceFactor * (i > primaryPlayingLineIndex ? 1 : -1) :
0);
line.YOffsetTransition.SetEasingType(canvasYScrollTransition.EasingType);
line.YOffsetTransition.SetInterpolator(canvasYScrollTransition.Interpolator);
line.YOffsetTransition.SetDuration(yScrollDuration);
line.YOffsetTransition.SetDelay(yScrollDelay);
// 设计之初是当 isLayoutChanged 为真时 jumpTo
@@ -187,98 +193,100 @@ namespace BetterLyrics.WinUI3.Logic
line.YOffsetTransition.Start(targetYScrollOffset);
}
if (isLayoutChanged || isSecondaryLinePlayingChanged)
if (isWordAnimationEnabled)
{
// 辉光动画
if (isGlowEnabled && lyricsEffect.LyricsGlowEffectScope == Enums.LyricsEffectScope.LineStartToCurrentChar
&& isSecondaryLinePlaying)
if (isSecondaryLinePlayingChanged)
{
foreach (var renderChar in line.PrimaryRenderChars)
// 辉光动画
if (isGlowEnabled && lyricsEffect.LyricsGlowEffectScope == Enums.LyricsEffectScope.LineStartToCurrentChar
&& isSecondaryLinePlaying)
{
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)
foreach (var renderChar in line.PrimaryRenderChars)
{
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;
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, stepDuration),
new Models.Keyframe<double>(0, stepDuration)
new Models.Keyframe<double>(targetCharGlow, stepInOutDuration),
new Models.Keyframe<double>(targetCharGlow, stepLastingDuration),
new Models.Keyframe<double>(0, stepInOutDuration)
);
}
}
syllable.IsPlayingLastFrame = isSyllablePlaying;
// 浮动动画
if (isFloatEnabled)
{
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.FloatTransition.Start(isSecondaryLinePlaying ? targetCharFloat : 0);
}
}
}
}
// 使动画步进一帧
foreach (var renderChar in line.PrimaryRenderChars)
{
renderChar.Update(elapsedTime);
// 字符动画
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

@@ -215,7 +215,7 @@ namespace BetterLyrics.WinUI3.Logic
lanesEndMs.Add(0);
}
lanesEndMs[assignedLane] = end;
lanesEndMs[assignedLane] = end ?? 0;
line.LaneIndex = assignedLane;
}
}

View File

@@ -74,7 +74,7 @@ namespace BetterLyrics.WinUI3.Logic
if (line == null) return state;
double lineEndMs = line.EndMs;
double lineEndMs = line.EndMs ?? 0;
// 还没到
if (currentTimeMs < line.StartMs) return state;
@@ -91,7 +91,7 @@ namespace BetterLyrics.WinUI3.Logic
switch (wordByWordEffectMode)
{
case WordByWordEffectMode.Auto:
if (line.PrimaryRenderSyllables.Count > 1)
if (line.IsPrimaryHasRealSyllableInfo)
{
return CalculateSyllableProgress(currentTimeMs, line, lineEndMs);
}
@@ -106,7 +106,7 @@ namespace BetterLyrics.WinUI3.Logic
state.SyllableProgress = 1f;
return state;
case WordByWordEffectMode.Always:
if (line.PrimaryRenderSyllables.Count > 1)
if (line.IsPrimaryHasRealSyllableInfo)
{
return CalculateSyllableProgress(currentTimeMs, line, lineEndMs);
}
@@ -129,8 +129,7 @@ namespace BetterLyrics.WinUI3.Logic
var timing = line.PrimaryRenderSyllables[i];
var nextTiming = (i + 1 < count) ? line.PrimaryRenderSyllables[i + 1] : null;
//double timingEndMs = timing.EndMs ?? nextTiming?.StartMs ?? lineEndMs;
double timingEndMs = timing.EndMs;
double timingEndMs = timing.EndMs ?? 0;
// 在当前字范围内
if (time >= timing.StartMs && time <= timingEndMs)

View File

@@ -7,8 +7,8 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
public class BaseLyrics
{
public int StartMs { get; set; }
public int EndMs { get; set; }
public int DurationMs => EndMs - StartMs;
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;

View File

@@ -22,6 +22,8 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
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++)

View File

@@ -20,18 +20,18 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
{
ScaleTransition = new(
initialValue: 1.0,
defaultTotalDuration: Time.AnimationDuration.TotalSeconds,
defaultEasingType: EasingType.EaseInOutSine
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: Time.AnimationDuration.TotalSeconds
);
GlowTransition = new(
initialValue: 0,
defaultTotalDuration: Time.AnimationDuration.TotalSeconds,
defaultEasingType: EasingType.EaseInOutSine
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: Time.AnimationDuration.TotalSeconds
);
FloatTransition = new(
initialValue: 0,
defaultTotalDuration: Time.LongAnimationDuration.TotalSeconds,
defaultEasingType: EasingType.EaseInOutSine
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: Time.LongAnimationDuration.TotalSeconds
);
LayoutRect = layoutRect;
}

View File

@@ -76,47 +76,49 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
public double? PrimaryLineHeight => PrimaryRenderChars.FirstOrDefault()?.LayoutRect.Height;
public bool IsPrimaryHasRealSyllableInfo { get; set; }
public RenderLyricsLine(LyricsLine lyricsLine) : base(lyricsLine)
{
AngleTransition = new(
initialValue: 0,
defaultTotalDuration: AnimationDuration,
defaultEasingType: EasingType.EaseInOutSine
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
BlurAmountTransition = new(
initialValue: 0,
defaultTotalDuration: AnimationDuration,
defaultEasingType: EasingType.EaseInOutSine
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
PhoneticOpacityTransition = new(
initialValue: 0,
defaultTotalDuration: AnimationDuration,
defaultEasingType: EasingType.EaseInOutSine
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
PlayedOriginalOpacityTransition = new(
initialValue: 0,
defaultTotalDuration: AnimationDuration,
defaultEasingType: EasingType.EaseInOutSine
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
UnplayedOriginalOpacityTransition = new(
initialValue: 0,
defaultTotalDuration: AnimationDuration,
defaultEasingType: EasingType.EaseInOutSine
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
TranslatedOpacityTransition = new(
initialValue: 0,
defaultTotalDuration: AnimationDuration,
defaultEasingType: EasingType.EaseInOutSine
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
ScaleTransition = new(
initialValue: 0,
defaultTotalDuration: AnimationDuration,
defaultEasingType: EasingType.EaseInOutSine
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
YOffsetTransition = new(
initialValue: 0,
defaultTotalDuration: AnimationDuration,
defaultEasingType: EasingType.EaseInOutSine
EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Sine),
defaultTotalDuration: AnimationDuration
);
ColorTransition = new(
initialValue: Colors.Transparent,
@@ -130,6 +132,7 @@ namespace BetterLyrics.WinUI3.Models.Lyrics
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)

View File

@@ -0,0 +1,25 @@
using BetterLyrics.Core.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Models
{
public class PluginDisplayModel
{
public IPlugin Plugin { get; }
public string Name => Plugin.Name;
public string Description => Plugin.Description;
public string Id => Plugin.Id;
public string Author => Plugin.Author;
public string Version => "v1.0.0"; // 如果您的接口有 Version 字段就读接口的
public string Glyph => !string.IsNullOrEmpty(Name) ? Name.Substring(0, 1) : "?";
public PluginDisplayModel(IPlugin plugin)
{
Plugin = plugin;
}
}
}

View File

@@ -1,6 +1,7 @@
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using static BetterLyrics.WinUI3.Helper.EasingHelper;
namespace BetterLyrics.WinUI3.Models.Settings
{
@@ -29,6 +30,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsFloatAnimationDuration { get; set; } = 450; // 450ms
[ObservableProperty][NotifyPropertyChangedRecipients] public partial EasingType LyricsScrollEasingType { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial EaseMode LyricsScrollEasingMode { get; set; } = EaseMode.Out;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsScrollDuration { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsScrollTopDuration { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial int LyricsScrollBottomDuration { get; set; }
@@ -79,6 +81,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
LyricsFloatAnimationDuration = this.LyricsFloatAnimationDuration,
LyricsScrollEasingType = this.LyricsScrollEasingType,
LyricsScrollEasingMode = this.LyricsScrollEasingMode,
LyricsScrollDuration = this.LyricsScrollDuration,
LyricsScrollTopDuration = this.LyricsScrollTopDuration,
LyricsScrollBottomDuration = this.LyricsScrollBottomDuration,

View File

@@ -37,7 +37,7 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty] public partial Rect DemoMonitorBounds { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial DockPlacement DockPlacement { get; set; } = DockPlacement.Top;
[ObservableProperty] public partial LyricsStyleSettings LyricsStyleSettings { get; set; } = new();
[ObservableProperty] public partial LyricsEffectSettings LyricsEffectSettings { get; set; } = new(500, 500, 500, EasingType.EaseInOutQuad);
[ObservableProperty] public partial LyricsEffectSettings LyricsEffectSettings { get; set; } = new(500, 500, 500, EasingType.Quad);
[ObservableProperty][NotifyPropertyChangedRecipients] public partial LyricsBackgroundSettings LyricsBackgroundSettings { get; set; } = new();
[ObservableProperty][NotifyPropertyChangedRecipients] public partial AlbumArtAreaStyleSettings AlbumArtLayoutSettings { get; set; } = new();
[ObservableProperty] public partial AlbumArtAreaEffectSettings AlbumArtAreaEffectSettings { get; set; } = new();

View File

@@ -13,7 +13,6 @@ namespace BetterLyrics.WinUI3.Models.Settings
[ObservableProperty][NotifyPropertyChangedRecipients] public partial ChineseRomanization ChineseRomanization { get; set; }
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsChineseRomanizationEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsJapaneseRomanizationEnabled { get; set; } = false;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial string CutletDockerServer { get; set; } = string.Empty;
[ObservableProperty][NotifyPropertyChangedRecipients] public partial bool IsTraditionalChineseEnabled { get; set; } = false;
public TranslationSettings() { }

View File

@@ -40,26 +40,14 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
startIndex += text.Length;
}
int lineEndMs = 0;
if (syllables.Count > 0)
{
var lastSyllable = syllables[syllables.Count - 1];
if (string.IsNullOrWhiteSpace(lastSyllable.Text))
{
lineEndMs = lastSyllable.StartMs;
syllables.RemoveAt(syllables.Count - 1);
}
}
if (syllables.Count > 1)
{
lrcLines.Add(new LyricsLine
{
StartMs = syllables[0].StartMs,
EndMs = lineEndMs,
PrimaryText = string.Concat(syllables.Select(s => s.Text)),
PrimarySyllables = syllables
PrimarySyllables = syllables,
IsPrimaryHasRealSyllableInfo = true
});
}
else
@@ -81,7 +69,13 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
content = bracketRegex!.Replace(line, "").Trim();
if (content == "//") content = "";
lrcLines.Add(new LyricsLine { StartMs = lineStartMs, PrimaryText = content });
var lyricsLine = new LyricsLine
{
StartMs = lineStartMs,
PrimaryText = content,
IsPrimaryHasRealSyllableInfo = false
};
lrcLines.Add(lyricsLine);
}
}
}
@@ -125,5 +119,6 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
}
}
}
}
}

View File

@@ -21,9 +21,8 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
var lineWrite = new LyricsLine
{
StartMs = lineRead.StartTime ?? 0,
EndMs = lineRead.EndTime ?? (nextLineRead?.StartTime ?? 0),
PrimaryText = lineRead.Text,
PrimarySyllables = [],
IsPrimaryHasRealSyllableInfo = true,
};
var syllables = (lineRead as Lyricify.Lyrics.Models.SyllableLineInfo)?.Syllables;

View File

@@ -127,7 +127,8 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
StartMs = containerStartMs,
EndMs = containerEndMs,
PrimaryText = fullOriginalText,
PrimarySyllables = syllables
PrimarySyllables = syllables,
IsPrimaryHasRealSyllableInfo = true,
});
var transSpan = container.Elements()
@@ -151,7 +152,8 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
StartMs = startMs,
EndMs = endMs,
PrimaryText = text
PrimaryText = text,
IsPrimaryHasRealSyllableInfo = false,
});
}
else
@@ -160,7 +162,8 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
StartMs = startMs,
EndMs = endMs,
PrimaryText = ""
PrimaryText = "",
IsPrimaryHasRealSyllableInfo = false,
});
}
}

View File

@@ -69,6 +69,9 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
LoadTransliteration(lyricsSearchResult);
GenerateTransliterationLyricsData();
EnsureEndMs(lyricsSearchResult?.Duration);
EnsureSyllables();
return _lyricsDataArr;
}
@@ -126,7 +129,7 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
{
romaji = await transliterationService.TransliterateText(main.WrappedOriginalText, PhoneticHelper.RomanCode, token);
_lyricsDataArr.FirstOrDefault()?.SetTransliteration(romaji);
transliterationSearchProvider = TransliterationSearchProvider.CutletDocker;
transliterationSearchProvider = TransliterationSearchProvider.Plugin;
}
catch (TaskCanceledException) { }
catch (Exception)
@@ -271,5 +274,84 @@ namespace BetterLyrics.WinUI3.Parsers.LyricsParser
}
}
private void EnsureEndMs(double? duration)
{
foreach (var lyricsData in _lyricsDataArr)
{
var lines = lyricsData.LyricsLines;
// 计算结束时间
for (int i = 0; i < lines.Count; i++)
{
// 计算行结束时间
if (lines[i].EndMs == null)
{
if (i + 1 < lines.Count)
{
lines[i].EndMs = lines[i + 1].StartMs;
}
else
{
lines[i].EndMs = (int)(duration ?? 0) * 1000;
}
}
// 计算音节结束时间
for (int j = 0; j < lines[i].PrimarySyllables.Count; j++)
{
var syllable = lines[i].PrimarySyllables[j];
if (syllable.EndMs == null)
{
if (j < lines[i].PrimarySyllables.Count - 1)
{
syllable.EndMs = lines[i].PrimarySyllables[j + 1].StartMs;
}
else
{
syllable.EndMs = lines[i].EndMs;
}
}
}
}
}
}
/// <summary>
/// Invoke this after <see cref="EnsureEndMs"/>
/// </summary>
private void EnsureSyllables()
{
foreach (var lyricsData in _lyricsDataArr)
{
if (lyricsData == null) continue;
var lines = lyricsData.LyricsLines;
if (lines == null) continue;
foreach (var line in lines)
{
if (line == null) continue;
if (line.IsPrimaryHasRealSyllableInfo) continue;
if (line.PrimarySyllables.Count > 0) continue;
var content = line.PrimaryText;
var length = content.Length;
if (length == 0) continue;
var avgSyllableDuration = line.DurationMs / length;
if (avgSyllableDuration == 0) continue;
for (int j = 0; j < length; j++)
{
line.PrimarySyllables.Add(new BaseLyrics
{
Text = content[j].ToString(),
StartIndex = j,
StartMs = line.StartMs + avgSyllableDuration * j,
EndMs = line.StartMs + avgSyllableDuration * (j + 1),
});
}
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
<linker>
<assembly fullname="System.Collections" preserve="all" />
<assembly fullname="System.IO.MemoryMappedFiles" preserve="all" />
<assembly fullname="System.Linq" preserve="all" />
<assembly fullname="System.Memory" preserve="all" />
<assembly fullname="System.ObjectModel" preserve="all" />
<assembly fullname="System.Runtime" preserve="all" />
<assembly fullname="System.Runtime.InteropServices" preserve="all" />
<assembly fullname="System.Text.RegularExpressions" preserve="all" />
<assembly fullname="System.Threading" preserve="all" />
<assembly fullname="BetterLyrics.WinUI3">
<type fullname="BetterLyrics.WinUI3.PluginConfigs.BetterLyrics_Plugins_Romaji_Config" preserve="all" />
</assembly>
</linker>

View File

@@ -0,0 +1,145 @@
// Auto-Generated by PluginAnalyzer
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace BetterLyrics.WinUI3.PluginConfigs;
internal static class BetterLyrics_Plugins_Romaji_Config
{
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.AppContext))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.AppDomain))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.ArgumentException))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.ArgumentNullException))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.ArgumentOutOfRangeException))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Array))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.AsyncCallback))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Boolean))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Buffer))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Byte))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Char))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.Generic.Dictionary<,>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.Generic.EqualityComparer<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.Generic.ICollection<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.Generic.IEnumerable<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.Generic.IEnumerator<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.Generic.IList<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.Generic.IReadOnlyCollection<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.Generic.IReadOnlyList<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.Generic.LinkedList<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.Generic.LinkedListNode<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.Generic.List<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.ICollection))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.IEnumerable))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.IEnumerator))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.IList))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.ObjectModel.Collection<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Collections.ObjectModel.ObservableCollection<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.ComponentModel.INotifyPropertyChanged))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.ComponentModel.PropertyChangedEventArgs))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.ComponentModel.PropertyChangedEventHandler))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Delegate))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Diagnostics.DebuggableAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Diagnostics.DebuggableAttribute.DebuggingModes))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Diagnostics.DebuggerBrowsableAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Diagnostics.DebuggerBrowsableState))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Diagnostics.DebuggerHiddenAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Enum))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Environment))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Exception))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Func<,>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.GC))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IAsyncResult))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IComparable<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IDisposable))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IndexOutOfRangeException))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Int16))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Int32))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Int64))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.InvalidOperationException))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.BinaryReader))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.File))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.FileAccess))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.FileMode))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.FileShare))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.FileStream))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.HandleInheritability))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.MemoryMappedFiles.MemoryMappedFile))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.MemoryMappedFiles.MemoryMappedFileAccess))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.MemoryMappedFiles.MemoryMappedViewAccessor))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.MemoryMappedFiles.MemoryMappedViewStream))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.Path))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.Stream))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.StreamReader))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.TextReader))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.IO.UnmanagedMemoryAccessor))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Linq.Enumerable))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Linq.IGrouping<,>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Math))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.MemoryExtensions))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.MulticastDelegate))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.NotImplementedException))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.NotSupportedException))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Nullable<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Object))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.ObjectDisposedException))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.ObsoleteAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.ParamArrayAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.ReadOnlySpan<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Reflection.Assembly))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Reflection.AssemblyCompanyAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Reflection.AssemblyConfigurationAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Reflection.AssemblyCopyrightAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Reflection.AssemblyDescriptionAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Reflection.AssemblyFileVersionAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Reflection.AssemblyInformationalVersionAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Reflection.AssemblyMetadataAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Reflection.AssemblyProductAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Reflection.AssemblyTitleAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Reflection.AssemblyTrademarkAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Reflection.DefaultMemberAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.CompilerServices.CallerMemberNameAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.CompilerServices.CompilationRelaxationsAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.CompilerServices.ExtensionAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.CompilerServices.IsReadOnlyAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.CompilerServices.IteratorStateMachineAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.CompilerServices.NullableAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.CompilerServices.NullableContextAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.CompilerServices.RefSafetyRulesAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.CompilerServices.RuntimeCompatibilityAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.CompilerServices.TupleElementNamesAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.InteropServices.InAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.Serialization.SerializationInfo))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.Serialization.StreamingContext))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.Versioning.SupportedOSPlatformAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.Versioning.TargetFrameworkAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Runtime.Versioning.TargetPlatformAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Security.Permissions.SecurityAction))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Security.Permissions.SecurityPermissionAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Security.UnverifiableCodeAttribute))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Single))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.String))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.StringSplitOptions))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Text.Encoding))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Text.RegularExpressions.Capture))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Text.RegularExpressions.Group))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Text.RegularExpressions.GroupCollection))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Text.RegularExpressions.Match))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Text.RegularExpressions.MatchCollection))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Text.RegularExpressions.Regex))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Text.RegularExpressions.RegexOptions))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Text.StringBuilder))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Threading.Interlocked))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Threading.Tasks.Task))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Threading.Tasks.Task<>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.TimeSpan))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.Type))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.UInt16))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.UInt32))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.ValueTuple<,>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(System.ValueType))]
[ModuleInitializer]
internal static void Initialize()
{
// This method runs automatically on startup.
}
}

View File

@@ -58,7 +58,7 @@ namespace BetterLyrics.WinUI3.Renderer
public CoverBackgroundRenderer()
{
_crossfadeTransition = new ValueTransition<double>(1.0, 0.7, defaultEasingType: EasingType.Linear);
_crossfadeTransition = new ValueTransition<double>(1.0, EasingHelper.GetInterpolatorByEasingType<double>(EasingType.Linear), 0.7);
}
public void SetCoverBitmap(CanvasBitmap? newBitmap)

View File

@@ -697,10 +697,6 @@ namespace BetterLyrics.WinUI3.Services.GSMTCService
_logger.LogInformation("Target LibreTranslate language code changed: {code}", _settingsService.AppSettings.TranslationSettings.SelectedTargetLanguageCode);
UpdateLyrics();
}
else if (message.PropertyName == nameof(TranslationSettings.CutletDockerServer))
{
UpdateLyrics();
}
else if (message.PropertyName == nameof(TranslationSettings.LibreTranslateServer))
{
UpdateLyrics();

View File

@@ -1,5 +1,6 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.Core.Interfaces;
using BetterLyrics.WinUI3.Constants;
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Extensions;
@@ -10,6 +11,7 @@ using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Providers;
using BetterLyrics.WinUI3.Services.FileSystemService;
using BetterLyrics.WinUI3.Services.LyricsCacheService;
using BetterLyrics.WinUI3.Services.PluginService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.SongSearchMapService;
using Lyricify.Lyrics.Helpers;
@@ -36,6 +38,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
private readonly IFileSystemService _fileSystemService;
private readonly ILyricsCacheService _lyricsCacheService;
private readonly ISongSearchMapService _songSearchMapService;
private readonly IPluginService _pluginService;
private readonly ILogger _logger;
public LyricsSearchService(
@@ -43,6 +46,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
IFileSystemService fileSystemService,
ILyricsCacheService lyricsCacheService,
ISongSearchMapService songSearchMapService,
IPluginService pluginService,
ILogger<LyricsSearchService> logger
)
{
@@ -50,6 +54,7 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
_fileSystemService = fileSystemService;
_lyricsCacheService = lyricsCacheService;
_songSearchMapService = songSearchMapService;
_pluginService = pluginService;
_logger = logger;
_lrcLibHttpClient = new();
@@ -207,11 +212,26 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
{
_logger.LogInformation("SearchAllAsync {SongInfo}", songInfo);
var results = new List<LyricsCacheItem>();
foreach (var provider in Enum.GetValues<LyricsSearchProvider>())
{
if (provider == LyricsSearchProvider.Plugin) continue;
var searchResult = await SearchSingleAsync(songInfo, provider, checkCache, token);
results.Add(searchResult);
}
foreach (var plugin in _pluginService.Plugins)
{
if (token.IsCancellationRequested) break;
if (plugin is ILyricsSearchPlugin lyricsSearchPlugin)
{
var pluginResult = await SearchPluginAsync(songInfo, lyricsSearchPlugin, token);
results.Add(pluginResult);
}
}
return results;
}
@@ -673,5 +693,40 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return lyricsSearchResult;
}
private async Task<LyricsCacheItem> SearchPluginAsync(SongInfo songInfo, ILyricsSearchPlugin plugin, CancellationToken token)
{
var cacheItem = new LyricsCacheItem
{
Provider = LyricsSearchProvider.Plugin,
};
try
{
var result = await plugin.GetLyricsAsync(songInfo.Title, songInfo.Artist, songInfo.Album, songInfo.Duration);
if (result != null && !string.IsNullOrEmpty(result.Raw))
{
cacheItem.Title = result.Title;
cacheItem.Artist = result.Artist;
cacheItem.Album = result.Album;
cacheItem.Duration = result.Duration;
cacheItem.Raw = result.Raw;
cacheItem.Translation = result.Translation;
cacheItem.Transliteration = result.Transliteration;
cacheItem.Reference = result.Reference ?? "about:blank";
cacheItem.MatchPercentage = MetadataComparer.CalculateScore(songInfo, cacheItem);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Plugin {PluginName} failed to search", plugin.Name);
}
return cacheItem;
}
}
}

View File

@@ -0,0 +1,16 @@
using BetterLyrics.Core.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace BetterLyrics.WinUI3.Services.PluginService
{
public interface IPluginService
{
IReadOnlyList<IPlugin> Plugins { get; }
void LoadPlugins();
void InstallPlugin(string zipPath);
void UninstallPlugin(string pluginId);
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
namespace BetterLyrics.WinUI3.Services.PluginService
{
public class PluginLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly? Load(AssemblyName assemblyName)
{
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
// return null to use the default AssemblyLoadContext
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
{
return LoadUnmanagedDllFromPath(libraryPath);
}
// return IntPtr.Zero to use the default AssemblyLoadContext
return IntPtr.Zero;
}
}
}

View File

@@ -0,0 +1,195 @@
using BetterLyrics.Core.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using Windows.Storage;
namespace BetterLyrics.WinUI3.Services.PluginService
{
public class PluginService : IPluginService
{
private List<IPlugin> _plugins = new();
public IReadOnlyList<IPlugin> Plugins => _plugins;
private Dictionary<string, PluginLoadContext> _pluginContexts = new();
private HashSet<string> _loadedDllPaths = new();
private readonly ILogger<PluginService> _logger;
public PluginService(ILogger<PluginService> logger)
{
_logger = logger;
}
public void LoadPlugins()
{
string pluginsRoot = Path.Combine(ApplicationData.Current.LocalFolder.Path, "plugins");
if (!Directory.Exists(pluginsRoot)) Directory.CreateDirectory(pluginsRoot);
var pluginFolders = Directory.GetDirectories(pluginsRoot);
foreach (var folder in pluginFolders)
{
var dllFiles = Directory.GetFiles(folder, "*.dll");
foreach (var dllPath in dllFiles)
{
if (_loadedDllPaths.Contains(dllPath)) continue;
TryLoadPlugin(dllPath);
}
}
}
private void TryLoadPlugin(string dllPath)
{
// 1. Create Context
var loadContext = new PluginLoadContext(dllPath);
try
{
var assembly = loadContext.LoadFromAssemblyPath(dllPath);
int loadedCount = 0; // Track successfully loaded plugins
// 2. [Safety Check] Safely retrieve types
IEnumerable<Type> types;
try
{
types = assembly.GetExportedTypes();
}
catch (ReflectionTypeLoadException ex)
{
// If some types fail to load, only keep the usable ones!
types = ex.Types.Where(t => t != null)!;
foreach (var loaderEx in ex.LoaderExceptions)
{
_logger.LogWarning("Partial type loading failure in DLL {path}: {msg}", dllPath, loaderEx?.Message);
}
}
foreach (var type in types)
{
// 3. Check if it is a valid plugin class
if (typeof(IPlugin).IsAssignableFrom(type) && !type.IsAbstract)
{
IPlugin? plugin = null;
try
{
// 4. [Instantiation Guard] Prevent plugin constructor errors from crashing the main app
plugin = (IPlugin?)Activator.CreateInstance(type);
if (plugin == null) continue;
// 5. Check for duplicate IDs
if (_plugins.Any(p => p.Id == plugin.Id))
{
_logger.LogWarning("Skipping duplicate plugin: {id} ({path})", plugin.Id, dllPath);
// Explicitly break reference to aid Unload
plugin = null;
continue;
}
// 6. Initialize
plugin.Initialize();
// 7. Add to collection
_plugins.Add(plugin);
_pluginContexts.Add(plugin.Id, loadContext);
loadedCount++;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize/instantiate plugin {type}", type.FullName);
// If plugin initialization fails, explicitly dispose if possible
if (plugin is IDisposable disposable)
{
try { disposable.Dispose(); } catch { }
}
plugin = null; // Break reference
}
}
}
// 8. Finalize: If no usable plugins were found in this DLL, unload Context
if (loadedCount > 0)
{
_loadedDllPaths.Add(dllPath);
_logger.LogInformation("Successfully loaded {count} plugin(s) from {path}", loadedCount, dllPath);
}
else
{
_logger.LogWarning("No valid plugins found in {path}. Unloading context.", dllPath);
// No plugin instances remain alive at this point, safe to unload
loadContext.Unload();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load assembly: {path}", dllPath);
// Only unload here if the loading process completely crashed
try { loadContext.Unload(); } catch { }
}
}
public void UninstallPlugin(string pluginId)
{
var plugin = _plugins.FirstOrDefault(p => p.Id == pluginId);
if (plugin == null) return;
var dllPath = plugin.GetType().Assembly.Location;
var folderPath = Path.GetDirectoryName(dllPath);
_plugins.Remove(plugin);
_loadedDllPaths.Remove(dllPath);
if (_pluginContexts.TryGetValue(pluginId, out var context))
{
context.Unload();
_pluginContexts.Remove(pluginId);
}
GC.Collect();
GC.WaitForPendingFinalizers();
if (Directory.Exists(folderPath))
{
try
{
Directory.Delete(folderPath, true);
}
catch (IOException ex)
{
_logger.LogError(ex, "Failed to delete plugin folder {}", folderPath);
}
}
}
public void InstallPlugin(string zipPath)
{
string pluginsRoot = Path.Combine(ApplicationData.Current.LocalFolder.Path, "plugins");
string folderName = Path.GetFileNameWithoutExtension(zipPath);
string installDir = Path.Combine(pluginsRoot, folderName);
if (Directory.Exists(installDir))
{
// TODO: 最好是先 Find plugin by path -> UninstallPlugin(id)
// 否则文件被锁住无法 Delete
try
{
Directory.Delete(installDir, true);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete existing plugin folder {}", installDir);
throw;
}
}
Directory.CreateDirectory(installDir);
ZipFile.ExtractToDirectory(zipPath, installDir);
}
}
}

View File

@@ -1,7 +1,11 @@
using BetterLyrics.WinUI3.Models.Http;
using BetterLyrics.Core.Interfaces;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models.Http;
using BetterLyrics.WinUI3.Serialization;
using BetterLyrics.WinUI3.Services.PluginService;
using BetterLyrics.WinUI3.Services.SettingsService;
using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
@@ -12,37 +16,31 @@ namespace BetterLyrics.WinUI3.Services.TransliterationService
public class TransliterationService : ITransliterationService
{
private readonly ISettingsService _settingsService;
private readonly IPluginService _pluginService;
private readonly HttpClient _httpClient;
public TransliterationService(ISettingsService settingsService)
public TransliterationService(ISettingsService settingsService, IPluginService pluginService)
{
_settingsService = settingsService;
_pluginService = pluginService;
_httpClient = new HttpClient();
}
public async Task<string> TransliterateText(string text, string targetLangCode, CancellationToken token)
{
string? result = null;
if (string.IsNullOrWhiteSpace(text))
{
throw new Exception(text + " is empty or null.");
}
if (string.IsNullOrEmpty(_settingsService.AppSettings.TranslationSettings.CutletDockerServer))
var plugin = (ILyricsTransliterationPlugin?)_pluginService.Plugins.FirstOrDefault(x => x is ILyricsTransliterationPlugin);
if (plugin != null)
{
throw new Exception("cutlet-docker server URL is not set in settings.");
result = await plugin.GetTransliterationAsync(text, PhoneticHelper.RomanCode);
}
var request = new CutletDockerRequest { Text = text };
var reqJson = System.Text.Json.JsonSerializer.Serialize(request, SourceGenerationContext.Default.CutletDockerRequest);
var url = $"{_settingsService.AppSettings.TranslationSettings.CutletDockerServer}/convert";
var response = await _httpClient.PostAsync(url, new StringContent(reqJson, Encoding.UTF8, "application/json"));
response.EnsureSuccessStatusCode();
var resJson = await response.Content.ReadAsStringAsync(token);
var result = System.Text.Json.JsonSerializer.Deserialize(resJson, SourceGenerationContext.Default.CutletDockerResponse);
return result?.RomajiText ?? string.Empty;
return result ?? "";
}
}
}

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>لا يمكن عرض المقطوعات لأن المسار غير موجود</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>سياسة الخصوصية</value>
</data>
@@ -798,34 +810,37 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>منطقة قابلة للسحب</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>EaseInOutBack</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>EaseInOutBounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>EaseInOutCirc</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>EaseInOutCubic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>EaseInOutElastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>EaseInOutExpo</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>EaseInOutQuad</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>EaseInOutQuart</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>EaseInOutQuint</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>EaseInOutSine</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
@@ -924,9 +939,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>بعد الاستيراد الناجح، سيقوم البرنامج بإعادة التشغيل تلقائياً</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>ستعطى الأولوية لقراءة النقل الصوتي داخل ملف الكلمات، وإذا لم يوجد تطابق، سيتم طلب نقل صوتي آلي من خادم cutlet-docker</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>الصوتيات اليابانية</value>
</data>
@@ -1254,6 +1266,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>اختصار التشغيل والإيقاف المؤقت</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>اختصار الأغنية السابقة</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>Titel können nicht angezeigt werden, da der Pfad nicht existiert</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Datenschutzrichtlinie</value>
</data>
@@ -798,35 +810,38 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Ziehbereich</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>Back Ease In-Out</value>
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>Bounce Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>Back</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>Circular Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>Bounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>Cubic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>Circular</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>Elastic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>Cubic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>Exponential Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>Elastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>Quadratic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>Exponential</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>Quartic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>Quadratic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>Quintic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>Quartic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>Sine Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>Quintic</value>
</data>
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>Sine</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>Linear</value>
@@ -924,9 +939,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>Die App startet nach erfolgreichem Import automatisch neu</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>Priorisiere Transliterationen innerhalb des Songtextes; falls keine Übereinstimmung gefunden wird, fordere maschinelle Transliteration vom cutlet-docker-Server an</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Japanische Lautschrift</value>
</data>
@@ -1254,6 +1266,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>Tastenkürzel für Wiedergabe/Pause</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>Tastenkürzel für vorherigen Titel</value>
</data>

View File

@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>Unable to view tracks because the path does not exist</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value>Install plugin</value>
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value>No plugins installed</value>
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value>Plugin Manager</value>
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value>Uninstall plugin</value>
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Privacy Policy</value>
</data>
@@ -798,35 +810,47 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Draggable Area</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>Back Ease In-Out</value>
<data name="SettingsPageEasingMode.Header" xml:space="preserve">
<value>Easing Mode</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>Bounce Ease In-Out</value>
<data name="SettingsPageEasingModeIn.Content" xml:space="preserve">
<value>In</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>Circular Ease In-Out</value>
<data name="SettingsPageEasingModeInOut.Content" xml:space="preserve">
<value>In-Out</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>Cubic Ease In-Out</value>
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value>Out</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>Elastic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>Back</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>Exponential Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>Bounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>Quadratic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>Circular</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>Quartic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>Cubic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>Quintic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>Elastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>Sine Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>Exponential</value>
</data>
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>Quadratic</value>
</data>
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>Quartic</value>
</data>
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>Quintic</value>
</data>
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>Sine</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>Linear</value>
@@ -924,9 +948,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>The app will restart automatically after successful import</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>Prioritize reading transliteration within lyrics; if no match, request machine transliteration from cutlet-docker server</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Japanese Phonetic</value>
</data>
@@ -1254,6 +1275,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>Play/Pause Shortcut</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value>Plugins</value>
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>Previous Track Shortcut</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>No se pueden ver las pistas porque la ruta no existe</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Política de privacidad</value>
</data>
@@ -798,35 +810,41 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Área arrastrable</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>Back Ease In-Out</value>
<data name="SettingsPageEasingMode.Header" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>Bounce Ease In-Out</value>
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>Circular Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>Back</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>Cubic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>Bounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>Elastic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>Circular</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>Exponential Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>Cubic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>Quadratic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>Elastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>Quartic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>Exponential</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>Quintic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>Quadratic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>Sine Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>Quartic</value>
</data>
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>Quintic</value>
</data>
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>Sine</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>Lineal</value>
@@ -924,9 +942,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>La aplicación se reiniciará automáticamente después de una importación exitosa</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>Priorizar la lectura de transliteración dentro de la letra; si no hay coincidencia, solicitar transliteración automática al servidor cutlet-docker</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Fonética japonesa</value>
</data>
@@ -1254,6 +1269,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>Atajo de Reproducir/Pausar</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>Atajo de pista anterior</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>Impossible d'afficher les pistes car le chemin n'existe pas</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Politique de confidentialité</value>
</data>
@@ -798,35 +810,38 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Zone déplaçable</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>Back Ease In-Out</value>
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>Bounce Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>Back</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>Circular Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>Bounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>Cubic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>Circular</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>Elastic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>Cubic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>Exponential Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>Elastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>Quadratic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>Exponential</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>Quartic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>Quadratic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>Quintic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>Quartic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>Sine Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>Quintic</value>
</data>
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>Sine</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>Linéaire</value>
@@ -924,9 +939,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>L'application redémarrera automatiquement après une importation réussie</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>Prioriser la lecture de la translittération dans les paroles ; si aucune correspondance, demander une translittération automatique au serveur cutlet-docker</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Phonétique japonaise</value>
</data>
@@ -1254,6 +1266,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>Raccourci Lecture/Pause</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>Raccourci piste précédente</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>ट्रैक देखने में असमर्थ क्योंकि पथ मौजूद नहीं है</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>गोपनीयता नीति</value>
</data>
@@ -798,34 +810,37 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>खींचने योग्य क्षेत्र</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>EaseInOutBack</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>EaseInOutBounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>EaseInOutCirc</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>EaseInOutCubic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>EaseInOutElastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>EaseInOutExpo</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>EaseInOutQuad</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>EaseInOutQuart</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>EaseInOutQuint</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>EaseInOutSine</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
@@ -924,9 +939,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>सफल आयात के बाद, प्रोग्राम स्वचालित रूप से पुनरारंभ हो जाएगा</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>बोल के भीतर लिप्यंतरण पढ़ने को प्राथमिकता दी जाएगी, यदि कोई मेल नहीं मिलता है तो cutlet-docker सर्वर से मशीनी लिप्यंतरण का अनुरोध किया जाएगा</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>जापानी ध्वन्यात्मकता</value>
</data>
@@ -1254,6 +1266,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>प्ले और पॉज़ शॉर्टकट</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>पिछला ट्रैक शॉर्टकट</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>Tidak dapat melihat trek karena jalur tidak ada</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Kebijakan Privasi</value>
</data>
@@ -798,34 +810,37 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Area yang Dapat Diseret</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>EaseInOutBack</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>EaseInOutBounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>EaseInOutCirc</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>EaseInOutCubic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>EaseInOutElastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>EaseInOutExpo</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>EaseInOutQuad</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>EaseInOutQuart</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>EaseInOutQuint</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>EaseInOutSine</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
@@ -924,9 +939,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>Setelah impor berhasil, program akan dimuat ulang secara otomatis</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>Akan memprioritaskan membaca transliterasi di dalam lirik, jika tidak ada kecocokan maka akan meminta transliterasi mesin dari server cutlet-docker</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Fonetik Bahasa Jepang</value>
</data>
@@ -1254,6 +1266,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>Pintasan Putar dan Jeda</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>Pintasan Lagu Sebelumnya</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>パスが存在しないため、曲を表示できません</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>個人情報保護方針</value>
</data>
@@ -798,34 +810,37 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>ドラッグ可能エリア</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>バックイーズ イン アウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>バウンスイーズ イン アウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>サーキュラーイーズ イン アウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>キュービックイーズ イン アウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>エラスティックイーズ イン アウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>エキスポネンシャルイーズ イン アウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>クアドラティックイーズ イン アウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>クオートイーズ イン アウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>クイントイーズ イン アウト</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>サインイーズ イン アウト</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
@@ -924,9 +939,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>インポートが成功すると、アプリは自動的に再起動します</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>歌詞内の音訳を優先します。一致しない場合は、cutlet-docker サーバーからマシンの音訳を要求します</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>ローマ字ルビ</value>
</data>
@@ -1254,6 +1266,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>再生/一時停止のショートカットキー</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>次のトラックのショートカットキー</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>경로가 존재하지 않아 트랙을 볼 수 없습니다</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>개인정보 처리방침</value>
</data>
@@ -798,35 +810,38 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>드래그 가능 영역</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>Back Ease In-Out</value>
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>Bounce Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>Back</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>Circular Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>Bounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>Cubic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>Circular</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>Elastic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>Cubic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>Exponential Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>Elastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>Quadratic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>Exponential</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>Quartic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>Quadratic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>Quintic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>Quartic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>Sine Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>Quintic</value>
</data>
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>Sine</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>Linear (선형)</value>
@@ -924,9 +939,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>가져오기가 성공하면 프로그램이 자동으로 다시 시작됩니다</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>가사 내의 발음 표기를 우선적으로 읽습니다. 일치하는 항목이 없으면 cutlet-docker 서버에 기계 음역을 요청합니다</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>일본어 발음 표기</value>
</data>
@@ -1254,6 +1266,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>재생 및 일시정지 단축키</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>이전 곡 단축키</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>Tidak dapat melihat trek kerana laluan tidak wujud</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Dasar Privasi</value>
</data>
@@ -798,34 +810,37 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Kawasan Boleh Diseret</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>EaseInOutBack</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>EaseInOutBounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>EaseInOutCirc</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>EaseInOutCubic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>EaseInOutElastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>EaseInOutExpo</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>EaseInOutQuad</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>EaseInOutQuart</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>EaseInOutQuint</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>EaseInOutSine</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
@@ -924,9 +939,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>Selepas import berjaya, program akan dimulakan semula secara automatik</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>Akan mengutamakan pembacaan transliterasi dalam lirik, jika tiada padanan maka akan meminta transliterasi mesin daripada pelayan cutlet-docker</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Fonetik Bahasa Jepun</value>
</data>
@@ -1254,6 +1266,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>Pintasan Main dan Jeda</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>Pintasan Lagu Sebelumnya</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>Não foi possível ver as faixas porque o caminho não existe</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Política de Privacidade</value>
</data>
@@ -798,34 +810,37 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Área de Arrastar</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>EaseInOutBack</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>EaseInOutBounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>EaseInOutCirc</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>EaseInOutCubic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>EaseInOutElastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>EaseInOutExpo</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>EaseInOutQuad</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>EaseInOutQuart</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>EaseInOutQuint</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>EaseInOutSine</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
@@ -924,9 +939,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>Após a importação bem-sucedida, o programa será reiniciado automaticamente</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>Dará prioridade à leitura da transliteração dentro da letra; se não houver correspondência, solicitará transliteração automática ao servidor cutlet-docker</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Fonética Japonesa</value>
</data>
@@ -1254,6 +1266,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>Atalho para Reproduzir e Pausar</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>Atalho para Faixa Anterior</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>Невозможно просмотреть треки, так как путь не существует</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Политика конфиденциальности</value>
</data>
@@ -798,35 +810,38 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Перетаскиваемая область</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>Back Ease In-Out</value>
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>Bounce Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>Back</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>Circular Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>Bounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>Cubic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>Circular</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>Elastic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>Cubic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>Exponential Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>Elastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>Quadratic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>Exponential</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>Quartic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>Quadratic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>Quintic Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>Quartic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>Sine Ease In-Out</value>
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>Quintic</value>
</data>
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>Sine</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>Linear</value>
@@ -924,9 +939,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>После успешного импорта приложение перезапустится автоматически</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>Приоритет отдается чтению транслитерации внутри текста; если совпадений нет, запрашивается машинная транслитерация у сервера cutlet-docker</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Японская фонетика</value>
</data>
@@ -1254,6 +1266,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>Горячая клавиша Воспроизведение/Пауза</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>Горячая клавиша предыдущего трека</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>ไม่สามารถดูแทร็กได้เนื่องจากเส้นทางไม่มีอยู่</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>นโยบายความเป็นส่วนตัว</value>
</data>
@@ -798,34 +810,37 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>พื้นที่ลาก</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>EaseInOutBack</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>EaseInOutBounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>EaseInOutCirc</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>EaseInOutCubic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>EaseInOutElastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>EaseInOutExpo</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>EaseInOutQuad</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>EaseInOutQuart</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>EaseInOutQuint</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>EaseInOutSine</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
@@ -924,9 +939,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>หลังจากนำเข้าสำเร็จ โปรแกรมจะรีสตาร์ทอัตโนมัติ</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>จะให้ความสำคัญกับการอ่านคำอ่านในเนื้อเพลงก่อน หากไม่พบจะขอคำอ่านจากเครื่องผ่านเซิร์ฟเวอร์ cutlet-docker</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>คำอ่านภาษาญี่ปุ่น</value>
</data>
@@ -1254,6 +1266,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>ทางลัดเล่นและหยุดชั่วคราว</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>ทางลัดเพลงก่อนหน้า</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>Không thể xem các bài hát vì đường dẫn không tồn tại</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>Chính sách bảo mật</value>
</data>
@@ -798,34 +810,37 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>Vùng có thể kéo</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>EaseInOutBack</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>EaseInOutBounce</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>EaseInOutCirc</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>EaseInOutCubic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>EaseInOutElastic</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>EaseInOutExpo</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>EaseInOutQuad</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>EaseInOutQuart</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>EaseInOutQuint</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>EaseInOutSine</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
@@ -924,9 +939,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>Sau khi nhập thành công, chương trình sẽ tự động khởi động lại</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>Sẽ ưu tiên đọc phiên âm trong lời bài hát, nếu không khớp sẽ yêu cầu phiên âm máy từ máy chủ cutlet-docker</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>Phiên âm tiếng Nhật</value>
</data>
@@ -1254,6 +1266,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>Phím tắt phát và tạm dừng</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>Phím tắt bài hát trước</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>无法查看曲目,因为路径不存在</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value>安装插件</value>
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value>未安装任何插件</value>
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value>插件管理</value>
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value>卸载插件</value>
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>隐私政策</value>
</data>
@@ -798,35 +810,47 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>可拖动区域</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<value>回弹缓入缓出</value>
<data name="SettingsPageEasingMode.Header" xml:space="preserve">
<value>缓动类型</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<value>弹跳缓入缓出</value>
<data name="SettingsPageEasingModeIn.Content" xml:space="preserve">
<value>缓入</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<value>圆形缓入缓出</value>
<data name="SettingsPageEasingModeInOut.Content" xml:space="preserve">
<value>缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<value>三次缓入缓出</value>
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value>缓入缓出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<value>弹性缓入缓出</value>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>弹</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<value>指数缓入缓出</value>
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>弹跳</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<value>二次缓入缓出</value>
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>圆形</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<value>四次缓入缓出</value>
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>三次</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<value>五次缓入缓出</value>
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>弹性</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<value>正弦缓入缓出</value>
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>指数</value>
</data>
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>二次</value>
</data>
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>四次</value>
</data>
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>五次</value>
</data>
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>正弦</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
<value>线性</value>
@@ -924,9 +948,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>导入成功后,该程序将自动重新启动</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>将优先读取歌词内音译,若无匹配则向 cutlet-docker 服务器请求机器音译</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>日语注音</value>
</data>
@@ -1254,6 +1275,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>播放与暂停快捷键</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value>插件</value>
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>上一曲目快捷键</value>
</data>

View File

@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -555,6 +555,18 @@
<data name="PlaylistViewFailed" xml:space="preserve">
<value>無法檢視曲目,因為路徑不存在</value>
</data>
<data name="PluginManagerControlInstall.Content" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlNoPluginsInstalled.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlTitle.Text" xml:space="preserve">
<value />
</data>
<data name="PluginManagerControlUninstall.Content" xml:space="preserve">
<value />
</data>
<data name="PrivacyPolicy.Content" xml:space="preserve">
<value>隱私權政策</value>
</data>
@@ -798,34 +810,37 @@
<data name="SettingsPageDragArea.Header" xml:space="preserve">
<value>可拖曳區域</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBack.Content" xml:space="preserve">
<data name="SettingsPageEasingModeOut.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPageEasingTypeEaseBack.Content" xml:space="preserve">
<value>回彈緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutBounce.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseBounce.Content" xml:space="preserve">
<value>彈跳緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCirc.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCirc.Content" xml:space="preserve">
<value>圓形緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutCubic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseCubic.Content" xml:space="preserve">
<value>三次緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutElastic.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseElastic.Content" xml:space="preserve">
<value>彈性緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutExpo.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseExpo.Content" xml:space="preserve">
<value>指數緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuad.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuad.Content" xml:space="preserve">
<value>二次緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuart.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuart.Content" xml:space="preserve">
<value>四次緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutQuint.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseQuint.Content" xml:space="preserve">
<value>五次緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeEaseInOutSine.Content" xml:space="preserve">
<data name="SettingsPageEasingTypeEaseSine.Content" xml:space="preserve">
<value>正弦緩入緩出</value>
</data>
<data name="SettingsPageEasingTypeLinear.Content" xml:space="preserve">
@@ -924,9 +939,6 @@
<data name="SettingsPageImportSettingsInfo.Content" xml:space="preserve">
<value>匯入成功後,該程式將自動重新啟動</value>
</data>
<data name="SettingsPageJapanese.Description" xml:space="preserve">
<value>將優先讀取歌詞內音譯,若無符合則向 cutlet-docker 伺服器請求機器音譯</value>
</data>
<data name="SettingsPageJapanese.Header" xml:space="preserve">
<value>日語注音</value>
</data>
@@ -1254,6 +1266,9 @@
<data name="SettingsPagePlayOrPauseSongHotKey.Header" xml:space="preserve">
<value>播放與暫停快速鍵</value>
</data>
<data name="SettingsPagePlugins.Content" xml:space="preserve">
<value />
</data>
<data name="SettingsPagePreviousSongHotKey.Header" xml:space="preserve">
<value>上一曲目快速鍵</value>
</data>

View File

@@ -39,9 +39,6 @@ namespace BetterLyrics.WinUI3.ViewModels
[ObservableProperty]
public partial bool IsLibreTranslateServerTesting { get; set; } = false;
[ObservableProperty]
public partial bool IsCutletDockerServerTesting { get; set; } = false;
[ObservableProperty]
public partial bool IsLXMusicServerTesting { get; set; } = false;
@@ -125,35 +122,6 @@ namespace BetterLyrics.WinUI3.ViewModels
});
}
[RelayCommand]
private void CutletDockerServerTest()
{
IsCutletDockerServerTesting = true;
Task.Run(async () =>
{
try
{
string result = await _transliterationService.TransliterateText(
"こんにちは", PhoneticHelper.RomanCode, new System.Threading.CancellationToken());
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
ToastHelper.ShowToast("SettingsPageServerTestSuccessInfo", null, InfoBarSeverity.Success);
});
}
catch (Exception)
{
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
ToastHelper.ShowToast("SettingsPageServerTestFailedInfo", null, InfoBarSeverity.Error);
});
}
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
IsCutletDockerServerTesting = false;
});
});
}
[RelayCommand]
private async Task LastFMAuthAsync()
{

View File

@@ -56,6 +56,12 @@
Glyph=&#xE9D2;}"
Tag="Stats" />
<NavigationViewItem
x:Uid="SettingsPagePlugins"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
Glyph=&#xE74C;}"
Tag="Plugins" />
<NavigationViewItem
x:Uid="SettingsPageAbout"
Icon="{ui:FontIcon FontFamily={StaticResource IconFontFamily},
@@ -96,6 +102,11 @@
<uc:StatsDashboardControl />
</controls:Case>
<!-- Plugins -->
<controls:Case Value="Plugins">
<uc:PluginManagerControl />
</controls:Case>
<!-- About -->
<controls:Case Value="About">
<uc:AboutControl />

View File

@@ -1,99 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.1.11312.151 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "BetterLyrics.WinUI3 (Package)", "BetterLyrics.WinUI3\BetterLyrics.WinUI3 (Package)\BetterLyrics.WinUI3 (Package).wapproj", "{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.WinUI3", "BetterLyrics.WinUI3\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj", "{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impressionist", "Impressionist\Impressionist\Impressionist.csproj", "{A678BCA5-03DE-71E4-73C1-388B7550E4E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorThief.WinUI3", "ColorThief.WinUI3\ColorThief.WinUI3.csproj", "{8F2FE667-2D91-428E-0630-05E6330F9625}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.Core", "BetterLyrics.Core\BetterLyrics.Core.csproj", "{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|ARM64.ActiveCfg = Debug|ARM64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|ARM64.Build.0 = Debug|ARM64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|ARM64.Deploy.0 = Debug|ARM64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x64.ActiveCfg = Debug|x64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x64.Build.0 = Debug|x64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x64.Deploy.0 = Debug|x64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x86.ActiveCfg = Debug|x86
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x86.Build.0 = Debug|x86
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x86.Deploy.0 = Debug|x86
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|ARM64.ActiveCfg = Release|ARM64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|ARM64.Build.0 = Release|ARM64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|ARM64.Deploy.0 = Release|ARM64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x64.ActiveCfg = Release|x64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x64.Build.0 = Release|x64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x64.Deploy.0 = Release|x64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x86.ActiveCfg = Release|x86
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x86.Build.0 = Release|x86
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x86.Deploy.0 = Release|x86
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|ARM64.ActiveCfg = Debug|ARM64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|ARM64.Build.0 = Debug|ARM64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|x64.ActiveCfg = Debug|x64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|x64.Build.0 = Debug|x64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|x86.ActiveCfg = Debug|x86
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|x86.Build.0 = Debug|x86
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|ARM64.ActiveCfg = Release|ARM64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|ARM64.Build.0 = Release|ARM64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x64.ActiveCfg = Release|x64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x64.Build.0 = Release|x64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x86.ActiveCfg = Release|x86
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x86.Build.0 = Release|x86
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|ARM64.Build.0 = Debug|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|x64.ActiveCfg = Debug|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|x64.Build.0 = Debug|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|x86.ActiveCfg = Debug|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Debug|x86.Build.0 = Debug|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|ARM64.ActiveCfg = Release|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|ARM64.Build.0 = Release|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x64.ActiveCfg = Release|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x64.Build.0 = Release|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x86.ActiveCfg = Release|Any CPU
{A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x86.Build.0 = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|ARM64.Build.0 = Debug|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|x64.ActiveCfg = Debug|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|x64.Build.0 = Debug|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|x86.ActiveCfg = Debug|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|x86.Build.0 = Debug|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|ARM64.ActiveCfg = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|ARM64.Build.0 = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x64.ActiveCfg = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x64.Build.0 = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x86.ActiveCfg = Release|Any CPU
{8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x86.Build.0 = Release|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Debug|ARM64.Build.0 = Debug|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Debug|x64.ActiveCfg = Debug|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Debug|x64.Build.0 = Debug|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Debug|x86.ActiveCfg = Debug|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Debug|x86.Build.0 = Debug|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|ARM64.ActiveCfg = Release|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|ARM64.Build.0 = Release|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|x64.ActiveCfg = Release|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|x64.Build.0 = Release|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|x86.ActiveCfg = Release|Any CPU
{0F47FE6F-D0AA-49E5-8F33-78DFDEB1F810}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0FD73F15-ED69-4DC2-9FD3-A4F03AF172A4}
EndGlobalSection
EndGlobal

21
BetterLyrics.slnx Normal file
View File

@@ -0,0 +1,21 @@
<Solution>
<Configurations>
<Platform Name="ARM64" />
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Project Path="BetterLyrics.Core/BetterLyrics.Core.csproj" />
<Project Path="BetterLyrics.Plugins.Romaji/BetterLyrics.Plugins.Romaji.csproj" />
<Project Path="BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package).wapproj" Type="c7167f0d-bc9f-4e6e-afe1-012c56b48db5">
<Deploy />
</Project>
<Project Path="BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
<Platform Solution="*|x86" Project="x86" />
</Project>
<Project Path="ColorThief.WinUI3/ColorThief.WinUI3.csproj" />
<Project Path="Impressionist/Impressionist/Impressionist.csproj" />
<Project Path="PluginAnalyzer/PluginAnalyzer.csproj" Id="8ee58bf7-807d-486c-8d7d-c00bb9ec2b81" />
<Project Path="RomajiConverter.Core/RomajiConverter.Core.csproj" />
</Solution>

View File

@@ -4,11 +4,6 @@
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<RootNamespace>ColorThief.WinUI3</RootNamespace>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.6" />
</ItemGroup>
</Project>

254
PluginAnalyzer/Program.cs Normal file
View File

@@ -0,0 +1,254 @@
using Mono.Cecil;
using System.Text;
using System.Text.RegularExpressions;
using System.Diagnostics;
class Program
{
// Global sets
static HashSet<string> _mergedAssemblies = new HashSet<string>();
static HashSet<string> _mergedSystemTypes = new HashSet<string>();
static string _currentScanDirectory = "";
static void Main(string[] args)
{
// =========================================================
// 🚀 MODE 1: CLI Automation Mode (被 VS 编译调用时)
// =========================================================
if (args.Length > 0)
{
try
{
string scanPath = args[0].Replace("\"", "").Trim();
string targetNs = args.Length > 1 ? args[1] : "BetterLyrics.WinUI3";
string prefix = args.Length > 2 ? args[2] : "Plugin";
// 读取第4个参数作为输出目录如果没传就默认用扫描目录
string outputDir = args.Length > 3 ? args[3].Replace("\"", "").Trim() : scanPath;
Console.WriteLine($"[Analyzer] Scanning: {scanPath}");
Console.WriteLine($"[Analyzer] Output to: {outputDir}");
RunBatch(scanPath, targetNs, prefix, outputDir, silent: true);
}
catch (Exception ex)
{
Console.WriteLine($"[Analyzer] Error: {ex.Message}");
Environment.Exit(1);
}
return;
}
// =========================================================
// 🚀 MODE 2: Interactive Mode (手动双击运行)
// =========================================================
Console.WriteLine("==================================================");
Console.WriteLine(" Plugin Dependency Analyzer (Auto-CLI Ready) ");
Console.WriteLine("==================================================");
while (true)
{
_mergedAssemblies.Clear();
_mergedSystemTypes.Clear();
Console.WriteLine("\n1. Enter Plugin FOLDER path (or 'exit'):");
Console.Write("> ");
string input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input)) continue;
if (input.Trim().ToLower() == "exit") break;
string path = input.Replace("\"", "").Trim();
if (!Directory.Exists(path)) { Console.WriteLine("[ERROR] Folder not found."); continue; }
Console.WriteLine("2. Enter Namespace (Default: BetterLyrics.WinUI3):");
Console.Write("> ");
string ns = Console.ReadLine().Trim();
if (string.IsNullOrWhiteSpace(ns)) ns = "BetterLyrics.WinUI3";
string prefix = new DirectoryInfo(path).Name;
Console.WriteLine($"3. Enter prefix (Default: {prefix}):");
Console.Write("> ");
string p = Console.ReadLine().Trim();
if (!string.IsNullOrWhiteSpace(p)) prefix = p;
// 👇👇👇【修复点在这里】👇👇👇
// 交互模式下,我们默认把文件生成在 exe 旁边的 AnalyzerOutput 文件夹里
string interactiveOutputDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "AnalyzerOutput");
// 传入 interactiveOutputDir 以匹配新的方法签名
RunBatch(path, ns, prefix, interactiveOutputDir, silent: false);
}
}
// ---------------------------------------------------------
// 下面的 RunBatch 和 GenerateFiles 签名已更新支持 outputDir
// ---------------------------------------------------------
static void RunBatch(string scanPath, string ns, string prefix, string outputDir, bool silent)
{
_currentScanDirectory = scanPath;
_mergedAssemblies.Clear();
_mergedSystemTypes.Clear();
ProcessDirectory(scanPath);
// 传递 outputDir
GenerateFiles(prefix, ns, outputDir, silent);
}
static void GenerateFiles(string prefix, string rootNamespace, string outputDir, bool silent)
{
Directory.CreateDirectory(outputDir);
string xmlFileName = $"{prefix}_TrimmerRoots.xml";
string csFileName = $"{prefix}_TrimmingConfig.cs";
// 👇 关键修改:区分 Assembly 名和 Namespace 名
string assemblyName = rootNamespace; // DLL 名字 (例如 BetterLyrics.WinUI3)
string codeNamespace = $"{rootNamespace}.PluginConfigs"; // C# 命名空间 (例如 BetterLyrics.WinUI3.PluginConfigs)
// 类名和全名
string configClassName = $"{prefix.Replace(".", "_").Replace(" ", "_")}_Config";
string fullConfigClassName = $"{codeNamespace}.{configClassName}";
// 路径变量
string xmlPath = Path.Combine(outputDir, xmlFileName);
string csPath = Path.Combine(outputDir, csFileName);
// ==========================================
// 1. Generate XML
// ==========================================
var xmlBuilder = new StringBuilder();
xmlBuilder.AppendLine("");
xmlBuilder.AppendLine("<linker>");
// A. 保护插件依赖
if (_mergedAssemblies.Count > 0)
{
foreach (var asm in _mergedAssemblies.OrderBy(n => n))
xmlBuilder.AppendLine($" <assembly fullname=\"{asm}\" preserve=\"all\" />");
}
// B. 保护生成的 Config 类
// ⚠️ 注意assembly fullname 必须是 rootNamespace (DLL名),而不是 codeNamespace
xmlBuilder.AppendLine($" <assembly fullname=\"{assemblyName}\">");
xmlBuilder.AppendLine($" <type fullname=\"{fullConfigClassName}\" preserve=\"all\" />");
xmlBuilder.AppendLine($" </assembly>");
xmlBuilder.AppendLine("</linker>");
// ==========================================
// 2. Generate C#
// ==========================================
var csBuilder = new StringBuilder();
csBuilder.AppendLine("// Auto-Generated by PluginAnalyzer");
csBuilder.AppendLine("using System.Diagnostics.CodeAnalysis;");
csBuilder.AppendLine("using System.Runtime.CompilerServices;");
csBuilder.AppendLine("");
// 👇 这里使用带后缀的命名空间
csBuilder.AppendLine($"namespace {codeNamespace};");
csBuilder.AppendLine($"internal static class {configClassName}");
csBuilder.AppendLine("{");
foreach (var type in _mergedSystemTypes.OrderBy(n => n))
{
csBuilder.AppendLine($" [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof({type}))]");
}
csBuilder.AppendLine(" [ModuleInitializer]");
csBuilder.AppendLine(" internal static void Initialize()");
csBuilder.AppendLine(" {");
csBuilder.AppendLine(" // This method runs automatically on startup.");
csBuilder.AppendLine(" }");
csBuilder.AppendLine("}");
// ==========================================
// 3. Write & Notify
// ==========================================
File.WriteAllText(xmlPath, xmlBuilder.ToString());
File.WriteAllText(csPath, csBuilder.ToString());
if (!silent)
{
Console.WriteLine($"[Analyzer] Generated: {xmlFileName}");
Process.Start("explorer.exe", outputDir);
}
else
{
Console.WriteLine($"[Analyzer] Created: {xmlPath}");
Console.WriteLine($"[Analyzer] Created: {csPath}");
}
}
// ---------------------------------------------------------
// Helpers (逻辑保持不变)
// ---------------------------------------------------------
static void ProcessDirectory(string folderPath)
{
var files = Directory.GetFiles(folderPath, "*.dll");
foreach (var file in files)
{
if (!IsHostOrSystemBinary(Path.GetFileName(file))) AnalyzeDll(file);
}
}
static void AnalyzeDll(string path)
{
try
{
var resolver = new DefaultAssemblyResolver();
resolver.AddSearchDirectory(Path.GetDirectoryName(path));
using var assembly = AssemblyDefinition.ReadAssembly(path, new ReaderParameters { AssemblyResolver = resolver });
foreach (var typeRef in assembly.MainModule.GetTypeReferences())
{
if (typeRef.Scope is AssemblyNameReference asmRef && !IsIgnoredDependency(asmRef.Name) && !IsLocalDll(asmRef.Name))
_mergedAssemblies.Add(asmRef.Name);
if (typeRef.FullName.StartsWith("System.") || typeRef.FullName.StartsWith("Microsoft.Win32"))
_mergedSystemTypes.Add(FormatTypeName(typeRef));
}
}
catch { }
}
static bool IsLocalDll(string asmName) => File.Exists(Path.Combine(_currentScanDirectory, asmName + ".dll"));
static bool IsHostOrSystemBinary(string fileName)
{
fileName = fileName.ToLower();
return fileName.StartsWith("betterlyrics.") && !fileName.Contains("plugin") ||
fileName.StartsWith("microsoft.windows") || fileName.StartsWith("winrt.") || fileName.StartsWith("system.");
}
static bool IsIgnoredDependency(string asmName) => asmName.Contains("BetterLyrics") || asmName.Contains("WindowsAppSDK") || asmName.Contains("Microsoft.Windows") || asmName == "netstandard" || asmName == "mscorlib";
static string FormatTypeName(TypeReference type)
{
string name = type.FullName;
// 1. 处理嵌套类型 (Cecil 用 '/'C# 用 '.')
name = name.Replace("/", ".");
// 2. 处理泛型 (核心修复:根据参数数量动态生成逗号)
// List`1 -> List<>
// Dictionary`2 -> Dictionary<,>
// Tuple`3 -> Tuple<,,>
if (name.Contains("`"))
{
name = Regex.Replace(name, @"`(\d+)", match =>
{
int argsCount = int.Parse(match.Groups[1].Value);
// 逗号数量 = 参数数量 - 1
// 比如 1个参数=0个逗号 (<>)
// 比如 2个参数=1个逗号 (<,>)
return "<" + new string(',', Math.Max(0, argsCount - 1)) + ">";
});
}
return name;
}
}

View File

@@ -20,7 +20,7 @@
<br>
<img src="Promotion/banner.png" alt="Banner" width="100%" style="border-radius: 10px;">
<img src="docs/assets/promotion/banner.png" alt="Banner" width="100%" style="border-radius: 10px;">
</div>
@@ -46,7 +46,7 @@
| :---: | :---: |
| <a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/en-us%20dark.svg" width="160"/></a><br>无限期免费试用(功能与付费版一致) | [**📦 最新版本 (.zip)**](https://github.com/jayfunc/BetterLyrics/releases/latest)<br>[查看安装指南](https://www.cnblogs.com/jayfunc/p/19212078) |
[📖 用户指南](https://github.com/jayfunc/BetterLyrics/wiki/使用指南) | [🔒 隐私政策](PrivacyPolicy.CN.md) | [⚖️ 服务条款](TermsofService.CN.md)
[📖 用户指南](docs/USER_GUIDE.CN.md) | [🔒 隐私政策](docs/PRIVACY_POLICY.CN.md) | [⚖️ 服务条款](docs/TERMS_OF_SERVICE.CN.md)
</div>
@@ -80,19 +80,19 @@
| 标准视图 | 侧边栏模式 |
| :---: | :---: |
| <img src="Screenshots/std.png" width="100%"> | <img src="Screenshots/narrow.png" width="100%"> |
| <img src="docs/assets/screenshots/std.png" width="100%"> | <img src="docs/assets/screenshots/narrow.png" width="100%"> |
| 歌词视觉特效 | 多模式共存 |
| :---: | :---: |
| <img src="Screenshots/effect.png" width="100%"> | <img src="Screenshots/all-in-one.png" width="100%"> |
| <img src="docs/assets/screenshots/effect.png" width="100%"> | <img src="docs/assets/screenshots/all-in-one.png" width="100%"> |
| 全屏模式 | 全屏模式 |
| :---: | :---: |
| <img src="Screenshots/fs3.png" width="100%"> | <img src="Screenshots/fs2.png" width="100%"> |
| <img src="docs/assets/screenshots/fs3.png" width="100%"> | <img src="docs/assets/screenshots/fs2.png" width="100%"> |
| 音乐库 | 播放统计 |
| :---: | :---: |
| <img src="Screenshots/music-gallery.png" width="100%"> | <img src="Screenshots/stats.png" width="100%"> |
| <img src="docs/assets/screenshots/music-gallery.png" width="100%"> | <img src="docs/assets/screenshots/stats.png" width="100%"> |
</div>
@@ -104,6 +104,8 @@
**协助翻译:** 找不到你的语言?[点此开始翻译](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file)。
**开发插件:** 想要扩展功能?[🧩 阅读插件开发指南](docs/PLUGIN_DEV.md)。
**从源码构建:**
> 构建前,请确保已替换 `Constants` 文件夹下的 `DiscordTemplate.cs` 和 `LastFM.cs`。
@@ -115,7 +117,7 @@
| 网页平台 | 支付宝 (扫码) | 微信 (扫码) |
| :---: | :---: | :---: |
| [PayPal](https://paypal.me/zhefangpay)<br><br>[Buy Me a Coffee](https://buymeacoffee.com/founchoo)<br><br>[爱发电 (Afdian)](https://afdian.com/a/jayfunc) | <img src="Donate/Alipay.jpg" width="150"> | <img src="Donate/WeChatReward.png" width="150"> |
| [PayPal](https://paypal.me/zhefangpay)<br><br>[Buy Me a Coffee](https://buymeacoffee.com/founchoo)<br><br>[爱发电 (Afdian)](https://afdian.com/a/jayfunc) | <img src="docs/assets/donate/Alipay.jpg" width="150"> | <img src="docs/assets/donate/WeChatReward.png" width="150"> |
**[查看完整赞助者名单 (Hall of Fame)](SPONSORS.md)**

View File

@@ -20,7 +20,7 @@
<br>
<img src="Promotion/banner.png" alt="Banner" width="100%" style="border-radius: 10px;">
<img src="docs/assets/promotion/banner.png" alt="Banner" width="100%" style="border-radius: 10px;">
</div>
@@ -46,7 +46,7 @@
| :---: | :---: |
| <a href="https://apps.microsoft.com/detail/9P1WCD1P597R?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/en-us%20dark.svg" width="160"/></a><br>Unlimited free trial (Same as paid) | [**📦 Latest Release (.zip)**](https://github.com/jayfunc/BetterLyrics/releases/latest)<br>See [Installation Guide](https://jayfunc.blog/blog/how-to-install-zip) |
[📖 User Guide](https://github.com/jayfunc/BetterLyrics/wiki/User-Guide) | [🔒 Privacy Policy](PrivacyPolicy.md) | [⚖️ Terms of Service](TermsofService.md)
[📖 User Guide](docs/USER_GUIDE.md) | [🔒 Privacy Policy](docs/PRIVACY_POLICY.md) | [⚖️ Terms of Service](docs/TERMS_OF_SERVICE.md)
</div>
@@ -80,19 +80,19 @@
| Standard View | Narrow Mode |
| :---: | :---: |
| <img src="Screenshots/std.png" width="100%"> | <img src="Screenshots/narrow.png" width="100%"> |
| <img src="docs/assets/screenshots/std.png" width="100%"> | <img src="docs/assets/screenshots/narrow.png" width="100%"> |
| Lyrics Visual Effects | Coexisting Modes |
| :---: | :---: |
| <img src="Screenshots/effect.png" width="100%"> | <img src="Screenshots/all-in-one.png" width="100%"> |
| <img src="docs/assets/screenshots/effect.png" width="100%"> | <img src="docs/assets/screenshots/all-in-one.png" width="100%"> |
| Fullscreen Mode | Fullscreen Mode |
| :---: | :---: |
| <img src="Screenshots/fs3.png" width="100%"> | <img src="Screenshots/fs2.png" width="100%"> |
| <img src="docs/assets/screenshots/fs3.png" width="100%"> | <img src="docs/assets/screenshots/fs2.png" width="100%"> |
| Music Gallery | Playback Statistics |
| :---: | :---: |
| <img src="Screenshots/music-gallery.png" width="100%"> | <img src="Screenshots/stats.png" width="100%"> |
| <img src="docs/assets/screenshots/music-gallery.png" width="100%"> | <img src="docs/assets/screenshots/stats.png" width="100%"> |
</div>
@@ -104,6 +104,8 @@
**Help us translate:** Cannot find your language? [Start translating here](https://github.com/jayfunc/BetterLyrics?tab=contributing-ov-file).
**Develop Plugins:** Want to extend functionality? [🧩 Read the Developer Guide](docs/PLUGIN_DEV.md).
**Build from source:**
> Before building, ensure you have replaced `DiscordTemplate.cs` and `LastFM.cs` in the `Constants` folder.
@@ -115,7 +117,7 @@ If you like BetterLyrics, please consider supporting it. Your support helps keep
| Web Platforms | Alipay (QR) | WeChat (QR) |
| :---: | :---: | :---: |
| [PayPal](https://paypal.me/zhefangpay)<br><br>[Buy Me a Coffee](https://buymeacoffee.com/founchoo)<br><br>[爱发电 (Afdian)](https://afdian.com/a/jayfunc) | <img src="Donate/Alipay.jpg" width="150"> | <img src="Donate/WeChatReward.png" width="150"> |
| [PayPal](https://paypal.me/zhefangpay)<br><br>[Buy Me a Coffee](https://buymeacoffee.com/founchoo)<br><br>[爱发电 (Afdian)](https://afdian.com/a/jayfunc) | <img src="docs/assets/donate/Alipay.jpg" width="150"> | <img src="docs/assets/donate/WeChatReward.png" width="150"> |
**[View the full Hall of Fame (Sponsors)](SPONSORS.md)**

View File

@@ -0,0 +1,13 @@
using System;
using System.Linq;
namespace RomajiConverter.Core.Extensions
{
public static class StringExtension
{
public static string[] LineToUnits(this string str)
{
return str.Split(new[] { ' ', ' ' }, StringSplitOptions.RemoveEmptyEntries);
}
}
}

View File

@@ -0,0 +1,203 @@
using System.Collections.Generic;
using System.Text;
namespace RomajiConverter.Core.Helpers
{
/// <summary>
/// 此类用于片假、平假互转
/// </summary>
public static class KanaHelper
{
/// <summary>
/// 转为片假名
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static string ToKatakana(string str)
{
var stringBuilder = new StringBuilder();
foreach (var c in str)
{
var bytes = Encoding.Unicode.GetBytes(c.ToString());
if (bytes.Length == 2 && bytes[1] == 0x30 && bytes[0] >= 0x40 && bytes[0] <= 0x9F)
stringBuilder.Append(Encoding.Unicode.GetString(new[] { (byte)(bytes[0] + 0x60), bytes[1] }));
else
stringBuilder.Append(c);
}
return stringBuilder.ToString();
}
/// <summary>
/// 转为平假名
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static string ToHiragana(string str)
{
var stringBuilder = new StringBuilder();
foreach (var c in str)
{
var bytes = Encoding.Unicode.GetBytes(c.ToString());
if (bytes.Length == 2 && bytes[1] == 0x30 && bytes[0] >= 0xA0 && bytes[0] <= 0xFA)
stringBuilder.Append(Encoding.Unicode.GetString(new[] { (byte)(bytes[0] - 0x60), bytes[1] }));
else
stringBuilder.Append(c);
}
return stringBuilder.ToString();
}
/// <summary>
/// 假名转罗马音
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static string KatakanaToRomaji(string str)
{
var result = new StringBuilder();
for (var i = 0; i < str.Length;)
{
if (i < str.Length - 1)
{
var extendedWord = str.Substring(i, 2);
if (ExtendedKanaDictionary.ContainsKey(extendedWord))
{
result.Append(ExtendedKanaDictionary[extendedWord]);
i += 2;
continue;
}
}
var word = str[i].ToString();
if (KanaDictionary.ContainsKey(word))
{
//正常转换
result.Append(KanaDictionary[word]);
}
else if (word == "ー")
{
//长音,取前一个音
result.Append(result.Length > 0 ? result[result.Length - 1].ToString() : word);
}
else
{
//不能识别,保持原样
result.Append(word);
}
i++;
}
//处理促音
for (var i = 0; i < result.Length; i++)
{
if (result[i] == 'っ' || result[i] == 'ッ')
if (i < result.Length - 1)
if (result[i + 1] == 'c')
result[i] = 't';
else
result[i] = result[i + 1];
else
result.Remove(i, 1);
}
return result.ToString();
}
public static Dictionary<string, string> KanaDictionary = new Dictionary<string, string>
{
//平假
{ "あ", "a" }, { "い", "i" }, { "う", "u" }, { "え", "e" }, { "お", "o" },
{ "か", "ka" }, { "き", "ki" }, { "く", "ku" }, { "け", "ke" }, { "こ", "ko" },
{ "さ", "sa" }, { "し", "shi" }, { "す", "su" }, { "せ", "se" }, { "そ", "so" },
{ "た", "ta" }, { "ち", "chi" }, { "つ", "tsu" }, { "て", "te" }, { "と", "to" },
{ "な", "na" }, { "に", "ni" }, { "ぬ", "nu" }, { "ね", "ne" }, { "の", "no" },
{ "は", "ha" }, { "ひ", "hi" }, { "ふ", "fu" }, { "へ", "he" }, { "ほ", "ho" },
{ "ま", "ma" }, { "み", "mi" }, { "む", "mu" }, { "め", "me" }, { "も", "mo" },
{ "や", "ya" }, { "ゆ", "yu" }, { "よ", "yo" },
{ "ら", "ra" }, { "り", "ri" }, { "る", "ru" }, { "れ", "re" }, { "ろ", "ro" },
{ "わ", "wa" }, { "を", "wo" },
{ "が", "ga" }, { "ぎ", "gi" }, { "ぐ", "gu" }, { "げ", "ge" }, { "ご", "go" },
{ "ざ", "za" }, { "じ", "ji" }, { "ず", "zu" }, { "ぜ", "ze" }, { "ぞ", "zo" },
{ "だ", "da" }, { "ぢ", "ji" }, { "づ", "zu" }, { "で", "de" }, { "ど", "do" },
{ "ば", "ba" }, { "び", "bi" }, { "ぶ", "bu" }, { "べ", "be" }, { "ぼ", "bo" },
{ "ぱ", "pa" }, { "ぴ", "pi" }, { "ぷ", "pu" }, { "ぺ", "pe" }, { "ぽ", "po" },
{ "ん", "n" },
//片假
{ "ア", "a" }, { "イ", "i" }, { "ウ", "u" }, { "エ", "e" }, { "オ", "o" },
{ "カ", "ka" }, { "キ", "ki" }, { "ク", "ku" }, { "ケ", "ke" }, { "コ", "ko" },
{ "サ", "sa" }, { "シ", "shi" }, { "ス", "su" }, { "セ", "se" }, { "ソ", "so" },
{ "タ", "ta" }, { "チ", "chi" }, { "ツ", "tsu" }, { "テ", "te" }, { "ト", "to" },
{ "ナ", "na" }, { "ニ", "ni" }, { "ヌ", "nu" }, { "ネ", "ne" }, { "", "no" },
{ "ハ", "ha" }, { "ヒ", "hi" }, { "フ", "fu" }, { "ヘ", "he" }, { "ホ", "ho" },
{ "マ", "ma" }, { "ミ", "mi" }, { "ム", "mu" }, { "メ", "me" }, { "モ", "mo" },
{ "ヤ", "ya" }, { "ユ", "yu" }, { "ヨ", "yo" },
{ "ラ", "ra" }, { "リ", "ri" }, { "ル", "ru" }, { "レ", "re" }, { "ロ", "ro" },
{ "ワ", "wa" }, { "ヲ", "wo" },
{ "ガ", "ga" }, { "ギ", "gi" }, { "グ", "gu" }, { "ゲ", "ge" }, { "ゴ", "go" },
{ "ザ", "za" }, { "ジ", "ji" }, { "ズ", "zu" }, { "ゼ", "ze" }, { "ゾ", "zo" },
{ "ダ", "da" }, { "ヂ", "ji" }, { "ヅ", "zu" }, { "デ", "de" }, { "ド", "do" },
{ "バ", "ba" }, { "ビ", "bi" }, { "ブ", "bu" }, { "ベ", "be" }, { "ボ", "bo" },
{ "パ", "pa" }, { "ピ", "pi" }, { "プ", "pu" }, { "ペ", "pe" }, { "ポ", "po" },
{ "ン", "n" },
//小版本
{ "ぁ", "a" }, { "ぃ", "i" }, { "ぅ", "u" }, { "ぇ", "e" }, { "ぉ", "o" },
{ "ゃ", "ya" }, { "ゅ", "yu" }, { "ょ", "yo" }, { "ゎ", "wa" },
{ "ァ", "a" }, { "ィ", "i" }, { "ゥ", "u" }, { "ェ", "e" }, { "ォ", "o" },
{ "ャ", "ya" }, { "ュ", "yu" }, { "ョ", "yo" }, { "ヮ", "wa" },
};
public static Dictionary<string, string> ExtendedKanaDictionary = new Dictionary<string, string>
{
//平假-拗音
{ "きゃ", "kya" }, { "きゅ", "kyu" }, { "きょ", "kyo" },
{ "しゃ", "sha" }, { "しゅ", "shu" }, { "しょ", "sho" },
{ "ちゃ", "cha" }, { "ちゅ", "chu" }, { "ちょ", "cho" },
{ "にゃ", "nya" }, { "にゅ", "nyu" }, { "にょ", "nyo" },
{ "ひゃ", "hya" }, { "ひゅ", "hyu" }, { "ひょ", "hyo" },
{ "みゃ", "mya" }, { "みゅ", "myu" }, { "みょ", "myo" },
{ "りゃ", "rya" }, { "りゅ", "ryu" }, { "りょ", "ryo" },
{ "ぎゃ", "gya" }, { "ぎゅ", "gyu" }, { "ぎょ", "gyo" },
{ "じゃ", "ja" }, { "じゅ", "ju" }, { "じょ", "jo" },
{ "ぢゃ", "ja" }, { "ぢゅ", "ju" }, { "ぢょ", "jo" },
{ "びゃ", "bya" }, { "びゅ", "byu" }, { "びょ", "byo" },
{ "ぴゃ", "pya" }, { "ぴゅ", "pyu" }, { "ぴょ", "pyo" },
//片假-拗音
{ "キャ", "kya" }, { "キュ", "kyu" }, { "キョ", "kyo" },
{ "シャ", "sha" }, { "シュ", "shu" }, { "ショ", "sho" },
{ "チャ", "cha" }, { "チュ", "chu" }, { "チョ", "cho" },
{ "ニャ", "nya" }, { "ニュ", "nyu" }, { "ニョ", "nyo" },
{ "ヒャ", "hya" }, { "ヒュ", "hyu" }, { "ヒョ", "hyo" },
{ "ミャ", "mya" }, { "ミュ", "myu" }, { "ミョ", "myo" },
{ "リャ", "rya" }, { "リュ", "ryu" }, { "リョ", "ryo" },
{ "ギャ", "gya" }, { "ギュ", "gyu" }, { "ギョ", "gyo" },
{ "ジャ", "ja" }, { "ジュ", "ju" }, { "ジョ", "jo" },
{ "ヂャ", "ja" }, { "ヂュ", "ju" }, { "ヂョ", "jo" },
{ "ビャ", "bya" }, { "ビュ", "byu" }, { "ビョ", "byo" },
{ "ピャ", "pya" }, { "ピュ", "pyu" }, { "ピョ", "pyo" },
//其他语言
{ "イェ", "ye" },
{ "ウィ", "wi" }, { "ウェ", "we" }, { "ウォ", "wo" },
{ "ヴァ", "va" }, { "ヴィ", "vi" }, { "ヴ", "vu" }, { "ヴェ", "ve" }, { "ヴォ", "vo" },
{ "ヴュ", "vyu" },
{ "クァ", "kwa" }, { "クィ", "kwi" }, { "クェ", "kwe" }, { "クォ", "kwo" },
{ "グァ", "gwa" },
{ "シェ", "she" },
{ "ジェ", "je" },
{ "チェ", "che" },
{ "ツァ", "tsa" }, { "ツィ", "tsi" }, { "ツェ", "tse" }, { "ツォ", "tso" },
{ "ティ", "ti" }, { "トゥ", "tu" },
{ "テュ", "tyu" },
{ "ディ", "di" }, { "ドゥ", "du" },
{ "デュ", "dyu" },
{ "ファ", "fa" }, { "フィ", "fi" }, { "フェ", "fe" }, { "フォ", "fo" },
{ "フュ", "fyu" },
};
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace RomajiConverter.Core.Helpers
{
public static class LrcParser
{
public static readonly Regex LrcLineRegex =
new Regex("(\\[(\\d+:)?\\d+:\\d+(\\.\\d+)?\\])+\\s*(?<text>.*?(?=\\[(\\d+:)?\\d+:\\d+(\\.\\d+)?\\]|$|\\r|\\n))",
RegexOptions.Compiled);
public static readonly Regex LrcTimeRegex =
new Regex("\\[((?<hour>\\d+):)?(?<minute>\\d+):(?<second>\\d+)(\\.(?<millisecond>\\d+))?\\]", RegexOptions.Compiled);
public static List<(TimeSpan Time, string Text)> Parse(string lrc)
{
var result = new List<(TimeSpan Time, string Text)>();
var lineMatches = LrcLineRegex.Matches(lrc);
foreach (Match lineMatch in lineMatches)
{
var text = lineMatch.Groups["text"].Value;
var timeMatches = LrcTimeRegex.Matches(lineMatch.Value);
foreach (Match timeMatch in timeMatches)
{
var hour = timeMatch.Groups["hour"].Success ? int.Parse(timeMatch.Groups["hour"].Value) : 0;
var minute = timeMatch.Groups["minute"].Success ? int.Parse(timeMatch.Groups["minute"].Value) : 0;
var second = timeMatch.Groups["second"].Success ? int.Parse(timeMatch.Groups["second"].Value) : 0;
var millisecond = timeMatch.Groups["millisecond"].Success
? int.Parse(timeMatch.Groups["millisecond"].Value)
: 0;
var time = new TimeSpan(0, hour, minute, second, millisecond);
result.Add((time, text));
}
}
return result;
}
}
}

View File

@@ -0,0 +1,372 @@
using MeCab;
using MeCab.Extension.UniDic;
using RomajiConverter.Core.Extensions;
using RomajiConverter.Core.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using RomajiConverter.Core.Options;
namespace RomajiConverter.Core.Helpers
{
public static class RomajiHelper
{
/// <summary>
/// 分词器
/// </summary>
private static MeCabTagger _tagger;
/// <summary>
/// 自定义词典<原文, 假名>
/// </summary>
private static Dictionary<string, string> _customizeDict;
public static void Init(string baseDirectory = null)
{
string rootPath = !string.IsNullOrEmpty(baseDirectory)
? baseDirectory
: AppDomain.CurrentDomain.BaseDirectory;
//词典路径
var dicPath = Path.Combine(rootPath, "unidic");
var parameter = new MeCabParam
{
DicDir = dicPath,
LatticeLevel = MeCabLatticeLevel.Zero
};
_tagger = MeCabTagger.Create(parameter);
var str = File.ReadAllText(Path.Combine(rootPath, "customizeDict.txt"));
var list = str.Split(Environment.NewLine.ToArray());
_customizeDict = new Dictionary<string, string>();
foreach (var item in list)
{
if (string.IsNullOrWhiteSpace(item)) continue;
var array = item.Split(' ');
if (array.Length < 2) continue;
if (!_customizeDict.ContainsKey(array[0]))
_customizeDict.Add(array[0], array[1]);
}
}
#region
/// <summary>
/// 生成转换结果列表
/// </summary>
/// <param name="text"></param>
/// <param name="options"></param>
/// <returns></returns>
public static IEnumerable<ConvertedLine> ToRomaji(string text, ToRomajiOptions options = null)
{
options = options ?? new ToRomajiOptions();
var timeSpans = new List<TimeSpan?>();
var lineTextList = text.Split(Environment.NewLine.ToArray()).Where(p => !string.IsNullOrWhiteSpace(p)).ToList();
for (var i = 0; i < lineTextList.Count; i++)
{
if (LrcParser.LrcLineRegex.IsMatch(lineTextList[i]))
{
var lyric = LrcParser.Parse(lineTextList[i]).FirstOrDefault();
timeSpans.Add(lyric.Time);
lineTextList[i] = lyric.Text;
}
else
{
timeSpans.Add(null);
}
}
ushort lineIndex = 0;
for (var index = 0; index < lineTextList.Count; index++)
{
var line = lineTextList[index];
if (IsChinese(line, options.ChineseRate)) continue;
var convertedLine = new ConvertedLine
{
Index = lineIndex,
Time = index < timeSpans.Count ? timeSpans[index] : null,
Japanese = line.Replace("\0", "")
};
foreach (var sentence in convertedLine.Japanese.LineToUnits())
{
if (IsEnglish(sentence))
{
convertedLine.Units.Add(new ConvertedUnit(lineIndex, sentence, sentence, sentence, false));
}
else
foreach (var unit in SentenceToRomaji(lineIndex, sentence, options.IsParticleAsPronunciation))
convertedLine.Units.Add(unit);
}
if (index + 1 < lineTextList.Count && IsChinese(lineTextList[index + 1], options.ChineseRate))
convertedLine.Chinese = lineTextList[index + 1];
lineIndex++;
yield return convertedLine;
}
}
/// <summary>
/// 分句转为罗马音
/// </summary>
/// <param name="lineIndex"></param>
/// <param name="str"></param>
/// <param name="isParticleAsPronunciation"></param>
/// <returns></returns>
public static IEnumerable<ConvertedUnit> SentenceToRomaji(ushort lineIndex, string str, bool isParticleAsPronunciation)
{
foreach (var item in _tagger.ParseToNodes(str))
{
var unit = MeCabNodeToUnit(lineIndex, item, isParticleAsPronunciation);
if (unit != null)
yield return unit;
}
}
public static ConvertedUnit MeCabNodeToUnit(ushort lineIndex, MeCabNode item, bool isParticleAsPronunciation)
{
ConvertedUnit unit = null;
if (item.CharType > 0)
{
var features = CustomSplit(item.Feature);
if (TryCustomConvert(item.Surface, out var customResult))
{
//用户自定义词典
unit = new ConvertedUnit(lineIndex,
item.Surface,
customResult,
KanaHelper.KatakanaToRomaji(customResult),
true);
}
else if (features.Length > 0 && (!isParticleAsPronunciation || item.GetPos1() != "助詞") && IsJapanese(item.Surface))
{
//纯假名
unit = new ConvertedUnit(lineIndex,
item.Surface,
KanaHelper.ToHiragana(item.Surface),
KanaHelper.KatakanaToRomaji(item.Surface),
false);
}
else if (features.Length <= 6 || new[] { "補助記号" }.Contains(item.GetPos1()))
{
//标点符号或无法识别的字
unit = new ConvertedUnit(lineIndex,
item.Surface,
item.Surface,
item.Surface,
false);
}
else if (IsEnglish(item.Surface))
{
//英文
unit = new ConvertedUnit(lineIndex,
item.Surface,
item.Surface,
item.Surface,
false);
}
else
{
//汉字或助词
var kana = GetKana(item);
unit = new ConvertedUnit(lineIndex,
item.Surface,
KanaHelper.ToHiragana(kana),
KanaHelper.KatakanaToRomaji(kana),
!IsJapanese(item.Surface));
var (replaceHiragana, replaceRomaji) = GetReplaceData(item);
unit.ReplaceHiragana = replaceHiragana;
unit.ReplaceRomaji = replaceRomaji;
}
}
else if (item.Stat != MeCabNodeStat.Bos && item.Stat != MeCabNodeStat.Eos)
{
unit = new ConvertedUnit(lineIndex,
item.Surface,
item.Surface,
item.Surface,
false);
}
return unit;
}
#endregion
#region
/// <summary>
/// 自定义分隔方法(Feature可能存在如 a,b,c,"d,e",f 格式的数据,此处不能把双引号中的内容也分隔开)
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
private static string[] CustomSplit(string str)
{
var list = new List<string>();
var item = new List<char>();
var haveMark = false;
foreach (var c in str)
if (c == ',' && !haveMark)
{
list.Add(new string(item.ToArray()));
item.Clear();
}
else if (c == '"')
{
item.Add(c);
haveMark = !haveMark;
}
else
{
item.Add(c);
}
return list.ToArray();
}
/// <summary>
/// 获取所有发音
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
private static (ObservableCollection<ReplaceString> replaceHiragana, ObservableCollection<ReplaceString>
replaceRomaji) GetReplaceData(MeCabNode node)
{
var length = node.Length;
var replaceNodeList = new List<MeCabNode>();
GetAllReplaceNode(replaceNodeList, node);
void GetAllReplaceNode(List<MeCabNode> list, MeCabNode n)
{
if (n != null && !list.Contains(n) && n.Length == length)
{
list.Add(n);
GetAllReplaceNode(list, n.BNext);
GetAllReplaceNode(list, n.ENext);
}
}
var replaceHiragana = new ObservableCollection<ReplaceString>();
var replaceRomaji = new ObservableCollection<ReplaceString>();
ushort i = 1;
foreach (var meCabNode in replaceNodeList
.GroupBy(GetKana)
.Select(g => g.First()))
{
var kana = GetKana(meCabNode);
if (kana != null)
{
replaceHiragana.Add(new ReplaceString(i, KanaHelper.ToHiragana(kana), true));
replaceRomaji.Add(new ReplaceString(i, KanaHelper.KatakanaToRomaji(kana), true));
i++;
}
}
return (replaceHiragana, replaceRomaji);
}
private static string GetKana(MeCabNode node)
{
return node.GetPos1() == "助詞" ? node.GetPron() : node.GetKana();
}
/// <summary>
/// 自定义转换规则
/// </summary>
/// <param name="str"></param>
/// <param name="result"></param>
/// <returns></returns>
private static bool TryCustomConvert(string str, out string result)
{
if (_customizeDict.ContainsKey(str))
{
result = _customizeDict[str];
return true;
}
result = "";
return false;
}
/// <summary>
/// 判断字符串(句子)是否简体中文
/// </summary>
/// <param name="str"></param>
/// <param name="rate">容错率(0-1)</param>
/// <returns></returns>
public static bool IsChinese(string str, float rate)
{
if (str.Length < 2)
return false;
var wordArray = str.ToCharArray();
var total = wordArray.Length;
var chCount = 0f;
var enCount = 0f;
foreach (var word in wordArray)
{
if (word != 'ー' && IsJapanese(word.ToString()))
//含有日文直接返回否
return false;
var gbBytes = Encoding.Unicode.GetBytes(word.ToString());
if (gbBytes.Length == 2) // double bytes char.
{
if (gbBytes[1] >= 0x4E && gbBytes[1] <= 0x9F) //中文
chCount++;
else
total--;
}
else if (gbBytes.Length == 1)
{
var byteAscii = int.Parse(gbBytes[0].ToString());
if ((byteAscii >= 65 && byteAscii <= 90) || (byteAscii >= 97 && byteAscii <= 122)) //英文字母
enCount++;
else
total--;
}
}
if (chCount == 0) return false; //一个简体中文都没有
return (chCount + enCount) / total >= rate;
}
/// <summary>
/// 判断字符串是否全为单字节
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static bool IsEnglish(string str)
{
return new Regex("^[\x20-\x7E]+$", RegexOptions.Compiled).IsMatch(str);
}
/// <summary>
/// 判断字符串是否全为假名
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
private static bool IsJapanese(string str)
{
return Regex.IsMatch(str, @"^[\u3040-\u30ff]+$", RegexOptions.Compiled);
}
#endregion
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.ObjectModel;
namespace RomajiConverter.Core.Models
{
public class ConvertedLine
{
public ushort Index { get; set; } = 0;
public TimeSpan? Time { get; set; }
public string Chinese { get; set; } = string.Empty;
public string Japanese { get; set; } = string.Empty;
public ObservableCollection<ConvertedUnit> Units { get; set; } = new ObservableCollection<ConvertedUnit>();
}
}

View File

@@ -0,0 +1,125 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace RomajiConverter.Core.Models
{
public class ConvertedUnit : INotifyPropertyChanged
{
private string _hiragana;
private bool _isKanji;
private string _japanese;
private ObservableCollection<ReplaceString> _replaceHiragana;
private ObservableCollection<ReplaceString> _replaceRomaji;
private string _romaji;
private ushort _selectId;
private ushort _lineIndex;
public ConvertedUnit(ushort lineIndex, string japanese, string hiragana, string romaji, bool isKanji)
{
LineIndex = lineIndex;
Japanese = japanese;
Romaji = romaji;
Hiragana = hiragana;
IsKanji = isKanji;
SelectId = 1;
ReplaceHiragana = new ObservableCollection<ReplaceString> { new ReplaceString(1, hiragana, true) };
ReplaceRomaji = new ObservableCollection<ReplaceString> { new ReplaceString(1, romaji, true) };
}
public ushort LineIndex
{
get => _lineIndex;
set
{
if (value == _lineIndex) return;
_lineIndex = value;
OnPropertyChanged();
}
}
public string Japanese
{
get => _japanese;
set
{
if (value == _japanese) return;
_japanese = value;
OnPropertyChanged();
}
}
public string Romaji
{
get => _romaji;
set
{
if (value == _romaji) return;
_romaji = value;
OnPropertyChanged();
}
}
public ObservableCollection<ReplaceString> ReplaceRomaji
{
get => _replaceRomaji;
set
{
if (Equals(value, _replaceRomaji)) return;
_replaceRomaji = value;
OnPropertyChanged();
}
}
public string Hiragana
{
get => _hiragana;
set
{
if (value == _hiragana) return;
_hiragana = value;
OnPropertyChanged();
}
}
public ObservableCollection<ReplaceString> ReplaceHiragana
{
get => _replaceHiragana;
set
{
if (Equals(value, _replaceHiragana)) return;
_replaceHiragana = value;
OnPropertyChanged();
}
}
public bool IsKanji
{
get => _isKanji;
set
{
if (value == _isKanji) return;
_isKanji = value;
OnPropertyChanged();
}
}
public ushort SelectId
{
get => _selectId;
set
{
if (value == _selectId) return;
_selectId = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -0,0 +1,23 @@
namespace RomajiConverter.Core.Models
{
public class ReplaceString
{
public ReplaceString(ushort id, string value, bool isSystem)
{
Id = id;
Value = value;
IsSystem = isSystem;
}
public ushort Id { get; set; }
public string Value { get; set; }
public bool IsSystem { get; set; }
public override string ToString()
{
return Value;
}
}
}

View File

@@ -0,0 +1,9 @@
namespace RomajiConverter.Core.Options
{
public class ToRomajiOptions
{
public float ChineseRate { get; set; } = 1f;
public bool IsParticleAsPronunciation { get; set; } = true;
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<PackageId>RomajiConverter.Core</PackageId>
<Version>2.1.0</Version>
<Authors>WL</Authors>
<RepositoryUrl>https://github.com/xyh20180101/RomajiConverter.WinUI</RepositoryUrl>
<PackageProjectUrl>https://github.com/xyh20180101/RomajiConverter.WinUI</PackageProjectUrl>
<PackageLicenseExpression>GPL-2.0-or-later</PackageLicenseExpression>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReadmeFile>README.md</PackageReadmeFile>
<MeCabUseDefaultDictionary>False</MeCabUseDefaultDictionary>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MeCab.DotNet" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<None Include="docs\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,58 @@
# RomajiConverter.Core
用于[RomajiConverter.WinUI](https://github.com/xyh20180101/RomajiConverter.WinUI)项目的罗马音转换相关逻辑的包
## 使用
```C#
//离线分词器
IEnumerable<ConvertedLine> list = RomajiHelper.ToRomaji(jpnStr);
//AI
var list = new ObservableCollection<ConvertedLine>();
await RomajiAIHelper.ToRomajiStreamingAsync(list, jpnStr, new ToRomajiAIOptions
{
BaseUrl = "",
Model = "",
ApiKey = ""
}); //流式插入
//以下是类结构
public class ConvertedLine
{
public ushort Index { get; set; } = 0;
public string Chinese { get; set; } = string.Empty;
public string Japanese { get; set; } = string.Empty;
public ObservableCollection<ConvertedUnit> Units { get; set; } = new ObservableCollection<ConvertedUnit>();
}
public class ConvertedUnit
{
public ushort LineIndex { get; set; }
public string Japanese { get; set; }
public string Romaji { get; set; }
public ObservableCollection<ReplaceString> ReplaceRomaji { get; set; }
public string Hiragana { get; set; }
public ObservableCollection<ReplaceString> ReplaceHiragana { get; set; }
public bool IsKanji { get; set; }
public ushort SelectId { get; set; }
}
public class ReplaceString
{
public ushort Id { get; set; }
public string Value { get; set; }
public bool IsSystem { get; set; }
}
```

View File

@@ -1,57 +0,0 @@
# Welcome to ShareHub | 欢迎来到 ShareHub
## Shared lyrics window status | 已分享的歌词窗口状态
Click on the links below to view and download the config files. | 点击以下链接查看和下载配置文件。
Each link is accompanied by a preview image of the lyrics window in that specific status. | 每个链接都附带该状态下歌词窗口的预览图像。
---
### [Desktop | 桌面](LyricsWindowStatus/desktop.json)
![](LyricsWindowStatus/desktop.png)
---
### [Docked (Bottom) | 停靠(底部)](LyricsWindowStatus/docked-bottom.json)
![](LyricsWindowStatus/docked-bottom.png)
---
### [Docked (Top) | 停靠(顶部)](LyricsWindowStatus/docked-top.json)
![](LyricsWindowStatus/docked-top.png)
---
### [Fullscreen (Horizontal) | 全屏(横屏)](LyricsWindowStatus/fs-horiz.json)
![](LyricsWindowStatus/fs-horiz.png)
---
### [Fullscreen (Vertical) | 全屏(竖屏)](LyricsWindowStatus/fs-vert.json)
![](LyricsWindowStatus/fs-vert.png)
---
### [Standard (Horizontal) | 标准(横屏)](LyricsWindowStatus/std-horiz.json)
![](LyricsWindowStatus/std-horiz.png)
---
### [Standard (Vertical) | 标准(竖屏)](LyricsWindowStatus/std-vert.json)
![](LyricsWindowStatus/std-vert.png)
---
### [Taskbar | 任务栏](LyricsWindowStatus/taskbar.json)
![](LyricsWindowStatus/taskbar.png)
---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

196
docs/PLUGIN_DEV.CN.md Normal file
View File

@@ -0,0 +1,196 @@
**中文** | [**English**](PLUGIN_DEV.md)
# BetterLyrics 插件开发指南 🧩
欢迎开发 BetterLyrics 插件!本文档将指导你如何创建一个标准插件,并利用我们的自动化构建工具链,完美解决 .NET 裁剪Trimming和依赖冲突问题。
## 🛠️ 核心机制简介
为了保证插件在主程序发布Native AOT / Trimmed后依然稳定运行我们采用了一套自动化工作流
1. **智能去重**:编译时,脚本会自动检测主程序已有的 DLL`Newtonsoft.Json``BetterLyrics.Core`),并从插件输出目录中剔除,防止版本冲突。
2. **自动分析**:编译后,`PluginAnalyzer` 工具会扫描你的插件,分析缺失的 System 库依赖。
3. **自动注册**:工具会在主程序的 `PluginConfigs` 目录生成配置,利用 `[ModuleInitializer]` 实现插件自动挂载,无需手动写注册代码。
---
## 🚀 快速开始
### 1. 创建项目
创建一个 **类库 (Class Library)** 项目。
* **框架**`.NET 10`
* **平台**`net10.0-windows10.0.19041.0`
### 2. 配置 `.csproj` (关键步骤)
请将你的 `.csproj` 文件内容替换为以下标准模板。这段配置包含了自动化构建的所有黑科技。
> ⚠️ **注意**:请根据实际文件结构,修改 `<HostProjectDir>` 和 `<AnalyzerPath>` 的路径。
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BetterLyrics.Core\BetterLyrics.Core.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
<Target Name="AutoExcludeSharedAssemblies" AfterTargets="ResolveAssemblyReferences">
<PropertyGroup>
<HostOutputDir>
..\BetterLyrics.WinUI3\BetterLyrics.WinUI3\bin\x64\$(Configuration)\$(TargetFramework)\</HostOutputDir>
</PropertyGroup>
<Message Text="[Debug] Searching for Host Assemblies in: $(HostOutputDir)" Importance="High" />
<ItemGroup>
<FilesToCopy Include="@(ReferenceCopyLocalPaths)" />
<SharedFiles Include="@(FilesToCopy)"
Condition="Exists('$(HostOutputDir)%(Filename)%(Extension)')" />
<ReferenceCopyLocalPaths Remove="@(SharedFiles)" />
</ItemGroup>
<Message
Text="[Smart Trim] Excluded shared assemblies:%0a@(SharedFiles->' -> %(Filename)%(Extension)', '%0a')"
Importance="High" Condition="'@(SharedFiles)' != ''" />
</Target>
<Target Name="RunPluginAnalyzer" AfterTargets="Build">
<PropertyGroup>
<AnalyzerPath>..\PluginAnalyzer\bin\Debug\net10.0\PluginAnalyzer.exe</AnalyzerPath>
<ScanDir>$(TargetDir)</ScanDir>
<Ns>BetterLyrics.WinUI3</Ns>
<Prefix>$(ProjectName)</Prefix>
<OutputDir>..\BetterLyrics.WinUI3\BetterLyrics.WinUI3\PluginConfigs\</OutputDir>
</PropertyGroup>
<Message Text="[Analyzer] Delivering configs to Main App..." Importance="High" />
<Exec
Command="&quot;$(AnalyzerPath)&quot; &quot;$(ScanDir)\&quot; &quot;$(Ns)&quot; &quot;$(Prefix)&quot; &quot;$(OutputDir)\&quot;" />
</Target>
</Project>
```
---
## 💻 开发流程
### 1. 引用依赖
你可以像平常一样引用 NuGet 包或第三方 DLL。
* **如果是公共库**(如 `Newtonsoft.Json`):无需做任何事,脚本会自动检测主程序里有没有。如果有,编译时自动剔除,运行时直接使用主程序加载的版本。
* **如果是私有库**(如 `MeCab`):脚本检测到主程序没有,会将其保留在你的插件目录里。
### 2. 实现接口
在代码中实现 `BetterLyrics.Core` 提供的接口(例如 `ILyricsProvider`)。
```csharp
using BetterLyrics.Core;
namespace BetterLyrics.Plugins.MyPlugin;
public class MyLyricsSearchPlugin : ILyricsSearchPlugin
{
public string Id => "your_name.plugin_name";
public string Name => "Plugin display name";
public string Description => "Plugin description.";
public string Author => "Your name";
public void Initialize()
{
// Do something if necessary ...
// string? pluginPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
// Do something if necessary ...
}
public async Task<LyricsSearchResult> GetLyricsAsync(string title, string artist, string album, double duration)
{
// Your code here
return new LyricsSearchResult(...);
}
}
```
### 3. 编译构建
点击 Visual Studio 的 **生成 (Build)**
观察 **输出 (Output)** 窗口,你会看到自动化工具在工作:
1. `[Smart Trim]`:告诉你哪些库被剔除了(例如 `BetterLyrics.Core.dll`)。
2. `[Analyzer]`:告诉你防裁剪配置文件 (`*_TrimmerRoots.xml`) 已经生成,并成功投递到了主程序的 `PluginConfigs` 文件夹。
---
## 🔍 原理详解 (Advanced)
### 为什么我看不到生成的配置文件?
生成的配置文件不会在你的插件项目里,而是直接生成到了 **主程序**`PluginConfigs` 文件夹下。
### 生成了什么?
1. **`MyPlugin_TrimmerRoots.xml`**
告诉主程序的裁剪器Trimmer保留所有你的插件需要用到、但主程序看起来没用到的 System 库。它还包含了一条指令,强制保留生成的 Config 类。
2. **`MyPlugin_TrimmingConfig.cs`**
包含一个带有 `[ModuleInitializer]` 特性的类。这个类会在主程序启动时 **自动运行**,确保相关的反射元数据被加载,防止运行时崩溃。
### 依赖关系图
```mermaid
graph TD
A[你的插件代码] -->|编译| B(插件 DLL)
B -->|智能去重| B
B -->|自动化分析| C{PluginAnalyzer}
C -->|生成| D[PluginConfigs/MyPlugin_Config.cs]
C -->|生成| E[PluginConfigs/MyPlugin_TrimmerRoots.xml]
D -->|自动投递| F[主程序项目]
E -->|自动投递| F
F -->|发布 Publish| G[最终可执行文件]
style C fill:#f9f,stroke:#333
style F fill:#bfb,stroke:#333
```
---
## ⚠️ 注意事项
1. **编译顺序**
请务必确保 **主程序 (Host) 至少被编译过一次**Debug 或 Release 对应模式)。插件的去重脚本依赖于读取主程序的输出目录来进行比对。如果主程序 bin 目录是空的,插件可能会错误地打包所有依赖。
2. **配置一致性**
如果你在 `Release` 模式下编译插件,确保主程序也是 `Release` 模式。脚本会自动根据 `$(Configuration)` 寻找对应的主程序输出路径。
3. **私有依赖处理**
如果你引用了一个主程序也有、但你需要**不同版本**的库(这种情况极少见且不推荐),你需要修改 `.csproj` 脚本,移除自动剔除逻辑。
---
## 🙋‍♂️ 常见问题
**Q: 编译时报错 "命令 ... 已退出,代码为 9009" 或 "不是内部或外部命令"**
A: 这说明构建脚本找不到 `PluginAnalyzer.exe`。请检查 `.csproj` 文件里的 `<AnalyzerPath>` 路径配置是否正确。
**Q: 运行时报错 "FileNotFoundException: BetterLyrics.Core"?**
A: 这是正常的。插件目录下不应该有 `BetterLyrics.Core.dll`(因为它由主程序提供)。请通过主程序加载插件进行测试,而不是直接运行插件 DLL。
**Q: 发布后插件崩溃,提示 System 库找不到?**
A: 请检查主程序的 `PluginConfigs` 目录下是否成功生成了对应的 `*_TrimmerRoots.xml` 文件。如果没有,请尝试重新生成插件项目。

198
docs/PLUGIN_DEV.md Normal file
View File

@@ -0,0 +1,198 @@
[**中文**](PLUGIN_DEV.CN.md) | **English**
# BetterLyrics Plugin Development Guide 🧩
Welcome to BetterLyrics plugin development! This document guides you through creating a standard plugin and using our automated build toolchain to perfectly resolve .NET Trimming and dependency conflict issues.
## 🛠️ Core Mechanics
To ensure plugins run stably after the main application is published (Native AOT / Trimmed), we use an automated workflow:
1. **Smart Exclusion**: During compilation, a script automatically detects DLLs already present in the host application (e.g., `Newtonsoft.Json`, `BetterLyrics.Core`) and removes them from the plugin output directory to prevent version conflicts.
2. **Auto Analysis**: After compilation, the `PluginAnalyzer` tool scans your plugin to analyze missing System library dependencies.
3. **Auto Registration**: The tool generates configurations in the host's `PluginConfigs` directory and uses `[ModuleInitializer]` to achieve automatic plugin mounting, eliminating the need for manual registration code.
---
## 🚀 Quick Start
### 1. Create Project
Create a **Class Library** project.
* **Framework**: `.NET 10`.
* **Platform**: `net10.0-windows10.0.19041.0`.
### 2. Configure `.csproj` (Key Step)
Please replace the content of your `.csproj` file with the standard template below. This configuration contains all the "magic" for the automated build process.
> ⚠️ **Note**: Please modify the paths for `<HostProjectDir>` and `<AnalyzerPath>` according to your actual file structure.
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\BetterLyrics.Core\BetterLyrics.Core.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
<Target Name="AutoExcludeSharedAssemblies" AfterTargets="ResolveAssemblyReferences">
<PropertyGroup>
<HostOutputDir>..\BetterLyrics.WinUI3\BetterLyrics.WinUI3\bin\x64\$(Configuration)\$(TargetFramework)\</HostOutputDir>
</PropertyGroup>
<Message Text="[Debug] Searching for Host Assemblies in: $(HostOutputDir)" Importance="High" />
<ItemGroup>
<FilesToCopy Include="@(ReferenceCopyLocalPaths)" />
<SharedFiles Include="@(FilesToCopy)" Condition="Exists('$(HostOutputDir)%(Filename)%(Extension)')" />
<ReferenceCopyLocalPaths Remove="@(SharedFiles)" />
</ItemGroup>
<Message Text="[Smart Trim] Excluded shared assemblies:%0a@(SharedFiles->' -> %(Filename)%(Extension)', '%0a')" Importance="High" Condition="'@(SharedFiles)' != ''" />
</Target>
<Target Name="RunPluginAnalyzer" AfterTargets="Build">
<PropertyGroup>
<AnalyzerPath>..\PluginAnalyzer\bin\Debug\net10.0\PluginAnalyzer.exe</AnalyzerPath>
<ScanDir>$(TargetDir)</ScanDir>
<Ns>BetterLyrics.WinUI3</Ns>
<Prefix>$(ProjectName)</Prefix>
<OutputDir>..\BetterLyrics.WinUI3\BetterLyrics.WinUI3\PluginConfigs\</OutputDir>
</PropertyGroup>
<Message Text="[Analyzer] Delivering configs to Main App..." Importance="High" />
<Exec Command="&quot;$(AnalyzerPath)&quot; &quot;$(ScanDir)\&quot; &quot;$(Ns)&quot; &quot;$(Prefix)&quot; &quot;$(OutputDir)\&quot;" />
</Target>
</Project>
```
---
## 💻 Development Workflow
### 1. Referencing Dependencies
You can reference NuGet packages or third-party DLLs as usual.
* **Shared Libraries** (e.g., `Newtonsoft.Json`): Do nothing. The script automatically detects if the host has it. If so, it's excluded during the plugin build, and the host's version is used at runtime.
* **Private Libraries** (e.g., `MeCab`): If the script detects the host doesn't have it, it will be preserved in your plugin directory.
### 2. Implementing Interfaces
Implement the interfaces provided by `BetterLyrics.Core` (e.g., `ILyricsSearchPlugin`) in your code.
```csharp
using BetterLyrics.Core;
namespace BetterLyrics.Plugins.MyPlugin;
public class MyLyricsSearchPlugin : ILyricsSearchPlugin
{
public string Id => "your_name.plugin_name";
public string Name => "Plugin display name";
public string Description => "Plugin description.";
public string Author => "Your name";
public void Initialize()
{
// Do something if necessary ...
// string? pluginPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
}
public async Task<LyricsSearchResult> GetLyricsAsync(string title, string artist, string album, double duration)
{
// Your code here
return new LyricsSearchResult(...);
}
}
```
### 3. Build & Compile
Click **Build** in Visual Studio.
Observe the **Output** window; you will see the automation tools at work:
1. `[Smart Trim]`: Tells you which libraries were excluded (e.g., `BetterLyrics.Core.dll`).
2. `[Analyzer]`: Tells you that the anti-trimming configuration (`*_TrimmerRoots.xml`) has been generated and successfully delivered to the host's `PluginConfigs` folder.
---
## 🔍 Deep Dive (Advanced)
### Why can't I see the generated config files?
The generated configuration files are not located in your plugin project; they are generated directly into the **Host Application's** `PluginConfigs` folder.
### What was generated?
1. **`MyPlugin_TrimmerRoots.xml`**:
Tells the host's Trimmer to preserve all System libraries required by your plugin that the host might otherwise discard. It also includes an instruction to force-preserve the generated Config class.
2. **`MyPlugin_TrimmingConfig.cs`**:
Contains a class with the `[ModuleInitializer]` attribute. This class runs **automatically** when the host starts, ensuring relevant reflection metadata is loaded to prevent runtime crashes.
### Dependency Graph
```mermaid
graph TD
A[Your Plugin Code] -->|Compile| B(Plugin DLL)
B -->|Smart Exclusion| B
B -->|Auto Analyzer| C{PluginAnalyzer}
C -->|Generate| D[PluginConfigs/MyPlugin_Config.cs]
C -->|Generate| E[PluginConfigs/MyPlugin_TrimmerRoots.xml]
D -->|Auto Delivery| F[Host Project]
E -->|Auto Delivery| F
F -->|Publish| G[Final Executable]
style C fill:#f9f,stroke:#333
style F fill:#bfb,stroke:#333
```
---
## ⚠️ Precautions
1. **Build Order**:
Please ensure the **Host Application is compiled at least once** (in the corresponding Debug or Release mode). The plugin's exclusion script relies on reading the host's output directory for comparison. If the host's bin directory is empty, the plugin might incorrectly bundle all dependencies.
2. **Configuration Consistency**:
If you compile the plugin in `Release` mode, ensure the host is also in `Release` mode. The script automatically looks for the corresponding host output path based on `$(Configuration)`.
3. **Private Dependency Handling**:
If you reference a library that the host also has, but you require a **different version** (this is rare and not recommended), you will need to modify the `.csproj` script to remove the automatic exclusion logic.
---
## 🙋‍♂️ FAQ
**Q: Build error "Command ... exited with code 9009" or "is not recognized as an internal or external command"?**
A: This means the build script cannot find `PluginAnalyzer.exe`. Please check if the `<AnalyzerPath>` configuration in your `.csproj` file is correct.
**Q: Runtime error "FileNotFoundException: BetterLyrics.Core"?**
A: This is normal. `BetterLyrics.Core.dll` should not exist in the plugin directory (as it is provided by the host). Please test by loading the plugin via the host application, rather than running the plugin DLL directly.
**Q: Plugin crashes after Publish, stating a System library is missing?**
A: Please check if the corresponding `*_TrimmerRoots.xml` file was successfully generated in the host's `PluginConfigs` directory. If not, try rebuilding the plugin project.

View File

@@ -1,3 +1,5 @@
**中文** | [**English**](PrivacyPolicy.md)
# BetterLyrics 隐私政策
**生效日期:** 2025 年 6 月 3 日

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