Merge pull request #1322 from Mbucari/master
Improve Libation's interaction with the file system & other minor fixes
This commit is contained in:
commit
6d0c4a9b3c
@ -126,8 +126,8 @@ namespace AaxDecrypter
|
||||
if (DownloadOptions.SeriesName is string series)
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "SERIES", series);
|
||||
|
||||
if (DownloadOptions.SeriesNumber is float part)
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PART", part.ToString());
|
||||
if (DownloadOptions.SeriesNumber is string part)
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddFreeformTag(tagDomain, "PART", part);
|
||||
}
|
||||
|
||||
OnRetrievedTitle(AaxFile.AppleTags.TitleSansUnabridged);
|
||||
|
||||
@ -43,7 +43,7 @@ namespace AaxDecrypter
|
||||
string? Publisher { get; }
|
||||
string? Language { get; }
|
||||
string? SeriesName { get; }
|
||||
float? SeriesNumber { get; }
|
||||
string? SeriesNumber { get; }
|
||||
NAudio.Lame.LameConfig? LameConfig { get; }
|
||||
bool Downsample { get; }
|
||||
bool MatchSourceBitrate { get; }
|
||||
|
||||
@ -121,7 +121,7 @@ namespace AppScaffolding
|
||||
zipFileSink["Name"] = "File";
|
||||
fileChanged = true;
|
||||
}
|
||||
var hooks = $"{nameof(LibationFileManager)}.{nameof(FileSinkHook)}, {nameof(LibationFileManager)}";
|
||||
var hooks = typeof(FileSinkHook).AssemblyQualifiedName;
|
||||
if (serilog.SelectToken("$.WriteTo[?(@.Name == 'File')].Args", false) is JObject fileSinkArgs
|
||||
&& fileSinkArgs["hooks"]?.Value<string>() != hooks)
|
||||
{
|
||||
@ -158,7 +158,8 @@ namespace AppScaffolding
|
||||
// - with class and method info: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception}";
|
||||
// output example: 2019-11-26 08:48:40.224 -05:00 [DBG] (at LibationWinForms.Program.init()) Begin Libation
|
||||
// {Properties:j} needed for expanded exception logging
|
||||
{ "outputTemplate", "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception} {Properties:j}" }
|
||||
{ "outputTemplate", "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] (at {Caller}) {Message:lj}{NewLine}{Exception} {Properties:j}" },
|
||||
{ "hooks", typeof(FileSinkHook).AssemblyQualifiedName }, // for FileSinkHook
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
#nullable enable
|
||||
namespace DataLayer
|
||||
{
|
||||
// only library importing should use tracking. All else should be NoTracking.
|
||||
@ -24,13 +25,13 @@ namespace DataLayer
|
||||
.Where(c => !c.Book.IsEpisodeParent() || includeParents)
|
||||
.ToList();
|
||||
|
||||
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
|
||||
public static LibraryBook? GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
|
||||
=> context
|
||||
.LibraryBooks
|
||||
.AsNoTrackingWithIdentityResolution()
|
||||
.GetLibraryBook(productId);
|
||||
|
||||
public static LibraryBook GetLibraryBook(this IQueryable<LibraryBook> library, string productId)
|
||||
public static LibraryBook? GetLibraryBook(this IQueryable<LibraryBook> library, string productId)
|
||||
=> library
|
||||
.GetLibrary()
|
||||
.SingleOrDefault(lb => lb.Book.AudibleProductId == productId);
|
||||
|
||||
@ -26,7 +26,7 @@ namespace FileLiberator
|
||||
public string Language => LibraryBook.Book.Language;
|
||||
public string? AudibleProductId => LibraryBookDto.AudibleProductId;
|
||||
public string? SeriesName => LibraryBookDto.FirstSeries?.Name;
|
||||
public float? SeriesNumber => LibraryBookDto.FirstSeries?.Number;
|
||||
public string? SeriesNumber => LibraryBookDto.FirstSeries?.Number;
|
||||
public NAudio.Lame.LameConfig? LameConfig { get; }
|
||||
public string UserAgent => AudibleApi.Resources.Download_User_Agent;
|
||||
public bool StripUnabridged => Config.AllowLibationFixup && Config.StripUnabridged;
|
||||
|
||||
@ -83,7 +83,7 @@ namespace FileLiberator
|
||||
.Select(sb
|
||||
=> new SeriesDto(
|
||||
sb.Series.Name,
|
||||
sb.Book.IsEpisodeParent() ? null : sb.Index,
|
||||
sb.Book.IsEpisodeParent() ? null : sb.Order,
|
||||
sb.Series.AudibleSeriesId)
|
||||
).ToList();
|
||||
}
|
||||
|
||||
74
Source/FileManager/FileSystemTest.cs
Normal file
74
Source/FileManager/FileSystemTest.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public static class FileSystemTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Additional characters which are illegal for filenames in Windows environments.
|
||||
/// Double quotes and slashes are already illegal filename characters on all platforms,
|
||||
/// so they are not included here.
|
||||
/// </summary>
|
||||
public static string AdditionalInvalidWindowsFilenameCharacters { get; } = "<>|:*?";
|
||||
|
||||
/// <summary>
|
||||
/// Test if the directory supports filenames with characters that are invalid on Windows (:, *, ?, <, >, |).
|
||||
/// </summary>
|
||||
public static bool CanWriteWindowsInvalidChars(LongPath directoryName)
|
||||
{
|
||||
var testFile = Path.Combine(directoryName, AdditionalInvalidWindowsFilenameCharacters + Guid.NewGuid().ToString());
|
||||
return CanWriteFile(testFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if the directory supports filenames with 255 unicode characters.
|
||||
/// </summary>
|
||||
public static bool CanWrite255UnicodeChars(LongPath directoryName)
|
||||
{
|
||||
const char unicodeChar = 'ü';
|
||||
var testFileName = new string(unicodeChar, 255);
|
||||
var testFile = Path.Combine(directoryName, testFileName);
|
||||
return CanWriteFile(testFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if a directory has write access by attempting to create an empty file in it.
|
||||
/// <para/>Returns true even if the temporary file can not be deleted.
|
||||
/// </summary>
|
||||
public static bool CanWriteDirectory(LongPath directoryName)
|
||||
{
|
||||
if (!Directory.Exists(directoryName))
|
||||
return false;
|
||||
|
||||
Serilog.Log.Logger.Debug("Testing write permissions for directory: {@DirectoryName}", directoryName);
|
||||
var testFilePath = Path.Combine(directoryName, Guid.NewGuid().ToString());
|
||||
return CanWriteFile(testFilePath);
|
||||
}
|
||||
|
||||
private static bool CanWriteFile(LongPath filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Testing ability to write filename: {@filename}", filename);
|
||||
File.WriteAllBytes(filename, []);
|
||||
Serilog.Log.Logger.Debug("Deleting test file after successful write: {@filename}", filename);
|
||||
try
|
||||
{
|
||||
FileUtility.SaferDelete(filename);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//An error deleting the file doesn't constitute a write failure.
|
||||
Serilog.Log.Logger.Debug(ex, "Error deleting test file: {@filename}", filename);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Debug(ex, "Error writing test file: {@filename}", filename);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,7 +56,7 @@ namespace FileManager
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(name, nameof(name));
|
||||
|
||||
name = ReplacementCharacters.Barebones.ReplaceFilenameChars(name);
|
||||
name = ReplacementCharacters.Barebones(true).ReplaceFilenameChars(name);
|
||||
return Task.Run(() => AddFileInternal(name, contents.Span, comment));
|
||||
}
|
||||
|
||||
|
||||
@ -74,12 +74,14 @@ namespace FileManager
|
||||
}
|
||||
public override int GetHashCode() => Replacements.GetHashCode();
|
||||
|
||||
public static readonly ReplacementCharacters Default
|
||||
= IsWindows
|
||||
? new()
|
||||
{
|
||||
Replacements = new Replacement[]
|
||||
{
|
||||
public static ReplacementCharacters Default(bool ntfs) => ntfs ? HiFi_NTFS : HiFi_Other;
|
||||
public static ReplacementCharacters LoFiDefault(bool ntfs) => ntfs ? LoFi_NTFS : LoFi_Other;
|
||||
public static ReplacementCharacters Barebones(bool ntfs) => ntfs ? BareBones_NTFS : BareBones_Other;
|
||||
|
||||
#region Defaults
|
||||
private static readonly ReplacementCharacters HiFi_NTFS = new()
|
||||
{
|
||||
Replacements = [
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.FilenameForwardSlash("∕"),
|
||||
Replacement.FilenameBackSlash(""),
|
||||
@ -91,28 +93,23 @@ namespace FileManager
|
||||
Replacement.Colon("_"),
|
||||
Replacement.Asterisk("✱"),
|
||||
Replacement.QuestionMark("?"),
|
||||
Replacement.Pipe("⏐"),
|
||||
}
|
||||
}
|
||||
: new()
|
||||
{
|
||||
Replacements = new Replacement[]
|
||||
{
|
||||
Replacement.Pipe("⏐")]
|
||||
};
|
||||
|
||||
private static readonly ReplacementCharacters HiFi_Other = new()
|
||||
{
|
||||
Replacements = [
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.FilenameForwardSlash("∕"),
|
||||
Replacement.FilenameBackSlash("\\"),
|
||||
Replacement.OpenQuote("“"),
|
||||
Replacement.CloseQuote("”"),
|
||||
Replacement.OtherQuote("\"")
|
||||
}
|
||||
};
|
||||
Replacement.OtherQuote("\"")]
|
||||
};
|
||||
|
||||
public static readonly ReplacementCharacters LoFiDefault
|
||||
= IsWindows
|
||||
? new()
|
||||
{
|
||||
Replacements = new Replacement[]
|
||||
{
|
||||
private static readonly ReplacementCharacters LoFi_NTFS = new()
|
||||
{
|
||||
Replacements = [
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.FilenameForwardSlash("_"),
|
||||
Replacement.FilenameBackSlash("_"),
|
||||
@ -121,56 +118,54 @@ namespace FileManager
|
||||
Replacement.OtherQuote("'"),
|
||||
Replacement.OpenAngleBracket("{"),
|
||||
Replacement.CloseAngleBracket("}"),
|
||||
Replacement.Colon("-"),
|
||||
}
|
||||
}
|
||||
: new ()
|
||||
{
|
||||
Replacements = new Replacement[]
|
||||
{
|
||||
Replacement.Colon("-")]
|
||||
};
|
||||
|
||||
private static readonly ReplacementCharacters LoFi_Other = new()
|
||||
{
|
||||
Replacements = [
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.FilenameForwardSlash("_"),
|
||||
Replacement.FilenameBackSlash("\\"),
|
||||
Replacement.OpenQuote("\""),
|
||||
Replacement.CloseQuote("\""),
|
||||
Replacement.OtherQuote("\"")
|
||||
}
|
||||
};
|
||||
Replacement.OtherQuote("\"")]
|
||||
};
|
||||
|
||||
public static readonly ReplacementCharacters Barebones
|
||||
= IsWindows
|
||||
? new ()
|
||||
{
|
||||
Replacements = new Replacement[]
|
||||
{
|
||||
private static readonly ReplacementCharacters BareBones_NTFS = new()
|
||||
{
|
||||
Replacements = [
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.FilenameForwardSlash("_"),
|
||||
Replacement.FilenameBackSlash("_"),
|
||||
Replacement.OpenQuote("_"),
|
||||
Replacement.CloseQuote("_"),
|
||||
Replacement.OtherQuote("_")
|
||||
}
|
||||
}
|
||||
: new ()
|
||||
{
|
||||
Replacements = new Replacement[]
|
||||
{
|
||||
Replacement.OtherQuote("_")]
|
||||
};
|
||||
|
||||
private static readonly ReplacementCharacters BareBones_Other = new()
|
||||
{
|
||||
Replacements = [
|
||||
Replacement.OtherInvalid("_"),
|
||||
Replacement.FilenameForwardSlash("_"),
|
||||
Replacement.FilenameBackSlash("\\"),
|
||||
Replacement.OpenQuote("\""),
|
||||
Replacement.CloseQuote("\""),
|
||||
Replacement.OtherQuote("\"")
|
||||
}
|
||||
};
|
||||
Replacement.OtherQuote("\"")]
|
||||
};
|
||||
#endregion
|
||||
/// <summary>
|
||||
/// Characters to consider invalid in filenames in addition to those returned by <see cref="Path.GetInvalidFileNameChars()"/>
|
||||
/// </summary>
|
||||
public static char[] AdditionalInvalidFilenameCharacters { get; set; } = [];
|
||||
|
||||
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 char[] invalidPathChars { get; } = Path.GetInvalidFileNameChars().Except(new[] {
|
||||
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar
|
||||
}).ToArray();
|
||||
|
||||
private static readonly char[] invalidSlashes = Path.GetInvalidFileNameChars().Intersect(new[] {
|
||||
private static char[] invalidSlashes { get; } = Path.GetInvalidFileNameChars().Intersect(new[] {
|
||||
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar
|
||||
}).ToArray();
|
||||
|
||||
@ -229,8 +224,11 @@ namespace FileManager
|
||||
return DefaultReplacement;
|
||||
}
|
||||
|
||||
private static bool CharIsPathInvalid(char c)
|
||||
=> invalidPathChars.Contains(c) || AdditionalInvalidFilenameCharacters.Contains(c);
|
||||
|
||||
public static bool ContainsInvalidPathChar(string path)
|
||||
=> path.Any(c => invalidPathChars.Contains(c));
|
||||
=> path.Any(CharIsPathInvalid);
|
||||
public static bool ContainsInvalidFilenameChar(string path)
|
||||
=> ContainsInvalidPathChar(path) || path.Any(c => invalidSlashes.Contains(c));
|
||||
|
||||
@ -242,7 +240,7 @@ namespace FileManager
|
||||
{
|
||||
var c = fileName[i];
|
||||
|
||||
if (invalidPathChars.Contains(c)
|
||||
if (CharIsPathInvalid(c)
|
||||
|| invalidSlashes.Contains(c)
|
||||
|| Replacements.Any(r => r.CharacterToReplace == c) /* Replace any other legal characters that they user wants. */ )
|
||||
{
|
||||
@ -267,14 +265,14 @@ namespace FileManager
|
||||
|
||||
if (
|
||||
(
|
||||
invalidPathChars.Contains(c)
|
||||
|| ( // Replace any other legal characters that they user wants.
|
||||
CharIsPathInvalid(c)
|
||||
|| ( // Replace any other legal characters that they user wants.
|
||||
c != Path.DirectorySeparatorChar
|
||||
&& c != Path.AltDirectorySeparatorChar
|
||||
&& Replacements.Any(r => r.CharacterToReplace == c)
|
||||
)
|
||||
)
|
||||
&& !( // replace all colons except drive letter designator on Windows
|
||||
&& !( // replace all colons except drive letter designator on Windows
|
||||
c == ':'
|
||||
&& i == 1
|
||||
&& Path.IsPathRooted(pathStr)
|
||||
@ -282,9 +280,9 @@ namespace FileManager
|
||||
)
|
||||
)
|
||||
{
|
||||
char preceding = i > 0 ? pathStr[i - 1] : default;
|
||||
char succeeding = i < pathStr.Length - 1 ? pathStr[i + 1] : default;
|
||||
builder.Append(GetPathCharReplacement(c, preceding, succeeding));
|
||||
char preceding = i > 0 ? pathStr[i - 1] : default;
|
||||
char succeeding = i < pathStr.Length - 1 ? pathStr[i + 1] : default;
|
||||
builder.Append(GetPathCharReplacement(c, preceding, succeeding));
|
||||
}
|
||||
else
|
||||
builder.Append(c);
|
||||
@ -301,23 +299,21 @@ namespace FileManager
|
||||
|
||||
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 replaceArr = jObj[nameof(Replacement)];
|
||||
var dict
|
||||
= replaceArr?.ToObject<Replacement[]>()?.ToList()
|
||||
?? ReplacementCharacters.Default.Replacements;
|
||||
|
||||
var dict = replaceArr?.ToObject<Replacement[]>()?.ToList() ?? defaults;
|
||||
|
||||
//Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid.
|
||||
//If not, reset to default.
|
||||
|
||||
for (int i = 0; i < Replacement.FIXED_COUNT; i++)
|
||||
{
|
||||
if (dict.Count < Replacement.FIXED_COUNT
|
||||
|| dict[i].CharacterToReplace != ReplacementCharacters.Barebones.Replacements[i].CharacterToReplace
|
||||
|| dict[i].Description != ReplacementCharacters.Barebones.Replacements[i].Description)
|
||||
|| dict[i].CharacterToReplace != defaults[i].CharacterToReplace
|
||||
|| dict[i].Description != defaults[i].Description)
|
||||
{
|
||||
dict = ReplacementCharacters.Default.Replacements;
|
||||
dict = defaults;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@ -111,7 +111,7 @@ namespace LibationAvalonia
|
||||
if (setupDialog.Config.LibationSettingsAreValid)
|
||||
{
|
||||
string? theme = setupDialog.SelectedTheme.Content as string;
|
||||
|
||||
|
||||
setupDialog.Config.SetString(theme, nameof(ThemeVariant));
|
||||
|
||||
await RunMigrationsAsync(setupDialog.Config);
|
||||
@ -120,7 +120,10 @@ namespace LibationAvalonia
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
{
|
||||
e.Cancel = true;
|
||||
await CancelInstallation(setupDialog);
|
||||
}
|
||||
}
|
||||
else if (setupDialog.IsReturningUser)
|
||||
{
|
||||
@ -128,7 +131,8 @@ namespace LibationAvalonia
|
||||
}
|
||||
else
|
||||
{
|
||||
await CancelInstallation();
|
||||
e.Cancel = true;
|
||||
await CancelInstallation(setupDialog);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -139,11 +143,11 @@ namespace LibationAvalonia
|
||||
var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
|
||||
try
|
||||
{
|
||||
await MessageBox.ShowAdminAlert(null, body, title, ex);
|
||||
await MessageBox.ShowAdminAlert(setupDialog, body, title, ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await MessageBox.Show($"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
await MessageBox.Show(setupDialog, $"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -190,6 +194,7 @@ namespace LibationAvalonia
|
||||
{
|
||||
// path did not result in valid settings
|
||||
var continueResult = await MessageBox.Show(
|
||||
libationFilesDialog,
|
||||
$"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
|
||||
"New install?",
|
||||
MessageBoxButtons.YesNo,
|
||||
@ -207,18 +212,18 @@ namespace LibationAvalonia
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
await CancelInstallation(libationFilesDialog);
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
await CancelInstallation(libationFilesDialog);
|
||||
}
|
||||
|
||||
libationFilesDialog.Close();
|
||||
}
|
||||
|
||||
static async Task CancelInstallation()
|
||||
static async Task CancelInstallation(Window window)
|
||||
{
|
||||
await MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
await MessageBox.Show(window, "Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
|
||||
@ -92,13 +92,11 @@ namespace LibationAvalonia.Controls
|
||||
base.UpdateDataValidation(property, state, error);
|
||||
if (property == CommandProperty)
|
||||
{
|
||||
if (state == BindingValueType.BindingError)
|
||||
var canExecure = !state.HasFlag(BindingValueType.HasError);
|
||||
if (canExecure != _commandCanExecute)
|
||||
{
|
||||
if (_commandCanExecute)
|
||||
{
|
||||
_commandCanExecute = false;
|
||||
UpdateIsEffectivelyEnabled();
|
||||
}
|
||||
_commandCanExecute = canExecure;
|
||||
UpdateIsEffectivelyEnabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
MinWidth="500" MinHeight="450"
|
||||
Width="500" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
|
||||
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
|
||||
x:DataType="dialogs:EditReplacementChars"
|
||||
Title="Illegal Character Replacement">
|
||||
|
||||
<Grid
|
||||
@ -23,31 +25,30 @@
|
||||
BeginningEdit="ReplacementGrid_BeginningEdit"
|
||||
CellEditEnding="ReplacementGrid_CellEditEnding"
|
||||
KeyDown="ReplacementGrid_KeyDown"
|
||||
ItemsSource="{Binding replacements}">
|
||||
ItemsSource="{CompiledBinding replacements}">
|
||||
|
||||
<DataGrid.Columns>
|
||||
|
||||
|
||||
<DataGridTemplateColumn Header="Char to
Replace">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox IsReadOnly="{Binding Mandatory}" Text="{Binding CharacterToReplace, Mode=TwoWay}" />
|
||||
<DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
|
||||
<TextBox IsReadOnly="{CompiledBinding Mandatory}" Text="{CompiledBinding CharacterToReplace, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTemplateColumn Header="Replacement
Text">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Text="{Binding ReplacementText, Mode=TwoWay}" />
|
||||
<DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
|
||||
<TextBox Text="{CompiledBinding ReplacementText, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTemplateColumn Width="*" Header="Description">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox IsReadOnly="{Binding Mandatory}" Text="{Binding Description, Mode=TwoWay}" />
|
||||
<DataTemplate x:DataType="dialogs:EditReplacementChars+ReplacementsExt">
|
||||
<TextBox IsReadOnly="{CompiledBinding Mandatory}" Text="{CompiledBinding Description, Mode=TwoWay}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
@ -55,21 +56,31 @@
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<StackPanel
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
RowDefinitions="Auto,Auto"
|
||||
Margin="5"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<Button Margin="0,0,10,0" Command="{Binding Defaults}" Content="Defaults" />
|
||||
<Button Margin="0,0,10,0" Command="{Binding LoFiDefaults}" Content="LoFi Defaults" />
|
||||
<Button Command="{Binding Barebones}" Content="Barebones" />
|
||||
</StackPanel>
|
||||
ColumnDefinitions="Auto,Auto,Auto,Auto">
|
||||
|
||||
<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" />
|
||||
|
||||
<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
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
VerticalAlignment="Bottom"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<Button Margin="0,0,10,0" Command="{Binding Close}" Content="Cancel" />
|
||||
|
||||
@ -13,6 +13,8 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
Configuration config;
|
||||
|
||||
public bool EnvironmentIsWindows => Configuration.IsWindows;
|
||||
|
||||
private readonly List<ReplacementsExt> SOURCE = new();
|
||||
public DataGridCollectionView replacements { get; }
|
||||
public EditReplacementChars()
|
||||
@ -23,7 +25,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
LoadTable(ReplacementCharacters.Default.Replacements);
|
||||
LoadTable(ReplacementCharacters.Default(true).Replacements);
|
||||
}
|
||||
|
||||
DataContext = this;
|
||||
@ -35,12 +37,12 @@ namespace LibationAvalonia.Dialogs
|
||||
LoadTable(config.ReplacementCharacters.Replacements);
|
||||
}
|
||||
|
||||
public void Defaults()
|
||||
=> LoadTable(ReplacementCharacters.Default.Replacements);
|
||||
public void LoFiDefaults()
|
||||
=> LoadTable(ReplacementCharacters.LoFiDefault.Replacements);
|
||||
public void Barebones()
|
||||
=> LoadTable(ReplacementCharacters.Barebones.Replacements);
|
||||
public void Defaults(bool isNtfs)
|
||||
=> LoadTable(ReplacementCharacters.Default(isNtfs).Replacements);
|
||||
public void LoFiDefaults(bool isNtfs)
|
||||
=> LoadTable(ReplacementCharacters.LoFiDefault(isNtfs).Replacements);
|
||||
public void Barebones(bool isNtfs)
|
||||
=> LoadTable(ReplacementCharacters.Barebones(isNtfs).Replacements);
|
||||
|
||||
protected override void SaveAndClose()
|
||||
{
|
||||
|
||||
@ -6,24 +6,28 @@
|
||||
Width="800" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.EditTemplateDialog"
|
||||
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
x:DataType="dialogs:EditTemplateDialog+EditTemplateViewModel"
|
||||
Title="EditTemplateDialog">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<Grid RowDefinitions="Auto,*,Auto" Margin="10">
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
RowDefinitions="Auto,Auto"
|
||||
ColumnDefinitions="*,Auto" Margin="5">
|
||||
ColumnDefinitions="*,Auto"
|
||||
Margin="0,0,0,10">
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Text="{Binding Description}" />
|
||||
Margin="0,0,0,10"
|
||||
Text="{CompiledBinding Description}" />
|
||||
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Name="userEditTbox"
|
||||
Text="{Binding UserTemplateText, Mode=TwoWay}" />
|
||||
Text="{CompiledBinding UserTemplateText, Mode=TwoWay}" />
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
@ -32,9 +36,10 @@
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Center"
|
||||
Content="Reset to Default"
|
||||
Command="{Binding ResetToDefault}"/>
|
||||
Command="{CompiledBinding ResetToDefault}"/>
|
||||
</Grid>
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,*">
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,*"
|
||||
Margin="0,0,0,10">
|
||||
|
||||
<DataGrid
|
||||
Grid.Row="0"
|
||||
@ -44,13 +49,13 @@
|
||||
GridLinesVisibility="All"
|
||||
AutoGenerateColumns="False"
|
||||
DoubleTapped="EditTemplateViewModel_DoubleTapped"
|
||||
ItemsSource="{Binding ListItems}" >
|
||||
ItemsSource="{CompiledBinding ListItems}" >
|
||||
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding Item1}" />
|
||||
<TextBlock Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{CompiledBinding Item1}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
@ -61,7 +66,7 @@
|
||||
<TextBlock
|
||||
Height="18"
|
||||
Margin="10,0,10,0"
|
||||
VerticalAlignment="Center" Text="{Binding Item2}" />
|
||||
VerticalAlignment="Center" Text="{CompiledBinding Item2}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
@ -71,23 +76,22 @@
|
||||
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
Margin="5,0,5,0"
|
||||
Margin="10,0,0,0"
|
||||
RowDefinitions="Auto,*,Auto"
|
||||
HorizontalAlignment="Stretch">
|
||||
|
||||
<TextBlock
|
||||
Margin="5,5,5,10"
|
||||
Margin="0,0,0,5"
|
||||
Text="Example:"/>
|
||||
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Margin="5"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="WrapWithOverflow"
|
||||
Inlines="{Binding Inlines}" />
|
||||
Inlines="{CompiledBinding Inlines}" />
|
||||
|
||||
</Border>
|
||||
|
||||
@ -95,13 +99,17 @@
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
Foreground="Firebrick"
|
||||
Text="{Binding WarningText}"
|
||||
IsVisible="{Binding WarningText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
Text="{CompiledBinding WarningText}"
|
||||
IsVisible="{CompiledBinding WarningText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<controls:LinkLabel
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Center"
|
||||
Text="Read about naming templates on the Wiki"
|
||||
Command="{Binding GoToNamingTemplateWiki}" />
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
Padding="30,5,30,5"
|
||||
HorizontalAlignment="Right"
|
||||
Content="Save"
|
||||
|
||||
@ -70,7 +70,7 @@ public partial class EditTemplateDialog : DialogWindow
|
||||
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await SaveAndCloseAsync();
|
||||
|
||||
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
||||
internal class EditTemplateViewModel : ViewModels.ViewModelBase
|
||||
{
|
||||
private readonly Configuration config;
|
||||
public InlineCollection Inlines { get; } = new();
|
||||
@ -96,6 +96,9 @@ public partial class EditTemplateDialog : DialogWindow
|
||||
|
||||
}
|
||||
|
||||
public void GoToNamingTemplateWiki()
|
||||
=> Go.To.Url(@"ht" + "tps://github.com/rmcrackan/Libation/blob/master/Documentation/NamingTemplates.md");
|
||||
|
||||
// hold the work-in-progress value. not guaranteed to be valid
|
||||
private string _userTemplateText;
|
||||
public string UserTemplateText
|
||||
|
||||
@ -13,11 +13,12 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class LocateAudiobooksDialog : DialogWindow
|
||||
{
|
||||
private event EventHandler<FilePathCache.CacheEntry> FileFound;
|
||||
private event EventHandler<FilePathCache.CacheEntry>? FileFound;
|
||||
private readonly CancellationTokenSource tokenSource = new();
|
||||
private readonly List<string> foundAsins = new();
|
||||
private readonly LocatedAudiobooksViewModel _viewModel;
|
||||
@ -41,7 +42,7 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
private void LocateAudiobooksDialog_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
private void LocateAudiobooksDialog_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
tokenSource.Cancel();
|
||||
//If this dialog is closed before it's completed, Closing is fired
|
||||
@ -50,7 +51,7 @@ namespace LibationAvalonia.Dialogs
|
||||
this.SaveSizeAndLocation(Configuration.Instance);
|
||||
}
|
||||
|
||||
private void LocateAudiobooks_FileFound(object sender, FilePathCache.CacheEntry e)
|
||||
private void LocateAudiobooks_FileFound(object? sender, FilePathCache.CacheEntry e)
|
||||
{
|
||||
var newItem = new Tuple<string, string>($"[{e.Id}]", Path.GetFileName(e.Path));
|
||||
_viewModel.FoundFiles.Add(newItem);
|
||||
@ -63,13 +64,13 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
private async void LocateAudiobooksDialog_Opened(object sender, EventArgs e)
|
||||
private async void LocateAudiobooksDialog_Opened(object? sender, EventArgs e)
|
||||
{
|
||||
var folderPicker = new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Select the folder to search for audiobooks",
|
||||
AllowMultiple = false,
|
||||
SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix)
|
||||
SuggestedStartLocation = await StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books?.PathWithoutPrefix ?? "")
|
||||
};
|
||||
|
||||
var selectedFolder = (await StorageProvider.OpenFolderPickerAsync(folderPicker))?.SingleOrDefault()?.TryGetLocalPath();
|
||||
@ -89,11 +90,13 @@ namespace LibationAvalonia.Dialogs
|
||||
FilePathCache.Insert(book);
|
||||
|
||||
var lb = context.GetLibraryBook_Flat_NoTracking(book.Id);
|
||||
if (lb?.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
|
||||
if (lb is not null && lb.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
|
||||
await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated));
|
||||
|
||||
tokenSource.Token.ThrowIfCancellationRequested();
|
||||
FileFound?.Invoke(this, book);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book);
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
using FileManager;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.Forms;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
@ -39,6 +41,21 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
|
||||
public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await SaveAndCloseAsync();
|
||||
{
|
||||
LongPath lonNewBooks = settingsDisp.ImportantSettings.GetBooksDirectory();
|
||||
if (!System.IO.Directory.Exists(lonNewBooks))
|
||||
{
|
||||
try
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(lonNewBooks);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await MessageBox.Show(this, $"Error creating Books Location:\n\n{ex.Message}", "Error creating directory", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await SaveAndCloseAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ namespace LibationAvalonia.ViewModels
|
||||
private bool _removeButtonsVisible = Design.IsDesignMode;
|
||||
private int _numAccountsScanning = 2;
|
||||
private int _accountsCount = 0;
|
||||
public string LocateAudiobooksTip => Configuration.GetHelpText("LocateAudiobooks");
|
||||
|
||||
/// <summary> Auto scanning accounts is enables </summary>
|
||||
public bool AutoScanChecked { get => _autoScanChecked; set => Configuration.Instance.AutoScan = this.RaiseAndSetIfChanged(ref _autoScanChecked, value); }
|
||||
@ -68,7 +69,8 @@ namespace LibationAvalonia.ViewModels
|
||||
MainWindow.Loaded += (_, _) =>
|
||||
{
|
||||
refreshImportMenu();
|
||||
AccountsSettingsPersister.Saved += refreshImportMenu;
|
||||
AccountsSettingsPersister.Saved += (_, _)
|
||||
=> Avalonia.Threading.Dispatcher.UIThread.Invoke(refreshImportMenu);
|
||||
};
|
||||
|
||||
AutoScanChecked = Configuration.Instance.AutoScan;
|
||||
@ -172,8 +174,19 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
public async Task LocateAudiobooksAsync()
|
||||
{
|
||||
var locateDialog = new LibationAvalonia.Dialogs.LocateAudiobooksDialog();
|
||||
await locateDialog.ShowDialog(MainWindow);
|
||||
var result = await MessageBox.Show(
|
||||
MainWindow,
|
||||
Configuration.GetHelpText(nameof(LibationAvalonia.Dialogs.LocateAudiobooksDialog)),
|
||||
"Locate Previously-Liberated Audiobook Files",
|
||||
MessageBoxButtons.OKCancel,
|
||||
MessageBoxIcon.Information,
|
||||
MessageBoxDefaultButton.Button1);
|
||||
|
||||
if (result is DialogResult.OK)
|
||||
{
|
||||
var locateDialog = new LibationAvalonia.Dialogs.LocateAudiobooksDialog();
|
||||
await locateDialog.ShowDialog(MainWindow);
|
||||
}
|
||||
}
|
||||
|
||||
private void setyNumScanningAccounts(int numScanning)
|
||||
@ -222,7 +235,7 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshImportMenu(object? _ = null, EventArgs? __ = null)
|
||||
private void refreshImportMenu()
|
||||
{
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
AccountsCount = persister.AccountsSettings.Accounts.Count;
|
||||
|
||||
@ -35,11 +35,8 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
}
|
||||
|
||||
public void SaveSettings(Configuration config)
|
||||
{
|
||||
LongPath lonNewBooks = Configuration.GetKnownDirectory(BooksDirectory) is Configuration.KnownDirectories.None ? BooksDirectory : System.IO.Path.Combine(BooksDirectory, "Books");
|
||||
if (!System.IO.Directory.Exists(lonNewBooks))
|
||||
System.IO.Directory.CreateDirectory(lonNewBooks);
|
||||
config.Books = lonNewBooks;
|
||||
{
|
||||
config.Books = GetBooksDirectory();
|
||||
config.SavePodcastsToParentFolder = SavePodcastsToParentFolder;
|
||||
config.OverwriteExisting = OverwriteExisting;
|
||||
config.CreationTime = CreationTime.Value;
|
||||
@ -47,6 +44,9 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
config.LogLevel = LoggingLevel;
|
||||
}
|
||||
|
||||
public LongPath GetBooksDirectory()
|
||||
=> Configuration.GetKnownDirectory(BooksDirectory) is Configuration.KnownDirectories.None ? BooksDirectory : System.IO.Path.Combine(BooksDirectory, "Books");
|
||||
|
||||
private static float scaleFactorToLinearRange(float scaleFactor)
|
||||
=> float.Round(100 * MathF.Log2(scaleFactor));
|
||||
private static float linearRangeToScaleFactor(float value)
|
||||
|
||||
@ -110,7 +110,7 @@
|
||||
<MenuItem IsVisible="{CompiledBinding MultipleAccounts}" IsEnabled="{CompiledBinding RemoveMenuItemsEnabled}" Command="{CompiledBinding RemoveBooksSomeAsync}" Header="_Remove Books from Some Accounts" />
|
||||
|
||||
<Separator />
|
||||
<MenuItem Command="{CompiledBinding LocateAudiobooksAsync}" Header="L_ocate Audiobooks..." />
|
||||
<MenuItem Command="{CompiledBinding LocateAudiobooksAsync}" Header="L_ocate Audiobooks..." ToolTip.Tip="{CompiledBinding LocateAudiobooksTip}" />
|
||||
|
||||
</MenuItem>
|
||||
|
||||
|
||||
@ -43,6 +43,22 @@ namespace LibationAvalonia.Views
|
||||
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ShowAccountsAsync), Gesture = new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Shift) });
|
||||
KeyBindings.Add(new KeyBinding { Command = ReactiveCommand.Create(ViewModel.ExportLibraryAsync), Gesture = new KeyGesture(Key.S, KeyModifiers.Control) });
|
||||
}
|
||||
|
||||
Configuration.Instance.PropertyChanged += Settings_PropertyChanged;
|
||||
Settings_PropertyChanged(this, null);
|
||||
}
|
||||
|
||||
[Dinah.Core.PropertyChangeFilter(nameof(Configuration.Books))]
|
||||
private void Settings_PropertyChanged(object sender, Dinah.Core.PropertyChangedEventArgsEx e)
|
||||
{
|
||||
if (!Configuration.IsWindows)
|
||||
{
|
||||
//The books directory does not support filenames with windows' invalid characters.
|
||||
//Tell the ReplacementCharacters configuration to treat those characters as invalid.
|
||||
ReplacementCharacters.AdditionalInvalidFilenameCharacters
|
||||
= Configuration.Instance.BooksCanWriteWindowsInvalidChars ? []
|
||||
: FileSystemTest.AdditionalInvalidWindowsFilenameCharacters.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private void AudibleApiStorage_LoadError(object sender, AccountSettingsLoadErrorEventArgs e)
|
||||
@ -54,7 +70,7 @@ namespace LibationAvalonia.Views
|
||||
FileUtility.SaferMoveToValidPath(
|
||||
e.SettingsFilePath,
|
||||
e.SettingsFilePath,
|
||||
ReplacementCharacters.Barebones,
|
||||
Configuration.Instance.ReplacementCharacters,
|
||||
"bak");
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
e.Handled = true;
|
||||
@ -103,6 +119,20 @@ namespace LibationAvalonia.Views
|
||||
|
||||
private async void MainWindow_Opened(object sender, EventArgs e)
|
||||
{
|
||||
if (AudibleFileStorage.BooksDirectory is null)
|
||||
{
|
||||
var result = await MessageBox.Show(
|
||||
this,
|
||||
"Please set a valid Books location in the settings dialog.",
|
||||
"Books Directory Not Set",
|
||||
MessageBoxButtons.OKCancel,
|
||||
MessageBoxIcon.Warning,
|
||||
MessageBoxDefaultButton.Button1);
|
||||
|
||||
if (result is DialogResult.OK)
|
||||
await new SettingsDialog().ShowDialog(this);
|
||||
}
|
||||
|
||||
if (Configuration.Instance.FirstLaunch)
|
||||
{
|
||||
var result = await MessageBox.Show(this, "Would you like a guided tour to get started?", "Libation Walkthrough", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1);
|
||||
@ -115,7 +145,7 @@ namespace LibationAvalonia.Views
|
||||
Configuration.Instance.FirstLaunch = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
productsDisplay?.CloseImageDisplay();
|
||||
|
||||
@ -5,14 +5,15 @@ using DataLayer;
|
||||
using LibationUiBase;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.Views
|
||||
{
|
||||
public delegate void QueueItemPositionButtonClicked(ProcessBookViewModel item, QueuePosition queueButton);
|
||||
public delegate void QueueItemCancelButtonClicked(ProcessBookViewModel item);
|
||||
public delegate void QueueItemPositionButtonClicked(ProcessBookViewModel? item, QueuePosition queueButton);
|
||||
public delegate void QueueItemCancelButtonClicked(ProcessBookViewModel? item);
|
||||
public partial class ProcessBookControl : UserControl
|
||||
{
|
||||
public static event QueueItemPositionButtonClicked PositionButtonClicked;
|
||||
public static event QueueItemCancelButtonClicked CancelButtonClicked;
|
||||
public static event QueueItemPositionButtonClicked? PositionButtonClicked;
|
||||
public static event QueueItemCancelButtonClicked? CancelButtonClicked;
|
||||
|
||||
public static readonly StyledProperty<ProcessBookStatus> ProcessBookStatusProperty =
|
||||
AvaloniaProperty.Register<ProcessBookControl, ProcessBookStatus>(nameof(ProcessBookStatus), enableDataValidation: true);
|
||||
@ -31,12 +32,13 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
ViewModels.MainVM.Configure_NonUI();
|
||||
DataContext = new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"));
|
||||
if (context.GetLibraryBook_Flat_NoTracking("B017V4IM1G") is LibraryBook book)
|
||||
DataContext = new ProcessBookViewModel(book);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private ProcessBookViewModel DataItem => DataContext is null ? null : DataContext as ProcessBookViewModel;
|
||||
private ProcessBookViewModel? DataItem => DataContext as ProcessBookViewModel;
|
||||
|
||||
public void Cancel_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> CancelButtonClicked?.Invoke(DataItem);
|
||||
|
||||
@ -34,44 +34,51 @@ namespace LibationAvalonia.Views
|
||||
var vm = new ProcessQueueViewModel();
|
||||
DataContext = vm;
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
|
||||
var trialBook = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G") ?? context.GetLibrary_Flat_NoTracking().FirstOrDefault();
|
||||
if (trialBook is null)
|
||||
return;
|
||||
|
||||
|
||||
List<ProcessBookViewModel> testList = new()
|
||||
{
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"))
|
||||
new ProcessBookViewModel(trialBook)
|
||||
{
|
||||
Result = ProcessBookResult.FailedAbort,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"))
|
||||
new ProcessBookViewModel(trialBook)
|
||||
{
|
||||
Result = ProcessBookResult.FailedSkip,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"))
|
||||
new ProcessBookViewModel(trialBook)
|
||||
{
|
||||
Result = ProcessBookResult.FailedRetry,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"))
|
||||
new ProcessBookViewModel(trialBook)
|
||||
{
|
||||
Result = ProcessBookResult.ValidationFail,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"))
|
||||
new ProcessBookViewModel(trialBook)
|
||||
{
|
||||
Result = ProcessBookResult.Cancelled,
|
||||
Status = ProcessBookStatus.Cancelled,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"))
|
||||
new ProcessBookViewModel(trialBook)
|
||||
{
|
||||
Result = ProcessBookResult.Success,
|
||||
Status = ProcessBookStatus.Completed,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"))
|
||||
new ProcessBookViewModel(trialBook)
|
||||
{
|
||||
Result = ProcessBookResult.None,
|
||||
Status = ProcessBookStatus.Working,
|
||||
},
|
||||
new ProcessBookViewModel(context.GetLibraryBook_Flat_NoTracking("B017V4IM1G"))
|
||||
new ProcessBookViewModel(trialBook)
|
||||
{
|
||||
Result = ProcessBookResult.None,
|
||||
Status = ProcessBookStatus.Queued,
|
||||
@ -99,7 +106,7 @@ namespace LibationAvalonia.Views
|
||||
|
||||
#region Control event handlers
|
||||
|
||||
private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel item)
|
||||
private async void ProcessBookControl2_CancelButtonClicked(ProcessBookViewModel? item)
|
||||
{
|
||||
if (item is not null)
|
||||
{
|
||||
@ -108,19 +115,20 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessBookControl2_ButtonClicked(ProcessBookViewModel item, QueuePosition queueButton)
|
||||
private void ProcessBookControl2_ButtonClicked(ProcessBookViewModel? item, QueuePosition queueButton)
|
||||
{
|
||||
Queue?.MoveQueuePosition(item, queueButton);
|
||||
if (item is not null)
|
||||
Queue?.MoveQueuePosition(item, queueButton);
|
||||
}
|
||||
|
||||
public async void CancelAllBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
public async void CancelAllBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
Queue?.ClearQueue();
|
||||
if (Queue?.Current is not null)
|
||||
await Queue.Current.CancelAsync();
|
||||
}
|
||||
|
||||
public void ClearFinishedBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
public void ClearFinishedBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
Queue?.ClearCompleted();
|
||||
|
||||
@ -128,12 +136,12 @@ namespace LibationAvalonia.Views
|
||||
_viewModel.RunningTime = string.Empty;
|
||||
}
|
||||
|
||||
public void ClearLogBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
public void ClearLogBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
_viewModel?.LogEntries.Clear();
|
||||
}
|
||||
|
||||
private async void LogCopyBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
private async void LogCopyBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel is ProcessQueueViewModel vm)
|
||||
{
|
||||
@ -143,14 +151,14 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
}
|
||||
|
||||
private async void cancelAllBtn_Click(object sender, EventArgs e)
|
||||
private async void cancelAllBtn_Click(object? sender, EventArgs e)
|
||||
{
|
||||
Queue?.ClearQueue();
|
||||
if (Queue?.Current is not null)
|
||||
await Queue.Current.CancelAsync();
|
||||
}
|
||||
|
||||
private void btnClearFinished_Click(object sender, EventArgs e)
|
||||
private void btnClearFinished_Click(object? sender, EventArgs e)
|
||||
{
|
||||
Queue?.ClearCompleted();
|
||||
|
||||
|
||||
@ -62,25 +62,22 @@ namespace LibationAvalonia.Views
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
List<LibraryBook> sampleEntries;
|
||||
LibraryBook?[] sampleEntries;
|
||||
try
|
||||
{
|
||||
sampleEntries = new()
|
||||
{
|
||||
//context.GetLibraryBook_Flat_NoTracking("B00DCD0OXU"),try{
|
||||
sampleEntries = [
|
||||
context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6"),
|
||||
context.GetLibraryBook_Flat_NoTracking("B017V4IWVG"),
|
||||
context.GetLibraryBook_Flat_NoTracking("B017V4JA2Q"),
|
||||
context.GetLibraryBook_Flat_NoTracking("B017V4NUPO"),
|
||||
context.GetLibraryBook_Flat_NoTracking("B017V4NMX4"),
|
||||
context.GetLibraryBook_Flat_NoTracking("B017V4NOZ0"),
|
||||
context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")
|
||||
};
|
||||
context.GetLibraryBook_Flat_NoTracking("B017WJ5ZK6")];
|
||||
}
|
||||
catch { sampleEntries = new(); }
|
||||
catch { sampleEntries = []; }
|
||||
|
||||
var pdvm = new ProductsDisplayViewModel();
|
||||
_ = pdvm.BindToGridAsync(sampleEntries);
|
||||
_ = pdvm.BindToGridAsync(sampleEntries.OfType<LibraryBook>().ToList());
|
||||
DataContext = pdvm;
|
||||
|
||||
setGridScale(1);
|
||||
|
||||
@ -5,6 +5,7 @@ using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.StepRunner;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using LibationAvalonia.Views;
|
||||
@ -164,8 +165,9 @@ namespace LibationAvalonia
|
||||
{
|
||||
//if we imported new books, wait for the grid to update before proceeding.
|
||||
if (newCount > 0)
|
||||
MainForm.ViewModel.ProductsDisplay.VisibleCountChanged += productsDisplay_VisibleCountChanged;
|
||||
else
|
||||
Avalonia.Threading.Dispatcher.UIThread.Invoke(() =>
|
||||
MainForm.ViewModel.ProductsDisplay.VisibleCountChanged += productsDisplay_VisibleCountChanged);
|
||||
else
|
||||
tcs.SetResult();
|
||||
}
|
||||
void productsDisplay_VisibleCountChanged(object sender, int e) => tcs.SetResult();
|
||||
@ -176,7 +178,7 @@ namespace LibationAvalonia
|
||||
var books = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
if (books.Count == 0) return true;
|
||||
|
||||
var firstAuthor = getFirstAuthor();
|
||||
var firstAuthor = getFirstAuthor()?.SurroundWithQuotes();
|
||||
if (firstAuthor == null) return true;
|
||||
|
||||
if (!await ProceedMessageBox("You can filter the grid entries by searching", "Searching"))
|
||||
@ -193,7 +195,7 @@ namespace LibationAvalonia
|
||||
|
||||
await displayControlAsync(MainForm.filterBtn);
|
||||
|
||||
MainForm.filterBtn.Command.Execute(null);
|
||||
MainForm.filterBtn.Command.Execute(firstAuthor);
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
@ -209,8 +211,7 @@ namespace LibationAvalonia
|
||||
|
||||
private async Task<bool> ShowQuickFilters()
|
||||
{
|
||||
var firstAuthor = getFirstAuthor();
|
||||
|
||||
var firstAuthor = getFirstAuthor()?.SurroundWithQuotes();
|
||||
if (firstAuthor == null) return true;
|
||||
|
||||
if (!await ProceedMessageBox("Queries that you perform regularly can be added to 'Quick Filters'", "Quick Filters"))
|
||||
@ -222,7 +223,7 @@ namespace LibationAvalonia
|
||||
|
||||
await Task.Delay(750);
|
||||
await displayControlAsync(MainForm.addQuickFilterBtn);
|
||||
MainForm.addQuickFilterBtn.Command.Execute(null);
|
||||
MainForm.addQuickFilterBtn.Command.Execute(firstAuthor);
|
||||
await displayControlAsync(MainForm.quickFiltersToolStripMenuItem);
|
||||
await displayControlAsync(editQuickFiltersToolStripMenuItem);
|
||||
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
using CommandLine;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationCli
|
||||
@ -6,6 +8,15 @@ namespace LibationCli
|
||||
[Verb("convert", HelpText = "Convert mp4 to mp3.")]
|
||||
public class ConvertOptions : ProcessableOptionsBase
|
||||
{
|
||||
protected override Task ProcessAsync() => RunAsync(CreateProcessable<FileLiberator.ConvertToMp3>());
|
||||
protected override Task ProcessAsync()
|
||||
{
|
||||
if (AudibleFileStorage.BooksDirectory is null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Books directory is not set. Please configure the 'Books' setting in Settings.json.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return RunAsync(CreateProcessable<FileLiberator.ConvertToMp3>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
using CommandLine;
|
||||
using DataLayer;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationCli
|
||||
@ -13,9 +15,17 @@ namespace LibationCli
|
||||
public bool PdfOnly { get; set; }
|
||||
|
||||
protected override Task ProcessAsync()
|
||||
=> PdfOnly
|
||||
{
|
||||
if (AudibleFileStorage.BooksDirectory is null)
|
||||
{
|
||||
Console.Error.WriteLine("Error: Books directory is not set. Please configure the 'Books' setting in Settings.json.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return PdfOnly
|
||||
? RunAsync(CreateProcessable<DownloadPdf>())
|
||||
: RunAsync(CreateBackupBook());
|
||||
}
|
||||
|
||||
private static Processable CreateBackupBook()
|
||||
{
|
||||
|
||||
@ -45,13 +45,24 @@ namespace LibationFileManager
|
||||
|
||||
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
|
||||
|
||||
public static LongPath BooksDirectory
|
||||
/// <summary>
|
||||
/// The fully-qualified Books durectory path if the directory exists, otherwise null.
|
||||
/// </summary>
|
||||
public static LongPath? BooksDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
|
||||
Configuration.Instance.Books = Configuration.DefaultBooksDirectory;
|
||||
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
|
||||
return null;
|
||||
try
|
||||
{
|
||||
return Directory.CreateDirectory(Configuration.Instance.Books)?.FullName;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error creating Books directory: {@BooksDirectory}", Configuration.Instance.Books);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
@ -129,8 +140,9 @@ namespace LibationFileManager
|
||||
protected override LongPath? GetFilePathCustom(string productId)
|
||||
=> GetFilePathsCustom(productId).FirstOrDefault();
|
||||
|
||||
private static BackgroundFileSystem newBookDirectoryFiles()
|
||||
=> new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||
private static BackgroundFileSystem? newBookDirectoryFiles()
|
||||
=> BooksDirectory is LongPath books ? new BackgroundFileSystem(books, "*.*", SearchOption.AllDirectories)
|
||||
: null;
|
||||
|
||||
protected override List<LongPath> GetFilePathsCustom(string productId)
|
||||
{
|
||||
@ -140,6 +152,7 @@ namespace LibationFileManager
|
||||
BookDirectoryFiles = newBookDirectoryFiles();
|
||||
|
||||
var regex = GetBookSearchRegex(productId);
|
||||
var diskFiles = BookDirectoryFiles?.FindFiles(regex) ?? [];
|
||||
|
||||
//Find all extant files matching the productId
|
||||
//using both the file system and the file path cache
|
||||
@ -148,17 +161,17 @@ namespace LibationFileManager
|
||||
.GetFiles(productId)
|
||||
.Where(c => c.fileType == FileType.Audio && File.Exists(c.path))
|
||||
.Select(c => c.path)
|
||||
.Union(BookDirectoryFiles.FindFiles(regex))
|
||||
.Union(diskFiles)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (BookDirectoryFiles is null)
|
||||
if (BookDirectoryFiles is null && BooksDirectory is not null)
|
||||
lock (bookDirectoryFilesLocker)
|
||||
BookDirectoryFiles = newBookDirectoryFiles();
|
||||
else
|
||||
BookDirectoryFiles?.RefreshFiles();
|
||||
|
||||
BookDirectoryFiles?.RefreshFiles();
|
||||
}
|
||||
|
||||
public LongPath? GetPath(string productId) => GetFilePath(productId);
|
||||
|
||||
@ -107,8 +107,27 @@ namespace LibationFileManager
|
||||
don't have a spatial audio version will be download
|
||||
as usual based on your other file quality settings.
|
||||
""" },
|
||||
}
|
||||
.AsReadOnly();
|
||||
{"LocateAudiobooks","""
|
||||
Scan the contents a folder to find audio files that
|
||||
match books in Libation's database. This is useful
|
||||
if you moved your Books folder or re-installed
|
||||
Libation and want it to be able to find your
|
||||
already downloaded audiobooks.
|
||||
|
||||
Prerequisite: An audiobook must already exist in
|
||||
Libation's database (through an Audible account
|
||||
scan) for a matching audio file to be found.
|
||||
""" },
|
||||
{"LocateAudiobooksDialog","""
|
||||
Libation will search all .m4b and .mp3 files in a folder, looking for audio files belonging to library books in Libation's database.
|
||||
|
||||
If an audiobook file is found that matches one of Libation's library books, Libation will mark that book as "Liberated" (green stoplight).
|
||||
|
||||
For an audio file to be identified, Libation must have that library book in its database. If you're on a fresh installation of Libation, be sure to add and scan all of your Audible accounts before running this action.
|
||||
|
||||
This may take a while, depending on the number of audio files in the folder and the speed of your storage device.
|
||||
""" }
|
||||
}.AsReadOnly();
|
||||
|
||||
public static string GetHelpText(string? settingName)
|
||||
=> settingName != null && HelpText.TryGetValue(settingName, out var value) ? value : "";
|
||||
|
||||
@ -84,7 +84,7 @@ namespace LibationFileManager
|
||||
ProcessDirectory,
|
||||
LocalAppData,
|
||||
UserProfile,
|
||||
Path.Combine(Path.GetTempPath(), "Libation")
|
||||
WinTemp,
|
||||
};
|
||||
|
||||
//Try to find and validate appsettings.json in each folder
|
||||
@ -181,7 +181,7 @@ namespace LibationFileManager
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Serilog.Log.Error(e, "Failed to run shell command. {Arguments}", psi.ArgumentList);
|
||||
Serilog.Log.Error(e, "Failed to run shell command. {@Arguments}", psi.ArgumentList);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ namespace LibationFileManager
|
||||
//https://github.com/serilog/serilog-settings-configuration/issues/406
|
||||
var readerOptions = new ConfigurationReaderOptions(
|
||||
typeof(ILogger).Assembly, // Serilog
|
||||
typeof(LoggerCallerEnrichmentConfiguration).Assembly, // Dinah.Core
|
||||
typeof(LoggerEnrichmentConfigurationExtensions).Assembly, // Serilog.Exceptions
|
||||
typeof(ConsoleLoggerConfigurationExtensions).Assembly, // Serilog.Sinks.Console
|
||||
typeof(FileLoggerConfigurationExtensions).Assembly); // Serilog.Sinks.File
|
||||
|
||||
@ -36,12 +36,12 @@ namespace LibationFileManager
|
||||
|
||||
[return: NotNullIfNotNull(nameof(defaultValue))]
|
||||
public T? GetNonString<T>(T defaultValue, [CallerMemberName] string propertyName = "")
|
||||
=> Settings.GetNonString(propertyName, defaultValue);
|
||||
=> Settings is null ? default : Settings.GetNonString(propertyName, defaultValue);
|
||||
|
||||
|
||||
[return: NotNullIfNotNull(nameof(defaultValue))]
|
||||
public string? GetString(string? defaultValue = null, [CallerMemberName] string propertyName = "")
|
||||
=> Settings.GetString(propertyName, defaultValue);
|
||||
=> Settings?.GetString(propertyName, defaultValue);
|
||||
|
||||
public object? GetObject([CallerMemberName] string propertyName = "") => Settings.GetObject(propertyName);
|
||||
|
||||
@ -111,7 +111,34 @@ namespace LibationFileManager
|
||||
public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Location for book storage. Includes destination of newly liberated books")]
|
||||
public LongPath? Books { get => GetString(); set => SetString(value); }
|
||||
public LongPath? Books {
|
||||
get => GetString();
|
||||
set
|
||||
{
|
||||
if (value != Books)
|
||||
{
|
||||
OnPropertyChanging(nameof(Books), Books, value);
|
||||
Settings.SetString(nameof(Books), value);
|
||||
m_BooksCanWrite255UnicodeChars = null;
|
||||
m_BooksCanWriteWindowsInvalidChars = null;
|
||||
OnPropertyChanged(nameof(Books), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool? m_BooksCanWrite255UnicodeChars;
|
||||
private bool? m_BooksCanWriteWindowsInvalidChars;
|
||||
/// <summary>
|
||||
/// True if the Books directory can be written to with 255 unicode character filenames
|
||||
/// <para/> Does not persist. Check and set this value at runtime and whenever Books is changed.
|
||||
/// </summary>
|
||||
public bool BooksCanWrite255UnicodeChars => m_BooksCanWrite255UnicodeChars ??= FileSystemTest.CanWrite255UnicodeChars(AudibleFileStorage.BooksDirectory);
|
||||
/// <summary>
|
||||
/// True if the Books directory can be written to with filenames containing characters invalid on Windows (:, *, ?, <, >, |)
|
||||
/// <para/> Always false on Windows platforms.
|
||||
/// <para/> Does not persist. Check and set this value at runtime and whenever Books is changed.
|
||||
/// </summary>
|
||||
public bool BooksCanWriteWindowsInvalidChars => !IsWindows && (m_BooksCanWriteWindowsInvalidChars ??= FileSystemTest.CanWriteWindowsInvalidChars(AudibleFileStorage.BooksDirectory));
|
||||
|
||||
[Description("Overwrite existing files if they already exist?")]
|
||||
public bool OverwriteExisting { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
@ -319,7 +346,7 @@ namespace LibationFileManager
|
||||
#region templates: custom file naming
|
||||
|
||||
[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")]
|
||||
public string FolderTemplate
|
||||
|
||||
@ -3,48 +3,58 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration : PropertyChangeFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if <see cref="SettingsFilePath"/> exists and the <see cref="Books"/> property has a non-null, non-empty value.
|
||||
/// Does not verify the existence of the <see cref="Books"/> directory.
|
||||
/// </summary>
|
||||
public bool LibationSettingsAreValid => SettingsFileIsValid(SettingsFilePath);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if <paramref name="settingsFile"/> exists and the <see cref="Books"/> property has a non-null, non-empty value.
|
||||
/// Does not verify the existence of the <see cref="Books"/> directory.
|
||||
/// </summary>
|
||||
/// <param name="settingsFile">File path to the settings JSON file</param>
|
||||
public static bool SettingsFileIsValid(string settingsFile)
|
||||
{
|
||||
if (!Directory.Exists(Path.GetDirectoryName(settingsFile)) || !File.Exists(settingsFile))
|
||||
return false;
|
||||
|
||||
var pDic = new PersistentDictionary(settingsFile, isReadOnly: false);
|
||||
|
||||
if (pDic.GetString(nameof(Books)) is not string booksDir)
|
||||
return false;
|
||||
|
||||
if (!Directory.Exists(booksDir))
|
||||
try
|
||||
{
|
||||
if (Path.GetDirectoryName(settingsFile) is not string dir)
|
||||
throw new DirectoryNotFoundException(settingsFile);
|
||||
|
||||
//"Books" is not null, so setup has already been run.
|
||||
//Since Books can't be found, try to create it
|
||||
//and then revert to the default books directory
|
||||
foreach (string d in new string[] { booksDir, DefaultBooksDirectory })
|
||||
var settingsJson = JObject.Parse(File.ReadAllText(settingsFile));
|
||||
return !string.IsNullOrWhiteSpace(settingsJson[nameof(Books)]?.Value<string>());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Failed to load settings file: {@SettingsFile}", settingsFile);
|
||||
try
|
||||
{
|
||||
Serilog.Log.Logger.Information("Deleting invalid settings file: {@SettingsFile}", settingsFile);
|
||||
FileUtility.SaferDelete(settingsFile);
|
||||
Serilog.Log.Logger.Information("Creating a new, empty setting file: {@SettingsFile}", settingsFile);
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(d);
|
||||
|
||||
pDic.SetString(nameof(Books), d);
|
||||
|
||||
return Directory.Exists(d);
|
||||
File.WriteAllText(settingsFile, "{}");
|
||||
}
|
||||
catch (Exception createEx)
|
||||
{
|
||||
Serilog.Log.Logger.Error(createEx, "Failed to create new settings file: {@SettingsFile}", settingsFile);
|
||||
}
|
||||
catch { /* Do Nothing */ }
|
||||
}
|
||||
catch (Exception deleteEx)
|
||||
{
|
||||
Serilog.Log.Logger.Error(deleteEx, "Failed to delete the invalid settings file: {@SettingsFile}", settingsFile);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#region singleton stuff
|
||||
|
||||
@ -7,9 +7,9 @@ public record SeriesDto : IFormattable
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
public float? Number { get; }
|
||||
public string? Number { get; }
|
||||
public string AudibleSeriesId { get; }
|
||||
public SeriesDto(string name, float? number, string audibleSeriesId)
|
||||
public SeriesDto(string name, string? number, string audibleSeriesId)
|
||||
{
|
||||
Name = name;
|
||||
Number = number;
|
||||
|
||||
@ -68,7 +68,7 @@ namespace LibationFileManager.Templates
|
||||
YearPublished = 2017,
|
||||
Authors = [new("Arthur Conan Doyle", "B000AQ43GQ"), new("Stephen Fry - introductions", "B000APAGVS")],
|
||||
Narrators = [new("Stephen Fry", null)],
|
||||
Series = [new("Sherlock Holmes", 1, "B08376S3R2"), new("Some Other Series", 1, "B000000000")],
|
||||
Series = [new("Sherlock Holmes", "1-6", "B08376S3R2"), new("Book Collection", "1", "B000000000")],
|
||||
Codec = "AAC-LC",
|
||||
LibationVersion = Configuration.LibationVersion?.ToVersionString(),
|
||||
FileVersion = "36217811",
|
||||
|
||||
@ -157,7 +157,7 @@ namespace LibationFileManager.Templates
|
||||
var maxFilenameLength = LongPath.MaxFilenameLength -
|
||||
(i < pathParts.Count - 1 || string.IsNullOrEmpty(fileExtension) ? 0 : fileExtension.Length + 5);
|
||||
|
||||
while (part.Sum(LongPath.GetFilesystemStringLength) > maxFilenameLength)
|
||||
while (part.Sum(GetFilenameLength) > maxFilenameLength)
|
||||
{
|
||||
int maxLength = part.Max(p => p.Length);
|
||||
var maxEntry = part.First(p => p.Length == maxLength);
|
||||
@ -173,6 +173,10 @@ namespace LibationFileManager.Templates
|
||||
return FileUtility.GetValidFilename(fullPath, replacements, fileExtension, returnFirstExisting);
|
||||
}
|
||||
|
||||
private static int GetFilenameLength(string filename)
|
||||
=> Configuration.Instance.BooksCanWrite255UnicodeChars ? filename.Length
|
||||
: System.Text.Encoding.UTF8.GetByteCount(filename);
|
||||
|
||||
/// <summary>
|
||||
/// Organize template parts into directories. Any Extra slashes will be
|
||||
/// returned as empty directories and are taken care of by Path.Combine()
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.Forms;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -95,6 +96,9 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
|
||||
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks)
|
||||
{
|
||||
if (!IsBooksDirectoryValid())
|
||||
return false;
|
||||
|
||||
var needsPdf = libraryBooks.Where(lb => lb.NeedsPdfDownload()).ToArray();
|
||||
if (needsPdf.Length > 0)
|
||||
{
|
||||
@ -107,6 +111,9 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
|
||||
public bool QueueConvertToMp3(IList<LibraryBook> libraryBooks)
|
||||
{
|
||||
if (!IsBooksDirectoryValid())
|
||||
return false;
|
||||
|
||||
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
|
||||
var preLiberated = libraryBooks.Where(lb => !lb.AbsentFromLastScan && lb.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated && lb.Book.ContentType is DataLayer.ContentType.Product).ToArray();
|
||||
if (preLiberated.Length > 0)
|
||||
@ -122,6 +129,9 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
|
||||
public bool QueueDownloadDecrypt(IList<LibraryBook> libraryBooks)
|
||||
{
|
||||
if (!IsBooksDirectoryValid())
|
||||
return false;
|
||||
|
||||
if (libraryBooks.Count == 1)
|
||||
{
|
||||
var item = libraryBooks[0];
|
||||
@ -157,6 +167,32 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsBooksDirectoryValid()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
|
||||
{
|
||||
Serilog.Log.Logger.Error("Books location is not set in configuration.");
|
||||
MessageBoxBase.Show(
|
||||
"Please choose a \"Books location\" folder in the Settings menu.",
|
||||
"Books Directory Not Set",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
else if (AudibleFileStorage.BooksDirectory is null)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Failed to create books directory: {@booksDir}", Configuration.Instance.Books);
|
||||
MessageBoxBase.Show(
|
||||
$"Libation was unable to create the \"Books location\" folder at:\n{Configuration.Instance.Books}\n\nPlease change the Books location in the settings menu.",
|
||||
"Failed to Create Books Directory",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsBookInQueue(LibraryBook libraryBook)
|
||||
=> Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId) is not ProcessBookViewModel entry ? false
|
||||
: entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed ? !Queue.RemoveCompleted(entry)
|
||||
|
||||
@ -50,13 +50,13 @@ namespace LibationWinForms.Dialogs
|
||||
}
|
||||
|
||||
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)
|
||||
=> LoadTable(ReplacementCharacters.Default.Replacements);
|
||||
=> LoadTable(ReplacementCharacters.Default(ntfs: true).Replacements);
|
||||
|
||||
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)
|
||||
|
||||
@ -28,161 +28,168 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.templateTb = new System.Windows.Forms.TextBox();
|
||||
this.templateLbl = new System.Windows.Forms.Label();
|
||||
this.resetToDefaultBtn = new System.Windows.Forms.Button();
|
||||
this.listView1 = new System.Windows.Forms.ListView();
|
||||
this.columnHeader1 = new System.Windows.Forms.ColumnHeader();
|
||||
this.columnHeader2 = new System.Windows.Forms.ColumnHeader();
|
||||
this.richTextBox1 = new System.Windows.Forms.RichTextBox();
|
||||
this.warningsLbl = new System.Windows.Forms.Label();
|
||||
this.exampleLbl = new System.Windows.Forms.Label();
|
||||
this.SuspendLayout();
|
||||
saveBtn = new System.Windows.Forms.Button();
|
||||
cancelBtn = new System.Windows.Forms.Button();
|
||||
templateTb = new System.Windows.Forms.TextBox();
|
||||
templateLbl = new System.Windows.Forms.Label();
|
||||
resetToDefaultBtn = new System.Windows.Forms.Button();
|
||||
listView1 = new System.Windows.Forms.ListView();
|
||||
columnHeader1 = new System.Windows.Forms.ColumnHeader();
|
||||
columnHeader2 = new System.Windows.Forms.ColumnHeader();
|
||||
richTextBox1 = new System.Windows.Forms.RichTextBox();
|
||||
warningsLbl = new System.Windows.Forms.Label();
|
||||
exampleLbl = new System.Windows.Forms.Label();
|
||||
llblGoToWiki = new System.Windows.Forms.LinkLabel();
|
||||
SuspendLayout();
|
||||
//
|
||||
// saveBtn
|
||||
//
|
||||
this.saveBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.saveBtn.Location = new System.Drawing.Point(714, 345);
|
||||
this.saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.saveBtn.Name = "saveBtn";
|
||||
this.saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.saveBtn.TabIndex = 98;
|
||||
this.saveBtn.Text = "Save";
|
||||
this.saveBtn.UseVisualStyleBackColor = true;
|
||||
this.saveBtn.Click += new System.EventHandler(this.saveBtn_Click);
|
||||
saveBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
saveBtn.Location = new System.Drawing.Point(714, 345);
|
||||
saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
saveBtn.Name = "saveBtn";
|
||||
saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
saveBtn.TabIndex = 98;
|
||||
saveBtn.Text = "Save";
|
||||
saveBtn.UseVisualStyleBackColor = true;
|
||||
saveBtn.Click += saveBtn_Click;
|
||||
//
|
||||
// cancelBtn
|
||||
//
|
||||
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.cancelBtn.Location = new System.Drawing.Point(832, 345);
|
||||
this.cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.cancelBtn.Name = "cancelBtn";
|
||||
this.cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
this.cancelBtn.TabIndex = 99;
|
||||
this.cancelBtn.Text = "Cancel";
|
||||
this.cancelBtn.UseVisualStyleBackColor = true;
|
||||
this.cancelBtn.Click += new System.EventHandler(this.cancelBtn_Click);
|
||||
cancelBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
cancelBtn.Location = new System.Drawing.Point(832, 345);
|
||||
cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
cancelBtn.Name = "cancelBtn";
|
||||
cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
cancelBtn.TabIndex = 99;
|
||||
cancelBtn.Text = "Cancel";
|
||||
cancelBtn.UseVisualStyleBackColor = true;
|
||||
cancelBtn.Click += cancelBtn_Click;
|
||||
//
|
||||
// templateTb
|
||||
//
|
||||
this.templateTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.templateTb.Location = new System.Drawing.Point(12, 27);
|
||||
this.templateTb.Name = "templateTb";
|
||||
this.templateTb.Size = new System.Drawing.Size(779, 23);
|
||||
this.templateTb.TabIndex = 1;
|
||||
this.templateTb.TextChanged += new System.EventHandler(this.templateTb_TextChanged);
|
||||
templateTb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
templateTb.Location = new System.Drawing.Point(12, 27);
|
||||
templateTb.Name = "templateTb";
|
||||
templateTb.Size = new System.Drawing.Size(779, 23);
|
||||
templateTb.TabIndex = 1;
|
||||
templateTb.TextChanged += templateTb_TextChanged;
|
||||
//
|
||||
// templateLbl
|
||||
//
|
||||
this.templateLbl.AutoSize = true;
|
||||
this.templateLbl.Location = new System.Drawing.Point(12, 9);
|
||||
this.templateLbl.Name = "templateLbl";
|
||||
this.templateLbl.Size = new System.Drawing.Size(89, 15);
|
||||
this.templateLbl.TabIndex = 0;
|
||||
this.templateLbl.Text = "[template desc]";
|
||||
templateLbl.AutoSize = true;
|
||||
templateLbl.Location = new System.Drawing.Point(12, 9);
|
||||
templateLbl.Name = "templateLbl";
|
||||
templateLbl.Size = new System.Drawing.Size(89, 15);
|
||||
templateLbl.TabIndex = 0;
|
||||
templateLbl.Text = "[template desc]";
|
||||
//
|
||||
// resetToDefaultBtn
|
||||
//
|
||||
this.resetToDefaultBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.resetToDefaultBtn.Location = new System.Drawing.Point(797, 26);
|
||||
this.resetToDefaultBtn.Name = "resetToDefaultBtn";
|
||||
this.resetToDefaultBtn.Size = new System.Drawing.Size(124, 23);
|
||||
this.resetToDefaultBtn.TabIndex = 2;
|
||||
this.resetToDefaultBtn.Text = "Reset to default";
|
||||
this.resetToDefaultBtn.UseVisualStyleBackColor = true;
|
||||
this.resetToDefaultBtn.Click += new System.EventHandler(this.resetToDefaultBtn_Click);
|
||||
resetToDefaultBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
resetToDefaultBtn.Location = new System.Drawing.Point(797, 26);
|
||||
resetToDefaultBtn.Name = "resetToDefaultBtn";
|
||||
resetToDefaultBtn.Size = new System.Drawing.Size(124, 23);
|
||||
resetToDefaultBtn.TabIndex = 2;
|
||||
resetToDefaultBtn.Text = "Reset to default";
|
||||
resetToDefaultBtn.UseVisualStyleBackColor = true;
|
||||
resetToDefaultBtn.Click += resetToDefaultBtn_Click;
|
||||
//
|
||||
// listView1
|
||||
//
|
||||
this.listView1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
|
||||
this.columnHeader1,
|
||||
this.columnHeader2});
|
||||
this.listView1.FullRowSelect = true;
|
||||
this.listView1.GridLines = true;
|
||||
this.listView1.Location = new System.Drawing.Point(12, 56);
|
||||
this.listView1.MultiSelect = false;
|
||||
this.listView1.Name = "listView1";
|
||||
this.listView1.Size = new System.Drawing.Size(328, 283);
|
||||
this.listView1.TabIndex = 3;
|
||||
this.listView1.UseCompatibleStateImageBehavior = false;
|
||||
this.listView1.View = System.Windows.Forms.View.Details;
|
||||
this.listView1.DoubleClick += new System.EventHandler(this.listView1_DoubleClick);
|
||||
listView1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { columnHeader1, columnHeader2 });
|
||||
listView1.FullRowSelect = true;
|
||||
listView1.GridLines = true;
|
||||
listView1.Location = new System.Drawing.Point(12, 56);
|
||||
listView1.MultiSelect = false;
|
||||
listView1.Name = "listView1";
|
||||
listView1.Size = new System.Drawing.Size(328, 283);
|
||||
listView1.TabIndex = 3;
|
||||
listView1.UseCompatibleStateImageBehavior = false;
|
||||
listView1.View = System.Windows.Forms.View.Details;
|
||||
listView1.DoubleClick += listView1_DoubleClick;
|
||||
//
|
||||
// columnHeader1
|
||||
//
|
||||
this.columnHeader1.Text = "Tag";
|
||||
this.columnHeader1.Width = 137;
|
||||
columnHeader1.Text = "Tag";
|
||||
columnHeader1.Width = 137;
|
||||
//
|
||||
// columnHeader2
|
||||
//
|
||||
this.columnHeader2.Text = "Description";
|
||||
this.columnHeader2.Width = 170;
|
||||
columnHeader2.Text = "Description";
|
||||
columnHeader2.Width = 170;
|
||||
//
|
||||
// richTextBox1
|
||||
//
|
||||
this.richTextBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.richTextBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
this.richTextBox1.Location = new System.Drawing.Point(346, 74);
|
||||
this.richTextBox1.Name = "richTextBox1";
|
||||
this.richTextBox1.ReadOnly = true;
|
||||
this.richTextBox1.Size = new System.Drawing.Size(574, 185);
|
||||
this.richTextBox1.TabIndex = 5;
|
||||
this.richTextBox1.Text = "";
|
||||
richTextBox1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
richTextBox1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
richTextBox1.Location = new System.Drawing.Point(346, 74);
|
||||
richTextBox1.Name = "richTextBox1";
|
||||
richTextBox1.ReadOnly = true;
|
||||
richTextBox1.Size = new System.Drawing.Size(574, 185);
|
||||
richTextBox1.TabIndex = 5;
|
||||
richTextBox1.Text = "";
|
||||
//
|
||||
// warningsLbl
|
||||
//
|
||||
this.warningsLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.warningsLbl.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
|
||||
this.warningsLbl.ForeColor = System.Drawing.Color.Firebrick;
|
||||
this.warningsLbl.Location = new System.Drawing.Point(346, 262);
|
||||
this.warningsLbl.Name = "warningsLbl";
|
||||
this.warningsLbl.Size = new System.Drawing.Size(574, 77);
|
||||
this.warningsLbl.TabIndex = 6;
|
||||
this.warningsLbl.Text = "[warnings]";
|
||||
warningsLbl.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
warningsLbl.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
|
||||
warningsLbl.ForeColor = System.Drawing.Color.Firebrick;
|
||||
warningsLbl.Location = new System.Drawing.Point(346, 262);
|
||||
warningsLbl.Name = "warningsLbl";
|
||||
warningsLbl.Size = new System.Drawing.Size(574, 77);
|
||||
warningsLbl.TabIndex = 6;
|
||||
warningsLbl.Text = "[warnings]";
|
||||
//
|
||||
// exampleLbl
|
||||
//
|
||||
this.exampleLbl.AutoSize = true;
|
||||
this.exampleLbl.Location = new System.Drawing.Point(346, 56);
|
||||
this.exampleLbl.Name = "exampleLbl";
|
||||
this.exampleLbl.Size = new System.Drawing.Size(55, 15);
|
||||
this.exampleLbl.TabIndex = 4;
|
||||
this.exampleLbl.Text = "Example:";
|
||||
exampleLbl.AutoSize = true;
|
||||
exampleLbl.Location = new System.Drawing.Point(346, 56);
|
||||
exampleLbl.Name = "exampleLbl";
|
||||
exampleLbl.Size = new System.Drawing.Size(54, 15);
|
||||
exampleLbl.TabIndex = 4;
|
||||
exampleLbl.Text = "Example:";
|
||||
//
|
||||
// llblGoToWiki
|
||||
//
|
||||
llblGoToWiki.AutoSize = true;
|
||||
llblGoToWiki.Location = new System.Drawing.Point(12, 357);
|
||||
llblGoToWiki.Name = "llblGoToWiki";
|
||||
llblGoToWiki.Size = new System.Drawing.Size(229, 15);
|
||||
llblGoToWiki.TabIndex = 100;
|
||||
llblGoToWiki.TabStop = true;
|
||||
llblGoToWiki.Text = "Read about naming templates on the Wiki";
|
||||
llblGoToWiki.LinkClicked += llblGoToWiki_LinkClicked;
|
||||
//
|
||||
// EditTemplateDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.CancelButton = this.cancelBtn;
|
||||
this.ClientSize = new System.Drawing.Size(933, 388);
|
||||
this.Controls.Add(this.exampleLbl);
|
||||
this.Controls.Add(this.warningsLbl);
|
||||
this.Controls.Add(this.richTextBox1);
|
||||
this.Controls.Add(this.listView1);
|
||||
this.Controls.Add(this.resetToDefaultBtn);
|
||||
this.Controls.Add(this.templateLbl);
|
||||
this.Controls.Add(this.templateTb);
|
||||
this.Controls.Add(this.cancelBtn);
|
||||
this.Controls.Add(this.saveBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "EditTemplateDialog";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Edit Template";
|
||||
this.Load += new System.EventHandler(this.EditTemplateDialog_Load);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
AcceptButton = saveBtn;
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
CancelButton = cancelBtn;
|
||||
ClientSize = new System.Drawing.Size(933, 388);
|
||||
Controls.Add(llblGoToWiki);
|
||||
Controls.Add(exampleLbl);
|
||||
Controls.Add(warningsLbl);
|
||||
Controls.Add(richTextBox1);
|
||||
Controls.Add(listView1);
|
||||
Controls.Add(resetToDefaultBtn);
|
||||
Controls.Add(templateLbl);
|
||||
Controls.Add(templateTb);
|
||||
Controls.Add(cancelBtn);
|
||||
Controls.Add(saveBtn);
|
||||
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "EditTemplateDialog";
|
||||
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
Text = "Edit Template";
|
||||
Load += EditTemplateDialog_Load;
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
@ -198,5 +205,6 @@
|
||||
private System.Windows.Forms.RichTextBox richTextBox1;
|
||||
private System.Windows.Forms.Label warningsLbl;
|
||||
private System.Windows.Forms.Label exampleLbl;
|
||||
private System.Windows.Forms.LinkLabel llblGoToWiki;
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
private void resetTextBox(string value) => this.templateTb.Text = value;
|
||||
private Configuration config { get; } = Configuration.Instance;
|
||||
private ITemplateEditor templateEditor { get;}
|
||||
private ITemplateEditor templateEditor { get; }
|
||||
|
||||
public EditTemplateDialog()
|
||||
{
|
||||
@ -54,7 +54,7 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
private void templateTb_TextChanged(object sender, EventArgs e)
|
||||
{
|
||||
templateEditor.SetTemplateText(templateTb.Text);
|
||||
templateEditor.SetTemplateText(templateTb.Text);
|
||||
|
||||
const char ZERO_WIDTH_SPACE = '\u200B';
|
||||
var sing = $"{Path.DirectorySeparatorChar}";
|
||||
@ -150,5 +150,11 @@ namespace LibationWinForms.Dialogs
|
||||
templateTb.Text = text.Insert(selStart, itemText);
|
||||
templateTb.SelectionStart = selStart + itemText.Length;
|
||||
}
|
||||
|
||||
private void llblGoToWiki_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||
{
|
||||
Go.To.Url(@"ht" + "tps://github.com/rmcrackan/Libation/blob/master/Documentation/NamingTemplates.md");
|
||||
e.Link.Visited = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,64 @@
|
||||
<root>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
|
||||
@ -83,9 +83,11 @@ namespace LibationWinForms.Dialogs
|
||||
if (lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
|
||||
await Task.Run(() => lb.UpdateBookStatus(LiberatedStatus.Liberated));
|
||||
|
||||
tokenSource.Token.ThrowIfCancellationRequested();
|
||||
this.Invoke(FileFound, this, book);
|
||||
}
|
||||
catch(Exception ex)
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book);
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class SettingsDialog
|
||||
@ -55,7 +56,7 @@ namespace LibationWinForms.Dialogs
|
||||
},
|
||||
Configuration.KnownDirectories.UserProfile,
|
||||
"Books");
|
||||
booksSelectControl.SelectDirectory(config.Books.PathWithoutPrefix);
|
||||
booksSelectControl.SelectDirectory(config.Books?.PathWithoutPrefix ?? "");
|
||||
|
||||
saveEpisodesToSeriesFolderCbox.Checked = config.SavePodcastsToParentFolder;
|
||||
overwriteExistingCbox.Checked = config.OverwriteExisting;
|
||||
@ -63,7 +64,7 @@ namespace LibationWinForms.Dialogs
|
||||
gridFontScaleFactorTbar.Value = scaleFactorToLinearRange(config.GridFontScaleFactor);
|
||||
}
|
||||
|
||||
private void Save_Important(Configuration config)
|
||||
private bool Save_Important(Configuration config)
|
||||
{
|
||||
var newBooks = booksSelectControl.SelectedDirectory;
|
||||
|
||||
@ -73,19 +74,29 @@ namespace LibationWinForms.Dialogs
|
||||
if (string.IsNullOrWhiteSpace(newBooks))
|
||||
{
|
||||
validationError("Cannot set Books Location to blank", "Location is blank");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
LongPath lonNewBooks = newBooks;
|
||||
if (!Directory.Exists(lonNewBooks))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(lonNewBooks);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
validationError($"Error creating Books Location:\r\n{ex.Message}", "Error creating directory");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
LongPath lonNewBooks = newBooks;
|
||||
if (!Directory.Exists(lonNewBooks))
|
||||
Directory.CreateDirectory(lonNewBooks);
|
||||
|
||||
config.Books = newBooks;
|
||||
|
||||
{
|
||||
var logLevelOld = config.LogLevel;
|
||||
var logLevelNew = (Serilog.Events.LogEventLevel)loggingLevelCb.SelectedItem;
|
||||
var logLevelNew = (loggingLevelCb.SelectedItem as Serilog.Events.LogEventLevel?) ?? Serilog.Events.LogEventLevel.Information;
|
||||
|
||||
config.LogLevel = logLevelNew;
|
||||
|
||||
@ -97,9 +108,9 @@ namespace LibationWinForms.Dialogs
|
||||
config.SavePodcastsToParentFolder = saveEpisodesToSeriesFolderCbox.Checked;
|
||||
config.OverwriteExisting = overwriteExistingCbox.Checked;
|
||||
|
||||
|
||||
config.CreationTime = ((EnumDisplay<Configuration.DateTimeSource>)creationTimeCb.SelectedItem).Value;
|
||||
config.LastWriteTime = ((EnumDisplay<Configuration.DateTimeSource>)lastWriteTimeCb.SelectedItem).Value;
|
||||
config.CreationTime = (creationTimeCb.SelectedItem as EnumDisplay<Configuration.DateTimeSource>)?.Value ?? Configuration.DateTimeSource.File;
|
||||
config.LastWriteTime = (lastWriteTimeCb.SelectedItem as EnumDisplay<Configuration.DateTimeSource>)?.Value ?? Configuration.DateTimeSource.File;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int scaleFactorToLinearRange(float scaleFactor)
|
||||
|
||||
@ -43,7 +43,7 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
Save_Important(config);
|
||||
if (!Save_Important(config)) return;
|
||||
Save_ImportLibrary(config);
|
||||
Save_DownloadDecrypt(config);
|
||||
Save_AudioSettings(config);
|
||||
|
||||
@ -16,7 +16,8 @@ namespace LibationWinForms
|
||||
private void Configure_ScanManual()
|
||||
{
|
||||
this.Load += refreshImportMenu;
|
||||
AccountsSettingsPersister.Saved += refreshImportMenu;
|
||||
AccountsSettingsPersister.Saved += (_, _) => Invoke(refreshImportMenu, null, null);
|
||||
locateAudiobooksToolStripMenuItem.ToolTipText = Configuration.GetHelpText("LocateAudiobooks");
|
||||
}
|
||||
|
||||
private void refreshImportMenu(object _, EventArgs __)
|
||||
@ -96,7 +97,16 @@ namespace LibationWinForms
|
||||
|
||||
private void locateAudiobooksToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
new LocateAudiobooksDialog().ShowDialog();
|
||||
var result = MessageBox.Show(
|
||||
this,
|
||||
Configuration.GetHelpText(nameof(LocateAudiobooksDialog)),
|
||||
"Locate Previously-Liberated Audiobook Files",
|
||||
MessageBoxButtons.OKCancel,
|
||||
MessageBoxIcon.Information,
|
||||
MessageBoxDefaultButton.Button1);
|
||||
|
||||
if (result is DialogResult.OK)
|
||||
new LocateAudiobooksDialog().ShowDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,9 +6,29 @@ namespace LibationWinForms
|
||||
{
|
||||
public partial class Form1
|
||||
{
|
||||
private void Configure_Settings() { }
|
||||
private void Configure_Settings()
|
||||
{
|
||||
Shown += FormShown_Settings;
|
||||
}
|
||||
|
||||
private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog().ShowDialog();
|
||||
private void FormShown_Settings(object sender, EventArgs e)
|
||||
{
|
||||
if (LibationFileManager.AudibleFileStorage.BooksDirectory is null)
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
this,
|
||||
"Please set a valid Books location in the settings dialog.",
|
||||
"Books Directory Not Set",
|
||||
MessageBoxButtons.OKCancel,
|
||||
MessageBoxIcon.Warning,
|
||||
MessageBoxDefaultButton.Button1);
|
||||
|
||||
if (result is DialogResult.OK)
|
||||
new SettingsDialog().ShowDialog(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog().ShowDialog();
|
||||
|
||||
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ namespace LibationWinForms
|
||||
FileUtility.SaferMoveToValidPath(
|
||||
e.SettingsFilePath,
|
||||
e.SettingsFilePath,
|
||||
ReplacementCharacters.Barebones,
|
||||
Configuration.Instance.ReplacementCharacters,
|
||||
"bak");
|
||||
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
|
||||
@ -148,7 +148,10 @@ namespace LibationWinForms
|
||||
}
|
||||
|
||||
if (setupDialog.IsNewUser)
|
||||
{
|
||||
Configuration.SetLibationFiles(defaultLibationFilesDir);
|
||||
config.Books = Configuration.DefaultBooksDirectory;
|
||||
}
|
||||
else if (setupDialog.IsReturningUser)
|
||||
{
|
||||
var libationFilesDialog = new LibationFilesDialog();
|
||||
@ -175,16 +178,11 @@ namespace LibationWinForms
|
||||
CancelInstallation();
|
||||
return;
|
||||
}
|
||||
config.Books = Configuration.DefaultBooksDirectory;
|
||||
}
|
||||
|
||||
// INIT DEFAULT SETTINGS
|
||||
// if 'new user' was clicked, or if 'returning user' chose new install: show basic settings dialog
|
||||
config.Books ??= Configuration.DefaultBooksDirectory;
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
CancelInstallation();
|
||||
if (!config.LibationSettingsAreValid)
|
||||
CancelInstallation();
|
||||
}
|
||||
|
||||
/// <summary>migrations which require Forms or are long-running</summary>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.StepRunner;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.Dialogs;
|
||||
@ -163,7 +164,7 @@ namespace LibationWinForms
|
||||
var books = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
if (books.Count == 0) return true;
|
||||
|
||||
var firstAuthor = getFirstAuthor();
|
||||
var firstAuthor = getFirstAuthor()?.SurroundWithQuotes();
|
||||
if (firstAuthor == null) return true;
|
||||
|
||||
if (!ProceedMessageBox("You can filter the grid entries by searching", "Searching"))
|
||||
@ -196,7 +197,7 @@ namespace LibationWinForms
|
||||
|
||||
private async Task<bool> ShowQuickFilters()
|
||||
{
|
||||
var firstAuthor = getFirstAuthor();
|
||||
var firstAuthor = getFirstAuthor()?.SurroundWithQuotes();
|
||||
if (firstAuthor == null) return true;
|
||||
|
||||
if (!ProceedMessageBox("Queries that you perform regularly can be added to 'Quick Filters'", "Quick Filters"))
|
||||
|
||||
@ -12,9 +12,9 @@ namespace FileUtilityTests
|
||||
[TestClass]
|
||||
public class GetSafePath
|
||||
{
|
||||
static readonly ReplacementCharacters Default = ReplacementCharacters.Default;
|
||||
static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault;
|
||||
static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones;
|
||||
static readonly ReplacementCharacters Default = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
|
||||
static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault(Environment.OSVersion.Platform == PlatformID.Win32NT);
|
||||
static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones(Environment.OSVersion.Platform == PlatformID.Win32NT);
|
||||
|
||||
[TestMethod]
|
||||
public void null_path_throws() => Assert.ThrowsException<ArgumentNullException>(() => FileUtility.GetSafePath(null, Default));
|
||||
@ -98,9 +98,9 @@ namespace FileUtilityTests
|
||||
[TestClass]
|
||||
public class GetSafeFileName
|
||||
{
|
||||
static readonly ReplacementCharacters Default = ReplacementCharacters.Default;
|
||||
static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault;
|
||||
static readonly ReplacementCharacters Barebones = ReplacementCharacters.Barebones;
|
||||
static readonly ReplacementCharacters Default = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
|
||||
static readonly ReplacementCharacters LoFiDefault = ReplacementCharacters.LoFiDefault(Environment.OSVersion.Platform == PlatformID.Win32NT);
|
||||
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()
|
||||
[TestMethod]
|
||||
@ -193,7 +193,7 @@ namespace FileUtilityTests
|
||||
[TestClass]
|
||||
public class GetValidFilename
|
||||
{
|
||||
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
|
||||
static ReplacementCharacters Replacements = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
|
||||
|
||||
[TestMethod]
|
||||
// dot-files
|
||||
|
||||
@ -23,8 +23,17 @@ namespace TemplatesTests
|
||||
|
||||
public static class Shared
|
||||
{
|
||||
[System.Runtime.CompilerServices.ModuleInitializer]
|
||||
public static void Init()
|
||||
{
|
||||
var thisDir = Path.GetDirectoryName(Environment.ProcessPath);
|
||||
LibationFileManager.Configuration.SetLibationFiles(thisDir);
|
||||
if (!LibationFileManager.Configuration.Instance.LibationSettingsAreValid)
|
||||
LibationFileManager.Configuration.Instance.Books = Path.Combine(thisDir, "Books");
|
||||
}
|
||||
|
||||
public static LibraryBookDto GetLibraryBook()
|
||||
=> GetLibraryBook([new SeriesDto("Sherlock Holmes", 1, "B08376S3R2")]);
|
||||
=> GetLibraryBook([new SeriesDto("Sherlock Holmes", "1", "B08376S3R2")]);
|
||||
|
||||
public static LibraryBookDto GetLibraryBook(IEnumerable<SeriesDto> series)
|
||||
=> new()
|
||||
@ -66,7 +75,7 @@ namespace TemplatesTests
|
||||
[TestClass]
|
||||
public class getFileNamingTemplate
|
||||
{
|
||||
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
|
||||
static ReplacementCharacters Replacements = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(null)]
|
||||
@ -367,12 +376,13 @@ namespace TemplatesTests
|
||||
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("<series>", "Series A, Series B, Series C")]
|
||||
[DataRow("<series[]>", "Series A, Series B, Series C")]
|
||||
[DataRow("<series>", "Series A, Series B, Series C, Series D")]
|
||||
[DataRow("<series[]>", "Series A, Series B, Series C, Series D")]
|
||||
[DataRow("<series[max(1)]>", "Series A")]
|
||||
[DataRow("<series[max(2)]>", "Series A, Series B")]
|
||||
[DataRow("<series[max(3)]>", "Series A, Series B, Series C")]
|
||||
[DataRow("<series[format({N}, {#}, {ID}) separator(; )]>", "Series A, 1, B1; Series B, 6, B2; Series C, 2, B3")]
|
||||
[DataRow("<series[max(4)]>", "Series A, Series B, Series C, Series D")]
|
||||
[DataRow("<series[format({N}, {#}, {ID}) separator(; )]>", "Series A, 1, B1; Series B, 6, B2; Series C, 2, B3; Series D, 1-5, B4")]
|
||||
[DataRow("<series[format({N}, {#}, {ID}) separator(; ) max(3)]>", "Series A, 1, B1; Series B, 6, B2; Series C, 2, B3")]
|
||||
[DataRow("<series[format({N}, {#}, {ID}) separator(; ) max(2)]>", "Series A, 1, B1; Series B, 6, B2")]
|
||||
[DataRow("<first series>", "Series A")]
|
||||
@ -383,9 +393,10 @@ namespace TemplatesTests
|
||||
var bookDto = GetLibraryBook();
|
||||
bookDto.Series =
|
||||
[
|
||||
new("Series A", 1, "B1"),
|
||||
new("Series B", 6, "B2"),
|
||||
new("Series C", 2, "B3")
|
||||
new("Series A", "1", "B1"),
|
||||
new("Series B", "6", "B2"),
|
||||
new("Series C", "2", "B3"),
|
||||
new("Series D", "1-5", "B4"),
|
||||
];
|
||||
|
||||
Templates.TryGetTemplate<Templates.FileTemplate>(template, out var fileTemplate).Should().BeTrue();
|
||||
@ -451,7 +462,7 @@ namespace Templates_Other
|
||||
[TestClass]
|
||||
public class GetFilePath
|
||||
{
|
||||
static ReplacementCharacters Replacements = ReplacementCharacters.Default;
|
||||
static ReplacementCharacters Replacements = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(@"C:\foo\bar", @"\\Folder\<title>\[<id>]\\", @"C:\foo\bar\Folder\my_ book 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\[ID123456].txt", PlatformID.Win32NT)]
|
||||
@ -887,7 +898,7 @@ namespace Templates_ChapterFile_Tests
|
||||
[TestClass]
|
||||
public class GetFilename
|
||||
{
|
||||
static readonly ReplacementCharacters Default = ReplacementCharacters.Default;
|
||||
static readonly ReplacementCharacters Default = ReplacementCharacters.Default(Environment.OSVersion.Platform == PlatformID.Win32NT);
|
||||
|
||||
[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)]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user