소스 검색

Merge remote-tracking branch 'origin/frank' into kenric

# Conflicts:
#	InABox.Core/CoreTable/CoreTable.cs
Kenric Nugteren 3 달 전
부모
커밋
769a8b1f53

+ 2 - 1
InABox.Avalonia.Platform.Android/Bluetooth/Android_BluetoothDevice.cs

@@ -2,7 +2,7 @@
 
 
 namespace InABox.Avalonia.Platform.Android;
 namespace InABox.Avalonia.Platform.Android;
 
 
-public class Android_BluetoothDevice(ScanResult scan, Guid[] availableservices, DateTime timestamp) : IBluetoothDevice, IDisposable
+public class Android_BluetoothDevice(ScanResult scan, Guid[] availableservices, DateTime timestamp) : IBluetoothDevice
 {
 {
     public ScanResult Scan { get; } = scan;
     public ScanResult Scan { get; } = scan;
     public string ID { get; } = scan.Device?.Address ?? string.Empty;
     public string ID { get; } = scan.Device?.Address ?? string.Empty;
@@ -12,6 +12,7 @@ public class Android_BluetoothDevice(ScanResult scan, Guid[] availableservices,
 
 
     public void Dispose()
     public void Dispose()
     {
     {
+        scan.Dispose();
         Scan.Dispose();
         Scan.Dispose();
     }
     }
 }
 }

+ 26 - 15
InABox.Avalonia.Platform.Android/Bluetooth/Android_ConnectedBluetoothDevice.cs

@@ -3,12 +3,12 @@ using Android.Bluetooth;
 
 
 namespace InABox.Avalonia.Platform.Android;
 namespace InABox.Avalonia.Platform.Android;
 
 
-public class Android_ConnectedBluetoothDevice(BluetoothDevice device) : BluetoothGattCallback,IConnectedBluetoothDevice
+public class Android_ConnectedBluetoothDevice : BluetoothGattCallback,IConnectedBluetoothDevice
 {
 {
 
 
-    private BluetoothDevice _device = device;
-    public string ID { get; } = device?.Address ?? string.Empty;
-    public string Name { get; } = device?.Name ?? "Unknown Device";
+    private BluetoothDevice? _device;
+    public string ID { get; }
+    public string Name { get; }
     public DateTime LastSeen { get; set; } = DateTime.Now;
     public DateTime LastSeen { get; set; } = DateTime.Now;
 
 
     private Guid[] _availableServices = [];
     private Guid[] _availableServices = [];
@@ -16,15 +16,22 @@ public class Android_ConnectedBluetoothDevice(BluetoothDevice device) : Bluetoot
         
         
     private BluetoothGatt? _bluetoothGatt;
     private BluetoothGatt? _bluetoothGatt;
     
     
-    private TaskCompletionSource<bool> _connectionTaskCompletionSource;
-    private TaskCompletionSource<bool> _serviceDiscoveryTaskCompletionSource;
-    private TaskCompletionSource<byte[]> _readTaskCompletionSource;
-    private TaskCompletionSource<bool> _writeTaskCompletionSource;
+    private TaskCompletionSource<bool>? _connectionTaskCompletionSource;
+    private TaskCompletionSource<bool>? _serviceDiscoveryTaskCompletionSource;
+    private TaskCompletionSource<byte[]>? _readTaskCompletionSource;
+    private TaskCompletionSource<bool>? _writeTaskCompletionSource;
+
+    public Android_ConnectedBluetoothDevice(BluetoothDevice? device)
+    {
+        _device = device;
+        ID = device?.Address ?? string.Empty;
+        Name = device?.Name ?? "Unknown Device";
+    }
 
 
     public async Task<bool> ConnectAsync()
     public async Task<bool> ConnectAsync()
     {
     {
         _connectionTaskCompletionSource = new TaskCompletionSource<bool>();
         _connectionTaskCompletionSource = new TaskCompletionSource<bool>();
-        _bluetoothGatt = _device.ConnectGatt(Application.Context, false, this);
+        _bluetoothGatt = _device?.ConnectGatt(Application.Context, false, this);
         return await _connectionTaskCompletionSource.Task;
         return await _connectionTaskCompletionSource.Task;
     }
     }
 
 
@@ -88,7 +95,7 @@ public class Android_ConnectedBluetoothDevice(BluetoothDevice device) : Bluetoot
     public async Task<String?> ReadStringAsync(Guid serviceid, Guid characteristicid)
     public async Task<String?> ReadStringAsync(Guid serviceid, Guid characteristicid)
     {
     {
         var data = await ReadBytesAsync(serviceid,characteristicid);
         var data = await ReadBytesAsync(serviceid,characteristicid);
-        return data != null ? System.Text.Encoding.UTF8.GetString(data) : null;
+        return data != null ? Encoding.UTF8.GetString(data) : null;
     }
     }
 
 
     private async Task<byte[]?> ReadCharacteristicAsync(BluetoothGattCharacteristic characteristic)
     private async Task<byte[]?> ReadCharacteristicAsync(BluetoothGattCharacteristic characteristic)
@@ -174,8 +181,9 @@ public class Android_ConnectedBluetoothDevice(BluetoothDevice device) : Bluetoot
         }
         }
     }
     }
     
     
