Replace ReactiveUI

This commit is contained in:
Jöran Malek 2024-05-02 21:44:13 +02:00
parent 43b4d50e43
commit 5584ab4ec8
41 changed files with 472 additions and 1013 deletions

View file

@ -5,13 +5,15 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia" Version="11.0.10" /> <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.Controls.DataGrid" Version="11.0.10" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.10" /> <PackageVersion Include="Avalonia.Desktop" Version="11.0.10" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.0.10" /> <PackageVersion Include="Avalonia.Fonts.Inter" Version="11.0.10" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.0.10" />
<PackageVersion Include="Avalonia.Themes.Fluent" 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.Avalonia" Version="11.0.0.7" />
<PackageVersion Include="Dock.Model.ReactiveUI" 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="FluentAvaloniaUI" Version="2.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.3" /> <PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.3" /> <PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.3" />
@ -26,6 +28,7 @@
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.1" /> <PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.1" />
<PackageVersion Include="SmartFormat" Version="3.3.2" /> <PackageVersion Include="SmartFormat" Version="3.3.2" />
<PackageVersion Include="Splat" Version="15.0.1" />
<PackageVersion Include="Splat.Microsoft.Extensions.DependencyInjection" Version="14.8.12" /> <PackageVersion Include="Splat.Microsoft.Extensions.DependencyInjection" Version="14.8.12" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="System.IO.Hashing" Version="8.0.0" /> <PackageVersion Include="System.IO.Hashing" Version="8.0.0" />

View file

@ -5,6 +5,7 @@
<Application.Styles> <Application.Styles>
<FluentTheme /> <FluentTheme />
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" /> <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
<DockFluentTheme /> <DockFluentTheme />
</Application.Styles> </Application.Styles>

View file

@ -8,8 +8,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using ReactiveUI;
using Splat; using Splat;
using Splat.Microsoft.Extensions.DependencyInjection; using Splat.Microsoft.Extensions.DependencyInjection;
@ -47,7 +45,6 @@ public partial class App : Application
services.UseMicrosoftDependencyResolver(); services.UseMicrosoftDependencyResolver();
Locator.CurrentMutable.InitializeSplat(); Locator.CurrentMutable.InitializeSplat();
Locator.CurrentMutable.InitializeReactiveUI();
services.AddInkForge(); services.AddInkForge();
} }

View file

@ -1,3 +1,4 @@
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
@ -7,8 +8,6 @@ using InkForge.Desktop.ViewModels.Workspaces;
using InkForge.Desktop.Views.Documents; using InkForge.Desktop.Views.Documents;
using InkForge.Desktop.Views.Workspaces; using InkForge.Desktop.Views.Workspaces;
using ReactiveUI;
namespace InkForge.Desktop; namespace InkForge.Desktop;
public class AppViewLocator : IDataTemplate public class AppViewLocator : IDataTemplate
@ -19,6 +18,7 @@ public class AppViewLocator : IDataTemplate
return param switch return param switch
#pragma warning restore CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). #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), ViewModels.Tools.WorkspaceTool viewModel => _(new Views.Tools.WorkspaceTool(), viewModel),
WelcomePageDocumentViewModel viewModel => _(new WelcomePageDocument(), viewModel), WelcomePageDocumentViewModel viewModel => _(new WelcomePageDocument(), viewModel),
WorkspaceViewModel viewModel => _(new WorkspaceView(), viewModel), WorkspaceViewModel viewModel => _(new WorkspaceView(), viewModel),
@ -26,9 +26,9 @@ public class AppViewLocator : IDataTemplate
static TView _<TView, TViewModel>(TView view, TViewModel viewModel) static TView _<TView, TViewModel>(TView view, TViewModel viewModel)
where TViewModel : class where TViewModel : class
where TView : IViewFor<TViewModel> where TView : StyledElement
{ {
view.ViewModel = viewModel; view.DataContext = viewModel;
return view; return view;
} }
} }
@ -36,6 +36,7 @@ public class AppViewLocator : IDataTemplate
public bool Match(object? data) public bool Match(object? data)
{ {
return data is return data is
NoteEditDocumentViewModel or
RecentItemViewModel or RecentItemViewModel or
ViewModels.Tools.WorkspaceTool or ViewModels.Tools.WorkspaceTool or
WelcomePageDocumentViewModel or WelcomePageDocumentViewModel or

View file

@ -12,18 +12,21 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.AvaloniaEdit" />
<PackageReference Include="Avalonia.Controls.DataGrid" /> <PackageReference Include="Avalonia.Controls.DataGrid" />
<PackageReference Include="Avalonia.Desktop" /> <PackageReference Include="Avalonia.Desktop" />
<PackageReference Include="Avalonia.Fonts.Inter" /> <PackageReference Include="Avalonia.Fonts.Inter" />
<PackageReference Include="Avalonia.ReactiveUI" />
<PackageReference Include="Avalonia.Themes.Fluent" /> <PackageReference Include="Avalonia.Themes.Fluent" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="Dock.Avalonia" /> <PackageReference Include="Dock.Avalonia" />
<PackageReference Include="Dock.Model.ReactiveUI" /> <PackageReference Include="Dock.Model.Mvvm" />
<PackageReference Include="DynamicData" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" /> <PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" /> <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />
<PackageReference Include="Microsoft.Extensions.Http" /> <PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="SmartFormat" /> <PackageReference Include="SmartFormat" />
<PackageReference Include="Splat" />
<PackageReference Include="Splat.Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Splat.Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="System.IO.Hashing" /> <PackageReference Include="System.IO.Hashing" />
</ItemGroup> </ItemGroup>

View file

@ -1,84 +1,67 @@
using Dock.Model.ReactiveUI; using Avalonia;
using Dock.Model.Controls; using Dock.Model.Controls;
using Dock.Model.Core; using Dock.Model.Core;
using Dock.Model.ReactiveUI.Controls; using Dock.Model.Mvvm;
using Dock.Avalonia.Controls; 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; using Microsoft.Extensions.DependencyInjection;
using Avalonia;
using InkForge.Desktop.ViewModels;
namespace InkForge.Desktop; namespace InkForge.Desktop;
public class InkForgeFactory : Factory public class InkForgeFactory : Factory
{ {
private readonly IDocumentDock _documentDock; private readonly IDock _mainDock;
private readonly IRootDock _rootDock; private readonly IRootDock _rootDock;
private readonly ViewModels.Tools.WorkspaceTool _workspaceTool; private readonly WelcomePageDocumentViewModel _welcomePage;
private readonly WorkspaceTool _workspaceTool;
public InkForgeFactory() public InkForgeFactory(WorkspaceManager workspace)
{ {
_rootDock = new RootDock _rootDock = CreateRootDock();
{ _mainDock = CreateDockDock();
IsCollapsable = false, _mainDock.IsCollapsable = false;
}; _mainDock.CanClose = false;
_documentDock = new DocumentDock
{
Id = "Documents",
Title = "Documents",
CanCreateDocument = false,
IsCollapsable = false,
Proportion = double.NaN,
};
_welcomePage = CreateWelcomePageDocumentViewModel();
_workspaceTool = CreateWorkspaceTool(); _workspaceTool = CreateWorkspaceTool();
workspace.WhenValueChanged(m => m.Workspace).Subscribe(OnWorkspaceChanged);
} }
public override IRootDock CreateLayout() public override IRootDock CreateLayout()
{ {
ProportionalDock workspaceLayout = new() ToolDock toolDock = new()
{ {
Proportion = 0.3, Alignment = Alignment.Left,
Proportion = 0.25,
VisibleDockables = [_workspaceTool], VisibleDockables = [_workspaceTool],
}; };
ProportionalDock windowLayoutContent = new() ProportionalDock windowLayoutContent = new()
{ {
Orientation = Orientation.Horizontal, Orientation = Orientation.Horizontal,
IsCollapsable = false, VisibleDockables = [toolDock, new ProportionalDockSplitter(), _mainDock]
VisibleDockables = [workspaceLayout, new ProportionalDockSplitter(), _documentDock]
}; };
RootDock windowLayout = new() _rootDock.VisibleDockables = [windowLayoutContent];
{ _rootDock.DefaultDockable = windowLayoutContent;
Title = "Default",
IsCollapsable = false,
VisibleDockables = [windowLayoutContent],
ActiveDockable = windowLayoutContent,
};
_rootDock.VisibleDockables = [windowLayout];
_rootDock.ActiveDockable = windowLayout;
_rootDock.DefaultDockable = windowLayout;
return _rootDock; return _rootDock;
} }
public override void InitLayout(IDockable layout) private static WelcomePageDocumentViewModel CreateWelcomePageDocumentViewModel()
{ {
DockableLocator = new Dictionary<string, Func<IDockable?>> return ActivatorUtilities.CreateInstance<WelcomePageDocumentViewModel>(
{ Application.Current!.GetValue(App.ServiceProviderProperty)
["Root"] = () => _rootDock, );
["Documents"] = () => _documentDock,
["Workspace"] = () => _workspaceTool,
};
HostWindowLocator = new Dictionary<string, Func<IHostWindow?>>
{
[nameof(IDockWindow)] = () => new HostWindow()
};
base.InitLayout(layout);
} }
private static ViewModels.Tools.WorkspaceTool CreateWorkspaceTool() private static ViewModels.Tools.WorkspaceTool CreateWorkspaceTool()
@ -87,4 +70,16 @@ public class InkForgeFactory : Factory
Application.Current!.GetValue(App.ServiceProviderProperty) 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);
}
} }

View file

@ -1,48 +1,38 @@
using Avalonia; using AvaloniaEdit.Document;
using Dock.Model.Core; using CommunityToolkit.Mvvm.Input;
using Dock.Model.Controls;
using InkForge.Desktop.Models; using InkForge.Desktop.Models;
using InkForge.Desktop.ViewModels.Documents; using InkForge.Desktop.ViewModels.Documents;
using Microsoft.Extensions.DependencyInjection;
using ReactiveUI;
namespace InkForge.Desktop.Managers; namespace InkForge.Desktop.Managers;
public class DocumentManager public partial class DocumentManager
{ {
private readonly IDock _documents;
private readonly InkForgeFactory _factory; private readonly InkForgeFactory _factory;
private readonly WelcomePageDocumentViewModel _welcomePage;
private readonly WorkspaceManager _workspaceManager;
public DocumentManager(WorkspaceManager workspaceManager, InkForgeFactory factory) public IDocumentDock Dock { get; }
public DocumentManager(NoteStore noteStore, InkForgeFactory factory)
{ {
_workspaceManager = workspaceManager;
_factory = factory; _factory = factory;
_documents = factory.GetDockable<IDock>("Documents")!; Dock = factory.CreateDocumentDock();
_welcomePage = CreateWelcomePageDocumentViewModel(); Dock.IsCollapsable = false;
workspaceManager.WhenAnyValue(v => v.Workspace).Subscribe(OnWorkspaceChanged); Dock.CanCreateDocument = true;
Dock.CreateDocument = CreateDocumentCommand;
} }
private void OnWorkspaceChanged(Workspace? workspace) [RelayCommand]
private void OnCreateDocument()
{ {
if (workspace is null) NoteEditDocumentViewModel editViewModel = new(new()
{ {
_factory.AddDockable(_documents, _welcomePage); Name = "Untitled Note",
} }, new());
else _factory.AddDockable(Dock, editViewModel);
{ _factory.SetActiveDockable(editViewModel);
_factory.RemoveDockable(_welcomePage, false); _factory.SetFocusedDockable(Dock, editViewModel);
}
}
private static WelcomePageDocumentViewModel CreateWelcomePageDocumentViewModel()
{
return ActivatorUtilities.CreateInstance<WelcomePageDocumentViewModel>(
Application.Current!.GetValue(App.ServiceProviderProperty)
);
} }
} }

View file

