feat: Create a new GUI project based on Avalonia. (Powered by Claude)

This commit is contained in:
real-zony
2026-01-09 23:38:57 +08:00
parent 00e2118645
commit 1f7414ead3
46 changed files with 3225 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
using Microsoft.Extensions.Localization;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
namespace ZonyLrcTools.Cli.Infrastructure.Localization;
/// <summary>
/// Provides localization services for CLI commands and options.
/// </summary>
public interface ICliLocalizationService
{
/// <summary>
/// Gets a localized string by key.
/// </summary>
string this[string key] { get; }
}
/// <summary>
/// Implementation of CLI localization service.
/// </summary>
public class CliLocalizationService : ICliLocalizationService, ISingletonDependency
{
private readonly IStringLocalizer<Cli.CommandStrings> _localizer;
public CliLocalizationService(IStringLocalizer<Cli.CommandStrings> localizer)
{
_localizer = localizer;
}
public string this[string key]
{
get
{
var value = _localizer[key];
return value.ResourceNotFound ? $"[{key}]" : value.Value;
}
}
}

View File

@@ -0,0 +1,10 @@
namespace ZonyLrcTools.Cli;
/// <summary>
/// Marker class for CLI command localization resources.
/// This class is used by IStringLocalizer&lt;CommandStrings&gt; to locate the resource files.
/// The namespace must be the root namespace so that IStringLocalizer looks for Resources/CommandStrings.resx
/// </summary>
public class CommandStrings
{
}

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Main Program -->
<data name="Program_Description" xml:space="preserve">
<value>ZonyLrcTools - Lyrics download command-line tool</value>
</data>
<!-- Download Command -->
<data name="Download_Description" xml:space="preserve">
<value>Download lyrics or album covers</value>
</data>
<data name="Download_Dir_Description" xml:space="preserve">
<value>Specify the folder path containing music files</value>
</data>
<data name="Download_Lyrics_Description" xml:space="preserve">
<value>Download lyrics files</value>
</data>
<data name="Download_Album_Description" xml:space="preserve">
<value>Download album covers</value>
</data>
<data name="Download_CSV_Description" xml:space="preserve">
<value>Import song list from CSV file for download</value>
</data>
<data name="Download_SongList_Description" xml:space="preserve">
<value>Import song list from NetEase playlist ID for download</value>
</data>
<!-- Utility Command -->
<data name="Utility_Description" xml:space="preserve">
<value>Utility commands</value>
</data>
<data name="Utility_Convert_Description" xml:space="preserve">
<value>Convert encrypted music files to normal format</value>
</data>
<data name="Utility_Source_Description" xml:space="preserve">
<value>Specify the folder path to convert</value>
</data>
<!-- Search Command -->
<data name="Search_Description" xml:space="preserve">
<value>Search for lyrics</value>
</data>
<data name="Search_Name_Description" xml:space="preserve">
<value>Song name</value>
</data>
<data name="Search_Artist_Description" xml:space="preserve">
<value>Artist name</value>
</data>
<!-- Common Options -->
<data name="Option_Help" xml:space="preserve">
<value>Show help information</value>
</data>
<data name="Option_Version" xml:space="preserve">
<value>Show version information</value>
</data>
</root>

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Main Program -->
<data name="Program_Description" xml:space="preserve">
<value>ZonyLrcTools - 歌词下载命令行工具</value>
</data>
<!-- Download Command -->
<data name="Download_Description" xml:space="preserve">
<value>下载歌词或专辑封面</value>
</data>
<data name="Download_Dir_Description" xml:space="preserve">
<value>指定音乐文件所在的文件夹路径</value>
</data>
<data name="Download_Lyrics_Description" xml:space="preserve">
<value>下载歌词文件</value>
</data>
<data name="Download_Album_Description" xml:space="preserve">
<value>下载专辑封面</value>
</data>
<data name="Download_CSV_Description" xml:space="preserve">
<value>从 CSV 文件导入歌曲列表进行下载</value>
</data>
<data name="Download_SongList_Description" xml:space="preserve">
<value>从网易云歌单 ID 导入歌曲列表进行下载</value>
</data>
<!-- Utility Command -->
<data name="Utility_Description" xml:space="preserve">
<value>工具类命令</value>
</data>
<data name="Utility_Convert_Description" xml:space="preserve">
<value>转换加密音乐文件为普通格式</value>
</data>
<data name="Utility_Source_Description" xml:space="preserve">
<value>指定需要转换的文件夹路径</value>
</data>
<!-- Search Command -->
<data name="Search_Description" xml:space="preserve">
<value>搜索歌词</value>
</data>
<data name="Search_Name_Description" xml:space="preserve">
<value>歌曲名称</value>
</data>
<data name="Search_Artist_Description" xml:space="preserve">
<value>艺术家名称</value>
</data>
<!-- Common Options -->
<data name="Option_Help" xml:space="preserve">
<value>显示帮助信息</value>
</data>
<data name="Option_Version" xml:space="preserve">
<value>显示版本信息</value>
</data>
</root>

View File

@@ -0,0 +1,21 @@
namespace ZonyLrcTools.Common.Infrastructure.Exceptions;
/// <summary>
/// 错误码帮助类接口。
/// </summary>
public interface IErrorCodeHelper
{
/// <summary>
/// 获取错误消息。
/// </summary>
/// <param name="errorCode">错误代码。</param>
/// <returns>对应的错误消息。</returns>
string GetMessage(int errorCode);
/// <summary>
/// 获取警告消息。
/// </summary>
/// <param name="warningCode">警告代码。</param>
/// <returns>对应的警告消息。</returns>
string GetWarningMessage(int warningCode);
}

View File

@@ -0,0 +1,44 @@
using System.Globalization;
namespace ZonyLrcTools.Common.Infrastructure.Localization;
/// <summary>
/// Provides localization services for the application.
/// </summary>
public interface ILocalizationService
{
/// <summary>
/// Gets a localized string by key.
/// </summary>
string this[string key] { get; }
/// <summary>
/// Gets a localized string by key with format arguments.
/// </summary>
string this[string key, params object[] args] { get; }
/// <summary>
/// Gets an error message by error code.
/// </summary>
string GetErrorMessage(int errorCode);
/// <summary>
/// Gets a warning message by warning code.
/// </summary>
string GetWarningMessage(int warningCode);
/// <summary>
/// Sets the current culture.
/// </summary>
void SetCulture(string cultureName);
/// <summary>
/// Gets the current culture name.
/// </summary>
string CurrentCulture { get; }
/// <summary>
/// Gets the list of supported cultures.
/// </summary>
IReadOnlyList<CultureInfo> SupportedCultures { get; }
}

View File

