feat: romaji plugin (without dict)

This commit is contained in:
Zhe Fang
2026-01-10 18:07:57 -05:00
parent fea7367671
commit d042993eb7
53 changed files with 2391 additions and 299 deletions

View File

@@ -0,0 +1,20 @@
name: Plugin Registry Check
on:
pull_request:
paths:
- 'Community/plugins-registry.json'
jobs:
check-collision:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Run Hash Collision Check
run: node Community/scripts/check-hash-collision.js

View File

@@ -5,12 +5,8 @@ using System.Text;
namespace BetterLyrics.Core.Interfaces
{
public interface ILyricsProvider
public interface ILyricsSearchPlugin : IPlugin
{
string Id { get; }
string Name { get; }
string Author { get; }
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

@@ -3,12 +3,16 @@ using BetterLyrics.Core.Models;
namespace BetterLyrics.Plugins.Demo
{
public class DemoLyricsProvider : ILyricsProvider
public class DemoLyricsProvider : ILyricsSearchPlugin
{
public string Id => "f7acc86b-6e3d-42c3-a9a9-8c05c5339412";
public string Name => "Demo Plugin";
public string Name => "Plugin name";
public string Author => "jayfunc";
public string Description => "Plugin description";
public void Initialize() { }
public async Task<LyricsSearchResult> GetLyricsAsync(string title, string artist, string album, double duration)
{
await Task.Delay(300);

View File

@@ -0,0 +1,101 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</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>
</Project>

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 = RomajiHelper.ToRomaji(text);
result = string.Join("\r\n", lines.Select(p => string.Join(" ", p.Units.Select(q => q.Romaji))));
}
return Task.FromResult(result);
}
}
}

View File

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

View File

@@ -155,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>();

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" />
@@ -271,6 +272,11 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PluginManagerControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\PatronControl.xaml">
<Generator>MSBuild:Compile</Generator>

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,151 @@
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
{
// TODO: 在 PluginService 里加一个 UninstallPlugin 方法
// 逻辑找到插件对应文件夹Directory.Delete(path, true)
// _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

@@ -47,15 +47,10 @@ namespace BetterLyrics.WinUI3.Models
[NotMapped][JsonIgnore] public LyricsSearchProvider? ProviderIfFound => IsFound ? Provider : null;
[MaxLength(128)]
public string? PluginId { get; set; }
public object Clone()
{
return new LyricsCacheItem()
{
PluginId = this.PluginId,
Provider = this.Provider,
TranslationProvider = this.TranslationProvider,
TransliterationProvider = this.TransliterationProvider,

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

@@ -212,7 +212,7 @@ 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;
@@ -221,13 +221,13 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
results.Add(searchResult);
}
if (_pluginService.Providers.Any())
foreach (var plugin in _pluginService.Plugins)
{
foreach (var plugin in _pluginService.Providers)
{
if (token.IsCancellationRequested) break;
if (token.IsCancellationRequested) break;
var pluginResult = await SearchPluginAsync(songInfo, plugin, token);
if (plugin is ILyricsSearchPlugin lyricsSearchPlugin)
{
var pluginResult = await SearchPluginAsync(songInfo, lyricsSearchPlugin, token);
results.Add(pluginResult);
}
}
@@ -694,12 +694,11 @@ namespace BetterLyrics.WinUI3.Services.LyricsSearchService
return lyricsSearchResult;
}
private async Task<LyricsCacheItem> SearchPluginAsync(SongInfo songInfo, ILyricsProvider plugin, CancellationToken token)
private async Task<LyricsCacheItem> SearchPluginAsync(SongInfo songInfo, ILyricsSearchPlugin plugin, CancellationToken token)
{
var cacheItem = new LyricsCacheItem
{
Provider = LyricsSearchProvider.Plugin,
PluginId = plugin.Id,
};
try

View File

@@ -7,8 +7,10 @@ namespace BetterLyrics.WinUI3.Services.PluginService
{
public interface IPluginService
{
IReadOnlyList<ILyricsProvider> Providers { get; }
IReadOnlyList<IPlugin> Plugins { get; }
void LoadPlugins();
void InstallPlugin(string zipPath);
void UninstallPlugin(string pluginId);
}
}

View File

@@ -11,13 +11,29 @@ namespace BetterLyrics.WinUI3.Services.PluginService
{
private AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath)
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly? Load(AssemblyName assemblyName)
{
var sharedAssemblies = new HashSet<string>
{
"BetterLyrics.Core",
"Microsoft.WindowsAppSDK",
"Microsoft.UI",
"Microsoft.UI.Xaml",
"Microsoft.Graphics",
"System.Runtime",
"Newtonsoft.Json"
};
if (assemblyName.Name == null || sharedAssemblies.Contains(assemblyName.Name))
{
return null;
}
string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
{
@@ -26,5 +42,15 @@ namespace BetterLyrics.WinUI3.Services.PluginService
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string? libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
{
return LoadUnmanagedDllFromPath(libraryPath);
}
return IntPtr.Zero;
}
}
}

View File

@@ -1,21 +1,188 @@
using BetterLyrics.Core.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader; // 必须引用
using Windows.Storage;
namespace BetterLyrics.WinUI3.Services.PluginService
{
public class PluginService : IPluginService
{
private List<ILyricsProvider> _providers = new();
// 1. 核心插件列表
private List<IPlugin> _plugins = new();
public IReadOnlyList<IPlugin> Plugins => _plugins;
public IReadOnlyList<ILyricsProvider> Providers => _providers;
// 2. 新增:上下文管理字典 (Key: 插件ID, Value: 加载上下文)
// 我们需要存着它,以便将来执行 Unload
private Dictionary<string, PluginLoadContext> _pluginContexts = new();
// 3. 新增:已加载的文件路径缓存 (防止同一个 DLL 被扫两遍)
private HashSet<string> _loadedDllPaths = new();
public void LoadPlugins()
{
// 在涉及加载程序集的地方:
// var context = new PluginLoadContext(pluginPath);
// 它是本文件夹下的 internal 或者是 public 类,直接用即可。
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)
{
// 🔥 防御 1基于路径的检查
// 如果这个文件已经在内存里了,绝对不要再 Load 一次
if (_loadedDllPaths.Contains(dllPath)) continue;
TryLoadPlugin(dllPath);
}
}
}
private void TryLoadPlugin(string dllPath)
{
try
{
// 创建上下文
var loadContext = new PluginLoadContext(dllPath);
// 加载程序集
var assembly = loadContext.LoadFromAssemblyPath(dllPath);
bool isPluginFound = false;
foreach (var type in assembly.GetExportedTypes())
{
if (typeof(IPlugin).IsAssignableFrom(type) && !type.IsAbstract)
{
// 实例化
var plugin = (IPlugin?)Activator.CreateInstance(type);
if (plugin == null) continue;
// 🔥 防御 2基于 ID 的检查
// 防止 "不同 DLL 或者是新版本" 导致 ID 冲突
if (_plugins.Any(p => p.Id == plugin.Id))
{
// 遇到重复 ID我们选择跳过新的保留旧的
// (或者你也可以设计成卸载旧的加载新的,这取决于策略)
// 由于我们已经加载了 assembly现在决定不用它必须卸载 context
loadContext.Unload();
return;
}
// 初始化插件 (如果有 Initialize 方法)
try
{
plugin.Initialize();
}
catch (Exception initEx)
{
// 如果初始化失败(比如缺字典文件),就不应该把它加到列表里
// 记录日志...
loadContext.Unload();
return;
}
// ✅ 成功入库
_plugins.Add(plugin);
_pluginContexts.Add(plugin.Id, loadContext); // 记录上下文
isPluginFound = true;
}
}
// 如果这个 DLL 里找到了插件,标记路径为已加载
if (isPluginFound)
{
_loadedDllPaths.Add(dllPath);
}
else
{
// 如果这个 DLL 里一个插件都没找到 (可能是依赖库)
// 为了节省内存,我们可以把这个 Context 卸载掉
// (前提是其他插件不依赖它,这块比较复杂,简单起见可以先卸载)
loadContext.Unload();
}
}
catch (Exception ex)
{
// 记录日志...
// throw new Exception($"Failed to load plugin from {dllPath}: {ex.Message}", ex);
}
}
public void UninstallPlugin(string pluginId)
{
var plugin = _plugins.FirstOrDefault(p => p.Id == pluginId);
if (plugin == null) return;
// 1. 获取相关信息
var dllPath = plugin.GetType().Assembly.Location;
var folderPath = Path.GetDirectoryName(dllPath);
// 2. 从列表中移除插件对象
_plugins.Remove(plugin);
_loadedDllPaths.Remove(dllPath); // 允许下次重新加载这个路径
// 3. 💥 核心:卸载上下文 (释放文件锁的关键)
if (_pluginContexts.TryGetValue(pluginId, out var context))
{
context.Unload();
_pluginContexts.Remove(pluginId);
}
// 4. 强制 GC (垃圾回收)
// 上下文卸载是“软卸载”,必须等 GC 跑过之后,文件锁才会真正释放
// 这几行代码对于“热删除”非常重要
GC.Collect();
GC.WaitForPendingFinalizers();
// 5. 物理删除文件
if (Directory.Exists(folderPath))
{
try
{
Directory.Delete(folderPath, true);
}
catch (IOException)
{
// 如果 GC 还没来得及释放锁,可能会报错
// 实际生产中,通常是标记为“待删除”,下次重启时删
// 或者提示用户重启
}
}
}
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);
// 如果已经存在,说明是更新或者重装
// 我们需要先根据文件夹找到旧插件的 ID然后执行标准的卸载流程
// (这里简化处理:直接尝试删文件夹,如果删不掉说明被占用)
if (Directory.Exists(installDir))
{
// TODO: 最好是先 Find plugin by path -> UninstallPlugin(id)
// 否则文件被锁住无法 Delete
try
{
Directory.Delete(installDir, true);
}
catch { /* 忽略或报错 */ }
}
Directory.CreateDirectory(installDir);
ZipFile.ExtractToDirectory(zipPath, installDir);
// 安装完后,如果不重启软件,你想立即生效的话:
// LoadPlugins(); // 因为加了路径去重,这里重新调一次是安全的
}
}
}
}

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,56 @@ 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)
//{
// if (string.IsNullOrWhiteSpace(text))
// {
// throw new Exception(text + " is empty or null.");
// }
// if (string.IsNullOrEmpty(_settingsService.AppSettings.TranslationSettings.CutletDockerServer))
// {
// throw new Exception("cutlet-docker server URL is not set in settings.");
// }
// 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;
//}
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>

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>

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>

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>

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>

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>

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>

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>

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>

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>

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>

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>

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>

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>

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>

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>

