Compare commits

...

33 Commits

Author SHA1 Message Date
Robert McRackan
012d94a146 incr ver 2023-03-29 18:02:33 -04:00
Mbucari
22bd1ed121 Fix autoscan bug 2023-03-29 15:54:46 -06:00
Mbucari
c832f26b08 Merge pull request #561 from Mbucari/master
Try fix #560
2023-03-29 15:40:52 -06:00
Mbucari
efd73d334e inv ver 2023-03-29 15:39:25 -06:00
Mbucari
0db3ee6fd7 Fix library scan bug 2023-03-29 15:38:57 -06:00
Robert McRackan
6aaf4f63d1 incr major ver 2023-03-29 15:58:57 -04:00
rmcrackan
ab392a9285 Merge pull request #558 from Mbucari/master
Refined Walkthrough
2023-03-29 15:54:15 -04:00
Mbucari
efc9ff4bd8 Disable buttons on new row 2023-03-29 13:31:39 -06:00
Mbucari
a52b466c85 Fix QuickFilter Walkthrough 2023-03-29 13:17:31 -06:00
Mbucari
5611431abf Quick Filters display moveup and movedown buttons appropriately 2023-03-29 13:06:18 -06:00
Mbucari
a75932d1f4 Refine Walkthrough 2023-03-29 11:35:17 -06:00
Mbucari
6c8464b650 Use HashSet 2023-03-29 11:32:07 -06:00
rmcrackan
ba4a1c5a51 Merge pull request #554 from Mbucari/master
Bug fixes and guided tour
2023-03-28 16:49:49 -04:00
Mbucari
3681c0f18f Final walkthrough tweaks 2023-03-28 14:08:51 -06:00
Mbucari
e365ba7296 Use AvaloniaList properties 2023-03-28 13:29:07 -06:00
Mbucari
2afb5365dd Add search and quick filters to walkthrough 2023-03-28 12:30:05 -06:00
Mbucari
00cf7693d5 Add code comments 2023-03-28 10:02:22 -06:00
MBucari
dac6877a06 Fix #556 2023-03-28 07:09:46 -06:00
MBucari
36005508a1 Allow users to cancel walkthrough 2023-03-27 20:24:15 -06:00
MBucari
d9e27fd32e Bring cover viewer to front 2023-03-27 19:56:50 -06:00
MBucari
d86bcbb414 Add usings 2023-03-27 19:52:26 -06:00
MBucari
00cbab5b58 Update window title 2023-03-27 19:51:10 -06:00
MBucari
807725f6ff Replace editable DataGridTextColumn with TextBox (#552) 2023-03-27 19:40:23 -06:00
MBucari
ec9356b36e Do not import orphaned episodes (#553) 2023-03-27 18:58:43 -06:00
MBucari
add31024da Improve book availability detection (#551) 2023-03-27 17:53:25 -06:00
MBucari
27d2ada5a4 Don't warn for blank password with external login 2023-03-27 17:23:46 -06:00
Mbucari
702219ee69 Add guided walkthrough 2023-03-27 16:18:21 -06:00
Mbucari
cdf1a01457 Do not launch settings dialog after installation 2023-03-27 13:18:37 -06:00
Mbucari
a71ccbac6e Add Series Order column 2023-03-27 12:13:56 -06:00
Mbucari
f8c6b836c3 Merge branch 'rmcrackan:master' into master 2023-03-27 11:15:19 -06:00
Michael Bucari-Tovo
090871f50d More migrations to Avalonia 11.0.0-preview6 2023-03-27 11:14:54 -06:00
Mbucari
68af6a5ebb Merge branch 'rmcrackan:master' into master 2023-03-26 21:04:53 -06:00
Michael Bucari-Tovo
8bba8538d5 Recheck for partially downloaded files. 2023-03-26 20:54:29 -06:00
117 changed files with 1432 additions and 720 deletions

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>9.4.7.1</Version>
<Version>10.0.2.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Octokit" Version="5.0.2" />

View File

@@ -1,5 +1,4 @@
using NPOI.XWPF.UserModel;
using System;
using System;
using System.Text.RegularExpressions;
namespace AppScaffolding

View File

@@ -12,7 +12,6 @@ using DtoImporterService;
using FileManager;
using LibationFileManager;
using Newtonsoft.Json.Linq;
using NPOI.OpenXmlFormats.Spreadsheet;
using Serilog;
using static DtoImporterService.PerfLogger;
@@ -21,7 +20,7 @@ namespace ApplicationServices
public static class LibraryCommands
{
public static event EventHandler<int> ScanBegin;
public static event EventHandler ScanEnd;
public static event EventHandler<int> ScanEnd;
public static bool Scanning { get; private set; }
private static object _lock { get; } = new();
@@ -95,7 +94,7 @@ namespace ApplicationServices
{
stop();
var putBreakPointHere = logOutput;
ScanEnd?.Invoke(null, null);
ScanEnd?.Invoke(null, 0);
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
}
}
@@ -108,7 +107,8 @@ namespace ApplicationServices
if (accounts is null || accounts.Length == 0)
return (0, 0);
try
int newCount = 0;
try
{
lock (_lock)
{
@@ -134,7 +134,7 @@ namespace ApplicationServices
Log.Logger.Information("Begin long-running import");
logTime($"pre {nameof(importIntoDbAsync)}");
var newCount = await importIntoDbAsync(importItems);
newCount = await importIntoDbAsync(importItems);
logTime($"post {nameof(importIntoDbAsync)}");
Log.Logger.Information($"Import complete. New count {newCount}");
@@ -167,7 +167,7 @@ namespace ApplicationServices
{
stop();
var putBreakPointHere = logOutput;
ScanEnd?.Invoke(null, null);
ScanEnd?.Invoke(null, newCount);
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
}
}
@@ -283,7 +283,8 @@ namespace ApplicationServices
catch(ImportValidationException ex)
{
await logDtoItemsAsync(ex.Items, ex.InnerExceptions.ToArray());
throw;
//If ImportValidationException is thrown, all Dto items get logged as part of the exception
throw new AggregateException(ex.InnerExceptions);
}
async Task logDtoItemsAsync(IEnumerable<AudibleApi.Common.Item> dtoItems, IEnumerable<Exception> exceptions = null)

View File

@@ -152,13 +152,17 @@ namespace AudibleUtilities
SetSeries(parent, children);
}
int orphansRemoved = items.RemoveAll(i => (i.IsEpisodes || i.IsSeriesParent) && i.Series is null);
if (orphansRemoved > 0)
Serilog.Log.Debug("{orphansRemoved} podcast orphans not imported", orphansRemoved);
sw.Stop();
totalTime += sw.Elapsed;
Serilog.Log.Logger.Information("Completed indexing series episodes after {elappsed_ms} ms.", sw.ElapsedMilliseconds);
Serilog.Log.Logger.Information($"Completed library scan in {totalTime.TotalMilliseconds:F0} ms.");
var allExceptions = IValidator.GetAllValidators().SelectMany(v => v.Validate(items));
if (allExceptions?.Any() is true)
var allExceptions = IValidator.GetAllValidators().SelectMany(v => v.Validate(items)).ToList();
if (allExceptions?.Count > 0)
throw new ImportValidationException(items, allExceptions);
return items;

View File

@@ -46,7 +46,7 @@ namespace DtoImporterService
var productIds = importItems
.Select(i => i.DtoItem.ProductId)
.Distinct()
.ToList();
.ToHashSet();
Cache = DbContext.Books
.GetBooks(b => productIds.Contains(b.AudibleProductId))

View File

@@ -41,11 +41,18 @@ namespace DtoImporterService
//
// CURRENT SOLUTION: don't re-insert
var existingEntries = DbContext.LibraryBooks.AsEnumerable().Where(l => l.Book is not null).ToDictionary(l => l.Book.AudibleProductId);
var hash = ToDictionarySafe(importItems, dto => dto.DtoItem.ProductId, tieBreak);
//When Books are upserted during the BookImporter run, they are linked to their LibraryBook in the DbContext
//instance. If a LibraryBook has a null book here, that means it's Book was not imported during by BookImporter.
//There should never be duplicates, but this is defensive.
var existingEntries = DbContext.LibraryBooks.AsEnumerable().Where(l => l.Book is not null).ToDictionarySafe(l => l.Book.AudibleProductId);
//If importItems are contains duplicates by asin, keep the Item that's "available"
var uniqueImportItems = ToDictionarySafe(importItems, dto => dto.DtoItem.ProductId, tieBreak);
int qtyNew = 0;
foreach (var item in hash.Values)
foreach (var item in uniqueImportItems.Values)
{
if (existingEntries.TryGetValue(item.DtoItem.ProductId, out LibraryBook existing))
{
@@ -109,7 +116,7 @@ namespace DtoImporterService
=> isPlusTitleUnavailable(item1) && !isPlusTitleUnavailable(item2) ? item2 : item1;
private static bool isPlusTitleUnavailable(ImportItem item)
=> item.DtoItem.IsAyce is true
&& item.DtoItem.Plans?.Any(p => p.IsAyce) is not true;
=> item.DtoItem.ContentType is null
|| (item.DtoItem.IsAyce is true && item.DtoItem.Plans?.Any(p => p.IsAyce) is not true);
}
}

View File

@@ -1,24 +1,24 @@
using Avalonia;
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using LibationFileManager;
using LibationAvalonia.Views;
using System;
using Avalonia.Platform;
using Avalonia.Styling;
using LibationAvalonia.Dialogs;
using System.Threading.Tasks;
using LibationAvalonia.Views;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.IO;
using ApplicationServices;
using Avalonia.Controls;
using Avalonia.Styling;
using System.Threading.Tasks;
namespace LibationAvalonia
{
public class App : Application
{
public static Window MainWindow { get;private set; }
public static Window MainWindow { get; private set; }
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
@@ -39,18 +39,15 @@ namespace LibationAvalonia
}
public static Task<List<DataLayer.LibraryBook>> LibraryTask;
public static bool SetupRequired;
public override void OnFrameworkInitializationCompleted()
{
LoadStyles();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (SetupRequired)
{
var config = Configuration.Instance;
var config = Configuration.Instance;
if (!config.LibationSettingsAreValid)
{
var defaultLibationFilesDir = Configuration.UserProfile;
// check for existing settings in default location
@@ -60,7 +57,7 @@ namespace LibationAvalonia
if (config.LibationSettingsAreValid)
{
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
ShowMainWindow(desktop);
}
else
@@ -86,11 +83,29 @@ namespace LibationAvalonia
{
// all returns should be preceded by either:
// - if config.LibationSettingsAreValid
// - error message, Exit()
// - error message, Exit()
if (setupDialog.IsNewUser)
{
Configuration.SetLibationFiles(Configuration.UserProfile);
ShowSettingsWindow(desktop, setupDialog.Config, OnSettingsCompleted);
setupDialog.Config.Books = Path.Combine(Configuration.UserProfile, nameof(Configuration.Books));
if (setupDialog.Config.LibationSettingsAreValid)
{
var theme
= setupDialog.SelectedTheme.Content is nameof(ThemeVariant.Dark)
? nameof(ThemeVariant.Dark)
: nameof(ThemeVariant.Light);
setupDialog.Config.SetString(theme, nameof(ThemeVariant));
await RunMigrationsAsync(setupDialog.Config);
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
ShowMainWindow(desktop);
}
else
await CancelInstallation();
}
else if (setupDialog.IsReturningUser)
{
@@ -130,40 +145,6 @@ namespace LibationAvalonia
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
}
private void ShowSettingsWindow(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, SettingsDialog, Configuration> OnClose)
{
config.Books ??= Path.Combine(Configuration.UserProfile, "Books");
var settingsDialog = new SettingsDialog();
desktop.MainWindow = settingsDialog;
settingsDialog.RestoreSizeAndLocation(Configuration.Instance);
settingsDialog.Show();
void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
{
settingsDialog.Closing -= WindowClosing;
e.Cancel = true;
OnClose?.Invoke(desktop, settingsDialog, config);
}
settingsDialog.Closing += WindowClosing;
}
private async void OnSettingsCompleted(IClassicDesktopStyleApplicationLifetime desktop, SettingsDialog settingsDialog, Configuration config)
{
if (config.LibationSettingsAreValid)
{
await RunMigrationsAsync(config);
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
ShowMainWindow(desktop);
}
else
await CancelInstallation();
settingsDialog.Close();
}
private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, LibationFilesDialog, Configuration> OnClose)
{
var libationFilesDialog = new LibationFilesDialog();
@@ -200,11 +181,23 @@ namespace LibationAvalonia
MessageBoxIcon.Question);
if (continueResult == DialogResult.Yes)
ShowSettingsWindow(desktop, config, OnSettingsCompleted);
{
config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books));
if (config.LibationSettingsAreValid)
{
await RunMigrationsAsync(config);
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
ShowMainWindow(desktop);
}
else
await CancelInstallation();
}
else
await CancelInstallation();
}
libationFilesDialog.Close();
}
@@ -230,11 +223,11 @@ namespace LibationAvalonia
private static void LoadStyles()
{
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookFailedBrush");
ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCompletedBrush");
ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCancelledBrush");
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources("SeriesEntryGridBackgroundBrush");
ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookDefaultBrush");
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush));
ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCompletedBrush));
ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookCancelledBrush));
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources(nameof(SeriesEntryGridBackgroundBrush));
ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookDefaultBrush));
HyperlinkVisited = AvaloniaUtils.GetBrushFromResources(nameof(HyperlinkVisited));
}
}

