Provide NTFS default characters for non-windows users (#1258)

This commit is contained in:
Michael Bucari-Tovo 2025-07-29 15:20:52 -06:00
parent 663f70b8bf
commit 7024bbf823
10 changed files with 98 additions and 96 deletions

View File

@ -56,7 +56,7 @@ namespace FileManager
{ {
ArgumentValidator.EnsureNotNull(name, nameof(name)); ArgumentValidator.EnsureNotNull(name, nameof(name));
name = ReplacementCharacters.Barebones.ReplaceFilenameChars(name); name = ReplacementCharacters.Barebones(true).ReplaceFilenameChars(name);
return Task.Run(() => AddFileInternal(name, contents.Span, comment)); return Task.Run(() => AddFileInternal(name, contents.Span, comment));
} }

View File

@ -74,12 +74,14 @@ namespace FileManager
} }
public override int GetHashCode() => Replacements.GetHashCode(); public override int GetHashCode() => Replacements.GetHashCode();
public static readonly ReplacementCharacters Default public static ReplacementCharacters Default(bool ntfs) => ntfs ? HiFi_NTFS : HiFi_Other;
= IsWindows public static ReplacementCharacters LoFiDefault(bool ntfs) => ntfs ? LoFi_NTFS : LoFi_Other;
? new() public static ReplacementCharacters Barebones(bool ntfs) => ntfs ? BareBones_NTFS : BareBones_Other;
{
Replacements = new Replacement[] #region Defaults
private static readonly ReplacementCharacters HiFi_NTFS = new()
{ {
Replacements = [
Replacement.OtherInvalid("_"), Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash(""), Replacement.FilenameForwardSlash(""),
Replacement.FilenameBackSlash(""), Replacement.FilenameBackSlash(""),
@ -91,28 +93,23 @@ namespace FileManager
Replacement.Colon("_"), Replacement.Colon("_"),
Replacement.Asterisk("✱"), Replacement.Asterisk("✱"),
Replacement.QuestionMark(""), Replacement.QuestionMark(""),
Replacement.Pipe("⏐"), Replacement.Pipe("⏐")]
} };
}
: new() private static readonly ReplacementCharacters HiFi_Other = new()
{
Replacements = new Replacement[]
{ {
Replacements = [
Replacement.OtherInvalid("_"), Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash(""), Replacement.FilenameForwardSlash(""),
Replacement.FilenameBackSlash("\\"), Replacement.FilenameBackSlash("\\"),
Replacement.OpenQuote("“"), Replacement.OpenQuote("“"),
Replacement.CloseQuote("”"), Replacement.CloseQuote("”"),
Replacement.OtherQuote("\"") Replacement.OtherQuote("\"")]
}
}; };
public static readonly ReplacementCharacters LoFiDefault private static readonly ReplacementCharacters LoFi_NTFS = new()
= IsWindows
? new()
{
Replacements = new Replacement[]
{ {
Replacements = [
Replacement.OtherInvalid("_"), Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"), Replacement.FilenameForwardSlash("_"),
Replacement.FilenameBackSlash("_"), Replacement.FilenameBackSlash("_"),
@ -121,50 +118,44 @@ namespace FileManager
Replacement.OtherQuote("'"), Replacement.OtherQuote("'"),
Replacement.OpenAngleBracket("{"), Replacement.OpenAngleBracket("{"),
Replacement.CloseAngleBracket("}"), Replacement.CloseAngleBracket("}"),
Replacement.Colon("-"), Replacement.Colon("-")]
} };
}
: new () private static readonly ReplacementCharacters LoFi_Other = new()
{
Replacements = new Replacement[]
{ {
Replacements = [
Replacement.OtherInvalid("_"), Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"), Replacement.FilenameForwardSlash("_"),
Replacement.FilenameBackSlash("\\"), Replacement.FilenameBackSlash("\\"),
Replacement.OpenQuote("\""), Replacement.OpenQuote("\""),
Replacement.CloseQuote("\""), Replacement.CloseQuote("\""),
Replacement.OtherQuote("\"") Replacement.OtherQuote("\"")]
}
}; };
public static readonly ReplacementCharacters Barebones private static readonly ReplacementCharacters BareBones_NTFS = new()
= IsWindows
? new ()
{
Replacements = new Replacement[]
{ {
Replacements = [
Replacement.OtherInvalid("_"), Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"), Replacement.FilenameForwardSlash("_"),
Replacement.FilenameBackSlash("_"), Replacement.FilenameBackSlash("_"),
Replacement.OpenQuote("_"), Replacement.OpenQuote("_"),
Replacement.CloseQuote("_"), Replacement.CloseQuote("_"),
Replacement.OtherQuote("_") Replacement.OtherQuote("_")]
} };
}
: new () private static readonly ReplacementCharacters BareBones_Other = new()
{
Replacements = new Replacement[]
{ {
Replacements = [
Replacement.OtherInvalid("_"), Replacement.OtherInvalid("_"),
Replacement.FilenameForwardSlash("_"), Replacement.FilenameForwardSlash("_"),
Replacement.FilenameBackSlash("\\"), Replacement.FilenameBackSlash("\\"),
Replacement.OpenQuote("\""), Replacement.OpenQuote("\""),
Replacement.CloseQuote("\""), Replacement.CloseQuote("\""),
Replacement.OtherQuote("\"") Replacement.OtherQuote("\"")]
}
}; };
#endregion
private static bool IsWindows => Environment.OSVersion.Platform is PlatformID.Win32NT; internal static bool IsWindows => Environment.OSVersion.Platform is PlatformID.Win32NT;
private static readonly char[] invalidPathChars = Path.GetInvalidFileNameChars().Except(new[] { private static readonly char[] invalidPathChars = Path.GetInvalidFileNameChars().Except(new[] {
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar
@ -301,23 +292,21 @@ namespace FileManager
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
var defaults = ReplacementCharacters.Default(ReplacementCharacters.IsWindows).Replacements;
var jObj = JObject.Load(reader); var jObj = JObject.Load(reader);
var replaceArr = jObj[nameof(Replacement)]; var replaceArr = jObj[nameof(Replacement)];
var dict var dict = replaceArr?.ToObject<Replacement[]>()?.ToList() ?? defaults;
= replaceArr?.ToObject<Replacement[]>()?.ToList()
?? ReplacementCharacters.Default.Replacements;
//Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid. //Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid.
//If not, reset to default. //If not, reset to default.
for (int i = 0; i < Replacement.FIXED_COUNT; i++) for (int i = 0; i < Replacement.FIXED_COUNT; i++)
{ {
if (dict.Count < Replacement.FIXED_COUNT if (dict.Count < Replacement.FIXED_COUNT
|| dict[i].CharacterToReplace != ReplacementCharacters.Barebones.Replacements[i].CharacterToReplace || dict[i].CharacterToReplace != defaults[i].CharacterToReplace
|| dict[i].Description != ReplacementCharacters.Barebones.Replacements[i].Description) || dict[i].Description != defaults[i].Description)
{ {
dict = ReplacementCharacters.Default.Replacements; dict = defaults;
break; break;
} }

View File

@ -6,6 +6,8 @@
MinWidth="500" MinHeight="450" MinWidth="500" MinHeight="450"
Width="500" Height="450" Width="500" Height="450"
x:Class="LibationAvalonia.Dialogs.EditReplacementChars" x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
x:DataType="dialogs:EditReplacementChars"
Title="Illegal Character Replacement"> Title="Illegal Character Replacement">
<Grid <Grid
@ -23,31 +25,30 @@
BeginningEdit="ReplacementGrid_BeginningEdit" BeginningEdit="ReplacementGrid_BeginningEdit"
CellEditEnding="ReplacementGrid_CellEditEnding" CellEditEnding="ReplacementGrid_CellEditEnding"
KeyDown="ReplacementGrid_KeyDown" KeyDown="ReplacementGrid_KeyDown"
ItemsSource="{Binding replacements}"> ItemsSource="{CompiledBinding replacements}">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTemplateColumn Header="Char to&#xa;Replace"> <DataGridTemplateColumn Header="Char to&#xa;Replace">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
<TextBox IsReadOnly="{Binding Mandatory}" Text="{Binding CharacterToReplace, Mode=TwoWay}" /> <TextBox IsReadOnly="{CompiledBinding Mandatory}" Text="{CompiledBinding CharacterToReplace, Mode=TwoWay}" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
<DataGridTemplateColumn Header="Replacement&#xa;Text"> <DataGridTemplateColumn Header="Replacement&#xa;Text">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
<TextBox Text="{Binding ReplacementText, Mode=TwoWay}" /> <TextBox Text="{CompiledBinding ReplacementText, Mode=TwoWay}" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
<DataGridTemplateColumn Width="*" Header="Description"> <DataGridTemplateColumn Width="*" Header="Description">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
<TextBox IsReadOnly="{Binding Mandatory}" Text="{Binding Description, Mode=TwoWay}" /> <TextBox IsReadOnly="{CompiledBinding Mandatory}" Text="{CompiledBinding Description, Mode=TwoWay}" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
@ -55,21 +56,31 @@
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
<StackPanel <Grid
Grid.Row="1" Grid.Row="1"
Grid.Column="0" Grid.Column="0"
RowDefinitions="Auto,Auto"
Margin="5" Margin="5"
Orientation="Horizontal"> ColumnDefinitions="Auto,Auto,Auto,Auto">
<Button Margin="0,0,10,0" Command="{Binding Defaults}" Content="Defaults" /> <TextBlock IsVisible="{CompiledBinding !EnvironmentIsWindows}" Text="This System:" Margin="0,0,10,0" VerticalAlignment="Center" />
<Button Margin="0,0,10,0" Command="{Binding LoFiDefaults}" Content="LoFi Defaults" /> <TextBlock IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Text="NTFS:" Margin="0,0,10,0" VerticalAlignment="Center" />
<Button Command="{Binding Barebones}" Content="Barebones" />
</StackPanel> <Button Grid.Column="1" Margin="0,0,10,0" Command="{CompiledBinding Defaults}" CommandParameter="{CompiledBinding EnvironmentIsWindows}" Content="Defaults" />
<Button Grid.Column="2" Margin="0,0,10,0" Command="{CompiledBinding LoFiDefaults}" CommandParameter="{CompiledBinding EnvironmentIsWindows}" Content="LoFi Defaults" />
<Button Grid.Column="3" Command="{CompiledBinding Barebones}" CommandParameter="{CompiledBinding EnvironmentIsWindows}" Content="Barebones" />
<Button IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Grid.Column="1" Margin="0,10,10,0" Command="{CompiledBinding Defaults}" CommandParameter="True" Content="Defaults" />
<Button IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Grid.Column="2" Margin="0,10,10,0" Command="{CompiledBinding LoFiDefaults}" CommandParameter="True" Content="LoFi Defaults" />
<Button IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Grid.Column="3" Margin="0,10,0,0" Command="{CompiledBinding Barebones}" CommandParameter="True" Content="Barebones" />
</Grid>
<StackPanel <StackPanel
Grid.Row="1" Grid.Row="1"
Grid.Column="1" Grid.Column="1"
Margin="5" Margin="5"
VerticalAlignment="Bottom"
Orientation="Horizontal"> Orientation="Horizontal">
<Button Margin="0,0,10,0" Command="{Binding Close}" Content="Cancel" /> <Button Margin="0,0,10,0" Command="{Binding Close}" Content="Cancel" />

View File

@ -13,6 +13,8 @@ namespace LibationAvalonia.Dialogs
{ {
Configuration config; Configuration config;
public bool EnvironmentIsWindows => Configuration.IsWindows;
private readonly List<ReplacementsExt> SOURCE = new(); private readonly List<ReplacementsExt> SOURCE = new();
public DataGridCollectionView replacements { get; } public DataGridCollectionView replacements { get; }
public EditReplacementChars() public EditReplacementChars()
@ -23,7 +25,7 @@ namespace LibationAvalonia.Dialogs
if (Design.IsDesignMode) if (Design.IsDesignMode)
{ {
LoadTable(ReplacementCharacters.Default.Replacements); LoadTable(ReplacementCharacters.Default(true).Replacements);
} }
DataContext = this; DataContext = this;
@ -35,12 +37,12 @@ namespace LibationAvalonia.Dialogs
LoadTable(config.ReplacementCharacters.Replacements); LoadTable(config.ReplacementCharacters.Replacements);
} }
public void Defaults() public void Defaults(bool isNtfs)
=> LoadTable(ReplacementCharacters.Default.Replacements); => LoadTable(ReplacementCharacters.Default(isNtfs).Replacements);
public void LoFiDefaults() public void LoFiDefaults(bool isNtfs)
=> LoadTable(ReplacementCharacters.LoFiDefault.Replacements); => LoadTable(ReplacementCharacters.LoFiDefault(isNtfs).Replacements);
public void Barebones() public void Barebones(bool isNtfs)
=> LoadTable(ReplacementCharacters.Barebones.Replacements); => LoadTable(ReplacementCharacters.Barebones(isNtfs).Replacements);
protected override void SaveAndClose() protected override void SaveAndClose()
{ {

View File

@ -54,7 +54,7 @@ namespace LibationAvalonia.Views
FileUtility.SaferMoveToValidPath( FileUtility.SaferMoveToValidPath(
e.SettingsFilePath, e.SettingsFilePath,
e.SettingsFilePath, e.SettingsFilePath,
ReplacementCharacters.Barebones, Configuration.Instance.ReplacementCharacters,
"bak"); "bak");
AudibleApiStorage.EnsureAccountsSettingsFileExists(); AudibleApiStorage.EnsureAccountsSettingsFileExists();
e.Handled = true; e.Handled = true;

View File

@ -319,7 +319,7 @@ namespace LibationFileManager
#region templates: custom file naming #region templates: custom file naming
[Description("Edit how filename characters are replaced")] [Description("Edit how filename characters are replaced")]
public ReplacementCharacters ReplacementCharacters { get => GetNonString(defaultValue: ReplacementCharacters.Default); set => SetNonString(value); } public ReplacementCharacters ReplacementCharacters { get => GetNonString(defaultValue: ReplacementCharacters.Default(IsWindows)); set => SetNonString(value); }
[Description("How to format the folders in which files will be saved")] [Description("How to format the folders in which files will be saved")]
public string FolderTemplate public string FolderTemplate

View File

@ -50,13 +50,13 @@ namespace LibationWinForms.Dialogs
} }
private void loFiDefaultsBtn_Click(object sender, EventArgs e) private void loFiDefaultsBtn_Click(object sender, EventArgs e)
=> LoadTable(ReplacementCharacters.LoFiDefault.Replacements); => LoadTable(ReplacementCharacters.LoFiDefault(ntfs: true).Replacements);
private void defaultsBtn_Click(object sender, EventArgs e) private void defaultsBtn_Click(object sender, EventArgs e)
=> LoadTable(ReplacementCharacters.Default.Replacements); => LoadTable(ReplacementCharacters.Default(ntfs: true).Replacements);
private void minDefaultBtn_Click(object sender, EventArgs e) private void minDefaultBtn_Click(object sender, EventArgs e)
=> LoadTable(ReplacementCharacters.Barebones.Replacements); => LoadTable(ReplacementCharacters.Barebones(ntfs: true).Replacements);
private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e) private void dataGridView1_CellEndEdit(object sender, DataGridViewCellEventArgs e)

View File

@ -47,7 +47,7 @@ namespace LibationWinForms
FileUtility.SaferMoveToValidPath( FileUtility.SaferMoveToValidPath(
e.SettingsFilePath, e.SettingsFilePath,
e.SettingsFilePath, e.SettingsFilePath,
ReplacementCharacters.Barebones, Configuration.Instance.ReplacementCharacters,
"bak"); "bak");
AudibleApiStorage.EnsureAccountsSettingsFileExists(); AudibleApiStorage.EnsureAccountsSettingsFileExists();

View File

@ -12,9 +12,9 @@ namespace FileUtilityTests
[TestClass] [TestClass]
public class GetSafePath public class GetSafePath
{ {
static readonly ReplacementCharacters Default = ReplacementCharacters.Default; static readonly ReplacementCharacters Default = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault; static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault(Environment.OSVersion.Platform == PlatformID.Win32NT);
static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones; static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones(Environment.OSVersion.Platform == PlatformID.Win32NT);
[TestMethod] [TestMethod]
public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null, Default)); public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null, Default));
@ -98,9 +98,9 @@ namespace FileUtilityTests
[TestClass] [TestClass]
public class GetSafeFileName public class GetSafeFileName
{ {
static readonly ReplacementCharacters Default = ReplacementCharacters.Default; static readonly ReplacementCharacters Default = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault; static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault(Environment.OSVersion.Platform == PlatformID.Win32NT);
static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones; static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones(Environment.OSVersion.Platform == PlatformID.Win32NT);
// needs separate method. middle null param not running correctly in TestExplorer when used in DataRow() // needs separate method. middle null param not running correctly in TestExplorer when used in DataRow()
[TestMethod] [TestMethod]
@ -193,7 +193,7 @@ namespace FileUtilityTests
[TestClass] [TestClass]
public class GetValidFilename public class GetValidFilename
{ {
static ReplacementCharacters Replacements = ReplacementCharacters.Default; static ReplacementCharacters Replacements = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
[TestMethod] [TestMethod]
// dot-files // dot-files

View File

@ -66,7 +66,7 @@ namespace TemplatesTests
[TestClass] [TestClass]
public class getFileNamingTemplate public class getFileNamingTemplate
{ {
static ReplacementCharacters Replacements = ReplacementCharacters.Default; static ReplacementCharacters Replacements = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
[TestMethod] [TestMethod]
[DataRow(null)] [DataRow(null)]
@ -453,7 +453,7 @@ namespace Templates_Other
[TestClass] [TestClass]
public class GetFilePath public class GetFilePath
{ {
static ReplacementCharacters Replacements = ReplacementCharacters.Default; static ReplacementCharacters Replacements = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
[TestMethod] [TestMethod]
[DataRow(@"C:\foo\bar", @"\\Folder\<title>\[<id>]\\", @"C:\foo\bar\Folder\my_ book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\[ID123456].txt", PlatformID.Win32NT)] [DataRow(@"C:\foo\bar", @"\\Folder\<title>\[<id>]\\", @"C:\foo\bar\Folder\my_ book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\[ID123456].txt", PlatformID.Win32NT)]
@ -889,7 +889,7 @@ namespace Templates_ChapterFile_Tests
[TestClass] [TestClass]
public class GetFilename public class GetFilename
{ {
static readonly ReplacementCharacters Default = ReplacementCharacters.Default; static readonly ReplacementCharacters Default = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
[TestMethod] [TestMethod]
[DataRow("[<id>] <ch# 0> of <ch count> - <ch title>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\[asin] 06 of 10 - chap.txt", PlatformID.Win32NT)] [DataRow("[<id>] <ch# 0> of <ch count> - <ch title>", @"C:\foo\", "txt", 6, 10, "chap", @"C:\foo\[asin] 06 of 10 - chap.txt", PlatformID.Win32NT)]