Compare commits

...

7 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
41 changed files with 375 additions and 358 deletions

View File

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

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;
@@ -23,19 +24,20 @@ namespace AppScaffolding
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 readonly bool IsWindows;
public static readonly bool IsLinux;
public static readonly bool IsMacOs;
static LibationScaffolding()
{
IsWindows = OperatingSystem.IsWindows();
IsLinux = OperatingSystem.IsLinux();
IsMacOs = OperatingSystem.IsMacOS();
}
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;
@@ -311,8 +313,8 @@ namespace AppScaffolding
{
AppName = EntryAssembly.GetName().Name,
Version = BuildVersion.ToString(),
ReleaseIdentifier = ReleaseIdentifier,
OS = OS,
ReleaseIdentifier,
OS,
Mode = mode,
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),

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.6.0.1" />
<PackageReference Include="AudibleApi" Version="4.6.3.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="4.4.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>

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

@@ -20,6 +20,16 @@
<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>
@@ -61,9 +71,7 @@
<PackageReference Include="XamlNameReferenceGenerator" Version="1.3.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
</ItemGroup>

View File

@@ -1,17 +1,13 @@
using ApplicationServices;
using AppScaffolding;
using Microsoft.EntityFrameworkCore;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using HangoverBase;
using ReactiveUI;
namespace HangoverAvalonia.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
private string dbFile;
private DatabaseTab _tab;
private string _databaseFileText;
private bool _databaseFound;
private string _sqlResults;
@@ -22,129 +18,20 @@ namespace HangoverAvalonia.ViewModels
public MainWindowViewModel()
{
dbFile = UNSAFE_MigrationHelper.DatabaseFile;
if (dbFile is null)
_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: {UNSAFE_MigrationHelper.DatabaseFile ?? "not found"}";
DatabaseFound = UNSAFE_MigrationHelper.DatabaseFile is not null;
DatabaseFileText = $"Database file: {_tab.DbFile}";
DatabaseFound = true;
}
public void ExecuteQuery()
{
ensureBackup();
SqlResults = string.Empty;
try
{
var sql = SqlQuery.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)
{
SqlResults = $"{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 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)
{
SqlResults += builder.ToString();
builder.Clear();
}
}
SqlResults += builder.ToString();
builder.Clear();
if (results == 0)
SqlResults = "[no results]";
else
{
SqlResults += $"\r\n{results} result";
if (results != 1) SqlResults += "s";
}
}
void nonQuery(string sql)
{
using var context = DbContexts.GetContext();
var results = context.Database.ExecuteSqlRaw(sql);
SqlResults += $"{results} record";
if (results != 1) SqlResults += "s";
SqlResults += " affected";
}
public void ExecuteQuery() => _tab.ExecuteQuery();
}
}

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

@@ -44,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>
@@ -55,7 +53,7 @@
</Compile>
</ItemGroup>
<Target Name="SpicNSpan" AfterTargets="Clean">
<Target Name="SpicNSpan" AfterTargets="Clean">
<!-- Remove obj folder -->
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
<!-- Remove bin folder -->

View File

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

View File

Before

Width:  |  Height:  |  Size: 133 KiB

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

Before

Width:  |  Height:  |  Size: 133 KiB

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,12 +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
@@ -156,6 +158,10 @@ Global
{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
@@ -181,6 +187,7 @@ Global
{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

@@ -29,33 +29,6 @@ namespace LibationAvalonia
public static Stream OpenAsset(string assetRelativePath)
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
public static bool GoToFile(string path)
=> AppScaffolding.LibationScaffolding.IsWindows ? Go.To.File(path)
: GoToFolder(path is null ? string.Empty : Path.GetDirectoryName(path));
public static bool GoToFolder(string path)
{
if (AppScaffolding.LibationScaffolding.IsWindows)
return Go.To.Folder(path);
else if (AppScaffolding.LibationScaffolding.IsLinux)
{
var startInfo = new System.Diagnostics.ProcessStartInfo()
{
FileName = "/bin/xdg-open",
Arguments = path is null ? string.Empty : $"\"{path}\"",
UseShellExecute = false, //Import in Linux environments
CreateNoWindow = false,
RedirectStandardOutput = true,
RedirectStandardError = true
};
System.Diagnostics.Process.Start(startInfo);
return true;
}
//Don't know how to do this for mac yet
else return true;
}
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);

View File

@@ -52,7 +52,7 @@ namespace LibationAvalonia.Dialogs
try
{
App.GoToFolder(dir.ShortPathName);
Go.To.Folder(dir.ShortPathName);
}
catch
{

View File

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

View File

@@ -36,7 +36,7 @@ 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}";
await MessageBox.Show($"File not found" + suffix);

View File

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

View File

@@ -149,8 +149,7 @@ namespace LibationAvalonia.Views
private void runWindowsUpgrader(string zipFile)
{
var thisExe = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
var thisExe = Environment.ProcessPath;
var thisDir = System.IO.Path.GetDirectoryName(thisExe);
var args = $"--input {zipFile} --output {thisDir} --executable {thisExe}";

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

@@ -44,7 +44,7 @@
<ItemGroup>
<PackageReference Include="Autoupdater.NET.Official" Version="1.7.4" />
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.2.3.1" />
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="5.0.0.1" />
</ItemGroup>
<ItemGroup>
@@ -89,7 +89,7 @@
</EmbeddedResource>
</ItemGroup>
<Target Name="SpicNSpan" AfterTargets="Clean">
<Target Name="SpicNSpan" AfterTargets="Clean">
<!-- Remove obj folder -->
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
<!-- Remove bin folder -->

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