feat: 本地化框架

This commit is contained in:
2026-06-10 19:25:57 +08:00
parent 6e664a7762
commit 3b3bf46097
21 changed files with 617 additions and 148 deletions
+4 -3
View File
@@ -17,12 +17,12 @@
### Windows ### Windows
数据库: %APPDATA%\Local\OCES\Resonance\Databases 数据库: %APPDATA%\Local\OCES\Resonance\Databases
偏好设置: %APPDATA%\Roaming\OCES\Resonance\ 偏好设置: %APPDATA%\Roaming\OCES\Resonance\preferences.json
### Linux ### Linux
数据库: XDG_DATA_HOME 数据库: XDG_DATA_HOME
偏好设置: XDG_CONFIG_HOME 偏好设置: XDG_CONFIG_HOME/preferences.json
缓存数据: XDG_CACHE_HOME 缓存数据: XDG_CACHE_HOME
### TODO ### TODO
@@ -30,7 +30,8 @@
- [ ] 扫描时如果报错,报错信息可能会填满整个窗口,导致Overlay无法关闭。 - [ ] 扫描时如果报错,报错信息可能会填满整个窗口,导致Overlay无法关闭。
- [ ] 读取时如果报错,没有任何警告,会静默报错。需要有一个界面右下方的toast,或是发送系统通知告知用户遇到了错误。 - [ ] 读取时如果报错,没有任何警告,会静默报错。需要有一个界面右下方的toast,或是发送系统通知告知用户遇到了错误。
- [x] 指针化Artwork字段。如果artwork的md5 - [x] 指针化Artwork字段。如果artwork的md5
- [ ] 本地化框架 - [x] 本地化框架
- [ ] 偏好设置/控制面板
## 技术栈 ## 技术栈
+3 -3
View File
@@ -3,6 +3,7 @@
x:Class="GUI.App" x:Class="GUI.App"
Name="Resonance" Name="Resonance"
xmlns:local="using:GUI" xmlns:local="using:GUI"
xmlns:l10n="using:GUI.Converters"
RequestedThemeVariant="Default"> RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
@@ -10,11 +11,10 @@
<local:ViewLocator/> <local:ViewLocator/>
</Application.DataTemplates> </Application.DataTemplates>
<NativeMenu.Menu> <NativeMenu.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="关于…" /> <NativeMenuItem Header="{l10n:Localize About}" />
<NativeMenuItem Header="偏好设置…"/> <NativeMenuItem Header="{l10n:Localize Preferences}"/>
</NativeMenu> </NativeMenu>
</NativeMenu.Menu> </NativeMenu.Menu>
+1 -3
View File
@@ -1,9 +1,7 @@
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using System.Linq;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using GUI.Services;
using GUI.ViewModels; using GUI.ViewModels;
using GUI.Views; using GUI.Views;
-48
View File
@@ -1,48 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace GUI {
using System;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Locale {
private static System.Resources.ResourceManager resourceMan;
private static System.Globalization.CultureInfo resourceCulture;
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Locale() {
}
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Resources.ResourceManager ResourceManager {
get {
if (object.Equals(null, resourceMan)) {
System.Resources.ResourceManager temp = new System.Resources.ResourceManager("GUI.Locale", typeof(Locale).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}
-21
View File
@@ -1,21 +0,0 @@
<?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:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+25
View File
@@ -0,0 +1,25 @@
using Avalonia.Data;
using Avalonia.Markup.Xaml;
using GUI.Services;
namespace GUI.Converters;
public class LocalizeExtension : MarkupExtension
{
public string Key { get; set; }
public LocalizeExtension(string key)
{
Key = key;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return new Binding
{
Source = LocalizationService.Instance,
Path = $"[{Key}]",
Mode = BindingMode.OneWay,
};
}
}
+3 -2
View File
@@ -1,5 +1,6 @@
using System.Globalization; using System.Globalization;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using GUI.Services;
namespace GUI.Converters; namespace GUI.Converters;
@@ -36,8 +37,8 @@ public class ChannelsFormatConverter : IValueConverter
{ {
return value switch return value switch
{ {
int ch when ch == 1 => "Mono", int ch when ch == 1 => LocalizationService.Instance["ChannelsMono"],
int ch when ch == 2 => "Stereo", int ch when ch == 2 => LocalizationService.Instance["ChannelsStereo"],
null => "--", null => "--",
int ch => $"{ch}ch", int ch => $"{ch}ch",
_ => "--", _ => "--",
+6 -11
View File
@@ -10,6 +10,7 @@
<ItemGroup> <ItemGroup>
<Folder Include="Models\"/> <Folder Include="Models\"/>
<Folder Include="Resources\"/>
<AvaloniaResource Include="Assets\**"/> <AvaloniaResource Include="Assets\**"/>
</ItemGroup> </ItemGroup>
@@ -32,17 +33,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Update="Assets\Locale.resx"> <EmbeddedResource Include="Resources\LocaleZh.xml">
<Generator>ResXFileCodeGenerator</Generator> <LogicalName>GUI.LocaleZh.xml</LogicalName>
<LastGenOutput>Locale.Designer.cs</LastGenOutput> </EmbeddedResource>
<EmbeddedResource Include="Resources\LocaleEn.xml">
<LogicalName>GUI.LocaleEn.xml</LogicalName>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Update="Assets\Locale.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Locale.resx</DependentUpon>
</Compile>
</ItemGroup>
</Project> </Project>
+157
View File
@@ -0,0 +1,157 @@
<?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:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="About" xml:space="preserve">
<value>About…</value>
</data>
<data name="Preferences" xml:space="preserve">
<value>Preferences…</value>
</data>
<data name="Database" xml:space="preserve">
<value>Database</value>
</data>
<data name="MenuDeleteOrphanArtwork" xml:space="preserve">
<value>Delete Unused Artworks</value>
</data>
<data name="MenuOpenDatabaseFolder" xml:space="preserve">
<value>Open database file in Finder...</value>
</data>
<data name="ScanDirectory" xml:space="preserve">
<value>Scan Directory</value>
</data>
<data name="SelectAudioDirectory" xml:space="preserve">
<value>Select Audio Directory</value>
</data>
<data name="SearchFilesHint" xml:space="preserve">
<value>Search files...</value>
</data>
<data name="Refresh" xml:space="preserve">
<value>Refresh</value>
</data>
<data name="ResetFilters" xml:space="preserve">
<value>Reset Filters</value>
</data>
<data name="NoFilesHint" xml:space="preserve">
<value>No files yet. Scan a directory to add audio files.</value>
</data>
<data name="Metadata" xml:space="preserve">
<value>Metadata</value>
</data>
<data name="Save" xml:space="preserve">
<value>Save</value>
</data>
<data name="BasicInfo" xml:space="preserve">
<value>Basic Info</value>
</data>
<data name="Rating" xml:space="preserve">
<value>Rating</value>
</data>
<data name="TagsAndDescription" xml:space="preserve">
<value>Tags &amp; Description</value>
</data>
<data name="Category" xml:space="preserve">
<value>Category</value>
</data>
<data name="KeywordsCommaSeparated" xml:space="preserve">
<value>Keywords (comma-separated)</value>
</data>
<data name="Description" xml:space="preserve">
<value>Description</value>
</data>
<data name="Notes" xml:space="preserve">
<value>Notes</value>
</data>
<data name="DurationLabel" xml:space="preserve">
<value>Duration: </value>
</data>
<data name="SampleRateLabel" xml:space="preserve">
<value>Sample Rate: </value>
</data>
<data name="BitDepthLabel" xml:space="preserve">
<value>Bit Depth: </value>
</data>
<data name="ChannelsLabel" xml:space="preserve">
<value>Channels: </value>
</data>
<data name="TypeLabel" xml:space="preserve">
<value>Format: </value>
</data>
<data name="SelectFileToViewMetadata" xml:space="preserve">
<value>Select a file to view metadata</value>
</data>
<data name="CategoryPlaceholder" xml:space="preserve">
<value>Enter category...</value>
</data>
<data name="KeywordsPlaceholder" xml:space="preserve">
<value>Enter keywords...</value>
</data>
<data name="DescriptionPlaceholder" xml:space="preserve">
<value>Enter description...</value>
</data>
<data name="NotesPlaceholder" xml:space="preserve">
<value>Enter notes...</value>
</data>
<data name="Processed" xml:space="preserve">
<value>Processed: </value>
</data>
<data name="AddedFormat" xml:space="preserve">
<value>Added: {0}</value>
</data>
<data name="SkippedFormat" xml:space="preserve">
<value>Skipped: {0}</value>
</data>
<data name="Close" xml:space="preserve">
<value>Close</value>
</data>
<data name="Browse" xml:space="preserve">
<value>Browse</value>
</data>
<data name="AllFiles" xml:space="preserve">
<value>All Files</value>
</data>
<data name="Tags" xml:space="preserve">
<value>Tags</value>
</data>
<data name="ScanPreparing" xml:space="preserve">
<value>Preparing to scan...</value>
</data>
<data name="ScanDiscovering" xml:space="preserve">
<value>Discovering files...</value>
</data>
<data name="ScanParsing" xml:space="preserve">
<value>Found {0} files, parsing metadata...</value>
</data>
<data name="ScanComplete" xml:space="preserve">
<value>Scan complete: {0} added, {1} skipped</value>
</data>
<data name="ScanError" xml:space="preserve">
<value>Scan error: {0}</value>
</data>
<data name="NoFileSelected" xml:space="preserve">
<value>No file selected</value>
</data>
<data name="ChannelsMono" xml:space="preserve">
<value>Mono</value>
</data>
<data name="ChannelsStereo" xml:space="preserve">
<value>Stereo</value>
</data>
<data name="SortByField" xml:space="preserve">
<value>Sort by</value>
</data>
</root>
+157
View File
@@ -0,0 +1,157 @@
<?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:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="About" xml:space="preserve">
<value>关于…</value>
</data>
<data name="Preferences" xml:space="preserve">
<value>偏好设置…</value>
</data>
<data name="Database" xml:space="preserve">
<value>数据库</value>
</data>
<data name="MenuDeleteOrphanArtwork" xml:space="preserve">
<value>删除没有使用的封面</value>
</data>
<data name="MenuOpenDatabaseFolder" xml:space="preserve">
<value>在访达中打开数据库文件…</value>
</data>
<data name="ScanDirectory" xml:space="preserve">
<value>扫描目录</value>
</data>
<data name="SelectAudioDirectory" xml:space="preserve">
<value>选择音频目录</value>
</data>
<data name="SearchFilesHint" xml:space="preserve">
<value>搜索文件...</value>
</data>
<data name="Refresh" xml:space="preserve">
<value>刷新</value>
</data>
<data name="ResetFilters" xml:space="preserve">
<value>重置筛选</value>
</data>
<data name="NoFilesHint" xml:space="preserve">
<value>暂无文件,请先扫描目录添加音频文件。</value>
</data>
<data name="Metadata" xml:space="preserve">
<value>元数据</value>
</data>
<data name="Save" xml:space="preserve">
<value>保存</value>
</data>
<data name="BasicInfo" xml:space="preserve">
<value>基础信息</value>
</data>
<data name="Rating" xml:space="preserve">
<value>评分</value>
</data>
<data name="TagsAndDescription" xml:space="preserve">
<value>标签与描述</value>
</data>
<data name="Category" xml:space="preserve">
<value>分类</value>
</data>
<data name="KeywordsCommaSeparated" xml:space="preserve">
<value>关键词(逗号分隔)</value>
</data>
<data name="Description" xml:space="preserve">
<value>描述</value>
</data>
<data name="Notes" xml:space="preserve">
<value>备注</value>
</data>
<data name="DurationLabel" xml:space="preserve">
<value>时长: </value>
</data>
<data name="SampleRateLabel" xml:space="preserve">
<value>采样率: </value>
</data>
<data name="BitDepthLabel" xml:space="preserve">
<value>位深: </value>
</data>
<data name="ChannelsLabel" xml:space="preserve">
<value>声道: </value>
</data>
<data name="TypeLabel" xml:space="preserve">
<value>格式: </value>
</data>
<data name="SelectFileToViewMetadata" xml:space="preserve">
<value>选择文件查看元数据</value>
</data>
<data name="CategoryPlaceholder" xml:space="preserve">
<value>输入分类...</value>
</data>
<data name="KeywordsPlaceholder" xml:space="preserve">
<value>输入关键词...</value>
</data>
<data name="DescriptionPlaceholder" xml:space="preserve">
<value>输入描述...</value>
</data>
<data name="NotesPlaceholder" xml:space="preserve">
<value>输入备注...</value>
</data>
<data name="Processed" xml:space="preserve">
<value>已处理: </value>
</data>
<data name="AddedFormat" xml:space="preserve">
<value>新增: {0}</value>
</data>
<data name="SkippedFormat" xml:space="preserve">
<value>跳过: {0}</value>
</data>
<data name="Close" xml:space="preserve">
<value>关闭</value>
</data>
<data name="Browse" xml:space="preserve">
<value>浏览</value>
</data>
<data name="AllFiles" xml:space="preserve">
<value>全部文件</value>
</data>
<data name="Tags" xml:space="preserve">
<value>标签</value>
</data>
<data name="ScanPreparing" xml:space="preserve">
<value>准备扫描...</value>
</data>
<data name="ScanDiscovering" xml:space="preserve">
<value>正在发现文件...</value>
</data>
<data name="ScanParsing" xml:space="preserve">
<value>发现 {0} 个文件,正在解析元数据...</value>
</data>
<data name="ScanComplete" xml:space="preserve">
<value>扫描完成:新增 {0},跳过 {1}</value>
</data>
<data name="ScanError" xml:space="preserve">
<value>扫描出错:{0}</value>
</data>
<data name="NoFileSelected" xml:space="preserve">
<value>未选择文件</value>
</data>
<data name="ChannelsMono" xml:space="preserve">
<value>单声道</value>
</data>
<data name="ChannelsStereo" xml:space="preserve">
<value>立体声</value>
</data>
<data name="SortByField" xml:space="preserve">
<value>排序字段</value>
</data>
</root>
+168
View File
@@ -0,0 +1,168 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using System.Text.Json;
using System.Xml;
using CommunityToolkit.Mvvm.ComponentModel;
namespace GUI.Services;
public sealed class LocalizationService : ObservableObject
{
public static LocalizationService Instance { get; } = new();
private readonly Dictionary<string, Dictionary<string, string>> _strings = new();
private CultureInfo _currentCulture = CultureInfo.GetCultureInfo("zh-CN");
private string _currentCultureKey = "zh-CN";
public CultureInfo CurrentCulture
{
get => _currentCulture;
set
{
var key = value.Name;
if (key != "zh-CN" && key != "en-US")
key = "zh-CN";
if (!SetProperty(ref _currentCulture, CultureInfo.GetCultureInfo(key), nameof(CurrentCulture)))
return;
_currentCultureKey = key;
CultureInfo.CurrentCulture = _currentCulture;
CultureInfo.CurrentUICulture = _currentCulture;
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
}
}
public IReadOnlyList<CultureInfo> SupportedCultures { get; }
private LocalizationService()
{
LoadResources("zh-CN", "GUI.LocaleZh.xml");
LoadResources("en-US", "GUI.LocaleEn.xml");
SupportedCultures = new ReadOnlyCollection<CultureInfo>(
[
CultureInfo.GetCultureInfo("zh-CN"),
CultureInfo.GetCultureInfo("en-US"),
]);
LoadCulturePreference();
}
private void LoadResources(string cultureKey, string resourceName)
{
var dict = new Dictionary<string, string>();
var assembly = Assembly.GetExecutingAssembly();
using var stream = FindResource(assembly, resourceName);
if (stream == null) return;
using var reader = XmlReader.Create(stream);
while (reader.Read())
{
if (reader.NodeType != XmlNodeType.Element || reader.Name != "data") continue;
var name = reader.GetAttribute("name");
reader.ReadToDescendant("value");
var value = reader.ReadElementContentAsString();
if (name != null)
dict[name] = value;
}
_strings[cultureKey] = dict;
}
private static Stream? FindResource(Assembly assembly, string resourceName)
{
var stream = assembly.GetManifestResourceStream(resourceName);
if (stream != null) return stream;
foreach (var name in assembly.GetManifestResourceNames())
{
if (name.EndsWith(resourceName, StringComparison.OrdinalIgnoreCase))
return assembly.GetManifestResourceStream(name);
}
return null;
}
public string this[string key]
{
get
{
if (_strings.TryGetValue(_currentCultureKey, out var dict) && dict.TryGetValue(key, out var val))
return val;
if (_currentCultureKey != "zh-CN" && _strings.TryGetValue("zh-CN", out var fallback) && fallback.TryGetValue(key, out var fb))
return fb;
return $"<{key}>";
}
}
public string GetFormatted(string key, params object[] args)
{
try
{
return string.Format(_currentCulture, this[key], args);
}
catch
{
return this[key];
}
}
private void LoadCulturePreference()
{
var path = GetPreferencesPath();
if (!File.Exists(path)) return;
try
{
var json = File.ReadAllText(path);
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("Language", out var lang))
{
var langStr = lang.GetString();
if (langStr is "zh-CN" or "en-US")
{
CurrentCulture = CultureInfo.GetCultureInfo(langStr);
}
}
}
catch
{
// ignore malformed preference file
}
}
public void SaveCulturePreference()
{
var path = GetPreferencesPath();
var dir = Path.GetDirectoryName(path);
if (dir != null)
Directory.CreateDirectory(dir);
var opts = new JsonSerializerOptions { WriteIndented = true };
var json = JsonSerializer.Serialize(new Dictionary<string, string>
{
["Language"] = _currentCultureKey,
}, opts);
File.WriteAllText(path, json);
}
private static string GetPreferencesPath()
{
if (OperatingSystem.IsMacOS())
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Library", "Preferences", "com.oces.Resonance.json");
if (OperatingSystem.IsWindows())
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"OCES", "Resonance", "preferences.json");
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"OCES", "Resonance", "preferences.json");
}
}
+3 -2
View File
@@ -11,7 +11,7 @@ public partial class PlayerBarViewModel : ViewModelBase
[ObservableProperty] private AudioFileMeta? _currentFile; [ObservableProperty] private AudioFileMeta? _currentFile;
[ObservableProperty] private bool _hasFile; [ObservableProperty] private bool _hasFile;
[ObservableProperty] private string _currentFileName = "未选择文件"; [ObservableProperty] private string _currentFileName = string.Empty;
[ObservableProperty] private bool _isPlaying; [ObservableProperty] private bool _isPlaying;
[ObservableProperty] private double _currentPosition; [ObservableProperty] private double _currentPosition;
@@ -21,6 +21,7 @@ public partial class PlayerBarViewModel : ViewModelBase
public PlayerBarViewModel(SyncChannel channel) public PlayerBarViewModel(SyncChannel channel)
{ {
_channel = channel; _channel = channel;
CurrentFileName = Loc["NoFileSelected"];
_channel.PropertyChanged += (_, e) => _channel.PropertyChanged += (_, e) =>
{ {
@@ -39,7 +40,7 @@ public partial class PlayerBarViewModel : ViewModelBase
if (file == null) if (file == null)
{ {
CurrentFileName = "未选择文件"; CurrentFileName = Loc["NoFileSelected"];
Duration = 0; Duration = 0;
HasFile = false; HasFile = false;
return; return;
+12 -5
View File
@@ -15,6 +15,9 @@ public partial class ScanProgressViewModel : ViewModelBase
[ObservableProperty] private double _progress; [ObservableProperty] private double _progress;
[ObservableProperty] private string _currentFile = string.Empty; [ObservableProperty] private string _currentFile = string.Empty;
public string AddedText => Loc.GetFormatted("AddedFormat", AddedFiles);
public string SkippedText => Loc.GetFormatted("SkippedFormat", SkippedFiles);
public event Action? ScanCompleted; public event Action? ScanCompleted;
[RelayCommand] [RelayCommand]
@@ -22,6 +25,7 @@ public partial class ScanProgressViewModel : ViewModelBase
{ {
if (string.IsNullOrWhiteSpace(directory)) return; if (string.IsNullOrWhiteSpace(directory)) return;
StatusText = Loc["ScanPreparing"];
Task.Run(() => ScanDirectoryInternal(directory)); Task.Run(() => ScanDirectoryInternal(directory));
} }
@@ -32,7 +36,7 @@ public partial class ScanProgressViewModel : ViewModelBase
{ {
IsScanning = false; IsScanning = false;
HasError = false; HasError = false;
StatusText = "准备扫描..."; StatusText = Loc["ScanPreparing"];
} }
private void ScanDirectoryInternal(string directory) private void ScanDirectoryInternal(string directory)
@@ -41,7 +45,7 @@ public partial class ScanProgressViewModel : ViewModelBase
{ {
HasError = false; HasError = false;
IsScanning = true; IsScanning = true;
StatusText = "正在发现文件..."; StatusText = Loc["ScanDiscovering"];
Database.InitializeDatabase(); Database.InitializeDatabase();
@@ -56,7 +60,7 @@ public partial class ScanProgressViewModel : ViewModelBase
SkippedFiles = 0; SkippedFiles = 0;
Progress = 0; Progress = 0;
StatusText = $"发现 {TotalFiles} 个文件,正在解析元数据..."; StatusText = Loc.GetFormatted("ScanParsing", TotalFiles);
foreach (string filePath in files) foreach (string filePath in files)
{ {
@@ -100,12 +104,12 @@ public partial class ScanProgressViewModel : ViewModelBase
UpdateProgress(); UpdateProgress();
} }
StatusText = $"扫描完成:新增 {AddedFiles},跳过 {SkippedFiles}"; StatusText = Loc.GetFormatted("ScanComplete", AddedFiles, SkippedFiles);
} }
catch (Exception ex) catch (Exception ex)
{ {
HasError = true; HasError = true;
StatusText = $"扫描出错:{ex.Message}"; StatusText = Loc.GetFormatted("ScanError", ex.Message);
} }
finally finally
{ {
@@ -119,4 +123,7 @@ public partial class ScanProgressViewModel : ViewModelBase
{ {
Progress = TotalFiles > 0 ? (double)ProcessedFiles / TotalFiles * 100 : 0; Progress = TotalFiles > 0 ? (double)ProcessedFiles / TotalFiles * 100 : 0;
} }
partial void OnAddedFilesChanged(int value) => OnPropertyChanged(nameof(AddedText));
partial void OnSkippedFilesChanged(int value) => OnPropertyChanged(nameof(SkippedText));
} }
+2
View File
@@ -1,7 +1,9 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using GUI.Services;
namespace GUI.ViewModels; namespace GUI.ViewModels;
public abstract class ViewModelBase : ObservableObject public abstract class ViewModelBase : ObservableObject
{ {
public LocalizationService Loc => LocalizationService.Instance;
} }
+6 -5
View File
@@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:converters="using:GUI.Converters" xmlns:converters="using:GUI.Converters"
xmlns:l10n="using:GUI.Converters"
mc:Ignorable="d" mc:Ignorable="d"
x:Class="GUI.Views.FileListView" x:Class="GUI.Views.FileListView"
x:DataType="vm:FileListViewModel"> x:DataType="vm:FileListViewModel">
@@ -25,22 +26,22 @@
BorderThickness="0,0,0,1"> BorderThickness="0,0,0,1">
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<Grid ColumnDefinitions="*,Auto" Margin="0,0,0,4"> <Grid ColumnDefinitions="*,Auto" Margin="0,0,0,4">
<TextBox Grid.Column="0" PlaceholderText="搜索文件..." Text="{Binding SearchText}"/> <TextBox Grid.Column="0" PlaceholderText="{l10n:Localize SearchFilesHint}" Text="{Binding SearchText}"/>
<Button Grid.Column="1" Content="⟳" Command="{Binding RefreshFilesCommand}" <Button Grid.Column="1" Content="⟳" Command="{Binding RefreshFilesCommand}"
Width="28" Height="28" ToolTip.Tip="刷新" Margin="4,0,0,0"/> Width="28" Height="28" ToolTip.Tip="{l10n:Localize Refresh}" Margin="4,0,0,0"/>
</Grid> </Grid>
<StackPanel Orientation="Horizontal" Spacing="4"> <StackPanel Orientation="Horizontal" Spacing="4">
<ComboBox ItemsSource="{Binding SortOptions}" SelectedItem="{Binding SelectedSortBy}" <ComboBox ItemsSource="{Binding SortOptions}" SelectedItem="{Binding SelectedSortBy}"
Width="140" ToolTip.Tip="排序字段"/> Width="140" ToolTip.Tip="{l10n:Localize SortByField}"/>
<Button Content="重置筛选" Command="{Binding ResetFiltersCommand}" <Button Content="{l10n:Localize ResetFilters}" Command="{Binding ResetFiltersCommand}"
Height="28" Margin="8,0,0,0"/> Height="28" Margin="8,0,0,0"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</Border> </Border>
<Grid Grid.Row="1"> <Grid Grid.Row="1">
<TextBlock Text="暂无文件,请先扫描目录添加音频文件。" <TextBlock Text="{l10n:Localize NoFilesHint}"
HorizontalAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource SystemBaseMediumColorBrush}" Foreground="{DynamicResource SystemBaseMediumColorBrush}"
FontSize="14" FontSize="14"
+6 -5
View File
@@ -4,6 +4,7 @@
xmlns:views="using:GUI.Views" xmlns:views="using:GUI.Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l10n="using:GUI.Converters"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
x:Class="GUI.Views.MainWindow" x:Class="GUI.Views.MainWindow"
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
@@ -19,10 +20,10 @@
<NativeMenu.Menu> <NativeMenu.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="Database" IsVisible="True"> <NativeMenuItem Header="{l10n:Localize Database}" IsVisible="True">
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="Delete Unused Artworks" Command="{Binding CleanOrphanedArtworkCommand}"></NativeMenuItem> <NativeMenuItem Header="{l10n:Localize MenuDeleteOrphanArtwork}" Command="{Binding CleanOrphanedArtworkCommand}"></NativeMenuItem>
<NativeMenuItem Header="Open database file in Finder..."/> <NativeMenuItem Header="{l10n:Localize MenuOpenDatabaseFolder}"/>
</NativeMenu> </NativeMenu>
</NativeMenuItem> </NativeMenuItem>
</NativeMenu> </NativeMenu>
@@ -34,8 +35,8 @@
<Border Grid.Row="0" Padding="6,4" BorderBrush="{DynamicResource SystemBaseLowColorBrush}" <Border Grid.Row="0" Padding="6,4" BorderBrush="{DynamicResource SystemBaseLowColorBrush}"
BorderThickness="0,0,0,1" Background="{DynamicResource SystemAltMediumColorBrush}"> BorderThickness="0,0,0,1" Background="{DynamicResource SystemAltMediumColorBrush}">
<StackPanel Orientation="Horizontal" Spacing="4"> <StackPanel Orientation="Horizontal" Spacing="4">
<Button Content="扫描目录" x:Name="ScanButton" Height="28" Padding="8,0"/> <Button Content="{l10n:Localize ScanDirectory}" x:Name="ScanButton" Height="28" Padding="8,0"/>
<Button Content="偏好设置" Height="28" Padding="8,0" IsEnabled="False"/> <Button Content="{l10n:Localize Preferences}" Height="28" Padding="8,0" IsEnabled="False"/>
</StackPanel> </StackPanel>
</Border> </Border>
+2 -1
View File
@@ -1,6 +1,7 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using GUI.Services;
using GUI.ViewModels; using GUI.ViewModels;
namespace GUI.Views; namespace GUI.Views;
@@ -19,7 +20,7 @@ public partial class MainWindow : Window
IReadOnlyList<IStorageFolder> folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions IReadOnlyList<IStorageFolder> folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{ {
Title = "选择音频目录", Title = LocalizationService.Instance["SelectAudioDirectory"],
AllowMultiple = false, AllowMultiple = false,
}); });
+35 -20
View File
@@ -3,7 +3,7 @@
xmlns:vm="using:GUI.ViewModels" xmlns:vm="using:GUI.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:converters="using:GUI.Converters" xmlns:l10n="using:GUI.Converters"
mc:Ignorable="d" mc:Ignorable="d"
x:Class="GUI.Views.MetadataPanelView" x:Class="GUI.Views.MetadataPanelView"
x:DataType="vm:MetadataPanelViewModel"> x:DataType="vm:MetadataPanelViewModel">
@@ -14,8 +14,8 @@
<ScrollViewer> <ScrollViewer>
<Grid RowDefinitions="Auto,*" Margin="8"> <Grid RowDefinitions="Auto,*" Margin="8">
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="4" Margin="0,0,0,8"> <StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="4" Margin="0,0,0,8">
<TextBlock Text="元数据" FontWeight="Bold" FontSize="14" VerticalAlignment="Center"/> <TextBlock Text="{l10n:Localize Metadata}" FontWeight="Bold" FontSize="14" VerticalAlignment="Center"/>
<Button Content="保存" Command="{Binding SaveCommand}" <Button Content="{l10n:Localize Save}" Command="{Binding SaveCommand}"
IsEnabled="{Binding IsDirty}" IsEnabled="{Binding IsDirty}"
Height="28" Width="60" HorizontalAlignment="Right"/> Height="28" Width="60" HorizontalAlignment="Right"/>
</StackPanel> </StackPanel>
@@ -27,14 +27,29 @@
<Border Padding="6" BorderBrush="{DynamicResource SystemBaseLowColorBrush}" BorderThickness="1" <Border Padding="6" BorderBrush="{DynamicResource SystemBaseLowColorBrush}" BorderThickness="1"
CornerRadius="4"> CornerRadius="4">
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Text="基础信息" FontWeight="SemiBold" FontSize="12" <TextBlock Text="{l10n:Localize BasicInfo}" FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource SystemBaseMediumColorBrush}"/> Foreground="{DynamicResource SystemBaseMediumColorBrush}"/>
<TextBlock Text="{Binding CurrentFile.Filename}" FontSize="13" FontWeight="Bold"/> <TextBlock Text="{Binding CurrentFile.Filename}" FontSize="13" FontWeight="Bold"/>
<TextBlock Text="{Binding CurrentFile.Duration, StringFormat='时长: {0:F2}s'}"/> <StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="{Binding CurrentFile.SampleRate, StringFormat='采样率: {0} Hz'}"/> <TextBlock Text="{l10n:Localize DurationLabel}"/>
<TextBlock Text="{Binding CurrentFile.BitDepth, StringFormat='位深: {0} bit'}"/> <TextBlock Text="{Binding CurrentFile.Duration, StringFormat='{}{0:F2}s'}"/>
<TextBlock Text="{Binding CurrentFile.Channels, StringFormat='声道: {0}'}"/> </StackPanel>
<TextBlock Text="{Binding CurrentFile.Type, StringFormat='格式: {0}'}"/> <StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="{l10n:Localize SampleRateLabel}"/>
<TextBlock Text="{Binding CurrentFile.SampleRate, StringFormat='{}{0} Hz'}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="{l10n:Localize BitDepthLabel}"/>
<TextBlock Text="{Binding CurrentFile.BitDepth, StringFormat='{}{0} bit'}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="{l10n:Localize ChannelsLabel}"/>
<TextBlock Text="{Binding CurrentFile.Channels}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="2">
<TextBlock Text="{l10n:Localize TypeLabel}"/>
<TextBlock Text="{Binding CurrentFile.Type}"/>
</StackPanel>
</StackPanel> </StackPanel>
</Border> </Border>
@@ -42,7 +57,7 @@
<Border Padding="6" BorderBrush="{DynamicResource SystemBaseLowColorBrush}" BorderThickness="1" <Border Padding="6" BorderBrush="{DynamicResource SystemBaseLowColorBrush}" BorderThickness="1"
CornerRadius="4"> CornerRadius="4">
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Text="评分" FontWeight="SemiBold" FontSize="12" <TextBlock Text="{l10n:Localize Rating}" FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource SystemBaseMediumColorBrush}"/> Foreground="{DynamicResource SystemBaseMediumColorBrush}"/>
<StackPanel Orientation="Horizontal" Spacing="2"> <StackPanel Orientation="Horizontal" Spacing="2">
<Button Content="☆" Command="{Binding SetRatingCommand}" CommandParameter="0" <Button Content="☆" Command="{Binding SetRatingCommand}" CommandParameter="0"
@@ -65,29 +80,29 @@
<Border Padding="6" BorderBrush="{DynamicResource SystemBaseLowColorBrush}" BorderThickness="1" <Border Padding="6" BorderBrush="{DynamicResource SystemBaseLowColorBrush}" BorderThickness="1"
CornerRadius="4"> CornerRadius="4">
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<TextBlock Text="标签与描述" FontWeight="SemiBold" FontSize="12" <TextBlock Text="{l10n:Localize TagsAndDescription}" FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource SystemBaseMediumColorBrush}"/> Foreground="{DynamicResource SystemBaseMediumColorBrush}"/>
<TextBlock Text="分类"/> <TextBlock Text="{l10n:Localize Category}"/>
<TextBox Text="{Binding CategoryText}" PlaceholderText="输入分类..."/> <TextBox Text="{Binding CategoryText}" PlaceholderText="{l10n:Localize CategoryPlaceholder}"/>
<TextBlock Text="关键词(逗号分隔)"/> <TextBlock Text="{l10n:Localize KeywordsCommaSeparated}"/>
<TextBox Text="{Binding KeywordsText}" PlaceholderText="输入关键词..." MinHeight="40" <TextBox Text="{Binding KeywordsText}" PlaceholderText="{l10n:Localize KeywordsPlaceholder}" MinHeight="40"
AcceptsReturn="True"/> AcceptsReturn="True"/>
<TextBlock Text="描述"/> <TextBlock Text="{l10n:Localize Description}"/>
<TextBox Text="{Binding DescriptionText}" PlaceholderText="输入描述..." MinHeight="40" <TextBox Text="{Binding DescriptionText}" PlaceholderText="{l10n:Localize DescriptionPlaceholder}" MinHeight="40"
AcceptsReturn="True"/> AcceptsReturn="True"/>
<TextBlock Text="备注"/> <TextBlock Text="{l10n:Localize Notes}"/>
<TextBox Text="{Binding NotesText}" PlaceholderText="输入备注..." MinHeight="40" <TextBox Text="{Binding NotesText}" PlaceholderText="{l10n:Localize NotesPlaceholder}" MinHeight="40"
AcceptsReturn="True"/> AcceptsReturn="True"/>
</StackPanel> </StackPanel>
</Border> </Border>
</StackPanel> </StackPanel>
<!-- 未选择文件 --> <!-- 未选择文件 -->
<TextBlock Grid.Row="1" Text="选择文件查看元数据" <TextBlock Grid.Row="1" Text="{l10n:Localize SelectFileToViewMetadata}"
HorizontalAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource SystemBaseMediumColorBrush}" Foreground="{DynamicResource SystemBaseMediumColorBrush}"
IsVisible="{Binding !HasFile}"/> IsVisible="{Binding !HasFile}"/>
+15 -10
View File
@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:GUI.ViewModels" xmlns:vm="using:GUI.ViewModels"
xmlns:converters="using:GUI.Converters" xmlns:converters="using:GUI.Converters"
xmlns:l10n="using:GUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" mc:Ignorable="d"
@@ -21,21 +22,25 @@
<StackPanel Spacing="8"> <StackPanel Spacing="8">
<TextBlock Text="{Binding StatusText}" FontWeight="SemiBold" FontSize="13" TextWrapping="Wrap"/> <TextBlock Text="{Binding StatusText}" FontWeight="SemiBold" FontSize="13" TextWrapping="Wrap"/>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding Progress}" Height="8"/> <ProgressBar Minimum="0" Maximum="100" Value="{Binding Progress}" Height="8"/>
<TextBlock FontSize="11" Foreground="{DynamicResource SystemBaseMediumColorBrush}"> <StackPanel Orientation="Horizontal" Spacing="2">
<Run Text="已处理: "/> <TextBlock Text="{l10n:Localize Processed}" FontSize="11"
<Run Text="{Binding ProcessedFiles}"/>
<Run Text=" / "/>
<Run Text="{Binding TotalFiles}"/>
</TextBlock>
<StackPanel Orientation="Horizontal" Spacing="12">
<TextBlock Text="{Binding AddedFiles, StringFormat='新增: {0}'}" FontSize="11"
Foreground="{DynamicResource SystemBaseMediumColorBrush}"/> Foreground="{DynamicResource SystemBaseMediumColorBrush}"/>
<TextBlock Text="{Binding SkippedFiles, StringFormat='跳过: {0}'}" FontSize="11" <TextBlock Text="{Binding ProcessedFiles}" FontSize="11"
Foreground="{DynamicResource SystemBaseMediumColorBrush}"/>
<TextBlock Text=" / " FontSize="11"
Foreground="{DynamicResource SystemBaseMediumColorBrush}"/>
<TextBlock Text="{Binding TotalFiles}" FontSize="11"
Foreground="{DynamicResource SystemBaseMediumColorBrush}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="12">
<TextBlock Text="{Binding AddedText}" FontSize="11"
Foreground="{DynamicResource SystemBaseMediumColorBrush}"/>
<TextBlock Text="{Binding SkippedText}" FontSize="11"
Foreground="{DynamicResource SystemBaseMediumColorBrush}"/> Foreground="{DynamicResource SystemBaseMediumColorBrush}"/>
</StackPanel> </StackPanel>
<TextBlock Text="{Binding CurrentFile, Converter={StaticResource MiddleTruncate}, ConverterParameter=60}" <TextBlock Text="{Binding CurrentFile, Converter={StaticResource MiddleTruncate}, ConverterParameter=60}"
FontSize="11"/> FontSize="11"/>
<Button Content="关闭" Command="{Binding DismissCommand}" <Button Content="{l10n:Localize Close}" Command="{Binding DismissCommand}"
IsVisible="{Binding HasError}" HorizontalAlignment="Right"/> IsVisible="{Binding HasError}" HorizontalAlignment="Right"/>
</StackPanel> </StackPanel>
</Border> </Border>
+6 -5
View File
@@ -3,6 +3,7 @@
xmlns:vm="using:GUI.ViewModels" xmlns:vm="using:GUI.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l10n="using:GUI.Converters"
mc:Ignorable="d" mc:Ignorable="d"
x:Class="GUI.Views.TagTreeView" x:Class="GUI.Views.TagTreeView"
x:DataType="vm:TagTreeViewModel"> x:DataType="vm:TagTreeViewModel">
@@ -12,18 +13,18 @@
<Grid RowDefinitions="Auto,*"> <Grid RowDefinitions="Auto,*">
<StackPanel Orientation="Horizontal" Margin="8,8,8,4" Spacing="4"> <StackPanel Orientation="Horizontal" Margin="8,8,8,4" Spacing="4">
<TextBlock Text="浏览" FontWeight="Bold" FontSize="14" VerticalAlignment="Center"/> <TextBlock Text="{l10n:Localize Browse}" FontWeight="Bold" FontSize="14" VerticalAlignment="Center"/>
<Button Content="⟳" Command="{Binding LoadDataCommand}" ToolTip.Tip="刷新" <Button Content="⟳" Command="{Binding LoadDataCommand}" ToolTip.Tip="{l10n:Localize Refresh}"
Width="28" Height="28" HorizontalAlignment="Right"/> Width="28" Height="28" HorizontalAlignment="Right"/>
</StackPanel> </StackPanel>
<ScrollViewer Grid.Row="1"> <ScrollViewer Grid.Row="1">
<StackPanel Spacing="2" Margin="8,0,8,8"> <StackPanel Spacing="2" Margin="8,0,8,8">
<Button Content="全部文件" Command="{Binding ClearFilterCommand}" <Button Content="{l10n:Localize AllFiles}" Command="{Binding ClearFilterCommand}"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Height="28" Margin="0,0,0,4"/> Height="28" Margin="0,0,0,4"/>
<TextBlock Text="分类" FontWeight="SemiBold" Margin="0,8,0,2" FontSize="12" <TextBlock Text="{l10n:Localize Category}" FontWeight="SemiBold" Margin="0,8,0,2" FontSize="12"
Foreground="{DynamicResource SystemBaseMediumColorBrush}"/> Foreground="{DynamicResource SystemBaseMediumColorBrush}"/>
<ListBox ItemsSource="{Binding CategoryNodes}" <ListBox ItemsSource="{Binding CategoryNodes}"
@@ -36,7 +37,7 @@
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
<TextBlock Text="标签" FontWeight="SemiBold" Margin="0,8,0,2" FontSize="12" <TextBlock Text="{l10n:Localize Tags}" FontWeight="SemiBold" Margin="0,8,0,2" FontSize="12"
Foreground="{DynamicResource SystemBaseMediumColorBrush}"/> Foreground="{DynamicResource SystemBaseMediumColorBrush}"/>
<ListBox ItemsSource="{Binding KeywordNodes}" <ListBox ItemsSource="{Binding KeywordNodes}"
+4 -2
View File
@@ -34,9 +34,11 @@
&lt;TestId&gt;xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::Core.Tests.DatabaseTests&lt;/TestId&gt; &lt;TestId&gt;xUnit::24F92458-FB39-44BE-A32F-41275183AF1B::net10.0::Core.Tests.DatabaseTests&lt;/TestId&gt;
&lt;/TestAncestor&gt; &lt;/TestAncestor&gt;
&lt;/SessionState&gt;</s:String> &lt;/SessionState&gt;</s:String>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=GUI_002FAssets_002FLocale/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=GUI_002FAssets_002FLocale/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=GUI_002FAssets_002FLocale/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=GUI_002FLocale/@EntryIndexedValue">False</s:Boolean> <s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=GUI_002FLocale/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=GUI_002FLocale/@EntryIndexRemoved">True</s:Boolean> <s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=GUI_002FLocale/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=GUI_002FResources1/@EntryIndexedValue">False</s:Boolean> <s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=GUI_002FResources1/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=GUI_002FResources1/@EntryIndexRemoved">True</s:Boolean> <s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=GUI_002FResources1/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/Initialized/@EntryValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/ResxEditorPersonal/Initialized/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/ShowComments/@EntryValue">False</s:Boolean></wpf:ResourceDictionary>