Compare commits

...

63 Commits

Author SHA1 Message Date
Robert McRackan
f04a5e0168 tweaks to getLatestRelease 2022-07-27 16:36:18 -04:00
rmcrackan
e093729707 Merge pull request #325 from Mbucari/master
Add app update to Avalonia Build
2022-07-27 15:49:41 -04:00
Michael Bucari-Tovo
369151ada2 Revert timeout time 2022-07-27 09:55:09 -06:00
Michael Bucari-Tovo
1f685ae8a0 Add release index download 2022-07-27 09:49:58 -06:00
Mbucari
bbe91099cb Update .releaseindex.json 2022-07-27 09:45:29 -06:00
Mbucari
92015ba4c2 Add files via upload 2022-07-27 09:27:57 -06:00
Mbucari
3bcacabadc Delete appcasttest.xml 2022-07-27 01:03:24 -06:00
Mbucari
f5736d9151 Merge branch 'rmcrackan:master' into master 2022-07-27 00:46:04 -06:00
Michael Bucari-Tovo
59015f438e Add auto app update to windows avalonia 2022-07-27 00:36:13 -06:00
Michael Bucari-Tovo
3af47ab395 Add update name pattern matching 2022-07-27 00:35:55 -06:00
Michael Bucari-Tovo
308619b01a Fix bug if MessageBox called from worker thread 2022-07-27 00:30:35 -06:00
Robert McRackan
4efce57488 gitignore bin-Avalonia 2022-07-26 22:15:23 -04:00
Robert McRackan
c8ee950f7d Linux beta 2022-07-26 21:14:43 -04:00
Mbucari
0bba0f9256 Add files via upload 2022-07-26 12:53:49 -06:00
rmcrackan
05bdff5123 Merge pull request #321 from Mbucari/master
Linux Beta
2022-07-26 08:16:55 -04:00
Mbucari
e58e6cfb9f Update README.md 2022-07-25 19:51:35 -06:00
Mbucari
b052871004 Update README.md 2022-07-25 19:48:01 -06:00
Mbucari
d738f4f35f Update README.md 2022-07-25 17:33:59 -06:00
Michael Bucari-Tovo
7286aee9dd Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-25 17:22:29 -06:00
Michael Bucari-Tovo
ca455978a5 Update for Linux 2022-07-25 17:22:17 -06:00
Mbucari
9c38bea5b7 Update README.md 2022-07-25 17:21:35 -06:00
Michael Bucari-Tovo
fbec1bc569 Linux pub 2022-07-25 14:45:39 -06:00
Michael Bucari-Tovo
6dd885f0b2 Wrap save and restore in tyy/catch blocks 2022-07-25 08:19:46 -06:00
Mbucari
ab38eb5571 Update README.md 2022-07-25 00:05:46 -06:00
Michael Bucari-Tovo
0e4b9ab396 Build standalone 2022-07-25 00:04:52 -06:00
Michael Bucari-Tovo
7dfedbc73b Remove beta checkbox 2022-07-24 16:51:38 -06:00
Michael Bucari-Tovo
625ae1d63c Removed Avalonia from LibationWinForms 2022-07-24 16:42:38 -06:00
Michael Bucari-Tovo
71098ef02f Publish Profiles 2022-07-24 16:33:45 -06:00
Mbucari
d63a6de543 Update README.md 2022-07-24 16:33:19 -06:00
Mbucari
2a71a85306 Update README.md 2022-07-24 16:19:03 -06:00
Michael Bucari-Tovo
6de3a8a2bf Linux instructions 2022-07-24 16:14:34 -06:00
Michael Bucari-Tovo
3fc1da66de Linux compat 2022-07-24 14:46:27 -06:00
Michael Bucari-Tovo
683c221ca8 Linux compatability 2022-07-24 14:18:26 -06:00
Michael Bucari-Tovo
fe6cfc899b Add Avalonia setup 2022-07-24 13:04:19 -06:00
Michael Bucari-Tovo
ffd947eb2e A 2022-07-23 21:04:27 -06:00
Michael Bucari-Tovo
8dd59cb08a Refactor 2022-07-23 20:54:02 -06:00
Michael Bucari-Tovo
1e4c489983 Libation Runs on Linux! 2022-07-23 18:07:04 -06:00
Michael Bucari-Tovo
17b0da358f Add LinkLabel control 2022-07-22 20:11:13 -06:00
Michael Bucari-Tovo
6aa0a1f8b9 Remove references to winforms 2022-07-22 19:28:31 -06:00
Michael Bucari-Tovo
ab731a63af Tweak MessageBox 2022-07-22 19:20:47 -06:00
Michael Bucari-Tovo
07d2c656fc Add description text 2022-07-22 18:33:49 -06:00
Michael Bucari-Tovo
9ecb32c3d2 Added login dialogs 2022-07-22 18:25:47 -06:00
Michael Bucari-Tovo
503e1e143e Separate invalid char check for folders and files. Files can't have slashes. 2022-07-22 18:11:39 -06:00
Mbucari
e34ce67a2c Merge branch 'rmcrackan:master' into master 2022-07-22 12:39:09 -06:00
Robert McRackan
a0fd0a3de6 Book details dialog. On open, tags should be first focus 2022-07-22 11:15:04 -04:00
Robert McRackan
7f3cbc454f Bug fix #319 : in some cases mp3 chapter metadata was incorrect 2022-07-21 22:29:12 -04:00
Mbucari
30eb117fa1 Merge branch 'rmcrackan:master' into master 2022-07-21 10:05:12 -06:00
Robert McRackan
63877160aa New feature #170 : book details, added link to audible's page for that book 2022-07-21 09:02:42 -04:00
Robert McRackan
77e61479cf New feature #284 : Add bitrate, sample rate, and channels to template options and to exports 2022-07-21 08:37:04 -04:00
Michael Bucari-Tovo
ca71283108 Revert 2022-07-20 20:10:07 -06:00
Michael Bucari-Tovo
285563af5e Revert 2022-07-20 20:08:53 -06:00
Michael Bucari-Tovo
62cbad0d8f Commit works in progress 2022-07-20 19:41:56 -06:00
Michael Bucari-Tovo
2cb2479d63 Added EditTemplateDialog and LibationFilesDialog 2022-07-20 13:35:30 -06:00
Mbucari
e7c5b1d8dc Merge branch 'rmcrackan:master' into master 2022-07-19 14:27:11 -06:00
Robert McRackan
7f086aeaac Bug fix #318: Audible changed their API, likely in conjunction with shutting down the Windows App. DownloadQuality.Extreme and DownloadQuality.Low now throw errors 2022-07-19 15:02:31 -04:00
Robert McRackan
78186d4973 update dependencies 2022-07-19 09:32:26 -04:00
Robert McRackan
4d84174ba6 AudibleApi: Make exceptions more flexible so that less logic is needed inside catch 2022-07-19 08:19:03 -04:00
Robert McRackan
579536f65a Revert: project publish => sln publish 2022-07-19 07:43:34 -04:00
Michael Bucari-Tovo
a4ff739684 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-18 23:01:20 -06:00
Michael Bucari-Tovo
9e06c70319 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-18 23:00:55 -06:00
Michael Bucari-Tovo
0c98ce000b Added SettingsDialog 2022-07-18 23:00:40 -06:00
Robert McRackan
230b23dc80 on startup, log BetaOptIn 2022-07-18 22:15:11 -04:00
Robert McRackan
d55b8eeeba Turn 'self contained' back on 2022-07-18 20:56:09 -04:00
205 changed files with 5075 additions and 2253 deletions

