Add HangoverAvalonia

This commit is contained in:
Michael Bucari-Tovo 2022-07-31 20:33:56 -06:00
parent 7cf4c63d79
commit 66679ace2f
24 changed files with 503 additions and 35 deletions

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View file

@ -0,0 +1,74 @@
<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>
<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 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>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
<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="..\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>
</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,16 @@
<?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>false</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,16 @@
<?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>false</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
</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

@ -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,150 @@
using ApplicationServices;
using AppScaffolding;
using Microsoft.EntityFrameworkCore;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace HangoverAvalonia.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
private string dbFile;
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()
{
dbFile = UNSAFE_MigrationHelper.DatabaseFile;
if (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;
}
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";
}
}
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB