diff --git a/ModerationClient/App.axaml b/ModerationClient/App.axaml
new file mode 100644
index 0000000..82366b6
--- /dev/null
+++ b/ModerationClient/App.axaml
@@ -0,0 +1,15 @@
+<Application xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ x:Class="ModerationClient.App"
+ xmlns:local="using:ModerationClient"
+ RequestedThemeVariant="Default">
+ <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
+
+ <Application.DataTemplates>
+ <local:ViewLocator/>
+ </Application.DataTemplates>
+
+ <Application.Styles>
+ <FluentTheme />
+ </Application.Styles>
+</Application>
\ No newline at end of file
diff --git a/ModerationClient/App.axaml.cs b/ModerationClient/App.axaml.cs
new file mode 100644
index 0000000..db584de
--- /dev/null
+++ b/ModerationClient/App.axaml.cs
@@ -0,0 +1,91 @@
+using System;
+using System.IO;
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Data.Core;
+using Avalonia.Data.Core.Plugins;
+using Avalonia.Markup.Xaml;
+using LibMatrix.Services;
+using MatrixUtils.Abstractions;
+using MatrixUtils.Desktop;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using ModerationClient.Services;
+using ModerationClient.ViewModels;
+using ModerationClient.Views;
+
+namespace ModerationClient;
+
+public partial class App : Application {
+ /// <summary>
+ /// Gets the current <see cref="App"/> instance in use
+ /// </summary>
+ public new static App Current => (App)Application.Current;
+
+ /// <summary>
+ /// Gets the <see cref="IServiceProvider"/> instance to resolve application services.
+ /// </summary>
+ public IServiceProvider Services => Host.Services;
+
+ public IHost Host { get; private set; }
+
+ public override void Initialize() {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ // ReSharper disable once AsyncVoidMethod
+ public override async void OnFrameworkInitializationCompleted() {
+ var builder = Microsoft.Extensions.Hosting.Host.CreateApplicationBuilder(Environment.GetCommandLineArgs());
+ builder.Services.AddTransient<MainWindowViewModel>();
+ ConfigureServices(builder.Services);
+ // builder.Services.AddHostedService<HostedBackgroundService>();
+
+ Host = builder.Build();
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
+ // Line below is needed to remove Avalonia data validation.
+ // Without this line you will get duplicate validations from both Avalonia and CT
+ BindingPlugins.DataValidators.RemoveAt(0);
+ // desktop.MainWindow = new MainWindow {
+ // DataContext = Host.Services.GetRequiredService<MainWindowViewModel>()
+ // };
+ desktop.MainWindow = Host.Services.GetRequiredService<MainWindow>();
+ desktop.Exit += (sender, args) => {
+ Host.StopAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult();
+ Host.Dispose();
+ };
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ await Host.StartAsync();
+ }
+
+ /// <summary>
+ /// Configures the services for the application.
+ /// </summary>
+ private static IServiceProvider ConfigureServices(IServiceCollection services) {
+ services.AddRoryLibMatrixServices(new() {
+ AppName = "ModerationClient",
+ });
+ services.AddSingleton<CommandLineConfiguration>();
+ services.AddSingleton<MatrixAuthenticationService>();
+ services.AddSingleton<ModerationClientConfiguration>();
+
+ services.AddSingleton<TieredStorageService>(x => {
+ var cmdLine = x.GetRequiredService<CommandLineConfiguration>();
+ return new TieredStorageService(
+ cacheStorageProvider: new FileStorageProvider(Directory.CreateTempSubdirectory($"modcli-{cmdLine.Profile}").FullName),
+ dataStorageProvider: new FileStorageProvider(Directory.CreateTempSubdirectory($"modcli-{cmdLine.Profile}").FullName)
+ );
+ }
+ );
+
+ // Register views
+ services.AddSingleton<MainWindow>();
+ services.AddTransient<LoginView>();
+ services.AddTransient<ClientView>();
+ // Register ViewModels
+ services.AddTransient<ClientViewModel>();
+
+ return services.BuildServiceProvider();
+ }
+}
\ No newline at end of file
diff --git a/ModerationClient/Assets/avalonia-logo.ico b/ModerationClient/Assets/avalonia-logo.ico
new file mode 100644
index 0000000..da8d49f
--- /dev/null
+++ b/ModerationClient/Assets/avalonia-logo.ico
Binary files differdiff --git a/ModerationClient/ModerationClient.csproj b/ModerationClient/ModerationClient.csproj
new file mode 100644
index 0000000..84adbc3
--- /dev/null
+++ b/ModerationClient/ModerationClient.csproj
@@ -0,0 +1,53 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>WinExe</OutputType>
+ <TargetFramework>net8.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
+ <ApplicationManifest>app.manifest</ApplicationManifest>
+ <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Folder Include="Models\"/>
+ <AvaloniaResource Include="Assets\**"/>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Avalonia" Version="11.1.0"/>
+ <PackageReference Include="Avalonia.Desktop" Version="11.1.0"/>
+ <PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.0"/>
+ <PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.0"/>
+ <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
+ <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.0"/>
+ <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1"/>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
+ <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\LibMatrix\LibMatrix\LibMatrix.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Content Include="appsettings*.json">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </Content>
+ </ItemGroup>
+
+ <ItemGroup>
+ <Compile Update="Views\LoginView.axaml.cs">
+ <DependentUpon>LoginWindow.axaml</DependentUpon>
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Update="Views\ClientView.axaml.cs">
+ <DependentUpon>ClientView.axaml</DependentUpon>
+ <SubType>Code</SubType>
+ </Compile>
+ </ItemGroup>
+
+ <ItemGroup>
+ <UpToDateCheckInput Remove="Windows\LoginWindow.axaml" />
+ </ItemGroup>
+</Project>
diff --git a/ModerationClient/Program.cs b/ModerationClient/Program.cs
new file mode 100644
index 0000000..9229194
--- /dev/null
+++ b/ModerationClient/Program.cs
@@ -0,0 +1,21 @@
+using Avalonia;
+using System;
+
+namespace ModerationClient;
+
+internal sealed 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()
+ .WithInterFont()
+ .LogToTrace();
+}
\ No newline at end of file
diff --git a/ModerationClient/Services/CommandLineConfiguration.cs b/ModerationClient/Services/CommandLineConfiguration.cs
new file mode 100644
index 0000000..4debd5c
--- /dev/null
+++ b/ModerationClient/Services/CommandLineConfiguration.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using ArcaneLibs;
+using Microsoft.Extensions.Logging;
+
+namespace ModerationClient.Services;
+
+public class CommandLineConfiguration {
+ public CommandLineConfiguration(ILogger<CommandLineConfiguration> logger) {
+ var args = Environment.GetCommandLineArgs();
+ logger.LogInformation("Command line arguments: " + string.Join(", ", args));
+ for (var i = 0; i < args.Length; i++) {
+ logger.LogInformation("Processing argument: " + args[i]);
+ switch (args[i]) {
+ case "--profile":
+ case "-p":
+ if (args.Length <= i + 1 || args[i + 1].StartsWith("-")) {
+ throw new ArgumentException("No profile specified");
+ }
+
+ Profile = args[++i];
+ logger.LogInformation("Set profile to: " + Profile);
+ break;
+ case "--temporary":
+ IsTemporary = true;
+ logger.LogInformation("Using temporary profile");
+ break;
+ case "--profile-dir":
+ ProfileDirectory = args[++i];
+ break;
+ case "--scale":
+ Scale = float.Parse(args[++i]);
+ break;
+ }
+ }
+
+ if (string.IsNullOrWhiteSpace(ProfileDirectory))
+ ProfileDirectory = IsTemporary
+ ? Directory.CreateTempSubdirectory("ModerationClient-tmp").FullName
+ : Util.ExpandPath($"$HOME/.local/share/ModerationClient/{Profile}");
+
+ logger.LogInformation("Profile directory: " + ProfileDirectory);
+ Directory.CreateDirectory(ProfileDirectory);
+ }
+
+ public string Profile { get; private set; } = "default";
+ public bool IsTemporary { get; private set; }
+
+ public string ProfileDirectory { get; private set; }
+ public float Scale { get; private set; } = 1f;
+}
\ No newline at end of file
diff --git a/ModerationClient/Services/FileStorageProvider.cs b/ModerationClient/Services/FileStorageProvider.cs
new file mode 100644
index 0000000..3658369
--- /dev/null
+++ b/ModerationClient/Services/FileStorageProvider.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using ArcaneLibs.Extensions;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces.Services;
+using Microsoft.Extensions.Logging;
+
+namespace MatrixUtils.Abstractions;
+
+public class FileStorageProvider : IStorageProvider {
+ private readonly ILogger<FileStorageProvider> _logger;
+
+ public string TargetPath { get; }
+
+ /// <summary>
+ /// Creates a new instance of <see cref="FileStorageProvider" />.
+ /// </summary>
+ /// <param name="targetPath"></param>
+ public FileStorageProvider(string targetPath) {
+ // new Logger<FileStorageProvider>(new LoggerFactory()).LogInformation("test");
+ Console.WriteLine($"Initialised FileStorageProvider with path {targetPath}");
+ TargetPath = targetPath;
+ if (!Directory.Exists(targetPath)) {
+ Directory.CreateDirectory(targetPath);
+ }
+ }
+
+ public async Task SaveObjectAsync<T>(string key, T value) => await File.WriteAllTextAsync(Path.Join(TargetPath, key), value?.ToJson());
+
+ [RequiresUnreferencedCode("This API uses reflection to deserialize JSON")]
+ public async Task<T?> LoadObjectAsync<T>(string key) => JsonSerializer.Deserialize<T>(await File.ReadAllTextAsync(Path.Join(TargetPath, key)));
+
+ public Task<bool> ObjectExistsAsync(string key) => Task.FromResult(File.Exists(Path.Join(TargetPath, key)));
+
+ public Task<List<string>> GetAllKeysAsync() => Task.FromResult(Directory.GetFiles(TargetPath).Select(Path.GetFileName).ToList());
+
+ public Task DeleteObjectAsync(string key) {
+ File.Delete(Path.Join(TargetPath, key));
+ return Task.CompletedTask;
+ }
+
+ public async Task SaveStreamAsync(string key, Stream stream) {
+ Directory.CreateDirectory(Path.GetDirectoryName(Path.Join(TargetPath, key)) ?? throw new InvalidOperationException());
+ await using var fileStream = File.Create(Path.Join(TargetPath, key));
+ await stream.CopyToAsync(fileStream);
+ }
+
+ public Task<Stream?> LoadStreamAsync(string key) => Task.FromResult<Stream?>(File.Exists(Path.Join(TargetPath, key)) ? File.OpenRead(Path.Join(TargetPath, key)) : null);
+}
diff --git a/ModerationClient/Services/MatrixAuthenticationService.cs b/ModerationClient/Services/MatrixAuthenticationService.cs
new file mode 100644
index 0000000..69f8810
--- /dev/null
+++ b/ModerationClient/Services/MatrixAuthenticationService.cs
@@ -0,0 +1,56 @@
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using ArcaneLibs;
+using ArcaneLibs.Extensions;
+using Avalonia.Controls.Diagnostics;
+using LibMatrix;
+using LibMatrix.Homeservers;
+using LibMatrix.Responses;
+using LibMatrix.Services;
+using MatrixUtils.Desktop;
+using Microsoft.Extensions.Logging;
+
+namespace ModerationClient.Services;
+
+public class MatrixAuthenticationService(ILogger<MatrixAuthenticationService> logger, HomeserverProviderService hsProvider, CommandLineConfiguration cfg) : NotifyPropertyChanged {
+ private bool _isLoggedIn = false;
+ public string Profile => cfg.Profile;
+ public AuthenticatedHomeserverGeneric? Homeserver { get; private set; }
+
+ public bool IsLoggedIn {
+ get => _isLoggedIn;
+ private set => SetField(ref _isLoggedIn, value);
+ }
+
+ public async Task LoadProfileAsync() {
+ if (!File.Exists(Util.ExpandPath($"{cfg.ProfileDirectory}/login.json")!)) return;
+ var loginJson = await File.ReadAllTextAsync(Util.ExpandPath($"{cfg.ProfileDirectory}/login.json")!);
+ var login = JsonSerializer.Deserialize<LoginResponse>(loginJson);
+ if (login is null) return;
+ try {
+ Homeserver = await hsProvider.GetAuthenticatedWithToken(login.Homeserver, login.AccessToken);
+ IsLoggedIn = true;
+ }
+ catch (MatrixException e) {
+ if (e is not { Error: MatrixException.ErrorCodes.M_UNKNOWN_TOKEN }) throw;
+ }
+ }
+
+ public async Task LoginAsync(string username, string password) {
+ Directory.CreateDirectory(Util.ExpandPath($"{cfg.ProfileDirectory}")!);
+ var mxidParts = username.Split(':', 2);
+ var res = await hsProvider.Login(mxidParts[1], username, password);
+ await File.WriteAllTextAsync(Path.Combine(cfg.ProfileDirectory, "login.json"), res.ToJson());
+ IsLoggedIn = true;
+
+ // Console.WriteLine("Login result: " + res.ToJson());
+ }
+
+ public async Task LogoutAsync() {
+ Directory.Delete(Util.ExpandPath($"{cfg.ProfileDirectory}")!, true);
+ IsLoggedIn = false;
+ }
+}
\ No newline at end of file
diff --git a/ModerationClient/Services/ModerationClientConfiguration.cs b/ModerationClient/Services/ModerationClientConfiguration.cs
new file mode 100644
index 0000000..f770fef
--- /dev/null
+++ b/ModerationClient/Services/ModerationClientConfiguration.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using ArcaneLibs;
+using ArcaneLibs.Extensions;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace MatrixUtils.Desktop;
+
+public class ModerationClientConfiguration
+{
+ private ILogger<ModerationClientConfiguration> _logger;
+
+ [RequiresUnreferencedCode("Uses reflection binding")]
+ public ModerationClientConfiguration(ILogger<ModerationClientConfiguration> logger, IConfiguration config, HostBuilderContext host)
+ {
+ _logger = logger;
+ logger.LogInformation("Loading configuration for environment: {}...", host.HostingEnvironment.EnvironmentName);
+ config.GetSection("ModerationClient").Bind(this);
+ DataStoragePath = Util.ExpandPath(DataStoragePath);
+ CacheStoragePath = Util.ExpandPath(CacheStoragePath);
+ }
+
+ public string? DataStoragePath { get; set; } = "";
+ public string? CacheStoragePath { get; set; } = "";
+ public string? SentryDsn { get; set; }
+}
\ No newline at end of file
diff --git a/ModerationClient/ViewLocator.cs b/ModerationClient/ViewLocator.cs
new file mode 100644
index 0000000..3de8107
--- /dev/null
+++ b/ModerationClient/ViewLocator.cs
@@ -0,0 +1,38 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+using Microsoft.Extensions.DependencyInjection;
+using ModerationClient.ViewModels;
+
+namespace ModerationClient;
+
+public class ViewLocator : IDataTemplate {
+ public Control? Build(object? data) {
+ try {
+ if (data is null)
+ return null;
+
+ var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
+ Console.WriteLine($"ViewLocator: Locating {name} for {data.GetType().FullName}");
+ var type = Type.GetType(name);
+ Console.WriteLine($"ViewLocator: Got {type?.FullName ?? "null"}");
+
+ if (type != null) {
+ var control = (Control)App.Current.Services.GetRequiredService(type);
+ Console.WriteLine($"ViewLocator: Created {control.GetType().FullName}");
+ control.DataContext = data;
+ return control;
+ }
+
+ return new TextBlock { Text = "Not Found: " + name };
+ }
+ catch (Exception e) {
+ Console.WriteLine($"ViewLocator: Error: {e}");
+ return new TextBlock { Text = e.ToString(), Foreground = Avalonia.Media.Brushes.Red };
+ }
+ }
+
+ public bool Match(object? data) {
+ return data is ViewModelBase;
+ }
+}
\ No newline at end of file
diff --git a/ModerationClient/ViewModels/ClientViewModel.cs b/ModerationClient/ViewModels/ClientViewModel.cs
new file mode 100644
index 0000000..340eb56
--- /dev/null
+++ b/ModerationClient/ViewModels/ClientViewModel.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using ArcaneLibs.Collections;
+using LibMatrix.Helpers;
+using LibMatrix.Responses;
+using Microsoft.Extensions.Logging;
+using ModerationClient.Services;
+
+namespace ModerationClient.ViewModels;
+
+public partial class ClientViewModel : ViewModelBase
+{
+ public ClientViewModel(ILogger<ClientViewModel> logger, MatrixAuthenticationService authService) {
+ this.logger = logger;
+ this.authService = authService;
+ _ = Task.Run(Run);
+ }
+
+ private readonly ILogger<ClientViewModel> logger;
+ private readonly MatrixAuthenticationService authService;
+
+ private Exception? _exception;
+
+ public Exception? Exception {
+ get => _exception;
+ private set => SetProperty(ref _exception, value);
+ }
+
+ public ObservableCollection<SpaceNode> DisplayedSpaces { get; } = [
+ new SpaceNode { Name = "Root", Children = [
+ new SpaceNode { Name = "Child 1" },
+ new SpaceNode { Name = "Child 2" },
+ new SpaceNode { Name = "Child 3" }
+ ] },
+ new SpaceNode { Name = "Root 2", Children = [
+ new SpaceNode { Name = "Child 4" },
+ new SpaceNode { Name = "Child 5" },
+ new SpaceNode { Name = "Child 6" }
+ ] }
+ ];
+
+ public async Task Run() {
+ var sh = new SyncStateResolver(authService.Homeserver, logger);
+ // var res = await sh.SyncAsync();
+ while (true) {
+ var res = await sh.ContinueAsync();
+ Console.WriteLine("mow");
+ }
+ }
+
+ private void ApplySpaceChanges(SyncResponse newSync) {
+ List<string> handledRoomIds = [];
+
+ }
+}
+
+public class SpaceNode {
+ public string Name { get; set; }
+ public ObservableCollection<SpaceNode> Children { get; set; } = [];
+}
\ No newline at end of file
diff --git a/ModerationClient/ViewModels/LoginViewModel.cs b/ModerationClient/ViewModels/LoginViewModel.cs
new file mode 100644
index 0000000..32f0d6e
--- /dev/null
+++ b/ModerationClient/ViewModels/LoginViewModel.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Threading.Tasks;
+using ModerationClient.Services;
+
+namespace ModerationClient.ViewModels;
+
+public partial class LoginViewModel(MatrixAuthenticationService authService) : ViewModelBase
+{
+ private Exception? _exception;
+ public string Username { get; set; }
+ public string Password { get; set; }
+
+ public Exception? Exception {
+ get => _exception;
+ private set => SetProperty(ref _exception, value);
+ }
+
+ public async Task LoginAsync() {
+ try {
+ Exception = null;
+ await authService.LoginAsync(Username, Password);
+ } catch (Exception e) {
+ Exception = e;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ModerationClient/ViewModels/MainWindowViewModel.cs b/ModerationClient/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000..01ec6d6
--- /dev/null
+++ b/ModerationClient/ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,33 @@
+using System;
+using ModerationClient.Services;
+using ModerationClient.Views;
+
+namespace ModerationClient.ViewModels;
+
+public partial class MainWindowViewModel(MatrixAuthenticationService authService, CommandLineConfiguration cfg) : ViewModelBase {
+ public MainWindow? MainWindow { get; set; }
+
+ private float _scale = 1.0f;
+ private ViewModelBase _currentViewModel = new LoginViewModel(authService);
+
+ public ViewModelBase CurrentViewModel {
+ get => _currentViewModel;
+ set => SetProperty(ref _currentViewModel, value);
+ }
+
+ public CommandLineConfiguration CommandLineConfiguration { get; } = cfg;
+ public MatrixAuthenticationService AuthService { get; } = authService;
+
+ public float Scale {
+ get => _scale;
+ set {
+ SetProperty(ref _scale, (float)Math.Round(value, 2));
+ OnPropertyChanged(nameof(ChildTargetWidth));
+ OnPropertyChanged(nameof(ChildTargetHeight));
+ }
+ }
+ public int ChildTargetWidth => (int)(MainWindow?.Width / Scale ?? 1);
+ public int ChildTargetHeight => (int)(MainWindow?.Height / Scale ?? 1);
+
+
+}
\ No newline at end of file
diff --git a/ModerationClient/ViewModels/ViewModelBase.cs b/ModerationClient/ViewModels/ViewModelBase.cs
new file mode 100644
index 0000000..0137aaf
--- /dev/null
+++ b/ModerationClient/ViewModels/ViewModelBase.cs
@@ -0,0 +1,7 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace ModerationClient.ViewModels;
+
+public class ViewModelBase : ObservableObject
+{
+}
\ No newline at end of file
diff --git a/ModerationClient/Views/ClientView.axaml b/ModerationClient/Views/ClientView.axaml
new file mode 100644
index 0000000..21ce5d9
--- /dev/null
+++ b/ModerationClient/Views/ClientView.axaml
@@ -0,0 +1,30 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:views="clr-namespace:ModerationClient.Views"
+ xmlns:viewModels="clr-namespace:ModerationClient.ViewModels"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="ModerationClient.Views.ClientView"
+ x:DataType="viewModels:ClientViewModel">
+ <Grid Width="{Binding $parent.Width}">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="128" MinWidth="16" />
+ <ColumnDefinition Width="1" />
+ <ColumnDefinition Width="128" MinWidth="16" />
+ <ColumnDefinition Width="1" />
+ <ColumnDefinition Width="*" MinWidth="16" />
+ </Grid.ColumnDefinitions>
+ <TreeView Grid.Column="0" Background="Red" ItemsSource="{CompiledBinding DisplayedSpaces}">
+ <TreeView.ItemTemplate>
+ <TreeDataTemplate ItemsSource="{Binding Children}">
+ <TextBlock Text="{Binding Name}" />
+ </TreeDataTemplate>
+ </TreeView.ItemTemplate>
+ </TreeView>
+ <GridSplitter Grid.Column="1" Background="Black" ResizeDirection="Columns" />
+ <Rectangle Grid.Column="2" Fill="Green" />
+ <GridSplitter Grid.Column="3" Background="Black" ResizeDirection="Columns" />
+ <Rectangle Grid.Column="4" Fill="Blue" />
+ </Grid>
+</UserControl>
\ No newline at end of file
diff --git a/ModerationClient/Views/ClientView.axaml.cs b/ModerationClient/Views/ClientView.axaml.cs
new file mode 100644
index 0000000..1ca5a89
--- /dev/null
+++ b/ModerationClient/Views/ClientView.axaml.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ModerationClient.Views;
+
+public partial class ClientView : UserControl {
+
+ public ClientView() {
+ InitializeComponent();
+
+ // PropertyChanged += (_, e) => {
+ // switch (e.Property.Name) {
+ // case nameof(Width): {
+ // //make sure all columns fit
+ // var grid = this.LogicalChildren.OfType<Grid>().FirstOrDefault();
+ // if(grid is null) {
+ // Console.WriteLine("Failed to find Grid in ClientView");
+ // return;
+ // }
+ // Console.WriteLine($"ClientView width changed to {Width}");
+ // var columns = grid.ColumnDefinitions;
+ // break;
+ // }
+ // }
+ // };
+ }
+
+ private void InitializeComponent() {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
diff --git a/ModerationClient/Views/LoginView.axaml b/ModerationClient/Views/LoginView.axaml
new file mode 100644
index 0000000..10e97c6
--- /dev/null
+++ b/ModerationClient/Views/LoginView.axaml
@@ -0,0 +1,25 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:views="clr-namespace:ModerationClient.Views"
+ xmlns:viewModels="clr-namespace:ModerationClient.ViewModels"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="ModerationClient.Views.LoginView"
+ x:DataType="viewModels:LoginViewModel">
+ <!-- DataContext="{Binding $self}"> -->
+ <!-- DataContext="{Binding $self}"> -->
+ <StackPanel>
+ <Label>Log in</Label>
+ <StackPanel Orientation="Horizontal">
+ <Label Width="100">User ID</Label>
+ <TextBox MinWidth="250" Text="{Binding Username, Mode=TwoWay}" />
+ </StackPanel>
+ <StackPanel Orientation="Horizontal">
+ <Label Width="100">Password</Label>
+ <MaskedTextBox MinWidth="250" PasswordChar="*" Text="{Binding Password, Mode=TwoWay}" />
+ </StackPanel>
+ <Button Click="Login">Login</Button>
+ <TextBlock Text="{CompiledBinding Exception}" Foreground="#ff3333"/>
+ </StackPanel>
+</UserControl>
\ No newline at end of file
diff --git a/ModerationClient/Views/LoginView.axaml.cs b/ModerationClient/Views/LoginView.axaml.cs
new file mode 100644
index 0000000..2e95e80
--- /dev/null
+++ b/ModerationClient/Views/LoginView.axaml.cs
@@ -0,0 +1,33 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.VisualTree;
+using Microsoft.Extensions.DependencyInjection;
+using ModerationClient.Services;
+using ModerationClient.ViewModels;
+
+namespace ModerationClient.Views;
+
+public partial class LoginView : UserControl {
+ private MatrixAuthenticationService AuthService { get; set; }
+
+ public LoginView() {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent() {
+ Console.WriteLine("LoginWindow loaded");
+
+ AvaloniaXamlLoader.Load(this);
+ Console.WriteLine("LoginWindow loaded 2");
+ }
+
+ // ReSharper disable once AsyncVoidMethod
+ private async void Login(object? sender, RoutedEventArgs e) {
+ Console.WriteLine("Login????");
+ // await AuthService.LoginAsync(Username, Password);
+ await ((LoginViewModel)DataContext).LoginAsync();
+ }
+}
diff --git a/ModerationClient/Views/MainWindow.axaml b/ModerationClient/Views/MainWindow.axaml
new file mode 100644
index 0000000..1c2b396
--- /dev/null
+++ b/ModerationClient/Views/MainWindow.axaml
@@ -0,0 +1,42 @@
+<Window xmlns="https://github.com/avaloniaui"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:vm="using:ModerationClient.ViewModels"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:views="clr-namespace:ModerationClient.Views"
+ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ x:Class="ModerationClient.Views.MainWindow"
+ x:DataType="vm:MainWindowViewModel"
+ Icon="/Assets/avalonia-logo.ico"
+ Title="ModerationClient"
+ Width="640" Height="480">
+
+ <Design.DataContext>
+ <!-- This only sets the DataContext for the previewer in an IDE,
+ to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
+ <vm:MainWindowViewModel />
+ </Design.DataContext>
+
+ <StackPanel>
+ <Grid ColumnDefinitions="Auto, *, Auto">
+ <StackPanel Orientation="Horizontal" Grid.Column="0">
+ <Label Content="{CompiledBinding Scale}" />
+ <Label>x</Label>
+ <Rectangle Width="32" />
+ <Label Content="{CompiledBinding ChildTargetWidth}" />
+ <Label>x</Label>
+ <Label Content="{CompiledBinding ChildTargetHeight}" />
+ </StackPanel>
+ <Label Grid.Column="2">Press '?' for keybinds</Label>
+ </Grid>
+ <Viewbox>
+ <ContentControl
+ Width="{CompiledBinding ChildTargetWidth}"
+ Height="{CompiledBinding ChildTargetHeight}"
+ Background="#222222"
+ Content="{CompiledBinding CurrentViewModel}" />
+ </Viewbox>
+
+ </StackPanel>
+
+</Window>
\ No newline at end of file
diff --git a/ModerationClient/Views/MainWindow.axaml.cs b/ModerationClient/Views/MainWindow.axaml.cs
new file mode 100644
index 0000000..ccabd71
--- /dev/null
+++ b/ModerationClient/Views/MainWindow.axaml.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Threading;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Diagnostics;
+using Avalonia.Input;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using ModerationClient.Services;
+using ModerationClient.ViewModels;
+
+namespace ModerationClient.Views;
+
+public partial class MainWindow : Window {
+ //viewmodel
+ private MainWindowViewModel? _viewModel { get; set; }
+
+ public MainWindow(CommandLineConfiguration cfg, MainWindowViewModel dataContext, IHostApplicationLifetime appLifetime) {
+ InitializeComponent();
+ Console.WriteLine("mainwnd");
+#if DEBUG
+ this.AttachDevTools(new DevToolsOptions() {
+ ShowAsChildWindow = true,
+ LaunchView = DevToolsViewKind.LogicalTree,
+ });
+#endif
+ PropertyChanged += (sender, args) => {
+ // Console.WriteLine($"MainWindow PropertyChanged: {args.Property.Name} ({args.OldValue} -> {args.NewValue})");
+ switch (args.Property.Name) {
+ case nameof(Height):
+ case nameof(Width): {
+ if (_viewModel is null) {
+ Console.WriteLine("WARN: MainWindowViewModel is null, ignoring height/width change!");
+ return;
+ }
+
+ // Console.WriteLine("height/width changed");
+ _viewModel.Scale = _viewModel.Scale;
+ break;
+ }
+ }
+ };
+ DataContext = _viewModel = dataContext;
+ _ = dataContext.AuthService.LoadProfileAsync();
+ dataContext.AuthService.PropertyChanged += (sender, args) => {
+ if (args.PropertyName == nameof(MatrixAuthenticationService.IsLoggedIn)) {
+ if (dataContext.AuthService.IsLoggedIn) {
+ // dataContext.CurrentViewModel = new ClientViewModel(dataContext.AuthService);
+ dataContext.CurrentViewModel = App.Current.Host.Services.GetRequiredService<ClientViewModel>();
+ }
+ else {
+ dataContext.CurrentViewModel = new LoginViewModel(dataContext.AuthService);
+ }
+ }
+ };
+ dataContext.MainWindow = this;
+ dataContext.Scale = cfg.Scale;
+ Width *= cfg.Scale;
+ Height *= cfg.Scale;
+
+ appLifetime.ApplicationStopping.Register(() => {
+ Console.WriteLine("ApplicationStopping triggered");
+ Close();
+ });
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e) => OnKeyDown(this, e);
+
+ private void OnKeyDown(object? _, KeyEventArgs e) {
+ if (_viewModel is null) {
+ Console.WriteLine("WARN: MainWindowViewModel is null, ignoring key press!");
+ return;
+ }
+
+ // Console.WriteLine("MainWindow KeyDown: " + e.Key);
+ if (e.Key == Key.Escape) {
+ _viewModel.Scale = 1.0f;
+ }
+ else if (e.Key == Key.F1) {
+ _viewModel.Scale -= 0.1f;
+ if (_viewModel.Scale < 0.1f) {
+ _viewModel.Scale = 0.1f;
+ }
+ }
+ else if (e.Key == Key.F2) {
+ _viewModel.Scale += 0.1f;
+ if (_viewModel.Scale > 5.0f) {
+ _viewModel.Scale = 5.0f;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ModerationClient/app.manifest b/ModerationClient/app.manifest
new file mode 100644
index 0000000..8c7b424
--- /dev/null
+++ b/ModerationClient/app.manifest
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
+ <!-- This manifest is used on Windows only.
+ Don't remove it as it might cause problems with window transparency and embedded controls.
+ For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
+ <assemblyIdentity version="1.0.0.0" name="ModerationClient.Desktop"/>
+
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <!-- A list of the Windows versions that this application has been tested on
+ and is designed to work with. Uncomment the appropriate elements
+ and Windows will automatically select the most compatible environment. -->
+
+ <!-- Windows 10 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
+ </application>
+ </compatibility>
+</assembly>
diff --git a/ModerationClient/appsettings.Development.json b/ModerationClient/appsettings.Development.json
new file mode 100644
index 0000000..a1add03
--- /dev/null
+++ b/ModerationClient/appsettings.Development.json
@@ -0,0 +1,14 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information"
+ }
+ },
+ "RMUDesktop": {
+ "DataStoragePath": "rmu-desktop/data",
+ "CacheStoragePath": "rmu-desktop/cache",
+ "SentryDsn": "https://a41e99dd2fdd45f699c432b21ebce632@sentry.thearcanebrony.net/15"
+ }
+}
diff --git a/ModerationClient/appsettings.json b/ModerationClient/appsettings.json
new file mode 100644
index 0000000..058723c
--- /dev/null
+++ b/ModerationClient/appsettings.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information"
+ }
+ },
+ "RMUDesktop": {
+ "DataStoragePath": "~/.local/share/rmu-desktop",
+ "CacheStoragePath": "~/.cache/rmu-desktop"
+ }
+}
|