diff --git a/Source/AaxDecrypter/AudiobookDownloadBase.cs b/Source/AaxDecrypter/AudiobookDownloadBase.cs
index d6529b39..89a94f2f 100644
--- a/Source/AaxDecrypter/AudiobookDownloadBase.cs
+++ b/Source/AaxDecrypter/AudiobookDownloadBase.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Dinah.Core;
@@ -52,7 +51,7 @@ namespace AaxDecrypter
// delete file after validation is complete
FileUtility.SaferDelete(OutputFileName);
- }
+ }
public abstract Task CancelAsync();
@@ -72,7 +71,7 @@ namespace AaxDecrypter
=> RetrievedNarrators?.Invoke(this, narrators);
protected void OnRetrievedCoverArt(byte[] coverArt)
=> RetrievedCoverArt?.Invoke(this, coverArt);
- protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
+ protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
=> DecryptProgressUpdate?.Invoke(this, downloadProgress);
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
=> DecryptTimeRemaining?.Invoke(this, timeRemaining);
@@ -111,8 +110,8 @@ namespace AaxDecrypter
{
FileUtility.SaferDelete(jsonDownloadState);
- if (DownloadOptions.AudibleKey is not null &&
- DownloadOptions.AudibleIV is not null &&
+ if (DownloadOptions.AudibleKey is not null &&
+ DownloadOptions.AudibleIV is not null &&
DownloadOptions.RetainEncryptedFile)
{
string aaxPath = Path.ChangeExtension(TempFilePath, ".aax");
@@ -156,12 +155,7 @@ namespace AaxDecrypter
private NetworkFileStreamPersister NewNetworkFilePersister()
{
- var headers = new System.Net.WebHeaderCollection
- {
- { "User-Agent", DownloadOptions.UserAgent }
- };
-
- var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, headers);
+ var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, new() { { "User-Agent", DownloadOptions.UserAgent } });
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
}
}
diff --git a/Source/AaxDecrypter/NetworkFileStream.cs b/Source/AaxDecrypter/NetworkFileStream.cs
index d03e44f8..2bd4863b 100644
--- a/Source/AaxDecrypter/NetworkFileStream.cs
+++ b/Source/AaxDecrypter/NetworkFileStream.cs
@@ -1,91 +1,53 @@
using Dinah.Core;
using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
+using System.Net.Http;
using System.Threading;
+using System.Threading.Tasks;
namespace AaxDecrypter
{
- ///
- /// A for a single Uri.
- ///
- public class SingleUriCookieContainer : CookieContainer
- {
- private Uri baseAddress;
- public Uri Uri
- {
- get => baseAddress;
- set
- {
- baseAddress = new UriBuilder(value.Scheme, value.Host).Uri;
- }
- }
-
- public CookieCollection GetCookies()
- {
- return GetCookies(Uri);
- }
- }
-
- ///
- /// A resumable, simultaneous file downloader and reader.
- ///
+ /// A resumable, simultaneous file downloader and reader.
public class NetworkFileStream : Stream, IUpdatable
{
public event EventHandler Updated;
#region Public Properties
- ///
- /// Location to save the downloaded data.
- ///
+ /// Location to save the downloaded data.
[JsonProperty(Required = Required.Always)]
public string SaveFilePath { get; }
- ///
- /// Http(s) address of the file to download.
- ///
+ /// Http(s) address of the file to download.
[JsonProperty(Required = Required.Always)]
public Uri Uri { get; private set; }
- ///
- /// All cookies set by caller or by the remote server.
- ///
+ /// Http headers to be sent to the server with the request.
[JsonProperty(Required = Required.Always)]
- public SingleUriCookieContainer CookieContainer { get; }
+ public Dictionary RequestHeaders { get; private set; }
- ///
- /// Http headers to be sent to the server with the request.
- ///
- [JsonProperty(Required = Required.Always)]
- public WebHeaderCollection RequestHeaders { get; private set; }
-
- ///
- /// The position in that has been written and flushed to disk.
- ///
+ /// The position in that has been written and flushed to disk.
[JsonProperty(Required = Required.Always)]
public long WritePosition { get; private set; }
- ///
- /// The total length of the file to download.
- ///
+ /// The total length of the file to download.
[JsonProperty(Required = Required.Always)]
public long ContentLength { get; private set; }
+ [JsonIgnore]
+ public bool IsCancelled { get; private set; }
+
#endregion
#region Private Properties
- private HttpWebRequest HttpRequest { get; set; }
private FileStream _writeFile { get; }
private FileStream _readFile { get; }
- private Stream _networkStream { get; set; }
- private bool hasBegunDownloading { get; set; }
- public bool IsCancelled { get; private set; }
- private EventWaitHandle downloadEnded { get; set; }
- private EventWaitHandle downloadedPiece { get; set; }
+ private EventWaitHandle _downloadedPiece { get; set; }
+ private Task _backgroundDownloadTask { get; set; }
#endregion
@@ -102,15 +64,12 @@ namespace AaxDecrypter
#region Constructor
- ///
- /// A resumable, simultaneous file downloader and reader.
- ///
+ /// A resumable, simultaneous file downloader and reader.
/// Path to a location on disk to save the downloaded data from
/// Http(s) address of the file to download.
/// The position in to begin downloading.
/// Http headers to be sent to the server with the .
- /// A with cookies to send with the . It will also be populated with any cookies set by the server.
- public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null)
+ public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, Dictionary requestHeaders = null)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri));
@@ -122,8 +81,7 @@ namespace AaxDecrypter
SaveFilePath = saveFilePath;
Uri = uri;
WritePosition = writePosition;
- RequestHeaders = requestHeaders ?? new WebHeaderCollection();
- CookieContainer = cookies ?? new SingleUriCookieContainer { Uri = uri };
+ RequestHeaders = requestHeaders ?? new();
_writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
{
@@ -139,12 +97,10 @@ namespace AaxDecrypter
#region Downloader
- ///
- /// Update the .
- ///
+ /// Update the .
private void Update()
{
- RequestHeaders = HttpRequest.Headers;
+ RequestHeaders["Range"] = $"bytes={WritePosition}-";
try
{
Updated?.Invoke(this, EventArgs.Empty);
@@ -155,9 +111,7 @@ namespace AaxDecrypter
}
}
- ///
- /// Set a different to the same file targeted by this instance of
- ///
+ /// Set a different to the same file targeted by this instance of
/// New host must match existing host.
public void SetUriForSameFile(Uri uriToSameFile)
{
@@ -165,37 +119,31 @@ namespace AaxDecrypter
if (uriToSameFile.Host != Uri.Host)
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
- if (hasBegunDownloading)
+ if (_backgroundDownloadTask is not null)
throw new InvalidOperationException("Cannot change Uri after download has started.");
Uri = uriToSameFile;
- HttpRequest = WebRequest.CreateHttp(Uri);
-
- HttpRequest.CookieContainer = CookieContainer;
- HttpRequest.Headers = RequestHeaders;
- //If NetworkFileStream is resuming, Header will already contain a range.
- HttpRequest.Headers.Remove("Range");
- HttpRequest.AddRange(WritePosition);
+ RequestHeaders["Range"] = $"bytes={WritePosition}-";
}
- ///
- /// Begins downloading to in a background thread.
- ///
- private void BeginDownloading()
- {
- downloadEnded = new EventWaitHandle(false, EventResetMode.ManualReset);
+ /// Begins downloading to in a background thread.
+ /// The downloader
+ private Task BeginDownloading()
+ {
if (ContentLength != 0 && WritePosition == ContentLength)
- {
- hasBegunDownloading = true;
- downloadEnded.Set();
- return;
- }
+ return Task.CompletedTask;
if (ContentLength != 0 && WritePosition > ContentLength)
throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
- var response = HttpRequest.GetResponse() as HttpWebResponse;
+
+ var request = new HttpRequestMessage(HttpMethod.Get, Uri);
+
+ foreach (var header in RequestHeaders)
+ request.Headers.Add(header.Key, header.Value);
+
+ var response = new HttpClient().Send(request, HttpCompletionOption.ResponseHeadersRead);
if (response.StatusCode != HttpStatusCode.PartialContent)
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
@@ -203,24 +151,17 @@ namespace AaxDecrypter
//Content length is the length of the range request, and it is only equal
//to the complete file length if requesting Range: bytes=0-
if (WritePosition == 0)
- ContentLength = response.ContentLength;
+ ContentLength = response.Content.Headers.ContentLength.GetValueOrDefault();
- _networkStream = response.GetResponseStream();
- downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
+ var networkStream = response.Content.ReadAsStream();
+ _downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Download the file in the background.
- new Thread(() => DownloadFile())
- { IsBackground = true }
- .Start();
-
- hasBegunDownloading = true;
- return;
+ return Task.Run(() => DownloadFile(networkStream));
}
- ///
- /// Download to .
- ///
- private void DownloadFile()
+ /// Download to .
+ private void DownloadFile(Stream networkStream)
{
var downloadPosition = WritePosition;
var nextFlush = downloadPosition + DATA_FLUSH_SZ;
@@ -231,7 +172,7 @@ namespace AaxDecrypter
int bytesRead;
do
{
- bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
+ bytesRead = networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
_writeFile.Write(buff, 0, bytesRead);
downloadPosition += bytesRead;
@@ -242,15 +183,12 @@ namespace AaxDecrypter
WritePosition = downloadPosition;
Update();
nextFlush = downloadPosition + DATA_FLUSH_SZ;
- downloadedPiece.Set();
+ _downloadedPiece.Set();
}
} while (downloadPosition < ContentLength && !IsCancelled && bytesRead > 0);
- _writeFile.Close();
- _networkStream.Close();
WritePosition = downloadPosition;
- Update();
if (!IsCancelled && WritePosition < ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
@@ -264,8 +202,10 @@ namespace AaxDecrypter
}
finally
{
- downloadedPiece.Set();
- downloadEnded.Set();
+ networkStream.Close();
+ _writeFile.Close();
+ _downloadedPiece.Set();
+ Update();
}
}
@@ -274,96 +214,7 @@ namespace AaxDecrypter
#region Json Connverters
public static JsonSerializerSettings GetJsonSerializerSettings()
- {
- var settings = new JsonSerializerSettings();
- settings.Converters.Add(new CookieContainerConverter());
- settings.Converters.Add(new WebHeaderCollectionConverter());
- return settings;
- }
-
- internal class CookieContainerConverter : JsonConverter
- {
- public override bool CanConvert(Type objectType)
- => objectType == typeof(SingleUriCookieContainer);
-
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
- {
- var jObj = JObject.Load(reader);
-
- var result = new SingleUriCookieContainer()
- {
- Uri = new Uri(jObj["Uri"].Value()),
- Capacity = jObj["Capacity"].Value(),
- MaxCookieSize = jObj["MaxCookieSize"].Value(),
- PerDomainCapacity = jObj["PerDomainCapacity"].Value()
- };
-
- var cookieList = jObj["Cookies"].ToList();
-
- foreach (var cookie in cookieList)
- {
- result.Add(
- new Cookie
- {
- Comment = cookie["Comment"].Value(),
- HttpOnly = cookie["HttpOnly"].Value(),
- Discard = cookie["Discard"].Value(),
- Domain = cookie["Domain"].Value(),
- Expired = cookie["Expired"].Value(),
- Expires = cookie["Expires"].Value(),
- Name = cookie["Name"].Value(),
- Path = cookie["Path"].Value(),
- Port = cookie["Port"].Value(),
- Secure = cookie["Secure"].Value(),
- Value = cookie["Value"].Value(),
- Version = cookie["Version"].Value(),
- });
- }
-
- return result;
- }
-
- public override bool CanWrite => true;
-
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
- {
- var cookies = value as SingleUriCookieContainer;
- var obj = (JObject)JToken.FromObject(value);
- var container = cookies.GetCookies();
- var propertyNames = container.Select(c => JToken.FromObject(c));
- obj.AddFirst(new JProperty("Cookies", new JArray(propertyNames)));
- obj.WriteTo(writer);
- }
- }
-
- internal class WebHeaderCollectionConverter : JsonConverter
- {
- public override bool CanConvert(Type objectType)
- => objectType == typeof(WebHeaderCollection);
-
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
- {
- var jObj = JObject.Load(reader);
- var result = new WebHeaderCollection();
-
- foreach (var kvp in jObj)
- result.Add(kvp.Key, kvp.Value.Value());
-
- return result;
- }
-
- public override bool CanWrite => true;
-
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
- {
- var jObj = new JObject();
- var type = value.GetType();
- var headers = value as WebHeaderCollection;
- var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k]));
- jObj.Add(jHeaders);
- jObj.WriteTo(writer);
- }
- }
+ => new JsonSerializerSettings();
#endregion
@@ -383,8 +234,7 @@ namespace AaxDecrypter
{
get
{
- if (!hasBegunDownloading)
- BeginDownloading();
+ _backgroundDownloadTask ??= BeginDownloading();
return ContentLength;
}
}
@@ -401,15 +251,14 @@ namespace AaxDecrypter
[JsonIgnore]
public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; }
- public override void Flush() => throw new NotImplementedException();
- public override void SetLength(long value) => throw new NotImplementedException();
- public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
+ public override void Flush() => throw new InvalidOperationException();
+ public override void SetLength(long value) => throw new InvalidOperationException();
+ public override void Write(byte[] buffer, int offset, int count) => throw new InvalidOperationException();
public override int Read(byte[] buffer, int offset, int count)
{
- if (!hasBegunDownloading)
- BeginDownloading();
-
+ _backgroundDownloadTask ??= BeginDownloading();
+
var toRead = Math.Min(count, Length - Position);
WaitToPosition(Position + toRead);
return _readFile.Read(buffer, offset, count);
@@ -428,38 +277,32 @@ namespace AaxDecrypter
return _readFile.Position = newPosition;
}
- ///
- /// Blocks until the file has downloaded to at least , then returns.
- ///
+ /// Blocks until the file has downloaded to at least , then returns.
/// The minimum required flished data length in .
private void WaitToPosition(long requiredPosition)
{
while (WritePosition < requiredPosition
- && hasBegunDownloading
- && !IsCancelled
- && !downloadEnded.WaitOne(0))
+ && _backgroundDownloadTask?.IsCompleted is false
+ && !IsCancelled)
{
- downloadedPiece.WaitOne(100);
+ _downloadedPiece.WaitOne(100);
}
}
public override void Close()
{
IsCancelled = true;
-
- while (downloadEnded is not null && !downloadEnded.WaitOne(100)) ;
+ _backgroundDownloadTask?.Wait();
_readFile.Close();
_writeFile.Close();
- _networkStream?.Close();
Update();
}
#endregion
~NetworkFileStream()
{
- downloadEnded?.Close();
- downloadedPiece?.Close();
+ _downloadedPiece?.Close();
}
}
}
diff --git a/Source/HangoverAvalonia/Controls/CheckedListBox.axaml b/Source/HangoverAvalonia/Controls/CheckedListBox.axaml
index f797e06b..df1c6319 100644
--- a/Source/HangoverAvalonia/Controls/CheckedListBox.axaml
+++ b/Source/HangoverAvalonia/Controls/CheckedListBox.axaml
@@ -8,7 +8,7 @@
-
+
diff --git a/Source/HangoverAvalonia/HangoverAvalonia.csproj b/Source/HangoverAvalonia/HangoverAvalonia.csproj
index 40795aff..c00bc51c 100644
--- a/Source/HangoverAvalonia/HangoverAvalonia.csproj
+++ b/Source/HangoverAvalonia/HangoverAvalonia.csproj
@@ -55,6 +55,9 @@
MainVM.cs
+
+ MainWindow.axaml
+
diff --git a/Source/LibationAvalonia/AvaloniaUtils.cs b/Source/LibationAvalonia/AvaloniaUtils.cs
index 66b4935d..8d9fd607 100644
--- a/Source/LibationAvalonia/AvaloniaUtils.cs
+++ b/Source/LibationAvalonia/AvaloniaUtils.cs
@@ -18,22 +18,6 @@ namespace LibationAvalonia
return defaultBrush;
}
- public static Window GetParentWindow(this IControl control)
- {
- Window window = null;
-
- var p = control.Parent;
- while (p != null)
- {
- if (p is Window)
- {
- window = (Window)p;
- break;
- }
- p = p.Parent;
- }
-
- return window;
- }
+ public static Window GetParentWindow(this IControl control) => control.VisualRoot as Window;
}
}
diff --git a/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs b/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs
index 02b85fe6..658e0e0b 100644
--- a/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs
+++ b/Source/LibationAvalonia/Controls/DirectoryOrCustomSelectControl.axaml.cs
@@ -5,6 +5,7 @@ using Dinah.Core;
using LibationFileManager;
using System.Collections.Generic;
using ReactiveUI;
+using System.Linq;
namespace LibationAvalonia.Controls
{
@@ -16,7 +17,6 @@ namespace LibationAvalonia.Controls
public static readonly StyledProperty SubDirectoryProperty =
AvaloniaProperty.Register(nameof(SubDirectory));
-
public static readonly StyledProperty DirectoryProperty =
AvaloniaProperty.Register(nameof(Directory));
@@ -90,8 +90,19 @@ namespace LibationAvalonia.Controls
private async void CustomDirBrowseBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
- OpenFolderDialog ofd = new();
- customStates.CustomDir = await ofd.ShowAsync(VisualRoot as Window);
+ var options = new Avalonia.Platform.Storage.FolderPickerOpenOptions
+ {
+ AllowMultiple = false
+ };
+
+ var selectedFolders = await (VisualRoot as Window).StorageProvider.OpenFolderPickerAsync(options);
+
+ customStates.CustomDir =
+ selectedFolders
+ .SingleOrDefault()?.
+ TryGetUri(out var uri) is true
+ ? uri.LocalPath
+ : customStates.CustomDir;
}
private void CheckStates_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
@@ -125,7 +136,6 @@ namespace LibationAvalonia.Controls
Directory = customStates.CustomChecked ? selectedDir : System.IO.Path.Combine(selectedDir, SubDirectory);
}
-
private void DirectoryOrCustomSelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property.Name == nameof(Directory) && e.OldValue is null)
diff --git a/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml.cs
index 8020436c..eab179ea 100644
--- a/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml.cs
@@ -9,6 +9,9 @@ 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
{
@@ -110,24 +113,29 @@ namespace LibationAvalonia.Dialogs
public async void ImportButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
-
- OpenFileDialog ofd = new();
- ofd.Filters.Add(new() { Name = "JSON File", Extensions = new() { "json" } });
- ofd.Directory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
- ofd.AllowMultiple = false;
+ var openFileDialogOptions = new FilePickerOpenOptions
+ {
+ Title = $"Select the audible-cli [account].json file",
+ AllowMultiple = false,
+ SuggestedStartLocation = new BclStorageFolder(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)),
+ FileTypeFilter = new FilePickerFileType[]
+ {
+ new("JSON files (*.json)") { Patterns = new[] { "json" } },
+ }
+ };
string audibleAppDataDir = GetAudibleCliAppDataPath();
-
if (Directory.Exists(audibleAppDataDir))
- ofd.Directory = audibleAppDataDir;
+ openFileDialogOptions.SuggestedStartLocation = new BclStorageFolder(audibleAppDataDir);
- var filePath = await ofd.ShowAsync(this);
+ var selectedFiles = await StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
+ var selectedFile = selectedFiles.SingleOrDefault();
- if (filePath is null || filePath.Length == 0) return;
+ if (!selectedFile.TryGetUri(out var uri)) return;
try
{
- var jsonText = File.ReadAllText(filePath[0]);
+ var jsonText = File.ReadAllText(uri.LocalPath);
var mkbAuth = Mkb79Auth.FromJson(jsonText);
var account = await mkbAuth.ToAccountAsync();
@@ -148,7 +156,7 @@ namespace LibationAvalonia.Dialogs
{
await MessageBox.ShowAdminAlert(
this,
- $"An error occurred while importing an account from:\r\n{filePath[0]}\r\n\r\nIs the file encrypted?",
+ $"An error occurred while importing an account from:\r\n{uri.LocalPath}\r\n\r\nIs the file encrypted?",
"Error Importing Account",
ex);
}
@@ -263,26 +271,36 @@ namespace LibationAvalonia.Dialogs
return;
}
- SaveFileDialog sfd = new();
- sfd.Filters.Add(new() { Name = "JSON File", Extensions = new() { "json" } });
+ var options = new FilePickerSaveOptions
+ {
+ Title = $"Save Sover Image",
+ SuggestedStartLocation = new BclStorageFolder(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)),
+ SuggestedFileName = $"{acc.AccountId}.json",
+ DefaultExtension = "json",
+ ShowOverwritePrompt = true,
+ FileTypeChoices = new FilePickerFileType[]
+ {
+ new("JSON files (*.json)") { Patterns = new[] { "json" } },
+ }
+ };
string audibleAppDataDir = GetAudibleCliAppDataPath();
if (Directory.Exists(audibleAppDataDir))
- sfd.Directory = audibleAppDataDir;
+ options.SuggestedStartLocation = new BclStorageFolder(audibleAppDataDir);
- string fileName = await sfd.ShowAsync(this);
- if (fileName is null)
- return;
+ var selectedFile = await StorageProvider.SaveFilePickerAsync(options);
+
+ if (!selectedFile.TryGetUri(out var uri)) return;
try
{
var mkbAuth = Mkb79Auth.FromAccount(account);
var jsonText = mkbAuth.ToJson();
- File.WriteAllText(fileName, jsonText);
+ File.WriteAllText(uri.LocalPath, jsonText);
- await MessageBox.Show(this, $"Successfully exported {account.AccountName} to\r\n\r\n{fileName}", "Success!");
+ await MessageBox.Show(this, $"Successfully exported {account.AccountName} to\r\n\r\n{uri.LocalPath}", "Success!");
}
catch (Exception ex)
{
diff --git a/Source/LibationAvalonia/Dialogs/ImageDisplayDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/ImageDisplayDialog.axaml.cs
index 992ae7a3..5d6f444e 100644
--- a/Source/LibationAvalonia/Dialogs/ImageDisplayDialog.axaml.cs
+++ b/Source/LibationAvalonia/Dialogs/ImageDisplayDialog.axaml.cs
@@ -7,6 +7,7 @@ using System;
using System.ComponentModel;
using System.IO;
using ReactiveUI;
+using Avalonia.Platform.Storage;
namespace LibationAvalonia.Dialogs
{
@@ -46,27 +47,30 @@ namespace LibationAvalonia.Dialogs
public async void SaveImage_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
+ var options = new FilePickerSaveOptions
+ {
+ Title = $"Save Sover Image",
+ SuggestedStartLocation = new Avalonia.Platform.Storage.FileIO.BclStorageFolder(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)),
+ SuggestedFileName = $"{PictureFileName}.jpg",
+ DefaultExtension = "jpg",
+ ShowOverwritePrompt = true,
+ FileTypeChoices = new FilePickerFileType[]
+ {
+ new("Jpeg (*.jpg)") { Patterns = new[] { "jpg" } }
+ }
+ };
- SaveFileDialog saveFileDialog = new();
- saveFileDialog.Filters.Add(new FileDialogFilter { Name = "Jpeg", Extensions = new System.Collections.Generic.List() { "jpg" } });
- saveFileDialog.InitialFileName = PictureFileName;
- saveFileDialog.Directory
- = !LibationFileManager.Configuration.IsWindows ? null
- : Directory.Exists(BookSaveDirectory) ? BookSaveDirectory
- : Path.GetDirectoryName(BookSaveDirectory);
+ var selectedFile = await StorageProvider.SaveFilePickerAsync(options);
- var fileName = await saveFileDialog.ShowAsync(this);
-
- if (fileName is null)
- return;
+ if (!selectedFile.TryGetUri(out var uri)) return;
try
{
- File.WriteAllBytes(fileName, CoverBytes);
+ File.WriteAllBytes(uri.LocalPath, CoverBytes);
}
catch (Exception ex)
{
- Serilog.Log.Logger.Error(ex, $"Failed to save picture to {fileName}");
+ Serilog.Log.Logger.Error(ex, $"Failed to save picture to {uri.LocalPath}");
await MessageBox.Show(this, $"An error was encountered while trying to save the picture\r\n\r\n{ex.Message}", "Failed to save picture", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
}
}
diff --git a/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml b/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml
index 0224c16c..b7c76037 100644
--- a/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml
+++ b/Source/LibationAvalonia/Dialogs/SettingsDialog.axaml
@@ -351,21 +351,27 @@
-
-
+
+
-
+
+
KnownDirectories { get; } = new()
+ {
+ Configuration.KnownDirectories.WinTemp,
+ Configuration.KnownDirectories.UserProfile,
+ Configuration.KnownDirectories.AppDir,
+ Configuration.KnownDirectories.MyDocs,
+ Configuration.KnownDirectories.LibationFiles
+ };
+
+ public string InProgressDirectory { get; set; }
public void LoadSettings(Configuration config)
{
BadBookAsk = config.BadBook is Configuration.BadBookAction.Ask;
@@ -252,9 +261,7 @@ namespace LibationAvalonia.Dialogs
FolderTemplate = config.FolderTemplate;
FileTemplate = config.FileTemplate;
ChapterFileTemplate = config.ChapterFileTemplate;
- InProgressDirectory
- = config.InProgress == Configuration.AppDir_Absolute ? Configuration.KnownDirectories.AppDir
- : Configuration.GetKnownDirectory(config.InProgress);
+ InProgressDirectory = config.InProgress;
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
}
@@ -289,9 +296,7 @@ namespace LibationAvalonia.Dialogs
config.FolderTemplate = FolderTemplate;
config.FileTemplate = FileTemplate;
config.ChapterFileTemplate = ChapterFileTemplate;
- config.InProgress
- = InProgressDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute
- : Configuration.GetKnownDirectoryPath(InProgressDirectory);
+ config.InProgress = InProgressDirectory;
config.UseCoverAsFolderIcon = UseCoverAsFolderIcon;
diff --git a/Source/LibationAvalonia/LibationAvalonia.csproj b/Source/LibationAvalonia/LibationAvalonia.csproj
index b31929d4..7059f5fa 100644
--- a/Source/LibationAvalonia/LibationAvalonia.csproj
+++ b/Source/LibationAvalonia/LibationAvalonia.csproj
@@ -18,95 +18,98 @@
-
-
- en;es
-
+ en;es
+
-
- ..\bin\Avalonia\Debug
- embedded
-
+
+ ..\bin\Avalonia\Debug
+ embedded
+
-
- ..\bin\Avalonia\Release
- embedded
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ ..\bin\Avalonia\Release
+ embedded
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- True
- True
- Resources.resx
-
-
+
+
+
+
+
-
-
- ResXFileCodeGenerator
- Resources.Designer.cs
-
-
+
+
+ True
+ True
+ Resources.resx
+
+
+ MainWindow.axaml
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+
-
-
-
-
@@ -120,16 +123,16 @@
-
- Always
-
-
- Always
-
-
- Always
-
-
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.BackupCounts.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.BackupCounts.cs
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow.BackupCounts.axaml.cs
rename to Source/LibationAvalonia/Views/MainWindow.BackupCounts.cs
diff --git a/Source/LibationAvalonia/Views/MainWindow.Export.cs b/Source/LibationAvalonia/Views/MainWindow.Export.cs
new file mode 100644
index 00000000..f5d903be
--- /dev/null
+++ b/Source/LibationAvalonia/Views/MainWindow.Export.cs
@@ -0,0 +1,62 @@
+using ApplicationServices;
+using Avalonia.Controls;
+using Avalonia.Platform.Storage;
+using LibationFileManager;
+using System;
+using System.Linq;
+
+namespace LibationAvalonia.Views
+{
+ //DONE
+ public partial class MainWindow
+ {
+ private void Configure_Export() { }
+
+ public async void exportLibraryToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ try
+ {
+ var options = new FilePickerSaveOptions
+ {
+ Title = "Where to export Library",
+ SuggestedStartLocation = new Avalonia.Platform.Storage.FileIO.BclStorageFolder(Configuration.Instance.Books),
+ SuggestedFileName = $"Libation Library Export {DateTime.Now:yyyy-MM-dd}.xlsx",
+ DefaultExtension = "xlsx",
+ ShowOverwritePrompt = true,
+ FileTypeChoices = new FilePickerFileType[]
+ {
+ new("Excel Workbook (*.xlsx)") { Patterns = new[] { "xlsx" } },
+ new("CSV files (*.csv)") { Patterns = new[] { "csv" } },
+ new("JSON files (*.json)") { Patterns = new[] { "json" } },
+ new("All files (*.*)") { Patterns = new[] { "*" } },
+ }
+ };
+
+ var selectedFile = await StorageProvider.SaveFilePickerAsync(options);
+
+ if (!selectedFile.TryGetUri(out var uri)) return;
+
+ var ext = System.IO.Path.GetExtension(uri.LocalPath);
+ switch (ext)
+ {
+ case "xlsx": // xlsx
+ default:
+ LibraryExporter.ToXlsx(uri.LocalPath);
+ break;
+ case "csv": // csv
+ LibraryExporter.ToCsv(uri.LocalPath);
+ break;
+ case "json": // json
+ LibraryExporter.ToJson(uri.LocalPath);
+ break;
+ }
+
+ await MessageBox.Show("Library exported to:\r\n" + uri.LocalPath, "Library Exported");
+ }
+ catch (Exception ex)
+ {
+ await MessageBox.ShowAdminAlert(this, "Error attempting to export your library.", "Error exporting", ex);
+ }
+ }
+ }
+}
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.Filter.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.Filter.cs
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow.Filter.axaml.cs
rename to Source/LibationAvalonia/Views/MainWindow.Filter.cs
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.Liberate.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.Liberate.cs
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow.Liberate.axaml.cs
rename to Source/LibationAvalonia/Views/MainWindow.Liberate.cs
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow._NoUI.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.NoUI.cs
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow._NoUI.axaml.cs
rename to Source/LibationAvalonia/Views/MainWindow.NoUI.cs
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.ProcessQueue.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.ProcessQueue.cs
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow.ProcessQueue.axaml.cs
rename to Source/LibationAvalonia/Views/MainWindow.ProcessQueue.cs
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.QuickFilters.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.QuickFilters.cs
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow.QuickFilters.axaml.cs
rename to Source/LibationAvalonia/Views/MainWindow.QuickFilters.cs
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.RemoveBooks.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.RemoveBooks.cs
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow.RemoveBooks.axaml.cs
rename to Source/LibationAvalonia/Views/MainWindow.RemoveBooks.cs
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.ScanAuto.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.ScanAuto.cs
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow.ScanAuto.axaml.cs
rename to Source/LibationAvalonia/Views/MainWindow.ScanAuto.cs
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.ScanManual.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.ScanManual.cs
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow.ScanManual.axaml.cs
rename to Source/LibationAvalonia/Views/MainWindow.ScanManual.cs
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.ScanNotification.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.ScanNotification.cs
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow.ScanNotification.axaml.cs
rename to Source/LibationAvalonia/Views/MainWindow.ScanNotification.cs
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.Settings.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.Settings.cs
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow.Settings.axaml.cs
rename to Source/LibationAvalonia/Views/MainWindow.Settings.cs
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.VisibleBooks.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.VisibleBooks.cs
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow.VisibleBooks.axaml.cs
rename to Source/LibationAvalonia/Views/MainWindow.VisibleBooks.cs
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.axaml b/Source/LibationAvalonia/Views/MainWindow.axaml
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow.axaml
rename to Source/LibationAvalonia/Views/MainWindow.axaml
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.axaml.cs b/Source/LibationAvalonia/Views/MainWindow.axaml.cs
similarity index 100%
rename from Source/LibationAvalonia/Views/MainWindow/MainWindow.axaml.cs
rename to Source/LibationAvalonia/Views/MainWindow.axaml.cs
diff --git a/Source/LibationAvalonia/Views/MainWindow/MainWindow.Export.axaml.cs b/Source/LibationAvalonia/Views/MainWindow/MainWindow.Export.axaml.cs
deleted file mode 100644
index fa39ebc3..00000000
--- a/Source/LibationAvalonia/Views/MainWindow/MainWindow.Export.axaml.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using ApplicationServices;
-using Avalonia.Controls;
-using System;
-using System.Linq;
-
-namespace LibationAvalonia.Views
-{
- //DONE
- public partial class MainWindow
- {
- private void Configure_Export() { }
-
- public async void exportLibraryToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
- {
- try
- {
- var saveFileDialog = new SaveFileDialog
- {
- Title = "Where to export Library",
- };
- saveFileDialog.Filters.Add(new FileDialogFilter { Name = "Excel Workbook (*.xlsx)", Extensions = new() { "xlsx" } });
- saveFileDialog.Filters.Add(new FileDialogFilter { Name = "CSV files (*.csv)", Extensions = new() { "csv" } });
- saveFileDialog.Filters.Add(new FileDialogFilter { Name = "JSON files (*.json)", Extensions = new() { "json" } });
- saveFileDialog.Filters.Add(new FileDialogFilter { Name = "All files (*.*)", Extensions = new() { "*" } });
-
- var fileName = await saveFileDialog.ShowAsync(this);
- if (fileName is null) return;
-
- var ext = System.IO.Path.GetExtension(fileName);
- switch (ext)
- {
- case "xlsx": // xlsx
- default:
- LibraryExporter.ToXlsx(fileName);
- break;
- case "csv": // csv
- LibraryExporter.ToCsv(fileName);
- break;
- case "json": // json
- LibraryExporter.ToJson(fileName);
- break;
- }
-
- await MessageBox.Show("Library exported to:\r\n" + fileName, "Library Exported");
- }
- catch (Exception ex)
- {
- await MessageBox.ShowAdminAlert(this, "Error attempting to export your library.", "Error exporting", ex);
- }
- }
- }
-}
diff --git a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
index 30cff7a5..8c479f94 100644
--- a/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
+++ b/Source/LibationAvalonia/Views/ProductsDisplay.axaml.cs
@@ -6,6 +6,7 @@ using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using Avalonia.Platform.Storage;
using DataLayer;
using FileLiberator;
using LibationAvalonia.Controls;
@@ -42,7 +43,7 @@ namespace LibationAvalonia.Views
};
var pdvm = new ProductsDisplayViewModel();
- pdvm.DisplayBooksAsync(sampleEntries);
+ _ = pdvm.DisplayBooksAsync(sampleEntries);
DataContext = pdvm;
return;
@@ -106,17 +107,22 @@ namespace LibationAvalonia.Views
{
try
{
- var openFileDialog = new OpenFileDialog
+ var openFileDialogOptions = new FilePickerOpenOptions
{
Title = $"Locate the audio file for '{entry.Book.Title}'",
- Filters = new() { new() { Name = "All files (*.*)", Extensions = new() { "*" } } },
- AllowMultiple = false
+ AllowMultiple = false,
+ SuggestedStartLocation = new Avalonia.Platform.Storage.FileIO.BclStorageFolder(Configuration.Instance.Books),
+ FileTypeFilter = new FilePickerFileType[]
+ {
+ new("All files (*.*)") { Patterns = new[] { "*" } },
+ }
};
- var filePaths = await openFileDialog.ShowAsync(this.GetParentWindow());
- var filePath = filePaths.SingleOrDefault();
- if (!string.IsNullOrWhiteSpace(filePath))
- FilePathCache.Insert(entry.AudibleProductId, filePath);
+ var selectedFiles = await this.GetParentWindow().StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
+ var selectedFile = selectedFiles.SingleOrDefault();
+
+ if (selectedFile.TryGetUri(out var uri))
+ FilePathCache.Insert(entry.AudibleProductId, uri.LocalPath);
}
catch (Exception ex)
{
diff --git a/Source/LibationFileManager/AudibleFileStorage.cs b/Source/LibationFileManager/AudibleFileStorage.cs
index ccf00fd6..f2cbda41 100644
--- a/Source/LibationFileManager/AudibleFileStorage.cs
+++ b/Source/LibationFileManager/AudibleFileStorage.cs
@@ -16,7 +16,16 @@ namespace LibationFileManager
public static LongPath DownloadsInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
public static LongPath DecryptInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
- private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
+ static AudibleFileStorage()
+ {
+ //Clean up any partially-decrypted files from previous Libation instances.
+ //Do no clean DownloadsInProgressDirectory because those files are resumable
+ foreach (var tempFile in Directory.EnumerateFiles(DecryptInProgressDirectory))
+ FileUtility.SaferDelete(tempFile);
+ }
+
+
+ private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
public static bool AaxcExists(string productId) => AAXC.Exists(productId);
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
diff --git a/Source/LibationFileManager/Configuration.KnownDirectories.cs b/Source/LibationFileManager/Configuration.KnownDirectories.cs
index c69d16a1..5ad6a75e 100644
--- a/Source/LibationFileManager/Configuration.KnownDirectories.cs
+++ b/Source/LibationFileManager/Configuration.KnownDirectories.cs
@@ -25,7 +25,7 @@ namespace LibationFileManager
[Description("The same folder that Libation is running from")]
AppDir = 2,
- [Description("Windows temporary folder")]
+ [Description("System temporary folder")]
WinTemp = 3,
[Description("My Documents")]