Merge pull request #413 from Mbucari/master
Update obsolete code and fix #347
This commit is contained in:
commit
7fd002d2c9
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Dinah.Core;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
{
|
||||
/// <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>
|
||||
/// <summary>A resumable, simultaneous file downloader and reader. </summary>
|
||||
public class NetworkFileStream : Stream, IUpdatable
|
||||
{
|
||||
public event EventHandler Updated;
|
||||
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// Location to save the downloaded data.
|
||||
/// </summary>
|
||||
/// <summary> Location to save the downloaded data. </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public string SaveFilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Http(s) address of the file to download.
|
||||
/// </summary>
|
||||
/// <summary> Http(s) address of the file to download. </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public Uri Uri { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// All cookies set by caller or by the remote server.
|
||||
/// </summary>
|
||||
/// <summary> Http headers to be sent to the server with the request. </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public SingleUriCookieContainer CookieContainer { get; }
|
||||
public Dictionary<string, string> RequestHeaders { get; private set; }
|
||||
|
||||
/// <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>
|
||||
/// <summary> The position in <see cref="SaveFilePath"/> that has been written and flushed to disk. </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public long WritePosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total length of the <see cref="Uri"/> file to download.
|
||||
/// </summary>
|
||||
/// <summary> The total length of the <see cref="Uri"/> file to download. </summary>
|
||||
[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
|
||||
|
||||
/// <summary>
|
||||
/// A resumable, simultaneous file downloader and reader.
|
||||
/// </summary>
|
||||
/// <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="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="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, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null)
|
||||
public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, Dictionary<string, string> 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
|
||||
|
||||
/// <summary>
|
||||
/// Update the <see cref="JsonFilePersister"/>.
|
||||
/// </summary>
|
||||
/// <summary> Update the <see cref="JsonFilePersister"/>. </summary>
|
||||
private void Update()
|
||||
{
|
||||
RequestHeaders = HttpRequest.Headers;
|
||||
RequestHeaders["Range"] = $"bytes={WritePosition}-";
|
||||
try
|
||||
{
|
||||
Updated?.Invoke(this, EventArgs.Empty);
|
||||
@ -155,9 +111,7 @@ namespace AaxDecrypter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a different <see cref="System.Uri"/> to the same file targeted by this instance of <see cref="NetworkFileStream"/>
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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}-";
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.
|
||||
/// </summary>
|
||||
private void DownloadFile()
|
||||
/// <summary> Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.</summary>
|
||||
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<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);
|
||||
}
|
||||
}
|
||||
=> new JsonSerializerSettings();
|
||||
|
||||
#endregion
|
||||
|
||||
@ -383,8 +234,7 @@ namespace AaxDecrypter
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!hasBegunDownloading)
|
||||
BeginDownloading();
|
||||
_backgroundDownloadTask ??= BeginDownloading();
|
||||
return ContentLength;
|
||||
}
|
||||
}
|
||||
@ -401,14 +251,13 @@ 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);
|
||||
@ -428,38 +277,32 @@ namespace AaxDecrypter
|
||||
return _readFile.Position = newPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blocks until the file has downloaded to at least <paramref name="requiredPosition"/>, then returns.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<UserControl.Resources>
|
||||
<RecyclePool x:Key="RecyclePool" />
|
||||
<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>
|
||||
<RecyclingElementFactory x:Key="elementFactory" RecyclePool="{StaticResource RecyclePool}">
|
||||
<RecyclingElementFactory.Templates>
|
||||
|
||||
@ -55,6 +55,9 @@
|
||||
<Compile Update="ViewModels\MainVM.*.cs">
|
||||
<DependentUpon>MainVM.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\MainWindow.*.cs">
|
||||
<DependentUpon>MainWindow.axaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<string> SubDirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(SubDirectory));
|
||||
|
||||
|
||||
public static readonly StyledProperty<string> DirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, string>(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)
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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<string>() { "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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -351,21 +351,27 @@
|
||||
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
BorderWidth="1"
|
||||
Label="Temporary Files Location">
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="5" >
|
||||
|
||||
<TextBlock
|
||||
Margin="0,0,0,10"
|
||||
Text="{Binding DownloadDecryptSettings.InProgressDescriptionText}" />
|
||||
|
||||
<controls:DirectorySelectControl
|
||||
SubDirectory="Libation\DecryptInProgress"
|
||||
SelectedDirectory="{Binding DownloadDecryptSettings.InProgressDirectory, Mode=TwoWay}" />
|
||||
<controls:DirectoryOrCustomSelectControl
|
||||
SubDirectory="Libation"
|
||||
Directory="{Binding DownloadDecryptSettings.InProgressDirectory, Mode=TwoWay}"
|
||||
KnownDirectories="{Binding DownloadDecryptSettings.KnownDirectories}" />
|
||||
|
||||
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<CheckBox
|
||||
Grid.Row="3"
|
||||
|
||||
@ -9,6 +9,7 @@ using ReactiveUI;
|
||||
using Dinah.Core;
|
||||
using System.Linq;
|
||||
using FileManager;
|
||||
using System.IO;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
@ -227,7 +228,6 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public class DownloadDecryptSettings : ViewModels.ViewModelBase, ISettingsDisplay
|
||||
{
|
||||
|
||||
private bool _badBookAsk;
|
||||
private bool _badBookAbort;
|
||||
private bool _badBookRetry;
|
||||
@ -242,7 +242,16 @@ namespace LibationAvalonia.Dialogs
|
||||
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)
|
||||
{
|
||||
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;
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
<None Remove=".gitignore" />
|
||||
<None Remove="Assets\Asterisk.png" />
|
||||
@ -94,6 +94,9 @@
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\MainWindow.*.cs">
|
||||
<DependentUpon>MainWindow.axaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
62
Source/LibationAvalonia/Views/MainWindow.Export.cs
Normal file
62
Source/LibationAvalonia/Views/MainWindow.Export.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -16,6 +16,15 @@ 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;
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@ -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")]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user