Merge pull request #413 from Mbucari/master

Update obsolete code and fix #347
This commit is contained in:
rmcrackan 2022-12-18 09:41:31 -05:00 committed by GitHub
commit 7fd002d2c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 340 additions and 445 deletions

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dinah.Core; using Dinah.Core;
@ -52,7 +51,7 @@ namespace AaxDecrypter
// delete file after validation is complete // delete file after validation is complete
FileUtility.SaferDelete(OutputFileName); FileUtility.SaferDelete(OutputFileName);
} }
public abstract Task CancelAsync(); public abstract Task CancelAsync();
@ -72,7 +71,7 @@ namespace AaxDecrypter
=> RetrievedNarrators?.Invoke(this, narrators); => RetrievedNarrators?.Invoke(this, narrators);
protected void OnRetrievedCoverArt(byte[] coverArt) protected void OnRetrievedCoverArt(byte[] coverArt)
=> RetrievedCoverArt?.Invoke(this, coverArt); => RetrievedCoverArt?.Invoke(this, coverArt);
protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress) protected void OnDecryptProgressUpdate(DownloadProgress downloadProgress)
=> DecryptProgressUpdate?.Invoke(this, downloadProgress); => DecryptProgressUpdate?.Invoke(this, downloadProgress);
protected void OnDecryptTimeRemaining(TimeSpan timeRemaining) protected void OnDecryptTimeRemaining(TimeSpan timeRemaining)
=> DecryptTimeRemaining?.Invoke(this, timeRemaining); => DecryptTimeRemaining?.Invoke(this, timeRemaining);
@ -111,8 +110,8 @@ namespace AaxDecrypter
{ {
FileUtility.SaferDelete(jsonDownloadState); FileUtility.SaferDelete(jsonDownloadState);
if (DownloadOptions.AudibleKey is not null && if (DownloadOptions.AudibleKey is not null &&
DownloadOptions.AudibleIV is not null && DownloadOptions.AudibleIV is not null &&
DownloadOptions.RetainEncryptedFile) DownloadOptions.RetainEncryptedFile)
{ {
string aaxPath = Path.ChangeExtension(TempFilePath, ".aax"); string aaxPath = Path.ChangeExtension(TempFilePath, ".aax");
@ -156,12 +155,7 @@ namespace AaxDecrypter
private NetworkFileStreamPersister NewNetworkFilePersister() private NetworkFileStreamPersister NewNetworkFilePersister()
{ {
var headers = new System.Net.WebHeaderCollection var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, new() { { "User-Agent", DownloadOptions.UserAgent } });
{
{ "User-Agent", DownloadOptions.UserAgent }
};
var networkFileStream = new NetworkFileStream(TempFilePath, new Uri(DownloadOptions.DownloadUrl), 0, headers);
return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState); return new NetworkFileStreamPersister(networkFileStream, jsonDownloadState);
} }
} }

View File