-    public void Dispose()
+    public new void Dispose()
     {
     {
+        base.Dispose();
         try
         try
         {
         {
             _bluetoothGatt?.Disconnect();
             _bluetoothGatt?.Disconnect();
@@ -185,12 +193,15 @@ public class Android_ConnectedBluetoothDevice(BluetoothDevice device) : Bluetoot
         {
         {
             Console.WriteLine($"Error during disposal: {ex.Message}");
             Console.WriteLine($"Error during disposal: {ex.Message}");
         }
         }
-        finally
-        {
-            _bluetoothGatt = null;
-        }
+        
+        _bluetoothGatt?.Dispose();
+        _bluetoothGatt = null;
+        
+        _device?.Dispose();
+        _device = null;
 
 
         Console.WriteLine("Resources released.");
         Console.WriteLine("Resources released.");
+        
     }
     }
     
     
 }
 }

+ 29 - 10
InABox.Avalonia.Platform.Android/Bluetooth/Bluetooth.Android.cs

@@ -29,7 +29,11 @@ public class Android_Bluetooth : IBluetooth
                 var stale = Devices.ToArray().Where(x => (x == null) || (x.LastSeen < DateTime.Now.Subtract(new TimeSpan(0, 0, 5))))
                 var stale = Devices.ToArray().Where(x => (x == null) || (x.LastSeen < DateTime.Now.Subtract(new TimeSpan(0, 0, 5))))
                     .ToArray();
                     .ToArray();
                 if (stale.Any())
                 if (stale.Any())
+                {
                     Devices.RemoveRange(stale);
                     Devices.RemoveRange(stale);
+                    Changed?.Invoke(this, EventArgs.Empty);
+                }
+                    
                 Task.Delay(500);
                 Task.Delay(500);
             }
             }
         });
         });
@@ -88,19 +92,34 @@ public class Android_Bluetooth : IBluetooth
     private void DoDeviceFound(ScanResult device, Guid configServiceId)
     private void DoDeviceFound(ScanResult device, Guid configServiceId)
     {
     {
 
 
-        var abd = Devices.FirstOrDefault(x => x.ID == device.Device?.Address);
-        if (abd == null)
+        var isTarget = device.ScanRecord?.ServiceUuids?
+            .Any(x=>string.Equals(x.ToString(),configServiceId.ToString(), StringComparison.OrdinalIgnoreCase)) 
+                       ?? false;
+        if (isTarget)
         {
         {
-            var services = device.ScanRecord?.ServiceUuids?
-                .Select(x => Guid.Parse(x.ToString()))
-                .Where(x => !x.ToString().ToUpper().EndsWith("-0000-1000-8000-00805F9B34FB") && configServiceId != x)
-                .ToArray() ?? [];
+            var abd = Devices.FirstOrDefault(x => x.ID == device.Device?.Address);
+            if (abd == null)
+            {
+
+                if (isTarget)
+                {
+                    var services = device.ScanRecord?.ServiceUuids?
+                        .Select(x => Guid.Parse(x.ToString()))
+                        .Where(x => !x.ToString().ToUpper().EndsWith("-0000-1000-8000-00805F9B34FB") &&
+                                    configServiceId != x)
+                        .ToArray() ?? [];
 
 
-            abd = new Android_BluetoothDevice(device, services, DateTime.Now);
-            Devices.Add(abd);
+                    if (services.Any())
+                    {
+                        abd = new Android_BluetoothDevice(device, services, DateTime.Now);
+                        Devices.Add(abd);
+                        Changed?.Invoke(this, EventArgs.Empty);
+                    }
+                }
+            }
+            else
+                abd.LastSeen = DateTime.Now;
         }
         }
-        else
-            abd.LastSeen = DateTime.Now;
 
 
     }
     }
 
 

