Merge pull request #1415 from Mbucari/master

Fix minor UI bugs
This commit is contained in:
rmcrackan
2025-11-08 18:04:13 -05:00
committed by GitHub
19 changed files with 462 additions and 362 deletions

View File

@@ -14,7 +14,7 @@ namespace FileManager
/// </summary>
public class BackgroundFileSystem : IDisposable
{
public LongPath RootDirectory { get; private set; }
public LongPath? RootDirectory { get; private set; }
public string SearchPattern { get; private set; }
public SearchOption SearchOption { get; private set; }
@@ -51,7 +51,8 @@ namespace FileManager
lock (fsCacheLocker)
{
fsCache.Clear();
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
if (Directory.Exists(RootDirectory))
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
}
}
@@ -60,7 +61,14 @@ namespace FileManager
Stop();
lock (fsCacheLocker)
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
{
if (!Directory.Exists(RootDirectory))
{
RootDirectory = null;
return;
}
fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
}
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
fileSystemWatcher = new FileSystemWatcher(RootDirectory)

View File

@@ -0,0 +1,25 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace LibationAvalonia.Controls;
internal class DataGridTextColumnExt : DataGridTextColumn
{
public static readonly StyledProperty<int> MaxLengthProperty =
AvaloniaProperty.Register<DataGridTextColumnExt, int>(nameof(MaxLength));
public int MaxLength
{
get => GetValue(MaxLengthProperty);
set => SetValue(MaxLengthProperty, value);
}
protected override object PrepareCellForEdit(Control editingElement, RoutedEventArgs editingEventArgs)
{
if (editingElement is TextBox textBox)
{
textBox.MaxLength = MaxLength;
}
return base.PrepareCellForEdit(editingElement, editingEventArgs);
}
}

View File

@@ -6,29 +6,63 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LibationAvalonia.Controls.DirectoryOrCustomSelectControl">
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" Name="grid">
<controls:DirectorySelectControl
Grid.Column="1"
Grid.Row="0"
IsEnabled="{Binding KnownChecked}"
SelectedDirectory="{Binding SelectedDirectory, Mode=TwoWay}"
SubDirectory="{Binding $parent[1].SubDirectory}"
KnownDirectories="{Binding $parent[1].KnownDirectories}" />
<Grid
RowDefinitions="Auto,Auto,Auto"
ColumnDefinitions="Auto,*,Auto">
<RadioButton
Grid.Column="0"
Grid.Row="0"
IsChecked="{Binding KnownChecked, Mode=TwoWay}"/>
<RadioButton
Grid.RowSpan="2"
Name="rbKnown" />
<RadioButton
Grid.Column="0"
Grid.Row="1"
IsChecked="{Binding CustomChecked, Mode=TwoWay}"/>
<TextBlock
Grid.Column="1"
Grid.ColumnSpan="2"
VerticalAlignment="Center"
Margin="10,0"
IsEnabled="False"
IsVisible="{Binding #cmbKnownDirs.SelectedItem, Converter={x:Static ObjectConverters.IsNull}}"
Text="Select Known Directory:" />
<Grid Grid.Column="1" Grid.Row="1" ColumnDefinitions="*,Auto"
IsEnabled="{Binding CustomChecked}">
<TextBox Grid.Column="0" IsReadOnly="True" Text="{Binding CustomDir, Mode=TwoWay}" />
<Button Grid.Column="1" Content="..." Margin="5,0,0,0" Padding="10,0,10,0" Click="CustomDirBrowseBtn_Click" VerticalAlignment="Stretch" />
</Grid>
</Grid>
<controls:WheelComboBox
Grid.Column="1"
Grid.ColumnSpan="2"
HorizontalAlignment="Stretch"
Margin="0,0,0,3"
IsEnabled="{Binding #rbKnown.IsChecked}"
Name="cmbKnownDirs" />
<TextBox
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="2"
IsReadOnly="True"
Margin="0,0,0,8"
Name="tboxKnownDirPath"
IsEnabled="{Binding #rbKnown.IsChecked}"
Text="{Binding #cmbKnownDirs.SelectedItem.Directory}" />
<RadioButton
Grid.Row="2"
Name="rbCustom" />
<TextBox
Grid.Row="2"
Grid.Column="1"
HorizontalAlignment="Stretch"
Name="tboxCustomDirPath"
Margin="0,0,10,0"
VerticalAlignment="Center"
Text="{Binding $parent[1].Directory, Mode=OneWayToSource}"
IsEnabled="{Binding #rbCustom.IsChecked}"/>
<Button
Grid.Row="2"
Grid.Column="2"
Name="btnBrowse"
IsEnabled="{Binding #rbCustom.IsChecked}">
<TextBlock Text="..." />
</Button>
</Grid>
</UserControl>

View File

