commit
51aabe5dd4
@ -90,6 +90,7 @@ namespace AppScaffolding
|
|||||||
Migrations.migrate_to_v6_6_9(config);
|
Migrations.migrate_to_v6_6_9(config);
|
||||||
Migrations.migrate_to_v11_5_0(config);
|
Migrations.migrate_to_v11_5_0(config);
|
||||||
Migrations.migrate_to_v11_6_5(config);
|
Migrations.migrate_to_v11_6_5(config);
|
||||||
|
Migrations.migrate_to_v12_0_1(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
|
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
|
||||||
@ -417,6 +418,82 @@ namespace AppScaffolding
|
|||||||
public List<string> Filters { get; set; } = new();
|
public List<string> Filters { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void migrate_to_v12_0_1(Configuration config)
|
||||||
|
{
|
||||||
|
#nullable enable
|
||||||
|
//Migrate from version 1 file cache to the dictionary-based version 2 cache
|
||||||
|
const string FILENAME_V1 = "FileLocations.json";
|
||||||
|
const string FILENAME_V2 = "FileLocationsV2.json";
|
||||||
|
|
||||||
|
var jsonFileV1 = Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V1);
|
||||||
|
var jsonFileV2 = Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V2);
|
||||||
|
|
||||||
|
if (!File.Exists(jsonFileV2) && File.Exists(jsonFileV1))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//FilePathCache loads the cache in its static constructor,
|
||||||
|
//so perform migration without using FilePathCache.CacheEntry
|
||||||
|
if (JArray.Parse(File.ReadAllText(jsonFileV1)) is not JArray v1Cache || v1Cache.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Dictionary<string, JArray> cache = new();
|
||||||
|
|
||||||
|
//Convert to c# objects to speed up searching by ID inside the iterator
|
||||||
|
var allItems
|
||||||
|
= v1Cache
|
||||||
|
.Select(i => new
|
||||||
|
{
|
||||||
|
Id = i["Id"]?.Value<string>(),
|
||||||
|
Path = i["Path"]?["Path"]?.Value<string>()
|
||||||
|
}).Where(i => i.Id != null)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var id in allItems.Select(i => i.Id).OfType<string>().Distinct())
|
||||||
|
{
|
||||||
|
//Use this opportunity to purge non-existent files and re-classify file types
|
||||||
|
//(due to *.aax files previously not being classified as FileType.AAXC)
|
||||||
|
var items = allItems
|
||||||
|
.Where(i => i.Id == id && File.Exists(i.Path))
|
||||||
|
.Select(i => new JObject
|
||||||
|
{
|
||||||
|
{ "Id", i.Id },
|
||||||
|
{ "FileType", (int)FileTypes.GetFileTypeFromPath(i.Path) },
|
||||||
|
{ "Path", new JObject{ { "Path", i.Path } } }
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (items.Length == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
cache[id] = new JArray(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheJson = new JObject { { "Dictionary", JObject.FromObject(cache) } };
|
||||||
|
var cacheFileText = cacheJson.ToString(Formatting.Indented);
|
||||||
|
|
||||||
|
void migrate()
|
||||||
|
{
|
||||||
|
File.WriteAllText(jsonFileV2, cacheFileText);
|
||||||
|
File.Delete(jsonFileV1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try { migrate(); }
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
try { migrate(); }
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
migrate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* eat */ }
|
||||||
|
}
|
||||||
|
#nullable restore
|
||||||
|
}
|
||||||
|
|
||||||
public static void migrate_to_v11_6_5(Configuration config)
|
public static void migrate_to_v11_6_5(Configuration config)
|
||||||
{
|
{
|
||||||
//Settings migration for unsupported sample rates (#1116)
|
//Settings migration for unsupported sample rates (#1116)
|
||||||
|
|||||||
@ -144,7 +144,7 @@ namespace ApplicationServices
|
|||||||
PictureId = a.Book.PictureId,
|
PictureId = a.Book.PictureId,
|
||||||
IsAbridged = a.Book.IsAbridged,
|
IsAbridged = a.Book.IsAbridged,
|
||||||
DatePublished = a.Book.DatePublished,
|
DatePublished = a.Book.DatePublished,
|
||||||
CategoriesNames = a.Book.LowestCategoryNames().Any() ? a.Book.LowestCategoryNames().Aggregate((a, b) => $"{a}, {b}") : "",
|
CategoriesNames = string.Join("; ", a.Book.LowestCategoryNames()),
|
||||||
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
|
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
|
||||||
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
|
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
|
||||||
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
|
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="using:LibationAvalonia"
|
xmlns:local="using:LibationAvalonia"
|
||||||
xmlns:controls="using:LibationAvalonia.Controls"
|
xmlns:controls="using:LibationAvalonia.Controls"
|
||||||
|
xmlns:dialogs="using:LibationAvalonia.Dialogs"
|
||||||
x:Class="LibationAvalonia.App"
|
x:Class="LibationAvalonia.App"
|
||||||
Name="Libation">
|
Name="Libation">
|
||||||
|
|
||||||
@ -12,6 +13,10 @@
|
|||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
|
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
|
<ControlTheme x:Key="{x:Type TextBlock}" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
</ControlTheme>
|
||||||
<ResourceDictionary.ThemeDictionaries>
|
<ResourceDictionary.ThemeDictionaries>
|
||||||
<ResourceDictionary x:Key="Light">
|
<ResourceDictionary x:Key="Light">
|
||||||
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
|
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
|
||||||
@ -81,6 +86,60 @@
|
|||||||
<!-- It's called AutoHide, but this is really the mouseover shrink/expand. -->
|
<!-- It's called AutoHide, but this is really the mouseover shrink/expand. -->
|
||||||
<Setter Property="AllowAutoHide" Value="false"/>
|
<Setter Property="AllowAutoHide" Value="false"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="dialogs|DialogWindow">
|
||||||
|
<Style Selector="^[UseCustomTitleBar=false]">
|
||||||
|
<Setter Property="SystemDecorations" Value="Full"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate>
|
||||||
|
<ContentPresenter Background="{DynamicResource SystemControlBackgroundAltHighBrush}" Content="{TemplateBinding Content}" />
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^[UseCustomTitleBar=true]">
|
||||||
|
<Setter Property="SystemDecorations" Value="BorderOnly"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate>
|
||||||
|
<Panel Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
|
||||||
|
<Grid RowDefinitions="30,*">
|
||||||
|
<Border Name="DialogWindowTitleBorder" Margin="5,0" Background="{DynamicResource SystemAltMediumColor}">
|
||||||
|
<Border.Styles>
|
||||||
|
<Style Selector="Button#DialogCloseButton">
|
||||||
|
<Style Selector="^:pointerover">
|
||||||
|
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="Red" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^ Path">
|
||||||
|
<Setter Property="Fill" Value="{DynamicResource IconFill}" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
</Border.Styles>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
|
||||||
|
<Path Name="DialogWindowTitleIcon" Margin="3,5,0,5" Fill="{DynamicResource IconFill}" Stretch="Uniform" Data="{StaticResource LibationGlassIcon}"/>
|
||||||
|
|
||||||
|
<TextBlock Name="DialogWindowTitleTextBlock" Margin="8,0,0,0" VerticalAlignment="Center" FontWeight="DemiBold" FontSize="12" Grid.Column="1" Text="{TemplateBinding Title}" />
|
||||||
|
|
||||||
|
<Button Name="DialogCloseButton" Grid.Column="2">
|
||||||
|
<Path Fill="{DynamicResource SystemControlBackgroundBaseLowBrush}" VerticalAlignment="Center" Stretch="Uniform" RenderTransform="{StaticResource Rotate45Transform}" Data="{StaticResource CancelButtonIcon}" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<Path Stroke="{DynamicResource SystemBaseMediumLowColor}" StrokeThickness="1" VerticalAlignment="Bottom" Stretch="Fill" Data="M0,0 L1,0" />
|
||||||
|
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
|
||||||
|
</Grid>
|
||||||
|
</Panel>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
|
|
||||||
<NativeMenu.Menu>
|
<NativeMenu.Menu>
|
||||||
|
|||||||
@ -44,7 +44,7 @@ namespace LibationAvalonia
|
|||||||
|
|
||||||
if (!config.LibationSettingsAreValid)
|
if (!config.LibationSettingsAreValid)
|
||||||
{
|
{
|
||||||
var defaultLibationFilesDir = Configuration.UserProfile;
|
var defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory;
|
||||||
|
|
||||||
// check for existing settings in default location
|
// check for existing settings in default location
|
||||||
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
||||||
@ -82,8 +82,8 @@ namespace LibationAvalonia
|
|||||||
// - error message, Exit()
|
// - error message, Exit()
|
||||||
if (setupDialog.IsNewUser)
|
if (setupDialog.IsNewUser)
|
||||||
{
|
{
|
||||||
Configuration.SetLibationFiles(Configuration.UserProfile);
|
Configuration.SetLibationFiles(Configuration.DefaultLibationFilesDirectory);
|
||||||
setupDialog.Config.Books = Path.Combine(Configuration.UserProfile, nameof(Configuration.Books));
|
setupDialog.Config.Books = Configuration.DefaultBooksDirectory;
|
||||||
|
|
||||||
if (setupDialog.Config.LibationSettingsAreValid)
|
if (setupDialog.Config.LibationSettingsAreValid)
|
||||||
{
|
{
|
||||||
@ -174,7 +174,7 @@ namespace LibationAvalonia
|
|||||||
|
|
||||||
if (continueResult == DialogResult.Yes)
|
if (continueResult == DialogResult.Yes)
|
||||||
{
|
{
|
||||||
config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books));
|
config.Books = Configuration.DefaultBooksDirectory;
|
||||||
|
|
||||||
if (config.LibationSettingsAreValid)
|
if (config.LibationSettingsAreValid)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -91,6 +91,32 @@
|
|||||||
S 192,128 147,147
|
S 192,128 147,147
|
||||||
</StreamGeometry>
|
</StreamGeometry>
|
||||||
|
|
||||||
|
<StreamGeometry x:Key="LibationGlassIcon">
|
||||||
|
M262,8
|
||||||
|
h-117
|
||||||
|
a 192,200 0 0 0 -36,82
|
||||||
|
a 222,334 41 0 0 138,236
|
||||||
|
v158
|
||||||
|
h-81
|
||||||
|
a 16,16 0 0 0 0,32
|
||||||
|
h192
|
||||||
|
a 16 16 0 0 0 0,-32
|
||||||
|
h-81
|
||||||
|
v-158
|
||||||
|
a 222,334 -41 0 0 138,-236
|
||||||
|
a 192,200 0 0 0 -36,-82
|
||||||
|
h-117
|
||||||
|
m-99,30
|
||||||
|
a 192,200 0 0 0 -26,95
|
||||||
|
a 187.5,334 35 0 0 125,159
|
||||||
|
a 187.5,334 -35 0 0 125,-159
|
||||||
|
a 192,200 0 0 0 -26,-95
|
||||||
|
h-198
|
||||||
|
M158,136
|
||||||
|
a 168,305 35 0 0 104,136
|
||||||
|
a 168,305 -35 0 0 104,-136
|
||||||
|
</StreamGeometry>
|
||||||
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</Styles.Resources>
|
</Styles.Resources>
|
||||||
</Styles>
|
</Styles>
|
||||||
|
|||||||
@ -51,7 +51,9 @@ namespace LibationAvalonia.Controls
|
|||||||
{
|
{
|
||||||
Configuration.KnownDirectories.WinTemp,
|
Configuration.KnownDirectories.WinTemp,
|
||||||
Configuration.KnownDirectories.UserProfile,
|
Configuration.KnownDirectories.UserProfile,
|
||||||
|
Configuration.KnownDirectories.ApplicationData,
|
||||||
Configuration.KnownDirectories.AppDir,
|
Configuration.KnownDirectories.AppDir,
|
||||||
|
Configuration.KnownDirectories.MyMusic,
|
||||||
Configuration.KnownDirectories.MyDocs,
|
Configuration.KnownDirectories.MyDocs,
|
||||||
Configuration.KnownDirectories.LibationFiles
|
Configuration.KnownDirectories.LibationFiles
|
||||||
};
|
};
|
||||||
|
|||||||
@ -167,15 +167,6 @@
|
|||||||
SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}"
|
SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}"
|
||||||
ItemsSource="{CompiledBinding Themes}"/>
|
ItemsSource="{CompiledBinding Themes}"/>
|
||||||
|
|
||||||
<TextBlock
|
|
||||||
Grid.Column="2"
|
|
||||||
FontSize="16"
|
|
||||||
FontWeight="Bold"
|
|
||||||
Margin="10,0"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
IsVisible="{CompiledBinding SelectionChanged}"
|
|
||||||
Text="Theme change takes effect on restart"/>
|
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -9,19 +11,98 @@ namespace LibationAvalonia.Dialogs
|
|||||||
{
|
{
|
||||||
public bool SaveAndRestorePosition { get; set; } = true;
|
public bool SaveAndRestorePosition { get; set; } = true;
|
||||||
public Control ControlToFocusOnShow { get; set; }
|
public Control ControlToFocusOnShow { get; set; }
|
||||||
|
protected override Type StyleKeyOverride => typeof(DialogWindow);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> UseCustomTitleBarProperty =
|
||||||
|
AvaloniaProperty.Register<DialogWindow, bool>(nameof(UseCustomTitleBar));
|
||||||
|
|
||||||
|
public bool UseCustomTitleBar
|
||||||
|
{
|
||||||
|
get { return GetValue(UseCustomTitleBarProperty); }
|
||||||
|
set { SetValue(UseCustomTitleBarProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
public DialogWindow()
|
public DialogWindow()
|
||||||
{
|
{
|
||||||
this.HideMinMaxBtns();
|
KeyDown += DialogWindow_KeyDown;
|
||||||
this.KeyDown += DialogWindow_KeyDown;
|
Initialized += DialogWindow_Initialized;
|
||||||
this.Initialized += DialogWindow_Initialized;
|
Opened += DialogWindow_Opened;
|
||||||
this.Opened += DialogWindow_Opened;
|
Closing += DialogWindow_Closing;
|
||||||
this.Closing += DialogWindow_Closing;
|
|
||||||
|
UseCustomTitleBar = Configuration.IsWindows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool fixedMinHeight = false;
|
||||||
|
private bool fixedMaxHeight = false;
|
||||||
|
private bool fixedHeight = false;
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
|
{
|
||||||
|
const int customTitleBarHeight = 30;
|
||||||
|
if (UseCustomTitleBar)
|
||||||
|
{
|
||||||
|
if (change.Property == MinHeightProperty && !fixedMinHeight)
|
||||||
|
{
|
||||||
|
fixedMinHeight = true;
|
||||||
|
MinHeight += customTitleBarHeight;
|
||||||
|
fixedMinHeight = false;
|
||||||
|
}
|
||||||
|
if (change.Property == MaxHeightProperty && !fixedMaxHeight)
|
||||||
|
{
|
||||||
|
fixedMaxHeight = true;
|
||||||
|
MaxHeight += customTitleBarHeight;
|
||||||
|
fixedMaxHeight = false;
|
||||||
|
}
|
||||||
|
if (change.Property == HeightProperty && !fixedHeight)
|
||||||
|
{
|
||||||
|
fixedHeight = true;
|
||||||
|
Height += customTitleBarHeight;
|
||||||
|
fixedHeight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base.OnPropertyChanged(change);
|
||||||
|
}
|
||||||
|
|
||||||
public DialogWindow(bool saveAndRestorePosition) : this()
|
public DialogWindow(bool saveAndRestorePosition) : this()
|
||||||
{
|
{
|
||||||
SaveAndRestorePosition = saveAndRestorePosition;
|
SaveAndRestorePosition = saveAndRestorePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnApplyTemplate(e);
|
||||||
|
|
||||||
|
if (!UseCustomTitleBar)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var closeButton = e.NameScope.Find<Button>("DialogCloseButton");
|
||||||
|
var border = e.NameScope.Get<Border>("DialogWindowTitleBorder");
|
||||||
|
var titleBlock = e.NameScope.Get<TextBlock>("DialogWindowTitleTextBlock");
|
||||||
|
var icon = e.NameScope.Get<Avalonia.Controls.Shapes.Path>("DialogWindowTitleIcon");
|
||||||
|
|
||||||
|
closeButton.Click += CloseButton_Click;
|
||||||
|
border.PointerPressed += Border_PointerPressed;
|
||||||
|
icon.IsVisible = Icon != null;
|
||||||
|
|
||||||
|
if (MinHeight == MaxHeight && MinWidth == MaxWidth)
|
||||||
|
{
|
||||||
|
CanResize = false;
|
||||||
|
border.Margin = new Thickness(0);
|
||||||
|
icon.Margin = new Thickness(8, 5, 0, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Border_PointerPressed(object sender, Avalonia.Input.PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.GetCurrentPoint(null).Properties.IsLeftButtonPressed)
|
||||||
|
BeginMoveDrag(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
CancelAndClose();
|
||||||
|
}
|
||||||
|
|
||||||
private void DialogWindow_Initialized(object sender, EventArgs e)
|
private void DialogWindow_Initialized(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||||
|
|||||||
@ -25,7 +25,6 @@ namespace LibationAvalonia.Dialogs
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
this.HideMinMaxBtns();
|
|
||||||
ControlToFocusOnShow = this.FindControl<Button>(nameof(ImportButton));
|
ControlToFocusOnShow = this.FindControl<Button>(nameof(ImportButton));
|
||||||
|
|
||||||
LoadAccounts();
|
LoadAccounts();
|
||||||
|
|||||||
@ -12,8 +12,6 @@ namespace LibationAvalonia.Dialogs
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
this.HideMinMaxBtns();
|
|
||||||
|
|
||||||
StringFields = @"
|
StringFields = @"
|
||||||
Search for wizard of oz:
|
Search for wizard of oz:
|
||||||
title:oz
|
title:oz
|
||||||
|
|||||||
@ -109,23 +109,5 @@ namespace LibationAvalonia
|
|||||||
public int Width;
|
public int Width;
|
||||||
public bool IsMaximized;
|
public bool IsMaximized;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void HideMinMaxBtns(this Window form)
|
|
||||||
{
|
|
||||||
if (Design.IsDesignMode || !Configuration.IsWindows || form.TryGetPlatformHandle() is not IPlatformHandle handle)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var currentStyle = GetWindowLong(handle.Handle, GWL_STYLE);
|
|
||||||
|
|
||||||
SetWindowLong(handle.Handle, GWL_STYLE, currentStyle & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
|
|
||||||
}
|
|
||||||
|
|
||||||
const long WS_MINIMIZEBOX = 0x00020000L;
|
|
||||||
const long WS_MAXIMIZEBOX = 0x10000L;
|
|
||||||
const int GWL_STYLE = -16;
|
|
||||||
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "GetWindowLong")]
|
|
||||||
static extern long GetWindowLong(IntPtr hWnd, int nIndex);
|
|
||||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
|
||||||
static extern int SetWindowLong(IntPtr hWnd, int nIndex, long dwNewLong);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -165,8 +165,6 @@ Libation.
|
|||||||
|
|
||||||
var dialog = new MessageBoxWindow(saveAndRestorePosition);
|
var dialog = new MessageBoxWindow(saveAndRestorePosition);
|
||||||
|
|
||||||
dialog.HideMinMaxBtns();
|
|
||||||
|
|
||||||
var vm = new MessageBoxViewModel(message, caption, buttons, icon, defaultButton);
|
var vm = new MessageBoxViewModel(message, caption, buttons, icon, defaultButton);
|
||||||
dialog.DataContext = vm;
|
dialog.DataContext = vm;
|
||||||
dialog.ControlToFocusOnShow = dialog.FindControl<Control>(defaultButton.ToString());
|
dialog.ControlToFocusOnShow = dialog.FindControl<Control>(defaultButton.ToString());
|
||||||
@ -190,11 +188,13 @@ Libation.
|
|||||||
|
|
||||||
tbx.Height = tbx.DesiredSize.Height;
|
tbx.Height = tbx.DesiredSize.Height;
|
||||||
tbx.Width = tbx.DesiredSize.Width;
|
tbx.Width = tbx.DesiredSize.Width;
|
||||||
dialog.MinHeight = vm.FormHeightFromTboxHeight((int)tbx.DesiredSize.Height);
|
|
||||||
|
var absoluteHeight = vm.FormHeightFromTboxHeight((int)tbx.DesiredSize.Height);
|
||||||
|
dialog.MinHeight = absoluteHeight;
|
||||||
dialog.MinWidth = vm.FormWidthFromTboxWidth((int)tbx.DesiredSize.Width);
|
dialog.MinWidth = vm.FormWidthFromTboxWidth((int)tbx.DesiredSize.Width);
|
||||||
dialog.MaxHeight = dialog.MinHeight;
|
dialog.MaxHeight = absoluteHeight;
|
||||||
dialog.MaxWidth = dialog.MinWidth;
|
dialog.MaxWidth = dialog.MinWidth;
|
||||||
dialog.Height = dialog.MinHeight;
|
dialog.Height = absoluteHeight;
|
||||||
dialog.Width = dialog.MinWidth;
|
dialog.Width = dialog.MinWidth;
|
||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ using DataLayer;
|
|||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
@ -44,9 +43,6 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
private void Configure_BackupCounts()
|
private void Configure_BackupCounts()
|
||||||
{
|
{
|
||||||
LibraryCommands.LibrarySizeChanged += async (object _, List<LibraryBook> libraryBooks)
|
|
||||||
=> await SetBackupCountsAsync(libraryBooks);
|
|
||||||
|
|
||||||
//Pass null to the setup count to get the whole library.
|
//Pass null to the setup count to get the whole library.
|
||||||
LibraryCommands.BookUserDefinedItemCommitted += async (_, _)
|
LibraryCommands.BookUserDefinedItemCommitted += async (_, _)
|
||||||
=> await SetBackupCountsAsync(null);
|
=> await SetBackupCountsAsync(null);
|
||||||
|
|||||||
@ -10,8 +10,8 @@ namespace LibationAvalonia.ViewModels
|
|||||||
{
|
{
|
||||||
partial class MainVM
|
partial class MainVM
|
||||||
{
|
{
|
||||||
private int _visibleNotLiberated = 1;
|
private int _visibleNotLiberated = 0;
|
||||||
private int _visibleCount = 1;
|
private int _visibleCount = 0;
|
||||||
|
|
||||||
/// <summary> The Bottom-right visible book count status text </summary>
|
/// <summary> The Bottom-right visible book count status text </summary>
|
||||||
public string VisibleCountText => $"Visible: {_visibleCount}";
|
public string VisibleCountText => $"Visible: {_visibleCount}";
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using LibationAvalonia.Views;
|
|||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LibationAvalonia.ViewModels
|
namespace LibationAvalonia.ViewModels
|
||||||
{
|
{
|
||||||
@ -38,7 +39,9 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
private async void LibraryCommands_LibrarySizeChanged(object sender, List<LibraryBook> fullLibrary)
|
private async void LibraryCommands_LibrarySizeChanged(object sender, List<LibraryBook> fullLibrary)
|
||||||
{
|
{
|
||||||
await ProductsDisplay.UpdateGridAsync(fullLibrary);
|
await Task.WhenAll(
|
||||||
|
SetBackupCountsAsync(fullLibrary),
|
||||||
|
ProductsDisplay.UpdateGridAsync(fullLibrary));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
|
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
|
||||||
|
|||||||
@ -118,7 +118,15 @@ namespace LibationAvalonia.ViewModels
|
|||||||
#region Add Books to Queue
|
#region Add Books to Queue
|
||||||
|
|
||||||
private bool isBookInQueue(LibraryBook libraryBook)
|
private bool isBookInQueue(LibraryBook libraryBook)
|
||||||
=> Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
|
{
|
||||||
|
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
|
||||||
|
if (entry == null)
|
||||||
|
return false;
|
||||||
|
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
|
||||||
|
return !Queue.RemoveCompleted(entry);
|
||||||
|
else
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void AddDownloadPdf(LibraryBook libraryBook)
|
public void AddDownloadPdf(LibraryBook libraryBook)
|
||||||
=> AddDownloadPdf(new List<LibraryBook>() { libraryBook });
|
=> AddDownloadPdf(new List<LibraryBook>() { libraryBook });
|
||||||
|
|||||||
@ -21,6 +21,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
|
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
|
||||||
{
|
{
|
||||||
Configuration.KnownDirectories.WinTemp,
|
Configuration.KnownDirectories.WinTemp,
|
||||||
|
Configuration.KnownDirectories.ApplicationData,
|
||||||
Configuration.KnownDirectories.UserProfile,
|
Configuration.KnownDirectories.UserProfile,
|
||||||
Configuration.KnownDirectories.AppDir,
|
Configuration.KnownDirectories.AppDir,
|
||||||
Configuration.KnownDirectories.MyDocs,
|
Configuration.KnownDirectories.MyDocs,
|
||||||
|
|||||||
@ -67,7 +67,8 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
{
|
{
|
||||||
Configuration.KnownDirectories.UserProfile,
|
Configuration.KnownDirectories.UserProfile,
|
||||||
Configuration.KnownDirectories.AppDir,
|
Configuration.KnownDirectories.AppDir,
|
||||||
Configuration.KnownDirectories.MyDocs
|
Configuration.KnownDirectories.MyDocs,
|
||||||
|
Configuration.KnownDirectories.MyMusic,
|
||||||
};
|
};
|
||||||
|
|
||||||
public string BooksText { get; } = Configuration.GetDescription(nameof(Configuration.Books));
|
public string BooksText { get; } = Configuration.GetDescription(nameof(Configuration.Books));
|
||||||
@ -100,11 +101,14 @@ namespace LibationAvalonia.ViewModels.Settings
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
this.RaiseAndSetIfChanged(ref themeVariant, value);
|
this.RaiseAndSetIfChanged(ref themeVariant, value);
|
||||||
|
App.Current.RequestedThemeVariant = themeVariant switch
|
||||||
SelectionChanged = ThemeVariant != initialThemeVariant;
|
{
|
||||||
this.RaisePropertyChanged(nameof(SelectionChanged));
|
nameof(Avalonia.Styling.ThemeVariant.Dark) => Avalonia.Styling.ThemeVariant.Dark,
|
||||||
|
nameof(Avalonia.Styling.ThemeVariant.Light) => Avalonia.Styling.ThemeVariant.Light,
|
||||||
|
// "System"
|
||||||
|
_ => Avalonia.Styling.ThemeVariant.Default
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public bool SelectionChanged { get; private set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ using ReactiveUI;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LibationAvalonia.Views
|
namespace LibationAvalonia.Views
|
||||||
{
|
{
|
||||||
@ -60,13 +61,14 @@ namespace LibationAvalonia.Views
|
|||||||
filterSearchTb.Focus();
|
filterSearchTb.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async System.Threading.Tasks.Task OnLibraryLoadedAsync(List<LibraryBook> initialLibrary)
|
public async Task OnLibraryLoadedAsync(List<LibraryBook> initialLibrary)
|
||||||
{
|
{
|
||||||
if (QuickFilters.UseDefault)
|
if (QuickFilters.UseDefault)
|
||||||
await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault());
|
await ViewModel.PerformFilter(QuickFilters.Filters.FirstOrDefault());
|
||||||
|
|
||||||
await ViewModel.SetBackupCountsAsync(initialLibrary);
|
await Task.WhenAll(
|
||||||
await ViewModel.ProductsDisplay.BindToGridAsync(initialLibrary);
|
ViewModel.SetBackupCountsAsync(initialLibrary),
|
||||||
|
ViewModel.ProductsDisplay.BindToGridAsync(initialLibrary));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook);
|
public void ProductsDisplay_LiberateClicked(object _, LibraryBook libraryBook) => ViewModel.LiberateClicked(libraryBook);
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
xmlns:views="clr-namespace:LibationAvalonia.Views"
|
xmlns:views="clr-namespace:LibationAvalonia.Views"
|
||||||
xmlns:viewModels="clr-namespace:LibationAvalonia.ViewModels"
|
xmlns:viewModels="clr-namespace:LibationAvalonia.ViewModels"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="850"
|
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="650"
|
||||||
x:Class="LibationAvalonia.Views.ProcessQueueControl">
|
x:Class="LibationAvalonia.Views.ProcessQueueControl">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
@ -39,7 +39,8 @@
|
|||||||
<ScrollViewer
|
<ScrollViewer
|
||||||
Name="scroller"
|
Name="scroller"
|
||||||
HorizontalScrollBarVisibility="Disabled"
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
VerticalScrollBarVisibility="Auto">
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
AllowAutoHide="False">
|
||||||
<ItemsRepeater IsVisible="True"
|
<ItemsRepeater IsVisible="True"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Name="repeater"
|
Name="repeater"
|
||||||
|
|||||||
@ -50,7 +50,7 @@ namespace LibationFileManager
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
|
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
|
||||||
Configuration.Instance.Books = Path.Combine(Configuration.UserProfile, "Books");
|
Configuration.Instance.Books = Configuration.DefaultBooksDirectory;
|
||||||
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
|
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,8 +14,12 @@ namespace LibationFileManager
|
|||||||
public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}";
|
public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}";
|
||||||
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY));
|
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY));
|
||||||
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
|
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
|
||||||
|
public static string MyMusic => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic), "Libation"));
|
||||||
public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation"));
|
public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation"));
|
||||||
public static string UserProfile => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation"));
|
public static string UserProfile => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation"));
|
||||||
|
public static string LocalAppData => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Libation"));
|
||||||
|
public static string DefaultLibationFilesDirectory => !IsWindows ? LocalAppData : UserProfile;
|
||||||
|
public static string DefaultBooksDirectory => Path.Combine(!IsWindows ? MyMusic : UserProfile, nameof(Books));
|
||||||
|
|
||||||
public enum KnownDirectories
|
public enum KnownDirectories
|
||||||
{
|
{
|
||||||
@ -34,19 +38,27 @@ namespace LibationFileManager
|
|||||||
MyDocs = 4,
|
MyDocs = 4,
|
||||||
|
|
||||||
[Description("Your settings folder (aka: Libation Files)")]
|
[Description("Your settings folder (aka: Libation Files)")]
|
||||||
LibationFiles = 5
|
LibationFiles = 5,
|
||||||
|
|
||||||
|
[Description("User Application Data Folder")]
|
||||||
|
ApplicationData = 6,
|
||||||
|
|
||||||
|
[Description("My Music")]
|
||||||
|
MyMusic = 7,
|
||||||
}
|
}
|
||||||
// use func calls so we always get the latest value of LibationFiles
|
// use func calls so we always get the latest value of LibationFiles
|
||||||
private static List<(KnownDirectories directory, Func<string?> getPathFunc)> directoryOptionsPaths { get; } = new()
|
private static List<(KnownDirectories directory, Func<string?> getPathFunc)> directoryOptionsPaths { get; } = new()
|
||||||
{
|
{
|
||||||
(KnownDirectories.None, () => null),
|
(KnownDirectories.None, () => null),
|
||||||
|
(KnownDirectories.ApplicationData, () => LocalAppData),
|
||||||
|
(KnownDirectories.MyMusic, () => MyMusic),
|
||||||
(KnownDirectories.UserProfile, () => UserProfile),
|
(KnownDirectories.UserProfile, () => UserProfile),
|
||||||
(KnownDirectories.AppDir, () => AppDir_Relative),
|
(KnownDirectories.AppDir, () => AppDir_Relative),
|
||||||
(KnownDirectories.WinTemp, () => WinTemp),
|
(KnownDirectories.WinTemp, () => WinTemp),
|
||||||
(KnownDirectories.MyDocs, () => MyDocs),
|
(KnownDirectories.MyDocs, () => MyDocs),
|
||||||
// this is important to not let very early calls try to accidentally load LibationFiles too early.
|
// this is important to not let very early calls try to accidentally load LibationFiles too early.
|
||||||
// also, keep this at bottom of this list
|
// also, keep this at bottom of this list
|
||||||
(KnownDirectories.LibationFiles, () => libationFilesPathCache)
|
(KnownDirectories.LibationFiles, () => LibationSettingsDirectory)
|
||||||
};
|
};
|
||||||
public static string? GetKnownDirectoryPath(KnownDirectories directory)
|
public static string? GetKnownDirectoryPath(KnownDirectories directory)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -22,11 +22,11 @@ namespace LibationFileManager
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (libationFilesPathCache is not null)
|
if (LibationSettingsDirectory is not null)
|
||||||
return libationFilesPathCache;
|
return LibationSettingsDirectory;
|
||||||
|
|
||||||
// FIRST: must write here before SettingsFilePath in next step reads cache
|
// FIRST: must write here before SettingsFilePath in next step reads cache
|
||||||
libationFilesPathCache = getLibationFilesSettingFromJson();
|
LibationSettingsDirectory = getLibationFilesSettingFromJson();
|
||||||
|
|
||||||
// SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist
|
// SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist
|
||||||
persistentDictionary = new PersistentDictionary(SettingsFilePath);
|
persistentDictionary = new PersistentDictionary(SettingsFilePath);
|
||||||
@ -42,11 +42,14 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
SetWithJsonPath(jsonpath, "path", logPath, true);
|
SetWithJsonPath(jsonpath, "path", logPath, true);
|
||||||
|
|
||||||
return libationFilesPathCache;
|
return LibationSettingsDirectory;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? libationFilesPathCache { get; set; }
|
/// <summary>
|
||||||
|
/// Directory pointed to by appsettings.json
|
||||||
|
/// </summary>
|
||||||
|
private static string? LibationSettingsDirectory { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try to find appsettings.json in the following locations:
|
/// Try to find appsettings.json in the following locations:
|
||||||
@ -79,7 +82,7 @@ namespace LibationFileManager
|
|||||||
string[] possibleAppsettingsDirectories = new[]
|
string[] possibleAppsettingsDirectories = new[]
|
||||||
{
|
{
|
||||||
ProcessDirectory,
|
ProcessDirectory,
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Libation"),
|
LocalAppData,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
Path.Combine(Path.GetTempPath(), "Libation")
|
Path.Combine(Path.GetTempPath(), "Libation")
|
||||||
};
|
};
|
||||||
@ -106,9 +109,15 @@ namespace LibationFileManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Valid appsettings.json not found. Try to create it in each folder.
|
//Valid appsettings.json not found. Try to create it in each folder.
|
||||||
var endingContents = new JObject { { LIBATION_FILES_KEY, UserProfile } }.ToString(Formatting.Indented);
|
var endingContents = new JObject { { LIBATION_FILES_KEY, DefaultLibationFilesDirectory } }.ToString(Formatting.Indented);
|
||||||
|
|
||||||
foreach (var dir in possibleAppsettingsDirectories)
|
foreach (var dir in possibleAppsettingsDirectories)
|
||||||
{
|
{
|
||||||
|
//Don't try to create appsettings.json in the program files directory on *.nix systems.
|
||||||
|
//However, still _look_ for one there for backwards compatibility with previous installations
|
||||||
|
if (!IsWindows && dir == ProcessDirectory)
|
||||||
|
continue;
|
||||||
|
|
||||||
var appsettingsFile = Path.Combine(dir, appsettings_filename);
|
var appsettingsFile = Path.Combine(dir, appsettings_filename);
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -180,7 +189,7 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
public static void SetLibationFiles(string directory)
|
public static void SetLibationFiles(string directory)
|
||||||
{
|
{
|
||||||
libationFilesPathCache = null;
|
LibationSettingsDirectory = null;
|
||||||
|
|
||||||
var startingContents = File.ReadAllText(AppsettingsJsonFile);
|
var startingContents = File.ReadAllText(AppsettingsJsonFile);
|
||||||
var jObj = JObject.Parse(startingContents);
|
var jObj = JObject.Parse(startingContents);
|
||||||
|
|||||||
@ -18,9 +18,8 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
var pDic = new PersistentDictionary(settingsFile, isReadOnly: false);
|
var pDic = new PersistentDictionary(settingsFile, isReadOnly: false);
|
||||||
|
|
||||||
var booksDir = pDic.GetString(nameof(Books));
|
if (pDic.GetString(nameof(Books)) is not string booksDir)
|
||||||
|
return false;
|
||||||
if (booksDir is null) return false;
|
|
||||||
|
|
||||||
if (!Directory.Exists(booksDir))
|
if (!Directory.Exists(booksDir))
|
||||||
{
|
{
|
||||||
@ -28,17 +27,21 @@ namespace LibationFileManager
|
|||||||
throw new DirectoryNotFoundException(settingsFile);
|
throw new DirectoryNotFoundException(settingsFile);
|
||||||
|
|
||||||
//"Books" is not null, so setup has already been run.
|
//"Books" is not null, so setup has already been run.
|
||||||
//Since Books can't be found, try to create it in Libation settings folder
|
//Since Books can't be found, try to create it
|
||||||
booksDir = Path.Combine(dir, nameof(Books));
|
//and then revert to the default books directory
|
||||||
|
foreach (string d in new string[] { booksDir, DefaultBooksDirectory })
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(booksDir);
|
Directory.CreateDirectory(d);
|
||||||
|
|
||||||
pDic.SetString(nameof(Books), booksDir);
|
pDic.SetString(nameof(Books), d);
|
||||||
|
|
||||||
return booksDir is not null && Directory.Exists(booksDir);
|
return Directory.Exists(d);
|
||||||
}
|
}
|
||||||
catch { return false; }
|
catch { /* Do Nothing */ }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dinah.Core.Collections.Immutable;
|
|
||||||
using FileManager;
|
using FileManager;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
@ -13,34 +13,31 @@ namespace LibationFileManager
|
|||||||
{
|
{
|
||||||
public record CacheEntry(string Id, FileType FileType, LongPath Path);
|
public record CacheEntry(string Id, FileType FileType, LongPath Path);
|
||||||
|
|
||||||
private const string FILENAME = "FileLocations.json";
|
private const string FILENAME_V2 = "FileLocationsV2.json";
|
||||||
|
|
||||||
public static event EventHandler<CacheEntry>? Inserted;
|
public static event EventHandler<CacheEntry>? Inserted;
|
||||||
public static event EventHandler<CacheEntry>? Removed;
|
public static event EventHandler<CacheEntry>? Removed;
|
||||||
|
|
||||||
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
|
private static LongPath jsonFileV2 => Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V2);
|
||||||
|
|
||||||
private static LongPath jsonFile => Path.Combine(Configuration.Instance.LibationFiles, FILENAME);
|
private static readonly FileCacheV2<CacheEntry> Cache = new();
|
||||||
|
|
||||||
static FilePathCache()
|
static FilePathCache()
|
||||||
{
|
{
|
||||||
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
|
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
|
||||||
if (!File.Exists(jsonFile))
|
if (!File.Exists(jsonFileV2))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var list = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(jsonFile));
|
Cache = JsonConvert.DeserializeObject<FileCacheV2<CacheEntry>>(File.ReadAllText(jsonFileV2))
|
||||||
if (list is null)
|
?? throw new NullReferenceException("File exists but deserialize is null. This will never happen when file is healthy.");
|
||||||
throw new NullReferenceException("File exists but deserialize is null. This will never happen when file is healthy.");
|
|
||||||
|
|
||||||
cache = new Cache<CacheEntry>(list);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Serilog.Log.Logger.Error(ex, "Error deserializing file. Wrong format. Possibly corrupt. Deleting file. {@DebugInfo}", new { jsonFile });
|
Serilog.Log.Logger.Error(ex, "Error deserializing file. Wrong format. Possibly corrupt. Deleting file. {@DebugInfo}", new { jsonFileV2 });
|
||||||
lock (locker)
|
lock (locker)
|
||||||
File.Delete(jsonFile);
|
File.Delete(jsonFileV2);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -48,42 +45,63 @@ namespace LibationFileManager
|
|||||||
public static bool Exists(string id, FileType type) => GetFirstPath(id, type) is not null;
|
public static bool Exists(string id, FileType type) => GetFirstPath(id, type) is not null;
|
||||||
|
|
||||||
public static List<(FileType fileType, LongPath path)> GetFiles(string id)
|
public static List<(FileType fileType, LongPath path)> GetFiles(string id)
|
||||||
=> getEntries(entry => entry.Id == id)
|
{
|
||||||
.Select(entry => (entry.FileType, entry.Path))
|
var matchingFiles = Cache.GetIdEntries(id);
|
||||||
.ToList();
|
|
||||||
|
bool cacheChanged = false;
|
||||||
|
|
||||||
|
//Verify all entries exist
|
||||||
|
for (int i = 0; i < matchingFiles.Count; i++)
|
||||||
|
{
|
||||||
|
if (!File.Exists(matchingFiles[i].Path))
|
||||||
|
{
|
||||||
|
matchingFiles.RemoveAt(i);
|
||||||
|
cacheChanged |= Remove(matchingFiles[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cacheChanged)
|
||||||
|
save();
|
||||||
|
|
||||||
|
return matchingFiles.Select(e => (e.FileType, e.Path)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public static LongPath? GetFirstPath(string id, FileType type)
|
public static LongPath? GetFirstPath(string id, FileType type)
|
||||||
=> getEntries(entry => entry.Id == id && entry.FileType == type)
|
|
||||||
?.FirstOrDefault()
|
|
||||||
?.Path;
|
|
||||||
|
|
||||||
private static IEnumerable<CacheEntry> getEntries(Func<CacheEntry, bool> predicate)
|
|
||||||
{
|
{
|
||||||
var entries = cache.Where(predicate).ToList();
|
var matchingFiles = Cache.GetIdEntries(id).Where(e => e.FileType == type).ToList();
|
||||||
if (entries is null || !entries.Any())
|
|
||||||
return Enumerable.Empty<CacheEntry>();
|
|
||||||
|
|
||||||
remove(entries.Where(e => !File.Exists(e.Path)).ToList());
|
bool cacheChanged = false;
|
||||||
|
try
|
||||||
return cache.Where(predicate).ToList();
|
{
|
||||||
|
//Verify entries exist, but return first matching 'type'
|
||||||
|
for (int i = 0; i < matchingFiles.Count; i++)
|
||||||
|
{
|
||||||
|
if (File.Exists(matchingFiles[i].Path))
|
||||||
|
return matchingFiles[i].Path;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
matchingFiles.RemoveAt(i);
|
||||||
|
cacheChanged |= Remove(matchingFiles[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void remove(List<CacheEntry> entries)
|
|
||||||
{
|
|
||||||
if (entries is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
lock (locker)
|
|
||||||
{
|
|
||||||
foreach (var entry in entries)
|
|
||||||
{
|
|
||||||
cache.Remove(entry);
|
|
||||||
Removed?.Invoke(null, entry);
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (cacheChanged)
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool Remove(CacheEntry entry)
|
||||||
|
{
|
||||||
|
if (Cache.Remove(entry.Id, entry))
|
||||||
|
{
|
||||||
|
Removed?.Invoke(null, entry);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static void Insert(string id, string path)
|
public static void Insert(string id, string path)
|
||||||
{
|
{
|
||||||
var type = FileTypes.GetFileTypeFromPath(path);
|
var type = FileTypes.GetFileTypeFromPath(path);
|
||||||
@ -92,7 +110,7 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
public static void Insert(CacheEntry entry)
|
public static void Insert(CacheEntry entry)
|
||||||
{
|
{
|
||||||
cache.Add(entry);
|
Cache.Add(entry.Id, entry);
|
||||||
Inserted?.Invoke(null, entry);
|
Inserted?.Invoke(null, entry);
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
@ -102,7 +120,7 @@ namespace LibationFileManager
|
|||||||
private static void save()
|
private static void save()
|
||||||
{
|
{
|
||||||
// create json if not exists
|
// create json if not exists
|
||||||
static void resave() => File.WriteAllText(jsonFile, JsonConvert.SerializeObject(cache.ToList(), Formatting.Indented));
|
static void resave() => File.WriteAllText(jsonFileV2, JsonConvert.SerializeObject(Cache, Formatting.Indented));
|
||||||
|
|
||||||
lock (locker)
|
lock (locker)
|
||||||
{
|
{
|
||||||
@ -112,11 +130,41 @@ namespace LibationFileManager
|
|||||||
try { resave(); }
|
try { resave(); }
|
||||||
catch (IOException ex)
|
catch (IOException ex)
|
||||||
{
|
{
|
||||||
Serilog.Log.Logger.Error(ex, $"Error saving {FILENAME}");
|
Serilog.Log.Logger.Error(ex, $"Error saving {FILENAME_V2}");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class FileCacheV2<TEntry>
|
||||||
|
{
|
||||||
|
[JsonProperty]
|
||||||
|
private readonly ConcurrentDictionary<string, List<TEntry>> Dictionary = new();
|
||||||
|
|
||||||
|
public List<TEntry> GetIdEntries(string id)
|
||||||
|
{
|
||||||
|
static List<TEntry> empty() => new();
|
||||||
|
|
||||||
|
return Dictionary.TryGetValue(id, out var entries) ? entries.ToList() : empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(string id, TEntry entry)
|
||||||
|
{
|
||||||
|
Dictionary.AddOrUpdate(id, [entry], (id, entries) => { entries.Add(entry); return entries; });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRange(string id, IEnumerable<TEntry> entries)
|
||||||
|
{
|
||||||
|
Dictionary.AddOrUpdate(id, entries.ToList(), (id, entries) =>
|
||||||
|
{
|
||||||
|
entries.AddRange(entries);
|
||||||
|
return entries;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(string id, TEntry entry)
|
||||||
|
=> Dictionary.TryGetValue(id, out List<TEntry>? entries) && entries.Remove(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ namespace LibationFileManager
|
|||||||
{
|
{
|
||||||
private static Dictionary<string, FileType> dic => new()
|
private static Dictionary<string, FileType> dic => new()
|
||||||
{
|
{
|
||||||
|
["aax"] = FileType.AAXC,
|
||||||
["aaxc"] = FileType.AAXC,
|
["aaxc"] = FileType.AAXC,
|
||||||
["cue"] = FileType.Cue,
|
["cue"] = FileType.Cue,
|
||||||
["pdf"] = FileType.PDF,
|
["pdf"] = FileType.PDF,
|
||||||
|
|||||||
@ -169,6 +169,16 @@ namespace LibationUiBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public T FirstOrDefault(Func<T, bool> predicate)
|
||||||
|
{
|
||||||
|
lock (lockObject)
|
||||||
|
{
|
||||||
|
return Current != null && predicate(Current) ? Current
|
||||||
|
: _completed.FirstOrDefault(predicate) is T completed ? completed
|
||||||
|
: _queued.FirstOrDefault(predicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void MoveQueuePosition(T item, QueuePosition requestedPosition)
|
public void MoveQueuePosition(T item, QueuePosition requestedPosition)
|
||||||
{
|
{
|
||||||
lock (lockObject)
|
lock (lockObject)
|
||||||
|
|||||||
@ -37,6 +37,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
inProgressSelectControl.SetDirectoryItems(new()
|
inProgressSelectControl.SetDirectoryItems(new()
|
||||||
{
|
{
|
||||||
Configuration.KnownDirectories.WinTemp,
|
Configuration.KnownDirectories.WinTemp,
|
||||||
|
Configuration.KnownDirectories.ApplicationData,
|
||||||
Configuration.KnownDirectories.UserProfile,
|
Configuration.KnownDirectories.UserProfile,
|
||||||
Configuration.KnownDirectories.AppDir,
|
Configuration.KnownDirectories.AppDir,
|
||||||
Configuration.KnownDirectories.MyDocs,
|
Configuration.KnownDirectories.MyDocs,
|
||||||
|
|||||||
@ -44,7 +44,8 @@ namespace LibationWinForms.Dialogs
|
|||||||
{
|
{
|
||||||
Configuration.KnownDirectories.UserProfile,
|
Configuration.KnownDirectories.UserProfile,
|
||||||
Configuration.KnownDirectories.AppDir,
|
Configuration.KnownDirectories.AppDir,
|
||||||
Configuration.KnownDirectories.MyDocs
|
Configuration.KnownDirectories.MyDocs,
|
||||||
|
Configuration.KnownDirectories.MyMusic,
|
||||||
},
|
},
|
||||||
Configuration.KnownDirectories.UserProfile,
|
Configuration.KnownDirectories.UserProfile,
|
||||||
"Books");
|
"Books");
|
||||||
|
|||||||
@ -105,13 +105,14 @@ namespace LibationWinForms
|
|||||||
splitContainer1.Panel2Collapsed = false;
|
splitContainer1.Panel2Collapsed = false;
|
||||||
processBookQueue1.popoutBtn.Visible = true;
|
processBookQueue1.popoutBtn.Visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Configuration.Instance.SetNonString(splitContainer1.Panel2Collapsed, nameof(splitContainer1.Panel2Collapsed));
|
||||||
toggleQueueHideBtn.Text = splitContainer1.Panel2Collapsed ? "❰❰❰" : "❱❱❱";
|
toggleQueueHideBtn.Text = splitContainer1.Panel2Collapsed ? "❰❰❰" : "❱❱❱";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToggleQueueHideBtn_Click(object sender, EventArgs e)
|
private void ToggleQueueHideBtn_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
SetQueueCollapseState(!splitContainer1.Panel2Collapsed);
|
SetQueueCollapseState(!splitContainer1.Panel2Collapsed);
|
||||||
Configuration.Instance.SetNonString(splitContainer1.Panel2Collapsed, nameof(splitContainer1.Panel2Collapsed));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessBookQueue1_PopOut(object sender, EventArgs e)
|
private void ProcessBookQueue1_PopOut(object sender, EventArgs e)
|
||||||
|
|||||||
@ -17,7 +17,7 @@ namespace LibationWinForms
|
|||||||
//Set this size before restoring form size and position
|
//Set this size before restoring form size and position
|
||||||
splitContainer1.Panel2MinSize = this.DpiScale(350);
|
splitContainer1.Panel2MinSize = this.DpiScale(350);
|
||||||
this.RestoreSizeAndLocation(Configuration.Instance);
|
this.RestoreSizeAndLocation(Configuration.Instance);
|
||||||
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
FormClosing += Form1_FormClosing;
|
||||||
|
|
||||||
// this looks like a perfect opportunity to refactor per below.
|
// this looks like a perfect opportunity to refactor per below.
|
||||||
// since this loses design-time tooling and internal access, for now I'm opting for partial classes
|
// since this loses design-time tooling and internal access, for now I'm opting for partial classes
|
||||||
@ -58,6 +58,14 @@ namespace LibationWinForms
|
|||||||
Shown += Form1_Shown;
|
Shown += Form1_Shown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
|
||||||
|
{
|
||||||
|
//Always close the queue before saving the form to prevent
|
||||||
|
//Form1 from getting excessively wide when it's restored.
|
||||||
|
SetQueueCollapseState(true);
|
||||||
|
this.SaveSizeAndLocation(Configuration.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
private async void Form1_Shown(object sender, EventArgs e)
|
private async void Form1_Shown(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (Configuration.Instance.FirstLaunch)
|
if (Configuration.Instance.FirstLaunch)
|
||||||
|
|||||||
@ -82,7 +82,15 @@ namespace LibationWinForms.ProcessQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool isBookInQueue(DataLayer.LibraryBook libraryBook)
|
private bool isBookInQueue(DataLayer.LibraryBook libraryBook)
|
||||||
=> Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
|
{
|
||||||
|
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
|
||||||
|
if (entry == null)
|
||||||
|
return false;
|
||||||
|
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
|
||||||
|
return !Queue.RemoveCompleted(entry);
|
||||||
|
else
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void AddDownloadPdf(DataLayer.LibraryBook libraryBook)
|
public void AddDownloadPdf(DataLayer.LibraryBook libraryBook)
|
||||||
=> AddDownloadPdf(new List<DataLayer.LibraryBook>() { libraryBook });
|
=> AddDownloadPdf(new List<DataLayer.LibraryBook>() { libraryBook });
|
||||||
|
|||||||
@ -98,7 +98,7 @@ namespace LibationWinForms
|
|||||||
if (config.LibationSettingsAreValid)
|
if (config.LibationSettingsAreValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var defaultLibationFilesDir = Configuration.UserProfile;
|
var defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory;
|
||||||
|
|
||||||
// check for existing settings in default location
|
// check for existing settings in default location
|
||||||
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
||||||
@ -154,7 +154,7 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
// INIT DEFAULT SETTINGS
|
// INIT DEFAULT SETTINGS
|
||||||
// if 'new user' was clicked, or if 'returning user' chose new install: show basic settings dialog
|
// if 'new user' was clicked, or if 'returning user' chose new install: show basic settings dialog
|
||||||
config.Books ??= Path.Combine(defaultLibationFilesDir, "Books");
|
config.Books ??= Configuration.DefaultBooksDirectory;
|
||||||
|
|
||||||
if (config.LibationSettingsAreValid)
|
if (config.LibationSettingsAreValid)
|
||||||
return;
|
return;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user