+ 1 - 1
InABox.Avalonia.Platform.Android/InABox.Avalonia.Platform.Android.csproj

@@ -25,7 +25,7 @@
     <ItemGroup>
     <ItemGroup>
       <PackageReference Include="Avalonia" Version="11.2.3" />
       <PackageReference Include="Avalonia" Version="11.2.3" />
       <PackageReference Include="bblanchon.PDFium.Android" Version="135.0.7009" />
       <PackageReference Include="bblanchon.PDFium.Android" Version="135.0.7009" />
-      <PackageReference Include="Microsoft.Maui.Essentials" Version="9.0.40" />
+      <PackageReference Include="Microsoft.Maui.Essentials" Version="8.0.93" />
       <PackageReference Include="PDFtoImage" Version="5.0.0" />
       <PackageReference Include="PDFtoImage" Version="5.0.0" />
     </ItemGroup>
     </ItemGroup>
 
 

+ 11 - 2
InABox.Avalonia.Platform.Desktop/Bluetooth/Desktop_Bluetooth.cs

@@ -3,7 +3,7 @@ using InABox.Core;
 
 
 namespace InABox.Avalonia.Platform.Desktop;
 namespace InABox.Avalonia.Platform.Desktop;
 
 
-public class Desktop_Bluetooth : IBluetooth
+public class Desktop_Bluetooth : IBluetooth, IDisposable
 {
 {
     public Logger? Logger { get; set; }
     public Logger? Logger { get; set; }
 
 
@@ -71,9 +71,18 @@ public class Desktop_Bluetooth : IBluetooth
     {
     {
         if (await IsAvailable())
         if (await IsAvailable())
         {
         {
-            if (device is Desktop_BluetoothDevice d)
+            if (device is Desktop_BluetoothDevice { Device: not null } d)
+            {
                 _adapter.Disconnect(d.Device);
                 _adapter.Disconnect(d.Device);
+            }
         }
         }
         return true;
         return true;
     }
     }
+
+    public void Dispose()
+    {
+        foreach (var device in Devices)
+            device.Dispose();
+        Devices.Clear();
+    }
 }
 }

+ 15 - 3
InABox.Avalonia.Platform.Desktop/Bluetooth/Desktop_BluetoothDevice.cs

@@ -2,11 +2,23 @@
 
 
 namespace InABox.Avalonia.Platform.Desktop;
 namespace InABox.Avalonia.Platform.Desktop;
 
 
-public class Desktop_BluetoothDevice(BLEDevice device) : IBluetoothDevice
+public class Desktop_BluetoothDevice : IBluetoothDevice
 {
 {
-    public BLEDevice? Device { get; } = device;
+    public Desktop_BluetoothDevice(BLEDevice device)
+    {
+        Device = device;
+        LastSeen = device.LastSeen;
+    }
+
+    public BLEDevice? Device { get; private set; }
     public string ID => Device?.MacAddress ?? string.Empty;
     public string ID => Device?.MacAddress ?? string.Empty;
     public string Name => Device?.Native?.Name ?? "Unknown Device";
     public string Name => Device?.Native?.Name ?? "Unknown Device";
     public Guid[] AvailableServices  => Device?.AvailableServices ?? [];
     public Guid[] AvailableServices  => Device?.AvailableServices ?? [];
-    public DateTime LastSeen { get; set; } = device.LastSeen;
+    public DateTime LastSeen { get; set; }
+
+    public void Dispose()
+    {
+        Device?.Dispose();
+        Device = null;
+    }
 }
 }

+ 9 - 6
InABox.Avalonia.Platform.Desktop/Bluetooth/Desktop_ConnectedBluetoothDevice.cs

@@ -3,12 +3,15 @@ using BluetoothLENet;
 
 
 namespace InABox.Avalonia.Platform.Desktop;
 namespace InABox.Avalonia.Platform.Desktop;
 
 
