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,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "8.0.1",
|
||||
"version": "8.0.3",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
},
|
||||
"dotnet-aspnet-codegenerator": {
|
||||
"version": "8.0.0",
|
||||
"version": "8.0.1",
|
||||
"commands": [
|
||||
"dotnet-aspnet-codegenerator"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,31 +2,35 @@
|
|||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
|
||||
<AvaloniaVersion>11.0.9</AvaloniaVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia" Version="$(AvaloniaVersion)" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="$(AvaloniaVersion)" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
|
||||
<PackageVersion Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
|
||||
<PackageVersion Include="Dock.Avalonia" Version="11.0.0.5" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Abstractions" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1" />
|
||||
<PackageVersion Include="Avalonia" Version="11.0.10" />
|
||||
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.10" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.0.10" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.0.10" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.0.10" />
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.2.2" />
|
||||
<PackageVersion Include="Dock.Avalonia" Version="11.0.0.7" />
|
||||
<PackageVersion Include="Dock.Model.Mvvm" Version="11.0.0.7" />
|
||||
<PackageVersion Include="DynamicData" Version="8.4.1" />
|
||||
<PackageVersion Include="FluentAvaloniaUI" Version="2.0.5" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.3" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />
|
||||
<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.FileProviders.Embedded" Version="8.0.3" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.0" />
|
||||
<PackageVersion Include="ReactiveUI" Version="19.5.41" />
|
||||
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.1" />
|
||||
<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="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageVersion Include="System.IO.Hashing" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
using InkForge.Api.Data.Infrastructure;
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
@ -12,25 +10,15 @@ public class ApiDbcontext(
|
|||
{
|
||||
public DbSet<WorkspaceEntity> Workspaces { get; set; } = default!;
|
||||
|
||||
public DbSet<WorkspaceVersionEntity> WorkspaceVersions { get; set; } = default!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
builder.Entity<WorkspaceEntity>(options =>
|
||||
{
|
||||
options.OwnsOne(m => m.Value);
|
||||
|
||||
options.HasKey(m => m.Id);
|
||||
});
|
||||
|
||||
builder.Entity<WorkspaceVersionEntity>(options =>
|
||||
{
|
||||
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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkForge.Migrations", "design\InkForge.Migrations\InkForge.Migrations.csproj", "{8DF3397E-2717-49F0-9592-82ABE9327A73}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkForge.Common", "app\InkForge.Common\InkForge.Common.csproj", "{DCE2DCD6-D15C-4F0D-8D7F-22FF82F62F6F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{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}
|
||||
{F8A7563F-2647-4623-88E7-470D20F25E93} = {A9F8087F-F148-47A5-94AE-F7B6E1D33096}
|
||||
{8DF3397E-2717-49F0-9592-82ABE9327A73} = {C7868400-84D7-45C5-B594-C30777EE5191}
|
||||
{DCE2DCD6-D15C-4F0D-8D7F-22FF82F62F6F} = {84CBD204-9573-4472-9334-68FB360BD6ED}
|
||||
EndGlobalSection
|
||||
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>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>InkForge</AssemblyName>
|
||||
<RootNamespace>InkForge.Desktop</RootNamespace>
|
||||
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
||||
<!-- <TrimMode>partial</TrimMode> -->
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.AvaloniaEdit" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" />
|
||||
<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.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="System.IO.Hashing" />
|
||||
</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>
|
||||
<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.ReactiveUI;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Threading;
|
||||
|
||||
using InkForge.Common;
|
||||
using InkForge.Common.ViewModels;
|
||||
using InkForge.Data;
|
||||
using InkForge.Desktop;
|
||||
using InkForge.Desktop.Views;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using ReactiveUI;
|
||||
|
||||
using Splat;
|
||||
using Splat.Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
static class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
=> BuildAvaloniaApp()
|
||||
.UseMicrosoftDependencyInjection()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
.UseMicrosoftDependencyInjection(out var configuration)
|
||||
.StartWithClassicDesktopLifetime(args, configuration.WithMicrosoftDependencyInjection);
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.UseReactiveUI()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
|
||||
private static void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
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)
|
||||
private static void SetupApp(this IServiceCollection services, AppBuilder appBuilder)
|
||||
{
|
||||
var dispatcher = Dispatcher.UIThread;
|
||||
|
||||
var app = appBuilder.Instance!;
|
||||
services
|
||||
.AddSingleton(app)
|
||||
|
|
@ -53,16 +33,56 @@ static class Program
|
|||
.AddSingleton(dispatcher);
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
serviceProvider.UseMicrosoftDependencyResolver();
|
||||
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 = [];
|
||||
ConfigureServices(services);
|
||||
builder.AfterSetup(services.OnSetup);
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
App.Configure(services, configuration);
|
||||
|
||||
builder.AfterSetup(services.SetupApp);
|
||||
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;
|
||||
|
||||
namespace InkForge.Common.Properties;
|
||||
namespace InkForge.Desktop.Properties;
|
||||
|
||||
[JsonSerializable(typeof(ApplicationSettings))]
|
||||
[JsonSerializable(typeof(IDictionary<string, object>))]
|
||||
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
|
||||
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: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"
|
||||
xmlns:inkforge="app:InkForge"
|
||||
xmlns:local="using:InkForge.Desktop.Views"
|
||||
xmlns:vm="using:InkForge.Desktop.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="800"
|
||||
d:DesignHeight="450"
|
||||
Width="800"
|
||||
Height="450"
|
||||
x:Class="InkForge.Desktop.Views.MainWindow"
|
||||
x:DataType="ifcvm:AppViewModel"
|
||||
Title="InkForge">
|
||||
<NativeMenu.Menu>
|
||||
<NativeMenu>
|
||||
<NativeMenuItem Header="Test" />
|
||||
</NativeMenu>
|
||||
</NativeMenu.Menu>
|
||||
x:DataType="vm:MainViewModel"
|
||||
Title="MainWindow"
|
||||
inkforge:TopLevels.Register="{CompiledBinding}">
|
||||
<NativeMenu.Menu>
|
||||
<NativeMenu />
|
||||
</NativeMenu.Menu>
|
||||
|
||||
<DockPanel>
|
||||
<NativeMenuBar />
|
||||
<DockPanel>
|
||||
<NativeMenuBar DockPanel.Dock="Top" />
|
||||
|
||||
<reactiveui:ViewModelViewHost ViewModel="{CompiledBinding View}" />
|
||||
</DockPanel>
|
||||
<DockControl Layout="{CompiledBinding Layout}" />
|
||||
</DockPanel>
|
||||
</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;
|
||||
|
||||
public partial class MainWindow : ReactiveWindow<AppViewModel>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
|
|
|||
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;
|
||||
|
||||
namespace InkForge.Data;
|
||||
|
||||
public class NoteDbContext(
|
||||
DbContextOptions<NoteDbContext> options
|
||||
DbContextOptions options
|
||||
) : DbContext(options)
|
||||
{
|
||||
public DbSet<Blob> Blobs { get; set; } = default!;
|
||||
|
||||
public DbSet<MetadataEntity> Metadata { 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)
|
||||
{
|
||||
modelBuilder.Entity<NoteEntity>(options =>
|
||||
modelBuilder.Entity<MetadataEntity>(options =>
|
||||
{
|
||||
options.OwnsOne(m => m.Value);
|
||||
|
||||
options.HasKey(m => m.Id);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<NoteVersionEntity>(options =>
|
||||
modelBuilder.Entity<NoteEntity>(options =>
|
||||
{
|
||||
options.OwnsOne(m => m.Value);
|
||||
options.Property(m => m.Id).IsRequired();
|
||||
options.HasKey(m => m.Version);
|
||||
options.HasIndex(nameof(NoteVersionEntity.Id), nameof(NoteVersionEntity.Version)).IsUnique();
|
||||
options.HasKey(m => m.Id);
|
||||
|
||||
options.OwnsOne(m => m.Value, m =>
|
||||
{
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
public partial class _20240401_Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
|
|
@ -15,25 +15,38 @@ namespace InkForge.Sqlite.Migrations
|
|||
name: "Blobs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Content = table.Column<byte[]>(type: "BLOB", nullable: false)
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Value = table.Column<byte[]>(type: "BLOB", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
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(
|
||||
name: "Notes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Id = table.Column<int>(type: "INTEGER", 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_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)
|
||||
ParentId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
|
@ -44,65 +57,30 @@ namespace InkForge.Sqlite.Migrations
|
|||
principalTable: "Blobs",
|
||||
principalColumn: "Id",
|
||||
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(
|
||||
name: "FK_NoteVersions_Blobs_Value_ContentId",
|
||||
column: x => x.Value_ContentId,
|
||||
principalTable: "Blobs",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_NoteVersions_Notes_Value_ParentId",
|
||||
column: x => x.Value_ParentId,
|
||||
name: "FK_Notes_Notes_ParentId",
|
||||
column: x => x.ParentId,
|
||||
principalTable: "Notes",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notes_ParentId",
|
||||
table: "Notes",
|
||||
column: "ParentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notes_Value_ContentId",
|
||||
table: "Notes",
|
||||
column: "Value_ContentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NoteVersions_Id_Version",
|
||||
table: "NoteVersions",
|
||||
columns: new[] { "Id", "Version" },
|
||||
column: "Value_ContentId",
|
||||
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 />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "NoteVersions");
|
||||
name: "Metadata");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Notes");
|
||||
|
|
@ -15,14 +15,15 @@ namespace InkForge.Sqlite.Migrations
|
|||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "8.0.3");
|
||||
|
||||
modelBuilder.Entity("InkForge.Data.Blob", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Content")
|
||||
b.Property<byte[]>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
|
|
@ -31,44 +32,49 @@ namespace InkForge.Sqlite.Migrations
|
|||
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()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ParentId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Notes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("InkForge.Data.Infrastructure.NoteVersionEntity", b =>
|
||||
modelBuilder.Entity("InkForge.Data.NoteEntity", b =>
|
||||
{
|
||||
b.Property<int?>("Version")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
b.HasOne("InkForge.Data.NoteEntity", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
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 =>
|
||||
b.OwnsOne("InkForge.Data.Note", "Value", b1 =>
|
||||
{
|
||||
b1.Property<int>("ParentId")
|
||||
b1.Property<int>("NoteEntityId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("ContentId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
b1.Property<int>("ContentId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
|
@ -83,83 +89,25 @@ namespace InkForge.Sqlite.Migrations
|
|||
b1.Property<DateTimeOffset>("Updated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("ParentId");
|
||||
b1.HasKey("NoteEntityId");
|
||||
|
||||
b1.HasIndex("ContentId");
|
||||
b1.HasIndex("ContentId")
|
||||
.IsUnique();
|
||||
|
||||
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")
|
||||
b1.HasOne("InkForge.Data.Blob", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("InkForge.Data.NoteEntity.Value#InkForge.Data.Note", "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");
|
||||
.HasForeignKey("NoteEntityId");
|
||||
});
|
||||
|
||||
b.Navigation("Parent");
|
||||
|
||||
b.Navigation("Value")
|
||||
.IsRequired();
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue