Compare commits

...

50 Commits

Author SHA1 Message Date
Robert McRackan
3272541e81 Audible changed how scanning works. You must upgrade to scan again 2022-08-02 21:20:14 -04:00
Robert McRackan
3b3d40e4e6 Add classic/chardonnay to About box 2022-08-02 14:40:58 -04:00
Robert McRackan
a47866b6f7 Open file/folder is now cross platform 2022-08-02 12:56:52 -04:00
Robert McRackan
0df4dfdef5 update dependencies 2022-08-02 09:14:36 -04:00
Robert McRackan
fe2de6ecf7 recommended: System.Environment.ProcessPath 2022-08-02 07:58:42 -04:00
Robert McRackan
fc25e73b1a attach avalonia primer notes 2022-08-01 20:56:08 -04:00
Robert McRackan
a3df85c87e Refactor hangover 2022-08-01 11:59:55 -04:00
Robert McRackan
553a936e7e incr ver 2022-08-01 09:49:45 -04:00
Robert McRackan
635764625e incl HangoverAvalonia in sln 2022-08-01 07:46:44 -04:00
rmcrackan
f5599f7c57 Merge pull request #338 from Mbucari/master
HangoverAvalonia and other fixes
2022-08-01 07:36:25 -04:00
Mbucari
dc6aaf2dd6 Updated instructions for Standalone Build (no dotnet runtime required) 2022-07-31 22:37:42 -06:00
Michael Bucari-Tovo
f1ba2b4ae8 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-31 22:24:28 -06:00
Michael Bucari-Tovo
742310b8d6 Fix install workflow 2022-07-31 22:24:17 -06:00
Mbucari
073787173d Merge branch 'rmcrackan:master' into master 2022-07-31 21:59:46 -06:00
Michael Bucari-Tovo
66679ace2f Add HangoverAvalonia 2022-07-31 20:33:56 -06:00
Robert McRackan
3982537d46 Tags no longer saved outside of database 2022-07-31 21:58:53 -04:00
Robert McRackan
7cf4c63d79 OSX-specific bug fix for search engine 2022-07-31 14:24:24 -04:00
Robert McRackan
7c4575cf66 incr ver 2022-07-30 22:39:41 -04:00
rmcrackan
f4749d703f Merge pull request #337 from Mbucari/master
Fixes and Improvements
2022-07-30 22:24:37 -04:00
Michael Bucari-Tovo
f2f562619b Updated dependencies 2022-07-30 20:09:17 -06:00
Robert McRackan
16c019a9c6 update dependencies 2022-07-30 21:54:10 -04:00
Robert McRackan
644dcbdd4d updated dependency 2022-07-30 21:40:31 -04:00
Michael Bucari-Tovo
6b112f5248 Delete obj and bin folders on clean 2022-07-30 18:03:33 -06:00
Michael Bucari-Tovo
0bfa609058 Libation Runs on MacOS 2022-07-30 16:09:31 -06:00
Michael Bucari-Tovo
8020ded642 Add platform preprocessor definitions 2022-07-30 13:42:11 -06:00
Michael Bucari-Tovo
c4cd6b16fc Add macOS ID 2022-07-30 11:04:01 -06:00
Michael Bucari-Tovo
310012fd17 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-30 11:03:19 -06:00
Michael Bucari-Tovo
06163db6ff Merge Conflict 2022-07-30 11:03:17 -06:00
Michael Bucari-Tovo
7689eed711 Add macOS identifier 2022-07-30 10:58:53 -06:00
Mbucari
d396d697d7 Fix typos 2022-07-30 10:33:04 -06:00
Michael Bucari-Tovo
27ed11d904 More universal updating 2022-07-30 09:49:11 -06:00
Michael Bucari-Tovo
9e7670b918 Fix Subdirectory being added to custom directory selection 2022-07-30 09:48:57 -06:00
Michael Bucari-Tovo
31e97defd1 Add ReleaseIdentifier to logging 2022-07-30 09:48:27 -06:00
Mbucari
1a447627c7 Merge branch 'rmcrackan:master' into master 2022-07-28 20:04:36 -06:00
Robert McRackan
962b386d07 Bug fix: update checking code 2022-07-28 21:35:59 -04:00
Michael Bucari-Tovo
d69ff24c2d Modularize update process 2022-07-28 17:18:43 -06:00
Michael Bucari-Tovo
070ed1d373 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-28 17:06:18 -06:00
Michael Bucari-Tovo
47729bf7b0 fix release getter 2022-07-28 17:06:12 -06:00
Robert McRackan
ed0ce2976b Bug fix #329 : Chardonnay-beta freezes after a scan 2022-07-28 13:26:16 -04:00
rmcrackan
2224f46ed5 Merge pull request #331 from Mbucari/master
Fix MessageBox hang
2022-07-28 13:22:03 -04:00
Michael Bucari-Tovo
433974323c Remove unnecessary extensions 2022-07-28 11:17:29 -06:00
Michael Bucari-Tovo
7525d318c0 Crean up helper methods 2022-07-28 11:03:22 -06:00
Michael Bucari-Tovo
92327dcc0d Add synchronous thread extensions 2022-07-28 10:40:39 -06:00
Michael Bucari-Tovo
aeaf234edd Merge branch 'master' of https://github.com/Mbucari/Libation 2022-07-28 10:13:28 -06:00
Michael Bucari-Tovo
a99b644917 Fix thread hang issue#329 2022-07-28 10:12:43 -06:00
Mbucari
d79a55e5c9 Merge branch 'rmcrackan:master' into master 2022-07-28 09:43:20 -06:00
Mbucari
16b0feeb82 Create feature request template 2022-07-28 09:43:11 -06:00
Mbucari
7b3a25e45a Create bug report template 2022-07-28 09:42:33 -06:00
Robert McRackan
8effdcb92d add macos publish options. standardize publish profiles 2022-07-28 10:43:00 -04:00
Robert McRackan
b12bef81bd These stupid unused language packs are 40% of our disk usage. And the SatelliteResourceLanguages bug *still* isn't fixed 2022-07-28 09:36:12 -04:00
100 changed files with 1345 additions and 770 deletions

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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.

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

View File

@@ -1,5 +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"
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay",
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-macos-chardonnay"
}

View File

@@ -2,15 +2,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Version>8.3.0.1</Version>
<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>

View File

@@ -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(),
@@ -309,18 +343,12 @@ namespace AppScaffolding
LibraryCommands.BookUserDefinedItemCommitted += (_, books) => SearchEngineCommands.UpdateBooks(books);
}
public enum ReleaseIdentifier
{
WindowsClassic,
WindowsAvalonia,
LinuxAvalonia
}
public static UpgradeProperties GetLatestRelease(ReleaseIdentifier releaseID = ReleaseIdentifier.WindowsClassic)
public static UpgradeProperties GetLatestRelease()
{
// timed out
var latest = getLatestRelease(TimeSpan.FromSeconds(10), releaseID);
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');
@@ -332,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
@@ -344,11 +372,11 @@ namespace AppScaffolding
return new(zipUrl, latest.HtmlUrl, zip.Name, latestRelease);
}
private static Octokit.Release getLatestRelease(TimeSpan timeout, ReleaseIdentifier releaseID)
private static (Octokit.Release, Octokit.ReleaseAsset) getLatestRelease(TimeSpan timeout)
{
try
{
var task = getLatestRelease(releaseID);
var task = getLatestRelease();
if (task.Wait(timeout))
return task.Result;
@@ -358,9 +386,9 @@ namespace AppScaffolding
{
Log.Logger.Error(aggEx, "Checking for new version too often");
}
return null;
return (null, null);
}
private static async System.Threading.Tasks.Task<Octokit.Release> getLatestRelease(ReleaseIdentifier releaseID)
private static async System.Threading.Tasks.Task<(Octokit.Release, Octokit.ReleaseAsset)> getLatestRelease()
{
var ownerAccount = "rmcrackan";
var repoName = "Libation";
@@ -370,14 +398,14 @@ namespace AppScaffolding
//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());
var regexPattern = releaseIndex.Value<string>(ReleaseIdentifier.ToString());
// https://octokitnet.readthedocs.io/en/latest/releases/
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;
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)));
}
}

View File

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

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="4.5.0.1" />
<PackageReference Include="AudibleApi" Version="4.6.3.1" />
</ItemGroup>
<ItemGroup>