@@ -1,142 +1,184 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Dinah.Core;
using LibationFileManager;
using ReactiveUI;
using System.Collections.Generic;
using System.IO;
using System.Linq;
#nullable enable
namespace LibationAvalonia.Controls
{
public partial class DirectoryOrCustomSelectControl : UserControl
{
public static readonly StyledProperty<List<Configuration.KnownDirectories>> KnownDirectoriesProperty =
AvaloniaProperty.Register<DirectorySelectControl, List<Configuration.KnownDirectories>>(nameof(KnownDirectories), DirectorySelectControl.DefaultKnownDirectories);
public static readonly StyledProperty<IList<Configuration.KnownDirectories>?> KnownDirectoriesProperty =
AvaloniaProperty.Register<DirectoryOrCustomSelectControl, IList<Configuration.KnownDirectories>?>(nameof(KnownDirectories), DefaultKnownDirectories);
public static readonly StyledProperty<string> SubDirectoryProperty =
AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(SubDirectory));
public static readonly StyledProperty<string?> SubDirectoryProperty =
AvaloniaProperty.Register<DirectoryOrCustomSelectControl, string?>(nameof(SubDirectory));
public static readonly StyledProperty<string> DirectoryProperty =
AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(Directory));
public static readonly StyledProperty<string?> DirectoryProperty =
AvaloniaProperty.Register<DirectoryOrCustomSelectControl, string?>(nameof(Directory));
public List<Configuration.KnownDirectories> KnownDirectories
public IList<Configuration.KnownDirectories>? KnownDirectories
{
get => GetValue(KnownDirectoriesProperty);
set => SetValue(KnownDirectoriesProperty, value);
}
public string Directory
public string? Directory
{
get => GetValue(DirectoryProperty);
set => SetValue(DirectoryProperty, value);
}
public string SubDirectory
public string? SubDirectory
{
get => GetValue(SubDirectoryProperty);
set => SetValue(SubDirectoryProperty, value);
}
private readonly DirectoryState directoryState = new();
public static IList<Configuration.KnownDirectories> DefaultKnownDirectories => [
Configuration.KnownDirectories.WinTemp,
Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.ApplicationData,
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyMusic,
Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.LibationFiles];
private readonly AvaloniaList<KnownDirectoryItem> _knownDirNames;
public DirectoryOrCustomSelectControl()
{
InitializeComponent();
grid.DataContext = directoryState;
directoryState.PropertyChanged += DirectoryState_PropertyChanged;
PropertyChanged += DirectoryOrCustomSelectControl_PropertyChanged;
_knownDirNames = new(GetKnownDirectories(DefaultKnownDirectories));
cmbKnownDirs.ItemsSource = _knownDirNames;
cmbKnownDirs.SelectionChanged += CmbKnownDirs_SelectionChanged;
btnBrowse.Click += Browse_Click;
}
private void DirectoryState_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
private void CmbKnownDirs_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (e.PropertyName is nameof(DirectoryState.SelectedDirectory) or nameof(DirectoryState.KnownChecked) &&
directoryState.KnownChecked &&
directoryState.SelectedDirectory is Configuration.KnownDirectories kdir &&
kdir is not Configuration.KnownDirectories.None)
if (cmbKnownDirs.SelectedItem is KnownDirectoryItem item && item.Directory is not null)
{
Directory = kdir is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute : Configuration.GetKnownDirectoryPath(kdir);
}
else if (e.PropertyName is nameof(DirectoryState.CustomDir) or nameof(DirectoryState.CustomChecked) &&
directoryState.CustomChecked &&
directoryState.CustomDir is not null)
{
Directory = directoryState.CustomDir;
Directory = item.Directory;
}
}
private class DirectoryState : ViewModels.ViewModelBase
{
private string _customDir;
private string _subDirectory;
private bool _knownChecked;
private bool _customChecked;
private Configuration.KnownDirectories? _selectedDirectory;
public string CustomDir { get => _customDir; set => this.RaiseAndSetIfChanged(ref _customDir, value); }
public string SubDirectory { get => _subDirectory; set => this.RaiseAndSetIfChanged(ref _subDirectory, value); }
public bool KnownChecked { get => _knownChecked; set => this.RaiseAndSetIfChanged(ref _knownChecked, value); }
public bool CustomChecked { get => _customChecked; set => this.RaiseAndSetIfChanged(ref _customChecked, value); }
private IEnumerable<KnownDirectoryItem> GetKnownDirectories(IEnumerable<Configuration.KnownDirectories> knownDirs)
=> knownDirs.Select(k => new KnownDirectoryItem(k, SubDirectory)).Where(k => k.Directory is not null);
public Configuration.KnownDirectories? SelectedDirectory { get => _selectedDirectory; set => this.RaiseAndSetIfChanged(ref _selectedDirectory, value); }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == SubDirectoryProperty)
{
foreach (var item in _knownDirNames)
{
item.SubDirectory = SubDirectory;
}
VerifyAndApplyDirectory(Directory);
}
else if (change.Property == KnownDirectoriesProperty)
{
var knownDirs = KnownDirectories?.Count > 0 ? KnownDirectories : DefaultKnownDirectories;
if (!_knownDirNames.Select(k => k.KnownDirectory).SequenceEqual(knownDirs))
{
_knownDirNames.Clear();
_knownDirNames.AddRange(GetKnownDirectories(knownDirs));
}
VerifyAndApplyDirectory(Directory);
}
else if (change.Property == DirectoryProperty)
{
VerifyAndApplyDirectory(Directory);
}
base.OnPropertyChanged(change);
}
private async void CustomDirBrowseBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private void VerifyAndApplyDirectory(string? directory)
{
var options = new Avalonia.Platform.Storage.FolderPickerOpenOptions
if (string.IsNullOrWhiteSpace(Directory))
return;
bool dirIsKnown = false;
foreach (var item in _knownDirNames)
{
if (item.IsSamePathAs(directory))
{
rbKnown.IsChecked = true;
Directory = item.Directory;
cmbKnownDirs.SelectedItem = item;
dirIsKnown = true;
break;
}
}
if (!dirIsKnown)
{
tboxCustomDirPath.Text = directory;
rbCustom.IsChecked = true;
}
}
public async void Browse_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (VisualRoot is not Window window)
return;
var options = new FolderPickerOpenOptions
{
AllowMultiple = false
};
var selectedFolders = await (VisualRoot as Window).StorageProvider.OpenFolderPickerAsync(options);
directoryState.CustomDir = selectedFolders.SingleOrDefault()?.TryGetLocalPath() ?? directoryState.CustomDir;
var selectedFolders = await window.StorageProvider.OpenFolderPickerAsync(options);
Directory = selectedFolders.SingleOrDefault()?.TryGetLocalPath() ?? Directory;
}
private void DirectoryOrCustomSelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
private class KnownDirectoryItem : ReactiveObject
{
if (e.Property == DirectoryProperty)
public Configuration.KnownDirectories KnownDirectory { get; set; }
private string? _directory;
public string? Directory { get => _directory; private set => this.RaiseAndSetIfChanged(ref _directory, value); }
public string? Name { get; }
private string? _subDir;
public string? SubDirectory
{
var directory = Directory?.Trim() ?? "";
var noSubDir = RemoveSubDirectoryFromPath(directory);
var known = Configuration.GetKnownDirectory(noSubDir);
if (known == Configuration.KnownDirectories.None && noSubDir == Configuration.AppDir_Absolute)
known = Configuration.KnownDirectories.AppDir;
if (known is Configuration.KnownDirectories.None)
get => _subDir;
set
{
directoryState.CustomDir = directory;
directoryState.CustomChecked = true;
}
else
{
directoryState.SelectedDirectory = known;
directoryState.KnownChecked = true;
_subDir = value;
if (Configuration.GetKnownDirectoryPath(KnownDirectory) is string dir)
{
Directory = Path.Combine(dir, _subDir ?? "");
}
}
}
else if (e.Property == KnownDirectoriesProperty &&
KnownDirectories.Count > 0 &&
directoryState.SelectedDirectory is null or Configuration.KnownDirectories.None)
directoryState.SelectedDirectory = KnownDirectories[0];
}
private string RemoveSubDirectoryFromPath(string path)
{
if (string.IsNullOrWhiteSpace(SubDirectory))
return path;
public KnownDirectoryItem(Configuration.KnownDirectories known, string? subDir)
{
Name = known.GetDescription();
KnownDirectory = known;
SubDirectory = subDir;
}
path = path?.Trim() ?? "";
if (string.IsNullOrWhiteSpace(path))
return path;
public bool IsSamePathAs(string? otherPath)
{
if (string.IsNullOrWhiteSpace(otherPath) || string.IsNullOrWhiteSpace(Directory))
return false;
var bottomDir = System.IO.Path.GetFileName(path);
if (SubDirectory.EqualsInsensitive(bottomDir))
return System.IO.Path.GetDirectoryName(path);
try
{
var p1 = Path.GetFullPath(Directory);
var p2 = Path.GetFullPath(otherPath);
return p1.Equals(p2, System.StringComparison.OrdinalIgnoreCase);
}
catch { return false; }
}
return path;
public override string? ToString() => Name?.ToString();
}
}
}