@@ -0,0 +1,72 @@
using System.Globalization;
using Microsoft.Extensions.Localization;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
namespace ZonyLrcTools.Common.Infrastructure.Localization;
/// <summary>
/// Implementation of <see cref="ILocalizationService"/> using .NET localization.
/// </summary>
public class LocalizationService : ILocalizationService, ISingletonDependency
{
private readonly IStringLocalizer<Common.Messages> _messagesLocalizer;
private readonly IStringLocalizer<Common.ErrorMessages> _errorLocalizer;
private static readonly CultureInfo[] _supportedCultures =
{
new("zh-CN"),
new("en-US")
};
public LocalizationService(
IStringLocalizer<Common.Messages> messagesLocalizer,
IStringLocalizer<Common.ErrorMessages> errorLocalizer)
{
_messagesLocalizer = messagesLocalizer;
_errorLocalizer = errorLocalizer;
}
public string this[string key]
{
get
{
var value = _messagesLocalizer[key];
return value.ResourceNotFound ? $"[{key}]" : value.Value;
}
}
public string this[string key, params object[] args]
{
get
{
var value = _messagesLocalizer[key];
if (value.ResourceNotFound) return $"[{key}]";
return string.Format(value.Value, args);
}
}
public string GetErrorMessage(int errorCode)
{
var key = $"Error_{errorCode}";
var value = _errorLocalizer[key];
return value.ResourceNotFound ? $"Unknown error: {errorCode}" : value.Value;
}
public string GetWarningMessage(int warningCode)
{
var key = $"Warning_{warningCode}";
var value = _errorLocalizer[key];
return value.ResourceNotFound ? $"Unknown warning: {warningCode}" : value.Value;
}
public string CurrentCulture => CultureInfo.CurrentUICulture.Name;
public IReadOnlyList<CultureInfo> SupportedCultures => _supportedCultures;
public void SetCulture(string cultureName)
{
var culture = new CultureInfo(cultureName);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
}

View File

@@ -0,0 +1,10 @@
namespace ZonyLrcTools.Common;
/// <summary>
/// Marker class for error message localization resources.
/// This class is used by IStringLocalizer&lt;ErrorMessages&gt; to locate the resource files.
/// The namespace must be the root namespace so that IStringLocalizer looks for Resources/ErrorMessages.resx
/// </summary>
public class ErrorMessages
{
}

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Error Messages -->
<data name="Error_10001" xml:space="preserve">
<value>The file extension to search cannot be empty.</value>
</data>
<data name="Error_10002" xml:space="preserve">
<value>The directory to scan does not exist, please verify the path.</value>
</data>
<data name="Error_10003" xml:space="preserve">
<value>Unable to get the file extension information.</value>
</data>
<data name="Error_10004" xml:space="preserve">
<value>No music files were found.</value>
</data>
<data name="Error_10005" xml:space="preserve">
<value>The specified encoding is not supported, please check the configuration.</value>
</data>
<data name="Error_10006" xml:space="preserve">
<value>Unable to get the song list from NetEase Music.</value>
</data>
<!-- Warning Messages -->
<data name="Warning_50001" xml:space="preserve">
<value>An error occurred while scanning files.</value>
</data>
<data name="Warning_50002" xml:space="preserve">
<value>Both song name and artist name are empty, unable to search.</value>
</data>
<data name="Warning_50003" xml:space="preserve">
<value>Song name cannot be empty, unable to search.</value>
</data>
<data name="Warning_50004" xml:space="preserve">
<value>The downloader did not find the corresponding song information.</value>
</data>
<data name="Warning_50005" xml:space="preserve">
<value>The download request returned an invalid response, possibly a server error.</value>
</data>
<data name="Warning_50006" xml:space="preserve">
<value>Tag info reader is empty, unable to parse music tag information.</value>
</data>
<data name="Warning_50007" xml:space="preserve">
<value>TagLib tag reader encountered an unexpected exception.</value>
</data>
<data name="Warning_50008" xml:space="preserve">
<value>Service interface restricted, unable to make request, please try using a proxy server.</value>
</data>
<data name="Warning_50009" xml:space="preserve">
<value>HTTP request to target server failed.</value>
</data>
<data name="Warning_50010" xml:space="preserve">
<value>Failed to deserialize HTTP response to JSON.</value>
</data>
<data name="Warning_50011" xml:space="preserve">
<value>Currently only NCM format song conversion is supported.</value>
</data>
</root>

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Error Messages -->
<data name="Error_10001" xml:space="preserve">
<value>待搜索的后缀不能为空。</value>
</data>
<data name="Error_10002" xml:space="preserve">
<value>需要扫描的目录不存在,请确认路径是否正确。</value>
</data>
<data name="Error_10003" xml:space="preserve">
<value>不能获取文件的后缀信息。</value>
</data>
<data name="Error_10004" xml:space="preserve">
<value>没有扫描到任何音乐文件。</value>
</data>
<data name="Error_10005" xml:space="preserve">
<value>指定的编码不受支持,请检查配置。</value>
</data>
<data name="Error_10006" xml:space="preserve">
<value>无法从网易云音乐获取歌曲列表。</value>
</data>
<!-- Warning Messages -->
<data name="Warning_50001" xml:space="preserve">
<value>扫描文件时出现了错误。</value>
</data>
<data name="Warning_50002" xml:space="preserve">
<value>歌曲名称或歌手名称均为空,无法进行搜索。</value>
</data>
<data name="Warning_50003" xml:space="preserve">
<value>歌曲名称不能为空,无法进行搜索。</value>
</data>
<data name="Warning_50004" xml:space="preserve">
<value>下载器没有搜索到对应的歌曲信息。</value>
</data>
<data name="Warning_50005" xml:space="preserve">
<value>下载请求的返回值不合法,可能是服务端故障。</value>
</data>
<data name="Warning_50006" xml:space="preserve">
<value>标签信息读取器为空,无法解析音乐 Tag 信息。</value>
</data>
<data name="Warning_50007" xml:space="preserve">
<value>TagLib 标签读取器出现了预期之外的异常。</value>
</data>
<data name="Warning_50008" xml:space="preserve">
<value>服务接口限制,无法进行请求,请尝试使用代理服务器。</value>
</data>
<data name="Warning_50009" xml:space="preserve">
<value>对目标服务器执行 HTTP 请求失败。</value>
</data>
<data name="Warning_50010" xml:space="preserve">
<value>HTTP 请求的结果反序列化为 JSON 失败。</value>
</data>
<data name="Warning_50011" xml:space="preserve">
<value>目前仅支持 NCM 格式的歌曲转换操作。</value>
</data>
</root>

View File

@@ -0,0 +1,10 @@
namespace ZonyLrcTools.Common;
/// <summary>
/// Marker class for general message localization resources.
/// This class is used by IStringLocalizer&lt;Messages&gt; to locate the resource files.
/// The namespace must be the root namespace so that IStringLocalizer looks for Resources/Messages.resx
/// </summary>
public class Messages
{
}

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Lyrics Download -->
<data name="Lyrics_DownloadStart" xml:space="preserve">
<value>Starting lyrics file download...</value>
</data>
<data name="Lyrics_DownloadComplete" xml:space="preserve">
<value>Lyrics download complete, success: {0} skipped (instrumental): {1} failed: {2}.</value>
</data>
<data name="Lyrics_SongDownloadSuccess" xml:space="preserve">
<value>Song: {0}, Artist: {1}, download successful.</value>
</data>
<data name="Lyrics_SongDownloadFailed" xml:space="preserve">
<value>Song: {0}, Artist: {1}, download failed.</value>
</data>
<!-- Album Download -->
<data name="Album_DownloadStart" xml:space="preserve">
<value>Starting album cover download...</value>
</data>
<data name="Album_DownloadComplete" xml:space="preserve">
<value>Album cover download complete, success: {0} failed: {1}.</value>
</data>
<!-- File Scanner -->
<data name="Scanner_Start" xml:space="preserve">
<value>Starting folder scan, please wait...</value>
</data>
<data name="Scanner_Complete" xml:space="preserve">
<value>Scan complete, {0} files found.</value>
</data>
<!-- General -->
<data name="General_Starting" xml:space="preserve">
<value>Starting...</value>
</data>
<data name="General_UnhandledException" xml:space="preserve">
<value>An unhandled exception occurred.</value>
</data>
<data name="General_ErrorCode" xml:space="preserve">
<value>Error code: {0}</value>
</data>
<data name="General_ErrorMessage" xml:space="preserve">
<value>Error message: {0}</value>
</data>
</root>

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Lyrics Download -->
<data name="Lyrics_DownloadStart" xml:space="preserve">
<value>开始下载歌词文件数据...</value>
</data>
<data name="Lyrics_DownloadComplete" xml:space="preserve">
<value>歌词数据下载完成,成功: {0} 跳过(纯音乐): {1} 失败: {2}。</value>
</data>
<data name="Lyrics_SongDownloadSuccess" xml:space="preserve">
<value>歌曲名: {0}, 艺术家: {1}, 下载成功。</value>
</data>
<data name="Lyrics_SongDownloadFailed" xml:space="preserve">
<value>歌曲名: {0}, 艺术家: {1}, 下载失败。</value>
</data>
<!-- Album Download -->
<data name="Album_DownloadStart" xml:space="preserve">
<value>开始下载专辑封面...</value>
</data>
<data name="Album_DownloadComplete" xml:space="preserve">
<value>专辑封面下载完成,成功: {0} 失败: {1}。</value>
</data>
<!-- File Scanner -->
<data name="Scanner_Start" xml:space="preserve">
<value>开始扫描文件夹,请稍等...</value>
</data>
<data name="Scanner_Complete" xml:space="preserve">
<value>扫描完成,共 {0} 个文件。</value>
</data>
<!-- General -->
<data name="General_Starting" xml:space="preserve">
<value>正在启动...</value>
</data>
<data name="General_UnhandledException" xml:space="preserve">
<value>出现了未处理的异常。</value>
</data>
<data name="General_ErrorCode" xml:space="preserve">
<value>错误代码: {0}</value>
</data>
<data name="General_ErrorMessage" xml:space="preserve">
<value>错误信息: {0}</value>
</data>
</root>

View File

@@ -0,0 +1,33 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ZonyLrcTools.Desktop.App"
RequestedThemeVariant="Default">
<Application.Styles>
<FluentTheme />
</Application.Styles>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="AppBackgroundBrush">#FFFFFF</SolidColorBrush>
<SolidColorBrush x:Key="AppAccentBrush">#0078D4</SolidColorBrush>
<SolidColorBrush x:Key="NavigationBackgroundBrush">#F3F3F3</SolidColorBrush>
<SolidColorBrush x:Key="SuccessColorBrush">#107C10</SolidColorBrush>
<SolidColorBrush x:Key="ErrorColorBrush">#D13438</SolidColorBrush>
<SolidColorBrush x:Key="WarningColorBrush">#FFB900</SolidColorBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="AppBackgroundBrush">#1F1F1F</SolidColorBrush>
<SolidColorBrush x:Key="AppAccentBrush">#60CDFF</SolidColorBrush>
<SolidColorBrush x:Key="NavigationBackgroundBrush">#2D2D2D</SolidColorBrush>
<SolidColorBrush x:Key="SuccessColorBrush">#6CCB5F</SolidColorBrush>
<SolidColorBrush x:Key="ErrorColorBrush">#FF99A4</SolidColorBrush>
<SolidColorBrush x:Key="WarningColorBrush">#FCE100</SolidColorBrush>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,47 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using ZonyLrcTools.Desktop.Services;
using ZonyLrcTools.Desktop.ViewModels;
using ZonyLrcTools.Desktop.Views;
namespace ZonyLrcTools.Desktop;
public partial class App : Application
{
public static ServiceProvider? Services { get; private set; }
public override void Initialize()
{
// Configure DI container BEFORE loading XAML
// This is required because LocalizeExtension needs App.Services during XAML loading
var services = new ServiceCollection();
services.AddDesktopServices();
Services = services.BuildServiceProvider();
// Initialize language settings before loading XAML
ServiceCollectionExtensions.InitializeLanguage();
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
// Avoid duplicate validations from both Avalonia and CommunityToolkit
BindingPlugins.DataValidators.RemoveAt(0);
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var mainViewModel = Services!.GetRequiredService<MainWindowViewModel>();
desktop.MainWindow = new MainWindow
{
DataContext = mainViewModel
};
}
base.OnFrameworkInitializationCompleted();
}
}

View File