1
.gitignore vendored
View File

@@ -370,3 +370,4 @@ FodyWeavers.xsd
/__TODO.txt
/DataLayer/LibationContext.db
*/bin-Avalonia

5
.releaseindex.json Normal file
View File

@@ -0,0 +1,5 @@
{
"WindowsClassic": "Libation\\.\\d+\\.\\d+\\.\\d+-win-classic\\.zip",
"WindowsAvalonia":"Libation\\.\\d+\\.\\d+\\.\\d+-win-chardonnay\\.zip",
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay"
}

View File

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

View File

@@ -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" />

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<Version>8.2.0.1</Version>
<TargetFramework>net6.0</TargetFramework>
<Version>8.3.0.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" />

View File

@@ -289,6 +289,7 @@ namespace AppScaffolding
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled(),
config.BetaOptIn,
config.LibationFiles,
AudibleFileStorage.BooksDirectory,
@@ -308,10 +309,17 @@ namespace AppScaffolding
LibraryCommands.BookUserDefinedItemCommitted += (_, books) => SearchEngineCommands.UpdateBooks(books);
}
public static UpgradeProperties GetLatestRelease()
public enum ReleaseIdentifier
{
WindowsClassic,
WindowsAvalonia,
LinuxAvalonia
}
public static UpgradeProperties GetLatestRelease(ReleaseIdentifier releaseID = ReleaseIdentifier.WindowsClassic)
{
// timed out
var latest = getLatestRelease(TimeSpan.FromSeconds(10));
var latest = getLatestRelease(TimeSpan.FromSeconds(10), releaseID);
if (latest is null)
return null;
@@ -336,11 +344,11 @@ namespace AppScaffolding
return new(zipUrl, latest.HtmlUrl, zip.Name, latestRelease);
}
private static Octokit.Release getLatestRelease(TimeSpan timeout)
private static Octokit.Release getLatestRelease(TimeSpan timeout, ReleaseIdentifier releaseID)
{
try
{
var task = System.Threading.Tasks.Task.Run(() => getLatestRelease());
var task = getLatestRelease(releaseID);
if (task.Wait(timeout))
return task.Result;
@@ -352,13 +360,23 @@ namespace AppScaffolding
}
return null;
}
private static Octokit.Release getLatestRelease()
private static async System.Threading.Tasks.Task<Octokit.Release> getLatestRelease(ReleaseIdentifier releaseID)
{
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>(releaseID.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);
var releases = await gitHubClient.Repository.Release.GetAll(ownerAccount, repoName);
var regex = new System.Text.RegularExpressions.Regex(regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
var latest = releases.FirstOrDefault(r => !r.Draft && !r.Prerelease && r.Assets.Any(a => regex.IsMatch(a.Name)));
return latest;
}
}