-public class Desktop_ConnectedBluetoothDevice(BLEDevice device)
-    : Desktop_BluetoothDevice(device), IConnectedBluetoothDevice
+public class Desktop_ConnectedBluetoothDevice : Desktop_BluetoothDevice, IConnectedBluetoothDevice
 {
 {
+    public Desktop_ConnectedBluetoothDevice(BLEDevice device) : base(device)
+    {
+    }
+
     public async Task<bool> WriteBytesAsync(Guid serviceid, Guid characteristicid, byte[] data)
     public async Task<bool> WriteBytesAsync(Guid serviceid, Guid characteristicid, byte[] data)
     {
     {
-        var service = Device.Services.FirstOrDefault(x=>x.Native.Uuid == serviceid);
+        var service = Device?.Services.FirstOrDefault(x=>x.Native.Uuid == serviceid);
         if (service != null)
         if (service != null)
         {
         {
             var characteristic = service.Characteristics.FirstOrDefault(x=>x.Native.Uuid == characteristicid);
             var characteristic = service.Characteristics.FirstOrDefault(x=>x.Native.Uuid == characteristicid);
@@ -36,10 +39,10 @@ public class Desktop_ConnectedBluetoothDevice(BLEDevice device)
 
 
     public async Task<byte[]?> ReadBytesAsync(Guid serviceid, Guid characteristicid)
     public async Task<byte[]?> ReadBytesAsync(Guid serviceid, Guid characteristicid)
     {
     {
-        var service = Device.Services.FirstOrDefault(x=>x.Native.Uuid == serviceid);
+        var service = Device?.Services.FirstOrDefault(x=>x.Native?.Uuid == serviceid);
         if (service != null)
         if (service != null)
         {
         {
-            var characteristic = service.Characteristics.FirstOrDefault(x=>x.Native.Uuid == characteristicid);
+            var characteristic = service.Characteristics.FirstOrDefault(x=>x.Native?.Uuid == characteristicid);
             if (characteristic != null)
             if (characteristic != null)
                 return await characteristic.ReadAsync();
                 return await characteristic.ReadAsync();
         }
         }
@@ -49,6 +52,6 @@ public class Desktop_ConnectedBluetoothDevice(BLEDevice device)
     public async Task<String?> ReadStringAsync(Guid serviceid, Guid characteristicid)
     public async Task<String?> ReadStringAsync(Guid serviceid, Guid characteristicid)
     {
     {
         var data = await ReadBytesAsync(serviceid,characteristicid);
         var data = await ReadBytesAsync(serviceid,characteristicid);
-        return data != null ? System.Text.Encoding.UTF8.GetString(data) : null;
+        return data != null ? Encoding.UTF8.GetString(data) : null;
     }
     }
 }
 }

+ 8 - 4
InABox.Avalonia.Platform.Desktop/InABox.Avalonia.Platform.Desktop.csproj

@@ -3,8 +3,10 @@
     <PropertyGroup>
     <PropertyGroup>
         <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
         <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <ImplicitUsings>enable</ImplicitUsings>
+        <RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
         <Nullable>enable</Nullable>
         <Nullable>enable</Nullable>
-        <RootNamespace>InABox.Avalonia.Platform.Desktop2</RootNamespace>
+        <RootNamespace>InABox.Avalonia.Platform.Desktop</RootNamespace>
+        <EnableMsixTooling>true</EnableMsixTooling>
     </PropertyGroup>
     </PropertyGroup>
 
 
     <ItemGroup>
     <ItemGroup>
@@ -13,8 +15,10 @@
     </ItemGroup>
     </ItemGroup>
 
 
     <ItemGroup>
     <ItemGroup>
-      <PackageReference Include="PDFtoImage" Version="5.0.0" />
-      <PackageReference Include="SkiaSharp" Version="3.116.1" />
+        <PackageReference Include="bblanchon.PDFium.Win32" Version="135.0.7019" />
+        <PackageReference Include="Microsoft.Maui.Essentials" Version="8.0.93" />
+        <PackageReference Include="PDFtoImage" Version="5.0.0" />
+        <PackageReference Include="SkiaSharp" Version="3.116.1" />
     </ItemGroup>
     </ItemGroup>
-
+    
 </Project>
 </Project>

+ 2 - 1
InABox.Avalonia.Platform.iOS/InABox.Avalonia.Platform.iOS.csproj

@@ -23,8 +23,9 @@
     <ItemGroup>
     <ItemGroup>
       <PackageReference Include="Avalonia.iOS" Version="11.2.3" />
       <PackageReference Include="Avalonia.iOS" Version="11.2.3" />
       <PackageReference Include="bblanchon.PDFium.iOS" Version="135.0.7009" />
       <PackageReference Include="bblanchon.PDFium.iOS" Version="135.0.7009" />
-      <PackageReference Include="Microsoft.Maui.Essentials" Version="9.0.40" />
+      <PackageReference Include="Microsoft.Maui.Essentials" Version="8.0.93" />
       <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
       <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
+      <PackageReference Include="SkiaSharp" Version="3.116.1" />
     </ItemGroup>
     </ItemGroup>
     
     
 </Project>
 </Project>

+ 1 - 1
InABox.Avalonia.Platform/Bluetooth/IBluetooth.cs

@@ -4,7 +4,7 @@ using InABox.Core;
 
 
 namespace InABox.Avalonia.Platform;
 namespace InABox.Avalonia.Platform;
 
 
-public interface IBluetoothDevice
+public interface IBluetoothDevice : IDisposable
 {
 {
      String ID { get; }
      String ID { get; }
      
      

+ 1 - 1
InABox.Avalonia.Platform/InABox.Avalonia.Platform.csproj

@@ -10,7 +10,7 @@
 
 
     <ItemGroup>
     <ItemGroup>
       <PackageReference Include="Autofac" Version="8.2.0" />
       <PackageReference Include="Autofac" Version="8.2.0" />
-      <PackageReference Include="Microsoft.Maui.Essentials" Version="9.0.40" />
+      <PackageReference Include="Microsoft.Maui.Essentials" Version="8.0.93" />
     </ItemGroup>
     </ItemGroup>
 
 
     <ItemGroup>
     <ItemGroup>

+ 6 - 0
InABox.Avalonia.Platform/PlatformTools.cs

@@ -1,6 +1,7 @@
 using Autofac;
 using Autofac;
 using InABox.Avalonia.Platform;
 using InABox.Avalonia.Platform;
 using InABox.Core;
 using InABox.Core;
+using Exception = System.Exception;
 
 
 namespace InABox.Avalonia.Platform;
 namespace InABox.Avalonia.Platform;
 
 
@@ -50,6 +51,11 @@ public static class PlatformTools
         }
         }
     }
     }
     
     
+    public static Guid DigitalKeyServiceId = Guid.Parse("ce6c0b18-0000-1000-8000-00805F9B34FB");
+    public static Guid DigitalKeyConfigId = Guid.Parse("447c1982-77ef-49be-a39a-2920f33c31e5");
+    public static Guid DigitalKeyControlId = Guid.Parse("5b804487-b73f-406a-8240-649c23ad1590");
+    public static double ScanTimeoutInSeconds = 15;
+    
     private static IBluetooth? _bluetooth;
     private static IBluetooth? _bluetooth;
     public static IBluetooth Bluetooth
     public static IBluetooth Bluetooth
     {
     {

+ 10 - 17
InABox.Avalonia/Components/CountdownTimer/AvaloniaCountdownTimer.cs

@@ -2,22 +2,15 @@
 using Avalonia.Controls;
 using Avalonia.Controls;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Threading;
 using Avalonia.Threading;
-using System;
-using System.IO;
-using System.Net;
 using System.Windows.Input;
 using System.Windows.Input;
-using Avalonia.Data;
-using Avalonia.Svg.Skia;
 using InABox.Core;
 using InABox.Core;
-using SkiaSharp;
-using Svg.Skia;
 
 
 namespace InABox.Avalonia.Components;
 namespace InABox.Avalonia.Components;
 
 
 public class CircularCountdownTimer : Control
 public class CircularCountdownTimer : Control
 {
 {
-    private DispatcherTimer _timer;
-    private DateTime? _startTime = null;
+    private readonly DispatcherTimer _timer;
+    private DateTime? _startTime;
     
     
     public static readonly StyledProperty<IImage?> ImageProperty =
     public static readonly StyledProperty<IImage?> ImageProperty =
         AvaloniaProperty.Register<CircularCountdownTimer, IImage?>(nameof(Image));
         AvaloniaProperty.Register<CircularCountdownTimer, IImage?>(nameof(Image));
@@ -37,19 +30,19 @@ public class CircularCountdownTimer : Control
         set => SetValue(IsActiveProperty, value);
         set => SetValue(IsActiveProperty, value);
     }
     }
     
     
-    public static readonly StyledProperty<ICommand> StartedProperty =
-        AvaloniaProperty.Register<CircularCountdownTimer, ICommand>(nameof(Started));
+    public static readonly StyledProperty<ICommand?> StartedProperty =
+        AvaloniaProperty.Register<CircularCountdownTimer, ICommand?>(nameof(Started));
 
 
-    public ICommand Started
+    public ICommand? Started
     {
     {
         get => GetValue(StartedProperty);
         get => GetValue(StartedProperty);
         set => SetValue(StartedProperty, value);
         set => SetValue(StartedProperty, value);
     }
     }
     
     
-    public static readonly StyledProperty<ICommand> StoppedProperty =
-        AvaloniaProperty.Register<CircularCountdownTimer, ICommand>(nameof(Stopped));
+    public static readonly StyledProperty<ICommand?> StoppedProperty =
+        AvaloniaProperty.Register<CircularCountdownTimer, ICommand?>(nameof(Stopped));
 
 
-    public ICommand Stopped
+    public ICommand? Stopped
     {
     {
         get => GetValue(StoppedProperty);
         get => GetValue(StoppedProperty);
         set => SetValue(StoppedProperty, value);
         set => SetValue(StoppedProperty, value);
@@ -119,9 +112,9 @@ public class CircularCountdownTimer : Control
     
     
     public CircularCountdownTimer()
     public CircularCountdownTimer()
     {
     {
-        _timer = new DispatcherTimer
+        _timer = new DispatcherTimer(DispatcherPriority.MaxValue)
         {
         {
-            Interval = TimeSpan.FromMilliseconds(100) // Update every 100ms
+            Interval = TimeSpan.FromMilliseconds(200),
         };
         };
         _timer.Tick += TimerTick;
         _timer.Tick += TimerTick;
     }
     }

+ 20 - 0
InABox.Avalonia/Converters/DateTimeToColorConverter.cs

@@ -0,0 +1,20 @@
+using Avalonia.Media;
+
+namespace InABox.Avalonia.Converters;
+
+public class DateTimeToColorConverter : AbstractConverter<DateTime,IBrush?>
+{
+
+    public DateTime? Threshold { get; set; } = null;    
+    
+    public IBrush? Expired{ get; set; }
+        
+    public IBrush? Current { get; set; }
+        
+    protected override IBrush? Convert(DateTime value, object? parameter = null)
+    {
+        var threshold = Threshold ?? DateTime.Now;
+        return value < threshold ? Expired : Current;
+    }
+
+}

+ 6 - 1
InABox.Avalonia/InABox.Avalonia.csproj

@@ -29,9 +29,10 @@
       <PackageReference Include="Material.Avalonia" Version="3.9.2" />
       <PackageReference Include="Material.Avalonia" Version="3.9.2" />
       <PackageReference Include="Material.Avalonia.DataGrid" Version="3.9.2" />
       <PackageReference Include="Material.Avalonia.DataGrid" Version="3.9.2" />
       <PackageReference Include="Material.Avalonia.Dialogs" Version="3.9.2" />
       <PackageReference Include="Material.Avalonia.Dialogs" Version="3.9.2" />
-      <PackageReference Include="Microsoft.Maui.Essentials" Version="9.0.40" />
+      <PackageReference Include="Microsoft.Maui.Essentials" Version="8.0.93" />
       <PackageReference Include="Serilog" Version="4.2.0" />
       <PackageReference Include="Serilog" Version="4.2.0" />
       <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
       <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
+      <PackageReference Include="SkiaSharp" Version="3.116.1" />
       <PackageReference Include="Syncfusion.Licensing" Version="26.2.14" />
       <PackageReference Include="Syncfusion.Licensing" Version="26.2.14" />
       <PackageReference Include="Syncfusion.Pdf.Net.Core" Version="26.2.14" />
       <PackageReference Include="Syncfusion.Pdf.Net.Core" Version="26.2.14" />
     </ItemGroup>
     </ItemGroup>
@@ -67,4 +68,8 @@
       <Folder Include="Components\Modules\" />
       <Folder Include="Components\Modules\" />
     </ItemGroup>
     </ItemGroup>
 
 
+    <ItemGroup>
+      <UpToDateCheckInput Remove="Dialogs\TextBoxDialog\TextBoxDialogView.axaml" />
+    </ItemGroup>
+
 </Project>
 </Project>

+ 82 - 0
InABox.Core/Authenticator.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Linq;
+using System.Security.Cryptography;
+
+namespace InABox.Core
+{
+    public static class Authenticator
+    {
+        
+        public static readonly int CODE_LENGTH = 6;
+        
+        private static readonly int CODE_MODULO = (int)Math.Pow(10, CODE_LENGTH);
+        
+        private static byte[] FromHexString(string hex) {
+            if (hex.Length % 2 == 1)
+                throw new Exception("The binary key cannot have an odd number of digits");
+        
+            byte[] arr = new byte[hex.Length >> 1];
+        
+            for (int i = 0; i < hex.Length >> 1; ++i)
+            {
+                arr[i] = (byte)((GetHexVal(hex[i << 1]) << 4) + (GetHexVal(hex[(i << 1) + 1])));
+            }
+        
+            return arr;
+        }
+        
+        public static int GetHexVal(char hex) {
+            int val = (int)hex;
+            //For uppercase A-F letters:
+            //return val - (val < 58 ? 48 : 55);
+            //For lowercase a-f letters:
+            //return val - (val < 58 ? 48 : 87);
+            //Or the two combined, but a bit slower:
+            return val - (val < 58 ? 48 : (val < 97 ? 55 : 87));
+        }
+
+        public static string GenerateGoogleAuthenticatorCode(byte[] key)
+        {
+            var _time = DateTimeOffset.Now.ToUnixTimeSeconds();
+            return GenerateGoogleAuthenticatorCode(_time, key);
+        }
+        
+        private static string GenerateGoogleAuthenticatorCode(long time, byte[] key)
+        {
+            var _window = time / 30;
+            var _hex = _window.ToString("x");
+            if (_hex.Length < 16)
+            {
+                _hex = _hex.PadLeft(16, '0');
+            }
+            var _bytes = FromHexString(_hex);
+            var _hash = new HMACSHA1(key).ComputeHash(_bytes);
+
+            var _offset = _hash.Last() & 0xf;
+            var _selected = new byte[4];
+            Buffer.BlockCopy(_hash, _offset, _selected, 0, 4);
+            if (BitConverter.IsLittleEndian)
+            {
+                Array.Reverse(_selected);
+            }
+
+            var _integer = BitConverter.ToInt32(_selected, 0);
+            var _truncated = _integer & 0x7fffffff;
+
+            return (_truncated % CODE_MODULO).ToString().PadLeft(CODE_LENGTH, '0');
+        }
+
+        public static bool CheckAuthenticationCode(byte[] key, string code)
+        {
+            var _time = DateTimeOffset.Now.ToUnixTimeSeconds();
+            for (long _l = _time - 30; _l <= _time; _l += 30)
+            {
+                if(GenerateGoogleAuthenticatorCode(_l, key) == code)
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+}

+ 1 - 0
InABox.Core/CoreTable/CoreTable.cs

@@ -232,6 +232,7 @@ namespace InABox.Core
             var result = new Dictionary<TKey, TValue>();
             var result = new Dictionary<TKey, TValue>();
             var sorted = sort == null ? Rows : Rows.OrderBy(x => x.Get(sort)).ToList();
             var sorted = sort == null ? Rows : Rows.OrderBy(x => x.Get(sort)).ToList();
             foreach (var row in sorted)
             foreach (var row in sorted)
+                // This threw an exception once.
                 result[row.Get(key)] = row.Get(value);
                 result[row.Get(key)] = row.Get(value);
             return result;
             return result;
         }
         }

+ 3 - 0
InABox.Serialization.Shared/InABox.Serialization.Shared.shproj

@@ -10,6 +10,9 @@
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
   <PropertyGroup />
   <PropertyGroup />
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <OutputPath>bin\Debug\</OutputPath>
+  </PropertyGroup>
   <Import Project="InABox.Serialization.Shared.projitems" Label="Shared" />
   <Import Project="InABox.Serialization.Shared.projitems" Label="Shared" />
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
 </Project>
 </Project>

+ 1 - 1
inabox.wpf/Forms/MessageWindow.xaml.cs

@@ -326,7 +326,7 @@ public partial class MessageWindow : Window, INotifyPropertyChanged
 
 
     private static void EmailLogs_Click(Exception e)
     private static void EmailLogs_Click(Exception e)
     {
     {
-        var logFile = CoreUtils.GetVersion();
+        var logFile = Path.Combine(CoreUtils.GetPath(), $"{DateTime.Today:yyyy-MM-dd}.log");
 
 
         const int nRead = 1024 * 1024;
         const int nRead = 1024 * 1024;