@ -1,91 +1,53 @@
using Dinah.Core; using Dinah.Core;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
namespace AaxDecrypter namespace AaxDecrypter
{ {
/// <summary> /// <summary>A resumable, simultaneous file downloader and reader. </summary>
/// A <see cref="CookieContainer"/> for a single Uri.
/// </summary>
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);
}
}
/// <summary>
/// A resumable, simultaneous file downloader and reader.
/// </summary>
public class NetworkFileStream : Stream, IUpdatable public class NetworkFileStream : Stream, IUpdatable
{ {
public event EventHandler Updated; public event EventHandler Updated;
#region Public Properties #region Public Properties
/// <summary> /// <summary> Location to save the downloaded data. </summary>
/// Location to save the downloaded data.
/// </summary>
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public string SaveFilePath { get; } public string SaveFilePath { get; }
/// <summary> /// <summary> Http(s) address of the file to download. </summary>
/// Http(s) address of the file to download.
/// </summary>
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public Uri Uri { get; private set; } public Uri Uri { get; private set; }
/// <summary> /// <summary> Http headers to be sent to the server with the request. </summary>
/// All cookies set by caller or by the remote server.
/// </summary>
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public SingleUriCookieContainer CookieContainer { get; } public Dictionary<string, string> RequestHeaders { get; private set; }
/// <summary> /// <summary> The position in <see cref="SaveFilePath"/> that has been written and flushed to disk. </summary>
/// Http headers to be sent to the server with the request.
/// </summary>
[JsonProperty(Required = Required.Always)]
public WebHeaderCollection RequestHeaders { get; private set; }
/// <summary>
/// The position in <see cref="SaveFilePath"/> that has been written and flushed to disk.
/// </summary>
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public long WritePosition { get; private set; } public long WritePosition { get; private set; }
/// <summary> /// <summary> The total length of the <see cref="Uri"/> file to download. </summary>
/// The total length of the <see cref="Uri"/> file to download.
/// </summary>
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public long ContentLength { get; private set; } public long ContentLength { get; private set; }
[JsonIgnore]
public bool IsCancelled { get; private set; }
#endregion #endregion
#region Private Properties #region Private Properties
private HttpWebRequest HttpRequest { get; set; }
private FileStream _writeFile { get; } private FileStream _writeFile { get; }
private FileStream _readFile { get; } private FileStream _readFile { get; }
private Stream _networkStream { get; set; } private EventWaitHandle _downloadedPiece { get; set; }
private bool hasBegunDownloading { get; set; } private Task _backgroundDownloadTask { get; set; }
public bool IsCancelled { get; private set; }
private EventWaitHandle downloadEnded { get; set; }
private EventWaitHandle downloadedPiece { get; set; }
#endregion #endregion
@ -102,15 +64,12 @@ namespace AaxDecrypter
#region Constructor #region Constructor
/// <summary> /// <summary> A resumable, simultaneous file downloader and reader. </summary>
/// A resumable, simultaneous file downloader and reader.
/// </summary>
/// <param name="saveFilePath">Path to a location on disk to save the downloaded data from <paramref name="uri"/></param> /// <param name="saveFilePath">Path to a location on disk to save the downloaded data from <paramref name="uri"/></param>
/// <param name="uri">Http(s) address of the file to download.</param> /// <param name="uri">Http(s) address of the file to download.</param>
/// <param name="writePosition">The position in <paramref name="uri"/> to begin downloading.</param> /// <param name="writePosition">The position in <paramref name="uri"/> to begin downloading.</param>
/// <param name="requestHeaders">Http headers to be sent to the server with the <see cref="HttpWebRequest"/>.</param> /// <param name="requestHeaders">Http headers to be sent to the server with the <see cref="HttpWebRequest"/>.</param>
/// <param name="cookies">A <see cref="SingleUriCookieContainer"/> with cookies to send with the <see cref="HttpWebRequest"/>. It will also be populated with any cookies set by the server. </param> public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, Dictionary<string, string> requestHeaders = null)
public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null)
{ {
ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath)); ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri)); ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri));
@ -122,8 +81,7 @@ namespace AaxDecrypter
SaveFilePath = saveFilePath; SaveFilePath = saveFilePath;
Uri = uri; Uri = uri;
WritePosition = writePosition; WritePosition = writePosition;
RequestHeaders = requestHeaders ?? new WebHeaderCollection(); RequestHeaders = requestHeaders ?? new();
CookieContainer = cookies ?? new SingleUriCookieContainer { Uri = uri };
_writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite) _writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
{ {
@ -139,12 +97,10 @@ namespace AaxDecrypter
#region Downloader #region Downloader
/// <summary> /// <summary> Update the <see cref="JsonFilePersister"/>. </summary>
/// Update the <see cref="JsonFilePersister"/>.
/// </summary>
private void Update() private void Update()
{ {
RequestHeaders = HttpRequest.Headers; RequestHeaders["Range"] = $"bytes={WritePosition}-";
try try
{ {
Updated?.Invoke(this, EventArgs.Empty); Updated?.Invoke(this, EventArgs.Empty);
@ -155,9 +111,7 @@ namespace AaxDecrypter
} }
} }
/// <summary> /// <summary> Set a different <see cref="System.Uri"/> to the same file targeted by this instance of <see cref="NetworkFileStream"/> </summary>
/// Set a different <see cref="System.Uri"/> to the same file targeted by this instance of <see cref="NetworkFileStream"/>
/// </summary>
/// <param name="uriToSameFile">New <see cref="System.Uri"/> host must match existing host.</param> /// <param name="uriToSameFile">New <see cref="System.Uri"/> host must match existing host.</param>
public void SetUriForSameFile(Uri uriToSameFile) public void SetUriForSameFile(Uri uriToSameFile)
{ {
@ -165,37 +119,31 @@ namespace AaxDecrypter
if (uriToSameFile.Host != Uri.Host) 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}"); 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."); throw new InvalidOperationException("Cannot change Uri after download has started.");
Uri = uriToSameFile; Uri = uriToSameFile;
HttpRequest = WebRequest.CreateHttp(Uri); RequestHeaders["Range"] = $"bytes={WritePosition}-";
HttpRequest.CookieContainer = CookieContainer;
HttpRequest.Headers = RequestHeaders;
//If NetworkFileStream is resuming, Header will already contain a range.
HttpRequest.Headers.Remove("Range");
HttpRequest.AddRange(WritePosition);
} }
/// <summary>
/// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread.
/// </summary>
private void BeginDownloading()
{
downloadEnded = new EventWaitHandle(false, EventResetMode.ManualReset);
/// <summary> Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread. </summary>
/// <returns>The downloader <see cref="Task"/></returns>
private Task BeginDownloading()
{
if (ContentLength != 0 && WritePosition == ContentLength) if (ContentLength != 0 && WritePosition == ContentLength)
{ return Task.CompletedTask;
hasBegunDownloading = true;
downloadEnded.Set();
return;
}
if (ContentLength != 0 && WritePosition > ContentLength) if (ContentLength != 0 && WritePosition > ContentLength)
throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10})."); 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) if (response.StatusCode != HttpStatusCode.PartialContent)
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}."); 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 //Content length is the length of the range request, and it is only equal
//to the complete file length if requesting Range: bytes=0- //to the complete file length if requesting Range: bytes=0-
if (WritePosition == 0) if (WritePosition == 0)
ContentLength = response.ContentLength; ContentLength = response.Content.Headers.ContentLength.GetValueOrDefault();
_networkStream = response.GetResponseStream(); var networkStream = response.Content.ReadAsStream();
downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset); _downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Download the file in the background. //Download the file in the background.
new Thread(() => DownloadFile()) return Task.Run(() => DownloadFile(networkStream));
{ IsBackground = true }
.Start();
hasBegunDownloading = true;
return;
} }
/// <summary> /// <summary> Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.</summary>
/// Download <see cref="Uri"/> to <see cref="SaveFilePath"/>. private void DownloadFile(Stream networkStream)
/// </summary>
private void DownloadFile()
{ {
var downloadPosition = WritePosition; var downloadPosition = WritePosition;
var nextFlush = downloadPosition + DATA_FLUSH_SZ; var nextFlush = downloadPosition + DATA_FLUSH_SZ;
@ -231,7 +172,7 @@ namespace AaxDecrypter
int bytesRead; int bytesRead;
do do
{ {
bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ); bytesRead = networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
_writeFile.Write(buff, 0, bytesRead); _writeFile.Write(buff, 0, bytesRead);
downloadPosition += bytesRead; downloadPosition += bytesRead;
@ -242,15 +183,12 @@ namespace AaxDecrypter
WritePosition = downloadPosition; WritePosition = downloadPosition;
Update(); Update();
nextFlush = downloadPosition + DATA_FLUSH_SZ; nextFlush = downloadPosition + DATA_FLUSH_SZ;
downloadedPiece.Set(); _downloadedPiece.Set();
} }
} while (downloadPosition < ContentLength && !IsCancelled && bytesRead > 0); } while (downloadPosition < ContentLength && !IsCancelled && bytesRead > 0);
_writeFile.Close();
_networkStream.Close();
WritePosition = downloadPosition; WritePosition = downloadPosition;
Update();
if (!IsCancelled && WritePosition < ContentLength) if (!IsCancelled && WritePosition < ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10})."); throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
@ -264,8 +202,10 @@ namespace AaxDecrypter
} }
finally finally
{ {
downloadedPiece.Set(); networkStream.Close();
downloadEnded.Set(); _writeFile.Close();
_downloadedPiece.Set();
Update();
} }
} }
@ -274,96 +214,7 @@ namespace AaxDecrypter
#region Json Connverters #region Json Connverters
public static JsonSerializerSettings GetJsonSerializerSettings() public static JsonSerializerSettings GetJsonSerializerSettings()
{ => new JsonSerializerSettings();
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<string>()),
Capacity = jObj["Capacity"].Value<int>(),
MaxCookieSize = jObj["MaxCookieSize"].Value<int>(),
PerDomainCapacity = jObj["PerDomainCapacity"].Value<int>()
};
var cookieList = jObj["Cookies"].ToList();
foreach (var cookie in cookieList)
{
result.Add(
new Cookie
{
Comment = cookie["Comment"].Value<string>(),
HttpOnly = cookie["HttpOnly"].Value<bool>(),
Discard = cookie["Discard"].Value<bool>(),
Domain = cookie["Domain"].Value<string>(),
Expired = cookie["Expired"].Value<bool>(),
Expires = cookie["Expires"].Value<DateTime>(),
Name = cookie["Name"].Value<string>(),
Path = cookie["Path"].Value<string>(),
Port = cookie["Port"].Value<string>(),
Secure = cookie["Secure"].Value<bool>(),
Value = cookie["Value"].Value<string>(),
Version = cookie["Version"].Value<int>(),
});
}
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<string>());
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);
}
}
#endregion #endregion
@ -383,8 +234,7 @@ namespace AaxDecrypter
{ {
get get
{ {
if (!hasBegunDownloading) _backgroundDownloadTask ??= BeginDownloading();
BeginDownloading();
return ContentLength; return ContentLength;
} }
} }
@ -401,15 +251,14 @@ namespace AaxDecrypter
[JsonIgnore] [JsonIgnore]
public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; } public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; }
public override void Flush() => throw new NotImplementedException(); public override void Flush() => throw new InvalidOperationException();
public override void SetLength(long value) => throw new NotImplementedException(); public override void SetLength(long value) => throw new InvalidOperationException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); public override void Write(byte[] buffer, int offset, int count) => throw new InvalidOperationException();
public override int Read(byte[] buffer, int offset, int count) public override int Read(byte[] buffer, int offset, int count)
{ {
if (!hasBegunDownloading) _backgroundDownloadTask ??= BeginDownloading();
BeginDownloading();
var toRead = Math.Min(count, Length - Position); var toRead = Math.Min(count, Length - Position);
WaitToPosition(Position + toRead); WaitToPosition(Position + toRead);
return _readFile.Read(buffer, offset, count); return _readFile.Read(buffer, offset, count);
@ -428,38 +277,32 @@ namespace AaxDecrypter
return _readFile.Position = newPosition; return _readFile.Position = newPosition;
} }
/// <summary> /// <summary>Blocks until the file has downloaded to at least <paramref name="requiredPosition"/>, then returns. </summary>
/// Blocks until the file has downloaded to at least <paramref name="requiredPosition"/>, then returns.
/// </summary>
/// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param> /// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
private void WaitToPosition(long requiredPosition) private void WaitToPosition(long requiredPosition)
{ {
while (WritePosition < requiredPosition while (WritePosition < requiredPosition
&& hasBegunDownloading && _backgroundDownloadTask?.IsCompleted is false
&& !IsCancelled && !IsCancelled)
&& !downloadEnded.WaitOne(0))
{ {
downloadedPiece.WaitOne(100); _downloadedPiece.WaitOne(100);
} }
} }
public override void Close() public override void Close()
{ {
IsCancelled = true; IsCancelled = true;
_backgroundDownloadTask?.Wait();
while (downloadEnded is not null && !downloadEnded.WaitOne(100)) ;
_readFile.Close(); _readFile.Close();
_writeFile.Close(); _writeFile.Close();
_networkStream?.Close();
Update(); Update();
} }
#endregion #endregion
~NetworkFileStream() ~NetworkFileStream()
{ {
downloadEnded?.Close(); _downloadedPiece?.Close();
downloadedPiece?.Close();
} }
} }
} }

