This commit is contained in:
Jöran Malek 2024-04-05 12:31:34 +02:00
parent 4c2b5cca93
commit 43b4d50e43
28 changed files with 674 additions and 249 deletions

View file

@ -8,6 +8,7 @@
<AssemblyName>InkForge</AssemblyName>
<RootNamespace>InkForge.Desktop</RootNamespace>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
<!-- <TrimMode>partial</TrimMode> -->
</PropertyGroup>
<ItemGroup>

View file

@ -22,7 +22,7 @@ public class InkForgeFactory : Factory
IsCollapsable = false,
};
_documentDock = new InkForgeDocumentDock
_documentDock = new DocumentDock
{
Id = "Documents",
Title = "Documents",

View file

@ -62,26 +62,25 @@ public class WorkspaceManager(IServiceProvider serviceProvider) : ReactiveObject
};
var dbFactory = serviceProvider.GetRequiredService<IDbContextFactory<NoteDbContext>>();
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();

View file

@ -34,6 +34,7 @@ public static class InkForgeServiceCollections
// Scoped
// - Concrete
services.AddScoped<LocalWorkspaceOptions>();
services.AddScoped<NoteStore>();
// - Service
services.AddScoped<IDbContextFactory<NoteDbContext>, NoteDbContextFactory>();

View file

@ -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<Note?> _parent;
private readonly BehaviorSubject<int?> _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<Note?>(),
}).Switch(null).ToProperty(this, nameof(Parent), deferSubscription: true);
}
}

View file

@ -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<NoteDbContext> _dbContextFactory;
private readonly SourceCache<Note, int> _notesCache = new(m => m.Id);
public ReadOnlyObservableCollection<Note> Notes { get; }
public NoteStore(IDbContextFactory<NoteDbContext> 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<Note?> 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;
}
}

View file

@ -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<NoteDbContext> _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<IDbContextFactory<NoteDbContext>>();
}
// 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));
// }
}

View file

@ -0,0 +1,147 @@
using System.Reactive.Disposables;
namespace System.Reactive.Linq;
public static class ObservableSwitch
{
public static IObservable<T> Switch<T>(this IObservable<IObservable<T>> observable, T defaultValue)
{
return new SwitchObservable<T>(observable, defaultValue);
}
private class SwitchObservable<T>(IObservable<IObservable<T>> sources, T defaultValue) : ObservableBase<T>
{
protected override IDisposable SubscribeCore(IObserver<T> observer)
{
_ _ = new(defaultValue, observer);
_.Run(sources);
return _;
}
private class _(T defaultValue, IObserver<T> observer) : ObserverBase<IObservable<T>>
{
private readonly SerialDisposable _innerSerialDisposable = new();
private readonly SingleAssignmentDisposable _upstream = new();
private bool _hasLatest;
private int _latest;
private bool _stopped = false;
public void Run(IObservable<IObservable<T>> sources)
{
_upstream.Disposable = sources.Subscribe(this);
if (_innerSerialDisposable.Disposable is null)
{
observer.OnNext(defaultValue);
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_innerSerialDisposable?.Dispose();
}
base.Dispose(disposing);
}
protected void ForwardOnCompleted() => observer.OnCompleted();
protected void ForwardOnError(Exception error) => observer.OnError(error);
protected void ForwardOnNext(T value) => observer.OnNext(value);
protected override void OnCompletedCore()
{
_upstream.Dispose();
_stopped = true;
if (!_hasLatest)
{
observer.OnCompleted();
}
}
protected override void OnErrorCore(Exception error) => ForwardOnError(error);
protected override void OnNextCore(IObservable<T> value)
{
uint id = unchecked((uint)Interlocked.Increment(ref _latest));
_hasLatest = true;
var innerObserver = new InnerObserver(this, id, defaultValue);
_innerSerialDisposable.Disposable = innerObserver;
innerObserver.Subscribe(value);
}
private class InnerObserver(_ parent, uint id, T defaultValue) : ObserverBase<T>
{
private readonly SingleAssignmentDisposable _upstream = new();
public bool Found { get; set; } = false;
public void Subscribe(IObservable<T> upstream)
{
_upstream.Disposable = upstream.SubscribeSafe(this);
if (!Found)
{
OnNext(defaultValue);
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_upstream.Dispose();
}
base.Dispose(disposing);
}
protected override void OnCompletedCore()
{
Dispose();
if (parent._latest == id)
{
parent._hasLatest = false;
if (!Found)
{
OnNextCore(defaultValue);
}
if (parent._stopped)
{
parent.ForwardOnCompleted();
}
}
}
protected override void OnErrorCore(Exception error)
{
Dispose();
if (parent._latest == id)
{
if (!Found)
{
OnNextCore(defaultValue);
}
parent.ForwardOnError(error);
}
}
protected override void OnNextCore(T value)
{
Found = true;
if (parent._latest == id)
{
parent.ForwardOnNext(value);
}
}
}
}
}
}

View file

@ -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<string> _workspaceNameProperty;
// public string WorkspaceName => _workspaceNameProperty.Value;
// public ReactiveCommand<Unit, Unit> AddDocument { get; }
public WorkspaceViewModel(NoteStore noteStore)
{
_noteStore = noteStore;
}
// public WorkspacesViewModel(Workspace workspace)
// {
// _workspace = workspace;

View file

@ -20,7 +20,7 @@
Spacing="3"
Grid.Column="1"
Grid.Row="0">
<Button>
<Button AutomationProperties.Name="Add">
<inkforge:FluentSymbolIcon Symbol="document_add" />
</Button>
<Button>