Compare commits

..

No commits in common. "918071670235258ca52a7e2ac9366b9d4b31f505" and "ff2957f935ee25ad1dd19792be3c9520b8c8453c" have entirely different histories.

8 changed files with 47 additions and 76 deletions

View file

@ -8,6 +8,7 @@
<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>
@ -15,12 +16,11 @@
<!--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.3.0</VersionPrefix> <VersionPrefix>1.0.2.0</VersionPrefix>
<!--Avalonia Settings--> <!--Avalonia Settings-->
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
@ -30,13 +30,6 @@
<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.")
: (resource is T castResource) : (!Utilities.TryCastTo<T>(resource, out var result))
? castResource ? throw new InvalidCastException($"Could not convert resource of type '{resource?.GetType()?.FullName}' to '{nameof(T)}'.")
: throw new InvalidCastException($"Could not convert resource of type '{resource?.GetType()?.FullName}' to '{nameof(T)}'."); : result;
} }
} }

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 (button.Parent?.Parent is not Border border) if (!Utilities.TryCastTo<Border>(button.Parent?.Parent, out var border))
throw new InvalidOperationException("Visual tree has an unexpected structure."); throw new InvalidOperationException("Visual tree has an unexpected structure.");
if (this.FindResource(base.ActualThemeVariant, "BotSelectionColor") is not ImmutableSolidColorBrush resourceColor) if (!Utilities.TryCastTo<ImmutableSolidColorBrush>(this.FindResource(base.ActualThemeVariant, "BotSelectionColor"), out var 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 (sender is Button button && this.ViewModel!.BotButtonList.First(x => x.Content == button.Content).IsEnabled) if (Utilities.TryCastTo<Button>(sender, out var button) && this.ViewModel!.BotButtonList.First(x => x.Content == button.Content).IsEnabled)
BotButtonClick?.Invoke(button, eventArgs); BotButtonClick?.Invoke(button, eventArgs);
} }
@ -108,11 +108,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>
private void OnBotButtonLoad(object? sender, VisualTreeAttachmentEventArgs eventArgs) private void OnBotButtonLoad(object? sender, VisualTreeAttachmentEventArgs eventArgs)
{ {
if (sender is not Panel panel if (!Utilities.TryCastTo<Panel>(sender, out var panel)
|| panel.Children[0] is not Border border || !Utilities.TryCastTo<SKImageView>(((Border)panel.Children[0]).Child, out var botAvatar)
|| border.Child is not SKImageView botAvatar || !Utilities.TryCastTo<Button>(panel.Children[1], out var button)
|| panel.Children[1] is not Button button || !Utilities.TryCastTo<Guid>(button.Content, out var botId))
|| 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
@ -133,7 +132,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 (sender is not Button button) if (!Utilities.TryCastTo<Button>(sender, out var 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);
@ -150,7 +149,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 (sender is not Button button) if (!Utilities.TryCastTo<Button>(sender, out var 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);
@ -168,7 +167,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 (component.Content is not Guid botId) return (!Utilities.TryCastTo<Guid>(component.Content, out var 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 = (button.Content ?? throw new InvalidOperationException("Bot button has no valid Id.")); var botId = (Guid)(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,10 +114,34 @@ 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;
return (string.IsNullOrWhiteSpace(botEntry.Version)) // Ellie is published as a single-file binary, so we have to extract
? await GetBotVersionFromAssemblyAsync(executableUri, cToken) // its contents first in order to read the assembly for its version.
: botEntry.Version; using var executableReader = new ExecutableReader(executableUri);
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/>
@ -373,51 +397,6 @@ 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 (sender is not Button button) if (!Utilities.TryCastTo<Button>(sender, out var 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 (sender is not Button button) if (!Utilities.TryCastTo<Button>(sender, out var 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, true); File.Move(destinationUri, destinationUri + OldFileSuffix);
// 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)