View File

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

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using Dinah.Core;
using Microsoft.EntityFrameworkCore;
namespace DataLayer
{

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

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

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>..\bin\publish\linux-x64\</PublishDir>
<PublishDir>..\bin\Release\win-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>

View File

@@ -0,0 +1,30 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using HangoverAvalonia.ViewModels;
using System;
namespace HangoverAvalonia
{
public class ViewLocator : IDataTemplate
{
public IControl Build(object data)
{
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
else
{
return new TextBlock { Text = "Not Found: " + name };
}
}
public bool Match(object data)
{
return data is ViewModelBase;
}
}
}

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

View File

@@ -0,0 +1,11 @@
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Text;
namespace HangoverAvalonia.ViewModels
{
public class ViewModelBase : ReactiveObject
{
}
}

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

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

View File

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

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

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

View File

@@ -1,6 +1,6 @@
using AppScaffolding;
namespace Hangover
namespace HangoverWinForms
{
public partial class Form1
{

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

View File

@@ -1,4 +1,4 @@
namespace Hangover
namespace HangoverWinForms
{
partial class Form1
{

View File

@@ -1,4 +1,4 @@
namespace Hangover
namespace HangoverWinForms
{
public partial class Form1 : Form
{

View File

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

View File

@@ -1,4 +1,4 @@
namespace Hangover
namespace HangoverWinForms
{
internal static class Program
{

View File

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

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 498 B

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

@@ -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,10 +64,14 @@ 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
Debug|Any CPU = Debug|Any CPU
@@ -150,6 +154,14 @@ Global
{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
@@ -174,6 +186,8 @@ Global
{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}

View File

@@ -17,10 +17,6 @@ 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; }
@@ -29,35 +25,10 @@ namespace LibationAvalonia
public static IAssetLoader AssetLoader { get; private set; }
public static readonly Uri AssetUriBase = new Uri("avares://Libation/Assets/");
public static readonly Uri AssetUriBase = new("avares://Libation/Assets/");
public static Stream OpenAsset(string assetRelativePath)
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
public static bool GoToFile(string path)
=> 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);
@@ -74,7 +45,6 @@ namespace LibationAvalonia
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)
@@ -107,7 +77,7 @@ namespace LibationAvalonia
base.OnFrameworkInitializationCompleted();
}
private void Setup_Closing(object sender, System.ComponentModel.CancelEventArgs e)
private async void Setup_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
var setupDialog = sender as SetupDialog;
var desktop = ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
@@ -117,28 +87,21 @@ namespace LibationAvalonia
// all returns should be preceded by either:
// - if config.LibationSettingsAreValid
// - error message, Exit()
if ((!setupDialog.IsNewUser
&& !setupDialog.IsReturningUser) ||
!RunInstall(setupDialog))
if (setupDialog.IsNewUser)
{
CancelInstallation();
setupDialog.Config.SetLibationFiles(Configuration.UserProfile);
ShowSettingsWindow(desktop, setupDialog.Config, OnSettingsCompleted);
}
else if (setupDialog.IsReturningUser)
{
ShowLibationFilesDialog(desktop, setupDialog.Config, OnLibationFilesCompleted);
}
else
{
await 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)
{
@@ -146,63 +109,110 @@ namespace LibationAvalonia
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);
await 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);
await MessageBox.Show($"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
return;
}
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
ShowMainWindow(desktop);
}
private static bool RunInstall(SetupDialog setupDialog)
private async Task RunMigrationsAsync(Configuration config)
{
var config = setupDialog.Config;
// most migrations go in here
AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
if (setupDialog.IsNewUser)
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)
{
config.SetLibationFiles(Configuration.UserProfile);
settingsDialog.Closing -= WindowClosing;
e.Cancel = true;
OnClose?.Invoke(desktop, settingsDialog, config);
}
else if (setupDialog.IsReturningUser)
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();
var libationFilesDialog = new LibationFilesDialog();
settingsDialog.Close();
}
if (libationFilesDialog.ShowDialogSynchronously<DialogResult>(setupDialog) != DialogResult.OK)
return false;
config.SetLibationFiles(libationFilesDialog.SelectedDirectory);
if (config.LibationSettingsAreValid)
return true;
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 = MessageBox.Show(
var continueResult = await MessageBox.Show(
$"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
"New install?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (continueResult != DialogResult.Yes)
return false;
if (continueResult == DialogResult.Yes)
ShowSettingsWindow(desktop, config, OnSettingsCompleted);
else
await CancelInstallation();
}
// 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;
libationFilesDialog.Close();
}
static void CancelInstallation()
static async Task CancelInstallation()
{
MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
await MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
Environment.Exit(0);
}
@@ -225,4 +235,4 @@ namespace LibationAvalonia
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources("SeriesEntryGridBackgroundBrush");
}
}
}
}

View File

@@ -1,4 +1,5 @@
using Avalonia.Media;
using Avalonia.Threading;
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -15,14 +16,5 @@ namespace LibationAvalonia
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

@@ -38,6 +38,7 @@ namespace LibationAvalonia.Controls
set => SetValue(SubDirectoryProperty, value);
}
CustomState customStates = new();
public DirectoryOrCustomSelectControl()
{
InitializeComponent();
@@ -53,9 +54,8 @@ namespace LibationAvalonia.Controls
customDirBrowseBtn.Click += CustomDirBrowseBtn_Click;
PropertyChanged += DirectoryOrCustomSelectControl_PropertyChanged;
directorySelectControl.PropertyChanged += DirectorySelectControl_PropertyChanged;
}
private class CustomState: ViewModels.ViewModelBase
{
private string _customDir;
@@ -116,12 +116,13 @@ namespace LibationAvalonia.Controls
private void setDirectory()
{
var path1
var selectedDir
= 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);
selectedDir ??= string.Empty;
Directory = customStates.CustomChecked ? selectedDir : System.IO.Path.Combine(selectedDir, SubDirectory);
}
@@ -140,7 +141,7 @@ namespace LibationAvalonia.Controls
if (known is Configuration.KnownDirectories.None)
{
customStates.CustomChecked = true;
customStates.CustomDir = noSubDir;
customStates.CustomDir = directory;
}
else
{

View File

@@ -136,7 +136,7 @@ namespace LibationAvalonia.Dialogs
if (persister.AccountsSettings.Accounts.Any(a => a.AccountId == account.AccountId && a.IdentityTokens.Locale.Name == account.Locale.Name))
{
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");
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");
return;
}
@@ -146,7 +146,7 @@ namespace LibationAvalonia.Dialogs
}
catch (Exception ex)
{
MessageBox.ShowAdminAlert(
await 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",
@@ -160,11 +160,11 @@ namespace LibationAvalonia.Dialogs
Export(acc);
}
protected override void SaveAndClose()
protected override async Task SaveAndCloseAsync()
{
try
{
if (!inputIsValid())
if (!await inputIsValid())
return;
// without transaction, accounts persister will write ANY EDIT immediately to file
@@ -178,7 +178,7 @@ namespace LibationAvalonia.Dialogs
}
catch (Exception ex)
{
MessageBox.ShowAdminAlert(this, "Error attempting to save accounts", "Error saving accounts", ex);
await MessageBox.ShowAdminAlert(this, "Error attempting to save accounts", "Error saving accounts", ex);
}
}
@@ -221,7 +221,7 @@ namespace LibationAvalonia.Dialogs
: dto.AccountName.Trim();
}
}
private bool inputIsValid()
private async Task<bool> inputIsValid()
{
foreach (var dto in Accounts.ToList())
{
@@ -233,13 +233,13 @@ namespace LibationAvalonia.Dialogs
if (string.IsNullOrWhiteSpace(dto.AccountId))
{
MessageBox.Show(this, "Account id cannot be blank. Please enter an account id for all accounts.", "Blank account", MessageBoxButtons.OK, MessageBoxIcon.Error);
await 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))
{
MessageBox.Show(this, "Please select a locale (i.e.: country or region) for all accounts.", "Blank region", MessageBoxButtons.OK, MessageBoxIcon.Error);
await MessageBox.Show(this, "Please select a locale (i.e.: country or region) for all accounts.", "Blank region", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}
@@ -259,7 +259,7 @@ namespace LibationAvalonia.Dialogs
if (account.IdentityTokens?.IsValid != true)
{
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");
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");
return;
}
@@ -282,11 +282,11 @@ namespace LibationAvalonia.Dialogs
File.WriteAllText(fileName, jsonText);
MessageBox.Show(this, $"Successfully exported {account.AccountName} to\r\n\r\n{fileName}", "Success!");
await MessageBox.Show(this, $"Successfully exported {account.AccountName} to\r\n\r\n{fileName}", "Success!");
}
catch (Exception ex)
{
MessageBox.ShowAdminAlert(
await MessageBox.ShowAdminAlert(
this,
$"An error occurred while exporting account:\r\n{account.AccountName}",
"Error Exporting Account",

View File

@@ -64,7 +64,7 @@ namespace LibationAvalonia.Dialogs
}
protected override async Task SaveAndCloseAsync()
{
if (!_viewModel.Validate())
if (!await _viewModel.Validate())
return;
TemplateText = _viewModel.workingTemplateText;
@@ -115,7 +115,7 @@ namespace LibationAvalonia.Dialogs
public void resetTextBox(string value) => workingTemplateText = value;
public bool Validate()
public async Task<bool> Validate()
{
if (template.IsValid(workingTemplateText))
return true;
@@ -123,7 +123,7 @@ namespace LibationAvalonia.Dialogs
.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);
await MessageBox.Show($"This template text is not valid. Errors:\r\n{errors}", "Invalid", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}

View File

@@ -51,7 +51,7 @@ namespace LibationAvalonia.Dialogs
saveFileDialog.Filters.Add(new FileDialogFilter { Name = "Jpeg", Extensions = new System.Collections.Generic.List<string>() { "jpg" } });
saveFileDialog.InitialFileName = PictureFileName;
saveFileDialog.Directory
= App.IsUnix ? null
= !AppScaffolding.LibationScaffolding.IsWindows ? null
: Directory.Exists(BookSaveDirectory) ? BookSaveDirectory
: Path.GetDirectoryName(BookSaveDirectory);
@@ -67,7 +67,7 @@ namespace LibationAvalonia.Dialogs
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, $"Failed to save picture to {fileName}");
MessageBox.Show(this, $"An error was encountered while trying to save the picture\r\n\r\n{ex.Message}", "Failed to save picture", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
await MessageBox.Show(this, $"An error was encountered while trying to save the picture\r\n\r\n{ex.Message}", "Failed to save picture", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
}
}

View File

@@ -7,6 +7,7 @@
MinWidth="800" MaxWidth="800"
x:Class="LibationAvalonia.Dialogs.LibationFilesDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
WindowStartupLocation="CenterScreen"
Title="Book Details"
Icon="/Assets/libation.ico">

View File

@@ -23,6 +23,7 @@ namespace LibationAvalonia.Dialogs
}
private DirSelectOptions dirSelectOptions;
public string SelectedDirectory => dirSelectOptions.Directory;
public DialogResult DialogResult { get; private set; }
public LibationFilesDialog()
{
InitializeComponent();
@@ -33,18 +34,19 @@ namespace LibationAvalonia.Dialogs
DataContext = dirSelectOptions = new();
}
public void SaveButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
public async 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);
await MessageBox.Show("Not saving change to Libation Files location. This folder does not exist:\r\n" + libationDir, "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error, saveAndRestorePosition: false);
return;
}
Close(DialogResult.OK);
DialogResult = DialogResult.OK;
Close(DialogResult);
}
private void InitializeComponent()