@ -1,3 +1,7 @@
using Avalonia;
using CommunityToolkit.Mvvm.ComponentModel;
using InkForge.Data; using InkForge.Data;
using InkForge.Desktop.Data.Options; using InkForge.Desktop.Data.Options;
using InkForge.Desktop.Models; using InkForge.Desktop.Models;
@ -5,24 +9,16 @@ using InkForge.Desktop.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using ReactiveUI;
namespace InkForge.Desktop.Managers; namespace InkForge.Desktop.Managers;
public class WorkspaceManager(IServiceProvider serviceProvider) : ReactiveObject public partial class WorkspaceManager(IServiceProvider serviceProvider) : ObservableObject
{ {
private readonly IServiceProvider _serviceProvider = serviceProvider; private readonly IServiceProvider _serviceProvider = serviceProvider;
private Workspace? _workspace; [ObservableProperty] private Workspace? _workspace;
public Workspace? Workspace
{
get => _workspace;
private set => this.RaiseAndSetIfChanged(ref _workspace, value);
}
public ValueTask CloseWorkspace() public ValueTask CloseWorkspace()
{ {
_workspace?.Dispose(); Workspace?.Dispose();
Workspace = null; Workspace = null;
return ValueTask.CompletedTask; return ValueTask.CompletedTask;
} }
@ -66,7 +62,7 @@ public class WorkspaceManager(IServiceProvider serviceProvider) : ReactiveObject
var db = dbContext.Database; var db = dbContext.Database;
if ((await db.GetPendingMigrationsAsync().ConfigureAwait(false)).Any()) if ((await db.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
{ {
if (file.Exists) if ((await db.GetAppliedMigrationsAsync().ConfigureAwait(false)).Any())
{ {
file.CopyTo(Path.ChangeExtension(file.FullName, $"{DateTime.Now:s}{file.Extension}")); file.CopyTo(Path.ChangeExtension(file.FullName, $"{DateTime.Now:s}{file.Extension}"));
} }
@ -74,6 +70,8 @@ public class WorkspaceManager(IServiceProvider serviceProvider) : ReactiveObject
await db.MigrateAsync().ConfigureAwait(false); await db.MigrateAsync().ConfigureAwait(false);
} }
await serviceProvider.GetRequiredService<NoteStore>().Load().ConfigureAwait(false);
scope = null; scope = null;
} }
catch (Exception) catch (Exception)

View file

@ -23,7 +23,6 @@ public static class InkForgeServiceCollections
// Singletons // Singletons
// - Concrete // - Concrete
services.AddSingleton<DocumentManager>();
services.AddSingleton<InkForgeFactory>(); services.AddSingleton<InkForgeFactory>();
services.AddSingleton<WorkspaceManager>(); services.AddSingleton<WorkspaceManager>();
@ -33,6 +32,7 @@ public static class InkForgeServiceCollections
// Scoped // Scoped
// - Concrete // - Concrete
services.AddScoped<DocumentManager>();
services.AddScoped<LocalWorkspaceOptions>(); services.AddScoped<LocalWorkspaceOptions>();
services.AddScoped<NoteStore>(); services.AddScoped<NoteStore>();

View file

@ -1,61 +1,12 @@
using System.Reactive.Linq; using CommunityToolkit.Mvvm.ComponentModel;
using System.Reactive.Subjects;
using ReactiveUI;
namespace InkForge.Desktop.Models; namespace InkForge.Desktop.Models;
public class Note : ReactiveObject public partial class Note : ObservableObject
{ {
private readonly ObservableAsPropertyHelper<Note?> _parent; [ObservableProperty] private DateTimeOffset _createdTime;
private readonly BehaviorSubject<int?> _parentId = new(default); [ObservableProperty] private int _id;
private DateTimeOffset _createdTime; [ObservableProperty] private string _name = default!;
private int _id; [ObservableProperty] private int? _parentId;
private string _name = default!; [ObservableProperty] private DateTimeOffset _updatedTime;
private DateTimeOffset _updatedTime;
public DateTimeOffset CreatedTime
{
get => _createdTime;
set => this.RaiseAndSetIfChanged(ref _createdTime, value);
}
public int Id
{
get => _id;
set => this.RaiseAndSetIfChanged(ref _id, value);
}
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
public DateTimeOffset UpdatedTime
{
get => _updatedTime;
set => this.RaiseAndSetIfChanged(ref _updatedTime, value);
}
public Note? Parent
{
get => _parent.Value;
set => ParentId = value?.Id;
}
public int? ParentId
{
get => _parentId.Value;
set => _parentId.OnNext(value);
}
public Note(NoteStore noteStore)
{
_parent = _parentId.Select(id => id switch
{
{ } => noteStore.Watch((int)id),
_ => Observable.Empty<Note?>(),
}).Switch(null).ToProperty(this, nameof(Parent), deferSubscription: true);
}
} }

View file

@ -1,7 +1,4 @@
using System.Collections.ObjectModel; using Avalonia.Collections;
using System.Reactive.Linq;
using DynamicData;
using InkForge.Data; using InkForge.Data;
@ -9,51 +6,46 @@ using Microsoft.EntityFrameworkCore;
namespace InkForge.Desktop.Models; namespace InkForge.Desktop.Models;
public class NoteStore public class NoteStore(IDbContextFactory<NoteDbContext> dbContextFactory)
{ {
private readonly IDbContextFactory<NoteDbContext> _dbContextFactory; public AvaloniaDictionary<int, Note> Notes { get; } = [];
private readonly SourceCache<Note, int> _notesCache = new(m => m.Id);
public ReadOnlyObservableCollection<Note> Notes { get; } public async ValueTask Load()
public NoteStore(IDbContextFactory<NoteDbContext> dbContextFactory)
{ {
_dbContextFactory = dbContextFactory; 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) public void AddNote(Note note)
{ {
using var dbContext = _dbContextFactory.CreateDbContext(); using var dbContext = dbContextFactory.CreateDbContext();
var entity = ToEntity(note); var entity = Map(note);
var entry = dbContext.Notes.Add(entity); var entry = dbContext.Notes.Add(entity);
dbContext.SaveChanges(); dbContext.SaveChanges();
} }
public Note CreateNote() => new(this);
public Note? GetById(int id) public Note? GetById(int id)
{ {
if (((Note?)_notesCache.Lookup(id)) is not Note note) if (!Notes.TryGetValue(id, out var note))
{ {
using var dbContext = _dbContextFactory.CreateDbContext(); using var dbContext = dbContextFactory.CreateDbContext();
if (dbContext.Notes.Find(id) is not { } dbNote) if (dbContext.Notes.Find(id) is not { } dbNote)
{ {
return null; return null;
} }
note = ToNote(dbNote); Notes.Add(id, note = Map(dbNote));
} }
return note; return note;
} }
public IObservable<Note?> Watch(int id) private NoteEntity Map(Note note)
{
return _notesCache.WatchValue(id);
}
private NoteEntity ToEntity(Note note)
{ {
return new() return new()
{ {
@ -64,25 +56,26 @@ public class NoteStore
Created = note.CreatedTime, Created = note.CreatedTime,
Updated = note.UpdatedTime, Updated = note.UpdatedTime,
}, },
Parent = note.Parent switch Parent = note.ParentId switch
{ {
{ Id: { } parentId } => new() { } parentId => new()
{ {
Id = parentId Id = parentId
}, },
_ => null, _ => null
} },
}; };
} }
private Note ToNote(NoteEntity entity) private Note Map(NoteEntity entity)
{ {
var note = CreateNote(); return new()
note.Id = entity.Id; {
note.Name = entity.Value.Name; Id = entity.Id,
note.CreatedTime = entity.Value.Created; Name = entity.Value.Name,
note.UpdatedTime = entity.Value.Updated; CreatedTime = entity.Value.Created,
note.ParentId = entity.Parent?.Id; UpdatedTime = entity.Value.Updated,
return note; ParentId = entity.Parent?.Id
};
} }
} }

View 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;
}
}
}