View File

@@ -9,15 +9,15 @@
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
Title="About Libation">
<Grid Margin="10" ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,*">
<Grid Margin="10" RowDefinitions="Auto,Auto,Auto,Auto,*">
<controls:LinkLabel Grid.ColumnSpan="2" FontSize="16" FontWeight="Bold" Text="{Binding Version}" ToolTip.Tip="View Release Notes" Tapped="ViewReleaseNotes_Tapped" />
<controls:LinkLabel FontSize="16" FontWeight="Bold" Text="{Binding Version}" ToolTip.Tip="View Release Notes" Tapped="ViewReleaseNotes_Tapped" />
<controls:LinkLabel Grid.Column="1" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Right" Text="https://getlibation.com" Tapped="Link_getlibation"/>
<controls:LinkLabel Grid.Row="1" FontSize="14" VerticalAlignment="Center" Text="https://getlibation.com" Tapped="Link_getlibation"/>
<Button Grid.Row="1" Grid.ColumnSpan="2" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Margin="0,20,0,0" IsEnabled="{Binding CanCheckForUpgrade}" Content="{Binding UpgradeButtonText}" Click="CheckForUpgrade_Click" />
<Button Grid.Row="2" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Margin="0,10,0,0" IsEnabled="{Binding CanCheckForUpgrade}" Content="{Binding UpgradeButtonText}" Click="CheckForUpgrade_Click" />
<Canvas Grid.Row="2" Grid.ColumnSpan="2" Margin="0,30,0,20" Width="280" Height="220">
<Canvas Grid.Row="3" Margin="0,30,0,20" Width="280" Height="220">
<Path Stretch="None" Fill="{DynamicResource IconFill}" Data="{DynamicResource LibationCheersIcon}">
<Path.RenderTransform>
<TransformGroup>
@@ -39,7 +39,7 @@
</Path>
</Canvas>
<controls:GroupBox Grid.Row="3" Label="Acknowledgements" Grid.ColumnSpan="2">
<controls:GroupBox Grid.Row="4" Label="Acknowledgements">
<StackPanel>
<StackPanel.Styles>
<Style Selector="controls|LinkLabel">