View File

@@ -9,12 +9,12 @@ namespace LibationAvalonia.Dialogs.Login
{
/// <returns>True if ShowDialog's DialogResult == OK</returns>
protected static bool ShowDialog(DialogWindow dialog)
protected static async Task<bool> ShowDialog(DialogWindow dialog)
{
if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
return false;
var result = dialog.ShowDialogSynchronously<DialogResult>(desktop.MainWindow);
var result = await dialog.ShowDialog<DialogResult>(desktop.MainWindow);
Serilog.Log.Logger.Debug("{@DebugInfo}", new { DialogResult = result });
return result == DialogResult.OK;
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using AudibleApi;
using AudibleUtilities;
@@ -13,43 +14,43 @@ namespace LibationAvalonia.Dialogs.Login
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
}
public string Get2faCode()
public async Task<string> Get2faCodeAsync()
{
var dialog = new _2faCodeDialog();
if (ShowDialog(dialog))
if (await ShowDialog(dialog))
return dialog.Code;
return null;
}
public string GetCaptchaAnswer(byte[] captchaImage)
public async Task<string> GetCaptchaAnswerAsync(byte[] captchaImage)
{
var dialog = new CaptchaDialog(captchaImage);
if (ShowDialog(dialog))
if (await ShowDialog(dialog))
return dialog.Answer;
return null;
}
public (string name, string value) GetMfaChoice(MfaConfig mfaConfig)
public async Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
{
var dialog = new MfaDialog(mfaConfig);
if (ShowDialog(dialog))
if (await ShowDialog(dialog))
return (dialog.SelectedName, dialog.SelectedValue);
return (null, null);
}
public (string email, string password) GetLogin()
public async Task<(string email, string password)> GetLoginAsync()
{
var dialog = new LoginCallbackDialog(_account);
if (ShowDialog(dialog))
if (await ShowDialog(dialog))
return (_account.AccountId, dialog.Password);
return (null, null);
}
public void ShowApprovalNeeded()
public async Task ShowApprovalNeededAsync()
{
var dialog = new ApprovalNeededDialog();
ShowDialog(dialog);
await ShowDialog(dialog);
}
}
}

View File

@@ -20,11 +20,11 @@ namespace LibationAvalonia.Dialogs.Login
LoginCallback = new AvaloniaLoginCallback(_account);
}
public ChoiceOut Start(ChoiceIn choiceIn)
public async Task<ChoiceOut> StartAsync(ChoiceIn choiceIn)
{
var dialog = new LoginChoiceEagerDialog(_account);
if (!ShowDialog(dialog))
if (!await ShowDialog(dialog))
return null;
@@ -33,15 +33,16 @@ namespace LibationAvalonia.Dialogs.Login
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;
}
{
var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
return await ShowDialog(externalDialog)
? ChoiceOut.External(externalDialog.ResponseUrl)
: null;
}
default:
throw new Exception($"Unknown {nameof(LoginMethod)} value");
}
}
}
}

View File

@@ -51,7 +51,7 @@ namespace LibationAvalonia.Dialogs.Login
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { ResponseUrl });
if (!Uri.TryCreate(ResponseUrl, UriKind.Absolute, out var result))
{
MessageBox.Show("Invalid response URL");
await MessageBox.Show("Invalid response URL");
return;
}
await base.SaveAndCloseAsync();

View File