@@ -0,0 +1,142 @@
using Avalonia.Data;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
namespace ZonyLrcTools.Desktop.Infrastructure.Localization;
/// <summary>
/// Avalonia markup extension for localization.
/// Usage: Text="{loc:Localize Key=Nav_Home}"
/// </summary>
public class LocalizeExtension : MarkupExtension
{
public LocalizeExtension()
{
}
public LocalizeExtension(string key)
{
Key = key;
}
/// <summary>
/// The resource key to look up.
/// </summary>
public string? Key { get; set; }
/// <summary>
/// Default value if the key is not found.
/// </summary>
public string? Default { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (string.IsNullOrEmpty(Key))
{
return Default ?? "[NO_KEY]";
}
var localizer = App.Services?.GetService<IStringLocalizer<Desktop.UIStrings>>();
if (localizer == null)
{
return Default ?? $"[{Key}]";
}
var localizedValue = localizer[Key];
if (localizedValue.ResourceNotFound)
{
return Default ?? $"[{Key}]";
}
return localizedValue.Value;
}
}
/// <summary>
/// UI localization service for dynamic language switching.
/// </summary>
public interface IUILocalizationService
{
/// <summary>
/// Gets a localized string by key.
/// </summary>
string this[string key] { get; }
/// <summary>
/// Gets a localized string by key with format arguments.
/// </summary>
string this[string key, params object[] args] { get; }
/// <summary>
/// Gets the current culture name.
/// </summary>
string CurrentCulture { get; }
/// <summary>
/// Changes the current UI language.
/// </summary>
void SetLanguage(string cultureName);
/// <summary>
/// Gets whether the language follows system settings.
/// </summary>
bool FollowSystem { get; set; }
/// <summary>
/// Event raised when the language changes.
/// </summary>
event EventHandler? LanguageChanged;
}
/// <summary>
/// Implementation of UI localization service.
/// </summary>
public class UILocalizationService : IUILocalizationService
{
private readonly IStringLocalizer<Desktop.UIStrings> _localizer;
private bool _followSystem = true;
public UILocalizationService(IStringLocalizer<Desktop.UIStrings> localizer)
{
_localizer = localizer;
}
public string this[string key]
{
get
{
var value = _localizer[key];
return value.ResourceNotFound ? $"[{key}]" : value.Value;
}
}
public string this[string key, params object[] args]
{
get
{
var value = _localizer[key];
if (value.ResourceNotFound) return $"[{key}]";
return string.Format(value.Value, args);
}
}
public string CurrentCulture => System.Globalization.CultureInfo.CurrentUICulture.Name;
public bool FollowSystem
{
get => _followSystem;
set => _followSystem = value;
}
public event EventHandler? LanguageChanged;
public void SetLanguage(string cultureName)
{
var culture = new System.Globalization.CultureInfo(cultureName);
System.Globalization.CultureInfo.CurrentCulture = culture;
System.Globalization.CultureInfo.CurrentUICulture = culture;
LanguageChanged?.Invoke(this, EventArgs.Empty);
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Text;
using Avalonia;
namespace ZonyLrcTools.Desktop;
internal static class Program
{
[STAThread]
public static void Main(string[] args)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}

View File

@@ -0,0 +1,10 @@
namespace ZonyLrcTools.Desktop;
/// <summary>
/// Marker class for UI localization resources.
/// This class is used by IStringLocalizer&lt;UIStrings&gt; to locate the resource files.
/// The namespace must be the root namespace so that IStringLocalizer looks for Resources/UIStrings.resx
/// </summary>
public class UIStrings
{
}

View File

@@ -0,0 +1,245 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Navigation -->
<data name="Nav_Home" xml:space="preserve">
<value>Home</value>
</data>
<data name="Nav_LyricsDownload" xml:space="preserve">
<value>Lyrics Download</value>
</data>
<data name="Nav_AlbumDownload" xml:space="preserve">
<value>Album Cover</value>
</data>
<data name="Nav_Settings" xml:space="preserve">
<value>Settings</value>
</data>
<!-- Home Page -->
<data name="Home_Welcome" xml:space="preserve">
<value>Welcome to ZonyLrcTools</value>
</data>
<data name="Home_Description" xml:space="preserve">
<value>An open-source lyrics download tool</value>
</data>
<data name="Home_QuickStart" xml:space="preserve">
<value>Quick Start</value>
</data>
<data name="Home_DownloadLyrics" xml:space="preserve">
<value>Download Lyrics</value>
</data>
<data name="Home_DownloadAlbumCover" xml:space="preserve">
<value>Download Album Covers</value>
</data>
<!-- Lyrics Download Page -->
<data name="Lyrics_Title" xml:space="preserve">
<value>Lyrics Download</value>
</data>
<data name="Lyrics_Description" xml:space="preserve">
<value>Batch download lyrics for your music files</value>
</data>
<data name="Lyrics_SelectFolder" xml:space="preserve">
<value>Select music folder...</value>
</data>
<data name="Lyrics_Browse" xml:space="preserve">
<value>Browse...</value>
</data>
<data name="Lyrics_Parallel" xml:space="preserve">
<value>Parallel:</value>
</data>
<data name="Lyrics_StartDownload" xml:space="preserve">
<value>Start Download</value>
</data>
<data name="Lyrics_StopDownload" xml:space="preserve">
<value>Stop Download</value>
</data>
<data name="Lyrics_Progress" xml:space="preserve">
<value>Download Progress</value>
</data>
<data name="Lyrics_Status_Idle" xml:space="preserve">
<value>Idle</value>
</data>
<data name="Lyrics_Status_Scanning" xml:space="preserve">
<value>Scanning files...</value>
</data>
<data name="Lyrics_Status_Downloading" xml:space="preserve">
<value>Downloading lyrics...</value>
</data>
<data name="Lyrics_Status_Complete" xml:space="preserve">
<value>Download Complete</value>
</data>
<data name="Common_Total" xml:space="preserve">
<value>Total:</value>
</data>
<data name="Common_Files" xml:space="preserve">
<value>files</value>
</data>
<data name="Common_Success" xml:space="preserve">
<value>Success:</value>
</data>
<data name="Common_Failed" xml:space="preserve">
<value>Failed:</value>
</data>
<!-- Album Download Page -->
<data name="Album_Title" xml:space="preserve">
<value>Album Cover Download</value>
</data>
<data name="Album_Description" xml:space="preserve">
<value>Download album artwork for your music collection</value>
</data>
<data name="Album_SelectFolder" xml:space="preserve">
<value>Select music folder...</value>
</data>
<data name="Album_Browse" xml:space="preserve">
<value>Browse...</value>
</data>
<data name="Album_Parallel" xml:space="preserve">
<value>Parallel:</value>
</data>
<data name="Album_StartDownload" xml:space="preserve">
<value>Start Download</value>
</data>
<data name="Album_StopDownload" xml:space="preserve">
<value>Stop Download</value>
</data>
<!-- Settings Page -->
<data name="Settings_Title" xml:space="preserve">
<value>Settings</value>
</data>
<data name="Settings_Language" xml:space="preserve">
<value>Language</value>
</data>
<data name="Settings_FollowSystem" xml:space="preserve">
<value>Follow System</value>
</data>
<data name="Settings_Chinese" xml:space="preserve">
<value>Simplified Chinese</value>
</data>
<data name="Settings_English" xml:space="preserve">
<value>English</value>
</data>
<data name="Settings_Theme" xml:space="preserve">
<value>Theme</value>
</data>
<data name="Settings_ThemeLight" xml:space="preserve">
<value>Light</value>
</data>
<data name="Settings_ThemeDark" xml:space="preserve">
<value>Dark</value>
</data>
<data name="Settings_ThemeSystem" xml:space="preserve">
<value>Follow System</value>
</data>
<data name="Settings_LyricsProvider" xml:space="preserve">
<value>Lyrics Provider</value>
</data>
<data name="Settings_Proxy" xml:space="preserve">
<value>Proxy Settings</value>
</data>
<data name="Settings_ProxyEnable" xml:space="preserve">
<value>Enable Proxy</value>
</data>
<data name="Settings_ProxyAddress" xml:space="preserve">
<value>Proxy Address</value>
</data>
<data name="Settings_ProxyPort" xml:space="preserve">
<value>Proxy Port</value>
</data>
<data name="Settings_Save" xml:space="preserve">
<value>Save Settings</value>
</data>
<data name="Settings_Reset" xml:space="preserve">
<value>Reset</value>
</data>
<!-- DataGrid Columns -->
<data name="Column_SongName" xml:space="preserve">
<value>Song Name</value>
</data>
<data name="Column_Artist" xml:space="preserve">
<value>Artist</value>
</data>
<data name="Column_Album" xml:space="preserve">
<value>Album</value>
</data>
<data name="Column_Status" xml:space="preserve">
<value>Status</value>
</data>
<data name="Column_FilePath" xml:space="preserve">
<value>File Path</value>
</data>
<!-- Status Messages -->
<data name="Status_Success" xml:space="preserve">
<value>Success</value>
</data>
<data name="Status_Failed" xml:space="preserve">
<value>Failed</value>
</data>
<data name="Status_Skipped" xml:space="preserve">
<value>Skipped</value>
</data>
<data name="Status_Pending" xml:space="preserve">
<value>Pending</value>
</data>
<data name="Status_Processing" xml:space="preserve">
<value>Processing</value>
</data>
<!-- Common -->
<data name="Common_OK" xml:space="preserve">
<value>OK</value>
</data>
<data name="Common_Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="Common_Yes" xml:space="preserve">
<value>Yes</value>
</data>
<data name="Common_No" xml:space="preserve">
<value>No</value>
</data>
<data name="Common_Error" xml:space="preserve">
<value>Error</value>
</data>
<data name="Common_Warning" xml:space="preserve">
<value>Warning</value>
</data>
<data name="Common_Info" xml:space="preserve">
<value>Info</value>
</data>
</root>

View File

@@ -0,0 +1,245 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</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:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Navigation -->
<data name="Nav_Home" xml:space="preserve">
<value>首页</value>
</data>
<data name="Nav_LyricsDownload" xml:space="preserve">
<value>歌词下载</value>
</data>
<data name="Nav_AlbumDownload" xml:space="preserve">
<value>专辑封面</value>
</data>
<data name="Nav_Settings" xml:space="preserve">
<value>设置</value>
</data>
<!-- Home Page -->
<data name="Home_Welcome" xml:space="preserve">
<value>欢迎使用 ZonyLrcTools</value>
</data>
<data name="Home_Description" xml:space="preserve">
<value>一款开源的歌词下载工具</value>
</data>
<data name="Home_QuickStart" xml:space="preserve">
<value>快速开始</value>
</data>
<data name="Home_DownloadLyrics" xml:space="preserve">
<value>下载歌词</value>
</data>
<data name="Home_DownloadAlbumCover" xml:space="preserve">
<value>下载专辑封面</value>
</data>
<!-- Lyrics Download Page -->
<data name="Lyrics_Title" xml:space="preserve">
<value>歌词下载</value>
</data>
<data name="Lyrics_Description" xml:space="preserve">
<value>批量下载音乐文件的歌词</value>
</data>
<data name="Lyrics_SelectFolder" xml:space="preserve">
<value>选择音乐文件夹...</value>
</data>
<data name="Lyrics_Browse" xml:space="preserve">
<value>浏览...</value>
</data>
<data name="Lyrics_Parallel" xml:space="preserve">
<value>并行:</value>
</data>
<data name="Lyrics_StartDownload" xml:space="preserve">
<value>开始下载</value>
</data>
<data name="Lyrics_StopDownload" xml:space="preserve">
<value>停止下载</value>
</data>
<data name="Lyrics_Progress" xml:space="preserve">
<value>下载进度</value>
</data>
<data name="Lyrics_Status_Idle" xml:space="preserve">
<value>等待中</value>
</data>
<data name="Lyrics_Status_Scanning" xml:space="preserve">
<value>正在扫描文件...</value>
</data>
<data name="Lyrics_Status_Downloading" xml:space="preserve">
<value>正在下载歌词...</value>
</data>
<data name="Lyrics_Status_Complete" xml:space="preserve">
<value>下载完成</value>
</data>
<data name="Common_Total" xml:space="preserve">
<value>总计:</value>
</data>
<data name="Common_Files" xml:space="preserve">
<value>个文件</value>
</data>
<data name="Common_Success" xml:space="preserve">
<value>成功:</value>
</data>
<data name="Common_Failed" xml:space="preserve">
<value>失败:</value>
</data>
<!-- Album Download Page -->
<data name="Album_Title" xml:space="preserve">
<value>专辑封面下载</value>
</data>
<data name="Album_Description" xml:space="preserve">
<value>批量下载音乐文件的专辑封面</value>
</data>
<data name="Album_SelectFolder" xml:space="preserve">
<value>选择音乐文件夹...</value>
</data>
<data name="Album_Browse" xml:space="preserve">
<value>浏览...</value>
</data>
<data name="Album_Parallel" xml:space="preserve">
<value>并行:</value>
</data>
<data name="Album_StartDownload" xml:space="preserve">
<value>开始下载</value>
</data>
<data name="Album_StopDownload" xml:space="preserve">
<value>停止下载</value>
</data>
<!-- Settings Page -->
<data name="Settings_Title" xml:space="preserve">
<value>设置</value>
</data>
<data name="Settings_Language" xml:space="preserve">
<value>语言</value>
</data>
<data name="Settings_FollowSystem" xml:space="preserve">
<value>跟随系统</value>
</data>
<data name="Settings_Chinese" xml:space="preserve">
<value>简体中文</value>
</data>
<data name="Settings_English" xml:space="preserve">
<value>English</value>
</data>
<data name="Settings_Theme" xml:space="preserve">
<value>主题</value>
</data>
<data name="Settings_ThemeLight" xml:space="preserve">
<value>浅色</value>
</data>
<data name="Settings_ThemeDark" xml:space="preserve">
<value>深色</value>
</data>
<data name="Settings_ThemeSystem" xml:space="preserve">
<value>跟随系统</value>
</data>
<data name="Settings_LyricsProvider" xml:space="preserve">
<value>歌词源</value>
</data>
<data name="Settings_Proxy" xml:space="preserve">
<value>代理设置</value>
</data>
<data name="Settings_ProxyEnable" xml:space="preserve">
<value>启用代理</value>
</data>
<data name="Settings_ProxyAddress" xml:space="preserve">
<value>代理地址</value>
</data>
<data name="Settings_ProxyPort" xml:space="preserve">
<value>代理端口</value>
</data>
<data name="Settings_Save" xml:space="preserve">
<value>保存设置</value>
</data>
<data name="Settings_Reset" xml:space="preserve">
<value>重置</value>
</data>
<!-- DataGrid Columns -->
<data name="Column_SongName" xml:space="preserve">
<value>歌曲名</value>
</data>
<data name="Column_Artist" xml:space="preserve">
<value>艺术家</value>
</data>
<data name="Column_Album" xml:space="preserve">
<value>专辑</value>
</data>
<data name="Column_Status" xml:space="preserve">
<value>状态</value>
</data>
<data name="Column_FilePath" xml:space="preserve">
<value>文件路径</value>
</data>
<!-- Status Messages -->
<data name="Status_Success" xml:space="preserve">
<value>成功</value>
</data>
<data name="Status_Failed" xml:space="preserve">
<value>失败</value>
</data>
<data name="Status_Skipped" xml:space="preserve">
<value>已跳过</value>
</data>
<data name="Status_Pending" xml:space="preserve">
<value>等待中</value>
</data>
<data name="Status_Processing" xml:space="preserve">
<value>处理中</value>
</data>
<!-- Common -->
<data name="Common_OK" xml:space="preserve">
<value>确定</value>
</data>
<data name="Common_Cancel" xml:space="preserve">
<value>取消</value>
</data>
<data name="Common_Yes" xml:space="preserve">
<value>是</value>
</data>
<data name="Common_No" xml:space="preserve">
<value>否</value>
</data>
<data name="Common_Error" xml:space="preserve">
<value>错误</value>
</data>
<data name="Common_Warning" xml:space="preserve">
<value>警告</value>
</data>
<data name="Common_Info" xml:space="preserve">
<value>提示</value>
</data>
</root>

View File

@@ -0,0 +1,67 @@
using System.Collections.ObjectModel;
using Avalonia.Threading;
using ZonyLrcTools.Common.Infrastructure.Logging;
namespace ZonyLrcTools.Desktop.Services;
/// <summary>
/// Avalonia implementation of IWarpLogger that supports UI binding.
/// </summary>
public class AvaloniaWarpLogger : IWarpLogger
{
private const int MaxLogEntries = 1000;
public ObservableCollection<LogEntry> LogEntries { get; } = new();
public event EventHandler<LogEntry>? LogAdded;
public Task DebugAsync(string message, Exception? exception = null)
{
return AddLogAsync(LogLevel.Debug, message, exception);
}
public Task InfoAsync(string message, Exception? exception = null)
{
return AddLogAsync(LogLevel.Info, message, exception);
}
public Task WarnAsync(string message, Exception? exception = null)
{
return AddLogAsync(LogLevel.Warning, message, exception);
}
public Task ErrorAsync(string message, Exception? exception = null)
{
return AddLogAsync(LogLevel.Error, message, exception);
}
private async Task AddLogAsync(LogLevel level, string message, Exception? exception)
{
var entry = new LogEntry(level, message, exception);
await Dispatcher.UIThread.InvokeAsync(() =>
{
LogEntries.Add(entry);
while (LogEntries.Count > MaxLogEntries)
{
LogEntries.RemoveAt(0);
}
LogAdded?.Invoke(this, entry);
});
}
}
public record LogEntry(LogLevel Level, string Message, Exception? Exception)
{
public DateTime Timestamp { get; } = DateTime.Now;
}
public enum LogLevel
{
Debug,
Info,
Warning,
Error
}

View File

@@ -0,0 +1,60 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform.Storage;
namespace ZonyLrcTools.Desktop.Services;
public class DialogService : IDialogService
{
private Window? MainWindow =>
(Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?
.MainWindow;
public async Task<string?> ShowFolderPickerAsync(string title)
{
if (MainWindow == null) return null;
var folders = await MainWindow.StorageProvider.OpenFolderPickerAsync(
new FolderPickerOpenOptions
{
Title = title,
AllowMultiple = false
});
return folders.FirstOrDefault()?.Path.LocalPath;
}
public async Task<string?> ShowFilePickerAsync(string title, string[]? filters = null)
{
if (MainWindow == null) return null;
var fileTypes = filters?.Select(f => new FilePickerFileType(f)
{
Patterns = new[] { f }
}).ToList();
var files = await MainWindow.StorageProvider.OpenFilePickerAsync(
new FilePickerOpenOptions
{
Title = title,
AllowMultiple = false,
FileTypeFilter = fileTypes
});
return files.FirstOrDefault()?.Path.LocalPath;
}
public async Task<bool> ShowConfirmDialogAsync(string title, string message)
{
// TODO: Implement custom confirm dialog
await Task.CompletedTask;
return true;
}
public async Task ShowMessageAsync(string title, string message)
{
// TODO: Implement custom message dialog
await Task.CompletedTask;
}
}

View File

@@ -0,0 +1,9 @@
namespace ZonyLrcTools.Desktop.Services;
public interface IDialogService
{
Task<string?> ShowFolderPickerAsync(string title);
Task<string?> ShowFilePickerAsync(string title, string[]? filters = null);
Task<bool> ShowConfirmDialogAsync(string title, string message);
Task ShowMessageAsync(string title, string message);
}

View File

@@ -0,0 +1,13 @@
using ZonyLrcTools.Desktop.ViewModels;
namespace ZonyLrcTools.Desktop.Services;
public interface INavigationService
{
ViewModelBase? CurrentViewModel { get; }
event EventHandler<ViewModelBase>? NavigationChanged;
TViewModel NavigateTo<TViewModel>() where TViewModel : ViewModelBase;
void GoBack();
bool CanGoBack { get; }
}

View File

@@ -0,0 +1,12 @@
using Avalonia.Styling;
namespace ZonyLrcTools.Desktop.Services;
public interface IThemeService
{
ThemeVariant CurrentTheme { get; }
event EventHandler<ThemeVariant>? ThemeChanged;
void SetTheme(ThemeVariant theme);
void ToggleTheme();
}

View File

@@ -0,0 +1,50 @@
using Microsoft.Extensions.DependencyInjection;
using ZonyLrcTools.Desktop.ViewModels;
namespace ZonyLrcTools.Desktop.Services;
public class NavigationService : INavigationService
{
private readonly IServiceProvider _serviceProvider;
private readonly Stack<ViewModelBase> _navigationStack = new();
public ViewModelBase? CurrentViewModel => _navigationStack.TryPeek(out var vm) ? vm : null;
public bool CanGoBack => _navigationStack.Count > 1;
public event EventHandler<ViewModelBase>? NavigationChanged;
public NavigationService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public TViewModel NavigateTo<TViewModel>() where TViewModel : ViewModelBase
{
var viewModel = _serviceProvider.GetRequiredService<TViewModel>();
if (_navigationStack.TryPeek(out var current))
{
_ = current.OnDeactivatedAsync();
}
_navigationStack.Push(viewModel);
_ = viewModel.OnActivatedAsync();
NavigationChanged?.Invoke(this, viewModel);
return viewModel;
}
public void GoBack()
{
if (!CanGoBack) return;
var current = _navigationStack.Pop();
_ = current.OnDeactivatedAsync();
if (_navigationStack.TryPeek(out var previous))
{
_ = previous.OnActivatedAsync();
NavigationChanged?.Invoke(this, previous);
}
}
}

View File

@@ -0,0 +1,76 @@
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Common.Infrastructure.Logging;
using ZonyLrcTools.Common.Infrastructure.Network;
using ZonyLrcTools.Desktop.Infrastructure.Localization;
using ZonyLrcTools.Desktop.ViewModels;
namespace ZonyLrcTools.Desktop.Services;
public static class ServiceCollectionExtensions
{
/// <summary>
/// Register all services for the desktop application.
/// </summary>
public static IServiceCollection AddDesktopServices(this IServiceCollection services)
{
// Register Common project's auto dependency injection
services.BeginAutoDependencyInject<AvaloniaWarpLogger>();
services.BeginAutoDependencyInject<IWarpHttpClient>();
// Configure Common services
services.ConfigureConfiguration();
services.ConfigureLocalization();
services.ConfigureToolService();
// Configure Desktop localization
// Note: Don't set ResourcesPath here because the embedded resource name is
// ZonyLrcTools.Desktop.UIStrings (not ZonyLrcTools.Desktop.Resources.UIStrings)
services.AddLocalization();
services.AddSingleton<IUILocalizationService, UILocalizationService>();
// Register GUI-specific IWarpLogger implementation
services.AddSingleton<AvaloniaWarpLogger>();
services.AddSingleton<IWarpLogger>(sp => sp.GetRequiredService<AvaloniaWarpLogger>());
// Register navigation and dialog services
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IDialogService, DialogService>();
services.AddSingleton<IThemeService, ThemeService>();
// Register ViewModels
services.AddTransient<MainWindowViewModel>();
services.AddTransient<HomeViewModel>();
services.AddTransient<LyricsDownloadViewModel>();
services.AddTransient<AlbumDownloadViewModel>();
services.AddTransient<SettingsViewModel>();
return services;
}
/// <summary>
/// Initialize language settings based on system culture or user preference.
/// </summary>
public static void InitializeLanguage(string? preferredCulture = null)
{
CultureInfo culture;
if (!string.IsNullOrEmpty(preferredCulture))
{
// Use user's preferred culture
culture = new CultureInfo(preferredCulture);
}
else
{
// Use system culture, fallback to zh-CN if not supported
var systemCulture = CultureInfo.CurrentUICulture.Name;
culture = systemCulture.StartsWith("zh") ? new CultureInfo("zh-CN") :
systemCulture.StartsWith("en") ? new CultureInfo("en-US") :
new CultureInfo("zh-CN");
}
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
}

View File

@@ -0,0 +1,28 @@
using Avalonia;
using Avalonia.Styling;
namespace ZonyLrcTools.Desktop.Services;
public class ThemeService : IThemeService
{
public ThemeVariant CurrentTheme => Application.Current?.ActualThemeVariant ?? ThemeVariant.Default;
public event EventHandler<ThemeVariant>? ThemeChanged;
public void SetTheme(ThemeVariant theme)
{
if (Application.Current == null) return;
Application.Current.RequestedThemeVariant = theme;
ThemeChanged?.Invoke(this, theme);
}
public void ToggleTheme()
{
var newTheme = CurrentTheme == ThemeVariant.Dark
? ThemeVariant.Light
: ThemeVariant.Dark;
SetTheme(newTheme);
}
}

View File

@@ -0,0 +1,137 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ZonyLrcTools.Common.Album;
using ZonyLrcTools.Desktop.Infrastructure.Localization;
using ZonyLrcTools.Desktop.Services;
namespace ZonyLrcTools.Desktop.ViewModels;
public partial class AlbumDownloadViewModel : ViewModelBase
{
private readonly IAlbumDownloader _albumDownloader;
private readonly IDialogService _dialogService;
private readonly IUILocalizationService? _localization;
private CancellationTokenSource? _downloadCts;
[ObservableProperty]
private string? _selectedFolderPath;
[ObservableProperty]
private int _parallelCount = 2;
[ObservableProperty]
private int _totalCount;
[ObservableProperty]
private int _completedCount;
[ObservableProperty]
private int _failedCount;
[ObservableProperty]
private double _progressPercentage;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanStartDownload))]
private bool _isDownloading;
// Localized strings
public string AlbumTitle => _localization?["Album_Title"] ?? "Album Cover Download";
public string AlbumDescription => _localization?["Album_Description"] ?? "Download album artwork for your music collection";
public string AlbumSelectFolder => _localization?["Album_SelectFolder"] ?? "Select music folder...";
public string AlbumBrowse => _localization?["Album_Browse"] ?? "Browse...";
public string AlbumParallel => _localization?["Album_Parallel"] ?? "Parallel:";
public string AlbumStartDownload => _localization?["Album_StartDownload"] ?? "Start Download";
public string AlbumStopDownload => _localization?["Album_StopDownload"] ?? "Stop Download";
public string CommonTotal => _localization?["Common_Total"] ?? "Total:";
public string CommonFiles => _localization?["Common_Files"] ?? "files";
public string CommonSuccess => _localization?["Common_Success"] ?? "Success:";
public string CommonFailed => _localization?["Common_Failed"] ?? "Failed:";
public string ColumnSongName => _localization?["Column_SongName"] ?? "Song Name";
public string ColumnArtist => _localization?["Column_Artist"] ?? "Artist";
public string ColumnStatus => _localization?["Column_Status"] ?? "Status";
public bool CanStartDownload => !IsDownloading && !string.IsNullOrEmpty(SelectedFolderPath);
public ObservableCollection<MusicFileViewModel> MusicFiles { get; } = new();
public AlbumDownloadViewModel(
IAlbumDownloader albumDownloader,
IDialogService dialogService,
IUILocalizationService? localization = null)
{
_albumDownloader = albumDownloader;
_dialogService = dialogService;
_localization = localization;
if (_localization != null)
{
_localization.LanguageChanged += OnLanguageChanged;
}
}
private void OnLanguageChanged(object? sender, EventArgs e)
{
OnPropertyChanged(nameof(AlbumTitle));
OnPropertyChanged(nameof(AlbumDescription));
OnPropertyChanged(nameof(AlbumSelectFolder));
OnPropertyChanged(nameof(AlbumBrowse));
OnPropertyChanged(nameof(AlbumParallel));
OnPropertyChanged(nameof(AlbumStartDownload));
OnPropertyChanged(nameof(AlbumStopDownload));
OnPropertyChanged(nameof(CommonTotal));
OnPropertyChanged(nameof(CommonFiles));
OnPropertyChanged(nameof(CommonSuccess));
OnPropertyChanged(nameof(CommonFailed));
OnPropertyChanged(nameof(ColumnSongName));
OnPropertyChanged(nameof(ColumnArtist));
OnPropertyChanged(nameof(ColumnStatus));
}
[RelayCommand]
private async Task SelectFolderAsync()
{
var folder = await _dialogService.ShowFolderPickerAsync("Select Music Folder");
if (string.IsNullOrEmpty(folder)) return;
SelectedFolderPath = folder;
OnPropertyChanged(nameof(CanStartDownload));
}
[RelayCommand]
private async Task StartDownloadAsync()
{
if (string.IsNullOrEmpty(SelectedFolderPath)) return;
IsDownloading = true;
CompletedCount = 0;
FailedCount = 0;
ProgressPercentage = 0;
_downloadCts = new CancellationTokenSource();
try
{
// TODO: Implement actual download logic using _albumDownloader
await Task.Delay(1000, _downloadCts.Token); // Placeholder
}
catch (OperationCanceledException)
{
// User cancelled
}
finally
{
IsDownloading = false;
_downloadCts?.Dispose();
_downloadCts = null;
}
}
[RelayCommand]
private void CancelDownload()
{
_downloadCts?.Cancel();
}
}

View File

@@ -0,0 +1,15 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace ZonyLrcTools.Desktop.ViewModels;
public partial class HomeViewModel : ViewModelBase
{
[ObservableProperty]
private string _welcomeMessage = "Welcome to ZonyLrcTools X";
[ObservableProperty]
private string _description = "A cross-platform tool for downloading lyrics and album covers for your music collection.";
[ObservableProperty]
private string _version = "4.0.3";
}

View File

@@ -0,0 +1,164 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ZonyLrcTools.Common.Lyrics;
using ZonyLrcTools.Desktop.Infrastructure.Localization;
using ZonyLrcTools.Desktop.Services;
namespace ZonyLrcTools.Desktop.ViewModels;
public partial class LyricsDownloadViewModel : ViewModelBase
{
private readonly ILyricsDownloader _lyricsDownloader;
private readonly IDialogService _dialogService;
private readonly IUILocalizationService? _localization;
private CancellationTokenSource? _downloadCts;
[ObservableProperty]
private string? _selectedFolderPath;
[ObservableProperty]
private int _parallelCount = 2;
[ObservableProperty]
private int _totalCount;
[ObservableProperty]
private int _completedCount;
[ObservableProperty]
private int _failedCount;
[ObservableProperty]
private double _progressPercentage;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(StartDownloadCommand))]
[NotifyPropertyChangedFor(nameof(CanStartDownload))]
private bool _isDownloading;
[ObservableProperty]
private string? _currentProcessingFile;
// Localized strings
public string LyricsTitle => _localization?["Lyrics_Title"] ?? "Lyrics Download";
public string LyricsDescription => _localization?["Lyrics_Description"] ?? "Batch download lyrics for your music files";
public string LyricsSelectFolder => _localization?["Lyrics_SelectFolder"] ?? "Select music folder...";
public string LyricsBrowse => _localization?["Lyrics_Browse"] ?? "Browse...";
public string LyricsParallel => _localization?["Lyrics_Parallel"] ?? "Parallel:";
public string LyricsStartDownload => _localization?["Lyrics_StartDownload"] ?? "Start Download";
public string LyricsStopDownload => _localization?["Lyrics_StopDownload"] ?? "Stop Download";
public string CommonTotal => _localization?["Common_Total"] ?? "Total:";
public string CommonFiles => _localization?["Common_Files"] ?? "files";
public string CommonSuccess => _localization?["Common_Success"] ?? "Success:";
public string CommonFailed => _localization?["Common_Failed"] ?? "Failed:";
public string ColumnSongName => _localization?["Column_SongName"] ?? "Song Name";
public string ColumnArtist => _localization?["Column_Artist"] ?? "Artist";
public string ColumnFilePath => _localization?["Column_FilePath"] ?? "File Path";
public string ColumnStatus => _localization?["Column_Status"] ?? "Status";
public bool CanStartDownload => !IsDownloading && !string.IsNullOrEmpty(SelectedFolderPath);
public ObservableCollection<MusicFileViewModel> MusicFiles { get; } = new();
public LyricsDownloadViewModel(
ILyricsDownloader lyricsDownloader,
IDialogService dialogService,
IUILocalizationService? localization = null)
{
_lyricsDownloader = lyricsDownloader;
_dialogService = dialogService;
_localization = localization;
if (_localization != null)
{
_localization.LanguageChanged += OnLanguageChanged;
}
}
private void OnLanguageChanged(object? sender, EventArgs e)
{
OnPropertyChanged(nameof(LyricsTitle));
OnPropertyChanged(nameof(LyricsDescription));
OnPropertyChanged(nameof(LyricsSelectFolder));
OnPropertyChanged(nameof(LyricsBrowse));
OnPropertyChanged(nameof(LyricsParallel));
OnPropertyChanged(nameof(LyricsStartDownload));
OnPropertyChanged(nameof(LyricsStopDownload));
OnPropertyChanged(nameof(CommonTotal));
OnPropertyChanged(nameof(CommonFiles));
OnPropertyChanged(nameof(CommonSuccess));
OnPropertyChanged(nameof(CommonFailed));
OnPropertyChanged(nameof(ColumnSongName));
OnPropertyChanged(nameof(ColumnArtist));
OnPropertyChanged(nameof(ColumnFilePath));
OnPropertyChanged(nameof(ColumnStatus));
}
[RelayCommand]
private async Task SelectFolderAsync()
{
var folder = await _dialogService.ShowFolderPickerAsync("Select Music Folder");
if (string.IsNullOrEmpty(folder)) return;
SelectedFolderPath = folder;
OnPropertyChanged(nameof(CanStartDownload));
}
[RelayCommand(CanExecute = nameof(CanStartDownload))]
private async Task StartDownloadAsync()
{
if (string.IsNullOrEmpty(SelectedFolderPath)) return;
IsDownloading = true;
CompletedCount = 0;
FailedCount = 0;
ProgressPercentage = 0;
_downloadCts = new CancellationTokenSource();
try
{
// TODO: Implement actual download logic using _lyricsDownloader
await Task.Delay(1000, _downloadCts.Token); // Placeholder
}
catch (OperationCanceledException)
{
// User cancelled
}
finally
{
IsDownloading = false;
_downloadCts?.Dispose();
_downloadCts = null;
}
}
[RelayCommand]
private void CancelDownload()
{
_downloadCts?.Cancel();
}
}
public partial class MusicFileViewModel : ObservableObject
{
[ObservableProperty]
private string _filePath = string.Empty;
[ObservableProperty]
private string _name = string.Empty;
[ObservableProperty]
private string _artist = string.Empty;
[ObservableProperty]
private bool _isProcessed;
[ObservableProperty]
private bool _isSuccessful;
[ObservableProperty]
private string? _statusMessage;
}

