diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 96f30af..1bbbcb0 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "8.0.2",
+ "version": "8.0.3",
"commands": [
"dotnet-ef"
]
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 54ac008..6b5521d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -10,8 +10,8 @@
-
-
+
+
diff --git a/InkForge.Api.Data/ApiDbContext.cs b/InkForge.Api.Data/ApiDbContext.cs
index 84c616b..b41d77b 100644
--- a/InkForge.Api.Data/ApiDbContext.cs
+++ b/InkForge.Api.Data/ApiDbContext.cs
@@ -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 Workspaces { get; set; } = default!;
- public DbSet WorkspaceVersions { get; set; } = default!;
-
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity(options =>
{
- options.OwnsOne(m => m.Value);
-
options.HasKey(m => m.Id);
- });
- builder.Entity(options =>
- {
options.OwnsOne(m => m.Value);
-
- options.HasKey(m => m.Version);
- options.HasIndex(nameof(WorkspaceVersionEntity.Id), nameof(WorkspaceVersionEntity.Version)).IsUnique();
});
}
}
diff --git a/InkForge.Api.Data/Domain/Workspace.cs b/InkForge.Api.Data/Domain/Workspace.cs
deleted file mode 100644
index 6a11037..0000000
--- a/InkForge.Api.Data/Domain/Workspace.cs
+++ /dev/null
@@ -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; }
-}
diff --git a/InkForge.Api.Data/Infrastructure/WorkspaceEntities.cs b/InkForge.Api.Data/Infrastructure/WorkspaceEntities.cs
deleted file mode 100644
index 74606fa..0000000
--- a/InkForge.Api.Data/Infrastructure/WorkspaceEntities.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using InkForge.Api.Data.Domain;
-using InkForge.Data;
-
-namespace InkForge.Api.Data.Infrastructure
-{
- public class WorkspaceEntity : Entity;
-
- public class WorkspaceVersionEntity : VersionedEntity;
-}
diff --git a/InkForge.Api.Data/Workspaces.cs b/InkForge.Api.Data/Workspaces.cs
new file mode 100644
index 0000000..7ba7b24
--- /dev/null
+++ b/InkForge.Api.Data/Workspaces.cs
@@ -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;
+}
diff --git a/app/InkForge.Desktop/InkForge.Desktop.csproj b/app/InkForge.Desktop/InkForge.Desktop.csproj
index 4d0c737..0475604 100644
--- a/app/InkForge.Desktop/InkForge.Desktop.csproj
+++ b/app/InkForge.Desktop/InkForge.Desktop.csproj
@@ -8,6 +8,7 @@
InkForge
InkForge.Desktop
true
+
diff --git a/app/InkForge.Desktop/InkForgeFactory.cs b/app/InkForge.Desktop/InkForgeFactory.cs
index 4511489..7183d12 100644
--- a/app/InkForge.Desktop/InkForgeFactory.cs
+++ b/app/InkForge.Desktop/InkForgeFactory.cs
@@ -22,7 +22,7 @@ public class InkForgeFactory : Factory
IsCollapsable = false,
};
- _documentDock = new InkForgeDocumentDock
+ _documentDock = new DocumentDock
{
Id = "Documents",
Title = "Documents",
diff --git a/app/InkForge.Desktop/Managers/WorkspaceManager.cs b/app/InkForge.Desktop/Managers/WorkspaceManager.cs
index 90b9261..3bee05d 100644
--- a/app/InkForge.Desktop/Managers/WorkspaceManager.cs
+++ b/app/InkForge.Desktop/Managers/WorkspaceManager.cs
@@ -62,26 +62,25 @@ public class WorkspaceManager(IServiceProvider serviceProvider) : ReactiveObject
};
var dbFactory = serviceProvider.GetRequiredService>();
- await using (var dbContext = dbFactory.CreateDbContext())
+ await using var dbContext = await dbFactory.CreateDbContextAsync().ConfigureAwait(false);
+ var db = dbContext.Database;
+ if ((await db.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
{
- var db = dbContext.Database;
- await using var transaction = await db.BeginTransactionAsync().ConfigureAwait(false);
- try
+ if (file.Exists)
{
- await db.MigrateAsync().ConfigureAwait(false);
- }
- catch
- {
- // Show Error through TopLevels.ActiveTopLevel
- await transaction.RollbackAsync().ConfigureAwait(false);
- return null;
+ file.CopyTo(Path.ChangeExtension(file.FullName, $"{DateTime.Now:s}{file.Extension}"));
}
- await transaction.CommitAsync().ConfigureAwait(false);
+ await db.MigrateAsync().ConfigureAwait(false);
}
scope = null;
}
+ catch (Exception)
+ {
+ // Show Error through TopLevels.ActiveTopLevel
+ return null;
+ }
finally
{
scope?.Dispose();
diff --git a/app/InkForge.Desktop/Microsoft/Extensions/DependencyInjection/InkForgeServiceCollection.cs b/app/InkForge.Desktop/Microsoft/Extensions/DependencyInjection/InkForgeServiceCollection.cs
index 31f3313..328f8ce 100644
--- a/app/InkForge.Desktop/Microsoft/Extensions/DependencyInjection/InkForgeServiceCollection.cs
+++ b/app/InkForge.Desktop/Microsoft/Extensions/DependencyInjection/InkForgeServiceCollection.cs
@@ -34,6 +34,7 @@ public static class InkForgeServiceCollections
// Scoped
// - Concrete
services.AddScoped();
+ services.AddScoped();
// - Service
services.AddScoped, NoteDbContextFactory>();
diff --git a/app/InkForge.Desktop/Models/Note.cs b/app/InkForge.Desktop/Models/Note.cs
index 7e04030..58a0295 100644
--- a/app/InkForge.Desktop/Models/Note.cs
+++ b/app/InkForge.Desktop/Models/Note.cs
@@ -1,6 +1,61 @@
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+
+using ReactiveUI;
+
namespace InkForge.Desktop.Models;
-public class Note
+public class Note : ReactiveObject
{
-
+ private readonly ObservableAsPropertyHelper _parent;
+ private readonly BehaviorSubject _parentId = new(default);
+ private DateTimeOffset _createdTime;
+ private int _id;
+ private string _name = default!;
+ 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(),
+ }).Switch(null).ToProperty(this, nameof(Parent), deferSubscription: true);
+ }
}
diff --git a/app/InkForge.Desktop/Models/NoteStore.cs b/app/InkForge.Desktop/Models/NoteStore.cs
new file mode 100644
index 0000000..6be8a08
--- /dev/null
+++ b/app/InkForge.Desktop/Models/NoteStore.cs
@@ -0,0 +1,88 @@
+using System.Collections.ObjectModel;
+using System.Reactive.Linq;
+
+using DynamicData;
+
+using InkForge.Data;
+
+using Microsoft.EntityFrameworkCore;
+
+namespace InkForge.Desktop.Models;
+
+public class NoteStore
+{
+ private readonly IDbContextFactory _dbContextFactory;
+ private readonly SourceCache _notesCache = new(m => m.Id);
+
+ public ReadOnlyObservableCollection Notes { get; }
+
+ public NoteStore(IDbContextFactory dbContextFactory)
+ {
+ _dbContextFactory = dbContextFactory;
+ }
+
+ public void AddNote(Note note)
+ {
+ using var dbContext = _dbContextFactory.CreateDbContext();
+ var entity = ToEntity(note);
+ var entry = dbContext.Notes.Add(entity);
+
+ dbContext.SaveChanges();
+ }
+
+ public Note CreateNote() => new(this);
+
+ public Note? GetById(int id)
+ {
+ if (((Note?)_notesCache.Lookup(id)) is not Note note)
+ {
+ using var dbContext = _dbContextFactory.CreateDbContext();
+ if (dbContext.Notes.Find(id) is not { } dbNote)
+ {
+ return null;
+ }
+
+ note = ToNote(dbNote);
+ }
+
+ return note;
+ }
+
+ public IObservable Watch(int id)
+ {
+ return _notesCache.WatchValue(id);
+ }
+
+ private NoteEntity ToEntity(Note note)
+ {
+ return new()
+ {
+ Id = note.Id,
+ Value = new()
+ {
+ Name = note.Name,
+ Created = note.CreatedTime,
+ Updated = note.UpdatedTime,
+ },
+ Parent = note.Parent switch
+ {
+ { Id: { } parentId } => new()
+ {
+ Id = parentId
+ },
+ _ => null,
+ }
+ };
+ }
+
+ private Note ToNote(NoteEntity entity)
+ {
+ var note = CreateNote();
+ note.Id = entity.Id;
+ note.Name = entity.Value.Name;
+ note.CreatedTime = entity.Value.Created;
+ note.UpdatedTime = entity.Value.Updated;
+ note.ParentId = entity.Parent?.Id;
+ return note;
+ }
+}
diff --git a/app/InkForge.Desktop/Models/Workspace.cs b/app/InkForge.Desktop/Models/Workspace.cs
index cb3b8b3..4cb23de 100644
--- a/app/InkForge.Desktop/Models/Workspace.cs
+++ b/app/InkForge.Desktop/Models/Workspace.cs
@@ -1,14 +1,11 @@
-using InkForge.Data;
using InkForge.Desktop.Data.Options;
-using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace InkForge.Desktop.Models;
public sealed class Workspace : IDisposable
{
- private readonly IDbContextFactory _dbContextFactory;
private bool _disposedValue;
private IServiceScope? _scope;
@@ -21,20 +18,9 @@ public sealed class Workspace : IDisposable
public Workspace(IServiceScope scope)
{
_scope = scope;
- _dbContextFactory = Services.GetRequiredService>();
}
- // public Note AddNote(Note? parent)
- // {
- // }
-
public void Dispose()
- {
- Dispose(disposing: true);
- GC.SuppressFinalize(this);
- }
-
- private void Dispose(bool disposing)
{
if (!_disposedValue)
{
@@ -47,4 +33,14 @@ public sealed class Workspace : IDisposable
_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));
+ // }
}
diff --git a/app/InkForge.Desktop/System/Reactive/Linq/Observable.Switch.cs b/app/InkForge.Desktop/System/Reactive/Linq/Observable.Switch.cs
new file mode 100644
index 0000000..edd81a3
--- /dev/null
+++ b/app/InkForge.Desktop/System/Reactive/Linq/Observable.Switch.cs
@@ -0,0 +1,147 @@
+using System.Reactive.Disposables;
+
+namespace System.Reactive.Linq;
+
+public static class ObservableSwitch
+{
+ public static IObservable Switch(this IObservable> observable, T defaultValue)
+ {
+ return new SwitchObservable(observable, defaultValue);
+ }
+
+ private class SwitchObservable(IObservable> sources, T defaultValue) : ObservableBase
+ {
+ protected override IDisposable SubscribeCore(IObserver observer)
+ {
+ _ _ = new(defaultValue, observer);
+ _.Run(sources);
+ return _;
+ }
+
+ private class _(T defaultValue, IObserver observer) : ObserverBase>
+ {
+ private readonly SerialDisposable _innerSerialDisposable = new();
+ private readonly SingleAssignmentDisposable _upstream = new();
+ private bool _hasLatest;
+ private int _latest;
+ private bool _stopped = false;
+
+ public void Run(IObservable> 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 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
+ {
+ private readonly SingleAssignmentDisposable _upstream = new();
+
+ public bool Found { get; set; } = false;
+
+ public void Subscribe(IObservable 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);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/InkForge.Desktop/ViewModels/Workspaces/WorkspaceViewModel.cs b/app/InkForge.Desktop/ViewModels/Workspaces/WorkspaceViewModel.cs
index 320ca4e..f28ea4e 100644
--- a/app/InkForge.Desktop/ViewModels/Workspaces/WorkspaceViewModel.cs
+++ b/app/InkForge.Desktop/ViewModels/Workspaces/WorkspaceViewModel.cs
@@ -4,15 +4,20 @@ using Microsoft.Extensions.DependencyInjection;
namespace InkForge.Desktop.ViewModels.Workspaces
{
- public class WorkspaceViewModel(Workspace workspace)
+ public class WorkspaceViewModel
{
- // private readonly Workspace _workspace;
+ private readonly NoteStore _noteStore;
// private readonly ObservableAsPropertyHelper _workspaceNameProperty;
// public string WorkspaceName => _workspaceNameProperty.Value;
// public ReactiveCommand AddDocument { get; }
+ public WorkspaceViewModel(NoteStore noteStore)
+ {
+ _noteStore = noteStore;
+ }
+
// public WorkspacesViewModel(Workspace workspace)
// {
// _workspace = workspace;
diff --git a/app/InkForge.Desktop/Views/Workspaces/WorkspaceView.axaml b/app/InkForge.Desktop/Views/Workspaces/WorkspaceView.axaml
index 7cee934..c07ca41 100644
--- a/app/InkForge.Desktop/Views/Workspaces/WorkspaceView.axaml
+++ b/app/InkForge.Desktop/Views/Workspaces/WorkspaceView.axaml
@@ -20,7 +20,7 @@
Spacing="3"
Grid.Column="1"
Grid.Row="0">
-