diff --git a/src/ZonyLrcTools.Cli/Infrastructure/Localization/CliLocalizationService.cs b/src/ZonyLrcTools.Cli/Infrastructure/Localization/CliLocalizationService.cs
new file mode 100644
index 0000000..7fa2328
--- /dev/null
+++ b/src/ZonyLrcTools.Cli/Infrastructure/Localization/CliLocalizationService.cs
@@ -0,0 +1,37 @@
+using Microsoft.Extensions.Localization;
+using ZonyLrcTools.Common.Infrastructure.DependencyInject;
+
+namespace ZonyLrcTools.Cli.Infrastructure.Localization;
+
+///
+/// Provides localization services for CLI commands and options.
+///
+public interface ICliLocalizationService
+{
+ ///
+ /// Gets a localized string by key.
+ ///
+ string this[string key] { get; }
+}
+
+///
+/// Implementation of CLI localization service.
+///
+public class CliLocalizationService : ICliLocalizationService, ISingletonDependency
+{
+ private readonly IStringLocalizer _localizer;
+
+ public CliLocalizationService(IStringLocalizer localizer)
+ {
+ _localizer = localizer;
+ }
+
+ public string this[string key]
+ {
+ get
+ {
+ var value = _localizer[key];
+ return value.ResourceNotFound ? $"[{key}]" : value.Value;
+ }
+ }
+}
diff --git a/src/ZonyLrcTools.Cli/Resources/CommandStrings.cs b/src/ZonyLrcTools.Cli/Resources/CommandStrings.cs
new file mode 100644
index 0000000..caac57d
--- /dev/null
+++ b/src/ZonyLrcTools.Cli/Resources/CommandStrings.cs
@@ -0,0 +1,10 @@
+namespace ZonyLrcTools.Cli;
+
+///
+/// Marker class for CLI command localization resources.
+/// This class is used by IStringLocalizer<CommandStrings> to locate the resource files.
+/// The namespace must be the root namespace so that IStringLocalizer looks for Resources/CommandStrings.resx
+///
+public class CommandStrings
+{
+}
diff --git a/src/ZonyLrcTools.Cli/Resources/CommandStrings.en-US.resx b/src/ZonyLrcTools.Cli/Resources/CommandStrings.en-US.resx
new file mode 100644
index 0000000..23d983d
--- /dev/null
+++ b/src/ZonyLrcTools.Cli/Resources/CommandStrings.en-US.resx
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ ZonyLrcTools - Lyrics download command-line tool
+
+
+
+ Download lyrics or album covers
+
+
+ Specify the folder path containing music files
+
+
+ Download lyrics files
+
+
+ Download album covers
+
+
+ Import song list from CSV file for download
+
+
+ Import song list from NetEase playlist ID for download
+
+
+
+ Utility commands
+
+
+ Convert encrypted music files to normal format
+
+
+ Specify the folder path to convert
+
+
+
+ Search for lyrics
+
+
+ Song name
+
+
+ Artist name
+
+
+
+ Show help information
+
+
+ Show version information
+
+
diff --git a/src/ZonyLrcTools.Cli/Resources/CommandStrings.resx b/src/ZonyLrcTools.Cli/Resources/CommandStrings.resx
new file mode 100644
index 0000000..b57300b
--- /dev/null
+++ b/src/ZonyLrcTools.Cli/Resources/CommandStrings.resx
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ ZonyLrcTools - 歌词下载命令行工具
+
+
+
+ 下载歌词或专辑封面
+
+
+ 指定音乐文件所在的文件夹路径
+
+
+ 下载歌词文件
+
+
+ 下载专辑封面
+
+
+ 从 CSV 文件导入歌曲列表进行下载
+
+
+ 从网易云歌单 ID 导入歌曲列表进行下载
+
+
+
+ 工具类命令
+
+
+ 转换加密音乐文件为普通格式
+
+
+ 指定需要转换的文件夹路径
+
+
+
+ 搜索歌词
+
+
+ 歌曲名称
+
+
+ 艺术家名称
+
+
+
+ 显示帮助信息
+
+
+ 显示版本信息
+
+
diff --git a/src/ZonyLrcTools.Common/Infrastructure/Exceptions/IErrorCodeHelper.cs b/src/ZonyLrcTools.Common/Infrastructure/Exceptions/IErrorCodeHelper.cs
new file mode 100644
index 0000000..e13320f
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Infrastructure/Exceptions/IErrorCodeHelper.cs
@@ -0,0 +1,21 @@
+namespace ZonyLrcTools.Common.Infrastructure.Exceptions;
+
+///
+/// 错误码帮助类接口。
+///
+public interface IErrorCodeHelper
+{
+ ///
+ /// 获取错误消息。
+ ///
+ /// 错误代码。
+ /// 对应的错误消息。
+ string GetMessage(int errorCode);
+
+ ///
+ /// 获取警告消息。
+ ///
+ /// 警告代码。
+ /// 对应的警告消息。
+ string GetWarningMessage(int warningCode);
+}
diff --git a/src/ZonyLrcTools.Common/Infrastructure/Localization/ILocalizationService.cs b/src/ZonyLrcTools.Common/Infrastructure/Localization/ILocalizationService.cs
new file mode 100644
index 0000000..33199ae
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Infrastructure/Localization/ILocalizationService.cs
@@ -0,0 +1,44 @@
+using System.Globalization;
+
+namespace ZonyLrcTools.Common.Infrastructure.Localization;
+
+///
+/// Provides localization services for the application.
+///
+public interface ILocalizationService
+{
+ ///
+ /// Gets a localized string by key.
+ ///
+ string this[string key] { get; }
+
+ ///
+ /// Gets a localized string by key with format arguments.
+ ///
+ string this[string key, params object[] args] { get; }
+
+ ///
+ /// Gets an error message by error code.
+ ///
+ string GetErrorMessage(int errorCode);
+
+ ///
+ /// Gets a warning message by warning code.
+ ///
+ string GetWarningMessage(int warningCode);
+
+ ///
+ /// Sets the current culture.
+ ///
+ void SetCulture(string cultureName);
+
+ ///
+ /// Gets the current culture name.
+ ///
+ string CurrentCulture { get; }
+
+ ///
+ /// Gets the list of supported cultures.
+ ///
+ IReadOnlyList SupportedCultures { get; }
+}
diff --git a/src/ZonyLrcTools.Common/Infrastructure/Localization/LocalizationService.cs b/src/ZonyLrcTools.Common/Infrastructure/Localization/LocalizationService.cs
new file mode 100644
index 0000000..c5019ed
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Infrastructure/Localization/LocalizationService.cs
@@ -0,0 +1,72 @@
+using System.Globalization;
+using Microsoft.Extensions.Localization;
+using ZonyLrcTools.Common.Infrastructure.DependencyInject;
+
+namespace ZonyLrcTools.Common.Infrastructure.Localization;
+
+///
+/// Implementation of using .NET localization.
+///
+public class LocalizationService : ILocalizationService, ISingletonDependency
+{
+ private readonly IStringLocalizer _messagesLocalizer;
+ private readonly IStringLocalizer _errorLocalizer;
+
+ private static readonly CultureInfo[] _supportedCultures =
+ {
+ new("zh-CN"),
+ new("en-US")
+ };
+
+ public LocalizationService(
+ IStringLocalizer messagesLocalizer,
+ IStringLocalizer 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 SupportedCultures => _supportedCultures;
+
+ public void SetCulture(string cultureName)
+ {
+ var culture = new CultureInfo(cultureName);
+ CultureInfo.CurrentCulture = culture;
+ CultureInfo.CurrentUICulture = culture;
+ }
+}
diff --git a/src/ZonyLrcTools.Common/Resources/ErrorMessages.cs b/src/ZonyLrcTools.Common/Resources/ErrorMessages.cs
new file mode 100644
index 0000000..35e9533
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Resources/ErrorMessages.cs
@@ -0,0 +1,10 @@
+namespace ZonyLrcTools.Common;
+
+///
+/// Marker class for error message localization resources.
+/// This class is used by IStringLocalizer<ErrorMessages> to locate the resource files.
+/// The namespace must be the root namespace so that IStringLocalizer looks for Resources/ErrorMessages.resx
+///
+public class ErrorMessages
+{
+}
diff --git a/src/ZonyLrcTools.Common/Resources/ErrorMessages.en-US.resx b/src/ZonyLrcTools.Common/Resources/ErrorMessages.en-US.resx
new file mode 100644
index 0000000..4f119ef
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Resources/ErrorMessages.en-US.resx
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ The file extension to search cannot be empty.
+
+
+ The directory to scan does not exist, please verify the path.
+
+
+ Unable to get the file extension information.
+
+
+ No music files were found.
+
+
+ The specified encoding is not supported, please check the configuration.
+
+
+ Unable to get the song list from NetEase Music.
+
+
+
+ An error occurred while scanning files.
+
+
+ Both song name and artist name are empty, unable to search.
+
+
+ Song name cannot be empty, unable to search.
+
+
+ The downloader did not find the corresponding song information.
+
+
+ The download request returned an invalid response, possibly a server error.
+
+
+ Tag info reader is empty, unable to parse music tag information.
+
+
+ TagLib tag reader encountered an unexpected exception.
+
+
+ Service interface restricted, unable to make request, please try using a proxy server.
+
+
+ HTTP request to target server failed.
+
+
+ Failed to deserialize HTTP response to JSON.
+
+
+ Currently only NCM format song conversion is supported.
+
+
diff --git a/src/ZonyLrcTools.Common/Resources/ErrorMessages.resx b/src/ZonyLrcTools.Common/Resources/ErrorMessages.resx
new file mode 100644
index 0000000..c4bb899
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Resources/ErrorMessages.resx
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ 待搜索的后缀不能为空。
+
+
+ 需要扫描的目录不存在,请确认路径是否正确。
+
+
+ 不能获取文件的后缀信息。
+
+
+ 没有扫描到任何音乐文件。
+
+
+ 指定的编码不受支持,请检查配置。
+
+
+ 无法从网易云音乐获取歌曲列表。
+
+
+
+ 扫描文件时出现了错误。
+
+
+ 歌曲名称或歌手名称均为空,无法进行搜索。
+
+
+ 歌曲名称不能为空,无法进行搜索。
+
+
+ 下载器没有搜索到对应的歌曲信息。
+
+
+ 下载请求的返回值不合法,可能是服务端故障。
+
+
+ 标签信息读取器为空,无法解析音乐 Tag 信息。
+
+
+ TagLib 标签读取器出现了预期之外的异常。
+
+
+ 服务接口限制,无法进行请求,请尝试使用代理服务器。
+
+
+ 对目标服务器执行 HTTP 请求失败。
+
+
+ HTTP 请求的结果反序列化为 JSON 失败。
+
+
+ 目前仅支持 NCM 格式的歌曲转换操作。
+
+
diff --git a/src/ZonyLrcTools.Common/Resources/Messages.cs b/src/ZonyLrcTools.Common/Resources/Messages.cs
new file mode 100644
index 0000000..338eed4
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Resources/Messages.cs
@@ -0,0 +1,10 @@
+namespace ZonyLrcTools.Common;
+
+///
+/// Marker class for general message localization resources.
+/// This class is used by IStringLocalizer<Messages> to locate the resource files.
+/// The namespace must be the root namespace so that IStringLocalizer looks for Resources/Messages.resx
+///
+public class Messages
+{
+}
diff --git a/src/ZonyLrcTools.Common/Resources/Messages.en-US.resx b/src/ZonyLrcTools.Common/Resources/Messages.en-US.resx
new file mode 100644
index 0000000..7b8c844
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Resources/Messages.en-US.resx
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ Starting lyrics file download...
+
+
+ Lyrics download complete, success: {0} skipped (instrumental): {1} failed: {2}.
+
+
+ Song: {0}, Artist: {1}, download successful.
+
+
+ Song: {0}, Artist: {1}, download failed.
+
+
+
+ Starting album cover download...
+
+
+ Album cover download complete, success: {0} failed: {1}.
+
+
+
+ Starting folder scan, please wait...
+
+
+ Scan complete, {0} files found.
+
+
+
+ Starting...
+
+
+ An unhandled exception occurred.
+
+
+ Error code: {0}
+
+
+ Error message: {0}
+
+
diff --git a/src/ZonyLrcTools.Common/Resources/Messages.resx b/src/ZonyLrcTools.Common/Resources/Messages.resx
new file mode 100644
index 0000000..0d73327
--- /dev/null
+++ b/src/ZonyLrcTools.Common/Resources/Messages.resx
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ 开始下载歌词文件数据...
+
+
+ 歌词数据下载完成,成功: {0} 跳过(纯音乐): {1} 失败: {2}。
+
+
+ 歌曲名: {0}, 艺术家: {1}, 下载成功。
+
+
+ 歌曲名: {0}, 艺术家: {1}, 下载失败。
+
+
+
+ 开始下载专辑封面...
+
+
+ 专辑封面下载完成,成功: {0} 失败: {1}。
+
+
+
+ 开始扫描文件夹,请稍等...
+
+
+ 扫描完成,共 {0} 个文件。
+
+
+
+ 正在启动...
+
+
+ 出现了未处理的异常。
+
+
+ 错误代码: {0}
+
+
+ 错误信息: {0}
+
+
diff --git a/src/ZonyLrcTools.Desktop/App.axaml b/src/ZonyLrcTools.Desktop/App.axaml
new file mode 100644
index 0000000..7e38d66
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/App.axaml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+ #FFFFFF
+ #0078D4
+ #F3F3F3
+ #107C10
+ #D13438
+ #FFB900
+
+
+ #1F1F1F
+ #60CDFF
+ #2D2D2D
+ #6CCB5F
+ #FF99A4
+ #FCE100
+
+
+
+
+
+
diff --git a/src/ZonyLrcTools.Desktop/App.axaml.cs b/src/ZonyLrcTools.Desktop/App.axaml.cs
new file mode 100644
index 0000000..4368b98
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/App.axaml.cs
@@ -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();
+
+ desktop.MainWindow = new MainWindow
+ {
+ DataContext = mainViewModel
+ };
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/Infrastructure/Localization/LocalizeExtension.cs b/src/ZonyLrcTools.Desktop/Infrastructure/Localization/LocalizeExtension.cs
new file mode 100644
index 0000000..0c6b11e
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Infrastructure/Localization/LocalizeExtension.cs
@@ -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;
+
+///
+/// Avalonia markup extension for localization.
+/// Usage: Text="{loc:Localize Key=Nav_Home}"
+///
+public class LocalizeExtension : MarkupExtension
+{
+ public LocalizeExtension()
+ {
+ }
+
+ public LocalizeExtension(string key)
+ {
+ Key = key;
+ }
+
+ ///
+ /// The resource key to look up.
+ ///
+ public string? Key { get; set; }
+
+ ///
+ /// Default value if the key is not found.
+ ///
+ public string? Default { get; set; }
+
+ public override object ProvideValue(IServiceProvider serviceProvider)
+ {
+ if (string.IsNullOrEmpty(Key))
+ {
+ return Default ?? "[NO_KEY]";
+ }
+
+ var localizer = App.Services?.GetService>();
+ if (localizer == null)
+ {
+ return Default ?? $"[{Key}]";
+ }
+
+ var localizedValue = localizer[Key];
+ if (localizedValue.ResourceNotFound)
+ {
+ return Default ?? $"[{Key}]";
+ }
+
+ return localizedValue.Value;
+ }
+}
+
+///
+/// UI localization service for dynamic language switching.
+///
+public interface IUILocalizationService
+{
+ ///
+ /// Gets a localized string by key.
+ ///
+ string this[string key] { get; }
+
+ ///
+ /// Gets a localized string by key with format arguments.
+ ///
+ string this[string key, params object[] args] { get; }
+
+ ///
+ /// Gets the current culture name.
+ ///
+ string CurrentCulture { get; }
+
+ ///
+ /// Changes the current UI language.
+ ///
+ void SetLanguage(string cultureName);
+
+ ///
+ /// Gets whether the language follows system settings.
+ ///
+ bool FollowSystem { get; set; }
+
+ ///
+ /// Event raised when the language changes.
+ ///
+ event EventHandler? LanguageChanged;
+}
+
+///
+/// Implementation of UI localization service.
+///
+public class UILocalizationService : IUILocalizationService
+{
+ private readonly IStringLocalizer _localizer;
+ private bool _followSystem = true;
+
+ public UILocalizationService(IStringLocalizer 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);
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/Program.cs b/src/ZonyLrcTools.Desktop/Program.cs
new file mode 100644
index 0000000..06c3b38
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Program.cs
@@ -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()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace();
+}
diff --git a/src/ZonyLrcTools.Desktop/Resources/UIStrings.cs b/src/ZonyLrcTools.Desktop/Resources/UIStrings.cs
new file mode 100644
index 0000000..fda7205
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Resources/UIStrings.cs
@@ -0,0 +1,10 @@
+namespace ZonyLrcTools.Desktop;
+
+///
+/// Marker class for UI localization resources.
+/// This class is used by IStringLocalizer<UIStrings> to locate the resource files.
+/// The namespace must be the root namespace so that IStringLocalizer looks for Resources/UIStrings.resx
+///
+public class UIStrings
+{
+}
diff --git a/src/ZonyLrcTools.Desktop/Resources/UIStrings.en-US.resx b/src/ZonyLrcTools.Desktop/Resources/UIStrings.en-US.resx
new file mode 100644
index 0000000..c4133c5
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Resources/UIStrings.en-US.resx
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ Home
+
+
+ Lyrics Download
+
+
+ Album Cover
+
+
+ Settings
+
+
+
+ Welcome to ZonyLrcTools
+
+
+ An open-source lyrics download tool
+
+
+ Quick Start
+
+
+ Download Lyrics
+
+
+ Download Album Covers
+
+
+
+ Lyrics Download
+
+
+ Batch download lyrics for your music files
+
+
+ Select music folder...
+
+
+ Browse...
+
+
+ Parallel:
+
+
+ Start Download
+
+
+ Stop Download
+
+
+ Download Progress
+
+
+ Idle
+
+
+ Scanning files...
+
+
+ Downloading lyrics...
+
+
+ Download Complete
+
+
+ Total:
+
+
+ files
+
+
+ Success:
+
+
+ Failed:
+
+
+
+ Album Cover Download
+
+
+ Download album artwork for your music collection
+
+
+ Select music folder...
+
+
+ Browse...
+
+
+ Parallel:
+
+
+ Start Download
+
+
+ Stop Download
+
+
+
+ Settings
+
+
+ Language
+
+
+ Follow System
+
+
+ Simplified Chinese
+
+
+ English
+
+
+ Theme
+
+
+ Light
+
+
+ Dark
+
+
+ Follow System
+
+
+ Lyrics Provider
+
+
+ Proxy Settings
+
+
+ Enable Proxy
+
+
+ Proxy Address
+
+
+ Proxy Port
+
+
+ Save Settings
+
+
+ Reset
+
+
+
+ Song Name
+
+
+ Artist
+
+
+ Album
+
+
+ Status
+
+
+ File Path
+
+
+
+ Success
+
+
+ Failed
+
+
+ Skipped
+
+
+ Pending
+
+
+ Processing
+
+
+
+ OK
+
+
+ Cancel
+
+
+ Yes
+
+
+ No
+
+
+ Error
+
+
+ Warning
+
+
+ Info
+
+
diff --git a/src/ZonyLrcTools.Desktop/Resources/UIStrings.resx b/src/ZonyLrcTools.Desktop/Resources/UIStrings.resx
new file mode 100644
index 0000000..0f9e519
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Resources/UIStrings.resx
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ 首页
+
+
+ 歌词下载
+
+
+ 专辑封面
+
+
+ 设置
+
+
+
+ 欢迎使用 ZonyLrcTools
+
+
+ 一款开源的歌词下载工具
+
+
+ 快速开始
+
+
+ 下载歌词
+
+
+ 下载专辑封面
+
+
+
+ 歌词下载
+
+
+ 批量下载音乐文件的歌词
+
+
+ 选择音乐文件夹...
+
+
+ 浏览...
+
+
+ 并行:
+
+
+ 开始下载
+
+
+ 停止下载
+
+
+ 下载进度
+
+
+ 等待中
+
+
+ 正在扫描文件...
+
+
+ 正在下载歌词...
+
+
+ 下载完成
+
+
+ 总计:
+
+
+ 个文件
+
+
+ 成功:
+
+
+ 失败:
+
+
+
+ 专辑封面下载
+
+
+ 批量下载音乐文件的专辑封面
+
+
+ 选择音乐文件夹...
+
+
+ 浏览...
+
+
+ 并行:
+
+
+ 开始下载
+
+
+ 停止下载
+
+
+
+ 设置
+
+
+ 语言
+
+
+ 跟随系统
+
+
+ 简体中文
+
+
+ English
+
+
+ 主题
+
+
+ 浅色
+
+
+ 深色
+
+
+ 跟随系统
+
+
+ 歌词源
+
+
+ 代理设置
+
+
+ 启用代理
+
+
+ 代理地址
+
+
+ 代理端口
+
+
+ 保存设置
+
+
+ 重置
+
+
+
+ 歌曲名
+
+
+ 艺术家
+
+
+ 专辑
+
+
+ 状态
+
+
+ 文件路径
+
+
+
+ 成功
+
+
+ 失败
+
+
+ 已跳过
+
+
+ 等待中
+
+
+ 处理中
+
+
+
+ 确定
+
+
+ 取消
+
+
+ 是
+
+
+ 否
+
+
+ 错误
+
+
+ 警告
+
+
+ 提示
+
+
diff --git a/src/ZonyLrcTools.Desktop/Services/AvaloniaWarpLogger.cs b/src/ZonyLrcTools.Desktop/Services/AvaloniaWarpLogger.cs
new file mode 100644
index 0000000..450dbc9
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Services/AvaloniaWarpLogger.cs
@@ -0,0 +1,67 @@
+using System.Collections.ObjectModel;
+using Avalonia.Threading;
+using ZonyLrcTools.Common.Infrastructure.Logging;
+
+namespace ZonyLrcTools.Desktop.Services;
+
+///
+/// Avalonia implementation of IWarpLogger that supports UI binding.
+///
+public class AvaloniaWarpLogger : IWarpLogger
+{
+ private const int MaxLogEntries = 1000;
+
+ public ObservableCollection LogEntries { get; } = new();
+
+ public event EventHandler? 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
+}
diff --git a/src/ZonyLrcTools.Desktop/Services/DialogService.cs b/src/ZonyLrcTools.Desktop/Services/DialogService.cs
new file mode 100644
index 0000000..5a030d9
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Services/DialogService.cs
@@ -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 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 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 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;
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/Services/IDialogService.cs b/src/ZonyLrcTools.Desktop/Services/IDialogService.cs
new file mode 100644
index 0000000..0428216
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Services/IDialogService.cs
@@ -0,0 +1,9 @@
+namespace ZonyLrcTools.Desktop.Services;
+
+public interface IDialogService
+{
+ Task ShowFolderPickerAsync(string title);
+ Task ShowFilePickerAsync(string title, string[]? filters = null);
+ Task ShowConfirmDialogAsync(string title, string message);
+ Task ShowMessageAsync(string title, string message);
+}
diff --git a/src/ZonyLrcTools.Desktop/Services/INavigationService.cs b/src/ZonyLrcTools.Desktop/Services/INavigationService.cs
new file mode 100644
index 0000000..f3697f0
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Services/INavigationService.cs
@@ -0,0 +1,13 @@
+using ZonyLrcTools.Desktop.ViewModels;
+
+namespace ZonyLrcTools.Desktop.Services;
+
+public interface INavigationService
+{
+ ViewModelBase? CurrentViewModel { get; }
+ event EventHandler? NavigationChanged;
+
+ TViewModel NavigateTo() where TViewModel : ViewModelBase;
+ void GoBack();
+ bool CanGoBack { get; }
+}
diff --git a/src/ZonyLrcTools.Desktop/Services/IThemeService.cs b/src/ZonyLrcTools.Desktop/Services/IThemeService.cs
new file mode 100644
index 0000000..03f14d3
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Services/IThemeService.cs
@@ -0,0 +1,12 @@
+using Avalonia.Styling;
+
+namespace ZonyLrcTools.Desktop.Services;
+
+public interface IThemeService
+{
+ ThemeVariant CurrentTheme { get; }
+ event EventHandler? ThemeChanged;
+
+ void SetTheme(ThemeVariant theme);
+ void ToggleTheme();
+}
diff --git a/src/ZonyLrcTools.Desktop/Services/NavigationService.cs b/src/ZonyLrcTools.Desktop/Services/NavigationService.cs
new file mode 100644
index 0000000..57fa5e2
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Services/NavigationService.cs
@@ -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 _navigationStack = new();
+
+ public ViewModelBase? CurrentViewModel => _navigationStack.TryPeek(out var vm) ? vm : null;
+ public bool CanGoBack => _navigationStack.Count > 1;
+
+ public event EventHandler? NavigationChanged;
+
+ public NavigationService(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public TViewModel NavigateTo() where TViewModel : ViewModelBase
+ {
+ var viewModel = _serviceProvider.GetRequiredService();
+
+ 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);
+ }
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/Services/ServiceCollectionExtensions.cs b/src/ZonyLrcTools.Desktop/Services/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..122768e
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Services/ServiceCollectionExtensions.cs
@@ -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
+{
+ ///
+ /// Register all services for the desktop application.
+ ///
+ public static IServiceCollection AddDesktopServices(this IServiceCollection services)
+ {
+ // Register Common project's auto dependency injection
+ services.BeginAutoDependencyInject();
+ services.BeginAutoDependencyInject();
+
+ // 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();
+
+ // Register GUI-specific IWarpLogger implementation
+ services.AddSingleton();
+ services.AddSingleton(sp => sp.GetRequiredService());
+
+ // Register navigation and dialog services
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ // Register ViewModels
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+
+ return services;
+ }
+
+ ///
+ /// Initialize language settings based on system culture or user preference.
+ ///
+ 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;
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/Services/ThemeService.cs b/src/ZonyLrcTools.Desktop/Services/ThemeService.cs
new file mode 100644
index 0000000..d8b1a57
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Services/ThemeService.cs
@@ -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? 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);
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/ViewModels/AlbumDownloadViewModel.cs b/src/ZonyLrcTools.Desktop/ViewModels/AlbumDownloadViewModel.cs
new file mode 100644
index 0000000..688b2b1
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/ViewModels/AlbumDownloadViewModel.cs
@@ -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 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();
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/ViewModels/HomeViewModel.cs b/src/ZonyLrcTools.Desktop/ViewModels/HomeViewModel.cs
new file mode 100644
index 0000000..197da45
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/ViewModels/HomeViewModel.cs
@@ -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";
+}
diff --git a/src/ZonyLrcTools.Desktop/ViewModels/LyricsDownloadViewModel.cs b/src/ZonyLrcTools.Desktop/ViewModels/LyricsDownloadViewModel.cs
new file mode 100644
index 0000000..ac0412e
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/ViewModels/LyricsDownloadViewModel.cs
@@ -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 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;
+}
diff --git a/src/ZonyLrcTools.Desktop/ViewModels/MainWindowViewModel.cs b/src/ZonyLrcTools.Desktop/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000..479dec8
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/ViewModels/MainWindowViewModel.cs
@@ -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);
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/ViewModels/SettingsViewModel.cs b/src/ZonyLrcTools.Desktop/ViewModels/SettingsViewModel.cs
new file mode 100644
index 0000000..9a4d70e
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/ViewModels/SettingsViewModel.cs
@@ -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 AvailableEncodings { get; } = new()
+ {
+ "utf-8",
+ "utf-8-bom",
+ "gb2312",
+ "gbk"
+ };
+
+ public ObservableCollection LyricsProviders { get; } = new();
+
+ public SettingsViewModel(IOptions 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;
+}
diff --git a/src/ZonyLrcTools.Desktop/ViewModels/ViewModelBase.cs b/src/ZonyLrcTools.Desktop/ViewModels/ViewModelBase.cs
new file mode 100644
index 0000000..df142a3
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/ViewModels/ViewModelBase.cs
@@ -0,0 +1,28 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace ZonyLrcTools.Desktop.ViewModels;
+
+///
+/// Base class for all ViewModels.
+///
+public abstract partial class ViewModelBase : ObservableObject
+{
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(IsNotBusy))]
+ private bool _isBusy;
+
+ [ObservableProperty]
+ private string? _busyMessage;
+
+ public bool IsNotBusy => !IsBusy;
+
+ ///
+ /// Called when the ViewModel is activated (navigated to).
+ ///
+ public virtual Task OnActivatedAsync() => Task.CompletedTask;
+
+ ///
+ /// Called when the ViewModel is deactivated (navigated away).
+ ///
+ public virtual Task OnDeactivatedAsync() => Task.CompletedTask;
+}
diff --git a/src/ZonyLrcTools.Desktop/Views/MainWindow.axaml b/src/ZonyLrcTools.Desktop/Views/MainWindow.axaml
new file mode 100644
index 0000000..d827c6f
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Views/MainWindow.axaml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ZonyLrcTools.Desktop/Views/MainWindow.axaml.cs b/src/ZonyLrcTools.Desktop/Views/MainWindow.axaml.cs
new file mode 100644
index 0000000..942028f
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Views/MainWindow.axaml.cs
@@ -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();
+ break;
+ case 1:
+ viewModel.CurrentPage = services.GetRequiredService();
+ break;
+ case 2:
+ viewModel.CurrentPage = services.GetRequiredService();
+ break;
+ case 3:
+ viewModel.CurrentPage = services.GetRequiredService();
+ break;
+ }
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/Views/Pages/AlbumDownloadPage.axaml b/src/ZonyLrcTools.Desktop/Views/Pages/AlbumDownloadPage.axaml
new file mode 100644
index 0000000..4db22c6
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Views/Pages/AlbumDownloadPage.axaml
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ZonyLrcTools.Desktop/Views/Pages/AlbumDownloadPage.axaml.cs b/src/ZonyLrcTools.Desktop/Views/Pages/AlbumDownloadPage.axaml.cs
new file mode 100644
index 0000000..e754c5b
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Views/Pages/AlbumDownloadPage.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace ZonyLrcTools.Desktop.Views.Pages;
+
+public partial class AlbumDownloadPage : UserControl
+{
+ public AlbumDownloadPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/Views/Pages/HomePage.axaml b/src/ZonyLrcTools.Desktop/Views/Pages/HomePage.axaml
new file mode 100644
index 0000000..0831fc6
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Views/Pages/HomePage.axaml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ZonyLrcTools.Desktop/Views/Pages/HomePage.axaml.cs b/src/ZonyLrcTools.Desktop/Views/Pages/HomePage.axaml.cs
new file mode 100644
index 0000000..4bdea57
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Views/Pages/HomePage.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace ZonyLrcTools.Desktop.Views.Pages;
+
+public partial class HomePage : UserControl
+{
+ public HomePage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/Views/Pages/LyricsDownloadPage.axaml b/src/ZonyLrcTools.Desktop/Views/Pages/LyricsDownloadPage.axaml
new file mode 100644
index 0000000..19e11d4
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Views/Pages/LyricsDownloadPage.axaml
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ZonyLrcTools.Desktop/Views/Pages/LyricsDownloadPage.axaml.cs b/src/ZonyLrcTools.Desktop/Views/Pages/LyricsDownloadPage.axaml.cs
new file mode 100644
index 0000000..004e109
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Views/Pages/LyricsDownloadPage.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace ZonyLrcTools.Desktop.Views.Pages;
+
+public partial class LyricsDownloadPage : UserControl
+{
+ public LyricsDownloadPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/Views/Pages/SettingsPage.axaml b/src/ZonyLrcTools.Desktop/Views/Pages/SettingsPage.axaml
new file mode 100644
index 0000000..3a018ff
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Views/Pages/SettingsPage.axaml
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ZonyLrcTools.Desktop/Views/Pages/SettingsPage.axaml.cs b/src/ZonyLrcTools.Desktop/Views/Pages/SettingsPage.axaml.cs
new file mode 100644
index 0000000..e18d72f
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/Views/Pages/SettingsPage.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace ZonyLrcTools.Desktop.Views.Pages;
+
+public partial class SettingsPage : UserControl
+{
+ public SettingsPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/ZonyLrcTools.Desktop/ZonyLrcTools.Desktop.csproj b/src/ZonyLrcTools.Desktop/ZonyLrcTools.Desktop.csproj
new file mode 100644
index 0000000..44a7e60
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/ZonyLrcTools.Desktop.csproj
@@ -0,0 +1,48 @@
+
+
+
+ net10.0
+ WinExe
+ enable
+ enable
+ true
+ app.manifest
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
diff --git a/src/ZonyLrcTools.Desktop/app.manifest b/src/ZonyLrcTools.Desktop/app.manifest
new file mode 100644
index 0000000..83f2e29
--- /dev/null
+++ b/src/ZonyLrcTools.Desktop/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ PerMonitorV2
+
+
+