fix: 扫描时界面会因为文件名不同而抖动的问题

This commit is contained in:
2026-06-08 13:40:27 +08:00
parent d72193a693
commit 40b8524ffd
5 changed files with 59 additions and 16 deletions
+6 -10
View File
@@ -1,5 +1,10 @@
# AGENTS.md # AGENTS.md
## AI 行为准则
- **称呼规范**:始终称呼用户为「思谦」或「王思谦」。
- **信息缺失处理**:遇到任何不确定、拿不准或信息缺失的情况,不要自行推测。先收集整理好问题,停下来向思谦确认后再继续。
## Build & Test ## Build & Test
```bash ```bash
@@ -26,23 +31,14 @@ dotnet run --project src/GUI # Launch the Avalonia de
## Known Issues / Gotchas ## Known Issues / Gotchas
- 扫描时如果报错,报错信息可能会填满整个窗口,导致Overlay无法关闭。 - 扫描时如果报错,报错信息可能会填满整个窗口,导致Overlay无法关闭。
- 读取时如果报错,没有任何警告,会静默报错。需要有一个界面右下方的toast,或是发送系统通知告知用户遇到了错误。 - 读取时如果报错,没有任何警告,会静默报错。需要有一个界面右下方的toast,或是发送系统通知告知用户遇到了错误。
- 扫描界面的宽度会因为文件名而发生变化。固定为窗口宽度的50%。如果文件名超过这个宽度则省略中间的字符,仅保留开头以及最后10个字符。
- **升级 Avalonia 版本时,必须同步更新所有次级包引用。** 仅修改主包 `Avalonia` 的 Version 不会自动更新 `Avalonia.Desktop``Avalonia.Themes.Fluent``Avalonia.Fonts.Inter` 等次级包。必须手动逐个修改,然后执行 `dotnet clean && dotnet restore`。否则运行时的原生库(如 `libAvaloniaNative.dylib`)会混用新旧版本,导致 macOS 上的 `StorageProvider` 文件/文件夹选择对话框在回调时 SIGSEGV 崩溃。 - **升级 Avalonia 版本时,必须同步更新所有次级包引用。** 仅修改主包 `Avalonia` 的 Version 不会自动更新 `Avalonia.Desktop``Avalonia.Themes.Fluent``Avalonia.Fonts.Inter` 等次级包。必须手动逐个修改,然后执行 `dotnet clean && dotnet restore`。否则运行时的原生库(如 `libAvaloniaNative.dylib`)会混用新旧版本,导致 macOS 上的 `StorageProvider` 文件/文件夹选择对话框在回调时 SIGSEGV 崩溃。
- 相关 Avalonia issues: [#21102](https://github.com/AvaloniaUI/Avalonia/issues/21102), [#21150](https://github.com/AvaloniaUI/Avalonia/issues/21150), [#21313](https://github.com/AvaloniaUI/Avalonia/issues/21313),修复 PR: [#21104](https://github.com/AvaloniaUI/Avalonia/pull/21104)。 - 相关 Avalonia issues: [#21102](https://github.com/AvaloniaUI/Avalonia/issues/21102), [#21150](https://github.com/AvaloniaUI/Avalonia/issues/21150), [#21313](https://github.com/AvaloniaUI/Avalonia/issues/21313),修复 PR: [#21104](https://github.com/AvaloniaUI/Avalonia/pull/21104)。
- **Tests create temp files/directories** under `Path.GetTempPath()` and clean them up via `IDisposable`. Don't rely on a real audio directory for tests. - **Tests create temp files/directories** under `Path.GetTempPath()` and clean them up via `IDisposable`. Don't rely on a real audio directory for tests.
- **`AudioMetadataReader` tests** use real WAV files from `data/source/` (not in git). When files are absent, tests silently return early (`if (wavFiles.Length == 0) return;`) — they pass without actually running assertions, not `Skip.If`. Running `dotnet test` will report 12 passes regardless of whether the fixture files exist. - **`AudioMetadataReader` tests** use real WAV files from `data/source/` (not in git). When files are absent, tests silently return early (`if (wavFiles.Length == 0) return;`) — they pass without actually running assertions, not `Skip.If`. Running `dotnet test` will report 12 passes regardless of whether the fixture files exist.
- **`.atl.txt` fixture files** are generated by `AtlFieldExtractor`. If the WAV files or the ATL library version change, re-run the extractor to regenerate them before running metadata tests. - **`.atl.txt` fixture files** are generated by `AtlFieldExtractor`. If the WAV files or the ATL library version change, re-run the extractor to regenerate them before running metadata tests.
- 需要一个UI设计。现在的界面属实有点丑陋了。
## Data / Test Fixtures
- **`data/source/`** 包含有版权保护的 WAV 音频文件,**不会提交到 git**。按厂商分子目录:`Boom/``Wow Sound/``The Odessy/``Sound Idea/`
- 每个 WAV 旁有对应的 `.atl.txt` 文件(由 `AtlFieldExtractor` 生成),记录了 ATL 库解析出的完整元数据,供测试断言参考。
- Boom 目录下的文件元数据最丰富(标准标签 + iXML 自定义字段 + BWF bext 字段 + 嵌入封面);Sound Idea 目录下的文件元数据最少(仅基础技术参数,无任何附加字段)。
- **`AudioMetadataReader` 测试**使用 `data/source/Boom/``Sound Idea/` 中的 WAV 文件。文件不在 git 仓库中,测试通过 `if (wavFiles.Length == 0) return;` 静默返回(测试仍然 pass,但未执行实际断言)。
- **路径解析**:测试运行时从 `bin/Debug/net10.0/` 启动,需向上回溯 6 级到达仓库根目录,再拼接 `data/source/`
## Conventions ## Conventions
+39
View File
@@ -100,6 +100,43 @@ public class InverseBoolConverter : IValueConverter
} }
} }
public class HalfDoubleConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is double d && !double.IsNaN(d) && !double.IsInfinity(d))
return d / 2.0;
return 400.0;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public class MiddleTruncateConverter : IValueConverter
{
private const int KeepTail = 10;
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not string text || string.IsNullOrEmpty(text)) return value;
int maxLength = 60;
if (parameter != null && int.TryParse(parameter.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
maxLength = parsed;
if (text.Length <= maxLength) return text;
int head = maxLength - KeepTail - 3;
if (head <= 0) return text[..maxLength];
return string.Concat(text.AsSpan(0, head), "...", text.AsSpan(text.Length - KeepTail, KeepTail));
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public static class ValueConverters public static class ValueConverters
{ {
public static readonly IValueConverter DurationFormat = new DurationFormatConverter(); public static readonly IValueConverter DurationFormat = new DurationFormatConverter();
@@ -109,4 +146,6 @@ public static class ValueConverters
public static readonly IValueConverter DurationToSliderMax = new DurationToSliderMaxConverter(); public static readonly IValueConverter DurationToSliderMax = new DurationToSliderMaxConverter();
public static readonly IValueConverter PositionFormat = new PositionFormatConverter(); public static readonly IValueConverter PositionFormat = new PositionFormatConverter();
public static readonly IValueConverter InverseBool = new InverseBoolConverter(); public static readonly IValueConverter InverseBool = new InverseBoolConverter();
public static readonly IValueConverter HalfDouble = new HalfDoubleConverter();
public static readonly IValueConverter MiddleTruncate = new MiddleTruncateConverter();
} }
+3 -3
View File
@@ -16,15 +16,15 @@
<Design.DataContext> <Design.DataContext>
<vm:MainWindowViewModel/> <vm:MainWindowViewModel/>
</Design.DataContext> </Design.DataContext>
<Grid RowDefinitions="Auto,*,Auto"> <Grid RowDefinitions="Auto,*,Auto">
<!-- 工具栏 --> <!-- 工具栏 -->
<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="扫描目录" x:Name="ScanButton" Height="28" Padding="8,0"/>
<Button Content="偏好设置" Height="28" Padding="8,0" IsEnabled="False"/> <Button Content="偏好设置" Height="28" Padding="8,0" IsEnabled="False"/>
</StackPanel> </StackPanel>
</Border> </Border>
+9 -3
View File
@@ -1,6 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
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: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"
@@ -10,8 +11,13 @@
<vm:ScanProgressViewModel/> <vm:ScanProgressViewModel/>
</Design.DataContext> </Design.DataContext>
<UserControl.Resources>
<converters:HalfDoubleConverter x:Key="HalfDouble"/>
<converters:MiddleTruncateConverter x:Key="MiddleTruncate"/>
</UserControl.Resources>
<Border Padding="16" Background="{DynamicResource SystemBaseLowColorBrush}" CornerRadius="8" <Border Padding="16" Background="{DynamicResource SystemBaseLowColorBrush}" CornerRadius="8"
MaxWidth="400"> Width="{Binding $parent[Window].Width, Converter={StaticResource HalfDouble}}">
<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"/>
@@ -27,8 +33,8 @@
<TextBlock Text="{Binding SkippedFiles, StringFormat='跳过: {0}'}" FontSize="11" <TextBlock Text="{Binding SkippedFiles, StringFormat='跳过: {0}'}" FontSize="11"
Foreground="{DynamicResource SystemBaseMediumColorBrush}"/> Foreground="{DynamicResource SystemBaseMediumColorBrush}"/>
</StackPanel> </StackPanel>
<TextBlock Text="{Binding CurrentFile}" FontSize="11" <TextBlock Text="{Binding CurrentFile, Converter={StaticResource MiddleTruncate}, ConverterParameter=60}"
TextTrimming="CharacterEllipsis" MaxWidth="368"/> FontSize="11"/>
<Button Content="关闭" Command="{Binding DismissCommand}" <Button Content="关闭" Command="{Binding DismissCommand}"
IsVisible="{Binding HasError}" HorizontalAlignment="Right"/> IsVisible="{Binding HasError}" HorizontalAlignment="Right"/>
</StackPanel> </StackPanel>
+2
View File
@@ -2,6 +2,8 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFormat_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fe2d95a1d7b987bbb7785b2ad7e2e3e29075ba79cdf7619dd26c10d266c5_003FFormat_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFormat_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fe2d95a1d7b987bbb7785b2ad7e2e3e29075ba79cdf7619dd26c10d266c5_003FFormat_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGUI_002EViewModels_002EScanProgressViewModel_002Eg_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fbb74b3c6eaaeb5d783f31310eae5345eba75596a_003FGUI_002EViewModels_002EScanProgressViewModel_002Eg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGUI_002EViewModels_002EScanProgressViewModel_002Eg_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fbb74b3c6eaaeb5d783f31310eae5345eba75596a_003FGUI_002EViewModels_002EScanProgressViewModel_002Eg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGUI_002EViewModels_002EScanProgressViewModel_002EStartScan_002Eg_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003F1a6f588df342309187674fcc524512a9a4a46540_003FGUI_002EViewModels_002EScanProgressViewModel_002EStartScan_002Eg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGUI_002EViewModels_002EScanProgressViewModel_002EStartScan_002Eg_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003F1a6f588df342309187674fcc524512a9a4a46540_003FGUI_002EViewModels_002EScanProgressViewModel_002EStartScan_002Eg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGUI_002EViewModels_002EScanProgressViewModel_002EStartScan_002Eg_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003Fvar_003Ffolders_003F5b_003F4wt_005Fpwy11fz5nkdv7s47r3780000gn_003FT_003FSourceGeneratedDocuments_003F8E641B210524A46B7DCD4502_003FCommunityToolkit_002EMvvm_002ESourceGenerators_003FCommunityToolkit_002EMvvm_002ESourceGenerators_002ERelayCommandGenerator_003FGUI_002EViewModels_002EScanProgressViewModel_002EStartScan_002Eg_002Ecs_002Fz_003A2_002D1/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGUI_002EViewModels_002EScanProgressViewModel_002EStartScan_002Eg_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003F1a6f588df342309187674fcc524512a9a4a46540_003FGUI_002EViewModels_002EScanProgressViewModel_002EStartScan_002Eg_002Ecs_002Fz_003A2_002D0/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDbConnection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01c4805efedf4381865d701be69b5ce5314910_003F77_003Ffb273e4d_003FIDbConnection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDbConnection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F01c4805efedf4381865d701be69b5ce5314910_003F77_003Ffb273e4d_003FIDbConnection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIRelayCommand_007BT_007D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003F49f2a292ecb417be1f61c426de242e503b4371a346bf5b68a843b462337e62_003FIRelayCommand_007BT_007D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIRelayCommand_007BT_007D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003F49f2a292ecb417be1f61c426de242e503b4371a346bf5b68a843b462337e62_003FIRelayCommand_007BT_007D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOSPlatform_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003F9e988c941e6515999ebab7336243c87df16e340b8faeb57b5f562d2b8a7c2c_003FOSPlatform_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOSPlatform_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003F9e988c941e6515999ebab7336243c87df16e340b8faeb57b5f562d2b8a7c2c_003FOSPlatform_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>