View File

@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="27.2.1" />
<PackageReference Include="CsvHelper" Version="28.0.1" />
<PackageReference Include="NPOI" Version="2.5.6" />
</ItemGroup>

View File

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

View File

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

View File

@@ -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.3.1.1" />
<PackageReference Include="AudibleApi" Version="4.5.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -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>

View File

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

View File

@@ -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'">

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net6.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
</PropertyGroup>
</Project>

View File

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

View File

@@ -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>

View File

@@ -0,0 +1,228 @@
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 bool IsWindows => PlatformID is PlatformID.Win32NT;
public static bool IsUnix => PlatformID is PlatformID.Unix;
public static readonly PlatformID PlatformID = Environment.OSVersion.Platform;
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 Uri("avares://Libation/Assets/");
public static Stream OpenAsset(string assetRelativePath)
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
public static bool GoToFile(string path)
=> PlatformID is PlatformID.Win32NT ? Go.To.File(path)
: GoToFolder(path is null ? string.Empty : Path.GetDirectoryName(path));
public static bool GoToFolder(string path)
{
if (PlatformID is PlatformID.Win32NT)
return Go.To.Folder(path);
else
{
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;
}
}
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 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) ||
!RunInstall(setupDialog))
{
CancelInstallation();
return;
}
// most migrations go in here
AppScaffolding.LibationScaffolding.RunPostConfigMigrations(setupDialog.Config);
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
{
MessageBox.ShowAdminAlert(null, body, title, ex);
}
catch
{
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 bool RunInstall(SetupDialog setupDialog)
{
var config = setupDialog.Config;
if (setupDialog.IsNewUser)
{
config.SetLibationFiles(Configuration.UserProfile);
}
else if (setupDialog.IsReturningUser)
{
var libationFilesDialog = new LibationFilesDialog();
if (libationFilesDialog.ShowDialogSynchronously<DialogResult>(setupDialog) != DialogResult.OK)
return false;
config.SetLibationFiles(libationFilesDialog.SelectedDirectory);
if (config.LibationSettingsAreValid)
return true;
// path did not result in valid settings
var continueResult = 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 new SettingsDialog().ShowDialogSynchronously<DialogResult>(setupDialog) == DialogResult.OK
&& config.LibationSettingsAreValid;
}
static void CancelInstallation()
{
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");
}
}
}

