Merge pull request #664 from Mbucari/startup-2
New settings, context menu, and performance improvements
This commit is contained in:
commit
5976706e40
@ -118,11 +118,7 @@ namespace AaxDecrypter
|
||||
public abstract Task CancelAsync();
|
||||
protected abstract Task<bool> Step_DownloadAndDecryptAudiobookAsync();
|
||||
|
||||
public virtual void SetCoverArt(byte[] coverArt)
|
||||
{
|
||||
if (coverArt is not null)
|
||||
OnRetrievedCoverArt(coverArt);
|
||||
}
|
||||
public virtual void SetCoverArt(byte[] coverArt) { }
|
||||
|
||||
protected void OnRetrievedTitle(string title)
|
||||
=> RetrievedTitle?.Invoke(this, title);
|
||||
|
||||
@ -127,6 +127,8 @@ namespace AaxDecrypter
|
||||
{
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
|
||||
|
||||
if (Path.GetFileName(uriToSameFile.LocalPath) != Path.GetFileName(Uri.LocalPath))
|
||||
throw new ArgumentException($"New uri to the same file must have the same file name.");
|
||||
if (uriToSameFile.Host != Uri.Host)
|
||||
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
|
||||
if (DownloadTask is not null)
|
||||
|
||||
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AaxDecrypter;
|
||||
using ApplicationServices;
|
||||
using AudibleApi.Common;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
@ -124,12 +125,12 @@ namespace FileLiberator
|
||||
var quality = (AudibleApi.DownloadQuality)config.FileDownloadQuality;
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId, quality);
|
||||
using var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic);
|
||||
using var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic);
|
||||
|
||||
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower());
|
||||
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
||||
|
||||
if (contentLic.DrmType != AudibleApi.Common.DrmType.Adrm)
|
||||
if (contentLic.DrmType != DrmType.Adrm)
|
||||
abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, dlOptions);
|
||||
else
|
||||
{
|
||||
@ -152,16 +153,34 @@ namespace FileLiberator
|
||||
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
||||
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
|
||||
|
||||
// REAL WORK DONE HERE
|
||||
return await abDownloader.RunAsync();
|
||||
// REAL WORK DONE HERE
|
||||
var success = await abDownloader.RunAsync();
|
||||
|
||||
if (success && config.SaveMetadataToFile)
|
||||
{
|
||||
var metadataFile = Templates.File.GetFilename(dlOptions.LibraryBookDto, Path.GetDirectoryName(outFileName), ".metadata.json");
|
||||
|
||||
saveMetadata(libraryBook, contentLic.ContentMetadata, metadataFile);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic)
|
||||
private void saveMetadata(LibraryBook libraryBook, ContentMetadata contentMetadata, string fileName)
|
||||
{
|
||||
var export = Newtonsoft.Json.Linq.JObject.FromObject(LibToDtos.ToDtos(new[] { libraryBook })[0]);
|
||||
export.Add(nameof(contentMetadata.ChapterInfo), Newtonsoft.Json.Linq.JObject.FromObject(contentMetadata.ChapterInfo));
|
||||
export.Add(nameof(contentMetadata.ContentReference), Newtonsoft.Json.Linq.JObject.FromObject(contentMetadata.ContentReference));
|
||||
|
||||
File.WriteAllText(fileName, export.ToString());
|
||||
OnFileCreated(libraryBook, fileName);
|
||||
}
|
||||
|
||||
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, ContentLicense contentLic)
|
||||
{
|
||||
//If DrmType != Adrm the delivered file is an unencrypted mp3.
|
||||
|
||||
var outputFormat
|
||||
= contentLic.DrmType != AudibleApi.Common.DrmType.Adrm || (config.AllowLibationFixup && config.DecryptToLossy)
|
||||
= contentLic.DrmType != DrmType.Adrm || (config.AllowLibationFixup && config.DecryptToLossy)
|
||||
? OutputFormat.Mp3
|
||||
: OutputFormat.M4b;
|
||||
|
||||
@ -183,7 +202,11 @@ namespace FileLiberator
|
||||
RuntimeLength = TimeSpan.FromMilliseconds(contentLic?.ContentMetadata?.ChapterInfo?.RuntimeLengthMs ?? 0),
|
||||
};
|
||||
|
||||
var chapters = flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters).OrderBy(c => c.StartOffsetMs).ToList();
|
||||
var titleConcat = config.CombineNestedChapterTitles ? ": " : null;
|
||||
var chapters
|
||||
= flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters, titleConcat)
|
||||
.OrderBy(c => c.StartOffsetMs)
|
||||
.ToList();
|
||||
|
||||
if (config.MergeOpeningAndEndCredits)
|
||||
combineCredits(chapters);
|
||||
@ -280,14 +303,19 @@ namespace FileLiberator
|
||||
|
||||
*/
|
||||
|
||||
public static List<AudibleApi.Common.Chapter> flattenChapters(IList<AudibleApi.Common.Chapter> chapters, string titleConcat = ": ")
|
||||
public static List<Chapter> flattenChapters(IList<Chapter> chapters, string titleConcat = ": ")
|
||||
{
|
||||
List<AudibleApi.Common.Chapter> chaps = new();
|
||||
List<Chapter> chaps = new();
|
||||
|
||||
foreach (var c in chapters)
|
||||
{
|
||||
if (c.Chapters is null)
|
||||
chaps.Add(c);
|
||||
else if (titleConcat is null)
|
||||
{
|
||||
chaps.Add(c);
|
||||
chaps.AddRange(flattenChapters(c.Chapters));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (c.LengthMs < 10000)
|
||||
@ -305,13 +333,12 @@ namespace FileLiberator
|
||||
child.Title = $"{c.Title}{titleConcat}{child.Title}";
|
||||
|
||||
chaps.AddRange(children);
|
||||
c.Chapters = null;
|
||||
}
|
||||
}
|
||||
return chaps;
|
||||
}
|
||||
|
||||
public static void combineCredits(IList<AudibleApi.Common.Chapter> chapters)
|
||||
public static void combineCredits(IList<Chapter> chapters)
|
||||
{
|
||||
if (chapters.Count > 1 && chapters[0].Title == "Opening Credits")
|
||||
{
|
||||
@ -351,11 +378,15 @@ namespace FileLiberator
|
||||
|
||||
private void AaxcDownloader_RetrievedCoverArt(object _, byte[] e)
|
||||
{
|
||||
if (Configuration.Instance.AllowLibationFixup)
|
||||
{
|
||||
e = OnRequestCoverArt();
|
||||
abDownloader.SetCoverArt(e);
|
||||
}
|
||||
|
||||
if (e is not null)
|
||||
OnCoverImageDiscovered(e);
|
||||
else if (Configuration.Instance.AllowLibationFixup)
|
||||
abDownloader.SetCoverArt(OnRequestCoverArt());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Move new files to 'Books' directory</summary>
|
||||
/// <returns>Return directory if audiobook file(s) were successfully created and can be located on disk. Else null.</returns>
|
||||
|
||||
@ -73,7 +73,15 @@
|
||||
<TextBlock Text="{CompiledBinding MergeOpeningEndCreditsText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding AllowLibationFixup, Mode=TwoWay}">
|
||||
<CheckBox
|
||||
ToolTip.Tip="{CompiledBinding CombineNestedChapterTitlesTip}"
|
||||
IsChecked="{CompiledBinding CombineNestedChapterTitles, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding CombineNestedChapterTitlesText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
ToolTip.Tip="{CompiledBinding AllowLibationFixupTip}"
|
||||
IsChecked="{CompiledBinding AllowLibationFixup, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding AllowLibationFixupText}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
@ -165,18 +165,32 @@
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<CheckBox
|
||||
<StackPanel
|
||||
Grid.Row="3"
|
||||
Margin="5"
|
||||
VerticalAlignment="Top"
|
||||
IsVisible="{CompiledBinding !Config.IsLinux}"
|
||||
IsChecked="{CompiledBinding UseCoverAsFolderIcon, Mode=TwoWay}">
|
||||
Orientation="Horizontal">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="{CompiledBinding UseCoverAsFolderIconText}" />
|
||||
<CheckBox
|
||||
Margin="5"
|
||||
VerticalAlignment="Top"
|
||||
IsVisible="{CompiledBinding !Config.IsLinux}"
|
||||
IsChecked="{CompiledBinding UseCoverAsFolderIcon, Mode=TwoWay}">
|
||||
|
||||
</CheckBox>
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="{CompiledBinding UseCoverAsFolderIconText}" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
Margin="5"
|
||||
VerticalAlignment="Top"
|
||||
IsChecked="{CompiledBinding SaveMetadataToFile, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="{CompiledBinding SaveMetadataToFileText}" />
|
||||
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
using ApplicationServices;
|
||||
using Avalonia.Threading;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.ViewModels
|
||||
@ -41,16 +44,17 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
private void Configure_BackupCounts()
|
||||
{
|
||||
MainWindow.Loaded += setBackupCounts;
|
||||
LibraryCommands.LibrarySizeChanged += setBackupCounts;
|
||||
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
|
||||
MainWindow.LibraryLoaded += (_, e) => setBackupCounts(e.Where(l => !l.Book.IsEpisodeParent()));
|
||||
LibraryCommands.LibrarySizeChanged += (_,_) => setBackupCounts();
|
||||
LibraryCommands.BookUserDefinedItemCommitted += (_, _) => setBackupCounts();
|
||||
}
|
||||
|
||||
private async void setBackupCounts(object _, object __)
|
||||
private async void setBackupCounts(IEnumerable<LibraryBook> libraryBooks = null)
|
||||
{
|
||||
if (updateCountsTask?.IsCompleted ?? true)
|
||||
{
|
||||
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts());
|
||||
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
||||
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
|
||||
var stats = await updateCountsTask;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
|
||||
|
||||
|
||||
@ -264,9 +264,12 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
|
||||
{
|
||||
byte[] coverData = PictureStorage
|
||||
.GetPictureSynchronously(
|
||||
new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500));
|
||||
var quality
|
||||
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High
|
||||
? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
|
||||
: new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
|
||||
|
||||
byte[] coverData = PictureStorage.GetPictureSynchronously(quality);
|
||||
|
||||
AudioDecodable_CoverImageDiscovered(this, coverData);
|
||||
return coverData;
|
||||
|
||||
@ -91,37 +91,21 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
#region Display Functions
|
||||
|
||||
internal void BindToGrid(List<LibraryBook> dbBooks)
|
||||
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
||||
{
|
||||
GridEntries = new(SOURCE) { Filter = CollectionFilter };
|
||||
|
||||
var geList = dbBooks
|
||||
.Where(lb => lb.Book.IsProduct())
|
||||
.Select(b => new LibraryBookEntry<AvaloniaEntryStatus>(b))
|
||||
.ToList<IGridEntry>();
|
||||
var geList = await LibraryBookEntry<AvaloniaEntryStatus>.GetAllProductsAsync(dbBooks);
|
||||
|
||||
var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild()).ToList();
|
||||
var seriesEntries = await SeriesEntry<AvaloniaEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
||||
|
||||
var seriesBooks = dbBooks.Where(lb => lb.Book.IsEpisodeParent()).ToList();
|
||||
|
||||
foreach (var parent in seriesBooks)
|
||||
{
|
||||
var seriesEpisodes = episodes.FindChildren(parent);
|
||||
|
||||
if (!seriesEpisodes.Any()) continue;
|
||||
|
||||
var seriesEntry = new SeriesEntry<AvaloniaEntryStatus>(parent, seriesEpisodes);
|
||||
seriesEntry.Liberate.Expanded = false;
|
||||
|
||||
geList.Add(seriesEntry);
|
||||
}
|
||||
SOURCE.AddRange(geList.Concat(seriesEntries).OrderDescending(new RowComparer(null)));
|
||||
|
||||
//Create the filtered-in list before adding entries to avoid a refresh
|
||||
FilteredInGridEntries = geList.Union(geList.OfType<ISeriesEntry>().SelectMany(s => s.Children)).FilterEntries(FilterString);
|
||||
SOURCE.AddRange(geList.OrderDescending(new RowComparer(null)));
|
||||
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(FilterString);
|
||||
|
||||
//Add all children beneath their parent
|
||||
foreach (var series in SOURCE.OfType<ISeriesEntry>().ToList())
|
||||
foreach (var series in seriesEntries)
|
||||
{
|
||||
var seriesIndex = SOURCE.IndexOf(series);
|
||||
foreach (var child in series.Children)
|
||||
|
||||
@ -44,6 +44,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public void LoadSettings(Configuration config)
|
||||
{
|
||||
CreateCueSheet = config.CreateCueSheet;
|
||||
CombineNestedChapterTitles = config.CombineNestedChapterTitles;
|
||||
AllowLibationFixup = config.AllowLibationFixup;
|
||||
DownloadCoverArt = config.DownloadCoverArt;
|
||||
RetainAaxFile = config.RetainAaxFile;
|
||||
@ -71,6 +72,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public void SaveSettings(Configuration config)
|
||||
{
|
||||
config.CreateCueSheet = CreateCueSheet;
|
||||
config.CombineNestedChapterTitles = CombineNestedChapterTitles;
|
||||
config.AllowLibationFixup = AllowLibationFixup;
|
||||
config.DownloadCoverArt = DownloadCoverArt;
|
||||
config.RetainAaxFile = RetainAaxFile;
|
||||
@ -99,7 +101,10 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public AvaloniaList<Configuration.ClipBookmarkFormat> ClipBookmarkFormats { get; } = new(Enum<Configuration.ClipBookmarkFormat>.GetValues());
|
||||
public string FileDownloadQualityText { get; } = Configuration.GetDescription(nameof(Configuration.FileDownloadQuality));
|
||||
public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet));
|
||||
public string CombineNestedChapterTitlesText { get; } = Configuration.GetDescription(nameof(Configuration.CombineNestedChapterTitles));
|
||||
public string CombineNestedChapterTitlesTip => Configuration.GetHelpText(nameof(CombineNestedChapterTitles));
|
||||
public string AllowLibationFixupText { get; } = Configuration.GetDescription(nameof(Configuration.AllowLibationFixup));
|
||||
public string AllowLibationFixupTip => Configuration.GetHelpText(nameof(AllowLibationFixup));
|
||||
public string DownloadCoverArtText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadCoverArt));
|
||||
public string RetainAaxFileText { get; } = Configuration.GetDescription(nameof(Configuration.RetainAaxFile));
|
||||
public string SplitFilesByChapterText { get; } = Configuration.GetDescription(nameof(Configuration.SplitFilesByChapter));
|
||||
@ -110,6 +115,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string MoveMoovToBeginningText { get; } = Configuration.GetDescription(nameof(Configuration.MoveMoovToBeginning));
|
||||
|
||||
public bool CreateCueSheet { get; set; }
|
||||
public bool CombineNestedChapterTitles { get; set; }
|
||||
public bool DownloadCoverArt { get; set; }
|
||||
public bool RetainAaxFile { get; set; }
|
||||
public bool DownloadClipsBookmarks { get => _downloadClipsBookmarks; set => this.RaiseAndSetIfChanged(ref _downloadClipsBookmarks, value); }
|
||||
|
||||
@ -38,6 +38,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
ChapterFileTemplate = config.ChapterFileTemplate;
|
||||
InProgressDirectory = config.InProgress;
|
||||
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
|
||||
SaveMetadataToFile = config.SaveMetadataToFile;
|
||||
}
|
||||
|
||||
public void SaveSettings(Configuration config)
|
||||
@ -54,9 +55,11 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
config.InProgress = InProgressDirectory;
|
||||
|
||||
config.UseCoverAsFolderIcon = UseCoverAsFolderIcon;
|
||||
config.SaveMetadataToFile = SaveMetadataToFile;
|
||||
}
|
||||
|
||||
public string UseCoverAsFolderIconText { get; } = Configuration.GetDescription(nameof(Configuration.UseCoverAsFolderIcon));
|
||||
public string SaveMetadataToFileText { get; } = Configuration.GetDescription(nameof(Configuration.SaveMetadataToFile));
|
||||
public string BadBookGroupboxText { get; } = Configuration.GetDescription(nameof(Configuration.BadBook));
|
||||
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription();
|
||||
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription();
|
||||
@ -72,6 +75,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string FileTemplate { get => _fileTemplate; set { this.RaiseAndSetIfChanged(ref _fileTemplate, value); } }
|
||||
public string ChapterFileTemplate { get => _chapterFileTemplate; set { this.RaiseAndSetIfChanged(ref _chapterFileTemplate, value); } }
|
||||
public bool UseCoverAsFolderIcon { get; set; }
|
||||
public bool SaveMetadataToFile { get; set; }
|
||||
|
||||
public bool BadBookAsk { get; set; }
|
||||
public bool BadBookAbort { get; set; }
|
||||
|
||||
@ -61,7 +61,7 @@ namespace LibationAvalonia.Views
|
||||
if (QuickFilters.UseDefault)
|
||||
await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault());
|
||||
|
||||
ViewModel.ProductsDisplay.BindToGrid(dbBooks);
|
||||
await ViewModel.ProductsDisplay.BindToGridAsync(dbBooks);
|
||||
}
|
||||
|
||||
private void selectAndFocusSearchBox()
|
||||
|
||||
@ -84,13 +84,13 @@
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</controls:DataGridTemplateColumnExt>
|
||||
|
||||
<DataGridTemplateColumn CanUserSort="False" Width="80" Header="Cover" SortMemberPath="Cover" ClipboardContentBinding="{Binding LibraryBook.Book.PictureLarge}">
|
||||
<controls:DataGridTemplateColumnExt CanUserSort="False" Width="80" Header="Cover" SortMemberPath="Cover" ClipboardContentBinding="{Binding LibraryBook.Book.PictureLarge}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate x:DataType="uibase:IGridEntry">
|
||||
<Image Opacity="{CompiledBinding Liberate.Opacity}" Tapped="Cover_Click" Height="80" Source="{CompiledBinding Cover}" ToolTip.Tip="Click to see full size" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</controls:DataGridTemplateColumnExt>
|
||||
|
||||
<controls:DataGridTemplateColumnExt MinWidth="150" Width="2*" Header="Title" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
|
||||
@ -51,7 +51,7 @@ namespace LibationAvalonia.Views
|
||||
catch { sampleEntries = new(); }
|
||||
|
||||
var pdvm = new ProductsDisplayViewModel();
|
||||
pdvm.BindToGrid(sampleEntries);
|
||||
_ = pdvm.BindToGridAsync(sampleEntries);
|
||||
DataContext = pdvm;
|
||||
|
||||
return;
|
||||
@ -78,163 +78,177 @@ namespace LibationAvalonia.Views
|
||||
|
||||
public void ProductsGrid_CellContextMenuStripNeeded(object sender, DataGridCellContextMenuStripNeededEventArgs args)
|
||||
{
|
||||
// stop light
|
||||
if (args.Column.SortMemberPath == "Liberate")
|
||||
if (args.Column.SortMemberPath is not "Liberate" and not "Cover")
|
||||
{
|
||||
var entry = args.GridEntry;
|
||||
|
||||
#region Liberate all Episodes
|
||||
|
||||
if (entry.Liberate.IsSeries)
|
||||
{
|
||||
var liberateEpisodesMenuItem = new MenuItem()
|
||||
{
|
||||
Header = "_Liberate All Episodes",
|
||||
IsEnabled = ((ISeriesEntry)entry).Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
|
||||
};
|
||||
|
||||
args.ContextMenuItems.Add(liberateEpisodesMenuItem);
|
||||
|
||||
liberateEpisodesMenuItem.Click += (_, _) => LiberateSeriesClicked?.Invoke(this, ((ISeriesEntry)entry));
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region Set Download status to Downloaded
|
||||
|
||||
var setDownloadMenuItem = new MenuItem()
|
||||
{
|
||||
Header = "Set Download status to '_Downloaded'",
|
||||
IsEnabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || entry.Liberate.IsSeries
|
||||
};
|
||||
|
||||
args.ContextMenuItems.Add(setDownloadMenuItem);
|
||||
|
||||
if (entry.Liberate.IsSeries)
|
||||
setDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.Liberated);
|
||||
else
|
||||
setDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.Liberated);
|
||||
|
||||
#endregion
|
||||
#region Set Download status to Not Downloaded
|
||||
|
||||
var setNotDownloadMenuItem = new MenuItem()
|
||||
{
|
||||
Header = "Set Download status to '_Not Downloaded'",
|
||||
IsEnabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || entry.Liberate.IsSeries
|
||||
};
|
||||
|
||||
args.ContextMenuItems.Add(setNotDownloadMenuItem);
|
||||
|
||||
if (entry.Liberate.IsSeries)
|
||||
setNotDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.NotLiberated);
|
||||
else
|
||||
setNotDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.NotLiberated);
|
||||
|
||||
#endregion
|
||||
#region Remove from library
|
||||
|
||||
var removeMenuItem = new MenuItem() { Header = "_Remove from library" };
|
||||
|
||||
args.ContextMenuItems.Add(removeMenuItem);
|
||||
|
||||
if (entry.Liberate.IsSeries)
|
||||
removeMenuItem.Click += async (_, __) => await ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).RemoveBooksAsync();
|
||||
else
|
||||
removeMenuItem.Click += async (_, __) => await Task.Run(entry.LibraryBook.RemoveBook);
|
||||
|
||||
#endregion
|
||||
|
||||
if (!entry.Liberate.IsSeries)
|
||||
{
|
||||
#region Locate file
|
||||
var locateFileMenuItem = new MenuItem() { Header = "_Locate file..." };
|
||||
|
||||
args.ContextMenuItems.Add(locateFileMenuItem);
|
||||
|
||||
locateFileMenuItem.Click += async (_, __) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var window = this.GetParentWindow();
|
||||
|
||||
var openFileDialogOptions = new FilePickerOpenOptions
|
||||
{
|
||||
Title = $"Locate the audio file for '{entry.Book.TitleWithSubtitle}'",
|
||||
AllowMultiple = false,
|
||||
SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix),
|
||||
FileTypeFilter = new FilePickerFileType[]
|
||||
{
|
||||
new("All files (*.*)") { Patterns = new[] { "*" } },
|
||||
}
|
||||
};
|
||||
|
||||
var selectedFiles = await window.StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
|
||||
var selectedFile = selectedFiles.SingleOrDefault()?.TryGetLocalPath();
|
||||
|
||||
if (selectedFile is not null)
|
||||
FilePathCache.Insert(entry.AudibleProductId, selectedFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var msg = "Error saving book's location";
|
||||
await MessageBox.ShowAdminAlert(null, msg, msg, ex);
|
||||
}
|
||||
};
|
||||
|
||||
#endregion
|
||||
#region Convert to Mp3
|
||||
var convertToMp3MenuItem = new MenuItem
|
||||
{
|
||||
Header = "_Convert to Mp3",
|
||||
IsEnabled = entry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated
|
||||
};
|
||||
args.ContextMenuItems.Add(convertToMp3MenuItem);
|
||||
|
||||
convertToMp3MenuItem.Click += (_, _) => ConvertToMp3Clicked?.Invoke(this, entry.LibraryBook);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
args.ContextMenuItems.Add(new Separator());
|
||||
|
||||
#region View Bookmarks/Clips
|
||||
|
||||
if (!entry.Liberate.IsSeries)
|
||||
{
|
||||
|
||||
var bookRecordMenuItem = new MenuItem { Header = "View _Bookmarks/Clips" };
|
||||
|
||||
args.ContextMenuItems.Add(bookRecordMenuItem);
|
||||
|
||||
bookRecordMenuItem.Click += async (_, _) => await new BookRecordsDialog(entry.LibraryBook).ShowDialog(VisualRoot as Window);
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region View All Series
|
||||
|
||||
if (entry.Book.SeriesLink.Any())
|
||||
{
|
||||
var header = entry.Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series";
|
||||
var viewSeriesMenuItem = new MenuItem { Header = header };
|
||||
|
||||
args.ContextMenuItems.Add(viewSeriesMenuItem);
|
||||
|
||||
viewSeriesMenuItem.Click += (_, _) => new SeriesViewDialog(entry.LibraryBook).Show();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
else
|
||||
{
|
||||
// any non-stop light column
|
||||
// (except for the Cover column which does not have a context menu)
|
||||
var menuItem = new MenuItem { Header = "_Copy Cell Contents" };
|
||||
|
||||
menuItem.Click += async (s, e)
|
||||
=> await App.MainWindow.Clipboard.SetTextAsync(args.CellClipboardContents);
|
||||
|
||||
args.ContextMenuItems.Add(menuItem);
|
||||
args.ContextMenuItems.Add(new Separator());
|
||||
}
|
||||
var entry = args.GridEntry;
|
||||
|
||||
#region Liberate all Episodes
|
||||
|
||||
if (entry.Liberate.IsSeries)
|
||||
{
|
||||
var liberateEpisodesMenuItem = new MenuItem()
|
||||
{
|
||||
Header = "_Liberate All Episodes",
|
||||
IsEnabled = ((ISeriesEntry)entry).Children.Any(c => c.Liberate.BookStatus is LiberatedStatus.NotLiberated or LiberatedStatus.PartialDownload)
|
||||
};
|
||||
|
||||
args.ContextMenuItems.Add(liberateEpisodesMenuItem);
|
||||
|
||||
liberateEpisodesMenuItem.Click += (_, _) => LiberateSeriesClicked?.Invoke(this, ((ISeriesEntry)entry));
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region Set Download status to Downloaded
|
||||
|
||||
var setDownloadMenuItem = new MenuItem()
|
||||
{
|
||||
Header = "Set Download status to '_Downloaded'",
|
||||
IsEnabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || entry.Liberate.IsSeries
|
||||
};
|
||||
|
||||
args.ContextMenuItems.Add(setDownloadMenuItem);
|
||||
|
||||
if (entry.Liberate.IsSeries)
|
||||
setDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.Liberated);
|
||||
else
|
||||
setDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.Liberated);
|
||||
|
||||
#endregion
|
||||
#region Set Download status to Not Downloaded
|
||||
|
||||
var setNotDownloadMenuItem = new MenuItem()
|
||||
{
|
||||
Header = "Set Download status to '_Not Downloaded'",
|
||||
IsEnabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || entry.Liberate.IsSeries
|
||||
};
|
||||
|
||||
args.ContextMenuItems.Add(setNotDownloadMenuItem);
|
||||
|
||||
if (entry.Liberate.IsSeries)
|
||||
setNotDownloadMenuItem.Click += (_, __) => ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).UpdateBookStatus(LiberatedStatus.NotLiberated);
|
||||
else
|
||||
setNotDownloadMenuItem.Click += (_, __) => entry.LibraryBook.UpdateBookStatus(LiberatedStatus.NotLiberated);
|
||||
|
||||
#endregion
|
||||
#region Remove from library
|
||||
|
||||
var removeMenuItem = new MenuItem() { Header = "_Remove from library" };
|
||||
|
||||
args.ContextMenuItems.Add(removeMenuItem);
|
||||
|
||||
if (entry.Liberate.IsSeries)
|
||||
removeMenuItem.Click += async (_, __) => await ((ISeriesEntry)entry).Children.Select(c => c.LibraryBook).RemoveBooksAsync();
|
||||
else
|
||||
removeMenuItem.Click += async (_, __) => await Task.Run(entry.LibraryBook.RemoveBook);
|
||||
|
||||
#endregion
|
||||
|
||||
if (!entry.Liberate.IsSeries)
|
||||
{
|
||||
#region Locate file
|
||||
var locateFileMenuItem = new MenuItem() { Header = "_Locate file..." };
|
||||
|
||||
args.ContextMenuItems.Add(locateFileMenuItem);
|
||||
|
||||
locateFileMenuItem.Click += async (_, __) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var window = this.GetParentWindow();
|
||||
|
||||
var openFileDialogOptions = new FilePickerOpenOptions
|
||||
{
|
||||
Title = $"Locate the audio file for '{entry.Book.TitleWithSubtitle}'",
|
||||
AllowMultiple = false,
|
||||
SuggestedStartLocation = await window.StorageProvider.TryGetFolderFromPathAsync(Configuration.Instance.Books.PathWithoutPrefix),
|
||||
FileTypeFilter = new FilePickerFileType[]
|
||||
{
|
||||
new("All files (*.*)") { Patterns = new[] { "*" } },
|
||||
}
|
||||
};
|
||||
|
||||
var selectedFiles = await window.StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
|
||||
var selectedFile = selectedFiles.SingleOrDefault()?.TryGetLocalPath();
|
||||
|
||||
if (selectedFile is not null)
|
||||
FilePathCache.Insert(entry.AudibleProductId, selectedFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var msg = "Error saving book's location";
|
||||
await MessageBox.ShowAdminAlert(null, msg, msg, ex);
|
||||
}
|
||||
};
|
||||
|
||||
#endregion
|
||||
#region Convert to Mp3
|
||||
var convertToMp3MenuItem = new MenuItem
|
||||
{
|
||||
Header = "_Convert to Mp3",
|
||||
IsEnabled = entry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated
|
||||
};
|
||||
args.ContextMenuItems.Add(convertToMp3MenuItem);
|
||||
|
||||
convertToMp3MenuItem.Click += (_, _) => ConvertToMp3Clicked?.Invoke(this, entry.LibraryBook);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region Force Re-Download
|
||||
if (!entry.Liberate.IsSeries)
|
||||
{
|
||||
var reDownloadMenuItem = new MenuItem()
|
||||
{
|
||||
Header = "Re-download this audiobook",
|
||||
IsEnabled = entry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated
|
||||
};
|
||||
|
||||
args.ContextMenuItems.Add(reDownloadMenuItem);
|
||||
reDownloadMenuItem.Click += (s, _) =>
|
||||
{
|
||||
//No need to persist this change. It only needs to last long for the file to start downloading
|
||||
entry.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
|
||||
LiberateClicked?.Invoke(s, entry.LibraryBook);
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
args.ContextMenuItems.Add(new Separator());
|
||||
|
||||
#region View Bookmarks/Clips
|
||||
|
||||
if (!entry.Liberate.IsSeries)
|
||||
{
|
||||
|
||||
var bookRecordMenuItem = new MenuItem { Header = "View _Bookmarks/Clips" };
|
||||
|
||||
args.ContextMenuItems.Add(bookRecordMenuItem);
|
||||
|
||||
bookRecordMenuItem.Click += async (_, _) => await new BookRecordsDialog(entry.LibraryBook).ShowDialog(VisualRoot as Window);
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region View All Series
|
||||
|
||||
if (entry.Book.SeriesLink.Any())
|
||||
{
|
||||
var header = entry.Liberate.IsSeries ? "View All Episodes in Series" : "View All Books in Series";
|
||||
var viewSeriesMenuItem = new MenuItem { Header = header };
|
||||
|
||||
args.ContextMenuItems.Add(viewSeriesMenuItem);
|
||||
|
||||
viewSeriesMenuItem.Click += (_, _) => new SeriesViewDialog(entry.LibraryBook).Show();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
36
Source/LibationFileManager/Configuration.HelpText.cs
Normal file
36
Source/LibationFileManager/Configuration.HelpText.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
{
|
||||
public static ReadOnlyDictionary<string, string> HelpText { get; } = new Dictionary<string, string>
|
||||
{
|
||||
{ nameof(CombineNestedChapterTitles),"""
|
||||
If the book has nested chapters, e.g. a chapter named "Part 1"
|
||||
that contains chapters "Chapter 1" and "Chapter 2", then combine
|
||||
the chapter titles like the following example:
|
||||
|
||||
Part 1: Chapter 1
|
||||
Part 1: Chapter 2
|
||||
"""},
|
||||
{nameof(AllowLibationFixup), """
|
||||
In addition to the options that are enabled if you allow
|
||||
"fixing up" the audiobook, it does the following:
|
||||
|
||||
* Sets the ©gen metadata tag for the genres.
|
||||
* Adds the TCOM (@wrt in M4B files) metadata tag for the narrators.
|
||||
* Unescapes the copyright symbol (replace © with ©)
|
||||
* Replaces the recording copyright (P) string with ℗
|
||||
* Adds various other metadata tags recognized by AudiobookShelf
|
||||
* Sets the embedded cover art image with cover art retrieved from Audible
|
||||
""" },
|
||||
}
|
||||
.AsReadOnly();
|
||||
|
||||
public static string GetHelpText(string settingName)
|
||||
=> HelpText.TryGetValue(settingName, out var value) ? value : null;
|
||||
|
||||
}
|
||||
}
|
||||
@ -73,9 +73,12 @@ namespace LibationFileManager
|
||||
|
||||
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
|
||||
|
||||
[Description("Set cover art as the folder's icon. (Windows and macOS only)")]
|
||||
[Description("Set cover art as the folder's icon.")]
|
||||
public bool UseCoverAsFolderIcon { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Save audiobook metadata to metadata.json")]
|
||||
public bool SaveMetadataToFile { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")]
|
||||
public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
@ -164,6 +167,9 @@ namespace LibationFileManager
|
||||
[Description("Save cover image alongside audiobook?")]
|
||||
public bool DownloadCoverArt { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Combine nested chapter titles")]
|
||||
public bool CombineNestedChapterTitles { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Download clips and bookmarks?")]
|
||||
public bool DownloadClipsBookmarks { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
@ -29,6 +33,39 @@ namespace LibationUiBase.GridView
|
||||
LoadCover();
|
||||
}
|
||||
|
||||
|
||||
public static async Task<List<IGridEntry>> GetAllProductsAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||
{
|
||||
var products = libraryBooks.Where(lb => lb.Book.IsProduct()).ToArray();
|
||||
|
||||
int parallelism = int.Max(1, Environment.ProcessorCount - 1);
|
||||
|
||||
(int numPer, int rem) = int.DivRem(products.Length, parallelism);
|
||||
if (rem != 0) numPer++;
|
||||
|
||||
var tasks = new Task<IGridEntry[]>[parallelism];
|
||||
var syncContext = SynchronizationContext.Current;
|
||||
|
||||
for (int i = 0; i < parallelism; i++)
|
||||
{
|
||||
int start = i * numPer;
|
||||
tasks[i] = Task.Run(() =>
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(syncContext);
|
||||
|
||||
int length = int.Min(numPer, products.Length - start);
|
||||
var result = new IGridEntry[length];
|
||||
|
||||
for (int j = 0; j < length; j++)
|
||||
result[j] = new LibraryBookEntry<TStatus>(products[start + j]);
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
return (await Task.WhenAll(tasks)).SelectMany(a => a).ToList();
|
||||
}
|
||||
|
||||
protected override string GetBookTags() => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase.GridView
|
||||
{
|
||||
@ -54,6 +58,60 @@ namespace LibationUiBase.GridView
|
||||
LoadCover();
|
||||
}
|
||||
|
||||
public static async Task<List<ISeriesEntry>> GetAllSeriesEntriesAsync(IEnumerable<LibraryBook> libraryBooks)
|
||||
{
|
||||
var seriesBooks = libraryBooks.Where(lb => lb.Book.IsEpisodeParent()).ToArray();
|
||||
var allEpisodes = libraryBooks.Where(lb => lb.Book.IsEpisodeChild()).ToArray();
|
||||
|
||||
int parallelism = int.Max(1, Environment.ProcessorCount - 1);
|
||||
|
||||
var tasks = new Task[parallelism];
|
||||
var syncContext = SynchronizationContext.Current;
|
||||
|
||||
var q = new BlockingCollection<(int, LibraryBook episode)>();
|
||||
|
||||
var seriesEntries = new ISeriesEntry[seriesBooks.Length];
|
||||
var seriesEpisodes = new ConcurrentBag<ILibraryBookEntry>[seriesBooks.Length];
|
||||
|
||||
for (int i = 0; i < parallelism; i++)
|
||||
{
|
||||
tasks[i] = Task.Run(() =>
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(syncContext);
|
||||
|
||||
while (q.TryTake(out var entry, -1))
|
||||
{
|
||||
var parent = seriesEntries[entry.Item1];
|
||||
var episodeBag = seriesEpisodes[entry.Item1];
|
||||
episodeBag.Add(new LibraryBookEntry<TStatus>(entry.episode, parent));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (int i = 0; i <seriesBooks.Length; i++)
|
||||
{
|
||||
var series = seriesBooks[i];
|
||||
seriesEntries[i] = new SeriesEntry<TStatus>(series, Enumerable.Empty<LibraryBook>());
|
||||
seriesEpisodes[i] = new ConcurrentBag<ILibraryBookEntry>();
|
||||
|
||||
foreach (var ep in allEpisodes.FindChildren(series))
|
||||
q.Add((i, ep));
|
||||
}
|
||||
|
||||
q.CompleteAdding();
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
for (int i = 0; i < seriesBooks.Length; i++)
|
||||
{
|
||||
var series = seriesEntries[i];
|
||||
series.Children.AddRange(seriesEpisodes[i].OrderByDescending(c => c.SeriesOrder));
|
||||
series.UpdateLibraryBook(series.LibraryBook);
|
||||
}
|
||||
|
||||
return seriesEntries.Where(s => s.Children.Count != 0).ToList();
|
||||
}
|
||||
|
||||
public void RemoveChild(ILibraryBookEntry lbe)
|
||||
{
|
||||
Children.Remove(lbe);
|
||||
|
||||
@ -14,12 +14,16 @@ namespace LibationWinForms.Dialogs
|
||||
this.createCueSheetCbox.Text = desc(nameof(config.CreateCueSheet));
|
||||
this.downloadCoverArtCbox.Text = desc(nameof(config.DownloadCoverArt));
|
||||
this.retainAaxFileCbox.Text = desc(nameof(config.RetainAaxFile));
|
||||
this.combineNestedChapterTitlesCbox.Text = desc(nameof(config.CombineNestedChapterTitles));
|
||||
this.splitFilesByChapterCbox.Text = desc(nameof(config.SplitFilesByChapter));
|
||||
this.mergeOpeningEndCreditsCbox.Text = desc(nameof(config.MergeOpeningAndEndCredits));
|
||||
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
|
||||
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
|
||||
this.moveMoovAtomCbox.Text = desc(nameof(config.MoveMoovToBeginning));
|
||||
|
||||
toolTip.SetToolTip(combineNestedChapterTitlesCbox, Configuration.GetHelpText(nameof(config.CombineNestedChapterTitles)));
|
||||
toolTip.SetToolTip(allowLibationFixupCbox, Configuration.GetHelpText(nameof(config.AllowLibationFixup)));
|
||||
|
||||
fileDownloadQualityCb.Items.AddRange(
|
||||
new object[]
|
||||
{
|
||||
@ -55,6 +59,7 @@ namespace LibationWinForms.Dialogs
|
||||
fileDownloadQualityCb.SelectedItem = config.FileDownloadQuality;
|
||||
clipsBookmarksFormatCb.SelectedItem = config.ClipsBookmarksFileFormat;
|
||||
retainAaxFileCbox.Checked = config.RetainAaxFile;
|
||||
combineNestedChapterTitlesCbox.Checked = config.CombineNestedChapterTitles;
|
||||
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
|
||||
mergeOpeningEndCreditsCbox.Checked = config.MergeOpeningAndEndCredits;
|
||||
stripUnabridgedCbox.Checked = config.StripUnabridged;
|
||||
@ -99,6 +104,7 @@ namespace LibationWinForms.Dialogs
|
||||
config.FileDownloadQuality = (Configuration.DownloadQuality)fileDownloadQualityCb.SelectedItem;
|
||||
config.ClipsBookmarksFileFormat = (Configuration.ClipBookmarkFormat)clipsBookmarksFormatCb.SelectedItem;
|
||||
config.RetainAaxFile = retainAaxFileCbox.Checked;
|
||||
config.CombineNestedChapterTitles = combineNestedChapterTitlesCbox.Checked;
|
||||
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
|
||||
config.MergeOpeningAndEndCredits = mergeOpeningEndCreditsCbox.Checked;
|
||||
config.StripUnabridged = stripUnabridgedCbox.Checked;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -32,6 +32,7 @@ namespace LibationWinForms.Dialogs
|
||||
badBookRetryRb.Text = Configuration.BadBookAction.Retry.GetDescription();
|
||||
badBookIgnoreRb.Text = Configuration.BadBookAction.Ignore.GetDescription();
|
||||
useCoverAsFolderIconCb.Text = desc(nameof(config.UseCoverAsFolderIcon));
|
||||
saveMetadataToFileCbox.Text = desc(nameof(config.SaveMetadataToFile));
|
||||
|
||||
inProgressSelectControl.SetDirectoryItems(new()
|
||||
{
|
||||
@ -60,6 +61,7 @@ namespace LibationWinForms.Dialogs
|
||||
fileTemplateTb.Text = config.FileTemplate;
|
||||
chapterFileTemplateTb.Text = config.ChapterFileTemplate;
|
||||
useCoverAsFolderIconCb.Checked = config.UseCoverAsFolderIcon;
|
||||
saveMetadataToFileCbox.Checked = config.SaveMetadataToFile;
|
||||
}
|
||||
|
||||
private void Save_DownloadDecrypt(Configuration config)
|
||||
@ -77,6 +79,7 @@ namespace LibationWinForms.Dialogs
|
||||
config.FileTemplate = fileTemplateTb.Text;
|
||||
config.ChapterFileTemplate = chapterFileTemplateTb.Text;
|
||||
config.UseCoverAsFolderIcon = useCoverAsFolderIconCb.Checked;
|
||||
config.SaveMetadataToFile = saveMetadataToFileCbox.Checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,12 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
private Configuration config { get; } = Configuration.Instance;
|
||||
private Func<string, string> desc { get; } = Configuration.GetDescription;
|
||||
private readonly ToolTip toolTip = new ToolTip
|
||||
{
|
||||
InitialDelay = 300,
|
||||
AutoPopDelay = 10000,
|
||||
ReshowDelay = 0
|
||||
};
|
||||
|
||||
public SettingsDialog()
|
||||
{
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Threading;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
@ -14,7 +16,6 @@ namespace LibationWinForms
|
||||
beginBookBackupsToolStripMenuItem.Format(0);
|
||||
beginPdfBackupsToolStripMenuItem.Format(0);
|
||||
|
||||
Load += setBackupCounts;
|
||||
LibraryCommands.LibrarySizeChanged += setBackupCounts;
|
||||
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
|
||||
|
||||
@ -40,7 +41,11 @@ namespace LibationWinForms
|
||||
while (runBackupCountsAgain)
|
||||
{
|
||||
runBackupCountsAgain = false;
|
||||
e.Result = LibraryCommands.GetCounts();
|
||||
|
||||
if (e.Argument is not IEnumerable<LibraryBook> lbs)
|
||||
lbs = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
e.Result = LibraryCommands.GetCounts(lbs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,10 +4,8 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Threading;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.Dialogs;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
@ -17,10 +15,6 @@ namespace LibationWinForms
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// Pre-requisite:
|
||||
// Before calling anything else, including subscribing to events, ensure database exists. If we wait and let it happen lazily, race conditions and errors are likely during new installs
|
||||
using var _ = DbContexts.GetContext();
|
||||
|
||||
this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||
|
||||
@ -57,8 +51,7 @@ namespace LibationWinForms
|
||||
|
||||
// Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1'
|
||||
{
|
||||
this.Load += (_, __) => productsDisplay.Display();
|
||||
LibraryCommands.LibrarySizeChanged += (_, __) => this.UIThreadAsync(() => productsDisplay.Display());
|
||||
LibraryCommands.LibrarySizeChanged += (_, __) => Invoke(() => productsDisplay.DisplayAsync());
|
||||
}
|
||||
Shown += Form1_Shown;
|
||||
}
|
||||
@ -78,6 +71,13 @@ namespace LibationWinForms
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InitLibraryAsync(List<LibraryBook> libraryBooks)
|
||||
{
|
||||
runBackupCountsAgain = true;
|
||||
updateCountsBw.RunWorkerAsync(libraryBooks.Where(b => !b.Book.IsEpisodeParent()));
|
||||
await productsDisplay.DisplayAsync(libraryBooks);
|
||||
}
|
||||
|
||||
private void Form1_Load(object sender, EventArgs e)
|
||||
{
|
||||
if (this.DesignMode)
|
||||
|
||||
@ -30,7 +30,6 @@ namespace LibationWinForms.GridView
|
||||
{
|
||||
SearchEngineCommands.SearchEngineUpdated += SearchEngineCommands_SearchEngineUpdated;
|
||||
ListChanged += GridEntryBindingList_ListChanged;
|
||||
refreshEntries();
|
||||
}
|
||||
|
||||
/// <returns>All items in the list, including those filtered out.</returns>
|
||||
|
||||
@ -206,6 +206,25 @@ namespace LibationWinForms.GridView
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region Force Re-Download
|
||||
if (!entry.Liberate.IsSeries)
|
||||
{
|
||||
var reDownloadMenuItem = new ToolStripMenuItem()
|
||||
{
|
||||
Text = "Re-download this audiobook",
|
||||
Enabled = entry.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated
|
||||
};
|
||||
|
||||
ctxMenu.Items.Add(reDownloadMenuItem);
|
||||
reDownloadMenuItem.Click += (s, _) =>
|
||||
{
|
||||
//No need to persist this change. It only needs to last long for the file to start downloading
|
||||
entry.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
|
||||
LiberateClicked?.Invoke(s, entry.LibraryBook);
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
ctxMenu.Items.Add(new ToolStripSeparator());
|
||||
|
||||
#region View Bookmarks/Clips
|
||||
@ -306,22 +325,22 @@ namespace LibationWinForms.GridView
|
||||
|
||||
#region UI display functions
|
||||
|
||||
public void Display()
|
||||
public async Task DisplayAsync(List<LibraryBook> libraryBooks = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// don't return early if lib size == 0. this will not update correctly if all books are removed
|
||||
var lib = DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
|
||||
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking(includeParents: true);
|
||||
|
||||
if (!hasBeenDisplayed)
|
||||
{
|
||||
// bind
|
||||
productsGrid.BindToGrid(lib);
|
||||
await productsGrid.BindToGridAsync(libraryBooks);
|
||||
hasBeenDisplayed = true;
|
||||
InitialLoaded?.Invoke(this, new());
|
||||
}
|
||||
else
|
||||
productsGrid.UpdateGrid(lib);
|
||||
productsGrid.UpdateGrid(libraryBooks);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -5,8 +5,11 @@ using LibationUiBase.GridView;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
@ -53,15 +56,11 @@ namespace LibationWinForms.GridView
|
||||
if (e.RowIndex < 0)
|
||||
return;
|
||||
|
||||
// cover
|
||||
else if (e.ColumnIndex == coverGVColumn.Index)
|
||||
return;
|
||||
|
||||
e.ContextMenuStrip = new ContextMenuStrip();
|
||||
// any non-stop light
|
||||
if (e.ColumnIndex != liberateGVColumn.Index)
|
||||
// any column except cover & stop light
|
||||
if (e.ColumnIndex != liberateGVColumn.Index && e.ColumnIndex != coverGVColumn.Index)
|
||||
{
|
||||
e.ContextMenuStrip.Items.Add("Copy", null, (_, __) =>
|
||||
e.ContextMenuStrip.Items.Add("Copy Cell Contents", null, (_, __) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -70,14 +69,13 @@ namespace LibationWinForms.GridView
|
||||
Clipboard.SetDataObject(text, false, 5, 150);
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var entry = getGridEntry(e.RowIndex);
|
||||
var name = gridEntryDataGridView.Columns[e.ColumnIndex].DataPropertyName;
|
||||
LiberateContextMenuStripNeeded?.Invoke(entry, e.ContextMenuStrip);
|
||||
});
|
||||
e.ContextMenuStrip.Items.Add(new ToolStripSeparator());
|
||||
}
|
||||
|
||||
var entry = getGridEntry(e.RowIndex);
|
||||
var name = gridEntryDataGridView.Columns[e.ColumnIndex].DataPropertyName;
|
||||
LiberateContextMenuStripNeeded?.Invoke(entry, e.ContextMenuStrip);
|
||||
}
|
||||
|
||||
private void EnableDoubleBuffering()
|
||||
@ -160,27 +158,23 @@ namespace LibationWinForms.GridView
|
||||
}
|
||||
}
|
||||
|
||||
internal void BindToGrid(List<LibraryBook> dbBooks)
|
||||
internal async Task BindToGridAsync(List<LibraryBook> dbBooks)
|
||||
{
|
||||
var geList = dbBooks
|
||||
.Where(lb => lb.Book.IsProduct())
|
||||
.Select(b => new LibraryBookEntry<WinFormsEntryStatus>(b))
|
||||
.ToList<IGridEntry>();
|
||||
var geList = await LibraryBookEntry<WinFormsEntryStatus>.GetAllProductsAsync(dbBooks);
|
||||
|
||||
var episodes = dbBooks.Where(lb => lb.Book.IsEpisodeChild());
|
||||
var seriesEntries = await SeriesEntry<WinFormsEntryStatus>.GetAllSeriesEntriesAsync(dbBooks);
|
||||
|
||||
var seriesBooks = dbBooks.Where(lb => lb.Book.IsEpisodeParent()).ToList();
|
||||
geList.AddRange(seriesEntries);
|
||||
//Sort descending by date (default sort property)
|
||||
var comparer = new RowComparer();
|
||||
geList.Sort((a, b) => comparer.Compare(b, a));
|
||||
|
||||
foreach (var parent in seriesBooks)
|
||||
//Add all children beneath their parent
|
||||
foreach (var series in seriesEntries)
|
||||
{
|
||||
var seriesEpisodes = episodes.FindChildren(parent);
|
||||
|
||||
if (!seriesEpisodes.Any()) continue;
|
||||
|
||||
var seriesEntry = new SeriesEntry<WinFormsEntryStatus>(parent, seriesEpisodes);
|
||||
|
||||
geList.Add(seriesEntry);
|
||||
geList.AddRange(seriesEntry.Children);
|
||||
var seriesIndex = geList.IndexOf(series);
|
||||
foreach (var child in series.Children)
|
||||
geList.Insert(++seriesIndex, child);
|
||||
}
|
||||
|
||||
bindingList = new GridEntryBindingList(geList);
|
||||
|
||||
@ -249,9 +249,12 @@ namespace LibationWinForms.ProcessQueue
|
||||
|
||||
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
|
||||
{
|
||||
byte[] coverData = PictureStorage
|
||||
.GetPictureSynchronously(
|
||||
new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500));
|
||||
var quality
|
||||
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High
|
||||
? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
|
||||
: new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
|
||||
|
||||
byte[] coverData = PictureStorage.GetPictureSynchronously(quality);
|
||||
|
||||
AudioDecodable_CoverImageDiscovered(this, coverData);
|
||||
return coverData;
|
||||
|
||||
@ -2,12 +2,13 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using AppScaffolding;
|
||||
using Dinah.Core;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.Dialogs;
|
||||
using Serilog;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
@ -20,6 +21,7 @@ namespace LibationWinForms
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
Task<List<LibraryBook>> libraryLoadTask;
|
||||
try
|
||||
{
|
||||
//// Uncomment to see Console. Must be called before anything writes to Console.
|
||||
@ -48,6 +50,17 @@ namespace LibationWinForms
|
||||
// migrations which require Forms or are long-running
|
||||
RunWindowsOnlyMigrations(config);
|
||||
|
||||
//*******************************************************************//
|
||||
// //
|
||||
// Start loading the library as soon as possible //
|
||||
// //
|
||||
// Before calling anything else, including subscribing to events, //
|
||||
// to ensure database exists. If we wait and let it happen lazily, //
|
||||
// race conditions and errors are likely during new installs //
|
||||
// //
|
||||
//*******************************************************************//
|
||||
libraryLoadTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
|
||||
MessageBoxLib.VerboseLoggingWarning_ShowIfTrue();
|
||||
|
||||
// logging is init'd here
|
||||
@ -71,7 +84,9 @@ namespace LibationWinForms
|
||||
// global exception handling (ShowAdminAlert) attempts to use logging. only call it after logging has been init'd
|
||||
postLoggingGlobalExceptionHandling();
|
||||
|
||||
Application.Run(new Form1());
|
||||
var form1 = new Form1();
|
||||
form1.Load += async (_, _) => await form1.InitLibraryAsync(await libraryLoadTask);
|
||||
Application.Run(form1);
|
||||
}
|
||||
|
||||
private static void RunInstaller(Configuration config)
|
||||
|
||||
@ -540,7 +540,6 @@ namespace FileLiberator.Tests
|
||||
value[i].StartOffsetMs.Should().Be(expected[i].StartOffsetMs);
|
||||
value[i].StartOffsetSec.Should().Be(expected[i].StartOffsetSec);
|
||||
value[i].LengthMs.Should().Be(expected[i].LengthMs);
|
||||
value[i].Chapters.Should().BeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user