View file

@ -4,21 +4,16 @@ using Microsoft.Extensions.DependencyInjection;
namespace InkForge.Desktop.Models; namespace InkForge.Desktop.Models;
public sealed class Workspace : IDisposable public sealed class Workspace(IServiceScope scope) : IDisposable
{ {
private bool _disposedValue; private bool _disposedValue;
private IServiceScope? _scope; private IServiceScope? _scope = scope;
public string Name { get; set; } = default!; public string Name { get; set; } = default!;
public LocalWorkspaceOptions Options { get; set; } = default!; public LocalWorkspaceOptions Options { get; set; } = default!;
public IServiceProvider Services => _scope!.ServiceProvider; public IServiceProvider Services { get; } = scope.ServiceProvider;
public Workspace(IServiceScope scope)
{
_scope = scope;
}
public void Dispose() public void Dispose()
{ {
@ -33,14 +28,4 @@ public sealed class Workspace : IDisposable
_disposedValue = true; _disposedValue = true;
} }
} }
// private async Task LoadNotes()
// {
// await using var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
// await foreach (var asdf in dbContext.Notes.AsAsyncEnumerable().ConfigureAwait(false))
// {
// }
// _ = (await dbContext.Notes.ToListAsync().ConfigureAwait(false));
// }
} }

View file

@ -1,6 +1,5 @@
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.ReactiveUI;
using Avalonia.Threading; using Avalonia.Threading;
using InkForge.Desktop; using InkForge.Desktop;
@ -20,7 +19,6 @@ static class Program
public static AppBuilder BuildAvaloniaApp() public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>() => AppBuilder.Configure<App>()
.UsePlatformDetect() .UsePlatformDetect()
.UseReactiveUI()
.WithInterFont() .WithInterFont()
.LogToTrace(); .LogToTrace();

View file

@ -1,4 +1,5 @@
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Runtime.InteropServices;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
@ -52,7 +53,7 @@ public class TopLevels
// Register any new context // Register any new context
if (e.NewValue != null) if (e.NewValue != null)
{ {
RegistrationMapper.Add(e.NewValue, sender); CollectionsMarshal.GetValueRefOrAddDefault(RegistrationMapper, e.NewValue, out _) = sender;
} }
} }

View file

@ -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!);
}
}

View file

