Remove .Common-project

Currently of no use
This commit is contained in:
Jöran Malek 2024-02-21 02:17:33 +01:00
parent 232231d20d
commit b1d3ec73c9
31 changed files with 16020 additions and 109 deletions

View file

@ -2,10 +2,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="InkForge.Desktop.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
</Application.Styles>
<Application.Resources>
<FontFamily x:Key="FluentSystemIcons-Filled">/Assets/Fonts#FluentSystemIcons-Filled</FontFamily>
<FontFamily x:Key="FluentSystemIcons-Regular">/Assets/Fonts#FluentSystemIcons-Regular</FontFamily>
</Application.Resources>
</Application>

View file

@ -2,6 +2,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Metadata;
using InkForge.Desktop.ViewModels;
@ -14,6 +15,11 @@ using ReactiveUI;
using Splat;
using Splat.Microsoft.Extensions.DependencyInjection;
[assembly: XmlnsPrefix("app:InkForge", "inkforge")]
[assembly: XmlnsDefinition("app:InkForge", "InkForge.Desktop.Controls")]
[assembly: XmlnsDefinition("app:InkForge", "InkForge.Desktop.MarkupExtensions")]
[assembly: XmlnsDefinition("app:InkForge", "InkForge.Desktop.Services")]
namespace InkForge.Desktop;
public partial class App : Application
@ -54,6 +60,7 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted()
{
// This kills Avalonia VSCode Previewer.
var viewModel = ActivatorUtilities.GetServiceOrCreateInstance<AppViewModel>(ServiceProvider);
var view = ViewLocator.Current.ResolveView(viewModel)!;
view.ViewModel = viewModel;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
1.1.227

View file

@ -1,68 +0,0 @@
using InkForge.Data;
using InkForge.Desktop.Models;
using InkForge.Desktop.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ReactiveUI;
namespace InkForge.Desktop.Controllers;
public class WorkspaceController : ReactiveObject
{
private readonly IServiceProvider _serviceProvider;
private Workspace _workspace;
public Workspace Workspace
{
get => _workspace;
set => this.RaiseAndSetIfChanged(ref _workspace, value);
}
public WorkspaceController(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task OpenWorkspace(string path, bool createFile = false)
{
if (await CreateWorkspace(path, createFile) is { } workspace)
{
Workspace = workspace;
}
}
private async ValueTask<Workspace?> CreateWorkspace(string path, bool createFile)
{
FileInfo file = new(path);
if (!(createFile || file.Exists))
{
return null;
}
file.Directory!.Create();
var scope = _serviceProvider.CreateScope();
var scopeServiceProvider = scope.ServiceProvider;
var context = scope.ServiceProvider.GetRequiredService<WorkspaceContext>();
context.DbPath = path;
var db = scopeServiceProvider.GetRequiredService<NoteDbContext>();
await using (var transaction = await db.Database.BeginTransactionAsync().ConfigureAwait(false))
{
try
{
await db.Database.MigrateAsync().ConfigureAwait(false);
}
catch
{
await transaction.RollbackAsync().ConfigureAwait(false);
return null;
}
await transaction.CommitAsync().ConfigureAwait(false);
}
return new(scope);
}
}

View file

@ -0,0 +1,175 @@
using System.IO.Hashing;
using System.Runtime.InteropServices;
using System.Text.Json;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
namespace InkForge.Desktop.Controls;
public class FluentSymbolIcon : IconElement
{
public static readonly StyledProperty<int> IconSizeProperty
= AvaloniaProperty.Register<FluentSymbolIcon, int>(nameof(IconSize), defaultValue: 20);
public static readonly StyledProperty<FontIconStyle> IconStyleProperty
= AvaloniaProperty.Register<FluentSymbolIcon, FontIconStyle>(nameof(IconStyle));
public static readonly StyledProperty<string> SymbolProperty
= AvaloniaProperty.Register<FluentSymbolIcon, string>(nameof(Symbol));
private static readonly Dictionary<(FontIconStyle, uint Key), string> _glyphCache = [];
private static readonly Dictionary<FontIconStyle, FontFamily> _iconFonts = [];
private TextLayout? _textLayout;
public int IconSize
{
get => GetValue(IconSizeProperty);
set => SetValue(IconSizeProperty, value);
}
public FontIconStyle IconStyle
{
get => GetValue(IconStyleProperty);
set => SetValue(IconStyleProperty, value);
}
public string Symbol
{
get => GetValue(SymbolProperty);
set => SetValue(SymbolProperty, value);
}
static FluentSymbolIcon()
{
AffectsMeasure<FluentSymbolIcon>([
IconStyleProperty,
SymbolProperty,
]);
}
public override void Render(DrawingContext context)
{
_textLayout ??= GenerateText();
var dstRect = new Rect(Bounds.Size);
using (context.PushClip(dstRect))
{
var pt = new Point(dstRect.Center.X - _textLayout.Width / 2,
dstRect.Center.Y - _textLayout.Height / 2);
_textLayout.Draw(context, pt);
}
}
protected override Size MeasureOverride(Size availableSize)
{
_textLayout ??= GenerateText();
return new Size(_textLayout.Width, _textLayout.Height);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(IconSize):
case nameof(IconStyle):
case nameof(Symbol):
InvalidateSymbolLayout();
break;
}
}
protected override void OnMeasureInvalidated()
{
_textLayout?.Dispose();
_textLayout = null;
base.OnMeasureInvalidated();
}
private TextLayout GenerateText()
{
var glyph = GetIconGlyph(this);
if (!_iconFonts.TryGetValue(IconStyle, out var fontFamily))
{
_iconFonts[IconStyle] = fontFamily = new FontFamily($"avares://InkForge/Assets/Fonts#FluentSystemIcons-{IconStyle}");
}
return new TextLayout(glyph, new Typeface(fontFamily), FontSize, Foreground, TextAlignment.Left);
}
private void InvalidateSymbolLayout()
{
InvalidateMeasure();
}
private static string GetIconGlyph(FluentSymbolIcon icon)
{
ReadOnlySpan<char> glyphKey = $"ic_fluent_{icon.Symbol}_{icon.IconSize:0}";
var hash = Hash(glyphKey, icon.IconStyle);
if (!_glyphCache.TryGetValue((icon.IconStyle, hash), out var glyph))
{
glyph = LoadIcons(icon.IconStyle, hash);
}
if (string.IsNullOrWhiteSpace(glyph))
{
return string.Empty;
}
return glyph;
}
private static uint Hash(ReadOnlySpan<char> key, FontIconStyle iconStyle)
{
return XxHash32.HashToUInt32(MemoryMarshal.AsBytes(key), (int)iconStyle);
}
private static string LoadIcons(FontIconStyle iconStyle, uint key)
{
Optional<string>? glyph = Optional<string>.Empty;
using (var stream = AssetLoader.Open(new($"avares://InkForge/Assets/Fonts/FluentSystemIcons-{iconStyle}.json")))
using (var document = JsonDocument.Parse(stream))
{
foreach (var element in document.RootElement.EnumerateObject())
{
if (element.Value.ValueKind is not JsonValueKind.Number)
{
continue;
}
var typeSeparator = element.Name.LastIndexOf('_');
if (typeSeparator == -1)
{
continue;
}
ReadOnlySpan<char> elementKey = element.Name.AsSpan(0, typeSeparator);
var hash = Hash(elementKey, iconStyle);
var elementGlyph = char.ConvertFromUtf32(element.Value.GetInt32())!;
if (hash == key)
{
glyph = glyph switch
{
{ HasValue: false } => elementGlyph,
_ => default(Optional<string>?)
};
}
_glyphCache[(iconStyle, hash)] = elementGlyph;
}
}
return glyph?.GetValueOrDefault() ?? string.Empty;
}
public enum FontIconStyle
{
Regular, Filled
}
}

View file

@ -1,14 +1,14 @@
using InkForge.Desktop.Services;
using InkForge.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using SmartFormat;
using InkForge.Desktop.Data.Options;
namespace InkForge.Desktop.Data;
public class NoteDbContextFactory(WorkspaceContext context, IConfiguration configuration) : IDbContextFactory<NoteDbContext>
public class NoteDbContextFactory(LocalWorkspaceOptions options, IConfiguration configuration) : IDbContextFactory<NoteDbContext>
{
private string? _connectionString;
@ -16,7 +16,7 @@ public class NoteDbContextFactory(WorkspaceContext context, IConfiguration confi
{
_connectionString ??= Smart.Format(configuration.GetConnectionString("DefaultConnection")!, new
{
WorkspaceFile = context.DbPath
WorkspaceFile = options.DbPath
});
DbContextOptionsBuilder<NoteDbContext> builder = new();

View file

@ -0,0 +1,6 @@
namespace InkForge.Desktop.Data.Options;
public class LocalWorkspaceOptions
{
public string DbPath { get; set; } = default!;
}

View file

@ -23,6 +23,7 @@
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="SmartFormat" />
<PackageReference Include="Splat.Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="System.IO.Hashing" />
</ItemGroup>
<ItemGroup>
@ -30,6 +31,10 @@
<ProjectReference Include="..\..\shared\migrations\InkForge.Sqlite\InkForge.Sqlite.csproj" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\appsettings.json" />
</ItemGroup>

View file

@ -0,0 +1,92 @@
using InkForge.Data;
using InkForge.Desktop.Data.Options;
using InkForge.Desktop.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ReactiveUI;
namespace InkForge.Desktop.Managers;
public class WorkspaceManager(IServiceProvider serviceProvider) : ReactiveObject
{
private readonly IServiceProvider _serviceProvider = serviceProvider;
private Workspace? _workspace;
public Workspace? Workspace
{
get => _workspace;
private set => this.RaiseAndSetIfChanged(ref _workspace, value);
}
public Task CloseWorkspace()
{
_workspace?.Dispose();
Workspace = null;
return Task.CompletedTask;
}
public async Task OpenWorkspace(string path, bool createFile = false)
{
await CloseWorkspace().ConfigureAwait(false);
if (await CreateLocalWorkspace(path, createFile).ConfigureAwait(false) is { } workspace)
{
Workspace = workspace;
}
}
private async ValueTask<Workspace?> CreateLocalWorkspace(string path, bool createFile)
{
FileInfo file = new(path);
if (!(createFile || file.Exists))
{
return null;
}
file.Directory!.Create();
IServiceScope? scope = null;
IWorkspaceAccessor workspaceAccessor;
try
{
scope = _serviceProvider.CreateScope();
var serviceProvider = scope.ServiceProvider;
var options = serviceProvider.GetRequiredService<LocalWorkspaceOptions>();
options.DbPath = path;
workspaceAccessor = serviceProvider.GetRequiredService<IWorkspaceAccessor>();
workspaceAccessor.Workspace = new Workspace(scope)
{
Name = Path.GetFileNameWithoutExtension(file.Name),
Options = options,
};
var dbFactory = serviceProvider.GetRequiredService<IDbContextFactory<NoteDbContext>>();
await using (var dbContext = dbFactory.CreateDbContext())
{
var db = dbContext.Database;
await using var transaction = await db.BeginTransactionAsync().ConfigureAwait(false);
try
{
await db.MigrateAsync().ConfigureAwait(false);
}
catch
{
// Show Error through TopLevels.ActiveTopLevel
await transaction.RollbackAsync().ConfigureAwait(false);
return null;
}
await transaction.CommitAsync().ConfigureAwait(false);
}
scope = null;
}
finally
{
scope?.Dispose();
}
return workspaceAccessor.Workspace;
}
}

View file

@ -0,0 +1,6 @@
namespace InkForge.Desktop.MarkupExtensions;
public class FluentSymbolIconExtension
{
}

View file

@ -1,8 +1,9 @@
using InkForge.Desktop.Controllers;
using InkForge.Desktop.Data;
using InkForge.Desktop.Services;
using InkForge.Desktop.ViewModels;
using InkForge.Data;
using InkForge.Desktop.Data;
using InkForge.Desktop.Data.Options;
using InkForge.Desktop.Managers;
using InkForge.Desktop.Models;
using InkForge.Desktop.ViewModels;
using Microsoft.EntityFrameworkCore;
@ -18,13 +19,13 @@ public static class InkForgeServiceCollections
{
services.AddHttpClient();
services.AddScoped<IWorkspaceAccessor, WorkspaceAccessor>();
services.AddScoped<IDbContextFactory<NoteDbContext>, NoteDbContextFactory>();
services.AddScoped(s => s.GetRequiredService<IDbContextFactory<NoteDbContext>>().CreateDbContext());
services.AddScoped<WorkspaceContext>();
services.AddScoped<LocalWorkspaceOptions>();
services.AddSingleton<LandingViewModel>();
services.AddSingleton<WorkspaceController>();
services.AddSingleton<WorkspaceManager>();
Locator.CurrentMutable.RegisterViewsForViewModels(typeof(InkForgeServiceCollections).Assembly);

View file

@ -1,19 +1,19 @@
namespace Microsoft.Extensions.DependencyInjection
{
public static class TypeFactory<TFactory, T>
where TFactory : struct, IObjectParameters<TFactory>
public static class TypeFactory<TArguments, T>
where TArguments : IFactoryArguments<TArguments>
{
private static ObjectFactory<T>? s_objectFactory;
public static T Create(in TFactory factory, IServiceProvider serviceProvider)
public static T Create(IServiceProvider serviceProvider, in TArguments factory)
{
s_objectFactory ??= ActivatorUtilities.CreateFactory<T>(TFactory.Types);
s_objectFactory ??= ActivatorUtilities.CreateFactory<T>(TArguments.Types);
return s_objectFactory(serviceProvider, (object[])factory);
}
}
public interface IObjectParameters<T>
where T : struct, IObjectParameters<T>
public interface IFactoryArguments<T>
where T : IFactoryArguments<T>
{
abstract static Type[] Types { get; }

View file

@ -1,8 +1,45 @@
using InkForge.Data;
using InkForge.Desktop.Data.Options;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace InkForge.Desktop.Models;
public class Workspace(IServiceScope scope)
public sealed class Workspace : IDisposable
{
public IServiceProvider ServiceProvider => scope.ServiceProvider;
private readonly IDbContextFactory<NoteDbContext> _dbContextFactory;
private bool _disposedValue;
private IServiceScope? _scope;
public string Name { get; set; } = default!;
public LocalWorkspaceOptions Options { get; set; } = default!;
public IServiceProvider Services => _scope!.ServiceProvider;
public Workspace(IServiceScope scope)
{
_scope = scope;
_dbContextFactory = Services.GetRequiredService<IDbContextFactory<NoteDbContext>>();
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposedValue)
{
{
_scope!.Dispose();
}
_scope = null;
_disposedValue = true;
}
}
}

View file

@ -0,0 +1,12 @@
namespace InkForge.Desktop.Models
{
public interface IWorkspaceAccessor
{
Workspace? Workspace { get; set; }
}
public class WorkspaceAccessor : IWorkspaceAccessor
{
public Workspace? Workspace { get; set; }
}
}

View file

@ -1,5 +1,6 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Metadata;
using Avalonia.ReactiveUI;
using Avalonia.Threading;

View file

@ -1,3 +1,5 @@
using System.Reactive.Linq;
using Avalonia;
using Avalonia.Controls;
@ -7,12 +9,14 @@ public class TopLevels
{
public static readonly AttachedProperty<object?> RegisterProperty
= AvaloniaProperty.RegisterAttached<TopLevels, Visual, object?>("Register");
private static readonly Dictionary<object, Visual> RegistrationMapper = [];
public static TopLevel? ActiveTopLevel { get; private set; }
static TopLevels()
{
RegisterProperty.Changed.AddClassHandler<Visual>(RegisterChanged);
WindowBase.IsActiveProperty.Changed.Subscribe(WindowActiveChanged);
}
public static object? GetRegister(AvaloniaObject element)
@ -51,4 +55,14 @@ public class TopLevels
RegistrationMapper.Add(e.NewValue, sender);
}
}
private static void WindowActiveChanged(AvaloniaPropertyChangedEventArgs<bool> e)
{
ActiveTopLevel = (e.GetOldAndNewValue<bool>(), e.Sender) switch
{
((false, true), TopLevel topLevel) => topLevel,
((true, false), { } topLevel) when topLevel == ActiveTopLevel => null,
_ => ActiveTopLevel,
};
}
}

View file

@ -1,4 +1,4 @@
using InkForge.Desktop.Controllers;
using InkForge.Desktop.Managers;
using InkForge.Desktop.Models;
using ReactiveUI;
@ -8,7 +8,7 @@ namespace InkForge.Desktop.ViewModels;
public class AppViewModel : ReactiveObject
{
private readonly LandingViewModel _landingViewModel;
private readonly WorkspaceController _workspace;
private readonly WorkspaceManager _workspace;
private object _view;
public object View
@ -17,7 +17,7 @@ public class AppViewModel : ReactiveObject
set => this.RaiseAndSetIfChanged(ref _view, value);
}
public AppViewModel(WorkspaceController workspace, LandingViewModel landingViewModel)
public AppViewModel(WorkspaceManager workspace, LandingViewModel landingViewModel)
{
_workspace = workspace;
_landingViewModel = landingViewModel;

View file

@ -3,7 +3,7 @@ using System.Reactive;
using Avalonia.Platform.Storage;
using InkForge.Desktop.Controllers;
using InkForge.Desktop.Managers;
using InkForge.Desktop.Services;
using ReactiveUI;
@ -13,7 +13,7 @@ namespace InkForge.Desktop.ViewModels;
public class LandingViewModel : ReactiveObject
{
private ReadOnlyObservableCollection<RecentItemViewModel> _recentItems;
private readonly WorkspaceController _workspaceController;
private readonly WorkspaceManager _workspaceController;
public ReactiveCommand<Unit, Unit> CreateNew { get; }
@ -21,7 +21,7 @@ public class LandingViewModel : ReactiveObject
public ReadOnlyObservableCollection<RecentItemViewModel> RecentItems => _recentItems;
public LandingViewModel(WorkspaceController workspaceController)
public LandingViewModel(WorkspaceManager workspaceController)
{
_workspaceController = workspaceController;
CreateNew = ReactiveCommand.CreateFromTask(OnCreateNew);

View file

@ -7,9 +7,13 @@ namespace InkForge.Desktop.ViewModels;
public class WorkspaceViewModel : ReactiveObject
{
private readonly Workspace _workspace;
private readonly ObservableAsPropertyHelper<string> _workspaceNameProperty;
public string WorkspaceName => _workspaceNameProperty.Value;
public WorkspaceViewModel(Workspace workspace)
{
_workspace = workspace;
_workspaceNameProperty = this.WhenAnyValue(v => v._workspace.Name).ToProperty(this, nameof(WorkspaceName));
}
}

View file

@ -4,13 +4,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:reactiveui="http://reactiveui.net"
xmlns:vm="using:InkForge.Desktop.ViewModels"
xmlns:services="using:InkForge.Desktop.Services"
xmlns:inkforge="app:InkForge"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:Class="InkForge.Desktop.Views.LandingView"
x:DataType="vm:LandingViewModel"
services:TopLevels.Register="{CompiledBinding}">
inkforge:TopLevels.Register="{CompiledBinding}">
<Grid RowDefinitions="Auto, *, Auto">
<Label Content="Open Recent"
Grid.Row="0" />

View file

@ -3,13 +3,15 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:reactiveui="http://reactiveui.net"
xmlns:inkforge="app:InkForge"
xmlns:vm="using:InkForge.Desktop.ViewModels"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
Width="800"
Height="450"
x:Class="InkForge.Desktop.Views.MainWindow"
x:DataType="vm:AppViewModel"
Title="MainWindow">
Title="MainWindow"
inkforge:TopLevels.Register="{CompiledBinding}">
<DockPanel>
<NativeMenuBar />

View file

@ -1,3 +1,4 @@
using Avalonia.Input;
using Avalonia.ReactiveUI;
using InkForge.Desktop.ViewModels;

View file

@ -3,11 +3,67 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:reactiveui="http://reactiveui.net"
xmlns:inkforge="app:InkForge"
xmlns:vm="using:InkForge.Desktop.ViewModels"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:Class="InkForge.Desktop.Views.WorkspaceView"
x:DataType="vm:WorkspaceViewModel">
Welcome to Avalonia!
x:DataType="vm:WorkspaceViewModel"
inkforge:TopLevels.Register="{CompiledBinding}">
<SplitView IsPaneOpen="true"
DisplayMode="Inline"
OpenPaneLength="300">
<SplitView.Pane>
<DockPanel x:Name="FilesView"
Background="Transparent">
<Grid ColumnDefinitions="*, Auto"
DockPanel.Dock="Top">
<TextBlock Text="Notes"
FontWeight="Bold"
Margin="3"
Grid.Column="0" />
<StackPanel x:Name="ToolBar"
Orientation="Horizontal"
Spacing="3"
Grid.Column="1">
<Button>
<inkforge:FluentSymbolIcon Symbol="document_add" />
</Button>
<Button>
<inkforge:FluentSymbolIcon Symbol="arrow_clockwise" />
</Button>
<Button>
<inkforge:FluentSymbolIcon Symbol="subtract_square_multiple" />
</Button>
<StackPanel.Styles>
<Style Selector="#ToolBar > :is(TemplatedControl)">
<Setter Property="Background"
Value="Transparent" />
<Setter Property="Padding"
Value="1" />
<Setter Property="VerticalAlignment"
Value="Center" />
</Style>
<Style Selector="#FilesView:not(:pointerover) StackPanel">
<Setter Property="IsVisible"
Value="False" />
</Style>
</StackPanel.Styles>
</StackPanel>
</Grid>
<TreeView ScrollViewer.VerticalScrollBarVisibility="Visible" />
</DockPanel>
</SplitView.Pane>
<TabControl>
<TabItem Header="Some Note.md">
Hello There!
</TabItem>
</TabControl>
</SplitView>
</UserControl>