diff --git a/Source/AppScaffolding/AppScaffolding.csproj b/Source/AppScaffolding/AppScaffolding.csproj index 9c1b8d7a..2dfce40e 100644 --- a/Source/AppScaffolding/AppScaffolding.csproj +++ b/Source/AppScaffolding/AppScaffolding.csproj @@ -11,12 +11,6 @@ - - - $(DefineConstants);WINDOWS - $(DefineConstants);LINUX - $(DefineConstants);MACOS - embedded diff --git a/Source/AppScaffolding/LibationScaffolding.cs b/Source/AppScaffolding/LibationScaffolding.cs index 56f6e781..3d6d0643 100644 --- a/Source/AppScaffolding/LibationScaffolding.cs +++ b/Source/AppScaffolding/LibationScaffolding.cs @@ -14,7 +14,6 @@ using Serilog; namespace AppScaffolding { - public enum ReleaseIdentifier { None, @@ -26,6 +25,16 @@ namespace AppScaffolding 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 ReleaseIdentifier ReleaseIdentifier { get; private set; } public static void SetReleaseIdentifier(ReleaseIdentifier releaseID) @@ -290,14 +299,11 @@ namespace AppScaffolding if (System.Diagnostics.Debugger.IsAttached) mode += " (Debugger attached)"; -#if MACOS - var os = "MacOS"; -#elif LINUX - var os = "Linux"; -#else - var os = "Windows"; -#endif - + string OS + = IsLinux ? "Linux" + : IsMacOs ? "MacOS" + : IsWindows ? "Windows" + : "UNKNOWN_OS"; // begin logging session with a form feed Log.Logger.Information("\r\n\f"); @@ -306,7 +312,7 @@ namespace AppScaffolding AppName = EntryAssembly.GetName().Name, Version = BuildVersion.ToString(), ReleaseIdentifier = ReleaseIdentifier, - OS = os, + OS = OS, Mode = mode, LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(), LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(), diff --git a/Source/HangoverAvalonia/App.axaml b/Source/HangoverAvalonia/App.axaml new file mode 100644 index 00000000..f4f24946 --- /dev/null +++ b/Source/HangoverAvalonia/App.axaml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/Source/HangoverAvalonia/App.axaml.cs b/Source/HangoverAvalonia/App.axaml.cs new file mode 100644 index 00000000..a64b8e22 --- /dev/null +++ b/Source/HangoverAvalonia/App.axaml.cs @@ -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(); + } + } +} diff --git a/Source/HangoverAvalonia/Assets/hangover.ico b/Source/HangoverAvalonia/Assets/hangover.ico new file mode 100644 index 00000000..c29a9b6a Binary files /dev/null and b/Source/HangoverAvalonia/Assets/hangover.ico differ diff --git a/Source/HangoverAvalonia/HangoverAvalonia.csproj b/Source/HangoverAvalonia/HangoverAvalonia.csproj new file mode 100644 index 00000000..e9473042 --- /dev/null +++ b/Source/HangoverAvalonia/HangoverAvalonia.csproj @@ -0,0 +1,74 @@ + + + WinExe + net6.0 + + copyused + true + true + + true + false + false + + true + + true + false + false + + hangover.ico + + + + ..\bin\Avalonia\Debug + embedded + + + + ..\bin\Avalonia\Release + embedded + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/HangoverAvalonia/Program.cs b/Source/HangoverAvalonia/Program.cs new file mode 100644 index 00000000..426b6853 --- /dev/null +++ b/Source/HangoverAvalonia/Program.cs @@ -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() + .UsePlatformDetect() + .LogToTrace() + .UseReactiveUI(); + } +} diff --git a/Source/HangoverAvalonia/Properties/PublishProfiles/LinuxProfile.pubxml b/Source/HangoverAvalonia/Properties/PublishProfiles/LinuxProfile.pubxml new file mode 100644 index 00000000..a846a327 --- /dev/null +++ b/Source/HangoverAvalonia/Properties/PublishProfiles/LinuxProfile.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + ..\bin\Release\linux-chardonnay + FileSystem + net6.0 + linux-x64 + false + false + + \ No newline at end of file diff --git a/Source/HangoverAvalonia/Properties/PublishProfiles/MacOSProfile.pubxml b/Source/HangoverAvalonia/Properties/PublishProfiles/MacOSProfile.pubxml new file mode 100644 index 00000000..5d1e98f9 --- /dev/null +++ b/Source/HangoverAvalonia/Properties/PublishProfiles/MacOSProfile.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + ..\bin\Release\macos-chardonnay + FileSystem + net6.0 + osx-x64 + false + false + + \ No newline at end of file diff --git a/Source/HangoverAvalonia/Properties/PublishProfiles/WindowsProfile.pubxml b/Source/HangoverAvalonia/Properties/PublishProfiles/WindowsProfile.pubxml new file mode 100644 index 00000000..1490c0a5 --- /dev/null +++ b/Source/HangoverAvalonia/Properties/PublishProfiles/WindowsProfile.pubxml @@ -0,0 +1,17 @@ + + + + + Release + Any CPU + ..\bin\Release\win-chardonnay + FileSystem + net6.0 + win-x64 + true + false + false + + \ No newline at end of file diff --git a/Source/HangoverAvalonia/ViewLocator.cs b/Source/HangoverAvalonia/ViewLocator.cs new file mode 100644 index 00000000..44467ea7 --- /dev/null +++ b/Source/HangoverAvalonia/ViewLocator.cs @@ -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; + } + } +} diff --git a/Source/HangoverAvalonia/ViewModels/MainWindowViewModel.cs b/Source/HangoverAvalonia/ViewModels/MainWindowViewModel.cs new file mode 100644 index 00000000..607e3ddb --- /dev/null +++ b/Source/HangoverAvalonia/ViewModels/MainWindowViewModel.cs @@ -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"; + } + } +} diff --git a/Source/HangoverAvalonia/ViewModels/ViewModelBase.cs b/Source/HangoverAvalonia/ViewModels/ViewModelBase.cs new file mode 100644 index 00000000..50908ee3 --- /dev/null +++ b/Source/HangoverAvalonia/ViewModels/ViewModelBase.cs @@ -0,0 +1,11 @@ +using ReactiveUI; +using System; +using System.Collections.Generic; +using System.Text; + +namespace HangoverAvalonia.ViewModels +{ + public class ViewModelBase : ReactiveObject + { + } +} diff --git a/Source/HangoverAvalonia/Views/MainWindow.axaml b/Source/HangoverAvalonia/Views/MainWindow.axaml new file mode 100644 index 00000000..b32eb8de --- /dev/null +++ b/Source/HangoverAvalonia/Views/MainWindow.axaml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + Database + + + + + + + + + + +