View File

Before

Width:  |  Height:  |  Size: 95 B

After

Width:  |  Height:  |  Size: 95 B

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

View File

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

View File

Before

Width:  |  Height:  |  Size: 747 B

After

Width:  |  Height:  |  Size: 747 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 482 B

After

Width:  |  Height:  |  Size: 482 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 383 B

After

Width:  |  Height:  |  Size: 383 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,28 @@
using Avalonia.Media;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace LibationAvalonia
{
internal static class AvaloniaUtils
{
public static IBrush GetBrushFromResources(string name)
=> GetBrushFromResources(name, Brushes.Transparent);
public static IBrush GetBrushFromResources(string name, IBrush defaultBrush)
{
if (App.Current.Styles.TryGetResource(name, out var value) && value is IBrush brush)
return brush;
return defaultBrush;
}
public static T ShowDialogSynchronously<T>(this Avalonia.Controls.Window window, Avalonia.Controls.Window owner)
{
using var source = new CancellationTokenSource();
var dialogTask = window.ShowDialog<T>(owner);
dialogTask.ContinueWith(t => source.Cancel(), TaskScheduler.FromCurrentSynchronizationContext());
Avalonia.Threading.Dispatcher.UIThread.MainLoop(source.Token);
return dialogTask.Result;
}
}
}

View File

@@ -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 >

View File

@@ -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
{

View File

@@ -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>

View File

@@ -0,0 +1,178 @@
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 path1
= customStates.CustomChecked ? customStates.CustomDir
: directorySelectControl.SelectedDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute
: Configuration.GetKnownDirectoryPath(directorySelectControl.SelectedDirectory);
Directory
= System.IO.Path.Combine(path1 ?? string.Empty, 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 = noSubDir;
}
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);
}
}
}

View File

@@ -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>

View File

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

View File

@@ -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>

View File