View File

@@ -0,0 +1,72 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ZonyLrcTools.Desktop.Infrastructure.Localization;
using ZonyLrcTools.Desktop.Services;
namespace ZonyLrcTools.Desktop.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
private readonly IThemeService _themeService;
private readonly IUILocalizationService _localization;
[ObservableProperty]
private ViewModelBase? _currentPage;
[ObservableProperty]
private int _selectedNavigationIndex;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ThemeButtonText))]
private bool _isDarkTheme;
[ObservableProperty]
private string _title = "ZonyLrcTools X";
// Navigation localized strings
public string NavHome => _localization["Nav_Home"];
public string NavLyricsDownload => _localization["Nav_LyricsDownload"];
public string NavAlbumDownload => _localization["Nav_AlbumDownload"];
public string NavSettings => _localization["Nav_Settings"];
public string HomeDescription => _localization["Home_Description"];
public string ThemeButtonText => IsDarkTheme
? _localization["Settings_ThemeLight"]
: _localization["Settings_ThemeDark"];
public MainWindowViewModel(
IThemeService themeService,
IUILocalizationService localization,
HomeViewModel homeViewModel)
{
_themeService = themeService;
_localization = localization;
// Subscribe to language changes
_localization.LanguageChanged += OnLanguageChanged;
// Set initial page
CurrentPage = homeViewModel;
SelectedNavigationIndex = 0;
}
private void OnLanguageChanged(object? sender, EventArgs e)
{
// Notify all localized properties have changed
OnPropertyChanged(nameof(NavHome));
OnPropertyChanged(nameof(NavLyricsDownload));
OnPropertyChanged(nameof(NavAlbumDownload));
OnPropertyChanged(nameof(NavSettings));
OnPropertyChanged(nameof(HomeDescription));
OnPropertyChanged(nameof(ThemeButtonText));
}
[RelayCommand]
private void ToggleTheme()
{
IsDarkTheme = !IsDarkTheme;
_themeService.SetTheme(IsDarkTheme
? Avalonia.Styling.ThemeVariant.Dark
: Avalonia.Styling.ThemeVariant.Light);
}
}

View File

@@ -0,0 +1,253 @@
using System.Collections.ObjectModel;
using System.Globalization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Options;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Desktop.Infrastructure.Localization;
namespace ZonyLrcTools.Desktop.ViewModels;
public partial class SettingsViewModel : ViewModelBase
{
private readonly GlobalOptions _globalOptions;
private readonly IUILocalizationService? _localizationService;
private bool _isLoading = true; // Prevent saving during initial load
[ObservableProperty]
private bool _followSystemLanguage = true;
[ObservableProperty]
private bool _isChineseSelected = true;
[ObservableProperty]
private bool _isEnglishSelected;
[ObservableProperty]
private bool _isProxyEnabled;
[ObservableProperty]
private string _proxyIp = "127.0.0.1";
[ObservableProperty]
private int _proxyPort = 7890;
[ObservableProperty]
private bool _isTranslationEnabled;
[ObservableProperty]
private bool _isOneLineMode;
[ObservableProperty]
private bool _skipExistingLyrics;
[ObservableProperty]
private string _selectedEncoding = "utf-8";
// Localized strings
public string SettingsTitle => _localizationService?["Settings_Title"] ?? "Settings";
public string SettingsLanguage => _localizationService?["Settings_Language"] ?? "Language";
public string SettingsFollowSystem => _localizationService?["Settings_FollowSystem"] ?? "Follow System";
public string SettingsChinese => _localizationService?["Settings_Chinese"] ?? "Chinese";
public string SettingsEnglish => _localizationService?["Settings_English"] ?? "English";
public string SettingsProxy => _localizationService?["Settings_Proxy"] ?? "Proxy";
public string SettingsProxyEnable => _localizationService?["Settings_ProxyEnable"] ?? "Enable Proxy";
public string SettingsProxyAddress => _localizationService?["Settings_ProxyAddress"] ?? "Proxy Address";
public string SettingsLyricsProvider => _localizationService?["Settings_LyricsProvider"] ?? "Lyrics Provider";
public string SettingsReset => _localizationService?["Settings_Reset"] ?? "Reset";
public ObservableCollection<string> AvailableEncodings { get; } = new()
{
"utf-8",
"utf-8-bom",
"gb2312",
"gbk"
};
public ObservableCollection<LyricsProviderSettingViewModel> LyricsProviders { get; } = new();
public SettingsViewModel(IOptions<GlobalOptions> options, IUILocalizationService? localizationService = null)
{
_globalOptions = options.Value;
_localizationService = localizationService;
// Subscribe to language changes
if (_localizationService != null)
{
_localizationService.LanguageChanged += OnLanguageChanged;
}
LoadSettings();
LoadLanguageSettings();
_isLoading = false; // Allow saving after initial load
}
private void OnLanguageChanged(object? sender, EventArgs e)
{
OnPropertyChanged(nameof(SettingsTitle));
OnPropertyChanged(nameof(SettingsLanguage));
OnPropertyChanged(nameof(SettingsFollowSystem));
OnPropertyChanged(nameof(SettingsChinese));
OnPropertyChanged(nameof(SettingsEnglish));
OnPropertyChanged(nameof(SettingsProxy));
OnPropertyChanged(nameof(SettingsProxyEnable));
OnPropertyChanged(nameof(SettingsProxyAddress));
OnPropertyChanged(nameof(SettingsLyricsProvider));
OnPropertyChanged(nameof(SettingsReset));
}
private void LoadLanguageSettings()
{
var currentCulture = CultureInfo.CurrentUICulture.Name;
IsChineseSelected = currentCulture.StartsWith("zh");
IsEnglishSelected = currentCulture.StartsWith("en");
}
partial void OnFollowSystemLanguageChanged(bool value)
{
if (value)
{
// Auto-detect system language
var systemCulture = CultureInfo.InstalledUICulture.Name;
ApplyLanguage(systemCulture.StartsWith("zh") ? "zh-CN" : "en-US");
}
SaveSettingsIfNotLoading();
}
partial void OnIsChineseSelectedChanged(bool value)
{
if (value && !FollowSystemLanguage)
{
ApplyLanguage("zh-CN");
}
SaveSettingsIfNotLoading();
}
partial void OnIsEnglishSelectedChanged(bool value)
{
if (value && !FollowSystemLanguage)
{
ApplyLanguage("en-US");
}
SaveSettingsIfNotLoading();
}
partial void OnIsProxyEnabledChanged(bool value) => SaveSettingsIfNotLoading();
partial void OnProxyIpChanged(string value) => SaveSettingsIfNotLoading();
partial void OnProxyPortChanged(int value) => SaveSettingsIfNotLoading();
partial void OnIsTranslationEnabledChanged(bool value) => SaveSettingsIfNotLoading();
partial void OnIsOneLineModeChanged(bool value) => SaveSettingsIfNotLoading();
partial void OnSkipExistingLyricsChanged(bool value) => SaveSettingsIfNotLoading();
partial void OnSelectedEncodingChanged(string value) => SaveSettingsIfNotLoading();
private void ApplyLanguage(string cultureName)
{
_localizationService?.SetLanguage(cultureName);
}
private void SaveSettingsIfNotLoading()
{
if (!_isLoading)
{
SaveSettingsToFile();
}
}
private void LoadSettings()
{
// Load network settings
IsProxyEnabled = _globalOptions.NetworkOptions?.IsEnable ?? false;
ProxyIp = _globalOptions.NetworkOptions?.Ip ?? "127.0.0.1";
ProxyPort = _globalOptions.NetworkOptions?.Port ?? 7890;
// Load lyrics settings
var lyricsConfig = _globalOptions.Provider?.Lyric?.Config;
if (lyricsConfig != null)
{
IsTranslationEnabled = lyricsConfig.IsEnableTranslation;
IsOneLineMode = lyricsConfig.IsOneLine;
SkipExistingLyrics = lyricsConfig.IsSkipExistLyricFiles;
SelectedEncoding = lyricsConfig.FileEncoding ?? "utf-8";
}
// Load lyrics providers
var providers = _globalOptions.Provider?.Lyric?.Plugin;
if (providers != null)
{
foreach (var provider in providers.OrderBy(p => p.Priority))
{
LyricsProviders.Add(new LyricsProviderSettingViewModel
{
Name = provider.Name ?? "Unknown",
Priority = provider.Priority,
IsEnabled = provider.Priority != -1
});
}
}
}
private void SaveSettingsToFile()
{
try
{
// Update GlobalOptions with current values
if (_globalOptions.NetworkOptions != null)
{
_globalOptions.NetworkOptions.IsEnable = IsProxyEnabled;
_globalOptions.NetworkOptions.Ip = ProxyIp;
_globalOptions.NetworkOptions.Port = ProxyPort;
}
if (_globalOptions.Provider?.Lyric?.Config != null)
{
_globalOptions.Provider.Lyric.Config.IsEnableTranslation = IsTranslationEnabled;
_globalOptions.Provider.Lyric.Config.IsOneLine = IsOneLineMode;
_globalOptions.Provider.Lyric.Config.IsSkipExistLyricFiles = SkipExistingLyrics;
_globalOptions.Provider.Lyric.Config.FileEncoding = SelectedEncoding;
}
// Serialize and save to file
var serializer = new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
var yaml = serializer.Serialize(_globalOptions);
var configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.yaml");
File.WriteAllText(configPath, yaml);
}
catch (Exception ex)
{
// Log error but don't crash - settings save is best effort
System.Diagnostics.Debug.WriteLine($"Failed to save settings: {ex.Message}");
}
}
[RelayCommand]
private void ResetToDefaults()
{
_isLoading = true; // Prevent multiple saves during reset
IsProxyEnabled = false;
ProxyIp = "127.0.0.1";
ProxyPort = 7890;
IsTranslationEnabled = false;
IsOneLineMode = false;
SkipExistingLyrics = true;
SelectedEncoding = "utf-8";
_isLoading = false;
SaveSettingsToFile(); // Save once after all resets
}
}
public partial class LyricsProviderSettingViewModel : ObservableObject
{
[ObservableProperty]
private string _name = string.Empty;
[ObservableProperty]
private int _priority;
[ObservableProperty]
private bool _isEnabled;
}

