EllieHub 1.0.3.0

This commit is contained in:
Toastie (DCS Team) 2024-07-02 12:49:31 +12:00
commit 9180716702
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
8 changed files with 76 additions and 47 deletions

View file

@ -8,7 +8,6 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors> <WarningsAsErrors>Nullable</WarningsAsErrors>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild> <EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile> <GenerateDocumentationFile>True</GenerateDocumentationFile>
<BuiltInComInteropSupport>True</BuiltInComInteropSupport> <BuiltInComInteropSupport>True</BuiltInComInteropSupport>
@ -16,11 +15,12 @@
<!--Publishing--> <!--Publishing-->
<PublishSingleFile>true</PublishSingleFile> <PublishSingleFile>true</PublishSingleFile>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
<!--Version--> <!--Version-->
<VersionPrefix>1.0.2.0</VersionPrefix> <VersionPrefix>1.0.3.0</VersionPrefix>
<!--Avalonia Settings--> <!--Avalonia Settings-->
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
@ -30,6 +30,13 @@
<UseAppHost>true</UseAppHost> <UseAppHost>true</UseAppHost>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<AvaloniaResource Include="Assets\**" /> <AvaloniaResource Include="Assets\**" />

View file

@ -83,8 +83,8 @@ public static class WindowExt
{ {
return (!activeView.TryFindResource(resourceName, theme, out var resource)) return (!activeView.TryFindResource(resourceName, theme, out var resource))
? throw new InvalidOperationException($"Resource '{resourceName}' was not found.") ? throw new InvalidOperationException($"Resource '{resourceName}' was not found.")
: (!Utilities.TryCastTo<T>(resource, out var result)) : (resource is T castResource)
? throw new InvalidCastException($"Could not convert resource of type '{resource?.GetType()?.FullName}' to '{nameof(T)}'.") ? castResource
: result; : throw new InvalidCastException($"Could not convert resource of type '{resource?.GetType()?.FullName}' to '{nameof(T)}'.");
} }
} }

View file