View File

@@ -2,92 +2,80 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
MinWidth="500" MinHeight="450"
Width="500" Height="450"
mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="450"
MinWidth="450" MinHeight="450"
Width="450" Height="450"
x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
x:DataType="dialogs:EditReplacementChars"
Title="Illegal Character Replacement">
<Grid
RowDefinitions="*,Auto"
ColumnDefinitions="*,Auto">
x:CompileBindings="True"
Title="File Path Character Replacement">
<Grid RowDefinitions="*,Auto">
<DataGrid
Grid.Row="0"
Grid.ColumnSpan="2"
GridLinesVisibility="All"
Margin="5"
Name="replacementGrid"
AutoGenerateColumns="False"
IsReadOnly="False"
BeginningEdit="ReplacementGrid_BeginningEdit"
CellEditEnding="ReplacementGrid_CellEditEnding"
KeyDown="ReplacementGrid_KeyDown"
ItemsSource="{CompiledBinding replacements}">
GridLinesVisibility="All"
CanUserSortColumns="False"
AutoGenerateColumns="False"
ItemsSource="{Binding Replacements}"
KeyDown="replacementGrid_KeyDown"
BeginningEdit="replacementGrid_BeginningEdit"
CellEditEnded="replacementGrid_CellEditEnded"
CellEditEnding="replacementGrid_CellEditEnding">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Char to&#xa;Replace">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
<TextBox IsReadOnly="{CompiledBinding Mandatory}" Text="{CompiledBinding CharacterToReplace, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Replacement&#xa;Text">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
<TextBox Text="{CompiledBinding ReplacementText, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<controls:DataGridTextColumnExt
x:DataType="dialogs:EditReplacementChars+ReplacementsExt"
MaxLength="1"
Header="Char to&#xa;Replace"
Binding="{Binding CharacterToReplace, Mode=TwoWay}"/>
<DataGridTemplateColumn Width="*" Header="Description">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
<TextBox IsReadOnly="{CompiledBinding Mandatory}" Text="{CompiledBinding Description, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
x:DataType="dialogs:EditReplacementChars+ReplacementsExt"
Header="Replacement&#xa;Text"
Binding="{Binding ReplacementText, Mode=TwoWay}"/>
<DataGridTextColumn
x:DataType="dialogs:EditReplacementChars+ReplacementsExt"
Header="Description"
Binding="{Binding Description, Mode=TwoWay}"/>
</DataGrid.Columns>
</DataGrid>
<Grid
Grid.Row="1"
Grid.Column="0"
RowDefinitions="Auto,Auto"
Margin="5"
ColumnDefinitions="Auto,Auto,Auto,Auto">
ColumnDefinitions="Auto,Auto,Auto,*,Auto,Auto"
Margin="5">
<Grid.Styles>
<Style Selector="Button">
<Setter Property="Margin" Value="2"/>
<Setter Property="Padding" Value="6"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
</Style>
<Style Selector="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</Grid.Styles>
<TextBlock IsVisible="{CompiledBinding !EnvironmentIsWindows}" Text="This System:" Margin="0,0,10,0" VerticalAlignment="Center" />
<TextBlock IsVisible="{CompiledBinding !EnvironmentIsWindows}" Grid.Row="1" Text="NTFS:" Margin="0,0,10,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="0" Text="This&#xa;System:" IsVisible="{Binding !EnvironmentIsWindows}" />
<TextBlock Grid.Row="1" Text="NTFS:" IsVisible="{Binding !EnvironmentIsWindows}" />
<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 Grid.Column="1" Command="{Binding Defaults}" CommandParameter="{Binding EnvironmentIsWindows}" Content="Defaults" />
<Button Grid.Column="2" Command="{Binding LoFiDefaults}" CommandParameter="{Binding EnvironmentIsWindows}" Content="LoFi Defaults" />
<Button Grid.Column="3" Command="{Binding Barebones}" CommandParameter="{Binding 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
Grid.Row="1"
Grid.Column="1"
Margin="5"
VerticalAlignment="Bottom"
Orientation="Horizontal">
<Button Margin="0,0,10,0" Command="{Binding Close}" Content="Cancel" />
<Button Padding="20,5,20,6" Command="{Binding SaveAndClose}" Content="Save" />
</StackPanel>
</Grid>
<Button Grid.Row="1" Grid.Column="1" IsVisible="{Binding !EnvironmentIsWindows}" Command="{Binding Defaults}" CommandParameter="True" Content="Defaults" />
<Button Grid.Row="1" Grid.Column="2" IsVisible="{Binding !EnvironmentIsWindows}" Command="{Binding LoFiDefaults}" CommandParameter="True" Content="LoFi Defaults" />
<Button Grid.Row="1" Grid.Column="3" IsVisible="{Binding !EnvironmentIsWindows}" Command="{Binding Barebones}" CommandParameter="True" Content="Barebones" />
<Button Grid.RowSpan="2" Grid.Column="4" Command="{Binding Close}" Content="Cancel" />
<Button Grid.RowSpan="2" Grid.Column="5" Padding="20,6" Command="{Binding SaveAndClose}" Content="Save" />
</Grid>
</Grid>
</Window>