View File

@@ -56,6 +56,12 @@
Glyph=&#xE9D2;}"
Tag="Stats" />
<NavigationViewItem
Content="Plugins"
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

@@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.Core", "Better
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.Plugins.Demo", "BetterLyrics.Plugins.Demo\BetterLyrics.Plugins.Demo.csproj", "{87D235CA-4311-4766-8186-AD9B193DFABC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.Plugins.Romaji", "BetterLyrics.Plugins.Romaji\BetterLyrics.Plugins.Romaji.csproj", "{DD2D477F-94CD-4D4B-8B59-C127F2850E34}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RomajiConverter.Core", "RomajiConverter.Core\RomajiConverter.Core.csproj", "{351807DB-CD63-B939-8071-B1FBFF969569}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -103,6 +107,30 @@ Global
{87D235CA-4311-4766-8186-AD9B193DFABC}.Release|x64.Build.0 = Release|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Release|x86.ActiveCfg = Release|Any CPU
{87D235CA-4311-4766-8186-AD9B193DFABC}.Release|x86.Build.0 = Release|Any CPU
{DD2D477F-94CD-4D4B-8B59-C127F2850E34}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{DD2D477F-94CD-4D4B-8B59-C127F2850E34}.Debug|ARM64.Build.0 = Debug|Any CPU
{DD2D477F-94CD-4D4B-8B59-C127F2850E34}.Debug|x64.ActiveCfg = Debug|Any CPU
{DD2D477F-94CD-4D4B-8B59-C127F2850E34}.Debug|x64.Build.0 = Debug|Any CPU
{DD2D477F-94CD-4D4B-8B59-C127F2850E34}.Debug|x86.ActiveCfg = Debug|Any CPU
{DD2D477F-94CD-4D4B-8B59-C127F2850E34}.Debug|x86.Build.0 = Debug|Any CPU
{DD2D477F-94CD-4D4B-8B59-C127F2850E34}.Release|ARM64.ActiveCfg = Release|Any CPU
{DD2D477F-94CD-4D4B-8B59-C127F2850E34}.Release|ARM64.Build.0 = Release|Any CPU
{DD2D477F-94CD-4D4B-8B59-C127F2850E34}.Release|x64.ActiveCfg = Release|Any CPU
{DD2D477F-94CD-4D4B-8B59-C127F2850E34}.Release|x64.Build.0 = Release|Any CPU
{DD2D477F-94CD-4D4B-8B59-C127F2850E34}.Release|x86.ActiveCfg = Release|Any CPU
{DD2D477F-94CD-4D4B-8B59-C127F2850E34}.Release|x86.Build.0 = Release|Any CPU
{351807DB-CD63-B939-8071-B1FBFF969569}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{351807DB-CD63-B939-8071-B1FBFF969569}.Debug|ARM64.Build.0 = Debug|Any CPU
{351807DB-CD63-B939-8071-B1FBFF969569}.Debug|x64.ActiveCfg = Debug|Any CPU
{351807DB-CD63-B939-8071-B1FBFF969569}.Debug|x64.Build.0 = Debug|Any CPU
{351807DB-CD63-B939-8071-B1FBFF969569}.Debug|x86.ActiveCfg = Debug|Any CPU
{351807DB-CD63-B939-8071-B1FBFF969569}.Debug|x86.Build.0 = Debug|Any CPU
{351807DB-CD63-B939-8071-B1FBFF969569}.Release|ARM64.ActiveCfg = Release|Any CPU
{351807DB-CD63-B939-8071-B1FBFF969569}.Release|ARM64.Build.0 = Release|Any CPU
{351807DB-CD63-B939-8071-B1FBFF969569}.Release|x64.ActiveCfg = Release|Any CPU
{351807DB-CD63-B939-8071-B1FBFF969569}.Release|x64.Build.0 = Release|Any CPU
{351807DB-CD63-B939-8071-B1FBFF969569}.Release|x86.ActiveCfg = Release|Any CPU
{351807DB-CD63-B939-8071-B1FBFF969569}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,40 @@
// Community/scripts/check-hash-collision.js
const fs = require('fs');
const path = require('path');
const registryPath = path.join(__dirname, '../plugins-registry.json');
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
function getStableId(pluginId) {
let hash = 23;
for (let i = 0; i < pluginId.length; i++) {
hash = (hash * 31 + pluginId.charCodeAt(i)) | 0;
}
return Math.abs(hash) + 1000;
}
const seenIds = new Set();
let hasError = false;
console.log("🔍 Starting to check for plugin ID conflicts...");
registry.forEach(item => {
const stableId = getStableId(item.id);
console.log(`Checking [${item.id}] -> Hash: ${stableId}`);
if (seenIds.has(stableId)) {
console.error(`⛔ Fatel error! Conflict detected!`);
console.error(`The hash value (${stableId}) calculated from the plugin ID [${item.id}] is duplicated with an existing plugin.`);
hasError = true;
}
seenIds.add(stableId);
});
if (hasError) {
console.log("⛔ Check failed, please change the plugin ID.");
process.exit(1);
} else {
console.log("✅ The check passed; no conflicts were found.");
process.exit(0);
}

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,239 @@
using OpenAI;
using OpenAI.Chat;
using RomajiConverter.Core.Models;
using RomajiConverter.Core.Options;
using System;
using System.ClientModel;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace RomajiConverter.Core.Helpers
{
public static class RomajiAIHelper
{
public const string Prompt = @"用户将输入一段日文歌词,你需要逐词转换为以下格式:
- 每行输出必须严格对应每行输入,禁止额外添加换行,禁止输出空行,不能因为遇到标点符号而换行,换行符必须使用单个\n
- 对每行日文进行分词处理,分词应以现代日语常规形态(助词、助动词、词尾变化)为最小单位
- 如果一个分词是日文且包含汉字,则需要给出平假名,用小括号在原文后标注,格式为:日文分词(平假名)。禁止在分词中间标注假名x(xx)xx要么标注整个分词的假名要么将标注之后的部分拆分为新的分词
- 纯假名分词不添加任何假名标注
- 遇到仅当 は/へ/を 作为独立分词并起语法助词作用时,在后面添加“|”以及它的口语化假名,非助词情况下只输出原文
- 遇到英文单词/字母、数字、标点符号、特殊符号、等非日文的unicode字符时必须保留且单独作为一个分词必须只输出原文不能给出平假名
- 每个分词之间必须用半角空格分隔
- 如果无法确定某分词是否为助词或其读音,请优先保持原文不转换
- 不要包含任何解释、注释、Markdown、额外字段或文本
- 示例仅供参考,不能直接输出,任何时候都需要根据上面给出的文本进行转换
示例:
输入昨日はColdな夜へ行を歌った
输出:昨日(きのう) は|わ Cold な 夜(よる) へ|え 行(い) を|お 歌った(うたった)";
private static Regex _formatRegex = new Regex(@"^(.*?)(\((.*?)\))*?(\|(.*?))*?$", RegexOptions.Compiled);
private static ChatCompletionOptions _chatCompletionOptions = new ChatCompletionOptions
{
Temperature = 0.2f
};
public static async Task LoadRomajiAsync(ICollection<ConvertedLine> convertedLines, string text, ToRomajiAIOptions options, CancellationToken cancellationToken = default)
{
//预处理为ConvertedLine列表, 其中会包含空行
var cacheList = GetCacheList(options, text);
if (cacheList.Count == 0) return;
//获取ai结果
var client = new ChatClient(
model: options.Model,
credential: new ApiKeyCredential(options.ApiKey),
options: new OpenAIClientOptions
{
Endpoint = new Uri(options.BaseUrl)
}
);
var prompt = string.IsNullOrEmpty(options.Prompt) ? Prompt : options.Prompt;
//发送的内容不包含空行
var content = string.Join("\n", cacheList.Where(p => !string.IsNullOrWhiteSpace(p.Japanese)).Select(p => p.Japanese));
var messages = new List<ChatMessage>
{
new SystemChatMessage(prompt),
new UserChatMessage(content)
};
Debug.WriteLine(prompt);
Debug.WriteLine(content);
var completionUpdates = client.CompleteChatStreamingAsync(messages, _chatCompletionOptions, cancellationToken: cancellationToken);
var stringBuilder = new StringBuilder();
ushort lineIndex = 0;
var l = 0;
var r = 0;
//插入直到下一个非空行
AddNextNotEmptyLine();
//处理流式返回
var enumerator = completionUpdates.GetAsyncEnumerator(cancellationToken);
try
{
while (await enumerator.MoveNextAsync())
{
var completionUpdate = enumerator.Current;
if (completionUpdate.ContentUpdate.Count > 0)
{
var delta = FixFormat(completionUpdate.ContentUpdate[0].Text);
if (string.IsNullOrEmpty(delta)) continue;
stringBuilder.Append(delta);
Debug.Write(completionUpdate.ContentUpdate[0].Text);
while (r < stringBuilder.Length)
{
if (stringBuilder[r] == '\n')
{
InsertUnit();
//插入直到下一个非空行
AddNextNotEmptyLine();
r++;
l = r;
}
else if (stringBuilder[r] == ' ')
{
InsertUnit();
r++;
l = r;
}
else
{
r++;
}
}
}
}
}
finally
{
await enumerator.DisposeAsync();
}
//处理完成,手动插入最后一个分词
if (l != r)
{
InsertUnit();
}
return;
void AddNextNotEmptyLine()
{
do
{
var newLine = new ConvertedLine
{
Time = lineIndex >= cacheList.Count ? (TimeSpan?)null : cacheList[lineIndex].Time,
Chinese = lineIndex >= cacheList.Count ? string.Empty : cacheList[lineIndex].Chinese,
Index = lineIndex,
Japanese = lineIndex >= cacheList.Count ? string.Empty : cacheList[lineIndex].Japanese
};
convertedLines.Add(newLine);
lineIndex++;
} while (string.IsNullOrWhiteSpace(convertedLines.Last().Japanese) && lineIndex < cacheList.Count);
}
void InsertUnit()
{
var lastLine = convertedLines.Last();
var lastUnitStr = stringBuilder.ToString(l, r - l);
if (!string.IsNullOrEmpty(lastUnitStr))
lastLine.Units.Add(GetUnit(lastLine.Index, lastUnitStr, options.IsParticleAsPronunciation));
}
}
private static List<ConvertedLine> GetCacheList(ToRomajiAIOptions options, string text)
{
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 lyrics = LrcParser.Parse(lineTextList[i]);
timeSpans.Add(lyrics.Count > 0 ? lyrics[0].Time : (TimeSpan?)null);
lineTextList[i] = lyrics.Count > 0 ? lyrics[0].Text : lineTextList[i];
}
else
{
timeSpans.Add(null);
}
}
var cacheList = new List<ConvertedLine>();
for (var index = 0; index < lineTextList.Count; index++)
{
var line = lineTextList[index];
if (RomajiHelper.IsChinese(line, options.ChineseRate)) continue;
var convertedLine = new ConvertedLine
{
Time = index < timeSpans.Count ? timeSpans[index] : null,
Japanese = line.Replace("\0", "")
};
if (index + 1 < lineTextList.Count &&
RomajiHelper.IsChinese(lineTextList[index + 1], options.ChineseRate))
convertedLine.Chinese = lineTextList[index + 1];
convertedLine.Index = (ushort)cacheList.Count;
cacheList.Add(convertedLine);
}
return cacheList;
}
private static string FixFormat(string content)
{
content = content.Replace("\r", "");
content = content.Replace("\\n", "\n");
return content;
}
private static ConvertedUnit GetUnit(ushort lineIndex, string unitString, bool isParticleAsPronunciation)
{
var match = _formatRegex.Match(unitString);
if (!match.Success)
{
return new ConvertedUnit(lineIndex, unitString, KanaHelper.ToHiragana(unitString),
KanaHelper.KatakanaToRomaji(unitString), false);
}
var origin = match.Groups[1].Value;
var kanji_gana = match.Groups[3].Value;
var particle_gana = match.Groups[5].Value;
if (!string.IsNullOrEmpty(kanji_gana))
{
return new ConvertedUnit(lineIndex, origin, kanji_gana,
KanaHelper.KatakanaToRomaji(kanji_gana), true);
}
else if (isParticleAsPronunciation && !string.IsNullOrEmpty(particle_gana))
{
return new ConvertedUnit(lineIndex, origin, particle_gana,
KanaHelper.KatakanaToRomaji(particle_gana), false);
}
else
{
return new ConvertedUnit(lineIndex, origin, KanaHelper.ToHiragana(origin),
KanaHelper.KatakanaToRomaji(origin), false);
}
}
}
}

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,16 @@
namespace RomajiConverter.Core.Options
{
public class ToRomajiAIOptions : ToRomajiOptions
{
public string BaseUrl { get; set; }
public string Model { get; set; }
public string ApiKey { get; set; }
/// <summary>
/// 提示词,可以不传,使用默认提示词
/// </summary>
public string Prompt { get; set; }
}
}

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,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<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" />
<PackageReference Include="OpenAI" Version="2.8.0" />
</ItemGroup>
<ItemGroup>
<Content Include="RomajiConverter.Core.targets" PackagePath="build/RomajiConverter.Core.targets" />
<None Include="docs\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,13 @@
<Project>
<ItemGroup>
<UniDicFiles Include="$(MSBuildThisFileDirectory)\..\contentFiles\any\any\unidic\**\*" />
<CustomizeDict Include="$(MSBuildThisFileDirectory)\..\contentFiles\any\any\customizeDict.txt" />
<VariantFiles Include="$(MSBuildThisFileDirectory)\..\contentFiles\any\any\Variants\**\*" />
</ItemGroup>
<Target Name="CopyFiles" BeforeTargets="Build">
<Copy SourceFiles="@(UniDicFiles)" DestinationFolder="$(TargetDir)unidic\%(RecursiveDir)" />
<Copy SourceFiles="@(CustomizeDict)" DestinationFolder="$(TargetDir)" />
<Copy SourceFiles="@(VariantFiles)" DestinationFolder="$(TargetDir)Variants\%(RecursiveDir)" />
</Target>
</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; }
}
```