View File

@@ -13,7 +13,7 @@ namespace LibationAvalonia
public static IBrush GetBrushFromResources(string name)
=> GetBrushFromResources(name, Brushes.Transparent);
public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
{
{
if (App.Current.TryGetResource(name, App.Current.ActualThemeVariant, out var value) && value is IBrush brush)
return brush;
return defaultBrush;

View File

@@ -3,9 +3,6 @@ using Avalonia.Collections;
using Avalonia.Controls;
using LibationAvalonia.ViewModels;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
namespace LibationAvalonia.Controls
{
@@ -15,7 +12,7 @@ namespace LibationAvalonia.Controls
AvaloniaProperty.Register<CheckedListBox, AvaloniaList<CheckBoxViewModel>>(nameof(Items));
public AvaloniaList<CheckBoxViewModel> Items { get => GetValue(ItemsProperty); set => SetValue(ItemsProperty, value); }
private CheckedListBoxViewModel _viewModel = new();
private CheckedListBoxViewModel _viewModel = new();
public CheckedListBox()
{

View File

@@ -3,7 +3,7 @@ using LibationUiBase.GridView;
namespace LibationAvalonia.Controls
{
public class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
public class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
{
protected override Control GenerateEditingElementDirect(DataGridCell cell, object dataItem)
{

View File

@@ -15,7 +15,7 @@ namespace LibationAvalonia.Controls
static DataGridContextMenus()
{
ContextMenu.Items = MenuItems;
ContextMenu.ItemsSource = MenuItems;
OwningColumnProperty = typeof(DataGridCell).GetProperty("OwningColumn", BindingFlags.Instance | BindingFlags.NonPublic);
}
@@ -66,6 +66,6 @@ namespace LibationAvalonia.Controls
public IGridEntry GridEntry { get; init; }
public ContextMenu ContextMenu { get; init; }
public AvaloniaList<Control> ContextMenuItems
=> ContextMenu.Items as AvaloniaList<Control>;
=> ContextMenu.ItemsSource as AvaloniaList<Control>;
}
}

View File

@@ -4,7 +4,6 @@ using Avalonia.Data;
using Avalonia.Interactivity;
using DataLayer;
using ReactiveUI;
using System;
namespace LibationAvalonia.Controls
{

View File

@@ -1,6 +1,4 @@
using Avalonia.Controls;
using System;
using System.Linq;
namespace LibationAvalonia.Controls
{

View File

@@ -1,10 +1,9 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Dinah.Core;
using LibationFileManager;
using System.Collections.Generic;
using ReactiveUI;
using System.Collections.Generic;
using System.Linq;
namespace LibationAvalonia.Controls
@@ -56,12 +55,12 @@ namespace LibationAvalonia.Controls
directorySelectControl.PropertyChanged += DirectorySelectControl_PropertyChanged;
}
private class CustomState: ViewModels.ViewModelBase
private class CustomState : ViewModels.ViewModelBase
{
private string _customDir;
private bool _knownChecked;
private bool _customChecked;
public string CustomDir { get=> _customDir; set => this.RaiseAndSetIfChanged(ref _customDir, value); }
public string CustomDir { get => _customDir; set => this.RaiseAndSetIfChanged(ref _customDir, value); }
public bool KnownChecked
{
get => _knownChecked;
@@ -141,7 +140,7 @@ namespace LibationAvalonia.Controls
var known = Configuration.GetKnownDirectory(noSubDir);
if (known == Configuration.KnownDirectories.None && noSubDir == Configuration.AppDir_Absolute)
known = Configuration.KnownDirectories.AppDir;
known = Configuration.KnownDirectories.AppDir;
if (known is Configuration.KnownDirectories.None)
{

View File

@@ -1,13 +1,12 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Dinah.Core;
using LibationFileManager;
using System.Collections.Generic;
using Avalonia.Data.Converters;
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Data;
using System.IO;
using System.Reactive.Subjects;

View File

@@ -1,6 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LibationAvalonia.Controls
{

View File

@@ -1,8 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Styling;
using System;

View File

@@ -18,7 +18,7 @@ namespace LibationAvalonia.Controls
public bool IsEditingMode { get; set; }
public Rating Rating { get => GetValue(RatingProperty); set => SetValue(RatingProperty, value); }
public MyRatingCellEditor()
{
InitializeComponent();

View File

@@ -1,11 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
using System;
using System.Collections;
using System.Linq;
namespace LibationAvalonia.Controls
{

View File

@@ -64,11 +64,14 @@
<DataGridCheckBoxColumn
Binding="{Binding LibraryScan, Mode=TwoWay}"
Header="Include in&#xa;library scan?"/>
<DataGridTextColumn
Width="2*"
Binding="{Binding AccountId, Mode=TwoWay}"
Header="Audible&#xa;email/login"/>
<DataGridTemplateColumn Width="2*" Header="Audible&#xa;email/login">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding AccountId, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="Auto" Header="Locale">
<DataGridTemplateColumn.CellTemplate>
@@ -93,10 +96,13 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="3*"
Binding="{Binding AccountName, Mode=TwoWay}"
Header="Account Nickname&#xa;(optional)"/>
<DataGridTemplateColumn Width="3*" Header="Account Nickname&#xa;(optional)">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding AccountName, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>

View File

@@ -1,27 +1,24 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ReactiveUI;
using AudibleApi;
using Avalonia.Platform.Storage;
using LibationFileManager;
using Avalonia.Platform.Storage.FileIO;
namespace LibationAvalonia.Dialogs
{
public partial class AccountsDialog : DialogWindow
{
public ObservableCollection<AccountDto> Accounts { get; } = new();
public AvaloniaList<AccountDto> Accounts { get; } = new();
public class AccountDto : ViewModels.ViewModelBase
{
private string _accountId;
private Locale _selectedLocale;
public IReadOnlyList<Locale> Locales => AccountsDialog.Locales;
public bool LibraryScan { get; set; } = true;
public string AccountId
@@ -31,19 +28,21 @@ namespace LibationAvalonia.Dialogs
{
this.RaiseAndSetIfChanged(ref _accountId, value);
this.RaisePropertyChanged(nameof(IsDefault));
}
}
public Locale SelectedLocale
{
get => _selectedLocale;
set
{
this.RaiseAndSetIfChanged(ref _selectedLocale, value);
this.RaisePropertyChanged(nameof(IsDefault));
}
}
public Locale SelectedLocale { get; set; }
public string AccountName { get; set; }
public bool IsDefault => string.IsNullOrEmpty(AccountId) && SelectedLocale is null;
public bool IsDefault => string.IsNullOrEmpty(AccountId);
public AccountDto() { }
public AccountDto(Account account)
{
LibraryScan = account.LibraryScan;
AccountId = account.AccountId;
SelectedLocale = Locales.Single(l => l.Name == account.Locale.Name);
AccountName = account.AccountName;
}
}
private static string GetAudibleCliAppDataPath()
@@ -58,57 +57,40 @@ namespace LibationAvalonia.Dialogs
// here: copy strings and dispose of persister
// only persist in 'save' step
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.Accounts;
if (accounts.Any())
{
foreach (var account in accounts)
AddAccountToGrid(account);
}
Accounts.CollectionChanged += Accounts_CollectionChanged;
Accounts.AddRange(persister.AccountsSettings.Accounts.Select(a => new AccountDto(a)));
DataContext = this;
addBlankAccount();
}
private void addBlankAccount()
{
var newBlank = new AccountDto();
newBlank.PropertyChanged += AccountDto_PropertyChanged;
Accounts.Insert(Accounts.Count, newBlank);
}
private void AddAccountToGrid(Account account)
private void Accounts_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
AccountDto accountDto = new()
if (e.Action is NotifyCollectionChangedAction.Add && e.NewItems?.Count > 0)
{
LibraryScan = account.LibraryScan,
AccountId = account.AccountId,
SelectedLocale = Locales.Single(l => l.Name == account.Locale.Name),
AccountName = account.AccountName,
};
accountDto.PropertyChanged += AccountDto_PropertyChanged;
//ObservableCollection doesn't fire CollectionChanged on Add, so use Insert instead
Accounts.Insert(Accounts.Count, accountDto);
foreach (var newItem in e.NewItems.OfType<AccountDto>())
newItem.PropertyChanged += AccountDto_PropertyChanged;
}
else if (e.Action is NotifyCollectionChangedAction.Remove && e.OldItems?.Count > 0)
{
foreach (var oldItem in e.OldItems.OfType<AccountDto>())
oldItem.PropertyChanged -= AccountDto_PropertyChanged;
}
}
private void addBlankAccount() => Accounts.Insert(Accounts.Count, new AccountDto());
private void AccountDto_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (Accounts.Any(a => a.IsDefault))
return;
addBlankAccount();
if (!Accounts.Any(a => a.IsDefault))
addBlankAccount();
}
public void DeleteButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (e.Source is Button expBtn && expBtn.DataContext is AccountDto acc)
{
var index = Accounts.IndexOf(acc);
if (index < 0) return;
acc.PropertyChanged -= AccountDto_PropertyChanged;
Accounts.Remove(acc);
}
}
public async void ImportButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -119,11 +101,11 @@ namespace LibationAvalonia.Dialogs
AllowMultiple = false,
FileTypeFilter = new FilePickerFileType[]
{
new("JSON files (*.json)")
{
Patterns = new[] { "*.json" },
AppleUniformTypeIdentifiers = new[] { "public.json" }
}
new("JSON files (*.json)")
{
Patterns = new[] { "*.json" },
AppleUniformTypeIdentifiers = new[] { "public.json" }
}
}
};
@@ -153,7 +135,7 @@ namespace LibationAvalonia.Dialogs
persister.AccountsSettings.Add(account);
AddAccountToGrid(account);
Accounts.Add(new AccountDto(account));
}
catch (Exception ex)
{

View File

@@ -1,11 +1,10 @@
using ApplicationServices;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using DataLayer;
using Dinah.Core;
using LibationFileManager;
using LibationAvalonia.ViewModels;
using LibationFileManager;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
@@ -42,14 +41,14 @@ namespace LibationAvalonia.Dialogs
LibraryBook = context.GetLibraryBook_Flat_NoTracking("B017V4IM1G");
}
}
public BookDetailsDialog(LibraryBook libraryBook) :this()
public BookDetailsDialog(LibraryBook libraryBook) : this()
{
LibraryBook = libraryBook;
}
protected override void SaveAndClose()
{
LibraryBook.Book.UpdateUserDefinedItem(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
{
LibraryBook.Book.UpdateUserDefinedItem(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
base.SaveAndClose();
}
@@ -61,7 +60,7 @@ namespace LibationAvalonia.Dialogs
}
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> SaveAndClose();
=> SaveAndClose();
private class BookDetailsDialogViewModel : ViewModelBase
{

View File

@@ -79,7 +79,7 @@ namespace LibationAvalonia.Dialogs
public void CheckAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
foreach (var record in bookRecordEntries)
foreach (var record in bookRecordEntries)
record.IsChecked = true;
}
public void UncheckAll_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)

View File

@@ -1,6 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System;
namespace LibationAvalonia.Dialogs
@@ -12,9 +11,7 @@ namespace LibationAvalonia.Dialogs
public DescriptionDisplayDialog()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
DescriptionTextBox = this.FindControl<TextBox>(nameof(DescriptionTextBox));
this.Activated += DescriptionDisplay_Activated;
Opened += DescriptionDisplay_Opened;

View File

@@ -1,5 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls;
using LibationFileManager;
using System;
using System.Threading.Tasks;
@@ -17,10 +16,6 @@ namespace LibationAvalonia.Dialogs
this.Initialized += DialogWindow_Initialized;
this.Opened += DialogWindow_Opened;
this.Closing += DialogWindow_Closing;
#if DEBUG
this.AttachDevTools();
#endif
}
public DialogWindow(bool saveAndRestorePosition) : this()
{

View File

@@ -42,13 +42,16 @@
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="*" Header="Filter">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding FilterString, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
Width="*"
IsReadOnly="False"
Binding="{Binding FilterString, Mode=TwoWay}"
Header="Filter"/>
<DataGridTemplateColumn Header="Move&#xa;Up">
<DataGridTemplateColumn.CellTemplate>

View File

@@ -1,9 +1,9 @@
using AudibleUtilities;
using Avalonia.Controls;
using LibationFileManager;
using ReactiveUI;
using System.Collections.ObjectModel;
using System.Linq;
using ReactiveUI;
namespace LibationAvalonia.Dialogs
{

View File

@@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
MinWidth="500" MinHeight="450"
Width="500" Height="450"
x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
Title="Illegal Character Replacement"
Icon="/Assets/libation.ico">
@@ -26,21 +27,31 @@
Items="{Binding replacements}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Char to&#xa;Replace">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox IsReadOnly="{Binding Mandatory}" Text="{Binding CharacterToReplace, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
IsReadOnly="False"
Binding="{Binding CharacterToReplace, Mode=TwoWay}"
Header="Char to&#xa;Replace"/>
<DataGridTemplateColumn Header="Replacement&#xa;Text">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding ReplacementText, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn
IsReadOnly="False"
Binding="{Binding ReplacementText, Mode=TwoWay}"
Header="Replacement&#xa;Text"/>
<DataGridTextColumn Width="*"
IsReadOnly="False"
Binding="{Binding Description, Mode=TwoWay}"
Header="Description"/>
<DataGridTemplateColumn Width="*" Header="Description">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox IsReadOnly="{Binding Mandatory}" Text="{Binding Description, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>

View File

@@ -1,12 +1,11 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Data;
using FileManager;
using LibationFileManager;
using System.Collections.Generic;
using ReactiveUI;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Data;
namespace LibationAvalonia.Dialogs
{
@@ -37,7 +36,7 @@ namespace LibationAvalonia.Dialogs
}
public void Defaults_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> LoadTable(ReplacementCharacters.Default.Replacements);
=> LoadTable(ReplacementCharacters.Default.Replacements);
public void LoFiDefaults_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> LoadTable(ReplacementCharacters.LoFiDefault.Replacements);
public void Barebones_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -50,7 +49,7 @@ namespace LibationAvalonia.Dialogs
protected override void SaveAndClose()
{
var replacements = SOURCE
.Where(r=> !r.IsDefault)
.Where(r => !r.IsDefault)
.Select(r => new Replacement(r.Character, r.ReplacementText, r.Description) { Mandatory = r.Mandatory })
.ToList();

View File

@@ -3,6 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
Width="800" Height="450"
x:Class="LibationAvalonia.Dialogs.EditTemplateDialog"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
Icon="/Assets/libation.ico"

View File

@@ -1,17 +1,15 @@
using Avalonia.Markup.Xaml;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Dinah.Core;
using LibationFileManager;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ReactiveUI;
using Avalonia.Controls.Documents;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Markup.Xaml.Templates;
namespace LibationAvalonia.Dialogs
{

View File

@@ -5,6 +5,7 @@
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="500"
x:Class="LibationAvalonia.Dialogs.ImageDisplayDialog"
MinWidth="500" MinHeight="500"
Width="500" Height="520"
Title="Cover"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">

View File

@@ -1,9 +1,8 @@
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using ReactiveUI;
using System;
using System.ComponentModel;
using ReactiveUI;
using Avalonia.Platform.Storage;
namespace LibationAvalonia.Dialogs
{

View File

@@ -1,10 +1,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LibationFileManager;
using LibationAvalonia.Controls;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs
{
@@ -18,7 +14,7 @@ namespace LibationAvalonia.Dialogs
Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyDocs
};
public string Directory { get; set; } = Configuration.GetKnownDirectoryPath(Configuration.KnownDirectories.UserProfile);
}
private DirSelectOptions dirSelectOptions;
@@ -28,9 +24,6 @@ namespace LibationAvalonia.Dialogs
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
DataContext = dirSelectOptions = new();
}

View File

@@ -8,6 +8,7 @@
Title="Liberated status: Whether the book has been downloaded"
MinHeight="100" MaxHeight="165"
MinWidth="600" MaxWidth="800"
Width="600"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">

View File

@@ -1,13 +1,11 @@
using Avalonia.Markup.Xaml;
namespace LibationAvalonia.Dialogs
{
public partial class LiberatedStatusBatchAutoDialog : DialogWindow
{
public bool SetDownloaded { get; set; }
public bool SetNotDownloaded { get; set; }
{
public bool SetDownloaded { get; set; }
public bool SetNotDownloaded { get; set; }
public LiberatedStatusBatchAutoDialog()
public LiberatedStatusBatchAutoDialog()
{
InitializeComponent();
DataContext = this;

View File

@@ -8,6 +8,7 @@
Title="Liberated status: Whether the book has been downloaded"
MinWidth="400" MinHeight="120"
MaxWidth="400" MaxHeight="120"
Width="400" Height="120"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">

View File

@@ -1,4 +1,3 @@
using Avalonia.Markup.Xaml;
using DataLayer;
using System.Collections;
using System.Collections.Generic;
@@ -34,13 +33,13 @@ namespace LibationAvalonia.Dialogs
new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" },
};
public LiberatedStatusBatchManualDialog(bool isPdf) : this()
{
if (isPdf)
this.Title = this.Title.Replace("book", "PDF");
}
public LiberatedStatusBatchManualDialog(bool isPdf) : this()
{
if (isPdf)
this.Title = this.Title.Replace("book", "PDF");
}
public LiberatedStatusBatchManualDialog()
public LiberatedStatusBatchManualDialog()
{
InitializeComponent();
SelectedItem = BookStatuses[0] as liberatedComboBoxItem;

View File

@@ -2,7 +2,6 @@ using ApplicationServices;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using DataLayer;
using LibationAvalonia.ViewModels;
using LibationFileManager;
@@ -53,7 +52,7 @@ namespace LibationAvalonia.Dialogs
private void LocateAudiobooks_FileFound(object sender, FilePathCache.CacheEntry e)
{
var newItem = new Tuple<string,string>($"[{e.Id}]", Path.GetFileName(e.Path));
var newItem = new Tuple<string, string>($"[{e.Id}]", Path.GetFileName(e.Path));
_viewModel.FoundFiles.Add(newItem);
foundAudiobooksLB.SelectedItem = newItem;
@@ -70,7 +69,7 @@ namespace LibationAvalonia.Dialogs
{
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();

View File

@@ -1,6 +1,3 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login

View File

@@ -1,7 +1,6 @@
using System;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi;
using AudibleUtilities;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{

View File

@@ -1,7 +1,7 @@
using System;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi;
using AudibleUtilities;
using System;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{

View File

@@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using LibationAvalonia.ViewModels;
using ReactiveUI;
@@ -21,7 +20,7 @@ namespace LibationAvalonia.Dialogs.Login
captchaBox = this.FindControl<TextBox>(nameof(captchaBox));
}
public CaptchaDialog(string password, byte[] captchaImage) :this()
public CaptchaDialog(string password, byte[] captchaImage) : this()
{
//Avalonia doesn't support animated gifs.
//Deconstruct gifs into frames and manually switch them.
@@ -34,7 +33,7 @@ namespace LibationAvalonia.Dialogs.Login
{
var frameMetadata = gif.Frames[i].Metadata.GetFormatMetadata(SixLabors.ImageSharp.Formats.Gif.GifFormat.Instance);
using var clonedFrame = gif.Frames.CloneFrame(i);
using var clonedFrame = gif.Frames.CloneFrame(i);
using var framems = new MemoryStream();
clonedFrame.Save(framems, gifEncoder);
@@ -66,7 +65,7 @@ namespace LibationAvalonia.Dialogs.Login
protected override async Task CancelAndCloseAsync()
{
await _viewModel.StopAsync();
await base.CancelAndCloseAsync();
await base.CancelAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -106,7 +105,7 @@ namespace LibationAvalonia.Dialogs.Login
private async Task SwitchFramesAsync(Bitmap[] gifFrames, int[] frameDelayMs)
{
int index = 0;
while(keepSwitching)
while (keepSwitching)
{
CaptchaImage = gifFrames[index];
await Task.Delay(frameDelayMs[index++]);

View File

@@ -1,7 +1,5 @@
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Dinah.Core;
using System.Linq;
using System.Threading.Tasks;

View File

@@ -1,8 +1,6 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System.Linq;
using System.Threading.Tasks;
@@ -26,7 +24,7 @@ namespace LibationAvalonia.Dialogs.Login
DataContext = this;
}
}
public LoginChoiceEagerDialog(Account account):this()
public LoginChoiceEagerDialog(Account account) : this()
{
Account = account;
DataContext = this;
@@ -34,7 +32,7 @@ namespace LibationAvalonia.Dialogs.Login
protected override async Task SaveAndCloseAsync()
{
if (string.IsNullOrWhiteSpace(Password))
if (LoginMethod is LoginMethod.Api && string.IsNullOrWhiteSpace(Password))
{
await MessageBox.Show(this, "Please enter your password");
return;

View File

@@ -1,7 +1,6 @@
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Dinah.Core;
using System;
using System.Linq;
@@ -28,7 +27,7 @@ namespace LibationAvalonia.Dialogs.Login
DataContext = this;
}
}
public LoginExternalDialog(Account account, string loginUrl):this()
public LoginExternalDialog(Account account, string loginUrl) : this()
{
Account = account;
ExternalLoginUrl = loginUrl;

View File

@@ -1,11 +1,10 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System.Threading.Tasks;
using Avalonia.Data;
using ReactiveUI;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Data;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{

View File

@@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
@@ -12,7 +11,7 @@ namespace LibationAvalonia.Dialogs.Login
public _2faCodeDialog()
{
AvaloniaXamlLoader.Load(this);
InitializeComponent();
_2FABox = this.FindControl<TextBox>(nameof(_2FABox));
}

View File

@@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Dinah.Core;
using FileManager;
using System;

View File

@@ -1,6 +1,3 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LibationAvalonia.ViewModels.Dialogs;
namespace LibationAvalonia.Dialogs
@@ -12,7 +9,7 @@ namespace LibationAvalonia.Dialogs
{
InitializeComponent();
}
public MessageBoxWindow(bool saveAndRestorePosition):base(saveAndRestorePosition)
public MessageBoxWindow(bool saveAndRestorePosition) : base(saveAndRestorePosition)
{
InitializeComponent();
}

View File

@@ -5,7 +5,7 @@
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="185"
x:Class="LibationAvalonia.Dialogs.ScanAccountsDialog"
MinWidth="500" MinHeight="160"
MaxWidth="500" MaxHeight="185"
Width="500" Height="200"
Title="Which Accounts?"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">

View File

@@ -1,12 +1,9 @@
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs
{

View File

@@ -5,6 +5,7 @@
mc:Ignorable="d" d:DesignWidth="950" d:DesignHeight="650"
MinWidth="950" MinHeight="650"
MaxWidth="950" MaxHeight="650"
Width="950" Height="650"
x:Class="LibationAvalonia.Dialogs.SearchSyntaxDialog"
Title="Filter Options"
WindowStartupLocation="CenterOwner"

View File

@@ -1,5 +1,3 @@
using Avalonia.Markup.Xaml;
namespace LibationAvalonia.Dialogs
{
public partial class SearchSyntaxDialog : DialogWindow

View File

@@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="750"
MinWidth="900" MinHeight="700"
Width="900" Height="700"
x:Class="LibationAvalonia.Dialogs.SettingsDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
Title="Edit Settings"
@@ -18,9 +19,10 @@
Height="30"
Padding="30,3,30,3"
Content="Save"
Name="saveBtn"
Click="SaveButton_Clicked" />
<TabControl Grid.Column="0">
<TabControl Name="tabControl" Grid.Column="0">
<TabControl.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="28"/>

View File

@@ -1,14 +1,14 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Dinah.Core;
using FileManager;
using LibationFileManager;
using LibationUiBase;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ReactiveUI;
using Dinah.Core;
using System.Linq;
using FileManager;
using Avalonia.Collections;
using LibationUiBase;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs
{
@@ -51,7 +51,7 @@ namespace LibationAvalonia.Dialogs
}
public async void EditFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
{
var newTemplate = await editTemplate(TemplateEditor<Templates.FileTemplate>.CreateFilenameEditor(config.Books, settingsDisp.DownloadDecryptSettings.FileTemplate));
if (newTemplate is not null)
settingsDisp.DownloadDecryptSettings.FileTemplate = newTemplate;
@@ -59,7 +59,7 @@ namespace LibationAvalonia.Dialogs
public async void EditChapterFileTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var newTemplate = await editTemplate(TemplateEditor<Templates.ChapterFileTemplate>.CreateFilenameEditor(config.Books, settingsDisp.DownloadDecryptSettings.ChapterFileTemplate));
if (newTemplate is not null)
settingsDisp.DownloadDecryptSettings.ChapterFileTemplate = newTemplate;
@@ -197,7 +197,7 @@ namespace LibationAvalonia.Dialogs
this.RaiseAndSetIfChanged(ref themeVariant, value);
SelectionChanged = ThemeVariant != InitialThemeVariant;
this.RaisePropertyChanged(nameof(SelectionChanged));
this.RaisePropertyChanged(nameof(SelectionChanged));
}
}
public string InitialThemeVariant { get; private set; }
@@ -242,7 +242,7 @@ namespace LibationAvalonia.Dialogs
public bool DownloadEpisodes { get; set; }
public bool AutoDownloadEpisodes { get; set; }
}
public class DownloadDecryptSettings : ViewModels.ViewModelBase, ISettingsDisplay
{
private bool _badBookAsk;
@@ -345,7 +345,7 @@ namespace LibationAvalonia.Dialogs
}
}
}
public bool BadBookRetry
public bool BadBookRetry
{
get => _badBookRetry;
set
@@ -392,7 +392,7 @@ namespace LibationAvalonia.Dialogs
public AvaloniaList<SampleRateSelection> SampleRates { get; }
= new(
new []
new[]
{
AAXClean.SampleRate.Hz_44100,
AAXClean.SampleRate.Hz_32000,

View File

@@ -3,14 +3,16 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="350"
MinWidth="500" MinHeight="350"
MaxWidth="500" MaxHeight="350"
x:Class="LibationAvalonia.Dialogs.SetupDialog"
WindowStartupLocation="CenterScreen"
Width="500" Height="350"
Icon="/Assets/libation.ico"
Title="Welcome to Libation">
<Grid Margin="10" ColumnDefinitions="*" RowDefinitions="*,Auto,Auto">
<Grid
Margin="10"
HorizontalAlignment="Stretch"
RowDefinitions="*,Auto,Auto">
<TextBlock Grid.Row="0" TextWrapping="Wrap" Text="This appears to be your first time using Libation or a previous setup was incomplete.
&#xa;
@@ -21,18 +23,48 @@
&#xa;
&#xa;Download your entire library from the &quot;Liberate&quot; tab or
&#xa;liberate your books one at a time by clicking the stoplight." />
<Button
Grid.Row="1"
Width="480"
Margin="0,0,0,10"
Click="NewUser_Click">
<Grid
Grid.Row="1"
HorizontalAlignment="Stretch"
ColumnDefinitions="*,Auto"
Margin="0,0,0,10">
<Button
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="0,20"
Margin="0,0,10,0"
Click="NewUser_Click">
<TextBlock
FontSize="18"
TextAlignment="Center"
Text="NEW USER"/>
</Button>
<Grid
Grid.Column="1"
RowDefinitions="*,*">
<TextBlock
TextAlignment="Center"
Text="NEW USER&#xa;&#xa;Choose Settings"/>
VerticalAlignment="Top"
Text="Theme: " />
</Button>
<ComboBox
Grid.Row="1"
VerticalAlignment="Bottom"
SelectedIndex="0"
SelectedItem="{Binding SelectedTheme, Mode=OneWayToSource}">
<ComboBox.Items>
<ComboBoxItem Content="Light" />
<ComboBoxItem Content="Dark" />
</ComboBox.Items>
</ComboBox>
</Grid>
</Grid>
<Button
Grid.Row="2"

View File

@@ -1,22 +1,18 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LibationFileManager;
namespace LibationAvalonia.Dialogs
{
public partial class SetupDialog : Window
{
public bool IsNewUser { get;private set; }
public bool IsReturningUser { get;private set; }
public bool IsNewUser { get; private set; }
public bool IsReturningUser { get; private set; }
public ComboBoxItem SelectedTheme { get; set; }
public Configuration Config { get; init; }
public SetupDialog()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
DataContext = this;
}
public void NewUser_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)

View File

@@ -6,6 +6,7 @@
x:Class="LibationAvalonia.Dialogs.TagsBatchDialog"
MinWidth="630" MinHeight="110"
MaxWidth="630" MaxHeight="110"
Width="630" Height="110"
Title="Replace Tags"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">

View File

@@ -1,6 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace LibationAvalonia.Dialogs
{

View File

@@ -6,6 +6,7 @@
x:Class="LibationAvalonia.Dialogs.TrashBinDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
MinWidth="630" MinHeight="480"
Width="630" Height="480"
Title="Trash Bin"
WindowStartupLocation="CenterOwner"
Icon="/Assets/libation.ico">

View File

@@ -1,7 +1,6 @@
using ApplicationServices;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Threading;
using DataLayer;
using LibationAvalonia.Controls;
using LibationAvalonia.ViewModels;
@@ -25,7 +24,7 @@ namespace LibationAvalonia.Dialogs
this.RestoreSizeAndLocation(Configuration.Instance);
DataContext = _viewModel = new();
this.Closing += (_,_) => this.SaveSizeAndLocation(Configuration.Instance);
this.Closing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
}
public async void EmptyTrash_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -40,7 +39,7 @@ namespace LibationAvalonia.Dialogs
public string CheckedCountText => $"Checked : {_checkedBooksCount} of {_totalBooksCount}";
private bool _controlsEnabled = true;
public bool ControlsEnabled { get => _controlsEnabled; set=> this.RaiseAndSetIfChanged(ref _controlsEnabled, value); }
public bool ControlsEnabled { get => _controlsEnabled; set => this.RaiseAndSetIfChanged(ref _controlsEnabled, value); }
private bool? everythingChecked = false;
public bool? EverythingChecked
@@ -96,7 +95,7 @@ namespace LibationAvalonia.Dialogs
{
foreach (var item in DeletedBooks)
item.IsChecked = true;
}
}
public void UncheckAll()
{

View File

@@ -1,7 +1,6 @@
using AppScaffolding;
using Avalonia.Controls;
using Dinah.Core;
using System;
namespace LibationAvalonia.Dialogs
{

View File

@@ -1,9 +1,9 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using LibationFileManager;
using System;
using System.Linq;
namespace LibationAvalonia
{

View File

@@ -1,16 +1,15 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
using DataLayer;
using Dinah.Core.Logging;
using LibationAvalonia.ViewModels.Dialogs;
using LibationAvalonia.Dialogs;
using LibationAvalonia.ViewModels.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using Avalonia.Threading;
namespace LibationAvalonia
{
@@ -64,23 +63,23 @@ namespace LibationAvalonia
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
=> ShowCoreAsync(null, text, caption, buttons, icon, defaultButton);
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, bool saveAndRestorePosition = true)
=> ShowCoreAsync(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1, saveAndRestorePosition);
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons)
=> ShowCoreAsync(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(string text, string caption)
=> ShowCoreAsync(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(string text)
=> ShowCoreAsync(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
=> ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton);
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, bool saveAndRestorePosition = true)
=> ShowCoreAsync(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1, saveAndRestorePosition);
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons)
=> ShowCoreAsync(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(string text, string caption)
=> ShowCoreAsync(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(string text)
=> ShowCoreAsync(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
=> ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton);
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
=> ShowCoreAsync(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons)
=> ShowCoreAsync(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(Window owner, string text, string caption)
=> ShowCoreAsync(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
=> ShowCoreAsync(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons)
=> ShowCoreAsync(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(Window owner, string text, string caption)
=> ShowCoreAsync(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static Task<DialogResult> Show(Window owner, string text)
=> ShowCoreAsync(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
@@ -102,10 +101,10 @@ Libation.
".Trim(), "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
/// <summary>
/// Note: the format field should use {0} and NOT use the `$` string interpolation. Formatting is done inside this method.
/// </summary>
public static async Task<DialogResult> ShowConfirmationDialog(Window owner, IEnumerable<LibraryBook> libraryBooks, string format, string title, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1)
/// <summary>
/// Note: the format field should use {0} and NOT use the `$` string interpolation. Formatting is done inside this method.
/// </summary>
public static async Task<DialogResult> ShowConfirmationDialog(Window owner, IEnumerable<LibraryBook> libraryBooks, string format, string title, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1)
{
if (libraryBooks is null || !libraryBooks.Any())
return DialogResult.Cancel;
@@ -120,7 +119,7 @@ Libation.
= string.Format(format, $"{thisThese} {count} {bookBooks}")
+ $"\r\n\r\n{titlesAgg}";
return await ShowCoreAsync(owner,
return await ShowCoreAsync(owner,
message,
title,
MessageBoxButtons.YesNo,

View File

@@ -43,8 +43,6 @@ namespace LibationAvalonia
// Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration
var config = LibationScaffolding.RunPreConfigMigrations();
App.SetupRequired = !config.LibationSettingsAreValid;
//Start as much work in parallel as possible.
var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime());
var appBuilderTask = Task.Run(BuildAvaloniaApp);
@@ -55,7 +53,7 @@ namespace LibationAvalonia
return;
if (!App.SetupRequired)
if (config.LibationSettingsAreValid)
{
if (!RunDbMigrations(config))
return;
@@ -63,7 +61,7 @@ namespace LibationAvalonia
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
}
(appBuilderTask.GetAwaiter().GetResult()).SetupWithLifetime(classicLifetimeTask.GetAwaiter().GetResult());
appBuilderTask.GetAwaiter().GetResult().SetupWithLifetime(classicLifetimeTask.GetAwaiter().GetResult());
classicLifetimeTask.Result.Start(null);
}

View File

@@ -6,7 +6,7 @@ using System;
namespace LibationAvalonia.ViewModels
{
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
public class AvaloniaEntryStatus : EntryStatus, IEntryStatus, IComparable
{
public override IBrush BackgroundBrush => IsEpisode ? App.SeriesEntryGridBackgroundBrush : Brushes.Transparent;

View File

@@ -22,8 +22,8 @@ namespace LibationAvalonia.ViewModels.Dialogs
public bool HasButton3 => !string.IsNullOrEmpty(Button3Text);
public bool HasButton2 => !string.IsNullOrEmpty(Button2Text);
public int WindowHeight { get;private set; }
public int WindowWidth { get;private set; }
public int WindowHeight { get; private set; }
public int WindowWidth { get; private set; }
public string Button1Text => _button switch
{
@@ -47,7 +47,7 @@ namespace LibationAvalonia.ViewModels.Dialogs
MessageBoxButtons.CancelTryContinue => "Try",
_ => string.Empty,
};
public string Button3Text => _button switch
{
MessageBoxButtons.AbortRetryIgnore => "Ignore",

View File

@@ -1,4 +1,6 @@
using ApplicationServices;
using Avalonia.Collections;
using Avalonia.Controls;
using LibationFileManager;
using ReactiveUI;
@@ -41,24 +43,25 @@ namespace LibationAvalonia.ViewModels
/// <summary> Auto scanning accounts is enables </summary>
public bool AutoScanChecked
{
get => _autoScanChecked;
set
{
public bool AutoScanChecked
{
get => _autoScanChecked;
set
{
if (value != _autoScanChecked)
Configuration.Instance.AutoScan = value;
this.RaiseAndSetIfChanged(ref _autoScanChecked, value);
}
}
}
public AvaloniaList<Control> QuickFilterMenuItems { get; } = new();
/// <summary> Indicates if the first quick filter is the default filter </summary>
public bool FirstFilterIsDefault
{
get => _firstFilterIsDefault;
set
{
{
get => _firstFilterIsDefault;
set
{
if (value != _firstFilterIsDefault)
QuickFilters.UseDefault = value;
this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value);

View File

@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ApplicationServices;
using ApplicationServices;
using AudibleApi;
using AudibleApi.Common;
using Avalonia.Media;
@@ -15,6 +11,10 @@ using FileLiberator;
using LibationFileManager;
using LibationUiBase;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.ViewModels
{
@@ -65,7 +65,7 @@ namespace LibationAvalonia.ViewModels
public string Author { get => _author; set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _author, value)); }
public string Title { get => _title; set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _title, value)); }
public int Progress { get => _progress; private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
public string ETA { get => _eta; private set => Dispatcher.UIThread.Post(() =>this.RaiseAndSetIfChanged(ref _eta, value)); }
public string ETA { get => _eta; private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _eta, value)); }
public Bitmap Cover { get => _cover; private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
public bool IsDownloading => Status is ProcessBookStatus.Working;
@@ -315,36 +315,36 @@ namespace LibationAvalonia.ViewModels
Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}");
UnlinkProcessable((Processable)sender);
if (Processes.Count == 0)
{
Completed?.Invoke(this, EventArgs.Empty);
if (Processes.Count == 0)
{
Completed?.Invoke(this, EventArgs.Empty);
return;
}
}
NextProcessable();
LinkProcessable(CurrentProcessable);
NextProcessable();
LinkProcessable(CurrentProcessable);
StatusHandler result;
try
try
{
result = await CurrentProcessable.ProcessSingleAsync(libraryBook, validate: true);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"{nameof(Processable_Completed)} error");
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"{nameof(Processable_Completed)} error");
result = new StatusHandler();
result.AddError($"{nameof(Processable_Completed)} error. See log for details. Error summary: {ex.Message}");
}
result = new StatusHandler();
result.AddError($"{nameof(Processable_Completed)} error. See log for details. Error summary: {ex.Message}");
}
if (result.HasErrors)
{
foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
Logger.Error(errorMessage);
if (result.HasErrors)
{
foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
Logger.Error(errorMessage);
Completed?.Invoke(this, EventArgs.Empty);
}
}
Completed?.Invoke(this, EventArgs.Empty);
}
}
#endregion

View File

@@ -33,7 +33,7 @@ namespace LibationAvalonia.ViewModels
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
if (Design.IsDesignMode)
_ = Configuration.Instance.LibationFiles;
_ = Configuration.Instance.LibationFiles;
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
}

View File

@@ -133,12 +133,12 @@ namespace LibationAvalonia.ViewModels
}
private void GridEntries_CollectionChanged(object sender = null, EventArgs e = null)
{
{
var count
= FilteredInGridEntries?.OfType<ILibraryBookEntry>().Count()
?? SOURCE.OfType<ILibraryBookEntry>().Count();
VisibleCountChanged?.Invoke(this, count);
VisibleCountChanged?.Invoke(this, count);
}
/// <summary>

View File

@@ -1,8 +1,4 @@
using Avalonia.Controls;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Text;
namespace LibationAvalonia.ViewModels
{

View File

@@ -35,7 +35,7 @@
Name="button"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsEnabled="{Binding IsButtonEnabled}" Padding="0" Click="Button_Click" >
IsEnabled="{CompiledBinding IsButtonEnabled}" Padding="0" Click="Button_Click" >
<Panel>
<Panel

View File

@@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Interactivity;
using DataLayer;
using LibationAvalonia.ViewModels;
using LibationUiBase.GridView;
using System;
namespace LibationAvalonia.Views
@@ -45,6 +46,16 @@ namespace LibationAvalonia.Views
PdfStatus = null;
IsSeries = true;
}
DataContextChanged += LiberateStatusButton_DataContextChanged;
}
private void LiberateStatusButton_DataContextChanged(object sender, EventArgs e)
{
//Force book status recheck when an entry is scrolled into view.
//This will force a recheck for a paprtially downloaded file.
var status = DataContext as ILibraryBookEntry;
status?.Liberate.Invalidate(nameof(status.Liberate.BookStatus));
}
private void Button_Click(object sender, RoutedEventArgs e) => Click?.Invoke(this, EventArgs.Empty);

View File

@@ -1,6 +1,4 @@
using ApplicationServices;
using System;
using System.Linq;
using Avalonia.Threading;
using System.Threading.Tasks;
@@ -19,7 +17,7 @@ namespace LibationAvalonia.Views
private void setBackupCounts(object _, object __)
{
if (updateCountsTask?.IsCompleted is not false)
updateCountsTask = Dispatcher.UIThread.InvokeAsync(() => _viewModel.LibraryStats = LibraryCommands.GetCounts());
updateCountsTask = Dispatcher.UIThread.InvokeAsync(() => _viewModel.LibraryStats = LibraryCommands.GetCounts());
}
}
}

View File

@@ -1,14 +1,11 @@
using ApplicationServices;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using FileManager;
using LibationFileManager;
using System;
using System.Linq;
namespace LibationAvalonia.Views
{
//DONE
public partial class MainWindow
{
private void Configure_Export() { }

View File

@@ -1,18 +1,16 @@
using Avalonia.Input;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Views
{
//DONE
public partial class MainWindow
{
protected void Configure_Filter() { }
public async void filterHelpBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await (new LibationAvalonia.Dialogs.SearchSyntaxDialog()).ShowDialog(this);
public async void filterSearchTb_KeyPress(object sender, KeyEventArgs e)
{
if (e.Key == Key.Return)

View File

@@ -5,7 +5,6 @@ using System.Threading.Tasks;
namespace LibationAvalonia.Views
{
//DONE
public partial class MainWindow
{
private void Configure_Liberate() { }

View File

@@ -1,8 +1,6 @@
using LibationFileManager;
using LibationUiBase;
using System;
using System.IO;
using System.Linq;
namespace LibationAvalonia.Views
{
@@ -13,11 +11,11 @@ namespace LibationAvalonia.Views
using var ms1 = new MemoryStream();
App.OpenAsset("img-coverart-prod-unavailable_80x80.jpg").CopyTo(ms1);
PictureStorage.SetDefaultImage(PictureSize._80x80, ms1.ToArray());
using var ms2 = new MemoryStream();
App.OpenAsset("img-coverart-prod-unavailable_300x300.jpg").CopyTo(ms2);
PictureStorage.SetDefaultImage(PictureSize._300x300, ms2.ToArray());
using var ms3 = new MemoryStream();
App.OpenAsset("img-coverart-prod-unavailable_500x500.jpg").CopyTo(ms3);
PictureStorage.SetDefaultImage(PictureSize._500x500, ms3.ToArray());

View File

@@ -3,7 +3,6 @@ using Dinah.Core;
using LibationFileManager;
using LibationUiBase.GridView;
using System;
using System.Collections.Generic;
using System.Linq;
namespace LibationAvalonia.Views

View File

@@ -1,11 +1,11 @@
using Avalonia.Controls;
using Avalonia;
using Avalonia.Controls;
using LibationFileManager;
using System;
using System.Linq;
using Avalonia.Data;
namespace LibationAvalonia.Views
{
//DONE
public partial class MainWindow
{
private void Configure_QuickFilters()
@@ -13,24 +13,54 @@ namespace LibationAvalonia.Views
_viewModel.FirstFilterIsDefault = QuickFilters.UseDefault;
Load += updateFiltersMenu;
QuickFilters.Updated += updateFiltersMenu;
//We need to be able to dynamically add and remove menu items from the Quick Filters menu.
//To do that, we need quick filter's menu items source to be writable, which we can only
//achieve by creating the list ourselves (instead of allowing Avalonia to create it from the xaml)
var startWithFilterMenuItem = new MenuItem
{
Header = "Start Libation with 1st filter _Default",
Icon = new CheckBox
{
BorderThickness = new Thickness(0),
IsHitTestVisible = false,
[!CheckBox.IsCheckedProperty] = new Binding(nameof(_viewModel.FirstFilterIsDefault))
}
};
var editFiltersMenuItem = new MenuItem { Header = "_Edit quick filters..." };
startWithFilterMenuItem.Click += firstFilterIsDefaultToolStripMenuItem_Click;
editFiltersMenuItem.Click += editQuickFiltersToolStripMenuItem_Click;
_viewModel.QuickFilterMenuItems.Add(startWithFilterMenuItem);
_viewModel.QuickFilterMenuItems.Add(editFiltersMenuItem);
_viewModel.QuickFilterMenuItems.Add(new Separator());
}
private async void QuickFiltersMenuItem_KeyDown(object sender, Avalonia.Input.KeyEventArgs e)
{
int keyNum = (int)e.Key - 34;
if (keyNum <=9 && keyNum >= 1)
{
var menuItem = _viewModel.QuickFilterMenuItems
.OfType<MenuItem>()
.FirstOrDefault(i => i.Header is string h && h.StartsWith($"_{keyNum}"));
if (menuItem is not null)
{
await performFilter(menuItem.Tag as string);
e.Handled = true;
}
}
}
private object quickFilterTag { get; } = new();
private void updateFiltersMenu(object _ = null, object __ = null)
{
var allItems = quickFiltersToolStripMenuItem
.Items
.Cast<Control>()
.ToList();
var toRemove = allItems
.OfType<MenuItem>()
.Where(mi => mi.Tag == quickFilterTag)
.ToList();
allItems = allItems
.Except(toRemove)
.ToList();
//Clear all filters
_viewModel.QuickFilterMenuItems.RemoveAll(_viewModel.QuickFilterMenuItems.Where(i => i.Tag is string).ToList());
// re-populate
var index = 0;
@@ -38,27 +68,23 @@ namespace LibationAvalonia.Views
{
var quickFilterMenuItem = new MenuItem
{
Tag = quickFilterTag,
Tag = filter,
Header = $"_{++index}: {filter}"
};
quickFilterMenuItem.Click += async (_, __) => await performFilter(filter);
allItems.Add(quickFilterMenuItem);
_viewModel.QuickFilterMenuItems.Add(quickFilterMenuItem);
}
quickFiltersToolStripMenuItem.Items = allItems;
}
public void firstFilterIsDefaultToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private void firstFilterIsDefaultToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (sender is MenuItem mi && mi.Icon is CheckBox checkBox)
{
checkBox.IsChecked = !(checkBox.IsChecked ?? false);
}
_viewModel.FirstFilterIsDefault = !_viewModel.FirstFilterIsDefault;
}
public void addQuickFilterBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private void addQuickFilterBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> QuickFilters.Add(_viewModel.FilterString);
public async void editQuickFiltersToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private async void editQuickFiltersToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await new Dialogs.EditQuickFilters().ShowDialog(this);
}
}

View File

@@ -1,14 +1,12 @@
using AudibleUtilities;
using LibationAvalonia.Dialogs;
using System;
using System.Linq;
namespace LibationAvalonia.Views
{
//DONE
public partial class MainWindow
{
private void Configure_RemoveBooks()
private void Configure_RemoveBooks()
{
if (Avalonia.Controls.Design.IsDesignMode)
return;

View File

@@ -43,7 +43,7 @@ namespace LibationAvalonia.Views
};
_viewModel.AutoScanChecked = Configuration.Instance.AutoScan;
// if enabled: begin on load
Opened += startAutoScan;

View File

@@ -1,6 +1,5 @@
using ApplicationServices;
using AudibleUtilities;
using Avalonia.Controls;
using LibationAvalonia.Dialogs;
using LibationFileManager;
using System;
@@ -10,7 +9,6 @@ using System.Threading.Tasks;
namespace LibationAvalonia.Views
{
//DONE
public partial class MainWindow
{
private void Configure_ScanManual()
@@ -69,7 +67,7 @@ namespace LibationAvalonia.Views
if (Configuration.Instance.ShowImportedStats && newAdded > 0)
await MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
}
catch(OperationCanceledException)
catch (OperationCanceledException)
{
Serilog.Log.Information("Audible login attempt cancelled by user");
}

View File

@@ -1,10 +1,8 @@
using ApplicationServices;
using System;
using System.Linq;
namespace LibationAvalonia.Views
{
//DONE
public partial class MainWindow
{
private void Configure_ScanNotification()
@@ -18,7 +16,7 @@ namespace LibationAvalonia.Views
_viewModel.NumAccountsScanning = accountsLength;
}
private void LibraryCommands_ScanEnd(object sender, EventArgs e)
private void LibraryCommands_ScanEnd(object sender, int newCount)
{
_viewModel.NumAccountsScanning = 0;
}

View File

@@ -3,7 +3,6 @@ using System;
namespace LibationAvalonia.Views
{
//DONE
public partial class MainWindow
{
private void Configure_Settings() { }
@@ -17,13 +16,16 @@ namespace LibationAvalonia.Views
public async void aboutToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await MessageBox.Show($"Libation {AppScaffolding.LibationScaffolding.Variety}{Environment.NewLine}Version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
public async void tourToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await new Walkthrough(this).RunAsync();
public void launchHangoverToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
try
{
System.Diagnostics.Process.Start("Hangover" + (Configuration.IsWindows ? ".exe" : ""));
}
catch(Exception ex)
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Failed to launch Hangover");
}

View File

@@ -5,8 +5,8 @@ using System.Threading.Tasks;
namespace LibationAvalonia.Views
{
public partial class MainWindow
{
public partial class MainWindow
{
private void Configure_Upgrade()
{
setProgressVisible(false);

View File

@@ -1,15 +1,11 @@
using ApplicationServices;
using Avalonia.Threading;
using DataLayer;
using Dinah.Core;
using LibationFileManager;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace LibationAvalonia.Views
{
//DONE
public partial class MainWindow
{
private void Configure_VisibleBooks()
@@ -52,15 +48,15 @@ namespace LibationAvalonia.Views
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
// do not use `$` string interpolation. See impl.
"Are you sure you want to replace tags in {0}?",
// do not use `$` string interpolation. See impl.
"Are you sure you want to replace tags in {0}?",
"Replace tags?");
if (confirmationResult != DialogResult.Yes)
return;
visibleLibraryBooks.UpdateTags(dialog.NewTags);
}
}
public async void setBookDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
@@ -74,73 +70,73 @@ namespace LibationAvalonia.Views
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
// do not use `$` string interpolation. See impl.
"Are you sure you want to replace book downloaded status in {0}?",
// do not use `$` string interpolation. See impl.
"Are you sure you want to replace book downloaded status in {0}?",
"Replace downloaded status?");
if (confirmationResult != DialogResult.Yes)
return;
visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus);
}
visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus);
}
public async void setPdfDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var dialog = new Dialogs.LiberatedStatusBatchManualDialog(isPdf: true);
var result = await dialog.ShowDialog<DialogResult>(this);
if (result != DialogResult.OK)
return;
public async void setPdfDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var dialog = new Dialogs.LiberatedStatusBatchManualDialog(isPdf: true);
var result = await dialog.ShowDialog<DialogResult>(this);
if (result != DialogResult.OK)
return;
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
// do not use `$` string interpolation. See impl.
"Are you sure you want to replace PDF downloaded status in {0}?",
"Replace downloaded status?");
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
// do not use `$` string interpolation. See impl.
"Are you sure you want to replace PDF downloaded status in {0}?",
"Replace downloaded status?");
if (confirmationResult != DialogResult.Yes)
return;
if (confirmationResult != DialogResult.Yes)
return;
visibleLibraryBooks.UpdatePdfStatus(dialog.BookLiberatedStatus);
}
visibleLibraryBooks.UpdatePdfStatus(dialog.BookLiberatedStatus);
}
public async void setDownloadedAutoToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var dialog = new Dialogs.LiberatedStatusBatchAutoDialog();
var result = await dialog.ShowDialog<DialogResult>(this);
if (result != DialogResult.OK)
return;
public async void setDownloadedAutoToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var dialog = new Dialogs.LiberatedStatusBatchAutoDialog();
var result = await dialog.ShowDialog<DialogResult>(this);
if (result != DialogResult.OK)
return;
var bulkSetStatus = new BulkSetDownloadStatus(_viewModel.ProductsDisplay.GetVisibleBookEntries(), dialog.SetDownloaded, dialog.SetNotDownloaded);
var count = await Task.Run(bulkSetStatus.Discover);
var bulkSetStatus = new BulkSetDownloadStatus(_viewModel.ProductsDisplay.GetVisibleBookEntries(), dialog.SetDownloaded, dialog.SetNotDownloaded);
var count = await Task.Run(bulkSetStatus.Discover);
if (count == 0)
return;
if (count == 0)
return;
var confirmationResult = await MessageBox.Show(
bulkSetStatus.AggregateMessage,
"Replace downloaded status?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button1);
var confirmationResult = await MessageBox.Show(
bulkSetStatus.AggregateMessage,
"Replace downloaded status?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button1);
if (confirmationResult != DialogResult.Yes)
return;
if (confirmationResult != DialogResult.Yes)
return;
bulkSetStatus.Execute();
}
bulkSetStatus.Execute();
}
public async void removeToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
public async void removeToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
// do not use `$` string interpolation. See impl.
"Are you sure you want to remove {0} from Libation's library?",
// do not use `$` string interpolation. See impl.
"Are you sure you want to remove {0} from Libation's library?",
"Remove books from Libation?",
MessageBoxDefaultButton.Button2);

View File

@@ -8,7 +8,7 @@
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
mc:Ignorable="d" d:DesignWidth="1850" d:DesignHeight="700"
x:Class="LibationAvalonia.Views.MainWindow"
Title="Libation"
Title="Libation: Liberate your Library"
Name="Form1"
Icon="/Assets/libation.ico">
@@ -19,6 +19,7 @@
<!-- Menu Strip -->
<Menu Grid.Column="0" VerticalAlignment="Top">
<!-- Decrease height of menu strop -->
<Menu.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="25"/>
@@ -27,7 +28,7 @@
<!-- Import Menu -->
<MenuItem Header="_Import">
<MenuItem Name="importToolStripMenuItem" Header="_Import">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
@@ -42,8 +43,8 @@
<MenuItem IsVisible="{Binding !AnyAccounts}" Click="noAccountsYetAddAccountToolStripMenuItem_Click" Header="No accounts yet. A_dd Account..." />
<!-- Scan Library -->
<MenuItem IsVisible="{Binding OneAccount}" IsEnabled="{Binding !ActivelyScanning}" Click="scanLibraryToolStripMenuItem_Click" Header="Scan _Library" />
<MenuItem IsVisible="{Binding MultipleAccounts}" IsEnabled="{Binding !ActivelyScanning}" Click="scanLibraryOfAllAccountsToolStripMenuItem_Click" Header="Scan Library of _All Accounts" />
<MenuItem IsVisible="{Binding OneAccount}" IsEnabled="{Binding !ActivelyScanning}" Name="scanLibraryToolStripMenuItem" Click="scanLibraryToolStripMenuItem_Click" Header="Scan _Library" />
<MenuItem IsVisible="{Binding MultipleAccounts}" IsEnabled="{Binding !ActivelyScanning}" Name="scanLibraryOfAllAccountsToolStripMenuItem" Click="scanLibraryOfAllAccountsToolStripMenuItem_Click" Header="Scan Library of _All Accounts" />
<MenuItem IsVisible="{Binding MultipleAccounts}" IsEnabled="{Binding !ActivelyScanning}" Click="scanLibraryOfSomeAccountsToolStripMenuItem_Click" Header="Scan Library of _Some Accounts" />
<Separator />
@@ -86,20 +87,13 @@
<!-- Quick Filters Menu -->
<MenuItem Name="quickFiltersToolStripMenuItem" Header="Quick _Filters">
<MenuItem Name="quickFiltersToolStripMenuItem" Header="Quick _Filters" ItemsSource="{Binding QuickFilterMenuItems}" KeyDown="QuickFiltersMenuItem_KeyDown">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem Click="firstFilterIsDefaultToolStripMenuItem_Click" Header="Start Libation with 1st filter _Default">
<MenuItem.Icon>
<CheckBox BorderThickness="0" IsChecked="{Binding FirstFilterIsDefault, Mode=TwoWay}" IsHitTestVisible="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Click="editQuickFiltersToolStripMenuItem_Click" Header="_Edit quick filters..." />
<Separator />
</MenuItem>
<!-- Visible Books Menu -->
@@ -121,19 +115,20 @@
<!-- Settings Menu -->
<MenuItem Header="_Settings">
<MenuItem Header="_Settings" Name="settingsToolStripMenuItem">
<!-- Remove height style property for menu item -->
<MenuItem.Styles>
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Height" Value="NaN"/>
</Style>
</MenuItem.Styles>
<MenuItem Click="accountsToolStripMenuItem_Click" Header="_Accounts..." />
<MenuItem Click="basicSettingsToolStripMenuItem_Click" Header="_Settings..." />
<MenuItem Name="accountsToolStripMenuItem" Click="accountsToolStripMenuItem_Click" Header="_Accounts..." />
<MenuItem Name="basicSettingsToolStripMenuItem" Click="basicSettingsToolStripMenuItem_Click" Header="_Settings..." />
<Separator />
<MenuItem Click="openTrashBinToolStripMenuItem_Click" Header="Trash Bin" />
<MenuItem Click="launchHangoverToolStripMenuItem_Click" Header="Launch _Hangover" />
<Separator />
<MenuItem Click="tourToolStripMenuItem_Click" Header="Take a Guided _Tour of Libation" />
<MenuItem Click="aboutToolStripMenuItem_Click" Header="A_bout..." />
</MenuItem>
</Menu>
@@ -160,8 +155,8 @@
</Grid.Styles>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Button Margin="0" Click="filterHelpBtn_Click" Content="?"/>
<Button Click="addQuickFilterBtn_Click" Content="Add To Quick Filters"/>
<Button Name="filterHelpBtn" Margin="0" Click="filterHelpBtn_Click" Content="?"/>
<Button Name="addQuickFilterBtn" Click="addQuickFilterBtn_Click" Content="Add To Quick Filters"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal">
@@ -169,10 +164,10 @@
<Button IsVisible="{Binding RemoveButtonsVisible}" Click="doneRemovingBtn_Click" Content="Done Removing Books"/>
</StackPanel>
<TextBox Grid.Column="1" Margin="10,0,0,0" IsVisible="{Binding !RemoveButtonsVisible}" Text="{Binding FilterString, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
<TextBox Grid.Column="1" Margin="10,0,0,0" Name="filterSearchTb" IsVisible="{Binding !RemoveButtonsVisible}" Text="{Binding FilterString, Mode=TwoWay}" KeyDown="filterSearchTb_KeyPress" />
<StackPanel Grid.Column="2" Height="30" Orientation="Horizontal">
<Button Click="filterBtn_Click" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
<Button Name="filterBtn" Click="filterBtn_Click" VerticalAlignment="Stretch" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Content="Filter"/>
<Button Padding="2,6,2,6" VerticalAlignment="Stretch" Click="ToggleQueueHideBtn_Click">
<Path Stretch="Uniform" Fill="{DynamicResource IconFill}" Data="{StaticResource LeftArrows}">
<Path.RenderTransform>

View File

@@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ApplicationServices;
using Avalonia.ReactiveUI;
using DataLayer;
using LibationAvalonia.ViewModels;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.Linq;
namespace LibationAvalonia.Views
{
@@ -48,6 +48,23 @@ namespace LibationAvalonia.Views
Closing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
}
Closing += MainWindow_Closing;
Opened += MainWindow_Opened;
}
private async void MainWindow_Opened(object sender, EventArgs e)
{
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);
if (result is DialogResult.Yes)
{
await new Walkthrough(this).RunAsync();
}
Configuration.Instance.FirstLaunch = false;
}
}
private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)

View File

@@ -2,20 +2,22 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels"
x:DataType="vm:ProcessBookViewModel"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="87" MaxHeight="87" MinHeight="87" MinWidth="300"
x:Class="LibationAvalonia.Views.ProcessBookControl" Background="{Binding BackgroundColor}">
x:Class="LibationAvalonia.Views.ProcessBookControl" Background="{CompiledBinding BackgroundColor}">
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseMediumBrush}" BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="Auto,*,Auto">
<Panel Grid.Column="0" Margin="3" Width="80" Height="80" HorizontalAlignment="Left">
<Image Width="80" Height="80" Source="{Binding Cover}" Stretch="Uniform" />
<Image Width="80" Height="80" Source="{CompiledBinding Cover}" Stretch="Uniform" />
</Panel>
<Grid Margin="0,3,0,3" Grid.Column="1" ColumnDefinitions="*" RowDefinitions="*,16">
<StackPanel Grid.Column="0" Grid.Row="0" Orientation="Vertical">
<TextBlock ClipToBounds="True" TextWrapping="Wrap" FontSize="11" Text="{Binding Title}" />
<TextBlock FontSize="10" TextWrapping="NoWrap" Text="{Binding Author}" />
<TextBlock FontSize="10" TextWrapping="NoWrap" Text="{Binding Narrator}" />
<TextBlock ClipToBounds="True" TextWrapping="Wrap" FontSize="11" Text="{CompiledBinding Title}" />
<TextBlock FontSize="10" TextWrapping="NoWrap" Text="{CompiledBinding Author}" />
<TextBlock FontSize="10" TextWrapping="NoWrap" Text="{CompiledBinding Narrator}" />
</StackPanel>
<Panel Grid.Column="0" Grid.Row="1">
<Panel.Styles>
@@ -23,8 +25,8 @@
<Setter Property="MinWidth" Value="20" />
</Style>
</Panel.Styles>
<ProgressBar IsVisible="{Binding IsDownloading}" Value="{Binding Progress}" ShowProgressText="True" FontSize="12" />
<TextBlock IsVisible="{Binding !IsDownloading}" Text="{Binding StatusText}"/>
<ProgressBar IsVisible="{CompiledBinding IsDownloading}" Value="{CompiledBinding Progress}" ShowProgressText="True" FontSize="12" />
<TextBlock IsVisible="{CompiledBinding !IsDownloading}" Text="{CompiledBinding StatusText}"/>
</Panel>
</Grid>
<Grid Margin="3" Grid.Column="2" HorizontalAlignment="Right" ColumnDefinitions="Auto,Auto">
@@ -39,7 +41,7 @@
</Style>
</Style>
</Grid.Styles>
<StackPanel IsVisible="{Binding Queued}" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right" Orientation="Vertical">
<StackPanel IsVisible="{CompiledBinding Queued}" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right" Orientation="Vertical">
<Button Click="MoveFirst_Click">
<Path VerticalAlignment="Top" Data="{StaticResource FirstButtonIcon}" />
@@ -55,13 +57,13 @@
</Button>
</StackPanel>
<Panel Margin="3,0,0,0" Grid.Column="1" VerticalAlignment="Top">
<Button Height="32" Background="{DynamicResource CancelRed}" Width="22" IsVisible="{Binding !IsFinished}" CornerRadius="11" Click="Cancel_Click">
<Button Height="32" Background="{DynamicResource CancelRed}" Width="22" IsVisible="{CompiledBinding !IsFinished}" CornerRadius="11" Click="Cancel_Click">
<Path Fill="{DynamicResource ProcessQueueBookDefaultBrush}" VerticalAlignment="Center" Data="{StaticResource CancelButtonIcon}" RenderTransform="{StaticResource Rotate45Transform}" />
</Button>
</Panel>
</Grid>
<Panel Margin="3" Width="50" Grid.Column="2">
<TextPresenter FontSize="9" VerticalAlignment="Bottom" HorizontalAlignment="Right" IsVisible="{Binding IsDownloading}" Text="{Binding ETA}" />
<TextPresenter FontSize="9" VerticalAlignment="Bottom" HorizontalAlignment="Right" IsVisible="{CompiledBinding IsDownloading}" Text="{CompiledBinding ETA}" />
</Panel>
</Grid>

View File

@@ -1,9 +1,7 @@
using System;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using LibationAvalonia.ViewModels;
using ApplicationServices;
using Avalonia.Controls;
using DataLayer;
using LibationAvalonia.ViewModels;
using LibationUiBase;
namespace LibationAvalonia.Views

View File

@@ -2,7 +2,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml;
using DataLayer;
using LibationAvalonia.ViewModels;
using LibationUiBase;
@@ -14,7 +13,7 @@ using System.Linq;
namespace LibationAvalonia.Views
{
public partial class ProcessQueueControl : UserControl
{
{
private TrackedQueue<ProcessBookViewModel> Queue => _viewModel.Queue;
private ProcessQueueViewModel _viewModel => DataContext as ProcessQueueViewModel;

View File

@@ -3,6 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:LibationAvalonia.Views"
xmlns:uibase="clr-namespace:LibationUiBase.GridView;assembly=LibationUiBase"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
mc:Ignorable="d" d:DesignWidth="1560" d:DesignHeight="400"
x:Class="LibationAvalonia.Views.ProductsDisplay">
@@ -37,6 +38,14 @@
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Fill" Value="{DynamicResource IconFill}" />
</Style>
<Style Selector="DataGridColumnHeader ContextMenu MenuItem">
<Setter Property="Padding" Value="0" />
<Style Selector="^:icon /template/ Viewbox#PART_IconPresenter">
<Setter Property="Height" Value="32" />
<Setter Property="Width" Value="32" />
<Setter Property="Margin" Value="6,0" />
</Style>
</Style>
</DataGrid.Styles>
<DataGrid.Columns>
@@ -51,25 +60,25 @@
Width="75">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<CheckBox
HorizontalAlignment="Center"
IsThreeState="True"
IsChecked="{Binding Remove, Mode=TwoWay}" />
IsChecked="{CompiledBinding Remove, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<controls:DataGridTemplateColumnExt CanUserSort="True" Width="75" Header="Liberate" SortMemberPath="Liberate" ClipboardContentBinding="{Binding Liberate.ToolTip}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<views:LiberateStatusButton
ToolTip.Tip="{Binding Liberate.ToolTip}"
BookStatus="{Binding Liberate.BookStatus}"
PdfStatus="{Binding Liberate.PdfStatus}"
IsUnavailable="{Binding Liberate.IsUnavailable}"
IsSeries="{Binding Liberate.IsSeries}"
Expanded="{Binding Liberate.Expanded}"
ToolTip.Tip="{CompiledBinding Liberate.ToolTip}"
BookStatus="{CompiledBinding Liberate.BookStatus}"
PdfStatus="{CompiledBinding Liberate.PdfStatus}"
IsUnavailable="{CompiledBinding Liberate.IsUnavailable}"
IsSeries="{CompiledBinding Liberate.IsSeries}"
Expanded="{CompiledBinding Liberate.Expanded}"
Click="LiberateButton_Click" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -77,17 +86,17 @@
<DataGridTemplateColumn CanUserSort="False" Width="80" Header="Cover" SortMemberPath="Cover" ClipboardContentBinding="{Binding LibraryBook.Book.PictureLarge}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Image Opacity="{Binding Liberate.Opacity}" Tapped="Cover_Click" Height="80" Source="{Binding Cover}" ToolTip.Tip="Click to see full size" />
<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 MinWidth="150" Width="2*" Header="Title" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Opacity="{Binding Liberate.Opacity}" Background="{Binding Liberate.BackgroundBrush}">
<TextBlock FontSize="14" Text="{Binding Title}" />
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<TextBlock FontSize="14" Text="{CompiledBinding Title}" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -95,9 +104,9 @@
<controls:DataGridTemplateColumnExt MinWidth="80" Width="1*" Header="Authors" CanUserSort="True" SortMemberPath="Authors" ClipboardContentBinding="{Binding Authors}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Opacity="{Binding Liberate.Opacity}" Background="{Binding Liberate.BackgroundBrush}">
<TextBlock Text="{Binding Authors}" />
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<TextBlock Text="{CompiledBinding Authors}" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -105,9 +114,9 @@
<controls:DataGridTemplateColumnExt MinWidth="80" Width="1*" Header="Narrators" CanUserSort="True" SortMemberPath="Narrators" ClipboardContentBinding="{Binding Narrators}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Opacity="{Binding Liberate.Opacity}" Background="{Binding Liberate.BackgroundBrush}">
<TextBlock Text="{Binding Narrators}" />
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<TextBlock Text="{CompiledBinding Narrators}" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -115,9 +124,9 @@
<controls:DataGridTemplateColumnExt Width="90" Header="Length" CanUserSort="True" SortMemberPath="Length" ClipboardContentBinding="{Binding Length}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Opacity="{Binding Liberate.Opacity}" Background="{Binding Liberate.BackgroundBrush}">
<TextBlock Text="{Binding Length}" />
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<TextBlock Text="{CompiledBinding Length}" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -125,9 +134,19 @@
<controls:DataGridTemplateColumnExt MinWidth="80" Width="1*" Header="Series" CanUserSort="True" SortMemberPath="Series" ClipboardContentBinding="{Binding Series}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Opacity="{Binding Liberate.Opacity}" Background="{Binding Liberate.BackgroundBrush}">
<TextBlock Text="{Binding Series}" />
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<TextBlock Text="{CompiledBinding Series}" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumnExt>
<controls:DataGridTemplateColumnExt Width="Auto" Header="Series&#xA;Order" CanUserSort="True" SortMemberPath="SeriesOrder" ClipboardContentBinding="{Binding Series}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<TextBlock Text="{CompiledBinding SeriesOrder}" HorizontalAlignment="Center" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -135,9 +154,9 @@
<controls:DataGridTemplateColumnExt MinWidth="100" Width="1*" Header="Description" CanUserSort="True" SortMemberPath="Description" ClipboardContentBinding="{Binding LongDescription}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Opacity="{Binding Liberate.Opacity}" Background="{Binding Liberate.BackgroundBrush}" Tapped="Description_Click" ToolTip.Tip="Click to see full description" >
<TextBlock Text="{Binding Description}" FontSize="11" VerticalAlignment="Top" />
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}" Tapped="Description_Click" ToolTip.Tip="Click to see full description" >
<TextBlock Text="{CompiledBinding Description}" FontSize="11" VerticalAlignment="Top" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -145,49 +164,51 @@
<controls:DataGridTemplateColumnExt Width="100" Header="Category" CanUserSort="True" SortMemberPath="Category" ClipboardContentBinding="{Binding Category}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Opacity="{Binding Liberate.Opacity}" Background="{Binding Liberate.BackgroundBrush}">
<TextBlock Text="{Binding Category}" />
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<TextBlock Text="{CompiledBinding Category}" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumnExt>
<controls:DataGridMyRatingColumn
x:DataType="uibase:IGridEntry"
Header="Product&#xA;Rating"
IsReadOnly="true"
Width="115"
SortMemberPath="ProductRating" CanUserSort="True"
OpacityBinding="{Binding Liberate.Opacity}"
BackgroundBinding="{Binding Liberate.BackgroundBrush}"
ClipboardContentBinding="{Binding ProductRating}"
Binding="{Binding ProductRating}" />
OpacityBinding="{CompiledBinding Liberate.Opacity}"
BackgroundBinding="{CompiledBinding Liberate.BackgroundBrush}"
ClipboardContentBinding="{CompiledBinding ProductRating}"
Binding="{CompiledBinding ProductRating}" />
<controls:DataGridTemplateColumnExt Width="90" Header="Purchase&#xA;Date" CanUserSort="True" SortMemberPath="PurchaseDate" ClipboardContentBinding="{Binding PurchaseDate}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Opacity="{Binding Liberate.Opacity}" Background="{Binding Liberate.BackgroundBrush}">
<TextBlock Text="{Binding PurchaseDate}" />
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<TextBlock Text="{CompiledBinding PurchaseDate}" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumnExt>
<controls:DataGridMyRatingColumn
x:DataType="uibase:IGridEntry"
Header="My Rating"
IsReadOnly="false"
Width="115"
SortMemberPath="MyRating" CanUserSort="True"
OpacityBinding="{Binding Liberate.Opacity}"
BackgroundBinding="{Binding Liberate.BackgroundBrush}"
ClipboardContentBinding="{Binding MyRating}"
Binding="{Binding MyRating, Mode=TwoWay}" />
OpacityBinding="{CompiledBinding Liberate.Opacity}"
BackgroundBinding="{CompiledBinding Liberate.BackgroundBrush}"
ClipboardContentBinding="{CompiledBinding MyRating}"
Binding="{CompiledBinding MyRating, Mode=TwoWay}" />
<controls:DataGridTemplateColumnExt Width="135" Header="Misc" CanUserSort="True" SortMemberPath="Misc" ClipboardContentBinding="{Binding Misc}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Opacity="{Binding Liberate.Opacity}" Background="{Binding Liberate.BackgroundBrush}">
<TextBlock Text="{Binding Misc}" TextWrapping="WrapWithOverflow" FontSize="10" />
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}">
<TextBlock Text="{CompiledBinding Misc}" TextWrapping="WrapWithOverflow" FontSize="10" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -195,9 +216,9 @@
<controls:DataGridTemplateColumnExt Width="102" Header="Last&#xA;Download" CanUserSort="True" SortMemberPath="LastDownload" ClipboardContentBinding="{Binding LastDownload}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Opacity="{Binding Liberate.Opacity}" Background="{Binding Liberate.BackgroundBrush}" ToolTip.Tip="{Binding LastDownload.ToolTipText}" DoubleTapped="Version_DoubleClick">
<TextBlock Text="{Binding LastDownload}" TextWrapping="WrapWithOverflow" FontSize="10" />
<DataTemplate x:DataType="uibase:IGridEntry">
<Panel Opacity="{CompiledBinding Liberate.Opacity}" Background="{CompiledBinding Liberate.BackgroundBrush}" ToolTip.Tip="{CompiledBinding LastDownload.ToolTipText}" DoubleTapped="Version_DoubleClick">
<TextBlock Text="{CompiledBinding LastDownload}" TextWrapping="WrapWithOverflow" FontSize="10" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -205,13 +226,13 @@
<controls:DataGridTemplateColumnExt CanUserSort="True" Width="100" Header="Tags" SortMemberPath="BookTags" ClipboardContentBinding="{Binding BookTags}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button IsVisible="{Binding !Liberate.IsSeries}" Width="100" Height="80" Click="OnTagsButtonClick" ToolTip.Tip="Click to edit tags" >
<Panel Opacity="{Binding Liberate.Opacity}">
<Panel Width="24" Height="24" IsVisible="{Binding BookTags, Converter={x:Static StringConverters.IsNullOrEmpty}}">
<DataTemplate x:DataType="uibase:IGridEntry">
<Button IsVisible="{CompiledBinding !Liberate.IsSeries}" Width="100" Height="80" Click="OnTagsButtonClick" ToolTip.Tip="Click to edit tags" >
<Panel Opacity="{CompiledBinding Liberate.Opacity}">
<Panel Width="24" Height="24" IsVisible="{CompiledBinding BookTags, Converter={x:Static StringConverters.IsNullOrEmpty}}">
<Path Stretch="Uniform" Fill="{DynamicResource IconFill}" Data="{StaticResource EditTagsIcon}" />
</Panel>
<TextBlock IsVisible="{Binding BookTags, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" FontSize="12" TextWrapping="WrapWithOverflow" HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding BookTags}"/>
<TextBlock IsVisible="{CompiledBinding BookTags, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" FontSize="12" TextWrapping="WrapWithOverflow" HorizontalAlignment="Center" VerticalAlignment="Center" Text="{CompiledBinding BookTags}"/>
</Panel>
</Button>
</DataTemplate>

View File

@@ -1,7 +1,6 @@
using ApplicationServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
using DataLayer;
using FileLiberator;
@@ -256,7 +255,7 @@ namespace LibationAvalonia.Views
contextMenu.MenuClosed += ContextMenu_MenuClosed;
contextMenu.ContextMenuOpening += ContextMenu_ContextMenuOpening;
List<Control> menuItems = new();
contextMenu.Items = menuItems;
contextMenu.ItemsSource = menuItems;
menuItems.Add(new MenuItem { Header = "Show / Hide Columns" });
menuItems.Add(new MenuItem { Header = "-" });
@@ -274,13 +273,9 @@ namespace LibationAvalonia.Views
(
new MenuItem
{
Header = ((string)column.Header).Replace((char)0xa, ' '),
Header = ((string)column.Header).Replace('\n', ' '),
Tag = column,
Margin = new Thickness(6, 0),
Icon = new CheckBox
{
Width = 50,
}
Icon = new CheckBox(),
}
);
@@ -419,7 +414,9 @@ namespace LibationAvalonia.Views
if (!isDefault)
PictureStorage.PictureCached -= PictureCached;
if (!imageDisplayDialog.IsVisible)
if (imageDisplayDialog.IsVisible)
imageDisplayDialog.Activate();
else
imageDisplayDialog.Show();
}

View File

@@ -1,16 +1,11 @@
using AudibleApi.Common;
using AudibleApi;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Media;
using DataLayer;
using Dinah.Core;
using FileLiberator;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
using LibationAvalonia.Dialogs;
using LibationUiBase.SeriesView;
using System;
using Avalonia.Media;
namespace LibationAvalonia.Views
{

View File

@@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
xmlns:uibase="clr-namespace:LibationUiBase.SeriesView;assembly=LibationUiBase"
x:Class="LibationAvalonia.Views.SeriesViewGrid">
<DataGrid
@@ -34,11 +35,11 @@
<DataGridTemplateColumn Width="80" Header="Cover" CanUserSort="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DataTemplate x:DataType="uibase:SeriesItem">
<Image
Tapped="Cover_Click"
Height="80"
Source="{Binding Cover}"
Source="{CompiledBinding Cover}"
ToolTip.Tip="Click to see full size" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -46,10 +47,10 @@
<DataGridTemplateColumn Width="Auto" Header="Series&#xa;Order" CanUserSort="True" SortMemberPath="Order">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DataTemplate x:DataType="uibase:SeriesItem">
<Panel>
<TextBlock
Text="{Binding Order}"
Text="{CompiledBinding Order}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Panel>
@@ -59,7 +60,7 @@
<DataGridTemplateColumn Width="Auto" Header="Availability" CanUserSort="True" SortMemberPath="Button">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DataTemplate x:DataType="uibase:SeriesItem">
<Panel>
<Panel.Styles>
<Style Selector="TextBlock">
@@ -71,17 +72,17 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Click="Availability_Click"
IsVisible="{Binding Button.HasButtonAction}"
IsEnabled="{Binding Button.Enabled}">
IsVisible="{CompiledBinding Button.HasButtonAction}"
IsEnabled="{CompiledBinding Button.Enabled}">
<TextBlock
Text="{Binding Button.DisplayText}"
Text="{CompiledBinding Button.DisplayText}"
TextAlignment="Center"
VerticalAlignment="Center" />
</Button>
<TextBlock
HorizontalAlignment="Center"
IsVisible="{Binding !Button.HasButtonAction}"
Text="{Binding Button.DisplayText}" />
IsVisible="{CompiledBinding !Button.HasButtonAction}"
Text="{CompiledBinding Button.DisplayText}" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -89,11 +90,11 @@
<DataGridTemplateColumn MinWidth="150" Width="*" Header="Title" CanUserSort="True" SortMemberPath="Title">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DataTemplate x:DataType="uibase:SeriesItem">
<Panel ToolTip.Tip="Open Audible product page">
<controls:LinkLabel
VerticalAlignment="Center"
Text="{Binding Title}"
Text="{CompiledBinding Title}"
Tapped="Title_Click" />
</Panel>
</DataTemplate>

View File

@@ -1,6 +1,4 @@
using ApplicationServices;
using AudibleApi.Common;
using AudibleUtilities;
using Avalonia.Collections;
using Avalonia.Controls;
using DataLayer;
@@ -83,7 +81,9 @@ namespace LibationAvalonia.Views
if (!isDefault)
PictureStorage.PictureCached -= PictureCached;
if (!imageDisplayDialog.IsVisible)
if (imageDisplayDialog.IsVisible)
imageDisplayDialog.Activate();
else
imageDisplayDialog.Show();
}
}

View File

@@ -0,0 +1,301 @@
using ApplicationServices;
using AudibleUtilities;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Styling;
using Dinah.Core.StepRunner;
using LibationAvalonia.Dialogs;
using LibationAvalonia.Views;
using LibationFileManager;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using static Avalonia.Threading.Dispatcher;
namespace LibationAvalonia
{
internal class Walkthrough
{
private Dictionary<string, string> settingTabMessages = new()
{
{ "Important Settings", "From here you can change where liberated books are stored and how detailed Libation's logs are.\r\n\r\nIf you experience a problem and need help, you'll be asked to provide your log file. In certain circumstances we may need you to reproduce the error with a higher level of logging detail."},
{ "Import Library", "In this tab you can change how your library is scanned and imported into Libation, as well as automatic liberation."},
{ "Download/Decrypt", "These settings allow you to control how liberated files and folders are named and stored.\r\nYou can customize the 'Naming Templates' to use any number of the audiobook's properties to build a customized file and folder naming format. Learn more about the syntax from the wiki at\r\n\r\nhttps://github.com/rmcrackan/Libation/blob/master/Documentation/NamingTemplates.md"},
{ "Audio File Settings", "Control how audio files are decrypted, including audio format and metadata handling.\r\n\r\nIf you choose to split your audiobook into multiple files by chapter marker, you may edit the chapter file 'Naming Template' to control how each chapter file is named."},
};
private static readonly IBrush FlashColor = Brushes.DodgerBlue;
private readonly MainWindow MainForm;
private readonly AsyncStepSequence sequence = new();
private readonly bool AutoScan;
public Walkthrough(MainWindow mainForm)
{
AutoScan = Configuration.Instance.AutoScan;
Configuration.Instance.AutoScan = false;
MainForm = mainForm;
sequence[nameof(ShowAccountDialog)] = () => UIThread.InvokeAsync(ShowAccountDialog);
sequence[nameof(ShowSettingsDialog)] = () => UIThread.InvokeAsync(ShowSettingsDialog);
sequence[nameof(ShowAccountScanning)] = () => UIThread.InvokeAsync(ShowAccountScanning);
sequence[nameof(ShowSearching)] = () => UIThread.InvokeAsync(ShowSearching);
sequence[nameof(ShowQuickFilters)] = () => UIThread.InvokeAsync(ShowQuickFilters);
sequence[nameof(ShowTourComplete)] = () => UIThread.InvokeAsync(ShowTourComplete);
}
public async Task RunAsync()
{
await sequence.RunAsync();
Configuration.Instance.AutoScan = AutoScan;
}
private async Task<bool> ShowAccountDialog()
{
if (!await ProceedMessageBox("First, add your Audible account(s).", "Add Accounts"))
return false;
await Task.Delay(750);
await displayControlAsync(MainForm.settingsToolStripMenuItem);
await displayControlAsync(MainForm.accountsToolStripMenuItem);
var accountSettings = new AccountsDialog();
accountSettings.Loaded += async (_, _) => await MessageBox.Show(accountSettings, "Add your Audible account(s), then save.", "Add an Account");
await accountSettings.ShowDialog(MainForm);
return true;
}
private async Task<bool> ShowSettingsDialog()
{
if (!await ProceedMessageBox("Next, adjust Libation's settings", "Change Settings"))
return false;
await Task.Delay(750);
await displayControlAsync(MainForm.settingsToolStripMenuItem);
await displayControlAsync(MainForm.basicSettingsToolStripMenuItem);
var settingsDialog = await UIThread.InvokeAsync(() => new SettingsDialog());
var tabsToVisit = settingsDialog.tabControl.Items.OfType<TabItem>().ToList();
foreach (var tab in tabsToVisit)
tab.PropertyChanged += TabControl_PropertyChanged;
settingsDialog.Loaded += SettingsDialog_Loaded;
settingsDialog.Closing += SettingsDialog_FormClosing;
settingsDialog.saveBtn.Content = "Next Tab";
await settingsDialog.ShowDialog(MainForm);
return true;
async Task ShowTabPageMessageBoxAsync(TabItem selectedTab)
{
tabsToVisit.Remove(selectedTab);
if (!selectedTab.IsVisible || !(selectedTab.Header is TextBlock header && settingTabMessages.ContainsKey(header.Text))) return;
if (tabsToVisit.Count == 0)
settingsDialog.saveBtn.Content = "Save";
await MessageBox.Show(settingsDialog, settingTabMessages[header.Text], header.Text + " Tab", MessageBoxButtons.OK);
settingTabMessages.Remove(header.Text);
}
async void SettingsDialog_Loaded(object sender, RoutedEventArgs e)
{
await ShowTabPageMessageBoxAsync(tabsToVisit[0]);
}
async void TabControl_PropertyChanged(object sender, Avalonia.AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == TabItem.IsSelectedProperty && settingsDialog.IsLoaded)
{
await ShowTabPageMessageBoxAsync(sender as TabItem);
}
}
void SettingsDialog_FormClosing(object sender, WindowClosingEventArgs e)
{
if (tabsToVisit.Count > 0)
{
settingsDialog.tabControl.SelectedItem = tabsToVisit[0];
e.Cancel = true;
}
}
}
private async Task<bool> ShowAccountScanning()
{
var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var count = persister.AccountsSettings.Accounts.Count;
persister.Dispose();
if (count < 1)
{
await MessageBox.Show(MainForm, "Add an Audible account, then sync your library through the 'Import' menu", "Add an Audible Account", MessageBoxButtons.OK, MessageBoxIcon.Information);
return false;
}
var accounts = count > 1 ? "accounts" : "account";
var library = count > 1 ? "libraries" : "library";
if(! await ProceedMessageBox($"Finally, scan your Audible {accounts} to sync your {library} with Libation.\r\n\r\nIf this is your first time scanning an account, you'll be prompted to enter your account's password to log into your Audible account.", $"Scan {accounts}"))
return false;
var scanItem = count > 1 ? MainForm.scanLibraryOfAllAccountsToolStripMenuItem : MainForm.scanLibraryToolStripMenuItem;
await Task.Delay(750);
await displayControlAsync(MainForm.importToolStripMenuItem);
await displayControlAsync(scanItem);
scanItem.RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent));
MainForm.importToolStripMenuItem.Close();
var tcs = new TaskCompletionSource();
LibraryCommands.ScanEnd += LibraryCommands_ScanEnd;
await tcs.Task;
LibraryCommands.ScanEnd -= LibraryCommands_ScanEnd;
MainForm.ViewModel.ProductsDisplay.VisibleCountChanged -= productsDisplay_VisibleCountChanged;
return true;
void LibraryCommands_ScanEnd(object sender, int newCount)
{
//if we imported new books, wait for the grid to update before proceeding.
if (newCount > 0)
MainForm.ViewModel.ProductsDisplay.VisibleCountChanged += productsDisplay_VisibleCountChanged;
else
tcs.SetResult();
}
void productsDisplay_VisibleCountChanged(object sender, int e) => tcs.SetResult();
}
private async Task<bool> ShowSearching()
{
var books = DbContexts.GetLibrary_Flat_NoTracking();
if (books.Count == 0) return true;
var firstAuthor = getFirstAuthor();
if (firstAuthor == null) return true;
if (!await ProceedMessageBox("You can filter the grid entries by searching", "Searching"))
return false;
await displayControlAsync(MainForm.filterSearchTb);
MainForm.filterSearchTb.Text = string.Empty;
foreach (var c in firstAuthor)
{
MainForm.filterSearchTb.Text += c;
await Task.Delay(150);
}
await displayControlAsync(MainForm.filterBtn);
MainForm.filterBtn.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
await Task.Delay(1000);
await MessageBox.Show(MainForm, "Libation provides a built-in cheat sheet for its query language", "Search Cheat Sheet");
await displayControlAsync(MainForm.filterHelpBtn);
var filterHelp = new SearchSyntaxDialog();
await filterHelp.ShowDialog(MainForm);
return true;
}
private async Task<bool> ShowQuickFilters()
{
var firstAuthor = getFirstAuthor();
if (firstAuthor == null) return true;
if (!await ProceedMessageBox("Queries that you perform regularly can be added to 'Quick Filters'", "Quick Filters"))
return false;
MainForm.filterSearchTb.Text = firstAuthor;
var editQuickFiltersToolStripMenuItem = MainForm.quickFiltersToolStripMenuItem.ItemsSource.OfType<MenuItem>().ElementAt(1);
await Task.Delay(750);
await displayControlAsync(MainForm.addQuickFilterBtn);
MainForm.addQuickFilterBtn.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
await displayControlAsync(MainForm.quickFiltersToolStripMenuItem);
await displayControlAsync(editQuickFiltersToolStripMenuItem);
var editQuickFilters = new EditQuickFilters();
editQuickFilters.Loaded += async (_, _) => await MessageBox.Show(editQuickFilters, "From here you can edit, delete, and change the order of Quick Filters", "Editing Quick Filters");
await editQuickFilters.ShowDialog(MainForm);
return true;
}
private async Task<bool> ShowTourComplete()
{
await MessageBox.Show(MainForm, "You're now ready to begin using Libation.\r\n\r\nEnjoy!", "Tour Finished");
return true;
}
private string getFirstAuthor()
{
var books = DbContexts.GetLibrary_Flat_NoTracking();
return books.SelectMany(lb => lb.Book.Authors).FirstOrDefault(a => !string.IsNullOrWhiteSpace(a.Name))?.Name;
}
private async Task displayControlAsync(TemplatedControl control)
{
await UIThread.InvokeAsync(() => control.IsEnabled = false);
await UIThread.InvokeAsync(MainForm.productsDisplay.Focus);
await UIThread.InvokeAsync(() => flashControlAsync(control));
if (control is MenuItem menuItem) await UIThread.InvokeAsync(menuItem.Open);
await Task.Delay(500);
await UIThread.InvokeAsync(() => control.IsEnabled = true);
}
private static async Task flashControlAsync(TemplatedControl control, int flashCount = 3)
{
for (int i = 0; i < flashCount; i++)
{
control.Styles.Add(disabledStyle);
control.Styles.Add(disabledStyle2);
await Task.Delay(200);
control.Styles.Remove(disabledStyle);
control.Styles.Remove(disabledStyle2);
control.Styles.Add(enabedStyle);
control.Styles.Add(enabedStyle2);
control.InvalidateVisual();
await Task.Delay(200);
control.Styles.Remove(enabedStyle);
control.Styles.Remove(enabedStyle2);
}
}
private async Task<bool> ProceedMessageBox(string message, string caption)
=> await MessageBox.Show(MainForm, message, caption, MessageBoxButtons.OKCancel) is DialogResult.OK;
private static readonly Setter HighlightSetter = new Setter(Border.BackgroundProperty, FlashColor);
private static readonly Setter HighlightSetter2 = new Setter(ContentPresenter.BackgroundProperty, FlashColor);
private static readonly Setter TransparentSetter = new Setter(Border.BackgroundProperty, Brushes.Transparent);
private static readonly Setter TransparentSetter2 = new Setter(ContentPresenter.BackgroundProperty, Brushes.Transparent);
private static Selector TemplateSelector = Selectors.Is<TemplatedControl>(null).PropertyEquals(Avalonia.Input.InputElement.IsEnabledProperty, false).Template();
private static Selector ContentPresenterSelector = TemplateSelector.Is<ContentPresenter>();
private static Selector BorderSelector = TemplateSelector.Is<Border>();
private static readonly Style disabledStyle = new Style(_ => BorderSelector);
private static readonly Style disabledStyle2 = new Style(_ => ContentPresenterSelector);
private static readonly Style enabedStyle = new Style(_ => BorderSelector);
private static readonly Style enabedStyle2 = new Style(_ => ContentPresenterSelector);
static Walkthrough()
{
disabledStyle.Setters.Add(HighlightSetter);
disabledStyle2.Setters.Add(HighlightSetter2);
enabedStyle.Setters.Add(TransparentSetter);
enabedStyle2.Setters.Add(TransparentSetter2);
}
}
}

Some files were not shown because too many files have changed in this diff Show More