Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c4575cf66 | ||
|
|
f4749d703f | ||
|
|
f2f562619b | ||
|
|
16c019a9c6 | ||
|
|
644dcbdd4d | ||
|
|
6b112f5248 | ||
|
|
0bfa609058 | ||
|
|
8020ded642 | ||
|
|
c4cd6b16fc | ||
|
|
310012fd17 | ||
|
|
06163db6ff | ||
|
|
7689eed711 | ||
|
|
d396d697d7 | ||
|
|
27ed11d904 | ||
|
|
9e7670b918 | ||
|
|
31e97defd1 | ||
|
|
1a447627c7 | ||
|
|
962b386d07 | ||
|
|
d69ff24c2d | ||
|
|
070ed1d373 | ||
|
|
47729bf7b0 | ||
|
|
ed0ce2976b | ||
|
|
2224f46ed5 | ||
|
|
433974323c | ||
|
|
7525d318c0 | ||
|
|
92327dcc0d | ||
|
|
aeaf234edd | ||
|
|
a99b644917 | ||
|
|
d79a55e5c9 | ||
|
|
16b0feeb82 | ||
|
|
7b3a25e45a | ||
|
|
8effdcb92d | ||
|
|
b12bef81bd | ||
|
|
f04a5e0168 | ||
|
|
e093729707 | ||
|
|
369151ada2 | ||
|
|
1f685ae8a0 | ||
|
|
bbe91099cb | ||
|
|
92015ba4c2 | ||
|
|
3bcacabadc | ||
|
|
f5736d9151 | ||
|
|
59015f438e | ||
|
|
3af47ab395 | ||
|
|
308619b01a | ||
|
|
4efce57488 | ||
|
|
c8ee950f7d | ||
|
|
0bba0f9256 | ||
|
|
05bdff5123 | ||
|
|
e58e6cfb9f | ||
|
|
b052871004 | ||
|
|
d738f4f35f | ||
|
|
7286aee9dd | ||
|
|
ca455978a5 | ||
|
|
9c38bea5b7 | ||
|
|
fbec1bc569 | ||
|
|
6dd885f0b2 | ||
|
|
ab38eb5571 | ||
|
|
0e4b9ab396 | ||
|
|
7dfedbc73b | ||
|
|
625ae1d63c | ||
|
|
71098ef02f | ||
|
|
d63a6de543 | ||
|
|
2a71a85306 | ||
|
|
6de3a8a2bf | ||
|
|
3fc1da66de | ||
|
|
683c221ca8 | ||
|
|
fe6cfc899b | ||
|
|
ffd947eb2e | ||
|
|
8dd59cb08a | ||
|
|
1e4c489983 | ||
|
|
17b0da358f | ||
|
|
6aa0a1f8b9 | ||
|
|
ab731a63af | ||
|
|
07d2c656fc | ||
|
|
9ecb32c3d2 | ||
|
|
503e1e143e | ||
|
|
e34ce67a2c | ||
|
|
a0fd0a3de6 | ||
|
|
7f3cbc454f | ||
|
|
30eb117fa1 | ||
|
|
63877160aa | ||
|
|
77e61479cf | ||
|
|
ca71283108 | ||
|
|
285563af5e | ||
|
|
62cbad0d8f | ||
|
|
2cb2479d63 | ||
|
|
e7c5b1d8dc | ||
|
|
7f086aeaac | ||
|
|
78186d4973 | ||
|
|
a4ff739684 | ||
|
|
9e06c70319 | ||
|
|
0c98ce000b |
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve Libation
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Platform**
|
||||
|
||||
[e.g. Windows 10, Windows 11, Mac, Linux (State distribution)]
|
||||
|
||||
**Log Files**
|
||||
Attach your Libation log file here.
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
1
.gitignore
vendored
@@ -370,3 +370,4 @@ FodyWeavers.xsd
|
||||
|
||||
/__TODO.txt
|
||||
/DataLayer/LibationContext.db
|
||||
*/bin-Avalonia
|
||||
6
.releaseindex.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"WindowsClassic": "Libation\\.\\d+\\.\\d+\\.\\d+-win-classic\\.zip",
|
||||
"WindowsAvalonia":"Libation\\.\\d+\\.\\d+\\.\\d+-win-chardonnay\\.zip",
|
||||
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay",
|
||||
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-macos-chardonnay"
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
- [Getting started](Documentation/GettingStarted.md)
|
||||
- [Download Libation](Documentation/GettingStarted.md#download-libation-1)
|
||||
- [Installation](Documentation/GettingStarted.md#installation)
|
||||
- [Installation on Ubuntu](Source/LibationAvalonia/README.md)
|
||||
- [Create Accounts](Documentation/GettingStarted.md#create-accounts)
|
||||
- [Import your library](Documentation/GettingStarted.md#import-your-library)
|
||||
- [Download your books -- DRM-free!](Documentation/GettingStarted.md#download-your-books----drm-free)
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.2.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
@@ -16,6 +12,9 @@
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.2.12" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<Version>8.2.3.1</Version>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Version>8.3.2.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="0.51.0" />
|
||||
<PackageReference Include="Octokit" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DefineConstants Condition=" '$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' ">$(DefineConstants);WINDOWS</DefineConstants>
|
||||
<DefineConstants Condition=" '$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' ">$(DefineConstants);LINUX</DefineConstants>
|
||||
<DefineConstants Condition=" '$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' ">$(DefineConstants);MACOS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -14,8 +14,23 @@ using Serilog;
|
||||
|
||||
namespace AppScaffolding
|
||||
{
|
||||
|
||||
public enum ReleaseIdentifier
|
||||
{
|
||||
None,
|
||||
WindowsClassic,
|
||||
WindowsAvalonia,
|
||||
LinuxAvalonia,
|
||||
MacOSAvalonia
|
||||
}
|
||||
|
||||
public static class LibationScaffolding
|
||||
{
|
||||
public static ReleaseIdentifier ReleaseIdentifier { get; private set; }
|
||||
|
||||
public static void SetReleaseIdentifier(ReleaseIdentifier releaseID)
|
||||
=> ReleaseIdentifier = releaseID;
|
||||
|
||||
// AppScaffolding
|
||||
private static Assembly _executingAssembly;
|
||||
private static Assembly ExecutingAssembly
|
||||
@@ -91,20 +106,20 @@ namespace AppScaffolding
|
||||
config.DecryptToLossy = false;
|
||||
|
||||
if (!config.Exists(nameof(config.LameTargetBitrate)))
|
||||
config.LameTargetBitrate = false;
|
||||
|
||||
config.LameTargetBitrate = false;
|
||||
|
||||
if (!config.Exists(nameof(config.LameDownsampleMono)))
|
||||
config.LameDownsampleMono = true;
|
||||
|
||||
|
||||
if (!config.Exists(nameof(config.LameBitrate)))
|
||||
config.LameBitrate = 64;
|
||||
|
||||
|
||||
if (!config.Exists(nameof(config.LameConstantBitrate)))
|
||||
config.LameConstantBitrate = false;
|
||||
|
||||
|
||||
if (!config.Exists(nameof(config.LameMatchSourceBR)))
|
||||
config.LameMatchSourceBR = true;
|
||||
|
||||
|
||||
if (!config.Exists(nameof(config.LameVBRQuality)))
|
||||
config.LameVBRQuality = 2;
|
||||
|
||||
@@ -275,12 +290,23 @@ namespace AppScaffolding
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
mode += " (Debugger attached)";
|
||||
|
||||
#if MACOS
|
||||
var os = "MacOS";
|
||||
#elif LINUX
|
||||
var os = "Linux";
|
||||
#else
|
||||
var os = "Windows";
|
||||
#endif
|
||||
|
||||
|
||||
// begin logging session with a form feed
|
||||
Log.Logger.Information("\r\n\f");
|
||||
Log.Logger.Information("Begin. {@DebugInfo}", new
|
||||
{
|
||||
AppName = EntryAssembly.GetName().Name,
|
||||
Version = BuildVersion.ToString(),
|
||||
ReleaseIdentifier = ReleaseIdentifier,
|
||||
OS = os,
|
||||
Mode = mode,
|
||||
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
||||
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
|
||||
@@ -312,8 +338,9 @@ namespace AppScaffolding
|
||||
public static UpgradeProperties GetLatestRelease()
|
||||
{
|
||||
// timed out
|
||||
var latest = getLatestRelease(TimeSpan.FromSeconds(10));
|
||||
if (latest is null)
|
||||
(var latest, var zip) = getLatestRelease(TimeSpan.FromSeconds(10));
|
||||
|
||||
if (latest is null || zip is null)
|
||||
return null;
|
||||
|
||||
var latestVersionString = latest.TagName.Trim('v');
|
||||
@@ -325,7 +352,7 @@ namespace AppScaffolding
|
||||
return null;
|
||||
|
||||
// we have an update
|
||||
var zip = latest.Assets.FirstOrDefault(a => a.BrowserDownloadUrl.EndsWith(".zip"));
|
||||
|
||||
var zipUrl = zip?.BrowserDownloadUrl;
|
||||
|
||||
Log.Logger.Information("Update available: {@DebugInfo}", new
|
||||
@@ -337,11 +364,11 @@ namespace AppScaffolding
|
||||
|
||||
return new(zipUrl, latest.HtmlUrl, zip.Name, latestRelease);
|
||||
}
|
||||
private static Octokit.Release getLatestRelease(TimeSpan timeout)
|
||||
private static (Octokit.Release, Octokit.ReleaseAsset) getLatestRelease(TimeSpan timeout)
|
||||
{
|
||||
try
|
||||
{
|
||||
var task = System.Threading.Tasks.Task.Run(() => getLatestRelease());
|
||||
var task = getLatestRelease();
|
||||
if (task.Wait(timeout))
|
||||
return task.Result;
|
||||
|
||||
@@ -351,16 +378,26 @@ namespace AppScaffolding
|
||||
{
|
||||
Log.Logger.Error(aggEx, "Checking for new version too often");
|
||||
}
|
||||
return null;
|
||||
return (null, null);
|
||||
}
|
||||
private static Octokit.Release getLatestRelease()
|
||||
private static async System.Threading.Tasks.Task<(Octokit.Release, Octokit.ReleaseAsset)> getLatestRelease()
|
||||
{
|
||||
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue("Libation"));
|
||||
var ownerAccount = "rmcrackan";
|
||||
var repoName = "Libation";
|
||||
|
||||
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue(repoName));
|
||||
|
||||
//Download the release index
|
||||
var bts = await gitHubClient.Repository.Content.GetRawContent(ownerAccount, repoName, ".releaseindex.json");
|
||||
var releaseIndex = JObject.Parse(System.Text.Encoding.ASCII.GetString(bts));
|
||||
var regexPattern = releaseIndex.Value<string>(ReleaseIdentifier.ToString());
|
||||
|
||||
// https://octokitnet.readthedocs.io/en/latest/releases/
|
||||
var releases = gitHubClient.Repository.Release.GetAll("rmcrackan", "Libation").GetAwaiter().GetResult();
|
||||
var latest = releases.First(r => !r.Draft && !r.Prerelease);
|
||||
return latest;
|
||||
var releases = await gitHubClient.Repository.Release.GetAll(ownerAccount, repoName);
|
||||
|
||||
var regex = new System.Text.RegularExpressions.Regex(regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
var latestRelease = releases.FirstOrDefault(r => !r.Draft && !r.Prerelease && r.Assets.Any(a => regex.IsMatch(a.Name)));
|
||||
return (latestRelease, latestRelease?.Assets?.FirstOrDefault(a => regex.IsMatch(a.Name)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -100,6 +100,9 @@ namespace ApplicationServices
|
||||
|
||||
[Name("Content Type")]
|
||||
public string ContentType { get; set; }
|
||||
|
||||
[Name("Audio Format")]
|
||||
public string AudioFormat { get; set; }
|
||||
}
|
||||
public static class LibToDtos
|
||||
{
|
||||
@@ -132,7 +135,8 @@ namespace ApplicationServices
|
||||
MyLibationTags = a.Book.UserDefinedItem.Tags,
|
||||
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
|
||||
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(),
|
||||
ContentType = a.Book.ContentType.ToString()
|
||||
ContentType = a.Book.ContentType.ToString(),
|
||||
AudioFormat = a.Book.AudioFormat.ToString()
|
||||
}).ToList();
|
||||
}
|
||||
public static class LibraryExporter
|
||||
@@ -176,33 +180,34 @@ namespace ApplicationServices
|
||||
var row = sheet.CreateRow(rowIndex);
|
||||
|
||||
var columns = new[] {
|
||||
nameof (ExportDto.Account),
|
||||
nameof (ExportDto.DateAdded),
|
||||
nameof (ExportDto.AudibleProductId),
|
||||
nameof (ExportDto.Locale),
|
||||
nameof (ExportDto.Title),
|
||||
nameof (ExportDto.AuthorNames),
|
||||
nameof (ExportDto.NarratorNames),
|
||||
nameof (ExportDto.LengthInMinutes),
|
||||
nameof (ExportDto.Description),
|
||||
nameof (ExportDto.Publisher),
|
||||
nameof (ExportDto.HasPdf),
|
||||
nameof (ExportDto.SeriesNames),
|
||||
nameof (ExportDto.SeriesOrder),
|
||||
nameof (ExportDto.CommunityRatingOverall),
|
||||
nameof (ExportDto.CommunityRatingPerformance),
|
||||
nameof (ExportDto.CommunityRatingStory),
|
||||
nameof (ExportDto.PictureId),
|
||||
nameof (ExportDto.IsAbridged),
|
||||
nameof (ExportDto.DatePublished),
|
||||
nameof (ExportDto.CategoriesNames),
|
||||
nameof (ExportDto.MyRatingOverall),
|
||||
nameof (ExportDto.MyRatingPerformance),
|
||||
nameof (ExportDto.MyRatingStory),
|
||||
nameof (ExportDto.MyLibationTags),
|
||||
nameof (ExportDto.BookStatus),
|
||||
nameof (ExportDto.PdfStatus),
|
||||
nameof (ExportDto.ContentType)
|
||||
nameof(ExportDto.Account),
|
||||
nameof(ExportDto.DateAdded),
|
||||
nameof(ExportDto.AudibleProductId),
|
||||
nameof(ExportDto.Locale),
|
||||
nameof(ExportDto.Title),
|
||||
nameof(ExportDto.AuthorNames),
|
||||
nameof(ExportDto.NarratorNames),
|
||||
nameof(ExportDto.LengthInMinutes),
|
||||
nameof(ExportDto.Description),
|
||||
nameof(ExportDto.Publisher),
|
||||
nameof(ExportDto.HasPdf),
|
||||
nameof(ExportDto.SeriesNames),
|
||||
nameof(ExportDto.SeriesOrder),
|
||||
nameof(ExportDto.CommunityRatingOverall),
|
||||
nameof(ExportDto.CommunityRatingPerformance),
|
||||
nameof(ExportDto.CommunityRatingStory),
|
||||
nameof(ExportDto.PictureId),
|
||||
nameof(ExportDto.IsAbridged),
|
||||
nameof(ExportDto.DatePublished),
|
||||
nameof(ExportDto.CategoriesNames),
|
||||
nameof(ExportDto.MyRatingOverall),
|
||||
nameof(ExportDto.MyRatingPerformance),
|
||||
nameof(ExportDto.MyRatingStory),
|
||||
nameof(ExportDto.MyLibationTags),
|
||||
nameof(ExportDto.BookStatus),
|
||||
nameof(ExportDto.PdfStatus),
|
||||
nameof(ExportDto.ContentType),
|
||||
nameof(ExportDto.AudioFormat)
|
||||
};
|
||||
var col = 0;
|
||||
foreach (var c in columns)
|
||||
@@ -268,6 +273,7 @@ namespace ApplicationServices
|
||||
row.CreateCell(col++).SetCellValue(dto.BookStatus);
|
||||
row.CreateCell(col++).SetCellValue(dto.PdfStatus);
|
||||
row.CreateCell(col++).SetCellValue(dto.ContentType);
|
||||
row.CreateCell(col++).SetCellValue(dto.AudioFormat);
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
@@ -152,6 +152,11 @@ namespace AudibleUtilities
|
||||
Serilog.Log.Logger.Debug("Completed library scan.");
|
||||
|
||||
#if DEBUG
|
||||
//// this will not work for multi accounts
|
||||
//var library_json = "library.json";
|
||||
//library_json = System.IO.Path.GetFullPath(library_json);
|
||||
//if (System.IO.File.Exists(library_json))
|
||||
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
|
||||
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
|
||||
#endif
|
||||
var validators = new List<IValidator>();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="4.4.0.1" />
|
||||
<PackageReference Include="AudibleApi" Version="4.6.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -11,12 +11,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="4.1.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.6">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -17,7 +17,6 @@ namespace DataLayer
|
||||
|
||||
public class AudioFormat : IComparable<AudioFormat>, IComparable
|
||||
{
|
||||
|
||||
internal int AudioFormatID { get; private set; }
|
||||
public int Bitrate { get; private init; }
|
||||
public int SampleRate { get; private init; }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -37,7 +37,11 @@ namespace FileLiberator
|
||||
Narrators = libraryBook.Book.Narrators.Select(c => c.Name).ToList(),
|
||||
|
||||
SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name,
|
||||
SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Order
|
||||
SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Order,
|
||||
|
||||
BitRate = libraryBook.Book.AudioFormat.Bitrate,
|
||||
SampleRate = libraryBook.Book.AudioFormat.SampleRate,
|
||||
Channels = libraryBook.Book.AudioFormat.Channels,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,13 @@ namespace FileManager
|
||||
public string Path { get; init; }
|
||||
public override string ToString() => Path;
|
||||
|
||||
private static readonly PlatformID PlatformID = Environment.OSVersion.Platform;
|
||||
|
||||
|
||||
public static implicit operator LongPath(string path)
|
||||
{
|
||||
if (PlatformID is PlatformID.Unix) return new LongPath { Path = path };
|
||||
|
||||
if (path is null) return null;
|
||||
|
||||
//File I/O functions in the Windows API convert "/" to "\" as part of converting
|
||||
@@ -58,6 +63,8 @@ namespace FileManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PlatformID is PlatformID.Unix) return Path;
|
||||
|
||||
//Short Path names are useful for navigating to the file in windows explorer,
|
||||
//which will not recognize paths longer than MAX_PATH. Short path names are not
|
||||
//always enabled on every volume. So to check if a volume enables short path
|
||||
@@ -96,6 +103,7 @@ namespace FileManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PlatformID is PlatformID.Unix) return Path;
|
||||
if (Path is null) return null;
|
||||
|
||||
StringBuilder longPathBuffer = new(MaxPathLength);
|
||||
@@ -106,15 +114,21 @@ namespace FileManager
|
||||
|
||||
[JsonIgnore]
|
||||
public string PathWithoutPrefix
|
||||
=> Path?.StartsWith(LONG_PATH_PREFIX) == true ?
|
||||
Path.Remove(0, LONG_PATH_PREFIX.Length) :
|
||||
Path;
|
||||
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PlatformID is PlatformID.Unix) return Path;
|
||||
return
|
||||
Path?.StartsWith(LONG_PATH_PREFIX) == true ? Path.Remove(0, LONG_PATH_PREFIX.Length)
|
||||
:Path;
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetShortPathName([MarshalAs(UnmanagedType.LPWStr)] string path, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder shortPath, int shortPathLength);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetLongPathName([MarshalAs(UnmanagedType.LPWStr)] string lpszShortPath, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpszLongPath, int cchBuffer);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace FileManager
|
||||
internal const char QUOTE_MARK = '"';
|
||||
[JsonIgnore] public bool Mandatory { get; internal set; }
|
||||
[JsonProperty] public char CharacterToReplace { get; private set; }
|
||||
[JsonProperty] public string ReplacementString { get; private set; }
|
||||
[JsonProperty] public string ReplacementString { get; set; }
|
||||
[JsonProperty] public string Description { get; private set; }
|
||||
public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})";
|
||||
|
||||
@@ -168,8 +168,10 @@ namespace FileManager
|
||||
}
|
||||
|
||||
|
||||
public static bool ContainsInvalid(string path)
|
||||
public static bool ContainsInvalidPathChar(string path)
|
||||
=> path.Any(c => invalidChars.Contains(c));
|
||||
public static bool ContainsInvalidFilenameChar(string path)
|
||||
=> path.Any(c => invalidChars.Concat(new char[] { '\\', '/' }).Contains(c));
|
||||
|
||||
public string ReplaceInvalidFilenameChars(string fileName)
|
||||
{
|
||||
@@ -246,7 +248,7 @@ namespace FileManager
|
||||
dict[3].CharacterToReplace != default3.CharacterToReplace || dict[3].Description != default3.Description ||
|
||||
dict[4].CharacterToReplace != default4.CharacterToReplace || dict[4].Description != default4.Description ||
|
||||
dict[5].CharacterToReplace != default5.CharacterToReplace || dict[5].Description != default5.Description ||
|
||||
dict.Any(r => ReplacementCharacters.ContainsInvalid(r.ReplacementString))
|
||||
dict.Any(r => ReplacementCharacters.ContainsInvalidPathChar(r.ReplacementString))
|
||||
)
|
||||
{
|
||||
dict = ReplacementCharacters.Default.Replacements;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>hangover.ico</ApplicationIcon>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPublishable>true</IsPublishable>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
@@ -54,4 +55,11 @@
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="SpicNSpan" AfterTargets="Clean">
|
||||
<!-- Remove obj folder -->
|
||||
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
|
||||
<!-- Remove bin folder -->
|
||||
<RemoveDir Directories="$(BaseOutputPath)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -12,5 +12,6 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -66,6 +66,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationFileManager.Tests",
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangover", "Hangover\Hangover.csproj", "{40C67036-C1A7-4FDF-AA83-8EC902E257F3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationAvalonia", "LibationAvalonia\LibationAvalonia.csproj", "{F612D06F-3134-4B9B-95CD-EB3FC798AE60}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -144,6 +146,10 @@ Global
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -167,6 +173,7 @@ Global
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:LibationWinForms.AvaloniaUI"
|
||||
x:Class="LibationWinForms.AvaloniaUI.App">
|
||||
xmlns:local="using:LibationAvalonia"
|
||||
x:Class="LibationAvalonia.App">
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
@@ -11,7 +11,7 @@
|
||||
<FluentTheme Mode="Light"/>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
|
||||
<StyleInclude Source="/AvaloniaUI/Assets/DataGridTheme.xaml"/>
|
||||
<StyleInclude Source="/AvaloniaUI/Assets/LibationStyles.xaml"/>
|
||||
<StyleInclude Source="/Assets/DataGridTheme.xaml"/>
|
||||
<StyleInclude Source="/Assets/LibationStyles.xaml"/>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
236
Source/LibationAvalonia/App.axaml.cs
Normal file
@@ -0,0 +1,236 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using LibationFileManager;
|
||||
using LibationAvalonia.Views;
|
||||
using System;
|
||||
using Avalonia.Platform;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using ApplicationServices;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
public class App : Application
|
||||
{
|
||||
public static readonly bool IsWindows;
|
||||
public static readonly bool IsLinux;
|
||||
public static readonly bool IsMacOs;
|
||||
static App()
|
||||
{
|
||||
IsWindows = OperatingSystem.IsWindows();
|
||||
IsLinux = OperatingSystem.IsLinux();
|
||||
IsMacOs = OperatingSystem.IsMacOS();
|
||||
}
|
||||
|
||||
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookDefaultBrush { get; private set; }
|
||||
public static IBrush SeriesEntryGridBackgroundBrush { get; private set; }
|
||||
|
||||
public static IAssetLoader AssetLoader { get; private set; }
|
||||
|
||||
public static readonly Uri AssetUriBase = new("avares://Libation/Assets/");
|
||||
public static Stream OpenAsset(string assetRelativePath)
|
||||
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
|
||||
|
||||
|
||||
public static bool GoToFile(string path)
|
||||
=> IsWindows ? Go.To.File(path)
|
||||
: GoToFolder(path is null ? string.Empty : Path.GetDirectoryName(path));
|
||||
|
||||
public static bool GoToFolder(string path)
|
||||
{
|
||||
if (IsWindows)
|
||||
return Go.To.Folder(path);
|
||||
else if (IsLinux)
|
||||
{
|
||||
var startInfo = new System.Diagnostics.ProcessStartInfo()
|
||||
{
|
||||
FileName = "/bin/xdg-open",
|
||||
Arguments = path is null ? string.Empty : $"\"{path}\"",
|
||||
UseShellExecute = false, //Import in Linux environments
|
||||
CreateNoWindow = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
System.Diagnostics.Process.Start(startInfo);
|
||||
return true;
|
||||
}
|
||||
//Don't know how to do this for mac yet
|
||||
else return true;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
AssetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
|
||||
}
|
||||
|
||||
public static Task<List<DataLayer.LibraryBook>> LibraryTask;
|
||||
public static bool SetupRequired;
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
LoadStyles();
|
||||
|
||||
var SEGOEUI = new Typeface(new FontFamily(new Uri("avares://Libation/Assets/WINGDING.TTF"), "SEGOEUI_Local"));
|
||||
var gtf = FontManager.Current.GetOrAddGlyphTypeface(SEGOEUI);
|
||||
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
if (SetupRequired)
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var defaultLibationFilesDir = Configuration.UserProfile;
|
||||
|
||||
// check for existing settings in default location
|
||||
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
||||
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
|
||||
config.SetLibationFiles(defaultLibationFilesDir);
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
{
|
||||
var setupDialog = new SetupDialog { Config = config };
|
||||
setupDialog.Closing += Setup_Closing;
|
||||
desktop.MainWindow = setupDialog;
|
||||
}
|
||||
}
|
||||
else
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
private async void Setup_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
var setupDialog = sender as SetupDialog;
|
||||
var desktop = ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
|
||||
|
||||
try
|
||||
{
|
||||
// all returns should be preceded by either:
|
||||
// - if config.LibationSettingsAreValid
|
||||
// - error message, Exit()
|
||||
|
||||
if ((!setupDialog.IsNewUser
|
||||
&& !setupDialog.IsReturningUser) ||
|
||||
!await RunInstall(setupDialog))
|
||||
{
|
||||
await CancelInstallation();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// most migrations go in here
|
||||
AppScaffolding.LibationScaffolding.RunPostConfigMigrations(setupDialog.Config);
|
||||
|
||||
await MessageBox.VerboseLoggingWarning_ShowIfTrue();
|
||||
|
||||
#if !DEBUG
|
||||
//AutoUpdater.NET only works for WinForms or WPF application projects.
|
||||
//checkForUpdate();
|
||||
#endif
|
||||
// logging is init'd here
|
||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(setupDialog.Config);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var title = "Fatal error, pre-logging";
|
||||
var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
|
||||
try
|
||||
{
|
||||
await MessageBox.ShowAdminAlert(null, body, title, ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await MessageBox.Show($"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
|
||||
private static async Task<bool> RunInstall(SetupDialog setupDialog)
|
||||
{
|
||||
var config = setupDialog.Config;
|
||||
|
||||
if (setupDialog.IsNewUser)
|
||||
{
|
||||
config.SetLibationFiles(Configuration.UserProfile);
|
||||
}
|
||||
else if (setupDialog.IsReturningUser)
|
||||
{
|
||||
|
||||
var libationFilesDialog = new LibationFilesDialog();
|
||||
|
||||
if (await libationFilesDialog.ShowDialog<DialogResult>(setupDialog) != DialogResult.OK)
|
||||
return false;
|
||||
|
||||
config.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||
if (config.LibationSettingsAreValid)
|
||||
return true;
|
||||
|
||||
// path did not result in valid settings
|
||||
var continueResult = await MessageBox.Show(
|
||||
$"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
|
||||
"New install?",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question);
|
||||
|
||||
if (continueResult != DialogResult.Yes)
|
||||
return false;
|
||||
}
|
||||
|
||||
// INIT DEFAULT SETTINGS
|
||||
// if 'new user' was clicked, or if 'returning user' chose new install: show basic settings dialog
|
||||
config.Books ??= Path.Combine(Configuration.UserProfile, "Books");
|
||||
|
||||
AppScaffolding.LibationScaffolding.PopulateMissingConfigValues(config);
|
||||
return await new SettingsDialog().ShowDialog<DialogResult>(setupDialog) == DialogResult.OK
|
||||
&& config.LibationSettingsAreValid;
|
||||
}
|
||||
|
||||
static async Task CancelInstallation()
|
||||
{
|
||||
await MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var mainWindow = new MainWindow();
|
||||
desktop.MainWindow = mainWindow;
|
||||
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
|
||||
mainWindow.OnLoad();
|
||||
mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult());
|
||||
mainWindow.Show();
|
||||
}
|
||||
|
||||
private static void LoadStyles()
|
||||
{
|
||||
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookFailedBrush");
|
||||
ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCompletedBrush");
|
||||
ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCancelledBrush");
|
||||
ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookDefaultBrush");
|
||||
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources("SeriesEntryGridBackgroundBrush");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 95 B After Width: | Height: | Size: 95 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
BIN
Source/LibationAvalonia/Assets/SEGOEUI.TTF
Normal file
BIN
Source/LibationAvalonia/Assets/WINGDING.TTF
Normal file
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
Source/LibationAvalonia/Assets/download-arrow.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
Source/LibationAvalonia/Assets/edit-tags-25x25.png
Normal file
|
After Width: | Height: | Size: 314 B |
BIN
Source/LibationAvalonia/Assets/edit-tags-50x50.png
Normal file
|
After Width: | Height: | Size: 573 B |
|
Before Width: | Height: | Size: 747 B After Width: | Height: | Size: 747 B |
BIN
Source/LibationAvalonia/Assets/edit_64x64.png
Normal file
|
After Width: | Height: | Size: 813 B |
BIN
Source/LibationAvalonia/Assets/error.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 482 B After Width: | Height: | Size: 482 B |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 383 B After Width: | Height: | Size: 383 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
BIN
Source/LibationAvalonia/Assets/liberate_green.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Source/LibationAvalonia/Assets/liberate_green_pdf_no.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
Source/LibationAvalonia/Assets/liberate_green_pdf_yes.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
Source/LibationAvalonia/Assets/liberate_red.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Source/LibationAvalonia/Assets/liberate_red_pdf_no.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
Source/LibationAvalonia/Assets/liberate_red_pdf_yes.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
Source/LibationAvalonia/Assets/liberate_yellow.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
Source/LibationAvalonia/Assets/liberate_yellow_pdf_no.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
Source/LibationAvalonia/Assets/liberate_yellow_pdf_yes.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
Source/LibationAvalonia/Assets/minus.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
Source/LibationAvalonia/Assets/plus.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,7 +1,10 @@
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationWinForms.AvaloniaUI
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
internal static class AvaloniaUtils
|
||||
{
|
||||
@@ -1,5 +1,5 @@
|
||||
<DataGridCheckBoxColumn xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LibationWinForms.AvaloniaUI.Controls.DataGridCheckBoxColumnExt">
|
||||
x:Class="LibationAvalonia.Controls.DataGridCheckBoxColumnExt">
|
||||
|
||||
</DataGridCheckBoxColumn >
|
||||
@@ -1,8 +1,8 @@
|
||||
using Avalonia.Controls;
|
||||
using LibationWinForms.AvaloniaUI.ViewModels;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using System;
|
||||
|
||||
namespace LibationWinForms.AvaloniaUI.Controls
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class DataGridCheckBoxColumnExt : DataGridCheckBoxColumn
|
||||
{
|
||||
@@ -0,0 +1,33 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="LibationAvalonia.Controls.DirectoryOrCustomSelectControl">
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto">
|
||||
<controls:DirectorySelectControl
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
Name="directorySelectControl"
|
||||
SubDirectory="{Binding $parent.SubDirectory}"
|
||||
KnownDirectories="{Binding $parent.KnownDirectories}" />
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Name="knownDirRadio"
|
||||
IsChecked="{Binding KnownChecked, Mode=TwoWay}" />
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Name="customDirRadio"
|
||||
IsChecked="{Binding CustomChecked, Mode=TwoWay}" />
|
||||
|
||||
<Grid Grid.Column="1" Grid.Row="1" ColumnDefinitions="*,Auto">
|
||||
<TextBox IsEnabled="{Binding CustomChecked}" Name="customDirTbox" Grid.Column="0" IsReadOnly="True" Text="{Binding CustomDir, Mode=TwoWay}" />
|
||||
<Button Name="customDirBrowseBtn" Grid.Column="1" Content="..." Margin="5,0,0,0" Padding="10,0,10,0" VerticalAlignment="Stretch" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,179 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using System.Collections.Generic;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class DirectoryOrCustomSelectControl : UserControl
|
||||
{
|
||||
public static readonly StyledProperty<List<Configuration.KnownDirectories>> KnownDirectoriesProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, List<Configuration.KnownDirectories>>(nameof(KnownDirectories), DirectorySelectControl.DefaultKnownDirectories);
|
||||
|
||||
public static readonly StyledProperty<string> SubDirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(SubDirectory));
|
||||
|
||||
|
||||
public static readonly StyledProperty<string> DirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(Directory));
|
||||
|
||||
public List<Configuration.KnownDirectories> KnownDirectories
|
||||
{
|
||||
get => GetValue(KnownDirectoriesProperty);
|
||||
set => SetValue(KnownDirectoriesProperty, value);
|
||||
}
|
||||
|
||||
public string Directory
|
||||
{
|
||||
get => GetValue(DirectoryProperty);
|
||||
set => SetValue(DirectoryProperty, value);
|
||||
}
|
||||
|
||||
public string SubDirectory
|
||||
{
|
||||
get => GetValue(SubDirectoryProperty);
|
||||
set => SetValue(SubDirectoryProperty, value);
|
||||
}
|
||||
CustomState customStates = new();
|
||||
|
||||
public DirectoryOrCustomSelectControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
customDirBrowseBtn = this.Find<Button>(nameof(customDirBrowseBtn));
|
||||
directorySelectControl = this.Find<DirectorySelectControl>(nameof(directorySelectControl));
|
||||
|
||||
this.Find<TextBox>(nameof(customDirTbox)).DataContext = customStates;
|
||||
this.Find<RadioButton>(nameof(knownDirRadio)).DataContext = customStates;
|
||||
this.Find<RadioButton>(nameof(customDirRadio)).DataContext = customStates;
|
||||
|
||||
customStates.PropertyChanged += CheckStates_PropertyChanged;
|
||||
customDirBrowseBtn.Click += CustomDirBrowseBtn_Click;
|
||||
PropertyChanged += DirectoryOrCustomSelectControl_PropertyChanged;
|
||||
directorySelectControl.PropertyChanged += DirectorySelectControl_PropertyChanged;
|
||||
}
|
||||
|
||||
private class CustomState: ViewModels.ViewModelBase
|
||||
{
|
||||
private string _customDir;
|
||||
private bool _knownChecked;
|
||||
private bool _customChecked;
|
||||
public string CustomDir { get=> _customDir; set => this.RaiseAndSetIfChanged(ref _customDir, value); }
|
||||
public bool KnownChecked
|
||||
{
|
||||
get => _knownChecked;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _knownChecked, value);
|
||||
if (value)
|
||||
CustomChecked = false;
|
||||
else if (!CustomChecked)
|
||||
CustomChecked = true;
|
||||
}
|
||||
}
|
||||
public bool CustomChecked
|
||||
{
|
||||
get => _customChecked;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _customChecked, value);
|
||||
if (value)
|
||||
KnownChecked = false;
|
||||
else if (!KnownChecked)
|
||||
KnownChecked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void CustomDirBrowseBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
OpenFolderDialog ofd = new();
|
||||
customStates.CustomDir = await ofd.ShowAsync(VisualRoot as Window);
|
||||
}
|
||||
|
||||
private void CheckStates_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName != nameof(CustomState.CustomDir))
|
||||
{
|
||||
directorySelectControl.IsEnabled = !customStates.CustomChecked;
|
||||
customDirBrowseBtn.IsEnabled = customStates.CustomChecked;
|
||||
}
|
||||
|
||||
setDirectory();
|
||||
}
|
||||
|
||||
|
||||
private void DirectorySelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property.Name == nameof(DirectorySelectControl.SelectedDirectory))
|
||||
{
|
||||
setDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
private void setDirectory()
|
||||
{
|
||||
var selectedDir
|
||||
= customStates.CustomChecked ? customStates.CustomDir
|
||||
: directorySelectControl.SelectedDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute
|
||||
: Configuration.GetKnownDirectoryPath(directorySelectControl.SelectedDirectory);
|
||||
selectedDir ??= string.Empty;
|
||||
|
||||
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)
|
||||
{
|
||||
var directory = Directory?.Trim() ?? "";
|
||||
|
||||
var noSubDir = RemoveSubDirectoryFromPath(directory);
|
||||
var known = Configuration.GetKnownDirectory(noSubDir);
|
||||
|
||||
if (known == Configuration.KnownDirectories.None && noSubDir == Configuration.AppDir_Absolute)
|
||||
known = Configuration.KnownDirectories.AppDir;
|
||||
|
||||
if (known is Configuration.KnownDirectories.None)
|
||||
{
|
||||
customStates.CustomChecked = true;
|
||||
customStates.CustomDir = directory;
|
||||
}
|
||||
else
|
||||
{
|
||||
customStates.KnownChecked = true;
|
||||
directorySelectControl.SelectedDirectory = known;
|
||||
}
|
||||
}
|
||||
else if (e.Property.Name == nameof(KnownDirectories))
|
||||
directorySelectControl.KnownDirectories = KnownDirectories;
|
||||
else if (e.Property.Name == nameof(SubDirectory))
|
||||
directorySelectControl.SubDirectory = SubDirectory;
|
||||
}
|
||||
|
||||
private string RemoveSubDirectoryFromPath(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SubDirectory))
|
||||
return path;
|
||||
|
||||
path = path?.Trim() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return path;
|
||||
|
||||
var bottomDir = System.IO.Path.GetFileName(path);
|
||||
if (SubDirectory.EqualsInsensitive(bottomDir))
|
||||
return System.IO.Path.GetDirectoryName(path);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="LibationAvalonia.Controls.DirectorySelectControl">
|
||||
|
||||
<UserControl.Resources>
|
||||
<controls:KnownDirectoryConverter x:Key="KnownDirectoryConverter" />
|
||||
</UserControl.Resources>
|
||||
|
||||
|
||||
<StackPanel Orientation="Vertical">
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
|
||||
<Setter Property="Height" Value="NaN"/>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<controls:WheelComboBox
|
||||
HorizontalContentAlignment = "Stretch"
|
||||
HorizontalAlignment = "Stretch"
|
||||
MinHeight="{Binding #displayPathTbox.MinHeight}"
|
||||
SelectedItem="{Binding $parent[1].SelectedDirectory, Mode=TwoWay}"
|
||||
Items="{Binding $parent[1].KnownDirectories}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
|
||||
<TextBlock
|
||||
Text="{Binding, Converter={StaticResource KnownDirectoryConverter}}" />
|
||||
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</controls:WheelComboBox>
|
||||
<TextBox Margin="0,10,0,10" IsReadOnly="True" Name="displayPathTbox" />
|
||||
</StackPanel>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,99 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Data.Converters;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data;
|
||||
using System.IO;
|
||||
using System.Reactive.Subjects;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public class KnownDirectoryConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is Configuration.KnownDirectories dir)
|
||||
return dir.GetDescription();
|
||||
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class DirectorySelectControl : UserControl
|
||||
{
|
||||
public static List<Configuration.KnownDirectories> DefaultKnownDirectories { get; }
|
||||
= new()
|
||||
{
|
||||
Configuration.KnownDirectories.WinTemp,
|
||||
Configuration.KnownDirectories.UserProfile,
|
||||
Configuration.KnownDirectories.AppDir,
|
||||
Configuration.KnownDirectories.MyDocs,
|
||||
Configuration.KnownDirectories.LibationFiles
|
||||
};
|
||||
|
||||
public static readonly StyledProperty<Configuration.KnownDirectories> SelectedDirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, Configuration.KnownDirectories>(nameof(SelectedDirectory));
|
||||
|
||||
public static readonly StyledProperty<List<Configuration.KnownDirectories>> KnownDirectoriesProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, List<Configuration.KnownDirectories>>(nameof(KnownDirectories), DefaultKnownDirectories);
|
||||
|
||||
public static readonly StyledProperty<string> SubDirectoryProperty =
|
||||
AvaloniaProperty.Register<DirectorySelectControl, string>(nameof(SubDirectory));
|
||||
|
||||
public DirectorySelectControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
displayPathTbox = this.Get<TextBox>(nameof(displayPathTbox));
|
||||
displayPathTbox.Bind(TextBox.TextProperty, TextboxPath);
|
||||
PropertyChanged += DirectorySelectControl_PropertyChanged;
|
||||
}
|
||||
|
||||
private Subject<string> TextboxPath = new Subject<string>();
|
||||
|
||||
private void DirectorySelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property.Name == nameof(SelectedDirectory))
|
||||
{
|
||||
TextboxPath.OnNext(
|
||||
Path.Combine(
|
||||
SelectedDirectory is Configuration.KnownDirectories.None ? string.Empty
|
||||
: SelectedDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute
|
||||
: Configuration.GetKnownDirectoryPath(SelectedDirectory)
|
||||
, SubDirectory ?? string.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
public List<Configuration.KnownDirectories> KnownDirectories
|
||||
{
|
||||
get => GetValue(KnownDirectoriesProperty);
|
||||
set => SetValue(KnownDirectoriesProperty, value);
|
||||
}
|
||||
|
||||
public Configuration.KnownDirectories SelectedDirectory
|
||||
{
|
||||
get => GetValue(SelectedDirectoryProperty);
|
||||
set => SetValue(SelectedDirectoryProperty, value);
|
||||
}
|
||||
|
||||
public string SubDirectory
|
||||
{
|
||||
get => GetValue(SubDirectoryProperty);
|
||||
set => SetValue(SubDirectoryProperty, value);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="LibationWinForms.AvaloniaUI.Controls.GroupBox">
|
||||
x:Class="LibationAvalonia.Controls.GroupBox">
|
||||
|
||||
<Design.DataContext>
|
||||
</Design.DataContext>
|
||||
@@ -2,7 +2,7 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace LibationWinForms.AvaloniaUI.Controls
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class GroupBox : ContentControl
|
||||
{
|
||||
13
Source/LibationAvalonia/Controls/LinkLabel.axaml
Normal file
@@ -0,0 +1,13 @@
|
||||
<TextBlock xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="LibationAvalonia.Controls.LinkLabel">
|
||||
<TextBlock.Styles>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="Foreground" Value="Blue"/>
|
||||
<Setter Property="TextDecorations" Value="Underline"/>
|
||||
</Style>
|
||||
</TextBlock.Styles>
|
||||
</TextBlock>
|
||||
34
Source/LibationAvalonia/Controls/LinkLabel.axaml.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Styling;
|
||||
using System;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class LinkLabel : TextBlock, IStyleable
|
||||
{
|
||||
Type IStyleable.StyleKey => typeof(TextBlock);
|
||||
private static readonly Cursor HandCursor = new Cursor(StandardCursorType.Hand);
|
||||
public LinkLabel()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
protected override void OnPointerEnter(PointerEventArgs e)
|
||||
{
|
||||
this.Cursor = HandCursor;
|
||||
base.OnPointerEnter(e);
|
||||
}
|
||||
protected override void OnPointerLeave(PointerEventArgs e)
|
||||
{
|
||||
this.Cursor = Cursor.Default;
|
||||
base.OnPointerLeave(e);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Source/LibationAvalonia/Controls/WheelComboBox.axaml
Normal file
@@ -0,0 +1,9 @@
|
||||
<ComboBox xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LibationAvalonia.Controls.WheelComboBox">
|
||||
<ComboBox.Styles>
|
||||
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
|
||||
<Setter Property="Height" Value="NaN"/>
|
||||
</Style>
|
||||
</ComboBox.Styles>
|
||||
</ComboBox>
|
||||
@@ -7,7 +7,7 @@ using System;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationWinForms.AvaloniaUI.Controls
|
||||
namespace LibationAvalonia.Controls
|
||||
{
|
||||
public partial class WheelComboBox : ComboBox, IStyleable
|
||||
{
|
||||
@@ -4,9 +4,9 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
Width="800" Height="450"
|
||||
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.AccountsDialog"
|
||||
x:Class="LibationAvalonia.Dialogs.AccountsDialog"
|
||||
Title="Audible Accounts"
|
||||
Icon="/AvaloniaUI/Assets/libation.ico">
|
||||
Icon="/Assets/libation.ico">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
|
||||
<Grid.Styles>
|
||||
@@ -10,7 +10,7 @@ using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
using AudibleApi;
|
||||
|
||||
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class AccountsDialog : DialogWindow
|
||||
{
|
||||
@@ -56,13 +56,17 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
// only persist in 'save' step
|
||||
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
|
||||
var accounts = persister.AccountsSettings.Accounts;
|
||||
if (!accounts.Any())
|
||||
return;
|
||||
if (accounts.Any())
|
||||
{
|
||||
foreach (var account in accounts)
|
||||
AddAccountToGrid(account);
|
||||
}
|
||||
|
||||
DataContext = this;
|
||||
|
||||
foreach (var account in accounts)
|
||||
AddAccountToGrid(account);
|
||||
addBlankAccount();
|
||||
}
|
||||
private void addBlankAccount()
|
||||
{
|
||||
|
||||
var newBlank = new AccountDto();
|
||||
newBlank.PropertyChanged += AccountDto_PropertyChanged;
|
||||
@@ -89,9 +93,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
if (Accounts.Any(a => a.IsDefault))
|
||||
return;
|
||||
|
||||
var newBlank = new AccountDto();
|
||||
newBlank.PropertyChanged += AccountDto_PropertyChanged;
|
||||
Accounts.Insert(Accounts.Count, newBlank);
|
||||
addBlankAccount();
|
||||
}
|
||||
|
||||
public void DeleteButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
@@ -160,7 +162,6 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
|
||||
protected override async Task SaveAndCloseAsync()
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
if (!await inputIsValid())
|
||||
@@ -190,8 +191,6 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void persist(AccountsSettings accountsSettings)
|
||||
{
|
||||
var existingAccounts = accountsSettings.Accounts;
|
||||
@@ -5,10 +5,10 @@
|
||||
mc:Ignorable="d" d:DesignWidth="550" d:DesignHeight="450"
|
||||
MinWidth="550" MinHeight="450"
|
||||
Width="650" Height="500"
|
||||
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.BookDetailsDialog"
|
||||
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
|
||||
x:Class="LibationAvalonia.Dialogs.BookDetailsDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
Title="Book Details" Name="BookDetails"
|
||||
Icon="/AvaloniaUI/Assets/libation.ico">
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
<Grid RowDefinitions="*,Auto,Auto,40">
|
||||
<Grid.Styles>
|
||||
@@ -17,13 +17,25 @@
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
</Style>
|
||||
</Grid.Styles>
|
||||
<Grid ColumnDefinitions="Auto,*" Margin="10,10,10,0">
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="*,Auto" Margin="10,10,10,0">
|
||||
<Panel VerticalAlignment="Top" Margin="5" Background="LightGray" Width="80" Height="80" >
|
||||
<Image Grid.Column="0" Width="80" Height="80" Source="{Binding Cover}" />
|
||||
</Panel>
|
||||
|
||||
<Panel Grid.Column="0" Grid.Row="1">
|
||||
|
||||
<controls:LinkLabel
|
||||
Margin="10"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
Tapped="GoToAudible_Tapped"
|
||||
Text="Open in
Audible
(Browser)" />
|
||||
</Panel>
|
||||
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
TextWrapping="Wrap"
|
||||
Margin="5"
|
||||
FontSize="12"
|
||||
@@ -43,7 +55,7 @@
|
||||
|
||||
<TextBox Margin="0,5,0,5"
|
||||
MinHeight="25"
|
||||
FontSize="12"
|
||||
FontSize="12" Name="tagsTbox"
|
||||
Text="{Binding Tags, Mode=TwoWay}"/>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
@@ -4,13 +4,14 @@ using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media.Imaging;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using LibationWinForms.AvaloniaUI.ViewModels;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class BookDetailsDialog : DialogWindow
|
||||
{
|
||||
@@ -34,6 +35,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
public BookDetailsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
ControlToFocusOnShow = this.Find<TextBox>(nameof(tagsTbox));
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
@@ -46,13 +48,18 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
LibraryBook = libraryBook;
|
||||
}
|
||||
|
||||
|
||||
protected override void SaveAndClose()
|
||||
{
|
||||
SaveButton_Clicked(null, null);
|
||||
base.SaveAndClose();
|
||||
}
|
||||
|
||||
public void GoToAudible_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
var locale = AudibleApi.Localization.Get(_libraryBook.Book.Locale);
|
||||
var link = $"https://www.audible.{locale.TopDomain}/pd/{_libraryBook.Book.AudibleProductId}";
|
||||
Go.To.Url(link);
|
||||
}
|
||||
|
||||
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
@@ -3,7 +3,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="540" d:DesignHeight="140"
|
||||
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.DescriptionDisplayDialog"
|
||||
x:Class="LibationAvalonia.Dialogs.DescriptionDisplayDialog"
|
||||
SystemDecorations="None"
|
||||
Title="DescriptionDisplay">
|
||||
|
||||
@@ -3,7 +3,7 @@ using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using System;
|
||||
|
||||
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class DescriptionDisplayDialog : Window
|
||||
{
|
||||
@@ -4,10 +4,11 @@ using LibationFileManager;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public abstract class DialogWindow : Window
|
||||
{
|
||||
public bool SaveAndRestorePosition { get; set; } = true;
|
||||
public Control ControlToFocusOnShow { get; set; }
|
||||
public DialogWindow()
|
||||
{
|
||||
@@ -21,16 +22,22 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
public DialogWindow(bool saveAndRestorePosition) : this()
|
||||
{
|
||||
SaveAndRestorePosition = saveAndRestorePosition;
|
||||
}
|
||||
|
||||
private void DialogWindow_Initialized(object sender, EventArgs e)
|
||||
{
|
||||
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
if (SaveAndRestorePosition)
|
||||
this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
}
|
||||
|
||||
private void DialogWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
this.SaveSizeAndLocation(Configuration.Instance);
|
||||
if (SaveAndRestorePosition)
|
||||
this.SaveSizeAndLocation(Configuration.Instance);
|
||||
}
|
||||
|
||||
private void DialogWindow_Opened(object sender, EventArgs e)
|
||||
@@ -4,9 +4,9 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
|
||||
Width="800" Height="450"
|
||||
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.EditQuickFilters"
|
||||
x:Class="LibationAvalonia.Dialogs.EditQuickFilters"
|
||||
Title="Audible Accounts"
|
||||
Icon="/AvaloniaUI/Assets/libation.ico">
|
||||
Icon="/Assets/libation.ico">
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
|
||||
<Grid.Styles>
|
||||
@@ -5,7 +5,7 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class EditQuickFilters : DialogWindow
|
||||
{
|
||||
61
Source/LibationAvalonia/Dialogs/EditReplacementChars.axaml
Normal file
@@ -0,0 +1,61 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="LibationAvalonia.Dialogs.EditReplacementChars"
|
||||
Title="EditReplacementChars">
|
||||
|
||||
<DataGrid
|
||||
GridLinesVisibility="All"
|
||||
AutoGenerateColumns="False"
|
||||
Items="{Binding replacements}">
|
||||
|
||||
<DataGrid.Columns>
|
||||
|
||||
<DataGridTemplateColumn Width="Auto" Header="Char to
Replace">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextPresenter
|
||||
Height="18"
|
||||
Margin="10,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="SEGOEUI_Local"
|
||||
Text="{Binding Replacement.CharacterToReplace}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTemplateColumn IsReadOnly="False" Width="Auto" Header="Replacement Text">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<Grid RowDefinitions="*" ColumnDefinitions="*">
|
||||
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
FontSize="14"
|
||||
FontFamily="SEGOEUI_Local"
|
||||
Foreground="{StaticResource SystemControlTransparentBrush}"
|
||||
SelectionBrush="{StaticResource SystemControlTransparentBrush}"
|
||||
BorderBrush="{StaticResource SystemControlTransparentBrush}"
|
||||
Text="{Binding ReplacementText, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
FontSize="14"
|
||||
FontFamily="SEGOEUI_Local"
|
||||
Text="{Binding ReplacementText}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,54 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using ReactiveUI;
|
||||
using System.Linq;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class EditReplacementChars : DialogWindow
|
||||
{
|
||||
Configuration config = Configuration.Instance;
|
||||
public ObservableCollection<ReplacementsExt> replacements { get; }
|
||||
public EditReplacementChars()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
|
||||
replacements = new(config.ReplacementCharacters.Replacements.Select(r => new ReplacementsExt { Replacement = r }));
|
||||
DataContext = this;
|
||||
}
|
||||
|
||||
|
||||
public class ReplacementsExt : ViewModels.ViewModelBase
|
||||
{
|
||||
public Replacement Replacement { get; init; }
|
||||
public string ReplacementText
|
||||
{
|
||||
get => Replacement.ReplacementString;
|
||||
set
|
||||
{
|
||||
Replacement.ReplacementString = value;
|
||||
this.RaisePropertyChanged(nameof(ReplacementText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
|
||||
private void LoadTable(IReadOnlyList<Replacement> replacements)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
121
Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml
Normal file
@@ -0,0 +1,121 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="LibationAvalonia.Dialogs.EditTemplateDialog"
|
||||
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
|
||||
Icon="/Assets/libation.ico"
|
||||
Title="EditTemplateDialog">
|
||||
|
||||
<Window.Resources>
|
||||
<dialogs:BracketEscapeConverter x:Key="BracketEscapeConverter" />
|
||||
</Window.Resources>
|
||||
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
RowDefinitions="Auto,Auto"
|
||||
ColumnDefinitions="*,Auto" Margin="5">
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Text="{Binding Description}" />
|
||||
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Text="{Binding workingTemplateText, Mode=TwoWay}" />
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Stretch"
|
||||
Padding="20,3,20,3"
|
||||
Content="Reset to Default"
|
||||
Click="ResetButton_Click" />
|
||||
</Grid>
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,*">
|
||||
|
||||
<Border
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="5"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
|
||||
|
||||
<DataGrid
|
||||
GridLinesVisibility="All"
|
||||
AutoGenerateColumns="False"
|
||||
Items="{Binding ListItems}" >
|
||||
|
||||
<DataGrid.Columns>
|
||||
|
||||
<DataGridTemplateColumn Width="Auto" Header="Tag">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextPresenter Height="18" Margin="10,0,10,0" VerticalAlignment="Center" Text="{Binding TagName, Converter={StaticResource BracketEscapeConverter}}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
<DataGridTemplateColumn Width="Auto" Header="Description">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextPresenter
|
||||
Height="18"
|
||||
Margin="10,0,10,0"
|
||||
VerticalAlignment="Center" Text="{Binding Description}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
</Border>
|
||||
|
||||
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
RowDefinitions="Auto,*,80" HorizontalAlignment="Stretch">
|
||||
|
||||
<TextBlock
|
||||
Margin="5,5,5,10"
|
||||
Text="Example:"/>
|
||||
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Margin="5"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
|
||||
|
||||
<WrapPanel
|
||||
Grid.Row="1"
|
||||
Name="wrapPanel"
|
||||
Orientation="Horizontal" />
|
||||
|
||||
</Border>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
Foreground="Firebrick"
|
||||
Text="{Binding WarningText}" />
|
||||
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
Padding="30,5,30,5"
|
||||
HorizontalAlignment="Right"
|
||||
Content="Save"
|
||||
Click="SaveButton_Click" />
|
||||
</Grid>
|
||||
</Window>
|
||||
247
Source/LibationAvalonia/Dialogs/EditTemplateDialog.axaml.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.TextFormatting;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
class BracketEscapeConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string str && str[0] != '<' && str[^1] != '>')
|
||||
return $"<{str}>";
|
||||
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string str && str[0] == '<' && str[^1] == '>')
|
||||
return str[1..^2];
|
||||
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
|
||||
}
|
||||
}
|
||||
public partial class EditTemplateDialog : DialogWindow
|
||||
{
|
||||
// final value. post-validity check
|
||||
public string TemplateText { get; private set; }
|
||||
|
||||
private EditTemplateViewModel _viewModel;
|
||||
|
||||
public EditTemplateDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = new(Configuration.Instance, this.Find<WrapPanel>(nameof(wrapPanel)));
|
||||
}
|
||||
|
||||
public EditTemplateDialog(Templates template, string inputTemplateText) : this()
|
||||
{
|
||||
_viewModel.template = ArgumentValidator.EnsureNotNull(template, nameof(template));
|
||||
Title = $"Edit {_viewModel.template.Name}";
|
||||
_viewModel.Description = _viewModel.template.Description;
|
||||
_viewModel.resetTextBox(inputTemplateText);
|
||||
|
||||
_viewModel.ListItems = _viewModel.template.GetTemplateTags();
|
||||
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
protected override async Task SaveAndCloseAsync()
|
||||
{
|
||||
if (!await _viewModel.Validate())
|
||||
return;
|
||||
|
||||
TemplateText = _viewModel.workingTemplateText;
|
||||
await base.SaveAndCloseAsync();
|
||||
}
|
||||
|
||||
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await SaveAndCloseAsync();
|
||||
|
||||
public void ResetButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> _viewModel.resetTextBox(_viewModel.template.DefaultTemplate);
|
||||
|
||||
private class EditTemplateViewModel : ViewModels.ViewModelBase
|
||||
{
|
||||
WrapPanel WrapPanel;
|
||||
public Configuration config { get; }
|
||||
public EditTemplateViewModel(Configuration configuration, WrapPanel panel)
|
||||
{
|
||||
config = configuration;
|
||||
WrapPanel = panel;
|
||||
}
|
||||
// hold the work-in-progress value. not guaranteed to be valid
|
||||
private string _workingTemplateText;
|
||||
public string workingTemplateText
|
||||
{
|
||||
get => _workingTemplateText;
|
||||
set
|
||||
{
|
||||
_workingTemplateText = template.Sanitize(value);
|
||||
templateTb_TextChanged();
|
||||
|
||||
}
|
||||
}
|
||||
private string _warningText;
|
||||
public string WarningText
|
||||
{
|
||||
get => _warningText;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref _warningText, value);
|
||||
}
|
||||
}
|
||||
|
||||
public Templates template { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
||||
public IEnumerable<TemplateTags> ListItems { get; set; }
|
||||
|
||||
public void resetTextBox(string value) => workingTemplateText = value;
|
||||
|
||||
public async Task<bool> Validate()
|
||||
{
|
||||
if (template.IsValid(workingTemplateText))
|
||||
return true;
|
||||
var errors = template
|
||||
.GetErrors(workingTemplateText)
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
await MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void templateTb_TextChanged()
|
||||
{
|
||||
var isChapterTitle = template == Templates.ChapterTitle;
|
||||
var isFolder = template == Templates.Folder;
|
||||
|
||||
var libraryBookDto = new LibraryBookDto
|
||||
{
|
||||
Account = "my account",
|
||||
AudibleProductId = "123456789",
|
||||
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
|
||||
Locale = "us",
|
||||
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
||||
Narrators = new List<string> { "Stephen Fry" },
|
||||
SeriesName = "Sherlock Holmes",
|
||||
SeriesNumber = "1"
|
||||
};
|
||||
var chapterName = "A Flight for Life";
|
||||
var chapterNumber = 4;
|
||||
var chaptersTotal = 10;
|
||||
|
||||
var partFileProperties = new AaxDecrypter.MultiConvertFileProperties()
|
||||
{
|
||||
OutputFileName = "",
|
||||
PartsPosition = chapterNumber,
|
||||
PartsTotal = chaptersTotal,
|
||||
Title = chapterName
|
||||
};
|
||||
|
||||
var books = config.Books;
|
||||
var folder = Templates.Folder.GetPortionFilename(
|
||||
libraryBookDto,
|
||||
isFolder ? workingTemplateText : config.FolderTemplate);
|
||||
|
||||
var file
|
||||
= template == Templates.ChapterFile
|
||||
? Templates.ChapterFile.GetPortionFilename(
|
||||
libraryBookDto,
|
||||
workingTemplateText,
|
||||
partFileProperties,
|
||||
"")
|
||||
: Templates.File.GetPortionFilename(
|
||||
libraryBookDto,
|
||||
isFolder ? config.FileTemplate : workingTemplateText);
|
||||
var ext = config.DecryptToLossy ? "mp3" : "m4b";
|
||||
|
||||
var chapterTitle = Templates.ChapterTitle.GetPortionTitle(libraryBookDto, workingTemplateText, partFileProperties);
|
||||
|
||||
const char ZERO_WIDTH_SPACE = '\u200B';
|
||||
var sing = $"{Path.DirectorySeparatorChar}";
|
||||
|
||||
// result: can wrap long paths. eg:
|
||||
// |-- LINE WRAP BOUNDARIES --|
|
||||
// \books\author with a very <= normal line break on space between words
|
||||
// long name\narrator narrator
|
||||
// \title <= line break on the zero-with space we added before slashes
|
||||
string slashWrap(string val) => val.Replace(sing, $"{ZERO_WIDTH_SPACE}{sing}");
|
||||
|
||||
WarningText
|
||||
= !template.HasWarnings(workingTemplateText)
|
||||
? ""
|
||||
: "Warning:\r\n" +
|
||||
template
|
||||
.GetWarnings(workingTemplateText)
|
||||
.Select(err => $"- {err}")
|
||||
.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
|
||||
var list = new List<TextCharacters>();
|
||||
|
||||
var bold = new Typeface(Typeface.Default.FontFamily, FontStyle.Normal, FontWeight.Bold);
|
||||
var normal = new Typeface(Typeface.Default.FontFamily, FontStyle.Normal, FontWeight.Normal);
|
||||
|
||||
var stringList = new List<(string, FontWeight)>();
|
||||
|
||||
if (isChapterTitle)
|
||||
{
|
||||
stringList.Add((chapterTitle, FontWeight.Bold));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
stringList.Add((slashWrap(books), FontWeight.Normal));
|
||||
stringList.Add((sing, FontWeight.Normal));
|
||||
|
||||
stringList.Add((slashWrap(folder), isFolder ? FontWeight.Bold : FontWeight.Normal));
|
||||
|
||||
stringList.Add((sing, FontWeight.Normal));
|
||||
|
||||
stringList.Add((file, !isFolder ? FontWeight.Bold : FontWeight.Normal));
|
||||
|
||||
stringList.Add(($".{ext}", FontWeight.Normal));
|
||||
}
|
||||
|
||||
WrapPanel.Children.Clear();
|
||||
|
||||
//Avalonia doesn't yet support anything like rich text, so add a new textblock for every word/style
|
||||
foreach (var item in stringList)
|
||||
{
|
||||
var wordsSplit = item.Item1.Split(' ');
|
||||
|
||||
for(int i = 0; i < wordsSplit.Length; i++)
|
||||
{
|
||||
var tb = new TextBlock
|
||||
{
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Bottom,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Text = wordsSplit[i] + (i == wordsSplit.Length - 1 ? "" : " "),
|
||||
FontWeight = item.Item2
|
||||
};
|
||||
|
||||
WrapPanel.Children.Add(tb);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="500"
|
||||
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.ImageDisplayDialog"
|
||||
x:Class="LibationAvalonia.Dialogs.ImageDisplayDialog"
|
||||
MinWidth="500" MinHeight="500"
|
||||
Title="Cover"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/AvaloniaUI/Assets/libation.ico">
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
<Image Stretch="Uniform" Source="{Binding CoverImage}">
|
||||
<Image.ContextMenu>
|
||||
@@ -8,7 +8,7 @@ using System.ComponentModel;
|
||||
using System.IO;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class ImageDisplayDialog : DialogWindow, INotifyPropertyChanged
|
||||
{
|
||||
@@ -49,8 +49,11 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
|
||||
SaveFileDialog saveFileDialog = new();
|
||||
saveFileDialog.Filters.Add(new FileDialogFilter { Name = "Jpeg", Extensions = new System.Collections.Generic.List<string>() { "jpg" } });
|
||||
saveFileDialog.Directory = Directory.Exists(BookSaveDirectory) ? BookSaveDirectory : Path.GetDirectoryName(BookSaveDirectory);
|
||||
saveFileDialog.InitialFileName = PictureFileName;
|
||||
saveFileDialog.Directory
|
||||
= !App.IsWindows ? null
|
||||
: Directory.Exists(BookSaveDirectory) ? BookSaveDirectory
|
||||
: Path.GetDirectoryName(BookSaveDirectory);
|
||||
|
||||
var fileName = await saveFileDialog.ShowAsync(this);
|
||||
|
||||
31
Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml
Normal file
@@ -0,0 +1,31 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="165"
|
||||
MinHeight="165" MaxHeight="165"
|
||||
MinWidth="800" MaxWidth="800"
|
||||
x:Class="LibationAvalonia.Dialogs.LibationFilesDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
Title="Book Details"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
<Grid
|
||||
RowDefinitions="Auto,Auto">
|
||||
|
||||
<controls:DirectoryOrCustomSelectControl
|
||||
Grid.Row="0"
|
||||
Margin="5"
|
||||
Directory="{Binding Directory, Mode=TwoWay}"
|
||||
SubDirectory=""
|
||||
KnownDirectories="{Binding KnownDirectories}" />
|
||||
|
||||
<Button
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="5"
|
||||
Padding="30,3,30,3"
|
||||
Content="Save"
|
||||
Click="SaveButton_Click" />
|
||||
</Grid>
|
||||
</Window>
|
||||
55
Source/LibationAvalonia/Dialogs/LibationFilesDialog.axaml.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using LibationFileManager;
|
||||
using LibationAvalonia.Controls;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class LibationFilesDialog : Window
|
||||
{
|
||||
private class DirSelectOptions
|
||||
{
|
||||
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
|
||||
{
|
||||
Configuration.KnownDirectories.UserProfile,
|
||||
Configuration.KnownDirectories.AppDir,
|
||||
Configuration.KnownDirectories.MyDocs
|
||||
};
|
||||
|
||||
public string Directory { get; set; } = Configuration.GetKnownDirectoryPath(Configuration.KnownDirectories.UserProfile);
|
||||
}
|
||||
private DirSelectOptions dirSelectOptions;
|
||||
public string SelectedDirectory => dirSelectOptions.Directory;
|
||||
public LibationFilesDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
DataContext = dirSelectOptions = new();
|
||||
}
|
||||
|
||||
public async void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
|
||||
var libationDir = dirSelectOptions.Directory;
|
||||
|
||||
if (!System.IO.Directory.Exists(libationDir))
|
||||
{
|
||||
await MessageBox.Show("Not saving change to Libation Files location. This folder does not exist:\r\n" + libationDir, "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error, saveAndRestorePosition: false);
|
||||
return;
|
||||
}
|
||||
|
||||
Close(DialogResult.OK);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="120"
|
||||
xmlns:controls="clr-namespace:LibationWinForms.AvaloniaUI.Controls"
|
||||
x:Class="LibationWinForms.AvaloniaUI.Views.Dialogs.LiberatedStatusBatchDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
x:Class="LibationAvalonia.Dialogs.LiberatedStatusBatchDialog"
|
||||
Title="Liberated status: Whether the book has been downloaded"
|
||||
MinWidth="400" MinHeight="120"
|
||||
MaxWidth="400" MaxHeight="120"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/AvaloniaUI/Assets/libation.ico">
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto">
|
||||
|
||||
@@ -3,7 +3,7 @@ using DataLayer;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LibationWinForms.AvaloniaUI.Views.Dialogs
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class LiberatedStatusBatchDialog : DialogWindow
|
||||
{
|
||||
@@ -0,0 +1,33 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="240" d:DesignHeight="120"
|
||||
MinWidth="240" MinHeight="120"
|
||||
MaxWidth="240" MaxHeight="120"
|
||||
x:Class="LibationAvalonia.Dialogs.Login.ApprovalNeededDialog"
|
||||
Title="Approval Alert Detected"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Margin="10"
|
||||
TextWrapping="Wrap"
|
||||
Text="Amazon is sending you an email."/>
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1" Margin="10,0,10,0"
|
||||
TextWrapping="Wrap"
|
||||
Text="Please press this button after you've approved the notification."/>
|
||||
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Margin="10"
|
||||
VerticalAlignment="Bottom"
|
||||
Padding="30,3,30,3"
|
||||
Content="Approve"
|
||||
Click="Approve_Click" />
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,30 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
public partial class ApprovalNeededDialog : DialogWindow
|
||||
{
|
||||
public ApprovalNeededDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
protected override Task SaveAndCloseAsync()
|
||||
{
|
||||
Serilog.Log.Logger.Information("Approve button clicked");
|
||||
|
||||
return base.SaveAndCloseAsync();
|
||||
}
|
||||
|
||||
public async void Approve_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await SaveAndCloseAsync();
|
||||
}
|
||||
}
|
||||
22
Source/LibationAvalonia/Dialogs/Login/AvaloniaLoginBase.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
public abstract class AvaloniaLoginBase
|
||||
{
|
||||
|
||||
/// <returns>True if ShowDialog's DialogResult == OK</returns>
|
||||
protected static async Task<bool> ShowDialog(DialogWindow dialog)
|
||||
{
|
||||
if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
|
||||
return false;
|
||||
|
||||
var result = await dialog.ShowDialog<DialogResult>(desktop.MainWindow);
|
||||
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
|
||||
return result == DialogResult.OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleUtilities;
|
||||
|
||||
namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
public class AvaloniaLoginCallback : AvaloniaLoginBase, ILoginCallback
|
||||
{
|
||||
private Account _account { get; }
|
||||
|
||||
public AvaloniaLoginCallback(Account account)
|
||||
{
|
||||
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
}
|
||||
|
||||
public async Task<string> Get2faCodeAsync()
|
||||
{
|
||||
var dialog = new _2faCodeDialog();
|
||||
if (await ShowDialog(dialog))
|
||||
return dialog.Code;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<string> GetCaptchaAnswerAsync(byte[] captchaImage)
|
||||
{
|
||||
var dialog = new CaptchaDialog(captchaImage);
|
||||
if (await ShowDialog(dialog))
|
||||
return dialog.Answer;
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
|
||||
{
|
||||
var dialog = new MfaDialog(mfaConfig);
|
||||
if (await ShowDialog(dialog))
|
||||
return (dialog.SelectedName, dialog.SelectedValue);
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
public async Task<(string email, string password)> GetLoginAsync()
|
||||
{
|
||||
var dialog = new LoginCallbackDialog(_account);
|
||||
if (await ShowDialog(dialog))
|
||||
return (_account.AccountId, dialog.Password);
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
public async Task ShowApprovalNeededAsync()
|
||||
{
|
||||
var dialog = new ApprovalNeededDialog();
|
||||
await ShowDialog(dialog);
|
||||
}
|
||||
}
|
||||
}
|
||||