Compare commits
10 commits
f703567aed
...
5584ab4ec8
| Author | SHA1 | Date | |
|---|---|---|---|
| 5584ab4ec8 | |||
| 43b4d50e43 | |||
| 4c2b5cca93 | |||
| 693d12b61c | |||
| b1d3ec73c9 | |||
| 232231d20d | |||
| a62b5a1f29 | |||
| e9c6e14965 | |||
| 26915defe1 | |||
| 6a9c9e006c |
89 changed files with 17511 additions and 911 deletions
|
|
@ -3,13 +3,13 @@
|
||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.3",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-ef"
|
"dotnet-ef"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dotnet-aspnet-codegenerator": {
|
"dotnet-aspnet-codegenerator": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.1",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-aspnet-codegenerator"
|
"dotnet-aspnet-codegenerator"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,35 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||||
|
|
||||||
<AvaloniaVersion>11.0.9</AvaloniaVersion>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="Avalonia" Version="$(AvaloniaVersion)" />
|
<PackageVersion Include="Avalonia" Version="11.0.10" />
|
||||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaVersion)" />
|
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
|
||||||
<PackageVersion Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
|
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.10" />
|
||||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
|
<PackageVersion Include="Avalonia.Desktop" Version="11.0.10" />
|
||||||
<PackageVersion Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
|
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.0.10" />
|
||||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
|
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.0.10" />
|
||||||
<PackageVersion Include="Dock.Avalonia" Version="11.0.0.5" />
|
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
|
<PackageVersion Include="Dock.Avalonia" Version="11.0.0.7" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.1" />
|
<PackageVersion Include="Dock.Model.Mvvm" Version="11.0.0.7" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
|
<PackageVersion Include="DynamicData" Version="8.4.1" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Abstractions" Version="8.0.1" />
|
<PackageVersion Include="FluentAvaloniaUI" Version="2.0.5" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.3" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.3" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.3" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.3" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Configuration.CommandLine" Version="8.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.3" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.0" />
|
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.1" />
|
||||||
<PackageVersion Include="ReactiveUI" Version="19.5.41" />
|
<PackageVersion Include="SmartFormat" Version="3.3.2" />
|
||||||
|
<PackageVersion Include="Splat" Version="15.0.1" />
|
||||||
<PackageVersion Include="Splat.Microsoft.Extensions.DependencyInjection" Version="14.8.12" />
|
<PackageVersion Include="Splat.Microsoft.Extensions.DependencyInjection" Version="14.8.12" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
<PackageVersion Include="System.IO.Hashing" Version="8.0.0" />
|
<PackageVersion Include="System.IO.Hashing" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
using InkForge.Api.Data.Infrastructure;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
@ -12,25 +10,15 @@ public class ApiDbcontext(
|
||||||
{
|
{
|
||||||
public DbSet<WorkspaceEntity> Workspaces { get; set; } = default!;
|
public DbSet<WorkspaceEntity> Workspaces { get; set; } = default!;
|
||||||
|
|
||||||
public DbSet<WorkspaceVersionEntity> WorkspaceVersions { get; set; } = default!;
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(builder);
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
builder.Entity<WorkspaceEntity>(options =>
|
builder.Entity<WorkspaceEntity>(options =>
|
||||||
{
|
{
|
||||||
options.OwnsOne(m => m.Value);
|
|
||||||
|
|
||||||
options.HasKey(m => m.Id);
|
options.HasKey(m => m.Id);
|
||||||
});
|
|
||||||
|
|
||||||
builder.Entity<WorkspaceVersionEntity>(options =>
|
|
||||||
{
|
|
||||||
options.OwnsOne(m => m.Value);
|
options.OwnsOne(m => m.Value);
|
||||||
|
|
||||||
options.HasKey(m => m.Version);
|
|
||||||
options.HasIndex(nameof(WorkspaceVersionEntity.Id), nameof(WorkspaceVersionEntity.Version)).IsUnique();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
|
|
||||||
namespace InkForge.Api.Data.Domain;
|
|
||||||
|
|
||||||
public class Workspace
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = default!;
|
|
||||||
|
|
||||||
public DateTimeOffset Created { get; set; }
|
|
||||||
|
|
||||||
public IdentityUser Owner { get; set; } = default!;
|
|
||||||
|
|
||||||
public DateTimeOffset Updated { get; set; }
|
|
||||||
|
|
||||||
public DateTimeOffset? Deleted { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
using InkForge.Api.Data.Domain;
|
|
||||||
using InkForge.Data;
|
|
||||||
|
|
||||||
namespace InkForge.Api.Data.Infrastructure
|
|
||||||
{
|
|
||||||
public class WorkspaceEntity : Entity<Workspace, int>;
|
|
||||||
|
|
||||||
public class WorkspaceVersionEntity : VersionedEntity<Workspace, int>;
|
|
||||||
}
|
|
||||||
21
InkForge.Api.Data/Workspaces.cs
Normal file
21
InkForge.Api.Data/Workspaces.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
using InkForge.Data;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace InkForge.Api.Data
|
||||||
|
{
|
||||||
|
public class Workspace
|
||||||
|
{
|
||||||
|
public DateTimeOffset Created { get; set; }
|
||||||
|
|
||||||
|
public DateTimeOffset? Deleted { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; } = default!;
|
||||||
|
|
||||||
|
public IdentityUser Owner { get; set; } = default!;
|
||||||
|
|
||||||
|
public DateTimeOffset Updated { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WorkspaceEntity : Entity<Workspace, int>;
|
||||||
|
}
|
||||||
|
|
@ -27,8 +27,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "design", "design", "{C78684
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkForge.Migrations", "design\InkForge.Migrations\InkForge.Migrations.csproj", "{8DF3397E-2717-49F0-9592-82ABE9327A73}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkForge.Migrations", "design\InkForge.Migrations\InkForge.Migrations.csproj", "{8DF3397E-2717-49F0-9592-82ABE9327A73}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkForge.Common", "app\InkForge.Common\InkForge.Common.csproj", "{DCE2DCD6-D15C-4F0D-8D7F-22FF82F62F6F}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -66,10 +64,6 @@ Global
|
||||||
{8DF3397E-2717-49F0-9592-82ABE9327A73}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8DF3397E-2717-49F0-9592-82ABE9327A73}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8DF3397E-2717-49F0-9592-82ABE9327A73}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8DF3397E-2717-49F0-9592-82ABE9327A73}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{8DF3397E-2717-49F0-9592-82ABE9327A73}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8DF3397E-2717-49F0-9592-82ABE9327A73}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{DCE2DCD6-D15C-4F0D-8D7F-22FF82F62F6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{DCE2DCD6-D15C-4F0D-8D7F-22FF82F62F6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{DCE2DCD6-D15C-4F0D-8D7F-22FF82F62F6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{DCE2DCD6-D15C-4F0D-8D7F-22FF82F62F6F}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{DD595B76-5FDE-4C37-822E-CB58BBB02C8C} = {C73D8E17-EA0A-4206-91D4-9E5BD63B3DB0}
|
{DD595B76-5FDE-4C37-822E-CB58BBB02C8C} = {C73D8E17-EA0A-4206-91D4-9E5BD63B3DB0}
|
||||||
|
|
@ -78,6 +72,5 @@ Global
|
||||||
{5AFA8AD9-9230-4218-BBFD-BD75F1E752DC} = {84CBD204-9573-4472-9334-68FB360BD6ED}
|
{5AFA8AD9-9230-4218-BBFD-BD75F1E752DC} = {84CBD204-9573-4472-9334-68FB360BD6ED}
|
||||||
{F8A7563F-2647-4623-88E7-470D20F25E93} = {A9F8087F-F148-47A5-94AE-F7B6E1D33096}
|
{F8A7563F-2647-4623-88E7-470D20F25E93} = {A9F8087F-F148-47A5-94AE-F7B6E1D33096}
|
||||||
{8DF3397E-2717-49F0-9592-82ABE9327A73} = {C7868400-84D7-45C5-B594-C30777EE5191}
|
{8DF3397E-2717-49F0-9592-82ABE9327A73} = {C7868400-84D7-45C5-B594-C30777EE5191}
|
||||||
{DCE2DCD6-D15C-4F0D-8D7F-22FF82F62F6F} = {84CBD204-9573-4472-9334-68FB360BD6ED}
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<Application xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
x:Class="InkForge.Common.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"/>
|
|
||||||
</Application.Styles>
|
|
||||||
</Application>
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
using InkForge.Common.ViewModels;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace InkForge.Common;
|
|
||||||
|
|
||||||
public partial class App : Application
|
|
||||||
{
|
|
||||||
public static readonly StyledProperty<IServiceProvider> ServiceProviderProperty = AvaloniaProperty.Register<App, IServiceProvider>(nameof(ServiceProvider));
|
|
||||||
|
|
||||||
public IServiceProvider ServiceProvider => GetValue(ServiceProviderProperty);
|
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
|
||||||
{
|
|
||||||
var viewModel = ActivatorUtilities.GetServiceOrCreateInstance<AppViewModel>(ServiceProvider);
|
|
||||||
var view = ViewLocator.Current.ResolveView(viewModel)!;
|
|
||||||
view.ViewModel = viewModel;
|
|
||||||
_ = ApplicationLifetime switch
|
|
||||||
{
|
|
||||||
IClassicDesktopStyleApplicationLifetime desktop => desktop.MainWindow = view as Window,
|
|
||||||
ISingleViewApplicationLifetime singleView => singleView.MainView = view as Control,
|
|
||||||
_ => throw new NotSupportedException(),
|
|
||||||
};
|
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
namespace InkForge.Common.Controllers;
|
|
||||||
|
|
||||||
public class WorkspaceController
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
using InkForge.Data;
|
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace InkForge.Common.Data;
|
|
||||||
|
|
||||||
public class NoteDbContextFactory : IDbContextFactory<NoteDbContext>
|
|
||||||
{
|
|
||||||
|
|
||||||
public NoteDbContext CreateDbContext()
|
|
||||||
{
|
|
||||||
return new NoteDbContext(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<RootNamespace>InkForge</RootNamespace>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Avalonia.Controls.DataGrid" />
|
|
||||||
<PackageReference Include="Avalonia.Fonts.Inter" />
|
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" />
|
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\shared\InkForge.Data\InkForge.Data.csproj" />
|
|
||||||
<ProjectReference Include="..\..\shared\migrations\InkForge.Sqlite\InkForge.Sqlite.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
using InkForge.Common.Controllers;
|
|
||||||
using InkForge.Common.Data;
|
|
||||||
using InkForge.Common.ViewModels;
|
|
||||||
using InkForge.Common.ViewModels.Landing;
|
|
||||||
using InkForge.Data;
|
|
||||||
|
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
using Splat;
|
|
||||||
|
|
||||||
namespace Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
public static class InkForgeServiceCollections
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddInkForge(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddHttpClient();
|
|
||||||
|
|
||||||
services.AddDbContextFactory<NoteDbContext, NoteDbContextFactory>();
|
|
||||||
|
|
||||||
services.AddSingleton<LandingViewModel>();
|
|
||||||
services.AddSingleton<LandingViewModelFactory>();
|
|
||||||
services.AddSingleton<WorkspaceController>();
|
|
||||||
|
|
||||||
Locator.CurrentMutable.RegisterViewsForViewModels(typeof(InkForgeServiceCollections).Assembly);
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
namespace Microsoft.Extensions.DependencyInjection
|
|
||||||
{
|
|
||||||
public static class TypeFactory<TFactory, T>
|
|
||||||
where TFactory : struct, IObjectParameters<TFactory>
|
|
||||||
{
|
|
||||||
private static ObjectFactory<T>? s_objectFactory;
|
|
||||||
|
|
||||||
public static T Create(in TFactory factory, IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
s_objectFactory ??= ActivatorUtilities.CreateFactory<T>(TFactory.Types);
|
|
||||||
return s_objectFactory(serviceProvider, (object[])factory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IObjectParameters<T>
|
|
||||||
where T : struct, IObjectParameters<T>
|
|
||||||
{
|
|
||||||
abstract static Type[] Types { get; }
|
|
||||||
|
|
||||||
abstract static implicit operator object[](in T self);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace InkForge.Common.ReactiveUI;
|
|
||||||
|
|
||||||
public abstract class RoutableReactiveObject(IScreen screen) : ReactiveObject, IRoutableViewModel
|
|
||||||
{
|
|
||||||
public abstract string? UrlPathSegment { get; }
|
|
||||||
|
|
||||||
public IScreen HostScreen => screen;
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace InkForge.Common.ReactiveUI;
|
|
||||||
|
|
||||||
public interface IViewModelFactory<T, TCreator>
|
|
||||||
{
|
|
||||||
abstract static ObjectFactory<T> CreateObjectFactory();
|
|
||||||
|
|
||||||
abstract static TCreator GetCreator(ObjectFactory<T> factory, IServiceProvider serviceProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ViewModelFactory<T, TFactory, TCreator>
|
|
||||||
where TFactory : IViewModelFactory<T, TCreator>
|
|
||||||
where TCreator : Delegate
|
|
||||||
{
|
|
||||||
private static ObjectFactory<T>? s_factory;
|
|
||||||
|
|
||||||
public TCreator CreateFactory(IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
s_factory ??= TFactory.CreateObjectFactory();
|
|
||||||
return TFactory.GetCreator(s_factory, serviceProvider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
using InkForge.Common.Controllers;
|
|
||||||
|
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace InkForge.Common.ViewModels;
|
|
||||||
|
|
||||||
public class AppViewModel : ReactiveObject
|
|
||||||
{
|
|
||||||
private object _view;
|
|
||||||
|
|
||||||
public object View
|
|
||||||
{
|
|
||||||
get => _view;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref _view, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AppViewModel(WorkspaceController workspace, LandingViewModel landingViewModel)
|
|
||||||
{
|
|
||||||
View = landingViewModel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
using InkForge.Common.Controllers;
|
|
||||||
using InkForge.Common.ReactiveUI;
|
|
||||||
|
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace InkForge.Common.ViewModels.Landing;
|
|
||||||
|
|
||||||
public class CreateWorkspaceViewModel : LandingViewModelBase
|
|
||||||
{
|
|
||||||
public override string? UrlPathSegment => null;
|
|
||||||
|
|
||||||
private string workspaceName;
|
|
||||||
|
|
||||||
public string WorkspaceName
|
|
||||||
{
|
|
||||||
get => workspaceName;
|
|
||||||
set => this.RaiseAndSetIfChanged(ref workspaceName, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CreateWorkspaceViewModel(LandingViewModel landing, WorkspaceController workspace) : base(landing)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
using InkForge.Common.ReactiveUI;
|
|
||||||
|
|
||||||
namespace InkForge.Common.ViewModels.Landing;
|
|
||||||
|
|
||||||
public abstract class LandingViewModelBase(LandingViewModel landing) : RoutableReactiveObject(landing)
|
|
||||||
{
|
|
||||||
public LandingViewModel Landing => landing;
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace InkForge.Common.ViewModels.Landing;
|
|
||||||
|
|
||||||
public class LandingViewModelFactory(IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
public T Create<T>(LandingViewModel landing) where T : LandingViewModelBase
|
|
||||||
{
|
|
||||||
LandingViewModelsObjectParameters objectParameters = new(landing);
|
|
||||||
return TypeFactory<LandingViewModelsObjectParameters, T>.Create(objectParameters, serviceProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly record struct LandingViewModelsObjectParameters(
|
|
||||||
LandingViewModel Landing
|
|
||||||
) : IObjectParameters<LandingViewModelsObjectParameters>
|
|
||||||
{
|
|
||||||
public static Type[] Types => [typeof(LandingViewModel)];
|
|
||||||
|
|
||||||
public static implicit operator object[](in LandingViewModelsObjectParameters self) => [self.Landing];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
using InkForge.Common.ReactiveUI;
|
|
||||||
|
|
||||||
namespace InkForge.Common.ViewModels.Landing;
|
|
||||||
|
|
||||||
public class OpenRecentViewModel : LandingViewModelBase
|
|
||||||
{
|
|
||||||
public override string? UrlPathSegment => null;
|
|
||||||
private readonly ReadOnlyObservableCollection<RecentItemViewModel> recentItems;
|
|
||||||
|
|
||||||
public ReadOnlyObservableCollection<RecentItemViewModel> RecentItems => recentItems;
|
|
||||||
|
|
||||||
public OpenRecentViewModel(LandingViewModel landing) : base(landing)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace InkForge.Common.ViewModels.Landing;
|
|
||||||
|
|
||||||
public record class RecentItemViewModel(
|
|
||||||
DateTimeOffset Created,
|
|
||||||
string Name,
|
|
||||||
DateTimeOffset LastUsed
|
|
||||||
) : ReactiveRecord;
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
using System.Reactive.Linq;
|
|
||||||
|
|
||||||
using InkForge.Common.ViewModels.Landing;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace InkForge.Common.ViewModels;
|
|
||||||
|
|
||||||
public class LandingViewModel : ReactiveObject, IScreen
|
|
||||||
{
|
|
||||||
private readonly LandingViewModelFactory _factory;
|
|
||||||
|
|
||||||
public RoutingState Router { get; } = new();
|
|
||||||
|
|
||||||
public LandingViewModel(LandingViewModelFactory factory)
|
|
||||||
{
|
|
||||||
_factory = factory;
|
|
||||||
|
|
||||||
Router.CurrentViewModel.Where(x => x is null)
|
|
||||||
.SelectMany(Observable.Return(factory.Create<OpenRecentViewModel>(this)))
|
|
||||||
.InvokeCommand<IRoutableViewModel>(Router.NavigateAndReset);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Navigate<T>() where T : LandingViewModelBase
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<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:reactiveui="http://reactiveui.net"
|
|
||||||
xmlns:ifcvm="using:InkForge.Common.ViewModels"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
d:DesignWidth="800"
|
|
||||||
d:DesignHeight="450"
|
|
||||||
x:Class="InkForge.Common.Views.LandingView"
|
|
||||||
x:DataType="ifcvm:LandingViewModel">
|
|
||||||
<Grid RowDefinitions="Auto, *, Auto">
|
|
||||||
<Label Content="InkForge"
|
|
||||||
Grid.Row="0" />
|
|
||||||
|
|
||||||
<reactiveui:RoutedViewHost Router="{CompiledBinding Router}"
|
|
||||||
Grid.Row="1" />
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
using Avalonia.ReactiveUI;
|
|
||||||
|
|
||||||
using InkForge.Common.ViewModels;
|
|
||||||
|
|
||||||
namespace InkForge.Common.Views;
|
|
||||||
|
|
||||||
public partial class LandingView : ReactiveUserControl<LandingViewModel>
|
|
||||||
{
|
|
||||||
public LandingView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<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"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
d:DesignWidth="800"
|
|
||||||
d:DesignHeight="450"
|
|
||||||
x:Class="InkForge.Common.Views.LandingViews.CreateWorkspaceView">
|
|
||||||
Welcome to Avalonia!
|
|
||||||
</UserControl>
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
using Avalonia.ReactiveUI;
|
|
||||||
|
|
||||||
using InkForge.Common.ViewModels.Landing;
|
|
||||||
|
|
||||||
namespace InkForge.Common.Views.LandingViews;
|
|
||||||
|
|
||||||
public partial class CreateWorkspaceView : ReactiveUserControl<CreateWorkspaceViewModel>
|
|
||||||
{
|
|
||||||
public CreateWorkspaceView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<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:vm="using:InkForge.Common.ViewModels.Landing"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
d:DesignWidth="800"
|
|
||||||
d:DesignHeight="450"
|
|
||||||
x:Class="InkForge.Common.Views.LandingViews.OpenRecentView"
|
|
||||||
x:DataType="vm:OpenRecentViewModel">
|
|
||||||
<Grid RowDefinitions="Auto, *">
|
|
||||||
<Label Content="Open Recent"
|
|
||||||
Grid.Row="0" />
|
|
||||||
|
|
||||||
<DataGrid IsReadOnly="true"
|
|
||||||
ItemsSource="{CompiledBinding RecentItems}"
|
|
||||||
Grid.Row="1">
|
|
||||||
<DataGrid.Columns>
|
|
||||||
<DataGridTextColumn Header="Created"
|
|
||||||
Binding="{CompiledBinding Created, StringFormat={}{0:d}}" />
|
|
||||||
<DataGridTextColumn Header="Name"
|
|
||||||
Width="*"
|
|
||||||
Binding="{CompiledBinding Name}" />
|
|
||||||
<DataGridTextColumn Header="Last Used"
|
|
||||||
Binding="{CompiledBinding LastUsed, StringFormat={}{0:d}}" />
|
|
||||||
</DataGrid.Columns>
|
|
||||||
</DataGrid>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
using Avalonia.ReactiveUI;
|
|
||||||
|
|
||||||
using InkForge.Common.ViewModels.Landing;
|
|
||||||
|
|
||||||
namespace InkForge.Common.Views.LandingViews;
|
|
||||||
|
|
||||||
public partial class OpenRecentView : ReactiveUserControl<OpenRecentViewModel>
|
|
||||||
{
|
|
||||||
public OpenRecentView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
app/InkForge.Desktop/App.axaml
Normal file
17
app/InkForge.Desktop/App.axaml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="InkForge.Desktop.App"
|
||||||
|
RequestedThemeVariant="Default">
|
||||||
|
|
||||||
|
<Application.Styles>
|
||||||
|
<FluentTheme />
|
||||||
|
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
|
||||||
|
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
|
||||||
|
<DockFluentTheme />
|
||||||
|
</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>
|
||||||
87
app/InkForge.Desktop/App.axaml.cs
Normal file
87
app/InkForge.Desktop/App.axaml.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.Templates;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
using DynamicData;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
|
||||||
|
using Splat;
|
||||||
|
using Splat.Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<IDataTemplate> AppDataTemplateProperty
|
||||||
|
= AvaloniaProperty.Register<App, IDataTemplate>(
|
||||||
|
name: nameof(AppDataTemplate),
|
||||||
|
coerce: OnAppDataTemplateChanged);
|
||||||
|
public static readonly StyledProperty<IServiceProvider> ServiceProviderProperty
|
||||||
|
= AvaloniaProperty.Register<App, IServiceProvider>(
|
||||||
|
name: nameof(ServiceProvider),
|
||||||
|
coerce: OnServiceProviderChanged);
|
||||||
|
|
||||||
|
public IDataTemplate AppDataTemplate => GetValue(AppDataTemplateProperty);
|
||||||
|
|
||||||
|
public IServiceProvider ServiceProvider => GetValue(ServiceProviderProperty);
|
||||||
|
|
||||||
|
public static void Configure(IServiceCollection services, ConfigurationManager configuration)
|
||||||
|
{
|
||||||
|
configuration.SetBasePath(AppContext.BaseDirectory);
|
||||||
|
configuration.AddJsonFile(
|
||||||
|
new ManifestEmbeddedFileProvider(typeof(App).Assembly),
|
||||||
|
"Properties/appsettings.json", false, false);
|
||||||
|
configuration.AddJsonFile(
|
||||||
|
Path.Combine(
|
||||||
|
Environment.GetFolderPath(
|
||||||
|
Environment.SpecialFolder.ApplicationData,
|
||||||
|
Environment.SpecialFolderOption.DoNotVerify),
|
||||||
|
"InkForge",
|
||||||
|
"usersettings.json"), true, true);
|
||||||
|
configuration.AddJsonFile("appsettings.json", true, true);
|
||||||
|
|
||||||
|
services.UseMicrosoftDependencyResolver();
|
||||||
|
Locator.CurrentMutable.InitializeSplat();
|
||||||
|
|
||||||
|
services.AddInkForge();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDataTemplate OnAppDataTemplateChanged(AvaloniaObject @object, IDataTemplate dataTemplate)
|
||||||
|
{
|
||||||
|
var host = (IDataTemplateHost)@object;
|
||||||
|
var original = @object.GetValue(AppDataTemplateProperty);
|
||||||
|
|
||||||
|
if (original is null && dataTemplate is not null)
|
||||||
|
{
|
||||||
|
host.DataTemplates.Add(dataTemplate);
|
||||||
|
}
|
||||||
|
else if (original is not null)
|
||||||
|
{
|
||||||
|
if (dataTemplate is null)
|
||||||
|
{
|
||||||
|
host.DataTemplates.Remove(original);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
host.DataTemplates.ReplaceOrAdd(original, dataTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataTemplate!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IServiceProvider OnServiceProviderChanged(AvaloniaObject @object, IServiceProvider provider)
|
||||||
|
{
|
||||||
|
provider.UseMicrosoftDependencyResolver();
|
||||||
|
@object.SetValue(AppDataTemplateProperty, provider.GetRequiredService<IDataTemplate>());
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
app/InkForge.Desktop/AppViewLocator.cs
Normal file
45
app/InkForge.Desktop/AppViewLocator.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Templates;
|
||||||
|
|
||||||
|
using InkForge.Desktop.ViewModels;
|
||||||
|
using InkForge.Desktop.ViewModels.Documents;
|
||||||
|
using InkForge.Desktop.ViewModels.Workspaces;
|
||||||
|
using InkForge.Desktop.Views.Documents;
|
||||||
|
using InkForge.Desktop.Views.Workspaces;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop;
|
||||||
|
|
||||||
|
public class AppViewLocator : IDataTemplate
|
||||||
|
{
|
||||||
|
public Control? Build(object? param)
|
||||||
|
{
|
||||||
|
#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
|
||||||
|
return param switch
|
||||||
|
#pragma warning restore CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
|
||||||
|
{
|
||||||
|
NoteEditDocumentViewModel viewModel => _(new NoteEditDocument(), viewModel),
|
||||||
|
ViewModels.Tools.WorkspaceTool viewModel => _(new Views.Tools.WorkspaceTool(), viewModel),
|
||||||
|
WelcomePageDocumentViewModel viewModel => _(new WelcomePageDocument(), viewModel),
|
||||||
|
WorkspaceViewModel viewModel => _(new WorkspaceView(), viewModel),
|
||||||
|
};
|
||||||
|
|
||||||
|
static TView _<TView, TViewModel>(TView view, TViewModel viewModel)
|
||||||
|
where TViewModel : class
|
||||||
|
where TView : StyledElement
|
||||||
|
{
|
||||||
|
view.DataContext = viewModel;
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Match(object? data)
|
||||||
|
{
|
||||||
|
return data is
|
||||||
|
NoteEditDocumentViewModel or
|
||||||
|
RecentItemViewModel or
|
||||||
|
ViewModels.Tools.WorkspaceTool or
|
||||||
|
WelcomePageDocumentViewModel or
|
||||||
|
WorkspaceViewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
7836
app/InkForge.Desktop/Assets/Fonts/FluentSystemIcons-Filled.json
Normal file
7836
app/InkForge.Desktop/Assets/Fonts/FluentSystemIcons-Filled.json
Normal file
File diff suppressed because it is too large
Load diff
BIN
app/InkForge.Desktop/Assets/Fonts/FluentSystemIcons-Filled.ttf
Normal file
BIN
app/InkForge.Desktop/Assets/Fonts/FluentSystemIcons-Filled.ttf
Normal file
Binary file not shown.
7715
app/InkForge.Desktop/Assets/Fonts/FluentSystemIcons-Regular.json
Normal file
7715
app/InkForge.Desktop/Assets/Fonts/FluentSystemIcons-Regular.json
Normal file
File diff suppressed because it is too large
Load diff
BIN
app/InkForge.Desktop/Assets/Fonts/FluentSystemIcons-Regular.ttf
Normal file
BIN
app/InkForge.Desktop/Assets/Fonts/FluentSystemIcons-Regular.ttf
Normal file
Binary file not shown.
1
app/InkForge.Desktop/Assets/Fonts/FluentSystemIcons.txt
Normal file
1
app/InkForge.Desktop/Assets/Fonts/FluentSystemIcons.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
1.1.227
|
||||||
175
app/InkForge.Desktop/Controls/FluentSymbolIcon.cs
Normal file
175
app/InkForge.Desktop/Controls/FluentSymbolIcon.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/InkForge.Desktop/Data/NoteDbContextFactory.cs
Normal file
27
app/InkForge.Desktop/Data/NoteDbContextFactory.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
using InkForge.Data;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
using SmartFormat;
|
||||||
|
using InkForge.Desktop.Data.Options;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.Data;
|
||||||
|
|
||||||
|
public class NoteDbContextFactory(LocalWorkspaceOptions options, IConfiguration configuration) : IDbContextFactory<NoteDbContext>
|
||||||
|
{
|
||||||
|
private string? _connectionString;
|
||||||
|
|
||||||
|
public NoteDbContext CreateDbContext()
|
||||||
|
{
|
||||||
|
_connectionString ??= Smart.Format(configuration.GetConnectionString("DefaultConnection")!, new
|
||||||
|
{
|
||||||
|
WorkspaceFile = options.DbPath
|
||||||
|
});
|
||||||
|
|
||||||
|
DbContextOptionsBuilder<NoteDbContext> builder = new();
|
||||||
|
builder.UseSqlite(_connectionString, o => o.MigrationsAssembly("InkForge.Sqlite"));
|
||||||
|
|
||||||
|
return new NoteDbContext(builder.Options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace InkForge.Desktop.Data.Options;
|
||||||
|
|
||||||
|
public class LocalWorkspaceOptions
|
||||||
|
{
|
||||||
|
public string DbPath { get; set; } = default!;
|
||||||
|
}
|
||||||
|
|
@ -6,16 +6,42 @@
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AssemblyName>InkForge</AssemblyName>
|
<AssemblyName>InkForge</AssemblyName>
|
||||||
|
<RootNamespace>InkForge.Desktop</RootNamespace>
|
||||||
|
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
||||||
|
<!-- <TrimMode>partial</TrimMode> -->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia.AvaloniaEdit" />
|
||||||
|
<PackageReference Include="Avalonia.Controls.DataGrid" />
|
||||||
<PackageReference Include="Avalonia.Desktop" />
|
<PackageReference Include="Avalonia.Desktop" />
|
||||||
|
<PackageReference Include="Avalonia.Fonts.Inter" />
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" />
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||||
<PackageReference Include="Dock.Avalonia" />
|
<PackageReference Include="Dock.Avalonia" />
|
||||||
|
<PackageReference Include="Dock.Model.Mvvm" />
|
||||||
|
<PackageReference Include="DynamicData" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||||
|
<PackageReference Include="SmartFormat" />
|
||||||
|
<PackageReference Include="Splat" />
|
||||||
<PackageReference Include="Splat.Microsoft.Extensions.DependencyInjection" />
|
<PackageReference Include="Splat.Microsoft.Extensions.DependencyInjection" />
|
||||||
|
<PackageReference Include="System.IO.Hashing" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\InkForge.Common\InkForge.Common.csproj" />
|
<ProjectReference Include="..\..\shared\InkForge.Data\InkForge.Data.csproj" />
|
||||||
|
<ProjectReference Include="..\..\shared\migrations\InkForge.Sqlite\InkForge.Sqlite.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
<ItemGroup>
|
||||||
|
<AvaloniaResource Include="Assets\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Properties\appsettings.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
|
||||||
85
app/InkForge.Desktop/InkForgeFactory.cs
Normal file
85
app/InkForge.Desktop/InkForgeFactory.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
using Avalonia;
|
||||||
|
|
||||||
|
using Dock.Model.Controls;
|
||||||
|
using Dock.Model.Core;
|
||||||
|
using Dock.Model.Mvvm;
|
||||||
|
using Dock.Model.Mvvm.Controls;
|
||||||
|
|
||||||
|
using DynamicData.Binding;
|
||||||
|
|
||||||
|
using InkForge.Desktop.Managers;
|
||||||
|
using InkForge.Desktop.Models;
|
||||||
|
using InkForge.Desktop.ViewModels.Documents;
|
||||||
|
using InkForge.Desktop.ViewModels.Tools;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop;
|
||||||
|
|
||||||
|
public class InkForgeFactory : Factory
|
||||||
|
{
|
||||||
|
private readonly IDock _mainDock;
|
||||||
|
private readonly IRootDock _rootDock;
|
||||||
|
private readonly WelcomePageDocumentViewModel _welcomePage;
|
||||||
|
private readonly WorkspaceTool _workspaceTool;
|
||||||
|
|
||||||
|
public InkForgeFactory(WorkspaceManager workspace)
|
||||||
|
{
|
||||||
|
_rootDock = CreateRootDock();
|
||||||
|
_mainDock = CreateDockDock();
|
||||||
|
_mainDock.IsCollapsable = false;
|
||||||
|
_mainDock.CanClose = false;
|
||||||
|
|
||||||
|
_welcomePage = CreateWelcomePageDocumentViewModel();
|
||||||
|
_workspaceTool = CreateWorkspaceTool();
|
||||||
|
|
||||||
|
workspace.WhenValueChanged(m => m.Workspace).Subscribe(OnWorkspaceChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IRootDock CreateLayout()
|
||||||
|
{
|
||||||
|
ToolDock toolDock = new()
|
||||||
|
{
|
||||||
|
Alignment = Alignment.Left,
|
||||||
|
Proportion = 0.25,
|
||||||
|
VisibleDockables = [_workspaceTool],
|
||||||
|
};
|
||||||
|
|
||||||
|
ProportionalDock windowLayoutContent = new()
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
VisibleDockables = [toolDock, new ProportionalDockSplitter(), _mainDock]
|
||||||
|
};
|
||||||
|
|
||||||
|
_rootDock.VisibleDockables = [windowLayoutContent];
|
||||||
|
_rootDock.DefaultDockable = windowLayoutContent;
|
||||||
|
|
||||||
|
return _rootDock;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WelcomePageDocumentViewModel CreateWelcomePageDocumentViewModel()
|
||||||
|
{
|
||||||
|
return ActivatorUtilities.CreateInstance<WelcomePageDocumentViewModel>(
|
||||||
|
Application.Current!.GetValue(App.ServiceProviderProperty)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ViewModels.Tools.WorkspaceTool CreateWorkspaceTool()
|
||||||
|
{
|
||||||
|
return ActivatorUtilities.CreateInstance<ViewModels.Tools.WorkspaceTool>(
|
||||||
|
Application.Current!.GetValue(App.ServiceProviderProperty)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWorkspaceChanged(Workspace? workspace)
|
||||||
|
{
|
||||||
|
IDockable dock = workspace switch
|
||||||
|
{
|
||||||
|
null => _welcomePage,
|
||||||
|
_ => workspace.Services.GetRequiredService<DocumentManager>().Dock,
|
||||||
|
};
|
||||||
|
|
||||||
|
AddDockable(_mainDock, dock);
|
||||||
|
CloseOtherDockables(dock);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/InkForge.Desktop/Managers/DocumentManager.cs
Normal file
38
app/InkForge.Desktop/Managers/DocumentManager.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
using AvaloniaEdit.Document;
|
||||||
|
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
using Dock.Model.Controls;
|
||||||
|
|
||||||
|
using InkForge.Desktop.Models;
|
||||||
|
using InkForge.Desktop.ViewModels.Documents;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.Managers;
|
||||||
|
|
||||||
|
public partial class DocumentManager
|
||||||
|
{
|
||||||
|
private readonly InkForgeFactory _factory;
|
||||||
|
|
||||||
|
public IDocumentDock Dock { get; }
|
||||||
|
|
||||||
|
public DocumentManager(NoteStore noteStore, InkForgeFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
Dock = factory.CreateDocumentDock();
|
||||||
|
Dock.IsCollapsable = false;
|
||||||
|
Dock.CanCreateDocument = true;
|
||||||
|
Dock.CreateDocument = CreateDocumentCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void OnCreateDocument()
|
||||||
|
{
|
||||||
|
NoteEditDocumentViewModel editViewModel = new(new()
|
||||||
|
{
|
||||||
|
Name = "Untitled Note",
|
||||||
|
}, new());
|
||||||
|
_factory.AddDockable(Dock, editViewModel);
|
||||||
|
_factory.SetActiveDockable(editViewModel);
|
||||||
|
_factory.SetFocusedDockable(Dock, editViewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
app/InkForge.Desktop/Managers/WorkspaceManager.cs
Normal file
89
app/InkForge.Desktop/Managers/WorkspaceManager.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
using Avalonia;
|
||||||
|
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
using InkForge.Data;
|
||||||
|
using InkForge.Desktop.Data.Options;
|
||||||
|
using InkForge.Desktop.Models;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.Managers;
|
||||||
|
|
||||||
|
public partial class WorkspaceManager(IServiceProvider serviceProvider) : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider = serviceProvider;
|
||||||
|
[ObservableProperty] private Workspace? _workspace;
|
||||||
|
|
||||||
|
public ValueTask CloseWorkspace()
|
||||||
|
{
|
||||||
|
Workspace?.Dispose();
|
||||||
|
Workspace = null;
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask 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;
|
||||||
|
IWorkspaceContext workspaceContext;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
scope = _serviceProvider.CreateScope();
|
||||||
|
var serviceProvider = scope.ServiceProvider;
|
||||||
|
var options = serviceProvider.GetRequiredService<LocalWorkspaceOptions>();
|
||||||
|
options.DbPath = path;
|
||||||
|
|
||||||
|
workspaceContext = serviceProvider.GetRequiredService<IWorkspaceContext>();
|
||||||
|
workspaceContext.Workspace = new Workspace(scope)
|
||||||
|
{
|
||||||
|
Name = Path.GetFileNameWithoutExtension(file.Name),
|
||||||
|
Options = options,
|
||||||
|
};
|
||||||
|
|
||||||
|
var dbFactory = serviceProvider.GetRequiredService<IDbContextFactory<NoteDbContext>>();
|
||||||
|
await using var dbContext = await dbFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
var db = dbContext.Database;
|
||||||
|
if ((await db.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
|
||||||
|
{
|
||||||
|
if ((await db.GetAppliedMigrationsAsync().ConfigureAwait(false)).Any())
|
||||||
|
{
|
||||||
|
file.CopyTo(Path.ChangeExtension(file.FullName, $"{DateTime.Now:s}{file.Extension}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.MigrateAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
await serviceProvider.GetRequiredService<NoteStore>().Load().ConfigureAwait(false);
|
||||||
|
|
||||||
|
scope = null;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Show Error through TopLevels.ActiveTopLevel
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
scope?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return workspaceContext.Workspace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace InkForge.Desktop.MarkupExtensions;
|
||||||
|
|
||||||
|
public class FluentSymbolIconExtension
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
using Avalonia.Controls.Templates;
|
||||||
|
|
||||||
|
using Dock.Model.Core;
|
||||||
|
|
||||||
|
using InkForge.Data;
|
||||||
|
using InkForge.Desktop;
|
||||||
|
using InkForge.Desktop.Data;
|
||||||
|
using InkForge.Desktop.Data.Options;
|
||||||
|
using InkForge.Desktop.Managers;
|
||||||
|
using InkForge.Desktop.Models;
|
||||||
|
using InkForge.Desktop.ViewModels.Workspaces;
|
||||||
|
using InkForge.Desktop.ViewModels.Workspaces.Internal;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
public static class InkForgeServiceCollections
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddInkForge(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddHttpClient();
|
||||||
|
|
||||||
|
// Singletons
|
||||||
|
// - Concrete
|
||||||
|
services.AddSingleton<InkForgeFactory>();
|
||||||
|
services.AddSingleton<WorkspaceManager>();
|
||||||
|
|
||||||
|
// - Service
|
||||||
|
services.AddSingleton<IDataTemplate, AppViewLocator>();
|
||||||
|
services.AddSingleton<IWorkspaceViewModelFactory, WorkspaceViewModelFactory>();
|
||||||
|
|
||||||
|
// Scoped
|
||||||
|
// - Concrete
|
||||||
|
services.AddScoped<DocumentManager>();
|
||||||
|
services.AddScoped<LocalWorkspaceOptions>();
|
||||||
|
services.AddScoped<NoteStore>();
|
||||||
|
|
||||||
|
// - Service
|
||||||
|
services.AddScoped<IDbContextFactory<NoteDbContext>, NoteDbContextFactory>();
|
||||||
|
services.AddScoped<IWorkspaceContext, WorkspaceContext>();
|
||||||
|
|
||||||
|
// - Forwarders
|
||||||
|
services.AddScoped(s => s.GetRequiredService<IWorkspaceContext>().Workspace!);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
app/InkForge.Desktop/Models/Note.cs
Normal file
12
app/InkForge.Desktop/Models/Note.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.Models;
|
||||||
|
|
||||||
|
public partial class Note : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private DateTimeOffset _createdTime;
|
||||||
|
[ObservableProperty] private int _id;
|
||||||
|
[ObservableProperty] private string _name = default!;
|
||||||
|
[ObservableProperty] private int? _parentId;
|
||||||
|
[ObservableProperty] private DateTimeOffset _updatedTime;
|
||||||
|
}
|
||||||
81
app/InkForge.Desktop/Models/NoteStore.cs
Normal file
81
app/InkForge.Desktop/Models/NoteStore.cs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
using Avalonia.Collections;
|
||||||
|
|
||||||
|
using InkForge.Data;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.Models;
|
||||||
|
|
||||||
|
public class NoteStore(IDbContextFactory<NoteDbContext> dbContextFactory)
|
||||||
|
{
|
||||||
|
public AvaloniaDictionary<int, Note> Notes { get; } = [];
|
||||||
|
|
||||||
|
public async ValueTask Load()
|
||||||
|
{
|
||||||
|
await using var dbContext = await dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
Notes.Clear();
|
||||||
|
await foreach (var note in dbContext.Notes.AsAsyncEnumerable().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
Notes.Add(note.Id, Map(note));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddNote(Note note)
|
||||||
|
{
|
||||||
|
using var dbContext = dbContextFactory.CreateDbContext();
|
||||||
|
var entity = Map(note);
|
||||||
|
var entry = dbContext.Notes.Add(entity);
|
||||||
|
|
||||||
|
dbContext.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Note? GetById(int id)
|
||||||
|
{
|
||||||
|
if (!Notes.TryGetValue(id, out var note))
|
||||||
|
{
|
||||||
|
using var dbContext = dbContextFactory.CreateDbContext();
|
||||||
|
if (dbContext.Notes.Find(id) is not { } dbNote)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Notes.Add(id, note = Map(dbNote));
|
||||||
|
}
|
||||||
|
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NoteEntity Map(Note note)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = note.Id,
|
||||||
|
Value = new()
|
||||||
|
{
|
||||||
|
Name = note.Name,
|
||||||
|
Created = note.CreatedTime,
|
||||||
|
Updated = note.UpdatedTime,
|
||||||
|
},
|
||||||
|
Parent = note.ParentId switch
|
||||||
|
{
|
||||||
|
{ } parentId => new()
|
||||||
|
{
|
||||||
|
Id = parentId
|
||||||
|
},
|
||||||
|
_ => null
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Note Map(NoteEntity entity)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = entity.Id,
|
||||||
|
Name = entity.Value.Name,
|
||||||
|
CreatedTime = entity.Value.Created,
|
||||||
|
UpdatedTime = entity.Value.Updated,
|
||||||
|
ParentId = entity.Parent?.Id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
131
app/InkForge.Desktop/Models/TextDocumentStore.cs
Normal file
131
app/InkForge.Desktop/Models/TextDocumentStore.cs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using Avalonia.Collections;
|
||||||
|
|
||||||
|
using AvaloniaEdit.Document;
|
||||||
|
|
||||||
|
using InkForge.Data;
|
||||||
|
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.Models;
|
||||||
|
|
||||||
|
public class TextDocumentStore(IDbContextFactory<NoteDbContext> dbContextFactory)
|
||||||
|
{
|
||||||
|
private readonly AvaloniaDictionary<int, TextDocument> _cache = [];
|
||||||
|
|
||||||
|
public void Close(int id)
|
||||||
|
{
|
||||||
|
if (_cache.Remove(id, out var document))
|
||||||
|
{
|
||||||
|
document.Remove(0, document.TextLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextDocument Get(int id)
|
||||||
|
{
|
||||||
|
if (!_cache.TryGetValue(id, out var document))
|
||||||
|
{
|
||||||
|
using var dbContext = dbContextFactory.CreateDbContext();
|
||||||
|
var connection = dbContext.Database.GetDbConnection();
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = $"SELECT rowid FROM Blobs WHERE Id={id}";
|
||||||
|
using var commandReader = command.ExecuteReader();
|
||||||
|
document = new();
|
||||||
|
if (commandReader.Read())
|
||||||
|
{
|
||||||
|
using SqliteBlob stream = new((SqliteConnection)connection, "Blob", "Value", commandReader.GetInt64(0), true);
|
||||||
|
CopyDocumentContent(stream, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
_cache.Add(id, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
return document;
|
||||||
|
|
||||||
|
static void CopyDocumentContent(Stream stream, TextDocument document)
|
||||||
|
{
|
||||||
|
DocumentTextWriter writer = new(document, 0);
|
||||||
|
StreamReader reader = new(stream, Encoding.UTF8);
|
||||||
|
Span<char> buffer = stackalloc char[128];
|
||||||
|
while (!reader.EndOfStream)
|
||||||
|
{
|
||||||
|
var read = reader.Read(buffer);
|
||||||
|
writer.Write(buffer[..read]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int? Save(int? id, TextDocument textDocument)
|
||||||
|
{
|
||||||
|
var state = textDocument.CreateSnapshot();
|
||||||
|
var bytes = TextBytes(state);
|
||||||
|
|
||||||
|
using var dbContext = dbContextFactory.CreateDbContext();
|
||||||
|
var connection = dbContext.Database.GetDbConnection();
|
||||||
|
using var transaction = dbContext.Database.BeginTransaction();
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = id switch
|
||||||
|
{
|
||||||
|
null => $"INSERT INTO Blobs(Value) VALUES(zeroblob({bytes})) RETURNING (Id, rowid)",
|
||||||
|
_ => $"UPDATE Blobs SET Value=zeroblob({bytes}) WHERE Id={id} RETURNING (Id, rowid)"
|
||||||
|
};
|
||||||
|
using var commandReader = command.ExecuteReader();
|
||||||
|
if (!commandReader.Read())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
id = commandReader.GetInt32(0);
|
||||||
|
using (SqliteBlob stream = new((SqliteConnection)connection, "Blobs", "Value", commandReader.GetInt64(1)))
|
||||||
|
{
|
||||||
|
using StreamWriter writer = new(stream, Encoding.UTF8);
|
||||||
|
state.WriteTextTo(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
_cache.TryAdd((int)id, textDocument);
|
||||||
|
return id;
|
||||||
|
|
||||||
|
static int TextBytes(ITextSource text)
|
||||||
|
{
|
||||||
|
int length = 0;
|
||||||
|
Span<byte> byteBuffer = stackalloc byte[128];
|
||||||
|
using var reader = text.CreateReader();
|
||||||
|
ArrayBufferWriter<char> buffer = new();
|
||||||
|
var encoder = Encoding.UTF8.GetEncoder();
|
||||||
|
while (reader.Peek() != -1)
|
||||||
|
{
|
||||||
|
var memory = buffer.GetMemory();
|
||||||
|
buffer.Advance(reader.Read(memory.Span));
|
||||||
|
ReadOnlySpan<char> chars = buffer.WrittenSpan;
|
||||||
|
|
||||||
|
convert:
|
||||||
|
encoder.Convert(buffer.WrittenSpan, byteBuffer, false, out var charsUsed, out var bytesUsed, out _);
|
||||||
|
chars = chars[charsUsed..];
|
||||||
|
if (bytesUsed > 0)
|
||||||
|
{
|
||||||
|
length += bytesUsed;
|
||||||
|
goto convert;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<char> remaining = [];
|
||||||
|
if (!chars.IsEmpty)
|
||||||
|
{
|
||||||
|
remaining = buffer.GetSpan(chars.Length);
|
||||||
|
chars.CopyTo(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.Clear();
|
||||||
|
if (!remaining.IsEmpty)
|
||||||
|
{
|
||||||
|
buffer.Write(remaining);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/InkForge.Desktop/Models/Workspace.cs
Normal file
31
app/InkForge.Desktop/Models/Workspace.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
using InkForge.Desktop.Data.Options;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.Models;
|
||||||
|
|
||||||
|
public sealed class Workspace(IServiceScope scope) : IDisposable
|
||||||
|
{
|
||||||
|
private bool _disposedValue;
|
||||||
|
private IServiceScope? _scope = scope;
|
||||||
|
|
||||||
|
public string Name { get; set; } = default!;
|
||||||
|
|
||||||
|
public LocalWorkspaceOptions Options { get; set; } = default!;
|
||||||
|
|
||||||
|
public IServiceProvider Services { get; } = scope.ServiceProvider;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposedValue)
|
||||||
|
{
|
||||||
|
if (_scope is { })
|
||||||
|
{
|
||||||
|
_scope.Dispose();
|
||||||
|
_scope = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposedValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
app/InkForge.Desktop/Models/WorkspaceContext.cs
Normal file
12
app/InkForge.Desktop/Models/WorkspaceContext.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
namespace InkForge.Desktop.Models
|
||||||
|
{
|
||||||
|
public interface IWorkspaceContext
|
||||||
|
{
|
||||||
|
Workspace? Workspace { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WorkspaceContext : IWorkspaceContext
|
||||||
|
{
|
||||||
|
public Workspace? Workspace { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,50 +1,30 @@
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
|
||||||
using InkForge.Common;
|
using InkForge.Desktop;
|
||||||
using InkForge.Common.ViewModels;
|
|
||||||
using InkForge.Data;
|
|
||||||
using InkForge.Desktop.Views;
|
using InkForge.Desktop.Views;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
using Splat;
|
|
||||||
using Splat.Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
static class Program
|
static class Program
|
||||||
{
|
{
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
=> BuildAvaloniaApp()
|
=> BuildAvaloniaApp()
|
||||||
.UseMicrosoftDependencyInjection()
|
.UseMicrosoftDependencyInjection(out var configuration)
|
||||||
.StartWithClassicDesktopLifetime(args);
|
.StartWithClassicDesktopLifetime(args, configuration.WithMicrosoftDependencyInjection);
|
||||||
|
|
||||||
public static AppBuilder BuildAvaloniaApp()
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
=> AppBuilder.Configure<App>()
|
=> AppBuilder.Configure<App>()
|
||||||
.UsePlatformDetect()
|
.UsePlatformDetect()
|
||||||
.UseReactiveUI()
|
|
||||||
.WithInterFont()
|
.WithInterFont()
|
||||||
.LogToTrace();
|
.LogToTrace();
|
||||||
|
|
||||||
private static void ConfigureServices(IServiceCollection services)
|
private static void SetupApp(this IServiceCollection services, AppBuilder appBuilder)
|
||||||
{
|
|
||||||
services.UseMicrosoftDependencyResolver();
|
|
||||||
var mutableResolver = Locator.CurrentMutable;
|
|
||||||
mutableResolver.InitializeSplat();
|
|
||||||
mutableResolver.InitializeReactiveUI();
|
|
||||||
|
|
||||||
services.AddInkForge();
|
|
||||||
|
|
||||||
services.AddTransient<IViewFor<AppViewModel>, MainWindow>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OnSetup(this IServiceCollection services, AppBuilder appBuilder)
|
|
||||||
{
|
{
|
||||||
var dispatcher = Dispatcher.UIThread;
|
var dispatcher = Dispatcher.UIThread;
|
||||||
|
|
||||||
var app = appBuilder.Instance!;
|
var app = appBuilder.Instance!;
|
||||||
services
|
services
|
||||||
.AddSingleton(app)
|
.AddSingleton(app)
|
||||||
|
|
@ -53,16 +33,56 @@ static class Program
|
||||||
.AddSingleton(dispatcher);
|
.AddSingleton(dispatcher);
|
||||||
|
|
||||||
var serviceProvider = services.BuildServiceProvider();
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
serviceProvider.UseMicrosoftDependencyResolver();
|
|
||||||
app.SetValue(App.ServiceProviderProperty, serviceProvider);
|
app.SetValue(App.ServiceProviderProperty, serviceProvider);
|
||||||
dispatcher.ShutdownFinished += (_, _) => serviceProvider.Dispose();
|
_ = new ServiceProviderDisposer(serviceProvider, dispatcher);
|
||||||
|
_ = app.ApplicationLifetime switch
|
||||||
|
{
|
||||||
|
IClassicDesktopStyleApplicationLifetime desktop => desktop.MainWindow = new MainWindow(),
|
||||||
|
_ => throw new NotSupportedException(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AppBuilder UseMicrosoftDependencyInjection(this AppBuilder builder)
|
private static AppBuilder UseMicrosoftDependencyInjection(this AppBuilder builder, out ConfigurationManager configuration)
|
||||||
{
|
{
|
||||||
|
configuration = new();
|
||||||
ServiceCollection services = [];
|
ServiceCollection services = [];
|
||||||
ConfigureServices(services);
|
services.AddSingleton<IConfiguration>(configuration);
|
||||||
builder.AfterSetup(services.OnSetup);
|
App.Configure(services, configuration);
|
||||||
|
|
||||||
|
builder.AfterSetup(services.SetupApp);
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void WithMicrosoftDependencyInjection(this ConfigurationManager configuration, IClassicDesktopStyleApplicationLifetime lifetime)
|
||||||
|
{
|
||||||
|
configuration.AddCommandLine(lifetime.Args ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ServiceProviderDisposer
|
||||||
|
{
|
||||||
|
private readonly ServiceProvider _serviceProvider;
|
||||||
|
private ValueTask? _shutdownTask;
|
||||||
|
|
||||||
|
public ServiceProviderDisposer(ServiceProvider serviceProvider, Dispatcher dispatcher)
|
||||||
|
{
|
||||||
|
dispatcher.ShutdownFinished += OnShutdownFinished;
|
||||||
|
dispatcher.ShutdownStarted += OnShutdownStarted;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnShutdownFinished(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_shutdownTask is { IsCompleted: false } disposeTask)
|
||||||
|
{
|
||||||
|
disposeTask.GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnShutdownStarted(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
#pragma warning disable CA2012 // This will only ever be awaited once in ShutdownFinished
|
||||||
|
_shutdownTask = _serviceProvider.DisposeAsync();
|
||||||
|
#pragma warning restore CA2012
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
6
app/InkForge.Desktop/Properties/ApplicationSettings.cs
Normal file
6
app/InkForge.Desktop/Properties/ApplicationSettings.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace InkForge.Desktop.Properties;
|
||||||
|
|
||||||
|
public class ApplicationSettings
|
||||||
|
{
|
||||||
|
public List<string> History { get; } = [];
|
||||||
|
}
|
||||||
6
app/InkForge.Desktop/Properties/AssemblyInfo.cs
Normal file
6
app/InkForge.Desktop/Properties/AssemblyInfo.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
using Avalonia.Metadata;
|
||||||
|
|
||||||
|
[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")]
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace InkForge.Common.Properties;
|
namespace InkForge.Desktop.Properties;
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(ApplicationSettings))]
|
||||||
[JsonSerializable(typeof(IDictionary<string, object>))]
|
[JsonSerializable(typeof(IDictionary<string, object>))]
|
||||||
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
|
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
|
||||||
public partial class ConfigContext : JsonSerializerContext;
|
public partial class ConfigContext : JsonSerializerContext;
|
||||||
5
app/InkForge.Desktop/Properties/appsettings.json
Normal file
5
app/InkForge.Desktop/Properties/appsettings.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Data Source={WorkspaceFile}"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
app/InkForge.Desktop/Services/StorageProviderExtensions.cs
Normal file
13
app/InkForge.Desktop/Services/StorageProviderExtensions.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.Services;
|
||||||
|
|
||||||
|
public static class StorageProviderExtensions
|
||||||
|
{
|
||||||
|
public static IStorageProvider? GetStorageProvider(this object? context)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
|
||||||
|
return TopLevels.GetTopLevelForContext(context)?.StorageProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/InkForge.Desktop/Services/TopLevels.cs
Normal file
69
app/InkForge.Desktop/Services/TopLevels.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.Services;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return element.GetValue(RegisterProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TopLevel? GetTopLevelForContext(object context)
|
||||||
|
{
|
||||||
|
return TopLevel.GetTopLevel(GetVisualForContext(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Visual? GetVisualForContext(object context)
|
||||||
|
{
|
||||||
|
return RegistrationMapper.TryGetValue(context, out var result) ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetRegister(AvaloniaObject element, object value)
|
||||||
|
{
|
||||||
|
element.SetValue(RegisterProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterChanged(Visual sender, AvaloniaPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(sender);
|
||||||
|
|
||||||
|
// Unregister any old registered context
|
||||||
|
if (e.OldValue != null)
|
||||||
|
{
|
||||||
|
RegistrationMapper.Remove(e.OldValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register any new context
|
||||||
|
if (e.NewValue != null)
|
||||||
|
{
|
||||||
|
CollectionsMarshal.GetValueRefOrAddDefault(RegistrationMapper, e.NewValue, out _) = 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
147
app/InkForge.Desktop/System/Reactive/Linq/Observable.Switch.cs
Normal file
147
app/InkForge.Desktop/System/Reactive/Linq/Observable.Switch.cs
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
|
||||||
|
namespace System.Reactive.Linq;
|
||||||
|
|
||||||
|
public static class ObservableSwitch
|
||||||
|
{
|
||||||
|
public static IObservable<T> Switch<T>(this IObservable<IObservable<T>> observable, T defaultValue)
|
||||||
|
{
|
||||||
|
return new SwitchObservable<T>(observable, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SwitchObservable<T>(IObservable<IObservable<T>> sources, T defaultValue) : ObservableBase<T>
|
||||||
|
{
|
||||||
|
protected override IDisposable SubscribeCore(IObserver<T> observer)
|
||||||
|
{
|
||||||
|
_ _ = new(defaultValue, observer);
|
||||||
|
_.Run(sources);
|
||||||
|
return _;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class _(T defaultValue, IObserver<T> observer) : ObserverBase<IObservable<T>>
|
||||||
|
{
|
||||||
|
private readonly SerialDisposable _innerSerialDisposable = new();
|
||||||
|
private readonly SingleAssignmentDisposable _upstream = new();
|
||||||
|
private bool _hasLatest;
|
||||||
|
private int _latest;
|
||||||
|
private bool _stopped = false;
|
||||||
|
|
||||||
|
public void Run(IObservable<IObservable<T>> sources)
|
||||||
|
{
|
||||||
|
_upstream.Disposable = sources.Subscribe(this);
|
||||||
|
if (_innerSerialDisposable.Disposable is null)
|
||||||
|
{
|
||||||
|
observer.OnNext(defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_innerSerialDisposable?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ForwardOnCompleted() => observer.OnCompleted();
|
||||||
|
|
||||||
|
protected void ForwardOnError(Exception error) => observer.OnError(error);
|
||||||
|
|
||||||
|
protected void ForwardOnNext(T value) => observer.OnNext(value);
|
||||||
|
|
||||||
|
protected override void OnCompletedCore()
|
||||||
|
{
|
||||||
|
_upstream.Dispose();
|
||||||
|
_stopped = true;
|
||||||
|
|
||||||
|
if (!_hasLatest)
|
||||||
|
{
|
||||||
|
observer.OnCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnErrorCore(Exception error) => ForwardOnError(error);
|
||||||
|
|
||||||
|
protected override void OnNextCore(IObservable<T> value)
|
||||||
|
{
|
||||||
|
uint id = unchecked((uint)Interlocked.Increment(ref _latest));
|
||||||
|
_hasLatest = true;
|
||||||
|
|
||||||
|
var innerObserver = new InnerObserver(this, id, defaultValue);
|
||||||
|
_innerSerialDisposable.Disposable = innerObserver;
|
||||||
|
innerObserver.Subscribe(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InnerObserver(_ parent, uint id, T defaultValue) : ObserverBase<T>
|
||||||
|
{
|
||||||
|
private readonly SingleAssignmentDisposable _upstream = new();
|
||||||
|
|
||||||
|
public bool Found { get; set; } = false;
|
||||||
|
|
||||||
|
public void Subscribe(IObservable<T> upstream)
|
||||||
|
{
|
||||||
|
_upstream.Disposable = upstream.SubscribeSafe(this);
|
||||||
|
if (!Found)
|
||||||
|
{
|
||||||
|
OnNext(defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_upstream.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnCompletedCore()
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
|
||||||
|
if (parent._latest == id)
|
||||||
|
{
|
||||||
|
parent._hasLatest = false;
|
||||||
|
if (!Found)
|
||||||
|
{
|
||||||
|
OnNextCore(defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent._stopped)
|
||||||
|
{
|
||||||
|
parent.ForwardOnCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnErrorCore(Exception error)
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
|
||||||
|
if (parent._latest == id)
|
||||||
|
{
|
||||||
|
if (!Found)
|
||||||
|
{
|
||||||
|
OnNextCore(defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.ForwardOnError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnNextCore(T value)
|
||||||
|
{
|
||||||
|
Found = true;
|
||||||
|
if (parent._latest == id)
|
||||||
|
{
|
||||||
|
parent.ForwardOnNext(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
|
||||||
|
using AvaloniaEdit.Document;
|
||||||
|
|
||||||
|
using Dock.Model.Mvvm.Controls;
|
||||||
|
|
||||||
|
using DynamicData.Binding;
|
||||||
|
|
||||||
|
using InkForge.Desktop.Models;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.ViewModels.Documents;
|
||||||
|
|
||||||
|
public class NoteEditDocumentViewModel : Document
|
||||||
|
{
|
||||||
|
public Note Note { get; }
|
||||||
|
|
||||||
|
public TextDocument Document { get; }
|
||||||
|
|
||||||
|
public NoteEditDocumentViewModel(Note note, TextDocument textDocument)
|
||||||
|
{
|
||||||
|
Note = note;
|
||||||
|
Document = textDocument;
|
||||||
|
|
||||||
|
Observable.CombineLatest(
|
||||||
|
this.WhenValueChanged(v => v.Note.Name),
|
||||||
|
this.WhenValueChanged(v => v.Document.UndoStack.IsOriginalFile),
|
||||||
|
(name, original) => original ? name : $"{name} *"
|
||||||
|
).Subscribe(title => Title = title!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
using System.Reactive;
|
||||||
|
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
|
||||||
|
using Dock.Model.Mvvm.Controls;
|
||||||
|
|
||||||
|
using InkForge.Desktop.Managers;
|
||||||
|
using InkForge.Desktop.Services;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.ViewModels.Documents;
|
||||||
|
|
||||||
|
public partial class WelcomePageDocumentViewModel : Document
|
||||||
|
{
|
||||||
|
private readonly WorkspaceManager _workspaceController;
|
||||||
|
|
||||||
|
public WelcomePageDocumentViewModel(WorkspaceManager workspaceController)
|
||||||
|
{
|
||||||
|
Title = "Welcome";
|
||||||
|
|
||||||
|
_workspaceController = workspaceController;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task OnCreateNew()
|
||||||
|
{
|
||||||
|
var storageProvider = this.GetStorageProvider()!;
|
||||||
|
|
||||||
|
var documents = await storageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents);
|
||||||
|
var file = await storageProvider.SaveFilePickerAsync(new()
|
||||||
|
{
|
||||||
|
DefaultExtension = ".ifdb",
|
||||||
|
FileTypeChoices =
|
||||||
|
[
|
||||||
|
new FilePickerFileType("InkForge Database File")
|
||||||
|
{
|
||||||
|
Patterns = [ "*.ifdb" ],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SuggestedStartLocation = documents,
|
||||||
|
Title = "Select InkForge Database Name",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (file?.TryGetLocalPath() is not { } filePath)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _workspaceController.OpenWorkspace(filePath, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task OnOpenNew()
|
||||||
|
{
|
||||||
|
var storageProvider = this.GetStorageProvider()!;
|
||||||
|
|
||||||
|
var documents = await storageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents);
|
||||||
|
var files = await storageProvider.OpenFilePickerAsync(new()
|
||||||
|
{
|
||||||
|
AllowMultiple = false,
|
||||||
|
SuggestedStartLocation = documents,
|
||||||
|
FileTypeFilter =
|
||||||
|
[
|
||||||
|
new FilePickerFileType("InkForge Database File")
|
||||||
|
{
|
||||||
|
Patterns = [ "*.ifdb" ]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Title = "Open InkForge Database file"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.Count != 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files[0].TryGetLocalPath() is not { } filePath)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _workspaceController.OpenWorkspace(filePath, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/InkForge.Desktop/ViewModels/MainViewModel.cs
Normal file
16
app/InkForge.Desktop/ViewModels/MainViewModel.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
using Dock.Model.Core;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.ViewModels;
|
||||||
|
|
||||||
|
public class MainViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public IDock Layout { get; }
|
||||||
|
|
||||||
|
public MainViewModel(InkForgeFactory factory)
|
||||||
|
{
|
||||||
|
Layout = factory.CreateLayout();
|
||||||
|
factory.InitLayout(Layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
app/InkForge.Desktop/ViewModels/RecentItemViewModel.cs
Normal file
14
app/InkForge.Desktop/ViewModels/RecentItemViewModel.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.ViewModels;
|
||||||
|
|
||||||
|
public partial class RecentItemViewModel(
|
||||||
|
DateTimeOffset created,
|
||||||
|
string name,
|
||||||
|
DateTimeOffset lastUsed
|
||||||
|
) : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private DateTimeOffset _created = created;
|
||||||
|
[ObservableProperty] private string _name = name;
|
||||||
|
[ObservableProperty] private DateTimeOffset _lastUsed = lastUsed;
|
||||||
|
}
|
||||||
40
app/InkForge.Desktop/ViewModels/Tools/WorkspaceTool.cs
Normal file
40
app/InkForge.Desktop/ViewModels/Tools/WorkspaceTool.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
using Dock.Model.Mvvm.Controls;
|
||||||
|
|
||||||
|
using DynamicData.Binding;
|
||||||
|
|
||||||
|
using InkForge.Desktop.Managers;
|
||||||
|
using InkForge.Desktop.Models;
|
||||||
|
using InkForge.Desktop.ViewModels.Workspaces;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.ViewModels.Tools;
|
||||||
|
|
||||||
|
public partial class WorkspaceTool : Tool
|
||||||
|
{
|
||||||
|
private readonly IWorkspaceViewModelFactory _workspaceViewModelFactory;
|
||||||
|
[ObservableProperty] private WorkspaceViewModel? _workspace;
|
||||||
|
|
||||||
|
public WorkspaceTool(WorkspaceManager workspaceManager, IWorkspaceViewModelFactory workspaceViewModelFactory)
|
||||||
|
{
|
||||||
|
_workspaceViewModelFactory = workspaceViewModelFactory;
|
||||||
|
|
||||||
|
Title = "Explorer";
|
||||||
|
CanClose = false;
|
||||||
|
CanFloat = false;
|
||||||
|
CanPin = false;
|
||||||
|
|
||||||
|
workspaceManager.WhenValueChanged(v => v.Workspace).Subscribe(OnWorkspaceManagerWorkspaceChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWorkspaceManagerWorkspaceChanged(Workspace? workspace)
|
||||||
|
{
|
||||||
|
Workspace = workspace switch
|
||||||
|
{
|
||||||
|
{ } v => _workspaceViewModelFactory.Create(v),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
using Avalonia;
|
||||||
|
|
||||||
|
using DynamicData;
|
||||||
|
|
||||||
|
using InkForge.Desktop.Models;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.ViewModels.Workspaces
|
||||||
|
{
|
||||||
|
public class WorkspaceViewModel
|
||||||
|
{
|
||||||
|
private readonly Workspace _workspace;
|
||||||
|
private readonly NoteStore _noteStore;
|
||||||
|
private readonly ReadOnlyObservableCollection<Node<Note, int>> _notes;
|
||||||
|
|
||||||
|
public string Name => _workspace.Name;
|
||||||
|
|
||||||
|
public ReadOnlyObservableCollection<Node<Note, int>> Notes => _notes;
|
||||||
|
|
||||||
|
public WorkspaceViewModel(Workspace workspace, NoteStore noteStore)
|
||||||
|
{
|
||||||
|
_workspace = workspace;
|
||||||
|
_noteStore = noteStore;
|
||||||
|
noteStore.Notes
|
||||||
|
.AsObservableChangeSet(m => m.Key)
|
||||||
|
.Transform(m => m.Value, true)
|
||||||
|
.TransformToTree(m => m.Id)
|
||||||
|
.Bind(out _notes).Subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IWorkspaceViewModelFactory
|
||||||
|
{
|
||||||
|
WorkspaceViewModel Create(Workspace workspace);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Internal
|
||||||
|
{
|
||||||
|
internal class WorkspaceViewModelFactory : IWorkspaceViewModelFactory
|
||||||
|
{
|
||||||
|
private static ObjectFactory<WorkspaceViewModel>? s_workspaceViewModelFactory;
|
||||||
|
|
||||||
|
public static WorkspaceViewModel Create(Workspace workspace)
|
||||||
|
{
|
||||||
|
s_workspaceViewModelFactory ??= ActivatorUtilities.CreateFactory<WorkspaceViewModel>([typeof(Workspace)]);
|
||||||
|
return s_workspaceViewModelFactory(workspace.Services, [workspace]);
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceViewModel IWorkspaceViewModelFactory.Create(Workspace workspace) => Create(workspace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/InkForge.Desktop/Views/Documents/NoteEditDocument.axaml
Normal file
15
app/InkForge.Desktop/Views/Documents/NoteEditDocument.axaml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<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:vm="using:InkForge.Desktop.ViewModels.Documents"
|
||||||
|
xmlns:avaloniaedit="https://github.com/avaloniaui/avaloniaedit"
|
||||||
|
xmlns:inkforge="app:InkForge"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="800"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
x:Class="InkForge.Desktop.Views.Documents.NoteEditDocument"
|
||||||
|
x:DataType="vm:NoteEditDocumentViewModel"
|
||||||
|
inkforge:TopLevels.Register="{CompiledBinding}">
|
||||||
|
<avaloniaedit:TextEditor Document="{CompiledBinding Document}" />
|
||||||
|
</UserControl>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.Views.Documents;
|
||||||
|
|
||||||
|
public partial class NoteEditDocument : UserControl
|
||||||
|
{
|
||||||
|
public NoteEditDocument()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<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:vm="using:InkForge.Desktop.ViewModels.Documents"
|
||||||
|
xmlns:inkforge="app:InkForge"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="800"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
x:Class="InkForge.Desktop.Views.Documents.WelcomePageDocument"
|
||||||
|
x:DataType="vm:WelcomePageDocumentViewModel"
|
||||||
|
inkforge:TopLevels.Register="{CompiledBinding}">
|
||||||
|
<Grid RowDefinitions="Auto, *, Auto">
|
||||||
|
<Label Target="RecentItemsList"
|
||||||
|
Content="Open Recent"
|
||||||
|
Grid.Row="0" />
|
||||||
|
|
||||||
|
<DataGrid Name="RecentItemsList"
|
||||||
|
IsEnabled="False"
|
||||||
|
IsReadOnly="True"
|
||||||
|
Grid.Row="1">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="Name"
|
||||||
|
Width="*" />
|
||||||
|
<DataGridTextColumn Header="Last Used" />
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
|
||||||
|
<Menu Grid.Row="2">
|
||||||
|
<MenuItem Header="Create New"
|
||||||
|
Command="{CompiledBinding CreateNewCommand}" />
|
||||||
|
<MenuItem Header="Open"
|
||||||
|
IsEnabled="False" />
|
||||||
|
<MenuItem Header="Open File"
|
||||||
|
Command="{CompiledBinding OpenNewCommand}" />
|
||||||
|
</Menu>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.Views.Documents;
|
||||||
|
|
||||||
|
public partial class WelcomePageDocument : UserControl
|
||||||
|
{
|
||||||
|
public WelcomePageDocument()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,23 +2,23 @@
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:reactiveui="http://reactiveui.net"
|
xmlns:inkforge="app:InkForge"
|
||||||
xmlns:ifcvm="using:InkForge.Common.ViewModels"
|
xmlns:local="using:InkForge.Desktop.Views"
|
||||||
|
xmlns:vm="using:InkForge.Desktop.ViewModels"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignWidth="800"
|
Width="800"
|
||||||
d:DesignHeight="450"
|
Height="450"
|
||||||
x:Class="InkForge.Desktop.Views.MainWindow"
|
x:Class="InkForge.Desktop.Views.MainWindow"
|
||||||
x:DataType="ifcvm:AppViewModel"
|
x:DataType="vm:MainViewModel"
|
||||||
Title="InkForge">
|
Title="MainWindow"
|
||||||
<NativeMenu.Menu>
|
inkforge:TopLevels.Register="{CompiledBinding}">
|
||||||
<NativeMenu>
|
<NativeMenu.Menu>
|
||||||
<NativeMenuItem Header="Test" />
|
<NativeMenu />
|
||||||
</NativeMenu>
|
</NativeMenu.Menu>
|
||||||
</NativeMenu.Menu>
|
|
||||||
|
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<NativeMenuBar />
|
<NativeMenuBar DockPanel.Dock="Top" />
|
||||||
|
|
||||||
<reactiveui:ViewModelViewHost ViewModel="{CompiledBinding View}" />
|
<DockControl Layout="{CompiledBinding Layout}" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Window>
|
</Window>
|
||||||
|
|
@ -1,13 +1,24 @@
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
using InkForge.Common.ViewModels;
|
using InkForge.Desktop.ViewModels;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace InkForge.Desktop.Views;
|
namespace InkForge.Desktop.Views;
|
||||||
|
|
||||||
public partial class MainWindow : ReactiveWindow<AppViewModel>
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
DataContext = CreateViewModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MainViewModel CreateViewModel()
|
||||||
|
{
|
||||||
|
return ActivatorUtilities.CreateInstance<MainViewModel>(
|
||||||
|
Application.Current!.GetValue(App.ServiceProviderProperty)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
app/InkForge.Desktop/Views/Tools/WorkspaceTool.axaml
Normal file
32
app/InkForge.Desktop/Views/Tools/WorkspaceTool.axaml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<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:local="using:InkForge.Desktop.Views.Tools"
|
||||||
|
xmlns:workspaces="using:InkForge.Desktop.Views.Workspaces"
|
||||||
|
xmlns:vm="using:InkForge.Desktop.ViewModels.Tools"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="800"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
x:Class="InkForge.Desktop.Views.Tools.WorkspaceTool"
|
||||||
|
x:DataType="vm:WorkspaceTool"
|
||||||
|
Classes.HasWorkspace="{CompiledBinding Workspace, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="local|WorkspaceTool">
|
||||||
|
<Setter Property="Content">
|
||||||
|
<Template>
|
||||||
|
<TextBlock>No workspace selected.</TextBlock>
|
||||||
|
</Template>
|
||||||
|
</Setter>
|
||||||
|
|
||||||
|
<Style Selector="^.HasWorkspace">
|
||||||
|
<Setter Property="Content">
|
||||||
|
<Template>
|
||||||
|
<workspaces:WorkspaceView DataContext="{CompiledBinding Workspace}" />
|
||||||
|
</Template>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
</UserControl>
|
||||||
11
app/InkForge.Desktop/Views/Tools/WorkspaceTool.axaml.cs
Normal file
11
app/InkForge.Desktop/Views/Tools/WorkspaceTool.axaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.Views.Tools;
|
||||||
|
|
||||||
|
public partial class WorkspaceTool : UserControl
|
||||||
|
{
|
||||||
|
public WorkspaceTool()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/InkForge.Desktop/Views/Workspaces/WorkspaceView.axaml
Normal file
38
app/InkForge.Desktop/Views/Workspaces/WorkspaceView.axaml
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<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:vm="using:InkForge.Desktop.ViewModels.Workspaces"
|
||||||
|
xmlns:inkforge="app:InkForge"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="800"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
x:Class="InkForge.Desktop.Views.Workspaces.WorkspaceView"
|
||||||
|
x:DataType="vm:WorkspaceViewModel">
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*, Auto"
|
||||||
|
RowDefinitions="Auto, *">
|
||||||
|
<TextBlock Text="{CompiledBinding Name}"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="0" />
|
||||||
|
|
||||||
|
<StackPanel Classes="WorkspaceToolbar"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="3"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="0">
|
||||||
|
<Button AutomationProperties.Name="Add">
|
||||||
|
<inkforge:FluentSymbolIcon Symbol="document_add" />
|
||||||
|
</Button>
|
||||||
|
<Button>
|
||||||
|
<inkforge:FluentSymbolIcon Symbol="arrow_clockwise" />
|
||||||
|
</Button>
|
||||||
|
<Button>
|
||||||
|
<inkforge:FluentSymbolIcon Symbol="subtract_square_multiple" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TreeView Grid.ColumnSpan="2"
|
||||||
|
Grid.Row="1" />
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
11
app/InkForge.Desktop/Views/Workspaces/WorkspaceView.axaml.cs
Normal file
11
app/InkForge.Desktop/Views/Workspaces/WorkspaceView.axaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace InkForge.Desktop.Views.Workspaces;
|
||||||
|
|
||||||
|
public partial class WorkspaceView : UserControl
|
||||||
|
{
|
||||||
|
public WorkspaceView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,4 +8,11 @@ Performs OpenAPI calls to Sync server.
|
||||||
|
|
||||||
*Consideration*: Allow for syncing to local backend.
|
*Consideration*: Allow for syncing to local backend.
|
||||||
|
|
||||||
|
## Technical
|
||||||
|
|
||||||
|
Figure out a way to get navigation/commands relative to the window they are in.<br>
|
||||||
|
I.e. make Windows scoped, then get a shell-object in each Window, which then
|
||||||
|
consumes a Menu-service, which navigates the shell-object tree to find eligible
|
||||||
|
menu-objects to present.
|
||||||
|
|
||||||
## Research
|
## Research
|
||||||
|
|
|
||||||
3
shared/InkForge.Data/Blob.cs
Normal file
3
shared/InkForge.Data/Blob.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
namespace InkForge.Data;
|
||||||
|
|
||||||
|
public class Blob : Entity<byte[], int>;
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
using InkForge.Data.Infrastructure;
|
|
||||||
|
|
||||||
namespace InkForge.Data.Domain;
|
|
||||||
|
|
||||||
public class Note
|
|
||||||
{
|
|
||||||
public DateTimeOffset Created { get; set; }
|
|
||||||
|
|
||||||
public NoteEntity? Parent { get; set; }
|
|
||||||
|
|
||||||
public string Name { get; set; } = default!;
|
|
||||||
|
|
||||||
public DateTimeOffset Updated { get; set; }
|
|
||||||
|
|
||||||
public DateTimeOffset? Deleted { get; set; }
|
|
||||||
|
|
||||||
public Blob Content { get; set; } = default!;
|
|
||||||
}
|
|
||||||
20
shared/InkForge.Data/Entities.cs
Normal file
20
shared/InkForge.Data/Entities.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
namespace InkForge.Data
|
||||||
|
{
|
||||||
|
public abstract class ValueEntity<TEntity>
|
||||||
|
{
|
||||||
|
public TEntity Value { get; set; } = default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class Entity<TEntity, TKey>
|
||||||
|
: ValueEntity<TEntity>
|
||||||
|
{
|
||||||
|
public TKey Id { get; set; } = default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class Entity<TSelf, TEntity, TKey>
|
||||||
|
: Entity<TEntity, TKey>
|
||||||
|
where TSelf : Entity<TSelf, TEntity, TKey>
|
||||||
|
{
|
||||||
|
public TSelf? Parent { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
namespace InkForge.Data;
|
|
||||||
|
|
||||||
public class Blob
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = default!;
|
|
||||||
|
|
||||||
public byte[] Content { get; set; } = default!;
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace InkForge.Data
|
|
||||||
{
|
|
||||||
public abstract class ValueEntity<TEntity>
|
|
||||||
{
|
|
||||||
public TEntity Value { get; set; } = default!;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class Entity<TEntity, TKey>
|
|
||||||
: ValueEntity<TEntity>
|
|
||||||
where TKey : struct, INumber<TKey>
|
|
||||||
{
|
|
||||||
public TKey? Id { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class VersionedEntity<TEntity, TKey>
|
|
||||||
: ValueEntity<TEntity>
|
|
||||||
where TKey : struct, INumber<TKey>
|
|
||||||
{
|
|
||||||
public TKey Id { get; set; }
|
|
||||||
|
|
||||||
public int? Version { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
using InkForge.Data.Domain;
|
|
||||||
|
|
||||||
namespace InkForge.Data.Infrastructure
|
|
||||||
{
|
|
||||||
public class NoteEntity : Entity<Note, int>;
|
|
||||||
|
|
||||||
public class NoteVersionEntity : VersionedEntity<Note, int>;
|
|
||||||
}
|
|
||||||
3
shared/InkForge.Data/Metadata.cs
Normal file
3
shared/InkForge.Data/Metadata.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
namespace InkForge.Data;
|
||||||
|
|
||||||
|
public class MetadataEntity : Entity<string, string>;
|
||||||
|
|
@ -1,34 +1,37 @@
|
||||||
using InkForge.Data.Infrastructure;
|
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace InkForge.Data;
|
namespace InkForge.Data;
|
||||||
|
|
||||||
public class NoteDbContext(
|
public class NoteDbContext(
|
||||||
DbContextOptions<NoteDbContext> options
|
DbContextOptions options
|
||||||
) : DbContext(options)
|
) : DbContext(options)
|
||||||
{
|
{
|
||||||
public DbSet<Blob> Blobs { get; set; } = default!;
|
public DbSet<Blob> Blobs { get; set; } = default!;
|
||||||
|
|
||||||
|
public DbSet<MetadataEntity> Metadata { get; set; } = default!;
|
||||||
|
|
||||||
public DbSet<NoteEntity> Notes { get; set; } = default!;
|
public DbSet<NoteEntity> Notes { get; set; } = default!;
|
||||||
|
|
||||||
public DbSet<NoteVersionEntity> NoteVersions { get; set; } = default!;
|
public NoteDbContext(DbContextOptions<NoteDbContext> options) : this((DbContextOptions)options)
|
||||||
|
{ }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder.Entity<NoteEntity>(options =>
|
modelBuilder.Entity<MetadataEntity>(options =>
|
||||||
{
|
{
|
||||||
options.OwnsOne(m => m.Value);
|
|
||||||
|
|
||||||
options.HasKey(m => m.Id);
|
options.HasKey(m => m.Id);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<NoteVersionEntity>(options =>
|
modelBuilder.Entity<NoteEntity>(options =>
|
||||||
{
|
{
|
||||||
options.OwnsOne(m => m.Value);
|
options.HasKey(m => m.Id);
|
||||||
options.Property(m => m.Id).IsRequired();
|
|
||||||
options.HasKey(m => m.Version);
|
options.OwnsOne(m => m.Value, m =>
|
||||||
options.HasIndex(nameof(NoteVersionEntity.Id), nameof(NoteVersionEntity.Version)).IsUnique();
|
{
|
||||||
|
m.HasOne<Blob>().WithOne().HasForeignKey<Note>(m => m.ContentId).IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
options.HasOne(m => m.Parent);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
shared/InkForge.Data/Notes.cs
Normal file
17
shared/InkForge.Data/Notes.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
namespace InkForge.Data
|
||||||
|
{
|
||||||
|
public class Note
|
||||||
|
{
|
||||||
|
public DateTimeOffset Created { get; set; }
|
||||||
|
|
||||||
|
public int ContentId { get; set; }
|
||||||
|
|
||||||
|
public DateTimeOffset? Deleted { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; } = default!;
|
||||||
|
|
||||||
|
public DateTimeOffset Updated { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NoteEntity : Entity<NoteEntity, Note, int>;
|
||||||
|
}
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using InkForge.Data;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace InkForge.Sqlite.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(NoteDbContext))]
|
|
||||||
[Migration("20240207000000_Initial")]
|
|
||||||
partial class Initial
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
|
|
||||||
|
|
||||||
modelBuilder.Entity("InkForge.Data.Blob", b =>
|
|
||||||
{
|
|
||||||
b.Property<string>("Id")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<byte[]>("Content")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("BLOB");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Blobs");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("InkForge.Data.Infrastructure.NoteEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<int?>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Notes");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("InkForge.Data.Infrastructure.NoteVersionEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<int?>("Version")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("Version");
|
|
||||||
|
|
||||||
b.HasIndex("Id", "Version")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("NoteVersions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("InkForge.Data.Infrastructure.NoteEntity", b =>
|
|
||||||
{
|
|
||||||
b.OwnsOne("InkForge.Data.Domain.Note", "Value", b1 =>
|
|
||||||
{
|
|
||||||
b1.Property<int>("ParentId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b1.Property<string>("ContentId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.Property<DateTimeOffset>("Created")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.Property<DateTimeOffset?>("Deleted")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.Property<DateTimeOffset>("Updated")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.HasKey("ParentId");
|
|
||||||
|
|
||||||
b1.HasIndex("ContentId");
|
|
||||||
|
|
||||||
b1.ToTable("Notes");
|
|
||||||
|
|
||||||
b1.HasOne("InkForge.Data.Blob", "Content")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ContentId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b1.WithOwner("Parent")
|
|
||||||
.HasForeignKey("ParentId");
|
|
||||||
|
|
||||||
b1.Navigation("Content");
|
|
||||||
|
|
||||||
b1.Navigation("Parent");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.Navigation("Value")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("InkForge.Data.Infrastructure.NoteVersionEntity", b =>
|
|
||||||
{
|
|
||||||
b.OwnsOne("InkForge.Data.Domain.Note", "Value", b1 =>
|
|
||||||
{
|
|
||||||
b1.Property<int>("NoteVersionEntityVersion")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b1.Property<string>("ContentId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.Property<DateTimeOffset>("Created")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.Property<DateTimeOffset?>("Deleted")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.Property<int?>("ParentId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b1.Property<DateTimeOffset>("Updated")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.HasKey("NoteVersionEntityVersion");
|
|
||||||
|
|
||||||
b1.HasIndex("ContentId");
|
|
||||||
|
|
||||||
b1.HasIndex("ParentId");
|
|
||||||
|
|
||||||
b1.ToTable("NoteVersions");
|
|
||||||
|
|
||||||
b1.HasOne("InkForge.Data.Blob", "Content")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ContentId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b1.WithOwner()
|
|
||||||
.HasForeignKey("NoteVersionEntityVersion");
|
|
||||||
|
|
||||||
b1.HasOne("InkForge.Data.Infrastructure.NoteEntity", "Parent")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentId");
|
|
||||||
|
|
||||||
b1.Navigation("Content");
|
|
||||||
|
|
||||||
b1.Navigation("Parent");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.Navigation("Value")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
120
shared/migrations/InkForge.Sqlite/Migrations/20240401_Initial.Designer.cs
generated
Normal file
120
shared/migrations/InkForge.Sqlite/Migrations/20240401_Initial.Designer.cs
generated
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using InkForge.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace InkForge.Sqlite.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(NoteDbContext))]
|
||||||
|
[Migration("20240401_Initial")]
|
||||||
|
partial class _20240401_Initial
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.3");
|
||||||
|
|
||||||
|
modelBuilder.Entity("InkForge.Data.Blob", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Blobs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("InkForge.Data.MetadataEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("InkForge.Data.NoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("ParentId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
|
b.ToTable("Notes");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("InkForge.Data.NoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("InkForge.Data.NoteEntity", "Parent")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ParentId");
|
||||||
|
|
||||||
|
b.OwnsOne("InkForge.Data.Note", "Value", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("NoteEntityId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<int>("ContentId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b1.Property<DateTimeOffset>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.Property<DateTimeOffset?>("Deleted")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.Property<DateTimeOffset>("Updated")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b1.HasKey("NoteEntityId");
|
||||||
|
|
||||||
|
b1.HasIndex("ContentId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b1.ToTable("Notes");
|
||||||
|
|
||||||
|
b1.HasOne("InkForge.Data.Blob", null)
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey("InkForge.Data.NoteEntity.Value#InkForge.Data.Note", "ContentId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("NoteEntityId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
|
||||||
|
b.Navigation("Value")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
namespace InkForge.Sqlite.Migrations
|
namespace InkForge.Sqlite.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class Initial : Migration
|
public partial class _20240401_Initial : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
|
@ -15,25 +15,38 @@ namespace InkForge.Sqlite.Migrations
|
||||||
name: "Blobs",
|
name: "Blobs",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
Content = table.Column<byte[]>(type: "BLOB", nullable: false)
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Value = table.Column<byte[]>(type: "BLOB", nullable: false)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
table.PrimaryKey("PK_Blobs", x => x.Id);
|
table.PrimaryKey("PK_Blobs", x => x.Id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Metadata",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Value = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Metadata", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
name: "Notes",
|
name: "Notes",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
Id = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
|
||||||
Value_Created = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
Value_Created = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||||
|
Value_ContentId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Value_Deleted = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
|
||||||
Value_Name = table.Column<string>(type: "TEXT", nullable: false),
|
Value_Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
Value_Updated = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
Value_Updated = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
||||||
Value_Deleted = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
|
ParentId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||||
Value_ContentId = table.Column<string>(type: "TEXT", nullable: false)
|
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
|
|
@ -44,65 +57,30 @@ namespace InkForge.Sqlite.Migrations
|
||||||
principalTable: "Blobs",
|
principalTable: "Blobs",
|
||||||
principalColumn: "Id",
|
principalColumn: "Id",
|
||||||
onDelete: ReferentialAction.Cascade);
|
onDelete: ReferentialAction.Cascade);
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "NoteVersions",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Version = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
|
||||||
Value_Created = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
|
||||||
Value_ParentId = table.Column<int>(type: "INTEGER", nullable: true),
|
|
||||||
Value_Name = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Value_Updated = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
|
|
||||||
Value_Deleted = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
|
|
||||||
Value_ContentId = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_NoteVersions", x => x.Version);
|
|
||||||
table.ForeignKey(
|
table.ForeignKey(
|
||||||
name: "FK_NoteVersions_Blobs_Value_ContentId",
|
name: "FK_Notes_Notes_ParentId",
|
||||||
column: x => x.Value_ContentId,
|
column: x => x.ParentId,
|
||||||
principalTable: "Blobs",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_NoteVersions_Notes_Value_ParentId",
|
|
||||||
column: x => x.Value_ParentId,
|
|
||||||
principalTable: "Notes",
|
principalTable: "Notes",
|
||||||
principalColumn: "Id");
|
principalColumn: "Id");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Notes_ParentId",
|
||||||
|
table: "Notes",
|
||||||
|
column: "ParentId");
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_Notes_Value_ContentId",
|
name: "IX_Notes_Value_ContentId",
|
||||||
table: "Notes",
|
table: "Notes",
|
||||||
column: "Value_ContentId");
|
column: "Value_ContentId",
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_NoteVersions_Id_Version",
|
|
||||||
table: "NoteVersions",
|
|
||||||
columns: new[] { "Id", "Version" },
|
|
||||||
unique: true);
|
unique: true);
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_NoteVersions_Value_ContentId",
|
|
||||||
table: "NoteVersions",
|
|
||||||
column: "Value_ContentId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_NoteVersions_Value_ParentId",
|
|
||||||
table: "NoteVersions",
|
|
||||||
column: "Value_ParentId");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "NoteVersions");
|
name: "Metadata");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "Notes");
|
name: "Notes");
|
||||||
|
|
@ -15,14 +15,15 @@ namespace InkForge.Sqlite.Migrations
|
||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.3");
|
||||||
|
|
||||||
modelBuilder.Entity("InkForge.Data.Blob", b =>
|
modelBuilder.Entity("InkForge.Data.Blob", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<int>("Id")
|
||||||
.HasColumnType("TEXT");
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<byte[]>("Content")
|
b.Property<byte[]>("Value")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("BLOB");
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
|
@ -31,44 +32,49 @@ namespace InkForge.Sqlite.Migrations
|
||||||
b.ToTable("Blobs");
|
b.ToTable("Blobs");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("InkForge.Data.Infrastructure.NoteEntity", b =>
|
modelBuilder.Entity("InkForge.Data.MetadataEntity", b =>
|
||||||
{
|
{
|
||||||
b.Property<int?>("Id")
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("InkForge.Data.NoteEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("ParentId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ParentId");
|
||||||
|
|
||||||
b.ToTable("Notes");
|
b.ToTable("Notes");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("InkForge.Data.Infrastructure.NoteVersionEntity", b =>
|
modelBuilder.Entity("InkForge.Data.NoteEntity", b =>
|
||||||
{
|
{
|
||||||
b.Property<int?>("Version")
|
b.HasOne("InkForge.Data.NoteEntity", "Parent")
|
||||||
.ValueGeneratedOnAdd()
|
.WithMany()
|
||||||
.HasColumnType("INTEGER");
|
.HasForeignKey("ParentId");
|
||||||
|
|
||||||
b.Property<int>("Id")
|
b.OwnsOne("InkForge.Data.Note", "Value", b1 =>
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("Version");
|
|
||||||
|
|
||||||
b.HasIndex("Id", "Version")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("NoteVersions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("InkForge.Data.Infrastructure.NoteEntity", b =>
|
|
||||||
{
|
|
||||||
b.OwnsOne("InkForge.Data.Domain.Note", "Value", b1 =>
|
|
||||||
{
|
{
|
||||||
b1.Property<int>("ParentId")
|
b1.Property<int>("NoteEntityId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b1.Property<string>("ContentId")
|
b1.Property<int>("ContentId")
|
||||||
.IsRequired()
|
.HasColumnType("INTEGER");
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.Property<DateTimeOffset>("Created")
|
b1.Property<DateTimeOffset>("Created")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
@ -83,83 +89,25 @@ namespace InkForge.Sqlite.Migrations
|
||||||
b1.Property<DateTimeOffset>("Updated")
|
b1.Property<DateTimeOffset>("Updated")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b1.HasKey("ParentId");
|
b1.HasKey("NoteEntityId");
|
||||||
|
|
||||||
b1.HasIndex("ContentId");
|
b1.HasIndex("ContentId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b1.ToTable("Notes");
|
b1.ToTable("Notes");
|
||||||
|
|
||||||
b1.HasOne("InkForge.Data.Blob", "Content")
|
b1.HasOne("InkForge.Data.Blob", null)
|
||||||
.WithMany()
|
.WithOne()
|
||||||
.HasForeignKey("ContentId")
|
.HasForeignKey("InkForge.Data.NoteEntity.Value#InkForge.Data.Note", "ContentId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b1.WithOwner("Parent")
|
|
||||||
.HasForeignKey("ParentId");
|
|
||||||
|
|
||||||
b1.Navigation("Content");
|
|
||||||
|
|
||||||
b1.Navigation("Parent");
|
|
||||||
});
|
|
||||||
|
|
||||||
b.Navigation("Value")
|
|
||||||
.IsRequired();
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("InkForge.Data.Infrastructure.NoteVersionEntity", b =>
|
|
||||||
{
|
|
||||||
b.OwnsOne("InkForge.Data.Domain.Note", "Value", b1 =>
|
|
||||||
{
|
|
||||||
b1.Property<int>("NoteVersionEntityVersion")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b1.Property<string>("ContentId")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.Property<DateTimeOffset>("Created")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.Property<DateTimeOffset?>("Deleted")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.Property<int?>("ParentId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b1.Property<DateTimeOffset>("Updated")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b1.HasKey("NoteVersionEntityVersion");
|
|
||||||
|
|
||||||
b1.HasIndex("ContentId");
|
|
||||||
|
|
||||||
b1.HasIndex("ParentId");
|
|
||||||
|
|
||||||
b1.ToTable("NoteVersions");
|
|
||||||
|
|
||||||
b1.HasOne("InkForge.Data.Blob", "Content")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ContentId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("NoteVersionEntityVersion");
|
.HasForeignKey("NoteEntityId");
|
||||||
|
|
||||||
b1.HasOne("InkForge.Data.Infrastructure.NoteEntity", "Parent")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("ParentId");
|
|
||||||
|
|
||||||
b1.Navigation("Content");
|
|
||||||
|
|
||||||
b1.Navigation("Parent");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
b.Navigation("Parent");
|
||||||
|
|
||||||
b.Navigation("Value")
|
b.Navigation("Value")
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue