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