@@ -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
{

View 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>

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

View 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>

View File

@@ -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
{

View File

@@ -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>

View File

@@ -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)
@@ -134,7 +136,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
if (persister.AccountsSettings.Accounts.Any(a => a.AccountId == account.AccountId && a.IdentityTokens.Locale.Name == account.Locale.Name))
{
await MessageBox.Show(this, $"An account with that account id and country already exists.\r\n\r\nAccount ID: {account.AccountId}\r\nCountry: {account.Locale.Name}", "Cannot Add Duplicate Account");
MessageBox.Show(this, $"An account with that account id and country already exists.\r\n\r\nAccount ID: {account.AccountId}\r\nCountry: {account.Locale.Name}", "Cannot Add Duplicate Account");
return;
}
@@ -144,7 +146,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(
MessageBox.ShowAdminAlert(
this,
$"An error occurred while importing an account from:\r\n{filePath[0]}\r\n\r\nIs the file encrypted?",
"Error Importing Account",
@@ -158,12 +160,11 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
Export(acc);
}
protected override async Task SaveAndCloseAsync()
protected override void SaveAndClose()
{
try
{
if (!await inputIsValid())
if (!inputIsValid())
return;
// without transaction, accounts persister will write ANY EDIT immediately to file
@@ -177,7 +178,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(this, "Error attempting to save accounts", "Error saving accounts", ex);
MessageBox.ShowAdminAlert(this, "Error attempting to save accounts", "Error saving accounts", ex);
}
}
@@ -190,8 +191,6 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
AvaloniaXamlLoader.Load(this);
}
private void persist(AccountsSettings accountsSettings)
{
var existingAccounts = accountsSettings.Accounts;
@@ -222,7 +221,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
: dto.AccountName.Trim();
}
}
private async Task<bool> inputIsValid()
private bool inputIsValid()
{
foreach (var dto in Accounts.ToList())
{
@@ -234,13 +233,13 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
if (string.IsNullOrWhiteSpace(dto.AccountId))
{
await MessageBox.Show(this, "Account id cannot be blank. Please enter an account id for all accounts.", "Blank account", MessageBoxButtons.OK, MessageBoxIcon.Error);
MessageBox.Show(this, "Account id cannot be blank. Please enter an account id for all accounts.", "Blank account", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
if (string.IsNullOrWhiteSpace(dto.SelectedLocale?.Name))
{
await MessageBox.Show(this, "Please select a locale (i.e.: country or region) for all accounts.", "Blank region", MessageBoxButtons.OK, MessageBoxIcon.Error);
MessageBox.Show(this, "Please select a locale (i.e.: country or region) for all accounts.", "Blank region", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}
@@ -260,7 +259,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
if (account.IdentityTokens?.IsValid != true)
{
await MessageBox.Show(this, "This account hasn't been authenticated yet. First scan your library to log into your account, then try exporting again.", "Account Not Authenticated");
MessageBox.Show(this, "This account hasn't been authenticated yet. First scan your library to log into your account, then try exporting again.", "Account Not Authenticated");
return;
}
@@ -283,11 +282,11 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
File.WriteAllText(fileName, jsonText);
await MessageBox.Show(this, $"Successfully exported {account.AccountName} to\r\n\r\n{fileName}", "Success!");
MessageBox.Show(this, $"Successfully exported {account.AccountName} to\r\n\r\n{fileName}", "Success!");
}
catch (Exception ex)
{
await MessageBox.ShowAdminAlert(
MessageBox.ShowAdminAlert(
this,
$"An error occurred while exporting account:\r\n{account.AccountName}",
"Error Exporting Account",

View File

@@ -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&#xa;Audible&#xa;(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>

View File

@@ -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)
{

View File

@@ -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">

View File

@@ -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
{

View File

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

View File

@@ -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>

View File

@@ -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
{

View 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&#xa;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>

View File

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

View 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>

View 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 (!_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 bool Validate()
{
if (template.IsValid(workingTemplateText))
return true;
var errors = template
.GetErrors(workingTemplateText)
.Select(err => $"- {err}")
.Aggregate((a, b) => $"{a}\r\n{b}");
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);
}
}
}
}
}
}

View File

@@ -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>

View File

@@ -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.IsUnix ? null
: Directory.Exists(BookSaveDirectory) ? BookSaveDirectory
: Path.GetDirectoryName(BookSaveDirectory);
var fileName = await saveFileDialog.ShowAsync(this);
@@ -64,7 +67,7 @@ namespace LibationWinForms.AvaloniaUI.Views.Dialogs
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"Failed to save picture to {fileName}");
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);
MessageBox.Show(this, $"An error was encountered while trying to save the picture\r\n\r\n{ex.Message}", "Failed to save picture", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
}
}

View File

@@ -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>

View 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 void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var libationDir = dirSelectOptions.Directory;
if (!System.IO.Directory.Exists(libationDir))
{
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);
}
}
}

View File

@@ -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">

View File