@ -2,33 +2,28 @@ using System.Reactive;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Dock.Model.ReactiveUI.Controls; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Dock.Model.Mvvm.Controls;
using InkForge.Desktop.Managers; using InkForge.Desktop.Managers;
using InkForge.Desktop.Services; using InkForge.Desktop.Services;
using ReactiveUI;
namespace InkForge.Desktop.ViewModels.Documents; namespace InkForge.Desktop.ViewModels.Documents;
public class WelcomePageDocumentViewModel : Document public partial class WelcomePageDocumentViewModel : Document
{ {
private readonly WorkspaceManager _workspaceController; private readonly WorkspaceManager _workspaceController;
public ReactiveCommand<Unit, Unit> CreateNew { get; }
public ReactiveCommand<Unit, Unit> OpenNew { get; }
public WelcomePageDocumentViewModel(WorkspaceManager workspaceController) public WelcomePageDocumentViewModel(WorkspaceManager workspaceController)
{ {
CanClose = false;
Title = "Welcome"; Title = "Welcome";
_workspaceController = workspaceController; _workspaceController = workspaceController;
CreateNew = ReactiveCommand.CreateFromTask(OnCreateNew);
OpenNew = ReactiveCommand.CreateFromTask(OnOpenNew);
} }
[RelayCommand]
private async Task OnCreateNew() private async Task OnCreateNew()
{ {
var storageProvider = this.GetStorageProvider()!; var storageProvider = this.GetStorageProvider()!;
@ -56,6 +51,7 @@ public class WelcomePageDocumentViewModel : Document
await _workspaceController.OpenWorkspace(filePath, true); await _workspaceController.OpenWorkspace(filePath, true);
} }
[RelayCommand]
private async Task OnOpenNew() private async Task OnOpenNew()
{ {
var storageProvider = this.GetStorageProvider()!; var storageProvider = this.GetStorageProvider()!;

View file

@ -1,13 +0,0 @@
using Dock.Model.Core;
using Dock.Model.ReactiveUI.Controls;
namespace InkForge.Desktop.ViewModels;
public class InkForgeDocumentDock : DocumentDock, IDock
{
bool IDock.IsEmpty
{
get => false;
set { }
}
}

View file

@ -1,32 +1,16 @@
using Avalonia; using CommunityToolkit.Mvvm.ComponentModel;
using Dock.Model.Core; using Dock.Model.Core;
using InkForge.Desktop.Managers;
using Microsoft.Extensions.DependencyInjection;
using ReactiveUI;
namespace InkForge.Desktop.ViewModels; namespace InkForge.Desktop.ViewModels;
public class MainViewModel : ReactiveObject public class MainViewModel : ObservableObject
{ {
private readonly DocumentManager _documentManager;
public IDock Layout { get; } public IDock Layout { get; }
public MainViewModel(InkForgeFactory factory) public MainViewModel(InkForgeFactory factory)
{ {
Layout = factory.CreateLayout(); Layout = factory.CreateLayout();
factory.InitLayout(Layout); factory.InitLayout(Layout);
_documentManager = CreateDocumentManager();
}
private static DocumentManager CreateDocumentManager()
{
return ActivatorUtilities.CreateInstance<DocumentManager>(
Application.Current!.GetValue(App.ServiceProviderProperty)
);
} }
} }

View file

@ -1,9 +1,14 @@
using ReactiveUI; using CommunityToolkit.Mvvm.ComponentModel;
namespace InkForge.Desktop.ViewModels; namespace InkForge.Desktop.ViewModels;
public record class RecentItemViewModel( public partial class RecentItemViewModel(
DateTimeOffset Created, DateTimeOffset created,
string Name, string name,
DateTimeOffset LastUsed DateTimeOffset lastUsed
) : ReactiveRecord; ) : ObservableObject
{
[ObservableProperty] private DateTimeOffset _created = created;
[ObservableProperty] private string _name = name;
[ObservableProperty] private DateTimeOffset _lastUsed = lastUsed;
}

View file

@ -1,32 +1,40 @@
using Dock.Model.ReactiveUI.Controls; using System.Reactive.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Dock.Model.Mvvm.Controls;
using DynamicData.Binding;
using InkForge.Desktop.Managers; using InkForge.Desktop.Managers;
using InkForge.Desktop.Models;
using InkForge.Desktop.ViewModels.Workspaces; using InkForge.Desktop.ViewModels.Workspaces;
using ReactiveUI;
namespace InkForge.Desktop.ViewModels.Tools; namespace InkForge.Desktop.ViewModels.Tools;
public class WorkspaceTool : Tool public partial class WorkspaceTool : Tool
{ {
private WorkspaceViewModel? _workspace; private readonly IWorkspaceViewModelFactory _workspaceViewModelFactory;
[ObservableProperty] private WorkspaceViewModel? _workspace;
public WorkspaceViewModel? Workspace
{
get => _workspace;
private set => this.RaiseAndSetIfChanged(ref _workspace, value);
}
public WorkspaceTool(WorkspaceManager workspaceManager, IWorkspaceViewModelFactory workspaceViewModelFactory) public WorkspaceTool(WorkspaceManager workspaceManager, IWorkspaceViewModelFactory workspaceViewModelFactory)
{ {
Title = "Workspace"; _workspaceViewModelFactory = workspaceViewModelFactory;
CanClose = false;
workspaceManager.WhenAnyValue(v => v.Workspace, Title = "Explorer";
v => v switch CanClose = false;
CanFloat = false;
CanPin = false;
workspaceManager.WhenValueChanged(v => v.Workspace).Subscribe(OnWorkspaceManagerWorkspaceChanged);
}
private void OnWorkspaceManagerWorkspaceChanged(Workspace? workspace)
{ {
{ } => workspaceViewModelFactory.Create(v), Workspace = workspace switch
{
{ } v => _workspaceViewModelFactory.Create(v),
_ => null _ => null
}).BindTo(this, v => v.Workspace); };
} }
} }

View file

@ -1,3 +1,9 @@
using System.Collections.ObjectModel;
using Avalonia;
using DynamicData;
using InkForge.Desktop.Models; using InkForge.Desktop.Models;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -6,30 +12,24 @@ namespace InkForge.Desktop.ViewModels.Workspaces
{ {
public class WorkspaceViewModel public class WorkspaceViewModel
{ {
private readonly Workspace _workspace;
private readonly NoteStore _noteStore; private readonly NoteStore _noteStore;
// private readonly ObservableAsPropertyHelper<string> _workspaceNameProperty; private readonly ReadOnlyObservableCollection<Node<Note, int>> _notes;
// public string WorkspaceName => _workspaceNameProperty.Value; public string Name => _workspace.Name;
// public ReactiveCommand<Unit, Unit> AddDocument { get; } public ReadOnlyObservableCollection<Node<Note, int>> Notes => _notes;
public WorkspaceViewModel(NoteStore noteStore) public WorkspaceViewModel(Workspace workspace, NoteStore noteStore)
{ {
_workspace = workspace;
_noteStore = noteStore; _noteStore = noteStore;
noteStore.Notes
.AsObservableChangeSet(m => m.Key)
.Transform(m => m.Value, true)
.TransformToTree(m => m.Id)
.Bind(out _notes).Subscribe();
} }
// public WorkspacesViewModel(Workspace workspace)
// {
// _workspace = workspace;
// _workspaceNameProperty = this.WhenAnyValue(v => v._workspace.Name).ToProperty(this, nameof(WorkspaceName));
// AddDocument = ReactiveCommand.Create(OnAddDocument);
// }
// private void OnAddDocument()
// {
// }
} }
public interface IWorkspaceViewModelFactory public interface IWorkspaceViewModelFactory
@ -39,14 +39,14 @@ namespace InkForge.Desktop.ViewModels.Workspaces
namespace Internal namespace Internal
{ {
internal class WorkspaceViewModelFactory(IServiceProvider services) : IWorkspaceViewModelFactory internal class WorkspaceViewModelFactory : IWorkspaceViewModelFactory
{ {
private static ObjectFactory<WorkspaceViewModel>? s_workspaceViewModelFactory; private static ObjectFactory<WorkspaceViewModel>? s_workspaceViewModelFactory;
public WorkspaceViewModel Create(Workspace workspace) public static WorkspaceViewModel Create(Workspace workspace)
{ {
s_workspaceViewModelFactory ??= ActivatorUtilities.CreateFactory<WorkspaceViewModel>([typeof(Workspace)]); s_workspaceViewModelFactory ??= ActivatorUtilities.CreateFactory<WorkspaceViewModel>([typeof(Workspace)]);
return s_workspaceViewModelFactory(services, [workspace]); return s_workspaceViewModelFactory(workspace.Services, [workspace]);
} }
WorkspaceViewModel IWorkspaceViewModelFactory.Create(Workspace workspace) => Create(workspace); WorkspaceViewModel IWorkspaceViewModelFactory.Create(Workspace workspace) => Create(workspace);

View 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>

View file

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace InkForge.Desktop.Views.Documents;
public partial class NoteEditDocument : UserControl
{
public NoteEditDocument()
{
InitializeComponent();
}
}

View file

@ -28,11 +28,11 @@
<Menu Grid.Row="2"> <Menu Grid.Row="2">
<MenuItem Header="Create New" <MenuItem Header="Create New"
Command="{CompiledBinding CreateNew}" /> Command="{CompiledBinding CreateNewCommand}" />
<MenuItem Header="Open" <MenuItem Header="Open"
IsEnabled="False" /> IsEnabled="False" />
<MenuItem Header="Open File" <MenuItem Header="Open File"
Command="{CompiledBinding OpenNew}" /> Command="{CompiledBinding OpenNewCommand}" />
</Menu> </Menu>
</Grid> </Grid>
</UserControl> </UserControl>

View file

@ -1,10 +1,8 @@
using Avalonia.ReactiveUI; using Avalonia.Controls;
using InkForge.Desktop.ViewModels.Documents;
namespace InkForge.Desktop.Views.Documents; namespace InkForge.Desktop.Views.Documents;
public partial class WelcomePageDocument : ReactiveUserControl<WelcomePageDocumentViewModel> public partial class WelcomePageDocument : UserControl
{ {
public WelcomePageDocument() public WelcomePageDocument()
{ {

View file

@ -1,5 +1,5 @@
using Avalonia; using Avalonia;
using Avalonia.ReactiveUI; using Avalonia.Controls;
using InkForge.Desktop.ViewModels; using InkForge.Desktop.ViewModels;
@ -7,12 +7,12 @@ using Microsoft.Extensions.DependencyInjection;
namespace InkForge.Desktop.Views; namespace InkForge.Desktop.Views;
public partial class MainWindow : ReactiveWindow<MainViewModel> public partial class MainWindow : Window
{ {
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
ViewModel = CreateViewModel(); DataContext = CreateViewModel();
} }
private static MainViewModel CreateViewModel() private static MainViewModel CreateViewModel()

View file

@ -1,8 +1,8 @@
using Avalonia.ReactiveUI; using Avalonia.Controls;
namespace InkForge.Desktop.Views.Tools; namespace InkForge.Desktop.Views.Tools;
public partial class WorkspaceTool : ReactiveUserControl<ViewModels.Tools.WorkspaceTool> public partial class WorkspaceTool : UserControl
{ {
public WorkspaceTool() public WorkspaceTool()
{ {

View file

@ -12,7 +12,8 @@
<Grid ColumnDefinitions="*, Auto" <Grid ColumnDefinitions="*, Auto"
RowDefinitions="Auto, *"> RowDefinitions="Auto, *">
<TextBlock Grid.Column="0" <TextBlock Text="{CompiledBinding Name}"
Grid.Column="0"
Grid.Row="0" /> Grid.Row="0" />
<StackPanel Classes="WorkspaceToolbar" <StackPanel Classes="WorkspaceToolbar"
@ -29,22 +30,6 @@
<Button> <Button>
<inkforge:FluentSymbolIcon Symbol="subtract_square_multiple" /> <inkforge:FluentSymbolIcon Symbol="subtract_square_multiple" />
</Button> </Button>
<!-- <StackPanel.Styles>
<Style Selector="#ToolBar > :is(TemplatedControl)">
<Setter Property="Background"
Value="Transparent" />
<Setter Property="Padding"
Value="1" />
<Setter Property="VerticalAlignment"
Value="Center" />
</Style>
<Style Selector="#FilesView:not(:pointerover) StackPanel">
<Setter Property="IsVisible"
Value="False" />
</Style>
</StackPanel.Styles> -->
</StackPanel> </StackPanel>
<TreeView Grid.ColumnSpan="2" <TreeView Grid.ColumnSpan="2"

View file

@ -1,10 +1,8 @@
using Avalonia.ReactiveUI; using Avalonia.Controls;
using InkForge.Desktop.ViewModels.Workspaces;
namespace InkForge.Desktop.Views.Workspaces; namespace InkForge.Desktop.Views.Workspaces;
public partial class WorkspaceView : ReactiveUserControl<WorkspaceViewModel> public partial class WorkspaceView : UserControl
{ {
public WorkspaceView() public WorkspaceView()
{ {

View file

@ -1,3 +1,3 @@
namespace InkForge.Data; namespace InkForge.Data;
public class Blob : Entity<byte[], string>; public class Blob : Entity<byte[], int>;

View file

@ -26,7 +26,10 @@ public class NoteDbContext(
{ {
options.HasKey(m => m.Id); options.HasKey(m => m.Id);
options.OwnsOne(m => m.Value); options.OwnsOne(m => m.Value, m =>
{
m.HasOne<Blob>().WithOne().HasForeignKey<Note>(m => m.ContentId).IsRequired();
});
options.HasOne(m => m.Parent); options.HasOne(m => m.Parent);
}); });

View file

@ -4,13 +4,13 @@ namespace InkForge.Data
{ {
public DateTimeOffset Created { get; set; } public DateTimeOffset Created { get; set; }
public string Name { get; set; } = default!; public int ContentId { get; set; }
public DateTimeOffset Updated { get; set; }
public DateTimeOffset? Deleted { get; set; } public DateTimeOffset? Deleted { get; set; }
public Blob Content { get; set; } = default!; public string Name { get; set; } = default!;
public DateTimeOffset Updated { get; set; }
} }
public class NoteEntity : Entity<NoteEntity, Note, int>; public class NoteEntity : Entity<NoteEntity, Note, int>;

View file

@ -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__01_Initial")]
partial class _01_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
}
}
}

View file

@ -1,207 +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("20240214000000__02_Metadata")]
partial class _02_Metadata
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.2");
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.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.Infrastructure.MetadataVersionEntity", b =>
{
b.Property<int?>("Version")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Version");
b.HasIndex("Id", "Version")
.IsUnique();
b.ToTable("MetadataHistory");
});
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
}
}
}

View file

@ -1,56 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace InkForge.Sqlite.Migrations
{
/// <inheritdoc />
public partial class _02_Metadata : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
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: "MetadataHistory",
columns: table => new
{
Version = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Value = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MetadataHistory", x => x.Version);
});
migrationBuilder.CreateIndex(
name: "IX_MetadataHistory_Id_Version",
table: "MetadataHistory",
columns: new[] { "Id", "Version" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Metadata");
migrationBuilder.DropTable(
name: "MetadataHistory");
}
}
}

View file

@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace InkForge.Sqlite.Migrations namespace InkForge.Sqlite.Migrations
{ {
[DbContext(typeof(NoteDbContext))] [DbContext(typeof(NoteDbContext))]
[Migration("20240405000000__03_DropHistory")] [Migration("20240401_Initial")]
partial class _03_DropHistory partial class _20240401_Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -22,8 +22,9 @@ namespace InkForge.Sqlite.Migrations
modelBuilder.Entity("InkForge.Data.Blob", b => modelBuilder.Entity("InkForge.Data.Blob", b =>
{ {
b.Property<string>("Id") b.Property<int>("Id")
.HasColumnType("TEXT"); .ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<byte[]>("Value") b.Property<byte[]>("Value")
.IsRequired() .IsRequired()
@ -75,9 +76,8 @@ namespace InkForge.Sqlite.Migrations
b1.Property<int>("NoteEntityId") b1.Property<int>("NoteEntityId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b1.Property<string>("ContentId") b1.Property<int>("ContentId")
.IsRequired() .HasColumnType("INTEGER");
.HasColumnType("TEXT");
b1.Property<DateTimeOffset>("Created") b1.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -94,20 +94,19 @@ namespace InkForge.Sqlite.Migrations
b1.HasKey("NoteEntityId"); b1.HasKey("NoteEntityId");
b1.HasIndex("ContentId"); b1.HasIndex("ContentId")
.IsUnique();
b1.ToTable("Notes"); b1.ToTable("Notes");
b1.HasOne("InkForge.Data.Blob", "Content") b1.HasOne("InkForge.Data.Blob", null)
.WithMany() .WithOne()
.HasForeignKey("ContentId") .HasForeignKey("InkForge.Data.NoteEntity.Value#InkForge.Data.Note", "ContentId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b1.WithOwner() b1.WithOwner()
.HasForeignKey("NoteEntityId"); .HasForeignKey("NoteEntityId");
b1.Navigation("Content");
}); });
b.Navigation("Parent"); b.Navigation("Parent");

View file

@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace InkForge.Sqlite.Migrations namespace InkForge.Sqlite.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class _01_Initial : Migration public partial class _20240401_Initial : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
@ -15,25 +15,38 @@ namespace InkForge.Sqlite.Migrations
name: "Blobs", name: "Blobs",
columns: table => new columns: table => new
{ {
Id = table.Column<string>(type: "TEXT", nullable: false), Id = table.Column<int>(type: "INTEGER", nullable: false)
Content = table.Column<byte[]>(type: "BLOB", nullable: false) .Annotation("Sqlite:Autoincrement", true),
Value = table.Column<byte[]>(type: "BLOB", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Blobs", x => x.Id); table.PrimaryKey("PK_Blobs", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "Metadata",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Metadata", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Notes", name: "Notes",
columns: table => new columns: table => new
{ {
Id = table.Column<int>(type: "INTEGER", nullable: false) Id = table.Column<int>(type: "INTEGER", nullable: false),
.Annotation("Sqlite:Autoincrement", true),
Value_Created = table.Column<DateTimeOffset>(type: "TEXT", nullable: false), Value_Created = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
Value_ContentId = table.Column<int>(type: "INTEGER", nullable: false),
Value_Deleted = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
Value_Name = table.Column<string>(type: "TEXT", nullable: false), Value_Name = table.Column<string>(type: "TEXT", nullable: false),
Value_Updated = table.Column<DateTimeOffset>(type: "TEXT", nullable: false), Value_Updated = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
Value_Deleted = table.Column<DateTimeOffset>(type: "TEXT", nullable: true), ParentId = table.Column<int>(type: "INTEGER", nullable: true)
Value_ContentId = table.Column<string>(type: "TEXT", nullable: false)
}, },
constraints: table => constraints: table =>
{ {
@ -44,65 +57,30 @@ namespace InkForge.Sqlite.Migrations
principalTable: "Blobs", principalTable: "Blobs",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "NoteVersions",
columns: table => new
{
Version = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Value_Created = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
Value_ParentId = table.Column<int>(type: "INTEGER", nullable: true),
Value_Name = table.Column<string>(type: "TEXT", nullable: false),
Value_Updated = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
Value_Deleted = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
Value_ContentId = table.Column<string>(type: "TEXT", nullable: false),
Id = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_NoteVersions", x => x.Version);
table.ForeignKey( table.ForeignKey(
name: "FK_NoteVersions_Blobs_Value_ContentId", name: "FK_Notes_Notes_ParentId",
column: x => x.Value_ContentId, column: x => x.ParentId,
principalTable: "Blobs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_NoteVersions_Notes_Value_ParentId",
column: x => x.Value_ParentId,
principalTable: "Notes", principalTable: "Notes",
principalColumn: "Id"); principalColumn: "Id");
}); });
migrationBuilder.CreateIndex(
name: "IX_Notes_ParentId",
table: "Notes",
column: "ParentId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Notes_Value_ContentId", name: "IX_Notes_Value_ContentId",
table: "Notes", table: "Notes",
column: "Value_ContentId"); column: "Value_ContentId",
migrationBuilder.CreateIndex(
name: "IX_NoteVersions_Id_Version",
table: "NoteVersions",
columns: new[] { "Id", "Version" },
unique: true); unique: true);
migrationBuilder.CreateIndex(
name: "IX_NoteVersions_Value_ContentId",
table: "NoteVersions",
column: "Value_ContentId");
migrationBuilder.CreateIndex(
name: "IX_NoteVersions_Value_ParentId",
table: "NoteVersions",
column: "Value_ParentId");
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "NoteVersions"); name: "Metadata");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Notes"); name: "Notes");

View file

@ -1,149 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace InkForge.Sqlite.Migrations
{
/// <inheritdoc />
public partial class _03_DropHistory : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MetadataHistory");
migrationBuilder.DropTable(
name: "NoteVersions");
migrationBuilder.RenameColumn(
name: "Content",
table: "Blobs",
newName: "Value");
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "Notes",
type: "INTEGER",
nullable: false,
oldClrType: typeof(int),
oldType: "INTEGER")
.OldAnnotation("Sqlite:Autoincrement", true);
migrationBuilder.AddColumn<int>(
name: "ParentId",
table: "Notes",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Notes_ParentId",
table: "Notes",
column: "ParentId");
migrationBuilder.AddForeignKey(
name: "FK_Notes_Notes_ParentId",
table: "Notes",
column: "ParentId",
principalTable: "Notes",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Notes_Notes_ParentId",
table: "Notes");
migrationBuilder.DropIndex(
name: "IX_Notes_ParentId",
table: "Notes");
migrationBuilder.DropColumn(
name: "ParentId",
table: "Notes");
migrationBuilder.RenameColumn(
name: "Value",
table: "Blobs",
newName: "Content");
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "Notes",
type: "INTEGER",
nullable: false,
oldClrType: typeof(int),
oldType: "INTEGER")
.Annotation("Sqlite:Autoincrement", true);
migrationBuilder.CreateTable(
name: "MetadataHistory",
columns: table => new
{
Version = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Id = table.Column<string>(type: "TEXT", nullable: true),
Value = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MetadataHistory", x => x.Version);
});
migrationBuilder.CreateTable(
name: "NoteVersions",
columns: table => new
{
Version = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Id = table.Column<int>(type: "INTEGER", nullable: false),
Value_ContentId = table.Column<string>(type: "TEXT", nullable: false),
Value_ParentId = table.Column<int>(type: "INTEGER", nullable: true),
Value_Created = table.Column<DateTimeOffset>(type: "TEXT", 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)
},
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,
principalTable: "Notes",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_MetadataHistory_Id_Version",
table: "MetadataHistory",
columns: new[] { "Id", "Version" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_NoteVersions_Id_Version",
table: "NoteVersions",
columns: new[] { "Id", "Version" },
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");
}
}
}

View file

@ -19,8 +19,9 @@ namespace InkForge.Sqlite.Migrations
modelBuilder.Entity("InkForge.Data.Blob", b => modelBuilder.Entity("InkForge.Data.Blob", b =>
{ {
b.Property<string>("Id") b.Property<int>("Id")
.HasColumnType("TEXT"); .ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<byte[]>("Value") b.Property<byte[]>("Value")
.IsRequired() .IsRequired()
@ -28,7 +29,7 @@ namespace InkForge.Sqlite.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Blobs", (string)null); b.ToTable("Blobs");
}); });
modelBuilder.Entity("InkForge.Data.MetadataEntity", b => modelBuilder.Entity("InkForge.Data.MetadataEntity", b =>
@ -42,7 +43,7 @@ namespace InkForge.Sqlite.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Metadata", (string)null); b.ToTable("Metadata");
}); });
modelBuilder.Entity("InkForge.Data.NoteEntity", b => modelBuilder.Entity("InkForge.Data.NoteEntity", b =>
@ -58,7 +59,7 @@ namespace InkForge.Sqlite.Migrations
b.HasIndex("ParentId"); b.HasIndex("ParentId");
b.ToTable("Notes", (string)null); b.ToTable("Notes");
}); });
modelBuilder.Entity("InkForge.Data.NoteEntity", b => modelBuilder.Entity("InkForge.Data.NoteEntity", b =>
@ -67,14 +68,13 @@ namespace InkForge.Sqlite.Migrations
.WithMany() .WithMany()
.HasForeignKey("ParentId"); .HasForeignKey("ParentId");
b.OwnsOne("InkForge.Data.NoteEntity.Value#InkForge.Data.Note", "Value", b1 => b.OwnsOne("InkForge.Data.Note", "Value", b1 =>
{ {
b1.Property<int>("NoteEntityId") b1.Property<int>("NoteEntityId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b1.Property<string>("ContentId") b1.Property<int>("ContentId")
.IsRequired() .HasColumnType("INTEGER");
.HasColumnType("TEXT");
b1.Property<DateTimeOffset>("Created") b1.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -91,20 +91,19 @@ namespace InkForge.Sqlite.Migrations
b1.HasKey("NoteEntityId"); b1.HasKey("NoteEntityId");
b1.HasIndex("ContentId"); b1.HasIndex("ContentId")
.IsUnique();
b1.ToTable("Notes", (string)null); b1.ToTable("Notes");
b1.HasOne("InkForge.Data.Blob", "Content") b1.HasOne("InkForge.Data.Blob", null)
.WithMany() .WithOne()
.HasForeignKey("ContentId") .HasForeignKey("InkForge.Data.NoteEntity.Value#InkForge.Data.Note", "ContentId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b1.WithOwner() b1.WithOwner()
.HasForeignKey("NoteEntityId"); .HasForeignKey("NoteEntityId");
b1.Navigation("Content");
}); });
b.Navigation("Parent"); b.Navigation("Parent");