View File

@ -8,7 +8,7 @@
<UserControl.Resources> <UserControl.Resources>
<RecyclePool x:Key="RecyclePool" /> <RecyclePool x:Key="RecyclePool" />
<DataTemplate x:Key="queuedBook"> <DataTemplate x:Key="queuedBook">
<CheckBox Margin="10,0,0,0" Content="{Binding Item}" IsChecked="{Binding IsChecked, Mode=TwoWay}" /> <CheckBox HorizontalAlignment="Stretch" Margin="10,0,0,0" Content="{Binding Item}" IsChecked="{Binding IsChecked, Mode=TwoWay}" />
</DataTemplate> </DataTemplate>
<RecyclingElementFactory x:Key="elementFactory" RecyclePool="{StaticResource RecyclePool}"> <RecyclingElementFactory x:Key="elementFactory" RecyclePool="{StaticResource RecyclePool}">
<RecyclingElementFactory.Templates> <RecyclingElementFactory.Templates>

View File

@ -55,6 +55,9 @@
<Compile Update="ViewModels\MainVM.*.cs"> <Compile Update="ViewModels\MainVM.*.cs">
<DependentUpon>MainVM.cs</DependentUpon> <DependentUpon>MainVM.cs</DependentUpon>
</Compile> </Compile>
<Compile Update="Views\MainWindow.*.cs">
<DependentUpon>MainWindow.axaml</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -18,22 +18,6 @@ namespace LibationAvalonia
return defaultBrush; return defaultBrush;
} }
public static Window GetParentWindow(this IControl control) public static Window GetParentWindow(this IControl control) => control.VisualRoot as Window;
{
Window window = null;
var p = control.Parent;
while (p != null)
{
if (p is Window)
{
window = (Window)p;
break;
}
p = p.Parent;
}
return window;
}
} }
} }

