Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3272541e81 | ||
|
|
3b3d40e4e6 | ||
|
|
a47866b6f7 | ||
|
|
0df4dfdef5 | ||
|
|
fe2de6ecf7 | ||
|
|
fc25e73b1a | ||
|
|
a3df85c87e | ||
|
|
553a936e7e | ||
|
|
635764625e | ||
|
|
f5599f7c57 | ||
|
|
dc6aaf2dd6 | ||
|
|
f1ba2b4ae8 | ||
|
|
742310b8d6 | ||
|
|
073787173d | ||
|
|
66679ace2f | ||
|
|
3982537d46 | ||
|
|
7cf4c63d79 | ||
|
|
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 | ||
|
|
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,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<Version>8.2.4.1</Version>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Version>8.3.6.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="1.0.0" />
|
||||
<PackageReference Include="Octokit" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Logging;
|
||||
using LibationFileManager;
|
||||
@@ -14,8 +15,33 @@ using Serilog;
|
||||
|
||||
namespace AppScaffolding
|
||||
{
|
||||
public enum ReleaseIdentifier
|
||||
{
|
||||
None,
|
||||
WindowsClassic,
|
||||
WindowsAvalonia,
|
||||
LinuxAvalonia,
|
||||
MacOSAvalonia
|
||||
}
|
||||
|
||||
// I know I'm taking the wine metaphor a bit far by naming this "Variety", but I don't know what else to call it
|
||||
public enum VarietyType { None, Classic, Chardonnay }
|
||||
|
||||
public static class LibationScaffolding
|
||||
{
|
||||
public static bool IsWindows { get; } = OperatingSystem.IsWindows();
|
||||
public static bool IsLinux { get; } = OperatingSystem.IsLinux();
|
||||
public static bool IsMacOs { get; } = OperatingSystem.IsMacOS();
|
||||
|
||||
public static ReleaseIdentifier ReleaseIdentifier { get; private set; }
|
||||
public static VarietyType Variety
|
||||
=> ReleaseIdentifier == ReleaseIdentifier.WindowsClassic ? VarietyType.Classic
|
||||
: ReleaseIdentifier.In(ReleaseIdentifier.WindowsAvalonia, ReleaseIdentifier.LinuxAvalonia, ReleaseIdentifier.MacOSAvalonia) ? VarietyType.Chardonnay
|
||||
: VarietyType.None;
|
||||
|
||||
public static void SetReleaseIdentifier(ReleaseIdentifier releaseID)
|
||||
=> ReleaseIdentifier = releaseID;
|
||||
|
||||
// AppScaffolding
|
||||
private static Assembly _executingAssembly;
|
||||
private static Assembly ExecutingAssembly
|
||||
@@ -91,20 +117,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 +301,20 @@ namespace AppScaffolding
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
mode += " (Debugger attached)";
|
||||
|
||||
string OS
|
||||
= IsLinux ? "Linux"
|
||||
: IsMacOs ? "MacOS"
|
||||
: IsWindows ? "Windows"
|
||||
: "UNKNOWN_OS";
|
||||
|
||||
// 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,
|
||||
OS,
|
||||
Mode = mode,
|
||||
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
||||
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
|
||||
@@ -312,8 +346,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 +360,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 +372,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 +386,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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace AppScaffolding
|
||||
: value;
|
||||
|
||||
#region appsettings.json
|
||||
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName), "appsettings.json");
|
||||
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "appsettings.json");
|
||||
|
||||
public static bool APPSETTINGS_Json_Exists => File.Exists(APPSETTINGS_JSON);
|
||||
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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.5.0.1" />
|
||||
<PackageReference Include="AudibleApi" Version="4.6.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -10,7 +10,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="4.1.1.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="5.1.0.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="5.0.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -29,10 +30,6 @@
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LibationFileManager\LibationFileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="migrate.json">
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
|
||||
@@ -30,10 +30,6 @@ namespace DataLayer
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
Book = book;
|
||||
|
||||
// import previously saved tags
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(book.AudibleProductId, nameof(book.AudibleProductId));
|
||||
Tags = LibationFileManager.TagsPersistence.GetTags(book.AudibleProductId);
|
||||
}
|
||||
|
||||
#region Tags
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using DataLayer.Configurations;
|
||||
using Dinah.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
public class LibationContext : InterceptableDbContext
|
||||
public class LibationContext : DbContext
|
||||
{
|
||||
// IMPORTANT: USING DbSet<>
|
||||
// ========================
|
||||
@@ -35,14 +34,6 @@ namespace DataLayer
|
||||
// see DesignTimeDbContextFactoryBase for info about ctors and connection strings/OnConfiguring()
|
||||
internal LibationContext(DbContextOptions options) : base(options) { }
|
||||
|
||||
// called on each instantiation
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
AddInterceptor(new TagPersistenceInterceptor());
|
||||
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
}
|
||||
|
||||
// typically only called once per execution; NOT once per instantiation
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
internal class TagPersistenceInterceptor : IDbInterceptor
|
||||
{
|
||||
public void Executed(DbContext context) { }
|
||||
|
||||
public void Executing(DbContext context)
|
||||
{
|
||||
var tagsCollection
|
||||
= context
|
||||
.ChangeTracker
|
||||
.Entries()
|
||||
.Where(e => e.State.In(EntityState.Modified, EntityState.Added))
|
||||
.Select(e => e.Entity as UserDefinedItem)
|
||||
.Where(udi => udi is not null)
|
||||
// do NOT filter out entires with blank tags. blank is the valid way to show the absence of tags
|
||||
.Select(t => (t.Book.AudibleProductId, t.Tags))
|
||||
.ToList();
|
||||
|
||||
LibationFileManager.TagsPersistence.Save(tagsCollection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="4.4.1.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="5.1.0.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
using ApplicationServices;
|
||||
using AppScaffolding;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Hangover
|
||||
{
|
||||
public partial class Form1
|
||||
{
|
||||
private string dbFile;
|
||||
|
||||
private void Load_databaseTab()
|
||||
{
|
||||
dbFile = UNSAFE_MigrationHelper.DatabaseFile;
|
||||
if (dbFile is null)
|
||||
{
|
||||
databaseFileLbl.Text = $"Database file not found";
|
||||
return;
|
||||
}
|
||||
|
||||
databaseFileLbl.Text = $"Database file: {UNSAFE_MigrationHelper.DatabaseFile ?? "not found"}";
|
||||
}
|
||||
|
||||
private void databaseTab_VisibleChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (!databaseTab.Visible)
|
||||
return;
|
||||
}
|
||||
|
||||
private void sqlExecuteBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
ensureBackup();
|
||||
|
||||
sqlResultsTb.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var sql = sqlTb.Text.Trim();
|
||||
|
||||
#region // explanation
|
||||
// Routing statements to non-query is a convenience.
|
||||
// I went down the rabbit hole of full parsing and it's more trouble than it's worth. The parsing is easy due to available libraries. The edge cases of what to do next got too complex for slight gains.
|
||||
// It's also not useful to take the extra effort to separate non-queries which don't return a row count. Eg: alter table, drop table
|
||||
// My half-assed solution here won't even catch simple mistakes like this -- and that's ok
|
||||
// -- line 1 is a comment
|
||||
// delete from foo
|
||||
#endregion
|
||||
var lower = sql.ToLower();
|
||||
if (lower.StartsWith("update") || lower.StartsWith("insert") || lower.StartsWith("delete"))
|
||||
nonQuery(sql);
|
||||
else
|
||||
query(sql);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sqlResultsTb.Text = $"{ex.Message}\r\n{ex.StackTrace}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
deleteUnneededBackups();
|
||||
}
|
||||
}
|
||||
|
||||
private string dbBackup;
|
||||
private DateTime dbFileLastModified;
|
||||
private void ensureBackup()
|
||||
{
|
||||
if (dbBackup is not null)
|
||||
return;
|
||||
|
||||
dbFileLastModified = File.GetLastWriteTimeUtc(dbFile);
|
||||
|
||||
dbBackup
|
||||
= Path.ChangeExtension(dbFile, "").TrimEnd('.')
|
||||
+ $"_backup_{DateTime.UtcNow:O}".Replace(':', '-').Replace('.', '-')
|
||||
+ Path.GetExtension(dbFile);
|
||||
File.Copy(dbFile, dbBackup);
|
||||
}
|
||||
|
||||
private void deleteUnneededBackups()
|
||||
{
|
||||
var newLastModified = File.GetLastWriteTimeUtc(dbFile);
|
||||
if (dbFileLastModified == newLastModified)
|
||||
{
|
||||
File.Delete(dbBackup);
|
||||
dbBackup = null;
|
||||
}
|
||||
}
|
||||
|
||||
void query(string sql)
|
||||
{
|
||||
// ef doesn't support truly generic queries. have to drop down to ado.net
|
||||
using var context = DbContexts.GetContext();
|
||||
using var conn = context.Database.GetDbConnection();
|
||||
conn.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
|
||||
var reader = cmd.ExecuteReader();
|
||||
var results = 0;
|
||||
var builder = new System.Text.StringBuilder();
|
||||
var lines = 0;
|
||||
while (reader.Read())
|
||||
{
|
||||
results++;
|
||||
|
||||
for (var i = 0; i < reader.FieldCount; i++)
|
||||
builder.Append(reader.GetValue(i) + "\t");
|
||||
builder.AppendLine();
|
||||
|
||||
lines++;
|
||||
if (lines % 10 == 0)
|
||||
{
|
||||
sqlResultsTb.AppendText(builder.ToString());
|
||||
builder.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
sqlResultsTb.AppendText(builder.ToString());
|
||||
builder.Clear();
|
||||
|
||||
if (results == 0)
|
||||
sqlResultsTb.Text = "[no results]";
|
||||
else
|
||||
{
|
||||
sqlResultsTb.AppendText($"\r\n{results} result");
|
||||
if (results != 1) sqlResultsTb.AppendText("s");
|
||||
}
|
||||
}
|
||||
|
||||
void nonQuery(string sql)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var results = context.Database.ExecuteSqlRaw(sql);
|
||||
|
||||
sqlResultsTb.AppendText($"{results} record");
|
||||
if (results != 1) sqlResultsTb.AppendText("s");
|
||||
sqlResultsTb.AppendText(" affected");
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Source/HangoverAvalonia/App.axaml
Normal file
@@ -0,0 +1,12 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:HangoverAvalonia"
|
||||
x:Class="HangoverAvalonia.App">
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme Mode="Light"/>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
29
Source/HangoverAvalonia/App.axaml.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using HangoverAvalonia.ViewModels;
|
||||
using HangoverAvalonia.Views;
|
||||
|
||||
namespace HangoverAvalonia
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow
|
||||
{
|
||||
DataContext = new MainWindowViewModel(),
|
||||
};
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
85
Source/HangoverAvalonia/HangoverAvalonia.csproj
Normal file
@@ -0,0 +1,85 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 -->
|
||||
<TrimMode>copyused</TrimMode>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<IsPublishable>true</IsPublishable>
|
||||
<AssemblyName>Hangover</AssemblyName>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<StartupObject />
|
||||
<IsPublishable>true</IsPublishable>
|
||||
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
|
||||
<ApplicationIcon>hangover.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
HACK FOR COMPILER BUG 2021-09-14. Hopefully will be fixed in future versions
|
||||
- Not using SatelliteResourceLanguages will load all language packs: works
|
||||
- Specifying 'en' semicolon 1 more should load 1 language pack: works
|
||||
- Specifying only 'en' should load no language packs: broken, still loads all
|
||||
-->
|
||||
<SatelliteResourceLanguages>en;es</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>..\bin\Avalonia\Debug</OutputPath>
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>..\bin\Avalonia\Release</OutputPath>
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
<AvaloniaXaml Remove="Models\**" />
|
||||
<Compile Remove="Models\**" />
|
||||
<EmbeddedResource Remove="Models\**" />
|
||||
<None Remove="Models\**" />
|
||||
<None Remove=".gitignore" />
|
||||
<None Remove="Assets\hangover.ico" />
|
||||
<None Remove="hangover.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="hangover.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!--This helps with theme dll-s trimming.
|
||||
If you will publish your application in self-contained mode with p:PublishTrimmed=true and it will use Fluent theme Default theme will be trimmed from the output and vice versa.
|
||||
https://github.com/AvaloniaUI/Avalonia/issues/5593 -->
|
||||
<TrimmableAssembly Include="Avalonia.Themes.Fluent" />
|
||||
<TrimmableAssembly Include="Avalonia.Themes.Default" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="0.10.17" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.17" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="0.10.17" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.17" />
|
||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.3.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<Target Name="SpicNSpan" AfterTargets="Clean">
|
||||
<!-- Remove obj folder -->
|
||||
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
|
||||
<!-- Remove bin folder -->
|
||||
<RemoveDir Directories="$(BaseOutputPath)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
24
Source/HangoverAvalonia/Program.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.ReactiveUI;
|
||||
using System;
|
||||
|
||||
namespace HangoverAvalonia
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||
// yet and stuff might break.
|
||||
[STAThread]
|
||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.LogToTrace()
|
||||
.UseReactiveUI();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Release\linux-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Release\macos-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -5,12 +5,13 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
<PublishDir>..\bin\publish\</PublishDir>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Release\win-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,9 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using LibationWinForms.AvaloniaUI.ViewModels;
|
||||
using HangoverAvalonia.ViewModels;
|
||||
using System;
|
||||
|
||||
namespace LibationWinForms.AvaloniaUI
|
||||
namespace HangoverAvalonia
|
||||
{
|
||||
public class ViewLocator : IDataTemplate
|
||||
{
|
||||
37
Source/HangoverAvalonia/ViewModels/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using HangoverBase;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace HangoverAvalonia.ViewModels
|
||||
{
|
||||
public class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
private DatabaseTab _tab;
|
||||
|
||||
private string _databaseFileText;
|
||||
private bool _databaseFound;
|
||||
private string _sqlResults;
|
||||
public string DatabaseFileText { get => _databaseFileText; set => this.RaiseAndSetIfChanged(ref _databaseFileText, value); }
|
||||
public string SqlQuery { get; set; }
|
||||
public bool DatabaseFound { get => _databaseFound; set => this.RaiseAndSetIfChanged(ref _databaseFound, value); }
|
||||
public string SqlResults { get => _sqlResults; set => this.RaiseAndSetIfChanged(ref _sqlResults, value); }
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
_tab = new(new(() => SqlQuery, s => SqlResults = s, s => SqlResults = s));
|
||||
|
||||
_tab.LoadDatabaseFile();
|
||||
if (_tab.DbFile is null)
|
||||
{
|
||||
DatabaseFileText = $"Database file not found";
|
||||
DatabaseFound = false;
|
||||
return;
|
||||
}
|
||||
|
||||
DatabaseFileText = $"Database file: {_tab.DbFile}";
|
||||
DatabaseFound = true;
|
||||
}
|
||||
|
||||
public void ExecuteQuery() => _tab.ExecuteQuery();
|
||||
}
|
||||
}
|
||||
11
Source/HangoverAvalonia/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace HangoverAvalonia.ViewModels
|
||||
{
|
||||
public class ViewModelBase : ReactiveObject
|
||||
{
|
||||
}
|
||||
}
|
||||
75
Source/HangoverAvalonia/Views/MainWindow.axaml
Normal file
@@ -0,0 +1,75 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:HangoverAvalonia.ViewModels"
|
||||
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="500"
|
||||
Width="800" Height="500"
|
||||
x:Class="HangoverAvalonia.Views.MainWindow"
|
||||
Icon="/Assets/hangover.ico "
|
||||
Title="Hangover: Libation debug and recovery tool">
|
||||
|
||||
<Design.DataContext>
|
||||
<vm:MainWindowViewModel/>
|
||||
</Design.DataContext>
|
||||
|
||||
<TabControl Grid.Row="0">
|
||||
<TabControl.Styles>
|
||||
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
|
||||
<Setter Property="Height" Value="18"/>
|
||||
</Style>
|
||||
<Style Selector="TabItem">
|
||||
<Setter Property="MinHeight" Value="30"/>
|
||||
<Setter Property="Height" Value="30"/>
|
||||
<Setter Property="Padding" Value="8,2,8,0"/>
|
||||
</Style>
|
||||
<Style Selector="TabItem#Header TextBlock">
|
||||
<Setter Property="MinHeight" Value="5"/>
|
||||
</Style>
|
||||
</TabControl.Styles>
|
||||
|
||||
<!-- Database Tab -->
|
||||
<TabItem>
|
||||
<TabItem.Header>
|
||||
<TextBlock FontSize="14" VerticalAlignment="Center">Database</TextBlock>
|
||||
</TabItem.Header>
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto,2*">
|
||||
|
||||
<TextBlock
|
||||
Margin="0,10,0,5"
|
||||
Grid.Row="0"
|
||||
Text="{Binding DatabaseFileText}" />
|
||||
|
||||
<TextBlock
|
||||
Margin="0,5,0,5"
|
||||
Grid.Row="1"
|
||||
Text="SQL (database command)" />
|
||||
|
||||
<TextBox
|
||||
Margin="0,5,0,5"
|
||||
Grid.Row="2" Text="{Binding SqlQuery, Mode=OneWayToSource}" />
|
||||
|
||||
<Button
|
||||
Grid.Row="3"
|
||||
Padding="20,5,20,5"
|
||||
Content="Execute"
|
||||
IsEnabled="{Binding DatabaseFound}"
|
||||
Click="Execute_Click" />
|
||||
|
||||
<TextBox
|
||||
Margin="0,5,0,10"
|
||||
IsReadOnly="True"
|
||||
Grid.Row="4"
|
||||
Text="{Binding SqlResults}" />
|
||||
</Grid>
|
||||
|
||||
</TabItem>
|
||||
<!-- Command Line Interface Tab -->
|
||||
<TabItem>
|
||||
<TabItem.Header>
|
||||
<TextBlock FontSize="14" VerticalAlignment="Center">Command Line Interface</TextBlock>
|
||||
</TabItem.Header>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Window>
|
||||
19
Source/HangoverAvalonia/Views/MainWindow.axaml.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Avalonia.Controls;
|
||||
using HangoverAvalonia.ViewModels;
|
||||
|
||||
namespace HangoverAvalonia.Views
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
MainWindowViewModel _viewModel => DataContext as MainWindowViewModel;
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void Execute_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
_viewModel.ExecuteQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
154
Source/HangoverBase/DatabaseTab.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System.Text;
|
||||
using ApplicationServices;
|
||||
using AppScaffolding;
|
||||
using Dinah.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HangoverBase
|
||||
{
|
||||
public class DatabaseTabCommands
|
||||
{
|
||||
public Func<string> SqlInput { get; }
|
||||
public Action<string> SqlOutputAppend { get; }
|
||||
public Action<string> SqlOutputOverwrite { get; }
|
||||
|
||||
public DatabaseTabCommands() { }
|
||||
public DatabaseTabCommands(
|
||||
Func<string> sqlInput,
|
||||
Action<string> sqlDisplayAppend,
|
||||
Action<string> sqlDisplayOverwrite)
|
||||
{
|
||||
SqlInput = ArgumentValidator.EnsureNotNull(sqlInput, nameof(sqlInput));
|
||||
SqlOutputAppend = ArgumentValidator.EnsureNotNull(sqlDisplayAppend, nameof(sqlDisplayAppend));
|
||||
SqlOutputOverwrite = ArgumentValidator.EnsureNotNull(sqlDisplayOverwrite, nameof(sqlDisplayOverwrite));
|
||||
}
|
||||
}
|
||||
|
||||
public class DatabaseTab
|
||||
{
|
||||
private DatabaseTabCommands _commands { get; }
|
||||
public string DbFile { get; private set; }
|
||||
|
||||
public DatabaseTab(DatabaseTabCommands commands)
|
||||
{
|
||||
_commands = ArgumentValidator.EnsureNotNull(commands, nameof(commands));
|
||||
|
||||
ArgumentValidator.EnsureNotNull(commands, nameof(_commands.SqlInput));
|
||||
ArgumentValidator.EnsureNotNull(commands, nameof(_commands.SqlOutputAppend));
|
||||
ArgumentValidator.EnsureNotNull(commands, nameof(_commands.SqlOutputOverwrite));
|
||||
}
|
||||
|
||||
public void LoadDatabaseFile() => DbFile = UNSAFE_MigrationHelper.DatabaseFile;
|
||||
|
||||
private string dbBackup;
|
||||
private DateTime dbFileLastModified;
|
||||
public void EnsureBackup()
|
||||
{
|
||||
if (dbBackup is not null)
|
||||
return;
|
||||
|
||||
dbFileLastModified = File.GetLastWriteTimeUtc(DbFile);
|
||||
|
||||
dbBackup
|
||||
= Path.ChangeExtension(DbFile, "").TrimEnd('.')
|
||||
+ $"_backup_{DateTime.UtcNow:O}".Replace(':', '-').Replace('.', '-')
|
||||
+ Path.GetExtension(DbFile);
|
||||
File.Copy(DbFile, dbBackup);
|
||||
}
|
||||
|
||||
public void ExecuteQuery()
|
||||
{
|
||||
EnsureBackup();
|
||||
|
||||
_commands.SqlOutputOverwrite("");
|
||||
|
||||
try
|
||||
{
|
||||
var sql = _commands.SqlInput().Trim();
|
||||
|
||||
#region // explanation
|
||||
// Routing statements to non-query is a convenience.
|
||||
// I went down the rabbit hole of full parsing and it's more trouble than it's worth. The parsing is easy due to available libraries. The edge cases of what to do next got too complex for slight gains.
|
||||
// It's also not useful to take the extra effort to separate non-queries which don't return a row count. Eg: alter table, drop table
|
||||
// My half-assed solution here won't even catch simple mistakes like this -- and that's ok
|
||||
// -- line 1 is a comment
|
||||
// delete from foo
|
||||
#endregion
|
||||
var lower = sql.ToLower();
|
||||
if (lower.StartsWith("update") || lower.StartsWith("insert") || lower.StartsWith("delete"))
|
||||
NonQuery(sql);
|
||||
else
|
||||
Query(sql);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_commands.SqlOutputOverwrite($"{ex.Message}\r\n{ex.StackTrace}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
DeleteUnneededBackups();
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteUnneededBackups()
|
||||
{
|
||||
var newLastModified = File.GetLastWriteTimeUtc(DbFile);
|
||||
if (dbFileLastModified == newLastModified)
|
||||
{
|
||||
File.Delete(dbBackup);
|
||||
dbBackup = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Query(string sql)
|
||||
{
|
||||
// ef doesn't support truly generic queries. have to drop down to ado.net
|
||||
using var context = DbContexts.GetContext();
|
||||
using var conn = context.Database.GetDbConnection();
|
||||
conn.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
|
||||
var reader = cmd.ExecuteReader();
|
||||
var results = 0;
|
||||
var builder = new StringBuilder();
|
||||
var lines = 0;
|
||||
while (reader.Read())
|
||||
{
|
||||
results++;
|
||||
|
||||
for (var i = 0; i < reader.FieldCount; i++)
|
||||
builder.Append(reader.GetValue(i) + "\t");
|
||||
builder.AppendLine();
|
||||
|
||||
lines++;
|
||||
if (lines % 10 == 0)
|
||||
{
|
||||
_commands.SqlOutputAppend(builder.ToString());
|
||||
builder.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
_commands.SqlOutputAppend(builder.ToString());
|
||||
builder.Clear();
|
||||
|
||||
if (results == 0)
|
||||
_commands.SqlOutputOverwrite("[no results]");
|
||||
else
|
||||
{
|
||||
_commands.SqlOutputAppend($"\r\n{results} result");
|
||||
if (results != 1) _commands.SqlOutputAppend("s");
|
||||
}
|
||||
}
|
||||
|
||||
public void NonQuery(string sql)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var results = context.Database.ExecuteSqlRaw(sql);
|
||||
|
||||
_commands.SqlOutputAppend($"{results} record");
|
||||
if (results != 1) _commands.SqlOutputAppend("s");
|
||||
_commands.SqlOutputAppend(" affected");
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Source/HangoverBase/HangoverBase.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,6 +1,6 @@
|
||||
using AppScaffolding;
|
||||
|
||||
namespace Hangover
|
||||
namespace HangoverWinForms
|
||||
{
|
||||
public partial class Form1
|
||||
{
|
||||
31
Source/HangoverWinForms/Form1.Database.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using HangoverBase;
|
||||
|
||||
namespace HangoverWinForms
|
||||
{
|
||||
public partial class Form1
|
||||
{
|
||||
private DatabaseTab _tab;
|
||||
|
||||
private void Load_databaseTab()
|
||||
{
|
||||
_tab = new(new(() => sqlTb.Text, sqlResultsTb.AppendText, s => sqlResultsTb.Text = s));
|
||||
|
||||
_tab.LoadDatabaseFile();
|
||||
if (_tab.DbFile is null)
|
||||
{
|
||||
databaseFileLbl.Text = $"Database file not found";
|
||||
return;
|
||||
}
|
||||
|
||||
databaseFileLbl.Text = $"Database file: {_tab.DbFile}";
|
||||
}
|
||||
|
||||
private void databaseTab_VisibleChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (!databaseTab.Visible)
|
||||
return;
|
||||
}
|
||||
|
||||
private void sqlExecuteBtn_Click(object sender, EventArgs e) => _tab.ExecuteQuery();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hangover
|
||||
namespace HangoverWinForms
|
||||
{
|
||||
partial class Form1
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hangover
|
||||
namespace HangoverWinForms
|
||||
{
|
||||
public partial class Form1 : Form
|
||||
{
|
||||
@@ -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>
|
||||
@@ -43,9 +44,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -54,4 +53,11 @@
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="SpicNSpan" AfterTargets="Clean">
|
||||
<!-- Remove obj folder -->
|
||||
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
|
||||
<!-- Remove bin folder -->
|
||||
<RemoveDir Directories="$(BaseOutputPath)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hangover
|
||||
namespace HangoverWinForms
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 498 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
BIN
Source/HangoverWinForms/hangover.ico
Normal file
|
After Width: | Height: | Size: 133 KiB |
@@ -8,8 +8,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solutio
|
||||
REFERENCE.txt = REFERENCE.txt
|
||||
_ARCHITECTURE NOTES.txt = _ARCHITECTURE NOTES.txt
|
||||
_DB_NOTES.txt = _DB_NOTES.txt
|
||||
_AvaloniaUI Primer.txt = _AvaloniaUI Primer.txt
|
||||
__README - COLLABORATORS.txt = __README - COLLABORATORS.txt
|
||||
__TODO.txt = __TODO.txt
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5 Domain Utilities (db aware)", "5 Domain Utilities (db aware)", "{41CDCC73-9B81-49DD-9570-C54406E852AF}"
|
||||
@@ -64,7 +64,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager.Tests", "_Tests
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationFileManager.Tests", "_Tests\LibationFileManager.Tests\LibationFileManager.Tests.csproj", "{EB781571-8548-477E-82AD-FB9FAB548D2F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangover", "Hangover\Hangover.csproj", "{40C67036-C1A7-4FDF-AA83-8EC902E257F3}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HangoverWinForms", "HangoverWinForms\HangoverWinForms.csproj", "{40C67036-C1A7-4FDF-AA83-8EC902E257F3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationAvalonia", "LibationAvalonia\LibationAvalonia.csproj", "{F612D06F-3134-4B9B-95CD-EB3FC798AE60}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HangoverAvalonia", "HangoverAvalonia\HangoverAvalonia.csproj", "{8A7B01D3-9830-44FD-91A1-D8D010996BEB}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HangoverBase", "HangoverBase\HangoverBase.csproj", "{5C7005BA-7D83-4E99-8073-D970943A7D61}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -144,6 +150,18 @@ 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
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -167,6 +185,9 @@ 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}
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61} = {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>
|
||||
238
Source/LibationAvalonia/App.axaml.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
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 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 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.Config.SetLibationFiles(Configuration.UserProfile);
|
||||
ShowSettingsWindow(desktop, setupDialog.Config, OnSettingsCompleted);
|
||||
}
|
||||
else if (setupDialog.IsReturningUser)
|
||||
{
|
||||
ShowLibationFilesDialog(desktop, setupDialog.Config, OnLibationFilesCompleted);
|
||||
}
|
||||
else
|
||||
{
|
||||
await CancelInstallation();
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunMigrationsAsync(Configuration config)
|
||||
{
|
||||
// most migrations go in here
|
||||
AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
|
||||
|
||||
await MessageBox.VerboseLoggingWarning_ShowIfTrue();
|
||||
|
||||
// logging is init'd here
|
||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
}
|
||||
|
||||
private void ShowSettingsWindow(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, SettingsDialog, Configuration> OnClose)
|
||||
{
|
||||
config.Books ??= Path.Combine(Configuration.UserProfile, "Books");
|
||||
|
||||
AppScaffolding.LibationScaffolding.PopulateMissingConfigValues(config);
|
||||
|
||||
var settingsDialog = new SettingsDialog();
|
||||
desktop.MainWindow = settingsDialog;
|
||||
settingsDialog.RestoreSizeAndLocation(Configuration.Instance);
|
||||
settingsDialog.Show();
|
||||
|
||||
void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
settingsDialog.Closing -= WindowClosing;
|
||||
e.Cancel = true;
|
||||
OnClose?.Invoke(desktop, settingsDialog, config);
|
||||
}
|
||||
settingsDialog.Closing += WindowClosing;
|
||||
}
|
||||
|
||||
private async void OnSettingsCompleted(IClassicDesktopStyleApplicationLifetime desktop, SettingsDialog settingsDialog, Configuration config)
|
||||
{
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
await RunMigrationsAsync(config);
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
|
||||
settingsDialog.Close();
|
||||
}
|
||||
|
||||
|
||||
private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, LibationFilesDialog, Configuration> OnClose)
|
||||
{
|
||||
var libationFilesDialog = new LibationFilesDialog();
|
||||
desktop.MainWindow = libationFilesDialog;
|
||||
libationFilesDialog.Show();
|
||||
|
||||
void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
libationFilesDialog.Closing -= WindowClosing;
|
||||
e.Cancel = true;
|
||||
OnClose?.Invoke(desktop, libationFilesDialog, config);
|
||||
}
|
||||
libationFilesDialog.Closing += WindowClosing;
|
||||
}
|
||||
|
||||
private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config)
|
||||
{
|
||||
config.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
await RunMigrationsAsync(config);
|
||||
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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)
|
||||
ShowSettingsWindow(desktop, config, OnSettingsCompleted);
|
||||
else
|
||||
await CancelInstallation();
|
||||
|
||||
}
|
||||
libationFilesDialog.Close();
|
||||
}
|
||||
|
||||
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 |