@@ -82,7 +82,7 @@ namespace LibationAvalonia.Dialogs.Login
});
if (selected is null)
{
MessageBox.Show("No MFA option selected", "None selected", MessageBoxButtons.OK, MessageBoxIcon.Error);
await MessageBox.Show("No MFA option selected", "None selected", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}

View File

@@ -28,7 +28,7 @@ namespace LibationAvalonia.Dialogs
DataContext = this;
}
private void GoToGithub_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private async void GoToGithub_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var url = "https://github.com/rmcrackan/Libation/issues";
try
@@ -37,11 +37,11 @@ namespace LibationAvalonia.Dialogs
}
catch
{
MessageBox.Show($"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error);
await MessageBox.Show($"Error opening url\r\n{url}", "Error opening url", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void GoToLogs_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
private async void GoToLogs_Tapped(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
LongPath dir = "";
try
@@ -52,11 +52,11 @@ namespace LibationAvalonia.Dialogs
try
{
App.GoToFolder(dir.ShortPathName);
Go.To.Folder(dir.ShortPathName);
}
catch
{
MessageBox.Show($"Error opening folder\r\n{dir}", "Error opening folder", MessageBoxButtons.OK, MessageBoxIcon.Error);
await MessageBox.Show($"Error opening folder\r\n{dir}", "Error opening folder", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}

View File

@@ -495,6 +495,7 @@
<RadioButton
Margin="0,5,0,5"
IsEnabled="{Binding AudioSettings.IsMp3Supported}"
IsChecked="{Binding AudioSettings.DecryptToLossy, Mode=TwoWay}">
<TextBlock
@@ -508,6 +509,7 @@
<StackPanel
Grid.Row="0"
IsVisible="{Binding AudioSettings.IsMp3Supported}"
Grid.Column="1">
<controls:GroupBox

View File

@@ -33,10 +33,10 @@ namespace LibationAvalonia.Dialogs
protected override async Task SaveAndCloseAsync()
{
if (!settingsDisp.SaveSettings(config))
if (!await settingsDisp.SaveSettingsAsync(config))
return;
MessageBox.VerboseLoggingWarning_ShowIfTrue();
await MessageBox.VerboseLoggingWarning_ShowIfTrue();
await base.SaveAndCloseAsync();
}
@@ -45,7 +45,7 @@ namespace LibationAvalonia.Dialogs
public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
App.GoToFolder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
}
public async void EditFolderTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
@@ -97,7 +97,7 @@ namespace LibationAvalonia.Dialogs
internal interface ISettingsDisplay
{
void LoadSettings(Configuration config);
bool SaveSettings(Configuration config);
Task<bool> SaveSettingsAsync(Configuration config);
}
public class SettingsPages : ISettingsDisplay
@@ -120,12 +120,12 @@ namespace LibationAvalonia.Dialogs
AudioSettings = new(config);
}
public bool SaveSettings(Configuration config)
public async Task<bool> SaveSettingsAsync(Configuration config)
{
var result = ImportantSettings.SaveSettings(config);
result &= ImportSettings.SaveSettings(config);
result &= DownloadDecryptSettings.SaveSettings(config);
result &= AudioSettings.SaveSettings(config);
var result = await ImportantSettings.SaveSettingsAsync(config);
result &= await ImportSettings.SaveSettingsAsync(config);
result &= await DownloadDecryptSettings.SaveSettingsAsync(config);
result &= await AudioSettings.SaveSettingsAsync(config);
return result;
}
@@ -146,13 +146,13 @@ namespace LibationAvalonia.Dialogs
BetaOptIn = config.BetaOptIn;
}
public bool SaveSettings(Configuration config)
public async Task<bool> SaveSettingsAsync(Configuration config)
{
#region validation
if (string.IsNullOrWhiteSpace(BooksDirectory))
{
MessageBox.Show("Cannot set Books Location to blank", "Location is blank", MessageBoxButtons.OK, MessageBoxIcon.Error);
await MessageBox.Show("Cannot set Books Location to blank", "Location is blank", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
@@ -204,14 +204,14 @@ namespace LibationAvalonia.Dialogs
AutoDownloadEpisodes = config.AutoDownloadEpisodes;
}
public bool SaveSettings(Configuration config)
public Task<bool> SaveSettingsAsync(Configuration config)
{
config.AutoScan = AutoScan;
config.ShowImportedStats = ShowImportedStats;
config.ImportEpisodes = ImportEpisodes;
config.DownloadEpisodes = DownloadEpisodes;
config.AutoDownloadEpisodes = AutoDownloadEpisodes;
return true;
return Task.FromResult(true);
}
public string AutoScanText { get; } = Configuration.GetDescription(nameof(Configuration.AutoScan));
@@ -259,25 +259,25 @@ namespace LibationAvalonia.Dialogs
: Configuration.GetKnownDirectory(config.InProgress);
}
public bool SaveSettings(Configuration config)
public async Task<bool> SaveSettingsAsync(Configuration config)
{
static void validationError(string text, string caption)
static Task validationError(string text, string caption)
=> MessageBox.Show(text, caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
// these 3 should do nothing. Configuration will only init these with a valid value. EditTemplateDialog ensures valid before returning
if (!Templates.Folder.IsValid(FolderTemplate))
{
validationError($"Not saving change to folder naming template. Invalid format.", "Invalid folder template");
await validationError($"Not saving change to folder naming template. Invalid format.", "Invalid folder template");
return false;
}
if (!Templates.File.IsValid(FileTemplate))
{
validationError($"Not saving change to file naming template. Invalid format.", "Invalid file template");
await validationError($"Not saving change to file naming template. Invalid format.", "Invalid file template");
return false;
}
if (!Templates.ChapterFile.IsValid(ChapterFileTemplate))
{
validationError($"Not saving change to chapter file naming template. Invalid format.", "Invalid chapter file template");
await validationError($"Not saving change to chapter file naming template. Invalid format.", "Invalid chapter file template");
return false;
}
@@ -381,6 +381,8 @@ namespace LibationAvalonia.Dialogs
private int _lameVBRQuality;
private string _chapterTitleTemplate;
public bool IsMp3Supported => AppScaffolding.LibationScaffolding.IsLinux || AppScaffolding.LibationScaffolding.IsWindows;
public AudioSettings(Configuration config)
{
LoadSettings(config);
@@ -405,7 +407,7 @@ namespace LibationAvalonia.Dialogs
LameVBRQuality = config.LameVBRQuality;
}
public bool SaveSettings(Configuration config)
public Task<bool> SaveSettingsAsync(Configuration config)
{
config.CreateCueSheet = CreateCueSheet;
config.AllowLibationFixup = AllowLibationFixup;
@@ -424,7 +426,7 @@ namespace LibationAvalonia.Dialogs
config.LameBitrate = LameBitrate;
config.LameVBRQuality = LameVBRQuality;
return true;
return Task.FromResult(true);
}
public string CreateCueSheetText { get; } = Configuration.GetDescription(nameof(Configuration.CreateCueSheet));

View File

@@ -113,7 +113,7 @@ namespace LibationAvalonia
public static void HideMinMaxBtns(this Window form)
{
if (Design.IsDesignMode || App.PlatformID is not PlatformID.Win32NT)
if (Design.IsDesignMode || !AppScaffolding.LibationScaffolding.IsWindows)
return;
var handle = form.PlatformImpl.Handle.Handle;
var currentStyle = GetWindowLong(handle, GWL_STYLE);

View File

@@ -10,19 +10,31 @@
<ApplicationIcon>libation.ico</ApplicationIcon>
<AssemblyName>Libation</AssemblyName>
<IsPublishable>true</IsPublishable>
<PublishReadyToRun>true</PublishReadyToRun>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<StartupObject />
</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>
<OutputPath>..\bin\Avalonia\Debug</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\bin-Avalonia\Release</OutputPath>
<OutputPath>..\bin\Avalonia\Release</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
@@ -71,8 +83,6 @@
<None Remove="Assets\SEGOEUI.TTF" />
<None Remove="Assets\up.png" />
<None Remove="Assets\WINGDING.TTF" />
<None Remove="MessageBox.cs~RF105afb8d.TMP" />
<None Remove="Views\MainWindow\MainWindow.Export.axaml.cs~RF10732d95.TMP" />
</ItemGroup>
<ItemGroup>
@@ -134,4 +144,12 @@
</None>
</ItemGroup>
<Target Name="SpicNSpan" AfterTargets="Clean">
<!-- Remove obj folder -->
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
<!-- Remove bin folder -->
<RemoveDir Directories="$(BaseOutputPath)" />
</Target>
</Project>

View File

@@ -9,6 +9,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using Avalonia.Threading;
namespace LibationAvalonia
{
@@ -60,34 +62,34 @@ namespace LibationAvalonia
public class MessageBox
{
public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
=> ShowCoreAsync(null, text, caption, buttons, icon, defaultButton);
public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, bool saveAndRestorePosition = true)
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, bool saveAndRestorePosition = true)
=> ShowCoreAsync(null, text, caption, buttons, icon, MessageBoxDefaultButton.Button1, saveAndRestorePosition);
public static DialogResult Show(string text, string caption, MessageBoxButtons buttons)
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons)
=> ShowCoreAsync(null, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static DialogResult Show(string text, string caption)
public static Task<DialogResult> Show(string text, string caption)
=> ShowCoreAsync(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static DialogResult Show(string text)
public static Task<DialogResult> Show(string text)
=> ShowCoreAsync(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static DialogResult Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
=> ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton);
public static DialogResult Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
=> ShowCoreAsync(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
public static DialogResult Show(Window owner, string text, string caption, MessageBoxButtons buttons)
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons)
=> ShowCoreAsync(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static DialogResult Show(Window owner, string text, string caption)
public static Task<DialogResult> Show(Window owner, string text, string caption)
=> ShowCoreAsync(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static DialogResult Show(Window owner, string text)
public static Task<DialogResult> Show(Window owner, string text)
=> ShowCoreAsync(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
public static void VerboseLoggingWarning_ShowIfTrue()
public static async Task VerboseLoggingWarning_ShowIfTrue()
{
// when turning on debug (and especially Verbose) to share logs, some privacy settings may not be obscured
if (Serilog.Log.Logger.IsVerboseEnabled())
Show(@"
await Show(@"
Warning: verbose logging is enabled.
This should be used for debugging only. It creates many
@@ -100,7 +102,7 @@ Libation.
".Trim(), "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
public static DialogResult ShowConfirmationDialog(Window owner, IEnumerable<LibraryBook> libraryBooks, string format, string title, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1)
public static async Task<DialogResult> ShowConfirmationDialog(Window owner, IEnumerable<LibraryBook> libraryBooks, string format, string title, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1)
{
if (libraryBooks is null || !libraryBooks.Any())
return DialogResult.Cancel;
@@ -115,7 +117,7 @@ Libation.
= string.Format(format, $"{thisThese} {count} {bookBooks}")
+ $"\r\n\r\n{titlesAgg}";
return ShowCoreAsync(owner,
return await ShowCoreAsync(owner,
message,
title,
MessageBoxButtons.YesNo,
@@ -130,7 +132,7 @@ Libation.
/// <param name="text">The text to display in the message box.</param>
/// <param name="caption">The text to display in the title bar of the message box.</param>
/// <param name="exception">Exception to log.</param>
public static void ShowAdminAlert(Window owner, string text, string caption, Exception exception)
public static async Task ShowAdminAlert(Window owner, string text, string caption, Exception exception)
{
// for development and debugging, show me what broke!
if (System.Diagnostics.Debugger.IsAttached)
@@ -144,16 +146,18 @@ Libation.
var form = new MessageBoxAlertAdminDialog(text, caption, exception);
DisplayWindow(form, owner);
await DisplayWindow(form, owner);
}
private static DialogResult ShowCoreAsync(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
=> Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => ShowCoreAsync2(owner, message, caption, buttons, icon, defaultButton, saveAndRestorePosition)).GetAwaiter().GetResult();
private static DialogResult ShowCoreAsync2(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
private static async Task<DialogResult> ShowCoreAsync(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
{
var dialog = await Dispatcher.UIThread.InvokeAsync(() => CreateMessageBox(owner, message, caption, buttons, icon, defaultButton, saveAndRestorePosition));
return await DisplayWindow(dialog, owner);
}
private static MessageBoxWindow CreateMessageBox(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
{
owner ??= (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).MainWindow;
var dialog = new MessageBoxWindow(saveAndRestorePosition);
@@ -186,16 +190,15 @@ Libation.
dialog.MaxWidth = dialog.MinWidth;
dialog.Height = dialog.MinHeight;
dialog.Width = dialog.MinWidth;
return DisplayWindow(dialog, owner);
return dialog;
}
private static DialogResult DisplayWindow(Window toDisplay, Window owner)
private static async Task<DialogResult> DisplayWindow(Window toDisplay, Window owner)
{
if (owner is null)
{
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
return toDisplay.ShowDialogSynchronously<DialogResult>(desktop.MainWindow);
return await toDisplay.ShowDialog<DialogResult>(desktop.MainWindow);
}
else
{
@@ -209,7 +212,7 @@ Libation.
};
window.Show();
var result = toDisplay.ShowDialogSynchronously<DialogResult>(window);
var result = await toDisplay.ShowDialog<DialogResult>(window);
window.Close();
return result;
}
@@ -217,7 +220,7 @@ Libation.
}
else
{
return toDisplay.ShowDialogSynchronously<DialogResult>(owner);
return await toDisplay.ShowDialog<DialogResult>(owner);
}
}

View File

@@ -14,7 +14,7 @@ namespace LibationAvalonia
{
private static string EXE_DIR = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
static async Task Main()
static void Main()
{
//***********************************************//
// //
@@ -30,6 +30,14 @@ namespace LibationAvalonia
var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime());
var appBuilderTask = Task.Run(BuildAvaloniaApp);
if (AppScaffolding.LibationScaffolding.IsWindows)
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.WindowsAvalonia);
else if (AppScaffolding.LibationScaffolding.IsLinux)
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.LinuxAvalonia);
else if (AppScaffolding.LibationScaffolding.IsMacOs)
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.MacOSAvalonia);
else return;
if (!App.SetupRequired)
{
@@ -39,9 +47,7 @@ namespace LibationAvalonia
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
}
(await appBuilderTask).SetupWithLifetime(await classicLifetimeTask);
(appBuilderTask.GetAwaiter().GetResult()).SetupWithLifetime(classicLifetimeTask.GetAwaiter().GetResult());
classicLifetimeTask.Result.Start(null);
}

View File

@@ -3,14 +3,15 @@
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>..\bin-Avalonia\publish\linux-x64\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
</PropertyGroup>
<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>

View File

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

View File

@@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>..\bin\publish\</PublishDir>
<PublishDir>..\bin\Release\win-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>

View File

@@ -4,54 +4,9 @@ This walkthrough should get you up and running with Libation on your Ubuntu mach
Some limitations of the linux release are:
- Cannot customize how illegial filename characters are replaced.
- The Auto-update function is unavailable
- The "Hangover" app for debugging is not yet available.
## Dependencies
### Dotnet Runtime
You must install the dotnet 6.0 runtime on your machine.
First, add the Microsoft package signing key to your list of trusted keys and add the package repository.
<details>
<summary>Ubuntu 22.04</summary>
```console
wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
```
</details>
<details>
<summary>Ubuntu 21.10</summary>
```console
wget https://packages.microsoft.com/config/ubuntu/21.10/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
```
</details>
<details>
<summary>Ubuntu 20.04</summary>
```console
wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
```
</details>
For other distributions, see [Microsoft's instructions for installing .NET on Linux](https://docs.microsoft.com/en-us/dotnet/core/install/linux).
Then install the dotnet 6.0 runtime
```console
sudo apt-get update; \
sudo apt-get install -y apt-transport-https && \
sudo apt-get update && \
sudo apt-get install -y dotnet-runtime-6.0
```
### FFMpeg (Optional)
If you want to convert your audiobooks to mp3, install FFMpeg using the following command:
@@ -67,8 +22,7 @@ Download the most recent linux-64 binaries zip file and save it as `libation-lin
<summary>install-libation.sh</summary>
```BASH
#!/bin/bash
#!/bin/bash
FILE=$1
@@ -77,10 +31,10 @@ Download the most recent linux-64 binaries zip file and save it as `libation-lin
exit
fi
if [ "$EUID" -ne 0 ]
then echo "Please run as root"
exit
fi
if [[ "$EUID" -ne 0 ]]
then echo "Please run as root"
exit
fi
if [ ! -f "$FILE" ]
then echo "The file \"$FILE\" does not exist."
@@ -99,12 +53,13 @@ fi
exit
fi
sudo -u $SUDO_USER chmod +700 ${FOLDER}/Libation
sudo -u $SUDO_USER chmod +700 ${FOLDER}/Hangover
sudo -u $SUDO_USER chmod +700 ${FOLDER}/LibationCli
#Remove previous installation program files and sym link
rm /usr/bin/Libation
rm /usr/bin/Hangover
rm /usr/bin/LibationCli
rm /usr/bin/libationcli
rm /usr/lib/libation -r
@@ -117,11 +72,11 @@ fi
chmod +666 /usr/share/icons/hicolor/scalable/apps/libation.svg
gtk-update-icon-cache -f /usr/share/icons/hicolor/
ln -s /usr/lib/libation/Libation /usr/bin/Libation
ln -s /usr/lib/libation/Hangover /usr/bin/Hangover
ln -s /usr/lib/libation/LibationCli /usr/bin/LibationCli
ln -s /usr/lib/libation/LibationCli /usr/bin/libationcli
echo "Done!"
```
</details>

View File

@@ -19,6 +19,7 @@ namespace LibationAvalonia.ViewModels
private int _visibleCount = 1;
private LibraryCommands.LibraryStats _libraryStats;
private int _visibleNotLiberated = 1;
public bool IsMp3Supported => AppScaffolding.LibationScaffolding.IsLinux || AppScaffolding.LibationScaffolding.IsWindows;
/// <summary> The Process Queue's viewmodel </summary>
public ProcessQueueViewModel ProcessQueue { get; } = new ProcessQueueViewModel();

View File

@@ -152,7 +152,7 @@ namespace LibationAvalonia.ViewModels
finally
{
if (Result == ProcessBookResult.None)
Result = showRetry(LibraryBook);
Result = await showRetry(LibraryBook);
Status = Result switch
{
@@ -313,7 +313,7 @@ namespace LibationAvalonia.ViewModels
#region Failure Handler
private ProcessBookResult showRetry(LibraryBook libraryBook)
private async Task<ProcessBookResult> showRetry(LibraryBook libraryBook)
{
Logger.Error("ERROR. All books have not been processed. Most recent book: processing failed");
@@ -346,7 +346,7 @@ $@" Title: {libraryBook.Book.Title}
}
// if null then ask user
dialogResult ??= MessageBox.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
dialogResult ??= await MessageBox.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
if (dialogResult == DialogResult.Abort)
return ProcessBookResult.FailedAbort;

View File

@@ -244,7 +244,7 @@ namespace LibationAvalonia.ViewModels
return;
var libraryBooks = selectedBooks.Select(rge => rge.LibraryBook).ToList();
var result = MessageBox.ShowConfirmationDialog(
var result = await MessageBox.ShowConfirmationDialog(
null,
libraryBooks,
$"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
@@ -317,7 +317,7 @@ namespace LibationAvalonia.ViewModels
}
catch (Exception ex)
{
MessageBox.ShowAdminAlert(
await MessageBox.ShowAdminAlert(
null,
"Error scanning library. You may still manually select books to remove from Libation's library.",
"Error scanning library",

View File

@@ -41,11 +41,11 @@ namespace LibationAvalonia.Views
break;
}
MessageBox.Show("Library exported to:\r\n" + fileName, "Library Exported");
await MessageBox.Show("Library exported to:\r\n" + fileName, "Library Exported");
}
catch (Exception ex)
{
MessageBox.ShowAdminAlert(this, "Error attempting to export your library.", "Error exporting", ex);
await MessageBox.ShowAdminAlert(this, "Error attempting to export your library.", "Error exporting", ex);
}
}
}

View File

@@ -39,7 +39,7 @@ namespace LibationAvalonia.Views
}
catch (Exception ex)
{
MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
await MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
// re-apply last good filter
await performFilter(lastGoodFilter);

View File

@@ -40,7 +40,7 @@ namespace LibationAvalonia.Views
public async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var result = MessageBox.Show(
var result = await MessageBox.Show(
"This converts all m4b titles in your library to mp3 files. Original files are not deleted."
+ "\r\nFor large libraries this will take a long time and will take up more disk space."
+ "\r\n\r\nContinue?"

View File

@@ -15,7 +15,7 @@ namespace LibationAvalonia.Views
SetQueueCollapseState(collapseState);
}
public void ProductsDisplay_LiberateClicked(object sender, LibraryBook libraryBook)
public async void ProductsDisplay_LiberateClicked(object sender, LibraryBook libraryBook)
{
try
{
@@ -36,10 +36,10 @@ namespace LibationAvalonia.Views
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
if (!App.GoToFile(filePath?.ShortPathName))
if (!Go.To.File(filePath?.ShortPathName))
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
MessageBox.Show($"File not found" + suffix);
await MessageBox.Show($"File not found" + suffix);
}
}
}

View File

@@ -26,7 +26,7 @@ namespace LibationAvalonia.Views
public async void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account");
await MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account");
await new Dialogs.AccountsDialog().ShowDialog(this);
}
@@ -66,11 +66,11 @@ namespace LibationAvalonia.Views
// this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop
if (Configuration.Instance.ShowImportedStats && newAdded > 0)
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
await MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
}
catch (Exception ex)
{
MessageBox.ShowAdminAlert(
await MessageBox.ShowAdminAlert(
this,
"Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator",
"Error importing library",

View File

@@ -8,11 +8,13 @@ namespace LibationAvalonia.Views
{
private void Configure_Settings() { }
public async void accountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => await new Dialogs.AccountsDialog().ShowDialog(this);
public async void accountsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await new Dialogs.AccountsDialog().ShowDialog(this);
public async void basicSettingsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) => await new Dialogs.SettingsDialog().ShowDialog(this);
public async void basicSettingsToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await new Dialogs.SettingsDialog().ShowDialog(this);
public void aboutToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> MessageBox.Show($"Running Libation version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
public async void aboutToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await MessageBox.Show($"Libation {AppScaffolding.LibationScaffolding.Variety}{Environment.NewLine}Version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
}
}

View File

@@ -47,7 +47,7 @@ namespace LibationAvalonia.Views
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = MessageBox.ShowConfirmationDialog(
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
"Are you sure you want to replace tags in {0}?",
@@ -70,7 +70,7 @@ namespace LibationAvalonia.Views
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = MessageBox.ShowConfirmationDialog(
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
"Are you sure you want to replace downloaded status in {0}?",
@@ -88,7 +88,7 @@ namespace LibationAvalonia.Views
{
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
var confirmationResult = MessageBox.ShowConfirmationDialog(
var confirmationResult = await MessageBox.ShowConfirmationDialog(
this,
visibleLibraryBooks,
"Are you sure you want to remove {0} from Libation's library?",

View File

@@ -65,7 +65,7 @@
</MenuItem.Styles>
<MenuItem Click="beginBookBackupsToolStripMenuItem_Click" Header="{Binding BookBackupsToolStripText}" />
<MenuItem Click="beginPdfBackupsToolStripMenuItem_Click" Header="{Binding PdfBackupsToolStripText}" />
<MenuItem Click="convertAllM4bToMp3ToolStripMenuItem_Click" Header="Convert all _M4b to Mp3 [Long-running]..." />
<MenuItem Click="convertAllM4bToMp3ToolStripMenuItem_Click" Header="Convert all _M4b to Mp3 [Long-running]..." IsVisible="{Binding IsMp3Supported}" />
<MenuItem Click="liberateVisible" Header="{Binding LiberateVisibleToolStripText}" IsEnabled="{Binding AnyVisibleNotLiberated}" />
</MenuItem>

View File

@@ -9,6 +9,7 @@ using LibationFileManager;
using DataLayer;
using System.Collections.Generic;
using System.Threading.Tasks;
using AppScaffolding;
namespace LibationAvalonia.Views
{
@@ -53,7 +54,7 @@ namespace LibationAvalonia.Views
this.LibraryLoaded += MainWindow_LibraryLoaded;
LibraryCommands.LibrarySizeChanged += async (_, _) => await _viewModel.ProductsDisplay.DisplayBooks(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
Closing += (_,_) => this.SaveSizeAndLocation(Configuration.Instance);
Closing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
}
Opened += MainWindow_Opened;
Closing += MainWindow_Closing;
@@ -67,50 +68,72 @@ namespace LibationAvalonia.Views
private async void MainWindow_Opened(object sender, EventArgs e)
{
#if !DEBUG
if (App.IsWindows)
//This is temporaty until we have a solution for linux/mac so that
//Libation doesn't download a zip every time it runs.
if (!LibationScaffolding.IsWindows)
return;
try
{
try
{
await Task.Run(checkForAndDownloadUpdate);
}
catch(Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error occured while checking for app updates.");
(string zipFile, UpgradeProperties upgradeProperties) = await Task.Run(() => downloadUpdate());
if (string.IsNullOrEmpty(zipFile) || !System.IO.File.Exists(zipFile))
return;
var result = await MessageBox.Show($"{upgradeProperties.HtmlUrl}\r\n\r\nWould you like to upgrade now?", "New version available", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1);
if (result != DialogResult.Yes)
return;
if (LibationScaffolding.IsWindows)
{
runWindowsUpgrader(zipFile);
}
else if (LibationScaffolding.IsLinux)
{
}
else if (LibationScaffolding.IsMacOs)
{
}
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "An error occured while checking for app updates.");
return;
}
#endif
}
private async Task checkForAndDownloadUpdate()
private async Task<(string zipFile, UpgradeProperties release)> downloadUpdate()
{
AppScaffolding.UpgradeProperties upgradeProperties;
UpgradeProperties upgradeProperties;
try
{
upgradeProperties = AppScaffolding.LibationScaffolding.GetLatestRelease(AppScaffolding.LibationScaffolding.ReleaseIdentifier.WindowsAvalonia);
upgradeProperties = LibationScaffolding.GetLatestRelease();
if (upgradeProperties is null)
return;
return (null,null);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Failed to check for update");
return;
return (null, null);
}
if (upgradeProperties.ZipUrl is null)
{
Serilog.Log.Logger.Information("Download link for new version not found");
return;
return (null, null);
}
//Silently download the update in the background, save it to a temp file.
var zipPath = System.IO.Path.GetTempFileName();
var zipFile = System.IO.Path.GetTempFileName();
try
{
System.Net.Http.HttpClient cli = new();
using (var fs = System.IO.File.OpenWrite(zipPath))
using (var fs = System.IO.File.OpenWrite(zipFile))
{
using (var dlStream = await cli.GetStreamAsync(new Uri(upgradeProperties.ZipUrl)))
await dlStream.CopyToAsync(fs);
@@ -119,18 +142,17 @@ namespace LibationAvalonia.Views
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Failed to download the update: {pdate}", upgradeProperties.ZipUrl);
return;
return (null, null);
}
return (zipFile, upgradeProperties);
}
var result = MessageBox.Show($"{upgradeProperties.HtmlUrl}\r\n\r\nWould you like to upgrade now?", "New version available", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1);
if (result != DialogResult.Yes)
return;
var thisExe = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
private void runWindowsUpgrader(string zipFile)
{
var thisExe = Environment.ProcessPath;
var thisDir = System.IO.Path.GetDirectoryName(thisExe);
var args = $"--input {zipPath} --output {thisDir} --executable {thisExe}";
var args = $"--input {zipFile} --output {thisDir} --executable {thisExe}";
var zipExtractor = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "ZipExtractor.exe");
@@ -147,7 +169,7 @@ namespace LibationAvalonia.Views
};
System.Diagnostics.Process.Start(psi);
Environment.Exit(0);
Environment.Exit(0);
}
public void ProductsDisplay_Initialized1(object sender, EventArgs e)

View File

@@ -1,27 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<PublishReadyToRun>true</PublishReadyToRun>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<IsPublishable>True</IsPublishable>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<PublishReadyToRun>true</PublishReadyToRun>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<IsPublishable>True</IsPublishable>
</PropertyGroup>
<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>
<SatelliteResourceLanguages>en;es</SatelliteResourceLanguages>
</PropertyGroup>
<!--
<!--
When LibationWinForms and LibationCli output to the same dir, LibationCli must build before LibationWinForms
VS > rt-clk solution > Properties
@@ -31,24 +30,31 @@
edit debug and release output paths
-->
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\bin\Debug</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\bin\Debug</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\bin\Release</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\bin\Release</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
</ItemGroup>
<Target Name="SpicNSpan" AfterTargets="Clean">
<!-- Remove obj folder -->
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
<!-- Remove bin folder -->
<RemoveDir Directories="$(BaseOutputPath)" />
</Target>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
</ItemGroup>
</Project>

View File

@@ -3,14 +3,15 @@
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>..\bin-Avalonia\publish\linux-x64</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
</PropertyGroup>
<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>

View File

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

View File

@@ -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\win-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<PublishTrimmed>false</PublishTrimmed>
</PropertyGroup>
</Project>

View File

@@ -1,70 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Polly;
using Polly.Retry;
namespace LibationFileManager
{
/// <summary>
/// Tags must also be stored in db for search performance. Stored in json file to survive a db reset.
/// json is only read when a product is first loaded into the db
/// json is only written to when tags are edited
/// json access is infrequent and one-off
/// </summary>
public static class TagsPersistence
{
private static string TagsFile => Path.Combine(Configuration.Instance.LibationFiles, "BookTags.json");
private static object locker { get; } = new object();
// if failed, retry only 1 time after a wait of 100 ms
// 1st save attempt sometimes fails with
// The requested operation cannot be performed on a file with a user-mapped section open.
private static RetryPolicy policy { get; }
= Policy.Handle<Exception>()
.WaitAndRetry(new[] { TimeSpan.FromMilliseconds(100) });
public static void Save(IEnumerable<(string productId, string tags)> tagsCollection)
{
ensureCache();
if (!tagsCollection.Any())
return;
// on initial reload, there's a huge benefit to adding to cache individually then updating the file only once
foreach ((string productId, string tags) in tagsCollection)
cache[productId] = tags;
lock (locker)
policy.Execute(() => File.WriteAllText(TagsFile, JsonConvert.SerializeObject(cache, Formatting.Indented)));
}
private static Dictionary<string, string> cache;
public static string GetTags(string productId)
{
ensureCache();
cache.TryGetValue(productId, out string value);
return value;
}
private static void ensureCache()
{
if (cache is not null)
return;
lock (locker)
{
if (File.Exists(TagsFile))
cache = JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(TagsFile));
// if file doesn't exist. or if file is corrupt and deserialize returns null
cache ??= new Dictionary<string, string>();
}
}
}
}

View File

@@ -9,11 +9,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="LuceneNet303r2" Version="3.0.3.2" />
<PackageReference Include="LuceneNet303r2" Version="3.0.3.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
<ProjectReference Include="..\LibationFileManager\LibationFileManager.csproj" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using AudibleApi;
using AudibleUtilities;
using LibationWinForms.Dialogs.Login;
@@ -14,42 +15,43 @@ namespace LibationWinForms.Login
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
}
public string Get2faCode()
public Task<string> Get2faCodeAsync()
{
using var dialog = new _2faCodeDialog();
if (ShowDialog(dialog))
return dialog.Code;
return null;
return Task.FromResult(dialog.Code);
return Task.FromResult<string>(null);
}
public string GetCaptchaAnswer(byte[] captchaImage)
public Task<string> GetCaptchaAnswerAsync(byte[] captchaImage)
{
using var dialog = new CaptchaDialog(captchaImage);
if (ShowDialog(dialog))
return dialog.Answer;
return null;
return Task.FromResult(dialog.Answer);
return Task.FromResult<string>(null);
}
public (string name, string value) GetMfaChoice(MfaConfig mfaConfig)
public Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)
{
using var dialog = new MfaDialog(mfaConfig);
if (ShowDialog(dialog))
return (dialog.SelectedName, dialog.SelectedValue);
return (null, null);
return Task.FromResult((dialog.SelectedName, dialog.SelectedValue));
return Task.FromResult<(string, string)>((null, null));
}
public (string email, string password) GetLogin()
public Task<(string email, string password)> GetLoginAsync()
{
using var dialog = new LoginCallbackDialog(_account);
if (ShowDialog(dialog))
return (dialog.Email, dialog.Password);
return (null, null);
return Task.FromResult((dialog.Email, dialog.Password));
return Task.FromResult<(string, string)>((null, null));
}
public void ShowApprovalNeeded()
public Task ShowApprovalNeededAsync()
{
using var dialog = new ApprovalNeededDialog();
ShowDialog(dialog);
return Task.CompletedTask;
}
}
}

View File

@@ -21,7 +21,7 @@ namespace LibationWinForms.Login
LoginCallback = new WinformLoginCallback(_account);
}
public ChoiceOut Start(ChoiceIn choiceIn)
public Task<ChoiceOut> StartAsync(ChoiceIn choiceIn)
{
using var dialog = new LoginChoiceEagerDialog(_account);
@@ -31,13 +31,14 @@ namespace LibationWinForms.Login
switch (dialog.LoginMethod)
{
case LoginMethod.Api:
return ChoiceOut.WithApi(dialog.Email, dialog.Password);
return Task.FromResult(ChoiceOut.WithApi(dialog.Email, dialog.Password));
case LoginMethod.External:
{
using var externalDialog = new LoginExternalDialog(_account, choiceIn.LoginUrl);
return ShowDialog(externalDialog)
? ChoiceOut.External(externalDialog.ResponseUrl)
: null;
return Task.FromResult(
ShowDialog(externalDialog)
? ChoiceOut.External(externalDialog.ResponseUrl)
: null);
}
default:
throw new Exception($"Unknown {nameof(LoginMethod)} value");

View File

@@ -13,6 +13,6 @@ namespace LibationWinForms
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();
private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
=> MessageBox.Show($"Running Libation version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
=> MessageBox.Show($"Libation {AppScaffolding.LibationScaffolding.Variety}{Environment.NewLine}Version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
}
}

View File

@@ -1,91 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>libation.ico</ApplicationIcon>
<AssemblyName>Libation</AssemblyName>
<PublishReadyToRun>true</PublishReadyToRun>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<StartupObject />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>libation.ico</ApplicationIcon>
<AssemblyName>Libation</AssemblyName>
<!-- Version is now in AppScaffolding.csproj -->
</PropertyGroup>
<PublishReadyToRun>true</PublishReadyToRun>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<StartupObject />
<IsPublishable>true</IsPublishable>
<!-- Version is now in AppScaffolding.csproj -->
</PropertyGroup>
<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>
<SatelliteResourceLanguages>en;es</SatelliteResourceLanguages>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\bin\Debug</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\bin\Debug</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\bin\Release</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<None Remove=".gitignore" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\bin\Release</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autoupdater.NET.Official" Version="1.7.4" />
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.2.3.1" />
</ItemGroup>
<ItemGroup>
<None Remove=".gitignore" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Form1.*.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Update="Dialogs\SettingsDialog.*.cs">
<DependentUpon>SettingsDialog.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Update="Form1.*.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
</ItemGroup> <ItemGroup>
<ItemGroup>
<PackageReference Include="Autoupdater.NET.Official" Version="1.7.4" />
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="5.0.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Form1.*.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Update="Dialogs\SettingsDialog.*.cs">
<DependentUpon>SettingsDialog.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Update="Form1.*.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Update="Form1.*.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<Target Name="SpicNSpan" AfterTargets="Clean">
<!-- Remove obj folder -->
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
<!-- Remove bin folder -->
<RemoveDir Directories="$(BaseOutputPath)" />
</Target>
</Project>

View File

@@ -30,6 +30,8 @@ namespace LibationWinForms
ApplicationConfiguration.Initialize();
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.WindowsClassic);
//***********************************************//
// //
// do not use Configuration before this line //
@@ -170,7 +172,7 @@ namespace LibationWinForms
try
{
upgradeProperties = AppScaffolding.LibationScaffolding.GetLatestRelease(AppScaffolding.LibationScaffolding.ReleaseIdentifier.WindowsClassic);
upgradeProperties = AppScaffolding.LibationScaffolding.GetLatestRelease();
if (upgradeProperties is null)
return;
}

View File

@@ -0,0 +1,104 @@
I don't expect you'll fully understand everything I said in this writeup on your first read, but I think they'll make much more sense after you've poked around LibationAvalonia and started reading code and wonder 'WTF?'. These are the brief explanations I wish I had while I was learning.
- Mbucari
===================================
Controls Basics
===============
Avalonia uses a xml layout with code-behind like WPF. It's actually a port of WPF, thus most of the Avalonia controls are also WPF controls. There are lots of built-in controls that you can browse in the documentation, but more important than understanding every specific control is understanding the few base controls.
*TemplatedControl - This base control allows you to define a layout template. The TemplatedControl.Template property is used to define the control's appearance.
*ContentControl - This base control is what allows other controls to be nested inside of it, like so:
<ContentControl>
<Control />
</ContentControl>
Doing that in XML sets the ContentControl.Content property to the <Control />
Common ContentContols:
- Button
- Label
- UserControl
*ItemsControl - This base control allows you to supply IEnumerable for content, and will display the items in the IEnumerable in some way, such as items in a ComboBox or ListBox, by setting the ItemsControl.Items property.
Common ItemsControls:
- DataGrid
- ListBox
- ComboBox
Extending Controls
==================
If you want to customize an Avalonia control by extending it and adding custom functionality. If you do this you MUST make your extended control inherit IStyleable.
See LibationAvalonia.Controls.LinkLabel for an example.
Data Binding
============
Every control implements IDataContextProvider. IDataContextProvider.DataContext is how you bind data to controls. It's null by default, but you need to set it for MVVM to work.
The simplest way to use data binding is to set the window/controls' DataContext to it's own instance. Just add the following line to the window/controls' constructor
this.DataContext = this;
Then, every public 'this' property is accessible in XML with data binding. For a simple example of that working, look at LibationAvalonia.Dialogs.Login.CaptchaDialog.
CaptchaDialog.axaml.cs has 2 public properties:
public string Answer { get; set; }
public Bitmap CaptchaImage { get; }
Inside CaptchaDialog.axaml, I bound the Image control to CaptchaImage like so:
<Image Source="{Binding CaptchaImage}" />
and I bound the TextBox control to Answer like so:
<TextBox Text="{Binding Answer}" />
Because Answer has public getter and setter, the binding mode is TwoWay. That means that changes to the binding source (the CaptchaDialog.Answer property) go to the binding target (the TextBox Control), and changes to the binding target (caused by the user typing text into the textbox) get pushed back to the binding source.
this example is lacking any change notifications, so the view will only get the Answer and CaptchaImage values on load. So those values MUST be set before the window is shown. If you change those properties' values while the window is shown, the view won't know they changed so it will not update to display the new values.
Data Binding to an INotifyPropertyChanged.
=========================================
One of the limitations of binding to 'this' is that you won't be able to notify the view that the value has changed. Change notification works using the INotifyPropertyChanged interface that you're already familiar with. For your DataContext to be able to notify the view that a value has changed it must be INotifyPropertyChanged. Avalonia uses ReactiveUI for this, but it's the same interface. For a (somewhat) simple example of this, see LibationAvalonia.Dialogs.EditQuickFilters.
In EditQuickFilters I set this.DataContext = _viewModel = new EditTemplateViewModel();
EditTemplateViewModel inherits ViewModelBase, which inherits ReactiveObject. Because it inherits ReactiveObject, I can call the ReactiveUI extension methods for notifying the view that a property has changed:
- this.RaiseAndSetIfChanged()
- this.RaisePropertyChanged()
Doing this will notify the view that the property has changed, and the view will update itself by getting the new value.
NOTE: unlike winforms, calling RaisePropertyChanged() with an empty or incorrect property name will not cause the whole view to update. In Avalonia you MUST call RaisePropertyChanged() for every property you want updated, and everything must be spelled correctly.
Binding to ItemsControl.Items
=============================
As I said above, the ItemsControl.Items property accepts an IEnumerable. You can bind a list to that property, and the ItemsControl will generate a new item for each list entry. Each generated item will be bound to its corresponding list entry.
An example of this can be found in LibationAvalonia.Dialogs.EditTemplateDialog. I set EditTemplateDialog's DataContext = EditTemplateViewModel. EditTemplateViewModel.ListItems is a list of TemplateTags. In EditTemplateDialog.axaml, I bound EditTemplateViewModel.ListItems to DataGrid.Items like so:
<DataGrid Items="{Binding ListItems}" >
By doing that, the DataGrid generates a new row for every TemplateTags item in ListItems, and the rows's DataContext is set to the TemplateTags that generated it.
That DataGrid has two column templates: Tag and Description. The Tag column is bound to TemplateTags.TagName, and the Description column is bound to TagName.Description. Binding the DataGrid to a List<TemplateTags> and binding column templates to TemplateTags properties is all you need to do for all your data to be displayed in the grid. This works the same for all ItemsControl controls. For another example using the ListBox control, see LibationAvalonia.Dialogs.ScanAccountsDialog.
Binding to ItemsControl.Items with notifications that the Items list has changed
================================================================================
Binding ItemsControl to a List is fine for a static display, but if you want changes to the list update the control (e.g. removing an Account from the accounts list or a Filter from the filters list in EditQuickFilters), you need to use an IEnumerable type that implements INotifyCollectionChanged. INotifyCollectionChanged is just like INotifyPropertyChanged but is for items in a collection. C#'s built-in tracking collection is System.Collections.ObjectModel.ObservableCollection<T>. If you put all items in an ObservableCollection<T> and bind that collection to a ItemsControl.Items property, then adding or removing items from that collection will cause the view to add or remove items. This is similar to BindingList<T>, with one very important difference: The ObservableCollection<T> is not aware of changes made to any of its items, only changes made to the collection.
For an example of binding ItemsControl.Items to an ObservableCollection<T>, see LibationAvalonia.Dialogs.EditQuickFilters.

View File

@@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Moq" Version="4.18.1" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">

View File

@@ -9,7 +9,7 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="Moq" Version="4.18.1" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2">

View File

@@ -1,26 +0,0 @@
-- begin PERSISTENCE ---------------------------------------------------------------------------------------------------------------------
user defined item persistence:
static events feel klugey. what's the right way to do them?
see also HACK note in UserDefinedItem.OnItemChanged()
would it be any better to use Interception of database operations in LibationContextFactory.cs?
https://devblogs.microsoft.com/dotnet/announcing-ef-core-3-0-and-ef-6-3-general-availability/#interception-of-database-operations
db originally was only things which came from audible. all else was stored in json files. user def'd tags stored in both. json is canonical, db is for search engine access
this allows easy db delete for testing, when migrations went bad, and when certain migrations weren't possible w/sqlite, pre .net5. This was also an easy way to remove returned books -- just delete db file and re-scan.
starting with stateful liberation status, the db is not as disposible
-- end PERSISTENCE ---------------------------------------------------------------------------------------------------------------------
-- begin CATEGORIES ---------------------------------------------------------------------------------------------------------------------
fully support multiple categories
learn about the different CategoryLadder.Root enums. probably only need Root.Genres
2020-05-26 -- audible started using categories nested more than 2 deep. to compensate, libation was changed in this hack way:
search for comments: "// CATEGORY HACK: "
CategoryImporter.upsertCategories // CATEGORY HACK: not yet supported: depth beyond 0 and 1
BookInporter.createNewBook // CATEGORY HACK: only use the 1st 2 categories
result: only at most 1st 2 categories (parent and child) will be captured
similar legacy knowledge exists elsewhere. eg:
Book.Category stuff
public partial class Item.Categories
-- end CATEGORIES ---------------------------------------------------------------------------------------------------------------------