View File

@@ -1,27 +1,27 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Data;
using FileManager;
using LibationFileManager;
using ReactiveUI;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace LibationAvalonia.Dialogs
{
public partial class EditReplacementChars : DialogWindow
{
Configuration config;
private Configuration? Config { get; }
public bool EnvironmentIsWindows => Configuration.IsWindows;
private readonly List<ReplacementsExt> SOURCE = new();
public DataGridCollectionView replacements { get; }
private readonly AvaloniaList<ReplacementsExt> SOURCE = new();
public DataGridCollectionView Replacements { get; }
public EditReplacementChars()
{
InitializeComponent();
replacements = new(SOURCE);
Replacements = new(SOURCE);
if (Design.IsDesignMode)
{
@@ -33,7 +33,7 @@ namespace LibationAvalonia.Dialogs
public EditReplacementChars(Configuration config) : this()
{
this.config = config;
Config = config;
LoadTable(config.ReplacementCharacters.Replacements);
}
@@ -44,15 +44,14 @@ namespace LibationAvalonia.Dialogs
public void Barebones(bool isNtfs)
=> LoadTable(ReplacementCharacters.Barebones(isNtfs).Replacements);
protected override void SaveAndClose()
public new void Close() => base.Close();
public new void SaveAndClose()
{
var replacements = SOURCE
.Where(r => !r.IsDefault)
.Select(r => new Replacement(r.Character, r.ReplacementText, r.Description) { Mandatory = r.Mandatory })
.ToList();
if (config is not null)
config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements };
if (Config is not null)
{
var replacements = SOURCE.Where(r => !r.IsDefault).Select(r => r.ToReplacement()).ToArray();
Config.ReplacementCharacters = new ReplacementCharacters { Replacements = replacements };
}
base.SaveAndClose();
}
@@ -61,59 +60,64 @@ namespace LibationAvalonia.Dialogs
SOURCE.Clear();
SOURCE.AddRange(replacements.Select(r => new ReplacementsExt(r)));
SOURCE.Add(new ReplacementsExt());
this.replacements.Refresh();
}
public void ReplacementGrid_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
private bool ColumnIsCharacter(DataGridColumn column)
=> column.DisplayIndex is 0;
private bool ColumnIsReplacement(DataGridColumn column)
=> column.DisplayIndex is 1;
private bool RowIsReadOnly(DataGridRow row)
=> row.DataContext is ReplacementsExt rep && rep.Mandatory;
private bool CanDeleteSelectedItem(ReplacementsExt selectedItem)
=> !selectedItem.Mandatory && (!selectedItem.IsDefault || SOURCE[^1] != selectedItem);
private void replacementGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
if (e.Key == Avalonia.Input.Key.Delete
&& ((DataGrid)sender).SelectedItem is ReplacementsExt repl
&& !repl.Mandatory
&& !repl.IsDefault)
{
replacements.Remove(repl);
}
e.Cancel = RowIsReadOnly(e.Row) && !ColumnIsReplacement(e.Column);
}
public void ReplacementGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
private void replacementGrid_CellEditEnding(object? sender, DataGridCellEditEndingEventArgs e)
{
var replacement = e.Row.DataContext as ReplacementsExt;
var colBinding = columnBindingPath(e.Column);
//Prevent duplicate CharacterToReplace
if (e.EditingElement is TextBox tbox
&& colBinding == nameof(replacement.CharacterToReplace)
&& SOURCE.Any(r => r != replacement && r.CharacterToReplace == tbox.Text))
//Disallow duplicates of CharacterToReplace
if (ColumnIsCharacter(e.Column) && e.Row.DataContext is ReplacementsExt r && r.CharacterToReplace.Length > 0 && SOURCE.Count(rep => rep.CharacterToReplace == r.CharacterToReplace) > 1)
{
tbox.Text = replacement.CharacterToReplace;
}
//Add new blank row
void Replacement_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (!SOURCE.Any(r => r.IsDefault))
{
var rewRepl = new ReplacementsExt();
SOURCE.Add(rewRepl);
}
replacement.PropertyChanged -= Replacement_PropertyChanged;
}
replacement.PropertyChanged += Replacement_PropertyChanged;
}
public void ReplacementGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)
{
var replacement = e.Row.DataContext as ReplacementsExt;
//Disallow editing of Mandatory CharacterToReplace and Descriptions
if (replacement.Mandatory
&& columnBindingPath(e.Column) != nameof(replacement.ReplacementText))
r.CharacterToReplace = "";
e.Cancel = true;
}
}
private static string columnBindingPath(DataGridColumn column)
=> ((Binding)((DataGridBoundColumn)column).Binding).Path;
private void replacementGrid_CellEditEnded(object? sender, DataGridCellEditEndedEventArgs e)
{
if (ColumnIsCharacter(e.Column) && e.Row.DataContext is ReplacementsExt r && r.CharacterToReplace.Length > 0 && !SOURCE[^1].IsDefault)
{
Replacements.AddNew();
}
}
private void replacementGrid_KeyDown(object? sender, Avalonia.Input.KeyEventArgs e)
{
if (e.Key == Avalonia.Input.Key.Delete && (sender as DataGrid)?.SelectedItem is ReplacementsExt r && CanDeleteSelectedItem(r))
{
if (Replacements.IsEditingItem)
{
if (Replacements.CanCancelEdit)
Replacements.CancelEdit();
else
Replacements.CommitEdit();
}
if (Replacements.IsAddingNew)
{
Replacements.CancelNew();
}
if (Replacements.CanRemove)
{
Replacements.Remove(r);
}
}
}
public class ReplacementsExt : ViewModels.ViewModelBase
{
@@ -122,7 +126,6 @@ namespace LibationAvalonia.Dialogs
_replacementText = string.Empty;
_description = string.Empty;
_characterToReplace = string.Empty;
IsDefault = true;
}
public ReplacementsExt(Replacement replacement)
{
@@ -131,41 +134,19 @@ namespace LibationAvalonia.Dialogs
_description = replacement.Description;
Mandatory = replacement.Mandatory;
}
private string _replacementText;
private string _description;
private string _characterToReplace;
public bool Mandatory { get; }
public string ReplacementText
{
get => _replacementText;
set
{
if (ReplacementCharacters.ContainsInvalidFilenameChar(value))
this.RaisePropertyChanged(nameof(ReplacementText));
else
this.RaiseAndSetIfChanged(ref _replacementText, value);
}
}
public string ReplacementText { get => _replacementText; set => this.RaiseAndSetIfChanged(ref _replacementText, value); }
public string Description { get => _description; set => this.RaiseAndSetIfChanged(ref _description, value); }
public string CharacterToReplace
{
get => _characterToReplace;
set
{
if (value?.Length != 1)
this.RaisePropertyChanged(nameof(CharacterToReplace));
else
{
IsDefault = false;
this.RaiseAndSetIfChanged(ref _characterToReplace, value);
}
}
}
public string CharacterToReplace { get => _characterToReplace; set => this.RaiseAndSetIfChanged(ref _characterToReplace, value); }
public char Character => string.IsNullOrEmpty(_characterToReplace) ? default : _characterToReplace[0];
public bool IsDefault { get; private set; }
public bool IsDefault => !Mandatory && string.IsNullOrEmpty(CharacterToReplace);
public bool Mandatory { get; }
public Replacement ToReplacement()
=> new(Character, ReplacementText, Description) { Mandatory = Mandatory };
}
}
}