@ -20,7 +20,7 @@ namespace EllieHub.Features.AppConfig.ViewModels;
/// </summary> /// </summary>
public class ConfigViewModel : ViewModelBase<ConfigView> public class ConfigViewModel : ViewModelBase<ConfigView>
{ {
private static readonly string _unixNotice = Environment.OSVersion.Platform is not PlatformID.Unix private static readonly string _unixNotice = (Environment.OSVersion.Platform is not PlatformID.Unix)
? string.Empty ? string.Empty
: Environment.NewLine + "To make the dependencies accessible to your bot instances without this updater, consider installing " + : Environment.NewLine + "To make the dependencies accessible to your bot instances without this updater, consider installing " +
$"them through your package manager or adding the directory \"{AppStatics.AppDepsUri}\" to your PATH environment variable."; $"them through your package manager or adding the directory \"{AppStatics.AppDepsUri}\" to your PATH environment variable.";

View file

@ -69,10 +69,10 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
/// <exception cref="InvalidOperationException">Occurs when the visual tree has an unexpected structure.</exception> /// <exception cref="InvalidOperationException">Occurs when the visual tree has an unexpected structure.</exception>
public void ApplyBotButtonBorder(Button button) public void ApplyBotButtonBorder(Button button)
{ {
if (!Utilities.TryCastTo<Border>(button.Parent?.Parent, out var border)) if (button.Parent?.Parent is not Border border)
throw new InvalidOperationException("Visual tree has an unexpected structure."); throw new InvalidOperationException("Visual tree has an unexpected structure.");
if (!Utilities.TryCastTo<ImmutableSolidColorBrush>(this.FindResource(base.ActualThemeVariant, "BotSelectionColor"), out var resourceColor)) if (this.FindResource(base.ActualThemeVariant, "BotSelectionColor") is not ImmutableSolidColorBrush resourceColor)
return; return;
border.BorderBrush = resourceColor; border.BorderBrush = resourceColor;
@ -95,7 +95,7 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
private void LoadBotViewModel(object sender, RoutedEventArgs eventArgs) private void LoadBotViewModel(object sender, RoutedEventArgs eventArgs)
{ {
// "sender", for some reason, is not one of the buttons stored in the lateral bar's view-model. // "sender", for some reason, is not one of the buttons stored in the lateral bar's view-model.
if (Utilities.TryCastTo<Button>(sender, out var button) && this.ViewModel!.BotButtonList.First(x => x.Content == button.Content).IsEnabled) if (sender is Button button && this.ViewModel!.BotButtonList.First(x => x.Content == button.Content).IsEnabled)
BotButtonClick?.Invoke(button, eventArgs); BotButtonClick?.Invoke(button, eventArgs);
} }
@ -108,10 +108,11 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
/// <exception cref="InvalidOperationException">Occurs when the visual tree has an unexpected structure.</exception> /// <exception cref="InvalidOperationException">Occurs when the visual tree has an unexpected structure.</exception>
private void OnBotButtonLoad(object? sender, VisualTreeAttachmentEventArgs eventArgs) private void OnBotButtonLoad(object? sender, VisualTreeAttachmentEventArgs eventArgs)
{ {
if (!Utilities.TryCastTo<Panel>(sender, out var panel) if (sender is not Panel panel
|| !Utilities.TryCastTo<SKImageView>(((Border)panel.Children[0]).Child, out var botAvatar) || panel.Children[0] is not Border border
|| !Utilities.TryCastTo<Button>(panel.Children[1], out var button) || border.Child is not SKImageView botAvatar
|| !Utilities.TryCastTo<Guid>(button.Content, out var botId)) || panel.Children[1] is not Button button
|| button.Content is not Guid botId)
throw new InvalidOperationException("Visual tree has an unexpected structure."); throw new InvalidOperationException("Visual tree has an unexpected structure.");
// Set the avatar // Set the avatar
@ -132,7 +133,7 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
/// <exception cref="InvalidOperationException">Occurs when <paramref name="sender"/> is not a <see cref="Button"/>.</exception> /// <exception cref="InvalidOperationException">Occurs when <paramref name="sender"/> is not a <see cref="Button"/>.</exception>
private void DownsizeBotAvatar(object? sender, PointerPressedEventArgs eventArgs) private void DownsizeBotAvatar(object? sender, PointerPressedEventArgs eventArgs)
{ {
if (!Utilities.TryCastTo<Button>(sender, out var button)) if (sender is not Button button)
throw new InvalidOperationException($"Sender is not a {nameof(Button)}."); throw new InvalidOperationException($"Sender is not a {nameof(Button)}.");
var botAvatar = FindAvatarComponent(button); var botAvatar = FindAvatarComponent(button);
@ -149,7 +150,7 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
/// <exception cref="InvalidOperationException">Occurs when <paramref name="sender"/> is not a <see cref="Button"/>.</exception> /// <exception cref="InvalidOperationException">Occurs when <paramref name="sender"/> is not a <see cref="Button"/>.</exception>
private void UpsizeBotAvatar(object? sender, PointerReleasedEventArgs eventArgs) private void UpsizeBotAvatar(object? sender, PointerReleasedEventArgs eventArgs)
{ {
if (!Utilities.TryCastTo<Button>(sender, out var button)) if (sender is not Button button)
throw new InvalidOperationException($"Sender is not a {nameof(Button)}."); throw new InvalidOperationException($"Sender is not a {nameof(Button)}.");
var botAvatar = FindAvatarComponent(button); var botAvatar = FindAvatarComponent(button);
@ -167,7 +168,7 @@ public partial class LateralBarView : ReactiveUserControl<LateralBarViewModel>
/// <exception cref="InvalidOperationException">Occurs when the component's content is not a Guid.</exception> /// <exception cref="InvalidOperationException">Occurs when the component's content is not a Guid.</exception>
private SKImageView FindAvatarComponent<T>(T component) where T : ContentControl private SKImageView FindAvatarComponent<T>(T component) where T : ContentControl
{ {
return (!Utilities.TryCastTo<Guid>(component.Content, out var botId)) return (component.Content is not Guid botId)
? throw new InvalidOperationException($"{nameof(T)} does not contain a bot Id.") ? throw new InvalidOperationException($"{nameof(T)} does not contain a bot Id.")
: FindAvatarComponent(botId); : FindAvatarComponent(botId);
} }

View file

@ -293,7 +293,7 @@ public partial class AppView : ReactiveWindow<AppViewModel>
private static BotConfigViewModel GetBotConfigViewModel(Button button, IServiceScopeFactory scopeFactory) private static BotConfigViewModel GetBotConfigViewModel(Button button, IServiceScopeFactory scopeFactory)
{ {
using var scope = scopeFactory.CreateScope(); using var scope = scopeFactory.CreateScope();
var botId = (Guid)(button.Content ?? throw new InvalidOperationException("Bot button has no valid Id.")); var botId = (button.Content ?? throw new InvalidOperationException("Bot button has no valid Id."));
var botResolver = scope.ServiceProvider.GetParameterizedService<EllieResolver>(botId); var botResolver = scope.ServiceProvider.GetParameterizedService<EllieResolver>(botId);
return scope.ServiceProvider.GetParameterizedService<BotConfigViewModel>(botResolver); return scope.ServiceProvider.GetParameterizedService<BotConfigViewModel>(botResolver);

View file

@ -114,34 +114,10 @@ public sealed partial class EllieResolver : IBotResolver
await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = null }, cToken); await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = null }, cToken);
return null; return null;
} }
if (!string.IsNullOrWhiteSpace(botEntry.Version))
return botEntry.Version;
// Ellie is published as a single-file binary, so we have to extract return (string.IsNullOrWhiteSpace(botEntry.Version))
// its contents first in order to read the assembly for its version. ? await GetBotVersionFromAssemblyAsync(executableUri, cToken)
using var executableReader = new ExecutableReader(executableUri); : botEntry.Version;
var extractDirectoryUri = Path.Combine(_tempDirectory, "EllieBotExtract");
var extractAssemblyUri = Path.Combine(extractDirectoryUri, "EllieBot.dll");
try
{
await executableReader.ExtractToDirectoryAsync(extractDirectoryUri, cToken);
var nadekoAssembly = Assembly.LoadFile(extractAssemblyUri);
var version = nadekoAssembly.GetName().Version
?? throw new InvalidOperationException($"Could not find version of the assembly at {extractAssemblyUri}.");
var currentVersion = $"{version.Major}.{version.Minor}.{version.Build}";
await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = currentVersion }, cToken);
return currentVersion;
}
finally
{
Utilities.TryDeleteDirectory(extractDirectoryUri);
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -397,6 +373,51 @@ public sealed partial class EllieResolver : IBotResolver
return response; return response;
} }
/// <summary>
/// Gets the bot version from the bot's assembly.
/// </summary>
/// <param name="executableUri">The path to the bot's executable file.</param>
/// <param name="cToken">The cancellation token.</param>
/// <returns>The version of the bot or <see langword="null"/> if the executable file is not found.</returns>
/// <exception cref="InvalidOperationException">Occurs when the assembly file is not found.</exception>
private async ValueTask<string?> GetBotVersionFromAssemblyAsync(string executableUri, CancellationToken cToken)
{
if (!File.Exists(executableUri))
return null;
var directoryUri = Directory.GetParent(executableUri)?.FullName ?? Path.GetPathRoot(executableUri)!;
var assemblyUri = Path.Join(directoryUri, "EllieBot.dll");
var isSingleFile = !File.Exists(assemblyUri);
// If Ellie is published as a single-file binary, we have to extract
// its contents first in order to read the assembly for its version.
if (isSingleFile)
{
directoryUri = Path.Join(_tempDirectory, "EllieBotExtract_" + DateTimeOffset.Now.Ticks);
assemblyUri = Path.Join(directoryUri, "EllieBot.dll");
using var executableReader = new ExecutableReader(executableUri);
await executableReader.ExtractToDirectoryAsync(directoryUri, cToken);
}
try
{
var ellieAssembly = Assembly.LoadFile(assemblyUri);
var version = ellieAssembly.GetName().Version
?? throw new InvalidOperationException($"Could not find version for the assembly at {assemblyUri}.");
var currentVersion = $"{version.Major}.{version.Minor}.{version.Build}";
await _appConfigManager.UpdateBotEntryAsync(Id, x => x with { Version = currentVersion }, cToken);
return currentVersion;
}
finally
{
if (isSingleFile)
Utilities.TryDeleteDirectory(directoryUri);
}
}
[GeneratedRegex(@"^(?:\S+\-)(\S+\-\S+)\-", RegexOptions.Compiled)] [GeneratedRegex(@"^(?:\S+\-)(\S+\-\S+)\-", RegexOptions.Compiled)]
private static partial Regex GenerateUnzipedDirRegex(); private static partial Regex GenerateUnzipedDirRegex();
} }

View file

@ -26,7 +26,7 @@ public partial class BotConfigView : ReactiveUserControl<BotConfigViewModel>
/// <param name="eventArgs">The event arguments.</param> /// <param name="eventArgs">The event arguments.</param>
private void AvatarButtonHover(object? sender, PointerEventArgs eventArgs) private void AvatarButtonHover(object? sender, PointerEventArgs eventArgs)
{ {
if (!Utilities.TryCastTo<Button>(sender, out var button)) if (sender is not Button button)
return; return;
button.Opacity = 3.0; button.Opacity = 3.0;
@ -40,7 +40,7 @@ public partial class BotConfigView : ReactiveUserControl<BotConfigViewModel>
/// <param name="eventArgs">The event arguments.</param> /// <param name="eventArgs">The event arguments.</param>
private void AvatarButtonUnhover(object? sender, PointerEventArgs eventArgs) private void AvatarButtonUnhover(object? sender, PointerEventArgs eventArgs)
{ {
if (!Utilities.TryCastTo<Button>(sender, out var button)) if (sender is not Button button)
return; return;
button.Opacity = 0.0; button.Opacity = 0.0;

View file

@ -142,7 +142,7 @@ public sealed class AppResolver : IAppResolver
// Rename the original file from "file" to "file_old". // Rename the original file from "file" to "file_old".
if (File.Exists(destinationUri)) if (File.Exists(destinationUri))
File.Move(destinationUri, destinationUri + OldFileSuffix); File.Move(destinationUri, destinationUri + OldFileSuffix, true);
// Move the new file to the application's directory. // Move the new file to the application's directory.
if (Environment.OSVersion.Platform is not PlatformID.Unix) if (Environment.OSVersion.Platform is not PlatformID.Unix)