@@ -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
{

View File

@@ -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>

View File

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

View 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 bool ShowDialog(DialogWindow dialog)
{
if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
return false;
var result = dialog.ShowDialogSynchronously<DialogResult>(desktop.MainWindow);
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
return result == DialogResult.OK;
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
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 string Get2faCode()
{
var dialog = new _2faCodeDialog();
if (ShowDialog(dialog))
return dialog.Code;
return null;
}
public string GetCaptchaAnswer(byte[] captchaImage)
{
var dialog = new CaptchaDialog(captchaImage);
if (ShowDialog(dialog))
return dialog.Answer;
return null;
}
public (string name, string value) GetMfaChoice(MfaConfig mfaConfig)
{
var dialog = new MfaDialog(mfaConfig);
if (ShowDialog(dialog))
return (dialog.SelectedName, dialog.SelectedValue);
return (null, null);
}
public (string email, string password) GetLogin()
{
var dialog = new LoginCallbackDialog(_account);
if (ShowDialog(dialog))
return (_account.AccountId, dialog.Password);
return (null, null);
}
public void ShowApprovalNeeded()
{
var dialog = new ApprovalNeededDialog();
ShowDialog(dialog);
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Threading.Tasks;
using AudibleApi;
using AudibleUtilities;
namespace LibationAvalonia.Dialogs.Login
{
public class AvaloniaLoginChoiceEager : AvaloniaLoginBase, ILoginChoiceEager
{
/// <summary>Convenience method. Recommended when wiring up Winforms to <see cref="ApplicationServices.LibraryCommands.ImportAccountAsync"/></summary>
public static async Task<ApiExtended> ApiExtendedFunc(Account account) => await ApiExtended.CreateAsync(account, new AvaloniaLoginChoiceEager(account));
public ILoginCallback LoginCallback { get; private set; }
private Account _account { get; }
public AvaloniaLoginChoiceEager(Account account)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
LoginCallback = new AvaloniaLoginCallback(_account);
}
public ChoiceOut Start(ChoiceIn choiceIn)
{
var dialog = new LoginChoiceEagerDialog(_account);
if (!ShowDialog(dialog))
return null;
switch (dialog.LoginMethod)
{
case LoginMethod.Api:
return ChoiceOut.WithApi(dialog.Account.AccountId, dialog.Password);
case LoginMethod.External:
{
var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
return ShowDialog(externalDialog)
? ChoiceOut.External(externalDialog.ResponseUrl)
: null;
}
default:
throw new Exception($"Unknown {nameof(LoginMethod)} value");
}
}
}
}

View File

@@ -0,0 +1,54 @@
<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="220" d:DesignHeight="180"
MinWidth="220" MinHeight="180"
MaxWidth="220" MaxHeight="180"
x:Class="LibationAvalonia.Dialogs.Login.CaptchaDialog"
Title="CAPTCHA"
Icon="/Assets/libation.ico">
<Grid
RowDefinitions="Auto,Auto,*"
ColumnDefinitions="Auto,*">
<Panel
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="10"
MinWidth="200"
MinHeight="70"
Background="LightGray">
<Image
Stretch="None"
Source="{Binding CaptchaImage}" />
</Panel>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="10,0,10,0"
VerticalAlignment="Center"
Text="CAPTCHA&#xa;answer:" />
<TextBox
Grid.Row="1"
Grid.Column="1"
Margin="10,0,10,0" Text="{Binding Answer}" />
<Button
Grid.Row="2"
Grid.Column="1"
Margin="10"
Padding="0,5,0,5"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Content="Submit"
Click="Submit_Click" />
</Grid>
</Window>

View File

@@ -0,0 +1,40 @@
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using System.IO;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs.Login
{
public partial class CaptchaDialog : DialogWindow
{
public string Answer { get; set; }
public Bitmap CaptchaImage { get; }
public CaptchaDialog()
{
InitializeComponent();
}
public CaptchaDialog(byte[] captchaImage) :this()
{
using var ms = new MemoryStream(captchaImage);
CaptchaImage = new Bitmap(ms);
DataContext = this;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Answer });
return base.SaveAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
}

Some files were not shown because too many files have changed in this diff Show More