View File

@@ -1,15 +1,23 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="200"
x:Class="LibationAvalonia.Dialogs.ScanAccountsDialog"
MinWidth="500" MinHeight="160"
Width="500" Height="200"
Title="Which Accounts?"
WindowStartupLocation="CenterOwner">
<Window
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="500" d:DesignHeight="340"
MinWidth="200" MinHeight="210"
Width="500" Height="500"
x:Class="LibationAvalonia.Dialogs.ScanAccountsDialog"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
x:DataType="dialogs:ScanAccountsDialog"
x:CompileBindings="True"
Title="Which Accounts?"
WindowStartupLocation="CenterOwner">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*,Auto">
<Grid
ColumnDefinitions="*,Auto"
RowDefinitions="Auto,*,Auto"
Margin="10">
<Grid.Styles>
<Style Selector="Button:focus">
@@ -22,54 +30,38 @@
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="10"
Text="Check the accounts to scan and import.&#xa;To change default selections, go to: Settings > Accounts"/>
<ScrollViewer
<DockPanel
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="10,0"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
Margin="0,10"
VerticalAlignment="Stretch">
<ListBox ItemsSource="{Binding Accounts}">
<ListBox Name="lbAccounts" ItemsSource="{Binding Accounts}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Height="20" Orientation="Horizontal">
<CheckBox
Margin="0,0,10,0"
IsChecked="{Binding IsChecked, Mode=TwoWay}" />
<TextBlock
FontSize="12"
VerticalAlignment="Center"
Text="{Binding Text}" />
</StackPanel>
<CheckBox
IsChecked="{Binding IsChecked, Mode=TwoWay}">
<TextBlock Text="{Binding Text}" />
</CheckBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<Button
Grid.Row="2"
Grid.Column="0"
Padding="20,5"
Margin="10"
Padding="20,6"
Content="Edit Accounts"
Command="{Binding EditAccountsAsync}"/>
<Button
Grid.Row="2"
Grid.Column="1"
Padding="30,5"
Margin="10"
Padding="30,6"
HorizontalAlignment="Right"
Content="Import"
Name="ImportButton"

View File

@@ -1,7 +1,6 @@
using AudibleUtilities;
using Avalonia.Controls;
using Avalonia.Collections;
using LibationUiBase.Forms;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -10,41 +9,36 @@ namespace LibationAvalonia.Dialogs
{
public partial class ScanAccountsDialog : DialogWindow
{
public List<Account> CheckedAccounts { get; } = new();
private List<listItem> _accounts { get; } = new();
public IList Accounts => _accounts;
private class listItem
public IEnumerable<Account> CheckedAccounts => Accounts.Where(a => a.IsChecked).Select(a => a.Account);
public AvaloniaList<ListItem> Accounts { get; } = new();
public class ListItem
{
public Account Account { get; set; }
public string Text { get; set; }
public bool IsChecked { get; set; } = true;
public ListItem(Account account)
{
Account = account;
IsChecked = account.LibraryScan;
Text = $"{account.AccountName} ({account.AccountId} - {account.Locale.Name})";
}
public Account Account { get; }
public string Text { get; }
public bool IsChecked { get; set; }
public override string ToString() => Text;
}
public ScanAccountsDialog()
{
InitializeComponent();
ControlToFocusOnShow = this.FindControl<Button>(nameof(ImportButton));
ControlToFocusOnShow = ImportButton;
DataContext = this;
LoadAccounts();
}
private void LoadAccounts()
{
_accounts.Clear();
Accounts.Clear();
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
foreach (var account in accounts)
_accounts.Add(new listItem
{
Account = account,
IsChecked = account.LibraryScan,
Text = $"{account.AccountName} ({account.AccountId} - {account.Locale.Name})"
});
DataContext = this;
Accounts.AddRange(accounts.Select(account => new ListItem(account)));
}
public async Task EditAccountsAsync()
@@ -56,12 +50,7 @@ namespace LibationAvalonia.Dialogs
}
}
protected override void SaveAndClose()
{
foreach (listItem item in _accounts.Where(a => a.IsChecked))
CheckedAccounts.Add(item.Account);
base.SaveAndClose();
}
public new void SaveAndClose()
=> base.SaveAndClose();
}
}

View File

@@ -50,25 +50,25 @@
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,*">
<TextBlock Text="STRING FIELDS" />
<TextBlock Grid.Row="1" Text="{CompiledBinding StringUsage}" />
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding StringFields}"/>
<ListBox Grid.Row="2" DoubleTapped="ListBox_DoubleTapped" ItemsSource="{CompiledBinding StringFields}"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="1" RowDefinitions="Auto,Auto,*">
<TextBlock Text="NUMBER FIELDS" />
<TextBlock Grid.Row="1" Text="{CompiledBinding NumberUsage}" />
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding NumberFields}"/>
<ListBox Grid.Row="2" DoubleTapped="ListBox_DoubleTapped" ItemsSource="{CompiledBinding NumberFields}"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="2" RowDefinitions="Auto,Auto,*">
<TextBlock Text="BOOLEAN (TRUE/FALSE) FIELDS" />
<TextBlock Grid.Row="1" Text="{CompiledBinding BoolUsage}" />
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding BoolFields}"/>
<ListBox Grid.Row="2" DoubleTapped="ListBox_DoubleTapped" ItemsSource="{CompiledBinding BoolFields}"/>
</Grid>
<Grid Grid.Row="1" Grid.Column="3" RowDefinitions="Auto,Auto,*">
<TextBlock Text="ID FIELDS" />
<TextBlock Grid.Row="1" Text="{CompiledBinding IdUsage}" />
<ListBox Grid.Row="2" ItemsSource="{CompiledBinding IdFields}"/>
<ListBox Grid.Row="2" DoubleTapped="ListBox_DoubleTapped" ItemsSource="{CompiledBinding IdFields}"/>
</Grid>
</Grid>
</Window>

View File

@@ -1,10 +1,14 @@
using Avalonia;
using LibationSearchEngine;
using System;
using System.Linq;
#nullable enable
namespace LibationAvalonia.Dialogs
{
public partial class SearchSyntaxDialog : DialogWindow
{
public event EventHandler<string>? TagDoubleClicked;
public string StringUsage { get; }
public string NumberUsage { get; }
public string BoolUsage { get; }
@@ -51,5 +55,13 @@ namespace LibationAvalonia.Dialogs
DataContext = this;
}
private void ListBox_DoubleTapped(object? sender, Avalonia.Input.TappedEventArgs e)
{
if (e.Source is StyledElement { DataContext: string tag })
{
TagDoubleClicked?.Invoke(this, tag);
}
}
}
}

View File