View File

@@ -0,0 +1,28 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace ZonyLrcTools.Desktop.ViewModels;
/// <summary>
/// Base class for all ViewModels.
/// </summary>
public abstract partial class ViewModelBase : ObservableObject
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsNotBusy))]
private bool _isBusy;
[ObservableProperty]
private string? _busyMessage;
public bool IsNotBusy => !IsBusy;
/// <summary>
/// Called when the ViewModel is activated (navigated to).
/// </summary>
public virtual Task OnActivatedAsync() => Task.CompletedTask;
/// <summary>
/// Called when the ViewModel is deactivated (navigated away).
/// </summary>
public virtual Task OnDeactivatedAsync() => Task.CompletedTask;
}

View File

@@ -0,0 +1,81 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ZonyLrcTools.Desktop.ViewModels"
xmlns:pages="using:ZonyLrcTools.Desktop.Views.Pages"
xmlns:loc="using:ZonyLrcTools.Desktop.Infrastructure.Localization"
x:Class="ZonyLrcTools.Desktop.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Title="{Binding Title}"
Width="1200" Height="800"
MinWidth="900" MinHeight="600"
WindowStartupLocation="CenterScreen">
<Grid ColumnDefinitions="220,*">
<!-- Left Navigation Panel -->
<Border Grid.Column="0"
Background="{DynamicResource NavigationBackgroundBrush}"
Padding="8">
<DockPanel>
<!-- App Title -->
<StackPanel DockPanel.Dock="Top" Margin="8,16,8,24">
<TextBlock Text="ZonyLrcTools"
FontSize="24"
FontWeight="Bold"
HorizontalAlignment="Center" />
<TextBlock Text="{Binding HomeDescription}"
FontSize="12"
Opacity="0.7"
HorizontalAlignment="Center"
Margin="0,4,0,0" />
</StackPanel>
<!-- Theme Toggle -->
<Button DockPanel.Dock="Bottom"
Content="{Binding ThemeButtonText}"
Command="{Binding ToggleThemeCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Margin="8" />
<!-- Navigation Menu -->
<ListBox SelectedIndex="{Binding SelectedNavigationIndex}"
Margin="0,8"
SelectionChanged="OnNavigationSelectionChanged">
<ListBoxItem>
<TextBlock Text="{Binding NavHome}" VerticalAlignment="Center" />
</ListBoxItem>
<ListBoxItem>
<TextBlock Text="{Binding NavLyricsDownload}" VerticalAlignment="Center" />
</ListBoxItem>
<ListBoxItem>
<TextBlock Text="{Binding NavAlbumDownload}" VerticalAlignment="Center" />
</ListBoxItem>
<ListBoxItem>
<TextBlock Text="{Binding NavSettings}" VerticalAlignment="Center" />
</ListBoxItem>
</ListBox>
</DockPanel>
</Border>
<!-- Right Content Area -->
<Border Grid.Column="1" Padding="24">
<ContentControl Content="{Binding CurrentPage}">
<ContentControl.DataTemplates>
<DataTemplate DataType="vm:HomeViewModel">
<pages:HomePage />
</DataTemplate>
<DataTemplate DataType="vm:LyricsDownloadViewModel">
<pages:LyricsDownloadPage />
</DataTemplate>
<DataTemplate DataType="vm:AlbumDownloadViewModel">
<pages:AlbumDownloadPage />
</DataTemplate>
<DataTemplate DataType="vm:SettingsViewModel">
<pages:SettingsPage />
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,38 @@
using Avalonia.Controls;
using Microsoft.Extensions.DependencyInjection;
using ZonyLrcTools.Desktop.ViewModels;
namespace ZonyLrcTools.Desktop.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void OnNavigationSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (DataContext is not MainWindowViewModel viewModel) return;
if (sender is not ListBox listBox) return;
var services = App.Services;
if (services == null) return;
switch (listBox.SelectedIndex)
{
case 0:
viewModel.CurrentPage = services.GetRequiredService<HomeViewModel>();
break;
case 1:
viewModel.CurrentPage = services.GetRequiredService<LyricsDownloadViewModel>();
break;
case 2:
viewModel.CurrentPage = services.GetRequiredService<AlbumDownloadViewModel>();
break;
case 3:
viewModel.CurrentPage = services.GetRequiredService<SettingsViewModel>();
break;
}
}
}