View File

@ -5,6 +5,7 @@ using Dinah.Core;
using LibationFileManager; using LibationFileManager;
using System.Collections.Generic; using System.Collections.Generic;
using ReactiveUI; using ReactiveUI;
using System.Linq;
namespace LibationAvalonia.Controls namespace LibationAvalonia.Controls
{ {
@ -16,7 +17,6 @@ namespace LibationAvalonia.Controls
public static readonly StyledProperty<string> SubDirectoryProperty = public static readonly StyledProperty<string> SubDirectoryProperty =
AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(SubDirectory)); AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(SubDirectory));
public static readonly StyledProperty<string> DirectoryProperty = public static readonly StyledProperty<string> DirectoryProperty =
AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(Directory)); AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(Directory));
@ -90,8 +90,19 @@ namespace LibationAvalonia.Controls
private async void CustomDirBrowseBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) private async void CustomDirBrowseBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
OpenFolderDialog ofd = new(); var options = new Avalonia.Platform.Storage.FolderPickerOpenOptions
customStates.CustomDir = await ofd.ShowAsync(VisualRoot as Window); {
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) 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); Directory = customStates.CustomChecked ? selectedDir : System.IO.Path.Combine(selectedDir, SubDirectory);
} }
private void DirectoryOrCustomSelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) private void DirectoryOrCustomSelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{ {
if (e.Property.Name == nameof(Directory) && e.OldValue is null) if (e.Property.Name == nameof(Directory) && e.OldValue is null)

View File

@ -9,6 +9,9 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using ReactiveUI; using ReactiveUI;
using AudibleApi; using AudibleApi;
using Avalonia.Platform.Storage;
using LibationFileManager;
using Avalonia.Platform.Storage.FileIO;
namespace LibationAvalonia.Dialogs namespace LibationAvalonia.Dialogs
{ {
@ -110,24 +113,29 @@ namespace LibationAvalonia.Dialogs
public async void ImportButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void ImportButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
var openFileDialogOptions = new FilePickerOpenOptions
OpenFileDialog ofd = new(); {
ofd.Filters.Add(new() { Name = "JSON File", Extensions = new() { "json" } }); Title = $"Select the audible-cli [account].json file",
ofd.Directory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); AllowMultiple = false,
ofd.AllowMultiple = false; SuggestedStartLocation = new BclStorageFolder(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)),
FileTypeFilter = new FilePickerFileType[]
{
new("JSON files (*.json)") { Patterns = new[] { "json" } },
}
};
string audibleAppDataDir = GetAudibleCliAppDataPath(); string audibleAppDataDir = GetAudibleCliAppDataPath();
if (Directory.Exists(audibleAppDataDir)) 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 try
{ {
var jsonText = File.ReadAllText(filePath[0]); var jsonText = File.ReadAllText(uri.LocalPath);
var mkbAuth = Mkb79Auth.FromJson(jsonText); var mkbAuth = Mkb79Auth.FromJson(jsonText);
var account = await mkbAuth.ToAccountAsync(); var account = await mkbAuth.ToAccountAsync();
@ -148,7 +156,7 @@ namespace LibationAvalonia.Dialogs
{ {
await MessageBox.ShowAdminAlert( await MessageBox.ShowAdminAlert(
this, 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", "Error Importing Account",
ex); ex);
} }
@ -263,26 +271,36 @@ namespace LibationAvalonia.Dialogs
return; return;
} }
SaveFileDialog sfd = new(); var options = new FilePickerSaveOptions
sfd.Filters.Add(new() { Name = "JSON File", Extensions = new() { "json" } }); {
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(); string audibleAppDataDir = GetAudibleCliAppDataPath();
if (Directory.Exists(audibleAppDataDir)) if (Directory.Exists(audibleAppDataDir))
sfd.Directory = audibleAppDataDir; options.SuggestedStartLocation = new BclStorageFolder(audibleAppDataDir);
string fileName = await sfd.ShowAsync(this); var selectedFile = await StorageProvider.SaveFilePickerAsync(options);
if (fileName is null)
return; if (!selectedFile.TryGetUri(out var uri)) return;
try try
{ {
var mkbAuth = Mkb79Auth.FromAccount(account); var mkbAuth = Mkb79Auth.FromAccount(account);
var jsonText = mkbAuth.ToJson(); 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) catch (Exception ex)
{ {

View File

@ -7,6 +7,7 @@ using System;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using ReactiveUI; using ReactiveUI;
using Avalonia.Platform.Storage;
namespace LibationAvalonia.Dialogs namespace LibationAvalonia.Dialogs
{ {
@ -46,27 +47,30 @@ namespace LibationAvalonia.Dialogs
public async void SaveImage_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) 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(); var selectedFile = await StorageProvider.SaveFilePickerAsync(options);
saveFileDialog.Filters.Add(new FileDialogFilter { Name = "Jpeg", Extensions = new System.Collections.Generic.List<string>() { "jpg" } });
saveFileDialog.InitialFileName = PictureFileName;
saveFileDialog.Directory
= !LibationFileManager.Configuration.IsWindows ? null
: Directory.Exists(BookSaveDirectory) ? BookSaveDirectory
: Path.GetDirectoryName(BookSaveDirectory);
var fileName = await saveFileDialog.ShowAsync(this); if (!selectedFile.TryGetUri(out var uri)) return;
if (fileName is null)
return;
try try
{ {
File.WriteAllBytes(fileName, CoverBytes); File.WriteAllBytes(uri.LocalPath, CoverBytes);
} }
catch (Exception ex) 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); 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);
} }
} }

View File

@ -351,21 +351,27 @@
</Grid> </Grid>
</controls:GroupBox> </controls:GroupBox>
<controls:GroupBox
<StackPanel
Grid.Row="2" Grid.Row="2"
Margin="5"
BorderWidth="1"
Label="Temporary Files Location">
<StackPanel
Margin="5" > Margin="5" >
<TextBlock <TextBlock
Margin="0,0,0,10" Margin="0,0,0,10"
Text="{Binding DownloadDecryptSettings.InProgressDescriptionText}" /> Text="{Binding DownloadDecryptSettings.InProgressDescriptionText}" />
<controls:DirectorySelectControl <controls:DirectoryOrCustomSelectControl
SubDirectory="Libation\DecryptInProgress" SubDirectory="Libation"
SelectedDirectory="{Binding DownloadDecryptSettings.InProgressDirectory, Mode=TwoWay}" /> Directory="{Binding DownloadDecryptSettings.InProgressDirectory, Mode=TwoWay}"
KnownDirectories="{Binding DownloadDecryptSettings.KnownDirectories}" />
</StackPanel> </StackPanel>
</controls:GroupBox>
<CheckBox <CheckBox
Grid.Row="3" Grid.Row="3"

View File

@ -9,6 +9,7 @@ using ReactiveUI;
using Dinah.Core; using Dinah.Core;
using System.Linq; using System.Linq;
using FileManager; using FileManager;
using System.IO;
namespace LibationAvalonia.Dialogs namespace LibationAvalonia.Dialogs
{ {
@ -227,7 +228,6 @@ namespace LibationAvalonia.Dialogs
public class DownloadDecryptSettings : ViewModels.ViewModelBase, ISettingsDisplay public class DownloadDecryptSettings : ViewModels.ViewModelBase, ISettingsDisplay
{ {
private bool _badBookAsk; private bool _badBookAsk;
private bool _badBookAbort; private bool _badBookAbort;
private bool _badBookRetry; private bool _badBookRetry;
@ -242,7 +242,16 @@ namespace LibationAvalonia.Dialogs
LoadSettings(config); LoadSettings(config);
} }
public Configuration.KnownDirectories InProgressDirectory { get; set; } public List<Configuration.KnownDirectories> 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) public void LoadSettings(Configuration config)
{ {
BadBookAsk = config.BadBook is Configuration.BadBookAction.Ask; BadBookAsk = config.BadBook is Configuration.BadBookAction.Ask;
@ -252,9 +261,7 @@ namespace LibationAvalonia.Dialogs
FolderTemplate = config.FolderTemplate; FolderTemplate = config.FolderTemplate;
FileTemplate = config.FileTemplate; FileTemplate = config.FileTemplate;
ChapterFileTemplate = config.ChapterFileTemplate; ChapterFileTemplate = config.ChapterFileTemplate;
InProgressDirectory InProgressDirectory = config.InProgress;
= config.InProgress == Configuration.AppDir_Absolute ? Configuration.KnownDirectories.AppDir
: Configuration.GetKnownDirectory(config.InProgress);
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon; UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
} }
@ -289,9 +296,7 @@ namespace LibationAvalonia.Dialogs
config.FolderTemplate = FolderTemplate; config.FolderTemplate = FolderTemplate;
config.FileTemplate = FileTemplate; config.FileTemplate = FileTemplate;
config.ChapterFileTemplate = ChapterFileTemplate; config.ChapterFileTemplate = ChapterFileTemplate;
config.InProgress config.InProgress = InProgressDirectory;
= InProgressDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute
: Configuration.GetKnownDirectoryPath(InProgressDirectory);
config.UseCoverAsFolderIcon = UseCoverAsFolderIcon; config.UseCoverAsFolderIcon = UseCoverAsFolderIcon;

View File

@ -18,95 +18,98 @@
<StartupObject /> <StartupObject />
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<!-- <!--
HACK FOR COMPILER BUG 2021-09-14. Hopefully will be fixed in future versions HACK FOR COMPILER BUG 2021-09-14. Hopefully will be fixed in future versions
- Not using SatelliteResourceLanguages will load all language packs: works - Not using SatelliteResourceLanguages will load all language packs: works
- Specifying 'en' semicolon 1 more should load 1 language pack: works - Specifying 'en' semicolon 1 more should load 1 language pack: works
- Specifying only 'en' should load no language packs: broken, still loads all - Specifying only 'en' should load no language packs: broken, still loads all
--> -->
<SatelliteResourceLanguages>en;es</SatelliteResourceLanguages> <SatelliteResourceLanguages>en;es</SatelliteResourceLanguages>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\bin\Avalonia\Debug</OutputPath> <OutputPath>..\bin\Avalonia\Debug</OutputPath>
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\bin\Avalonia\Release</OutputPath> <OutputPath>..\bin\Avalonia\Release</OutputPath>
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<None Remove=".gitignore" />
<None Remove="Assets\Asterisk.png" />
<None Remove="Assets\cancel.png" />
<None Remove="Assets\completed.png" />
<None Remove="Assets\down.png" />
<None Remove="Assets\download-arrow.png" />
<None Remove="Assets\edit-tags-25x25.png" />
<None Remove="Assets\edit-tags-50x50.png" />
<None Remove="Assets\edit_25x25.png" />
<None Remove="Assets\edit_64x64.png" />
<None Remove="Assets\error.png" />
<None Remove="Assets\errored.png" />
<None Remove="Assets\Exclamation.png" />
<None Remove="Assets\first.png" />
<None Remove="Assets\glass-with-glow_16.png" />
<None Remove="Assets\img-coverart-prod-unavailable_300x300.jpg" />
<None Remove="Assets\img-coverart-prod-unavailable_500x500.jpg" />
<None Remove="Assets\img-coverart-prod-unavailable_80x80.jpg" />
<None Remove="Assets\import_16x16.png" />
<None Remove="Assets\last.png" />
<None Remove="Assets\libation.ico" />
<None Remove="Assets\LibationStyles.xaml" />
<None Remove="Assets\liberate_green.png" />
<None Remove="Assets\liberate_green_pdf_no.png" />
<None Remove="Assets\liberate_green_pdf_yes.png" />
<None Remove="Assets\liberate_red.png" />
<None Remove="Assets\liberate_red_pdf_no.png" />
<None Remove="Assets\liberate_red_pdf_yes.png" />
<None Remove="Assets\liberate_yellow.png" />
<None Remove="Assets\liberate_yellow_pdf_no.png" />
<None Remove="Assets\liberate_yellow_pdf_yes.png" />
<None Remove="Assets\MBIcons\Asterisk.png" />
<None Remove="Assets\MBIcons\error.png" />
<None Remove="Assets\MBIcons\Exclamation.png" />
<None Remove="Assets\MBIcons\Question.png" />
<None Remove="Assets\minus.png" />
<None Remove="Assets\plus.png" />
<None Remove="Assets\Question.png" />
<None Remove="Assets\queued.png" />
<None Remove="Assets\up.png" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" /> <AvaloniaResource Include="Assets\**" />
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" /> <None Remove=".gitignore" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" /> <None Remove="Assets\Asterisk.png" />
</ItemGroup> <None Remove="Assets\cancel.png" />
<None Remove="Assets\completed.png" />
<None Remove="Assets\down.png" />
<None Remove="Assets\download-arrow.png" />
<None Remove="Assets\edit-tags-25x25.png" />
<None Remove="Assets\edit-tags-50x50.png" />
<None Remove="Assets\edit_25x25.png" />
<None Remove="Assets\edit_64x64.png" />
<None Remove="Assets\error.png" />
<None Remove="Assets\errored.png" />
<None Remove="Assets\Exclamation.png" />
<None Remove="Assets\first.png" />
<None Remove="Assets\glass-with-glow_16.png" />
<None Remove="Assets\img-coverart-prod-unavailable_300x300.jpg" />
<None Remove="Assets\img-coverart-prod-unavailable_500x500.jpg" />
<None Remove="Assets\img-coverart-prod-unavailable_80x80.jpg" />
<None Remove="Assets\import_16x16.png" />
<None Remove="Assets\last.png" />
<None Remove="Assets\libation.ico" />
<None Remove="Assets\LibationStyles.xaml" />
<None Remove="Assets\liberate_green.png" />
<None Remove="Assets\liberate_green_pdf_no.png" />
<None Remove="Assets\liberate_green_pdf_yes.png" />
<None Remove="Assets\liberate_red.png" />
<None Remove="Assets\liberate_red_pdf_no.png" />
<None Remove="Assets\liberate_red_pdf_yes.png" />
<None Remove="Assets\liberate_yellow.png" />
<None Remove="Assets\liberate_yellow_pdf_no.png" />
<None Remove="Assets\liberate_yellow_pdf_yes.png" />
<None Remove="Assets\MBIcons\Asterisk.png" />
<None Remove="Assets\MBIcons\error.png" />
<None Remove="Assets\MBIcons\Exclamation.png" />
<None Remove="Assets\MBIcons\Question.png" />
<None Remove="Assets\minus.png" />
<None Remove="Assets\plus.png" />
<None Remove="Assets\Question.png" />
<None Remove="Assets\queued.png" />
<None Remove="Assets\up.png" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Properties\Resources.Designer.cs"> <ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<DesignTime>True</DesignTime> <ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
<AutoGen>True</AutoGen> <ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
<DependentUpon>Resources.resx</DependentUpon> </ItemGroup>
</Compile>
</ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx"> <Compile Update="Properties\Resources.Designer.cs">
<Generator>ResXFileCodeGenerator</Generator> <DesignTime>True</DesignTime>
<LastGenOutput>Resources.Designer.cs</LastGenOutput> <AutoGen>True</AutoGen>
</EmbeddedResource> <DependentUpon>Resources.resx</DependentUpon>
</ItemGroup> </Compile>
<Compile Update="Views\MainWindow.*.cs">
<DependentUpon>MainWindow.axaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Controls\GroupBox.axaml" />
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Controls\GroupBox.axaml" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.0-preview4" /> <PackageReference Include="Avalonia" Version="11.0.0-preview4" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-preview4" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-preview4" />
@ -120,16 +123,16 @@
<ItemGroup> <ItemGroup>
<None Update="glass-with-glow_256.svg"> <None Update="glass-with-glow_256.svg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="Libation.desktop"> <None Update="Libation.desktop">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="ZipExtractor.exe"> <None Update="ZipExtractor.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
<Target Name="SpicNSpan" AfterTargets="Clean"> <Target Name="SpicNSpan" AfterTargets="Clean">

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -6,6 +6,7 @@ using Avalonia;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
using DataLayer; using DataLayer;
using FileLiberator; using FileLiberator;
using LibationAvalonia.Controls; using LibationAvalonia.Controls;
@ -42,7 +43,7 @@ namespace LibationAvalonia.Views
}; };
var pdvm = new ProductsDisplayViewModel(); var pdvm = new ProductsDisplayViewModel();
pdvm.DisplayBooksAsync(sampleEntries); _ = pdvm.DisplayBooksAsync(sampleEntries);
DataContext = pdvm; DataContext = pdvm;
return; return;
@ -106,17 +107,22 @@ namespace LibationAvalonia.Views
{ {
try try
{ {
var openFileDialog = new OpenFileDialog var openFileDialogOptions = new FilePickerOpenOptions
{ {
Title = $"Locate the audio file for '{entry.Book.Title}'", 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)) var selectedFiles = await this.GetParentWindow().StorageProvider.OpenFilePickerAsync(openFileDialogOptions);
FilePathCache.Insert(entry.AudibleProductId, filePath); var selectedFile = selectedFiles.SingleOrDefault();
if (selectedFile.TryGetUri(out var uri))
FilePathCache.Insert(entry.AudibleProductId, uri.LocalPath);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -16,7 +16,16 @@ namespace LibationFileManager
public static LongPath DownloadsInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName; 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; 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 bool AaxcExists(string productId) => AAXC.Exists(productId);
public static AudioFileStorage Audio { get; } = new AudioFileStorage(); public static AudioFileStorage Audio { get; } = new AudioFileStorage();

View File

@ -25,7 +25,7 @@ namespace LibationFileManager
[Description("The same folder that Libation is running from")] [Description("The same folder that Libation is running from")]
AppDir = 2, AppDir = 2,
[Description("Windows temporary folder")] [Description("System temporary folder")]
WinTemp = 3, WinTemp = 3,
[Description("My Documents")] [Description("My Documents")]