@@ -20,7 +20,7 @@ namespace LibationAvalonia.Dialogs
{
TopMessage = UpdateMessage;
Title = "Libation version 8.7.0 is now available.";
DownloadLinkText = "Libation.8.7.0-macos-chardonnay.tar.gz";
DownloadLinkText = "\r\nLibation.12.7.0-macOS-chardonnay-arm64.tgz ";
ReleaseNotes = "New features:\r\n\r\n* 'Remove' now removes forever. Removed books won't be re-added on next scan\r\n* #406 : Right Click Menu for Stop-Light Icon\r\n* #398 : Grid, right-click, copy\r\n* Add option for user to choose custom temp folder\r\n* Build Docker image\r\n\r\nEnhancements\r\n\r\n* Illegal Char Replace dialog in Chardonnay\r\n* Filename character replacement allows replacing any char, not just illegal\r\n* #352 : Better error messages for license denial\r\n* Improve 'cancel download'\r\n\r\nThanks to @Mbucari (u/MSWMan), @pixil98 (u/pixil)\r\n\r\nLibation is a free, open source audible library manager for Windows. Decrypt, backup, organize, and search your audible library\r\n\r\nI intend to keep Libation free and open source, but if you want to leave a tip, who am I to argue?";
OkText = "Yes";
DataContext = this;

View File

@@ -55,7 +55,7 @@ namespace LibationAvalonia.ViewModels
public void AddQuickFilterBtn() { if (SelectedNamedFilter != null) QuickFilters.Add(SelectedNamedFilter); }
public async Task FilterBtn(string filterString) => await PerformFilter(new(filterString, null));
public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow);
public void FilterHelpBtn() => MainWindow.ShowSearchSyntaxDialog();
public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault;
public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow);
public async Task PerformFilter(QuickFilters.NamedFilter? namedFilter)

View File

@@ -67,10 +67,11 @@ namespace LibationAvalonia.ViewModels.Settings
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
{
Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.LibationFiles,
Configuration.KnownDirectories.MyMusic,
Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.UserProfile
};
public string BooksText { get; } = Configuration.GetDescription(nameof(Configuration.Books));

View File

@@ -223,5 +223,28 @@ namespace LibationAvalonia.Views
}
private void setProgressVisible(bool visible) => ViewModel.DownloadProgress = visible ? 0 : null;
public SearchSyntaxDialog ShowSearchSyntaxDialog()
{
var dialog = new SearchSyntaxDialog();
dialog.TagDoubleClicked += Dialog_TagDoubleClicked;
dialog.Closed += Dialog_Closed;
filterHelpBtn.IsEnabled = false;
dialog.Show(this);
return dialog;
void Dialog_Closed(object sender, EventArgs e)
{
dialog.TagDoubleClicked -= Dialog_TagDoubleClicked;
filterHelpBtn.IsEnabled = true;
}
void Dialog_TagDoubleClicked(object sender, string tag)
{
var text = filterSearchTb.Text;
filterSearchTb.Text = text.Insert(Math.Min(Math.Max(0, filterSearchTb.CaretIndex), text.Length), tag);
filterSearchTb.CaretIndex += tag.Length;
filterSearchTb.Focus();
}
}
}
}

View File

@@ -203,9 +203,10 @@ namespace LibationAvalonia
await displayControlAsync(MainForm.filterHelpBtn);
var filterHelp = new SearchSyntaxDialog();
await filterHelp.ShowDialog(MainForm);
var searchDialog = MainForm.ShowSearchSyntaxDialog();
var tcs = new TaskCompletionSource();
searchDialog.Closed += (_, _) => tcs.SetResult();
await tcs.Task;
return true;
}

View File

@@ -146,16 +146,7 @@ namespace LibationFileManager
protected override List<LongPath> GetFilePathsCustom(string productId)
{
// If user changed the BooksDirectory: reinitialize
lock (bookDirectoryFilesLocker)
{
if (BooksDirectory != BookDirectoryFiles?.RootDirectory)
{
BookDirectoryFiles?.Dispose();
BookDirectoryFiles = newBookDirectoryFiles();
}
}
ValidateBookDirectoryFiles();
var regex = GetBookSearchRegex(productId);
var diskFiles = BookDirectoryFiles?.FindFiles(regex) ?? [];
@@ -172,14 +163,25 @@ namespace LibationFileManager
public void Refresh()
{
lock (bookDirectoryFilesLocker)
{
BookDirectoryFiles ??= newBookDirectoryFiles();
}
ValidateBookDirectoryFiles();
BookDirectoryFiles?.RefreshFiles();
}
private void ValidateBookDirectoryFiles()
{
lock (bookDirectoryFilesLocker)
{
if (BooksDirectory != BookDirectoryFiles?.RootDirectory)
{
//Will happen if the user changed the Books directory
//or if BackgroundFileSystem errored out.
BookDirectoryFiles?.Dispose();
BookDirectoryFiles = newBookDirectoryFiles();
}
}
}
public LongPath? GetPath(string productId) => GetFilePath(productId);
public static async IAsyncEnumerable<FilePathCache.CacheEntry> FindAudiobooksAsync(LongPath searchDirectory, [EnumeratorCancellation] CancellationToken cancellationToken)

View File

@@ -10,8 +10,8 @@ namespace LibationFileManager
{
public partial class Configuration
{
public static string ProcessDirectory { get; } = Path.GetDirectoryName(Exe.FileLocationOnDisk)!;
public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}";
public static string ProcessDirectory { get; } = Path.GetDirectoryName(Environment.ProcessPath)!;
public static string AppDir_Relative => $@".{Path.DirectorySeparatorChar}{LIBATION_FILES_KEY}";
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY));
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
public static string MyMusic => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic), "Libation"));

View File

@@ -43,6 +43,8 @@ public class LibationContributor
GitHubUser("patienttruth"),
GitHubUser("stickystyle"),
GitHubUser("cherez"),
GitHubUser("delebash"),
GitHubUser("twsouthwick"),
]);
private LibationContributor(string name, LibationContributorType type,Uri link)