View File

@@ -0,0 +1,142 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ZonyLrcTools.Desktop.ViewModels"
x:Class="ZonyLrcTools.Desktop.Views.Pages.AlbumDownloadPage"
x:DataType="vm:AlbumDownloadViewModel">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<!-- Title -->
<StackPanel Grid.Row="0" Margin="0,0,0,24">
<TextBlock Text="{Binding AlbumTitle}"
FontSize="28"
FontWeight="SemiBold" />
<TextBlock Text="{Binding AlbumDescription}"
Opacity="0.7"
Margin="0,4,0,0" />
</StackPanel>
<!-- Controls -->
<Border Grid.Row="1"
Background="{DynamicResource NavigationBackgroundBrush}"
CornerRadius="8"
Padding="16"
Margin="0,0,0,16">
<StackPanel Spacing="16">
<!-- Row 1: Folder Selection -->
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
Text="{Binding SelectedFolderPath}"
Watermark="{Binding AlbumSelectFolder}"
IsReadOnly="True"
Margin="0,0,12,0" />
<Button Grid.Column="1"
Content="{Binding AlbumBrowse}"
Command="{Binding SelectFolderCommand}"
MinWidth="80" />
</Grid>
<!-- Row 2: Options and Actions -->
<Grid ColumnDefinitions="Auto,Auto,*,Auto">
<!-- Parallel Count -->
<TextBlock Grid.Column="0"
Text="{Binding AlbumParallel}"
VerticalAlignment="Center"
Margin="0,0,8,0" />
<TextBox Grid.Column="1"
Text="{Binding ParallelCount}"
Width="60"
TextAlignment="Center"
VerticalContentAlignment="Center" />
<!-- Spacer -->
<Panel Grid.Column="2" />
<!-- Download Buttons -->
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="8">
<Button Command="{Binding StartDownloadCommand}"
Content="{Binding AlbumStartDownload}"
IsVisible="{Binding !IsDownloading}"
Classes="accent"
MinWidth="100" />
<Button Command="{Binding CancelDownloadCommand}"
Content="{Binding AlbumStopDownload}"
IsVisible="{Binding IsDownloading}"
MinWidth="100" />
</StackPanel>
</Grid>
<!-- Progress Bar (visible during download) -->
<StackPanel IsVisible="{Binding IsDownloading}" Spacing="8">
<ProgressBar Value="{Binding ProgressPercentage}"
Maximum="100"
Height="6" />
<TextBlock HorizontalAlignment="Center" FontSize="12" Opacity="0.8">
<Run Text="{Binding CompletedCount}" />
<Run Text=" / " />
<Run Text="{Binding TotalCount}" />
</TextBlock>
</StackPanel>
</StackPanel>
</Border>
<!-- File List -->
<Border Grid.Row="2"
Background="{DynamicResource NavigationBackgroundBrush}"
CornerRadius="8"
Padding="1">
<DataGrid ItemsSource="{Binding MusicFiles}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="Horizontal"
BorderThickness="0">
<DataGrid.Columns>
<DataGridTextColumn Header="{Binding ColumnSongName}"
Binding="{Binding Name}"
Width="*" />
<DataGridTextColumn Header="{Binding ColumnArtist}"
Binding="{Binding Artist}"
Width="200" />
<DataGridTextColumn Header="{Binding ColumnStatus}"
Binding="{Binding StatusMessage}"
Width="120" />
</DataGrid.Columns>
</DataGrid>
</Border>
<!-- Status Bar -->
<Border Grid.Row="3"
Background="{DynamicResource NavigationBackgroundBrush}"
Padding="16,12"
Margin="0,16,0,0"
CornerRadius="8">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<!-- Total -->
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="4">
<TextBlock Text="{Binding CommonTotal}" Opacity="0.8" />
<TextBlock Text="{Binding TotalCount}" FontWeight="SemiBold" />
<TextBlock Text="{Binding CommonFiles}" Opacity="0.8" />
</StackPanel>
<!-- Spacer -->
<Panel Grid.Column="1" />
<!-- Success -->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="4" Margin="0,0,24,0">
<TextBlock Text="{Binding CommonSuccess}" Opacity="0.8" />
<TextBlock Text="{Binding CompletedCount}"
FontWeight="SemiBold"
Foreground="#4CAF50" />
</StackPanel>
<!-- Failed -->
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="4">
<TextBlock Text="{Binding CommonFailed}" Opacity="0.8" />
<TextBlock Text="{Binding FailedCount}"
FontWeight="SemiBold"
Foreground="#F44336" />
</StackPanel>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace ZonyLrcTools.Desktop.Views.Pages;
public partial class AlbumDownloadPage : UserControl
{
public AlbumDownloadPage()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,97 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ZonyLrcTools.Desktop.ViewModels"
x:Class="ZonyLrcTools.Desktop.Views.Pages.HomePage"
x:DataType="vm:HomeViewModel">
<ScrollViewer>
<StackPanel Spacing="24">
<!-- Header -->
<StackPanel Spacing="8">
<TextBlock Text="{Binding WelcomeMessage}"
FontSize="32"
FontWeight="Bold" />
<TextBlock Text="{Binding Description}"
FontSize="14"
Opacity="0.7"
TextWrapping="Wrap" />
</StackPanel>
<!-- Quick Actions -->
<Border Background="{DynamicResource NavigationBackgroundBrush}"
CornerRadius="8"
Padding="24">
<StackPanel Spacing="16">
<TextBlock Text="Quick Actions"
FontSize="18"
FontWeight="SemiBold" />
<WrapPanel Orientation="Horizontal">
<Button Margin="0,0,16,16"
Padding="24,16"
CornerRadius="8">
<StackPanel Spacing="8">
<TextBlock Text="Download Lyrics"
FontWeight="SemiBold" />
<TextBlock Text="Batch download lyrics for your music"
FontSize="12"
Opacity="0.7" />
</StackPanel>
</Button>
<Button Margin="0,0,16,16"
Padding="24,16"
CornerRadius="8">
<StackPanel Spacing="8">
<TextBlock Text="Download Album Covers"
FontWeight="SemiBold" />
<TextBlock Text="Get album artwork for your collection"
FontSize="12"
Opacity="0.7" />
</StackPanel>
</Button>
</WrapPanel>
</StackPanel>
</Border>
<!-- Features -->
<Border Background="{DynamicResource NavigationBackgroundBrush}"
CornerRadius="8"
Padding="24">
<StackPanel Spacing="16">
<TextBlock Text="Features"
FontSize="18"
FontWeight="SemiBold" />
<ItemsControl>
<ItemsControl.Items>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,4">
<TextBlock Text="*" FontWeight="Bold" Foreground="{DynamicResource AppAccentBrush}" />
<TextBlock Text="Multiple lyrics sources: NetEase, QQ Music, KuGou, KuWo" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,4">
<TextBlock Text="*" FontWeight="Bold" Foreground="{DynamicResource AppAccentBrush}" />
<TextBlock Text="Batch processing with parallel downloads" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,4">
<TextBlock Text="*" FontWeight="Bold" Foreground="{DynamicResource AppAccentBrush}" />
<TextBlock Text="Support for translation lyrics" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,4">
<TextBlock Text="*" FontWeight="Bold" Foreground="{DynamicResource AppAccentBrush}" />
<TextBlock Text="Cross-platform: Windows, macOS, Linux" />
</StackPanel>
</ItemsControl.Items>
</ItemsControl>
</StackPanel>
</Border>
<!-- Version Info -->
<TextBlock Opacity="0.5" FontSize="12">
<Run Text="Version " />
<Run Text="{Binding Version}" />
</TextBlock>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace ZonyLrcTools.Desktop.Views.Pages;
public partial class HomePage : UserControl
{
public HomePage()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,145 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ZonyLrcTools.Desktop.ViewModels"
x:Class="ZonyLrcTools.Desktop.Views.Pages.LyricsDownloadPage"
x:DataType="vm:LyricsDownloadViewModel">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<!-- Title -->
<StackPanel Grid.Row="0" Margin="0,0,0,24">
<TextBlock Text="{Binding LyricsTitle}"
FontSize="28"
FontWeight="SemiBold" />
<TextBlock Text="{Binding LyricsDescription}"
Opacity="0.7"
Margin="0,4,0,0" />
</StackPanel>
<!-- Controls -->
<Border Grid.Row="1"
Background="{DynamicResource NavigationBackgroundBrush}"
CornerRadius="8"
Padding="16"
Margin="0,0,0,16">
<StackPanel Spacing="16">
<!-- Row 1: Folder Selection -->
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0"
Text="{Binding SelectedFolderPath}"
Watermark="{Binding LyricsSelectFolder}"
IsReadOnly="True"
Margin="0,0,12,0" />
<Button Grid.Column="1"
Content="{Binding LyricsBrowse}"
Command="{Binding SelectFolderCommand}"
MinWidth="80" />
</Grid>
<!-- Row 2: Options and Actions -->
<Grid ColumnDefinitions="Auto,Auto,*,Auto">
<!-- Parallel Count -->
<TextBlock Grid.Column="0"
Text="{Binding LyricsParallel}"
VerticalAlignment="Center"
Margin="0,0,8,0" />
<TextBox Grid.Column="1"
Text="{Binding ParallelCount}"
Width="60"
TextAlignment="Center"
VerticalContentAlignment="Center" />
<!-- Spacer -->
<Panel Grid.Column="2" />
<!-- Download Buttons -->
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="8">
<Button Command="{Binding StartDownloadCommand}"
Content="{Binding LyricsStartDownload}"
IsVisible="{Binding !IsDownloading}"
Classes="accent"
MinWidth="100" />
<Button Command="{Binding CancelDownloadCommand}"
Content="{Binding LyricsStopDownload}"
IsVisible="{Binding IsDownloading}"
MinWidth="100" />
</StackPanel>
</Grid>
<!-- Progress Bar (visible during download) -->
<StackPanel IsVisible="{Binding IsDownloading}" Spacing="8">
<ProgressBar Value="{Binding ProgressPercentage}"
Maximum="100"
Height="6" />
<TextBlock HorizontalAlignment="Center" FontSize="12" Opacity="0.8">
<Run Text="{Binding CompletedCount}" />
<Run Text=" / " />
<Run Text="{Binding TotalCount}" />
</TextBlock>
</StackPanel>
</StackPanel>
</Border>
<!-- File List -->
<Border Grid.Row="2"
Background="{DynamicResource NavigationBackgroundBrush}"
CornerRadius="8"
Padding="1">
<DataGrid ItemsSource="{Binding MusicFiles}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="Horizontal"
BorderThickness="0">
<DataGrid.Columns>
<DataGridTextColumn Header="{Binding ColumnSongName}"
Binding="{Binding Name}"
Width="*" />
<DataGridTextColumn Header="{Binding ColumnArtist}"
Binding="{Binding Artist}"
Width="150" />
<DataGridTextColumn Header="{Binding ColumnFilePath}"
Binding="{Binding FilePath}"
Width="250" />
<DataGridTextColumn Header="{Binding ColumnStatus}"
Binding="{Binding StatusMessage}"
Width="100" />
</DataGrid.Columns>
</DataGrid>
</Border>
<!-- Status Bar -->
<Border Grid.Row="3"
Background="{DynamicResource NavigationBackgroundBrush}"
Padding="16,12"
Margin="0,16,0,0"
CornerRadius="8">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<!-- Total -->
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="4">
<TextBlock Text="{Binding CommonTotal}" Opacity="0.8" />
<TextBlock Text="{Binding TotalCount}" FontWeight="SemiBold" />
<TextBlock Text="{Binding CommonFiles}" Opacity="0.8" />
</StackPanel>
<!-- Spacer -->
<Panel Grid.Column="1" />
<!-- Success -->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="4" Margin="0,0,24,0">
<TextBlock Text="{Binding CommonSuccess}" Opacity="0.8" />
<TextBlock Text="{Binding CompletedCount}"
FontWeight="SemiBold"
Foreground="#4CAF50" />
</StackPanel>
<!-- Failed -->
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="4">
<TextBlock Text="{Binding CommonFailed}" Opacity="0.8" />
<TextBlock Text="{Binding FailedCount}"
FontWeight="SemiBold"
Foreground="#F44336" />
</StackPanel>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace ZonyLrcTools.Desktop.Views.Pages;
public partial class LyricsDownloadPage : UserControl
{
public LyricsDownloadPage()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,141 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ZonyLrcTools.Desktop.ViewModels"
x:Class="ZonyLrcTools.Desktop.Views.Pages.SettingsPage"
x:DataType="vm:SettingsViewModel">
<ScrollViewer>
<StackPanel Spacing="24">
<!-- Title -->
<StackPanel Margin="0,0,0,8">
<TextBlock Text="{Binding SettingsTitle}"
FontSize="28"
FontWeight="SemiBold" />
</StackPanel>
<!-- Language Settings -->
<Border Background="{DynamicResource NavigationBackgroundBrush}"
CornerRadius="8"
Padding="24">
<StackPanel Spacing="16">
<TextBlock Text="{Binding SettingsLanguage}"
FontSize="18"
FontWeight="SemiBold" />
<CheckBox Content="{Binding SettingsFollowSystem}"
IsChecked="{Binding FollowSystemLanguage}" />
<StackPanel Orientation="Horizontal" Spacing="16" IsEnabled="{Binding !FollowSystemLanguage}">
<RadioButton GroupName="Language"
Content="{Binding SettingsChinese}"
IsChecked="{Binding IsChineseSelected}" />
<RadioButton GroupName="Language"
Content="{Binding SettingsEnglish}"
IsChecked="{Binding IsEnglishSelected}" />
</StackPanel>
</StackPanel>
</Border>
<!-- Network Settings -->
<Border Background="{DynamicResource NavigationBackgroundBrush}"
CornerRadius="8"
Padding="24">
<StackPanel Spacing="16">
<TextBlock Text="{Binding SettingsProxy}"
FontSize="18"
FontWeight="SemiBold" />
<CheckBox Content="{Binding SettingsProxyEnable}"
IsChecked="{Binding IsProxyEnabled}" />
<Grid ColumnDefinitions="*,Auto,120" IsEnabled="{Binding IsProxyEnabled}">
<TextBox Grid.Column="0"
Text="{Binding ProxyIp}"
Watermark="{Binding SettingsProxyAddress}"
Margin="0,0,8,0" />
<TextBlock Grid.Column="1"
Text=":"
VerticalAlignment="Center"
Margin="0,0,8,0" />
<NumericUpDown Grid.Column="2"
Value="{Binding ProxyPort}"
Minimum="1"
Maximum="65535" />
</Grid>
</StackPanel>
</Border>
<!-- Lyrics Settings -->
<Border Background="{DynamicResource NavigationBackgroundBrush}"
CornerRadius="8"
Padding="24">
<StackPanel Spacing="16">
<TextBlock Text="Lyrics"
FontSize="18"
FontWeight="SemiBold" />
<CheckBox Content="Enable translation lyrics"
IsChecked="{Binding IsTranslationEnabled}" />
<CheckBox Content="One-line mode (original + translation on same line)"
IsChecked="{Binding IsOneLineMode}" />
<CheckBox Content="Skip existing lyrics files"
IsChecked="{Binding SkipExistingLyrics}" />
<StackPanel Orientation="Horizontal" Spacing="16">
<TextBlock Text="File Encoding:"
VerticalAlignment="Center" />
<ComboBox ItemsSource="{Binding AvailableEncodings}"
SelectedItem="{Binding SelectedEncoding}"
Width="150" />
</StackPanel>
</StackPanel>
</Border>
<!-- Lyrics Providers -->
<Border Background="{DynamicResource NavigationBackgroundBrush}"
CornerRadius="8"
Padding="24">
<StackPanel Spacing="16">
<TextBlock Text="{Binding SettingsLyricsProvider}"
FontSize="18"
FontWeight="SemiBold" />
<ItemsControl ItemsSource="{Binding LyricsProviders}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:LyricsProviderSettingViewModel">
<Border Background="{DynamicResource AppBackgroundBrush}"
CornerRadius="4"
Padding="12,8"
Margin="0,4">
<Grid ColumnDefinitions="Auto,*,Auto">
<CheckBox Grid.Column="0"
IsChecked="{Binding IsEnabled}"
Margin="0,0,12,0" />
<TextBlock Grid.Column="1"
Text="{Binding Name}"
VerticalAlignment="Center" />
<TextBlock Grid.Column="2"
VerticalAlignment="Center"
Opacity="0.5">
<Run Text="Priority: " />
<Run Text="{Binding Priority}" />
</TextBlock>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<!-- Actions -->
<StackPanel Orientation="Horizontal" Spacing="16" HorizontalAlignment="Right">
<Button Content="{Binding SettingsReset}"
Command="{Binding ResetToDefaultsCommand}" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace ZonyLrcTools.Desktop.Views.Pages;
public partial class SettingsPage : UserControl
{
public SettingsPage()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>WinExe</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<!-- Avalonia -->
<PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.Desktop" />
<PackageReference Include="Avalonia.Themes.Fluent" />
<PackageReference Include="Avalonia.Fonts.Inter" />
<PackageReference Include="Avalonia.Controls.DataGrid" />
<PackageReference Include="Avalonia.Diagnostics" Condition="'$(Configuration)' == 'Debug'" />
<!-- MVVM -->
<PackageReference Include="CommunityToolkit.Mvvm" />
<!-- DI & Hosting -->
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<!-- Logging -->
<PackageReference Include="Serilog.Extensions.Hosting" />
<PackageReference Include="Serilog.Sinks.File" />
<!-- Encoding -->
<PackageReference Include="System.Text.Encoding.CodePages" />
<!-- Localization -->
<PackageReference Include="Microsoft.Extensions.Localization" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ZonyLrcTools.Common\ZonyLrcTools.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\ZonyLrcTools.Cli\config.yaml" Link="config.yaml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\ZonyLrcTools.Cli\BlockWords.json" Link="BlockWords.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="ZonyLrcTools.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>