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,6 +5,7 @@
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
<DockFluentTheme />
</Application.Styles>
@ -13,4 +14,4 @@
<FontFamily x:Key="FluentSystemIcons-Filled">/Assets/Fonts#FluentSystemIcons-Filled</FontFamily>
<FontFamily x:Key="FluentSystemIcons-Regular">/Assets/Fonts#FluentSystemIcons-Regular</FontFamily>
</Application.Resources>
</Application>
</Application>

View file

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

View file

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

View file

@ -12,18 +12,21 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.AvaloniaEdit" />
<PackageReference Include="Avalonia.Controls.DataGrid" />
<PackageReference Include="Avalonia.Desktop" />
<PackageReference Include="Avalonia.Fonts.Inter" />
<PackageReference Include="Avalonia.ReactiveUI" />
<PackageReference Include="Avalonia.Themes.Fluent" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<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.Json" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="SmartFormat" />
<PackageReference Include="Splat" />
<PackageReference Include="Splat.Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="System.IO.Hashing" />
</ItemGroup>
@ -41,4 +44,4 @@
<EmbeddedResource Include="Properties\appsettings.json" />
</ItemGroup>
</Project>
</Project>

View file

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

View file

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

View file

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

View file

@ -1,61 +1,12 @@
using System.Reactive.Linq;
using System.Reactive.Subjects;
using ReactiveUI;
using CommunityToolkit.Mvvm.ComponentModel;
namespace InkForge.Desktop.Models;
public class Note : ReactiveObject
public partial class Note : ObservableObject
{
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);
}
[ObservableProperty] private DateTimeOffset _createdTime;
[ObservableProperty] private int _id;
[ObservableProperty] private string _name = default!;
[ObservableProperty] private int? _parentId;
[ObservableProperty] private DateTimeOffset _updatedTime;
}

View file

@ -1,7 +1,4 @@
using System.Collections.ObjectModel;
using System.Reactive.Linq;
using DynamicData;
using Avalonia.Collections;
using InkForge.Data;
@ -9,51 +6,46 @@ using Microsoft.EntityFrameworkCore;
namespace InkForge.Desktop.Models;
public class NoteStore
public class NoteStore(IDbContextFactory<NoteDbContext> dbContextFactory)
{
private readonly IDbContextFactory<NoteDbContext> _dbContextFactory;
private readonly SourceCache<Note, int> _notesCache = new(m => m.Id);
public AvaloniaDictionary<int, Note> Notes { get; } = [];
public ReadOnlyObservableCollection<Note> Notes { get; }
public NoteStore(IDbContextFactory<NoteDbContext> dbContextFactory)
public async ValueTask Load()
{
_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)
{
using var dbContext = _dbContextFactory.CreateDbContext();
var entity = ToEntity(note);
using var dbContext = dbContextFactory.CreateDbContext();
var entity = Map(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)
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)
{
return null;
}
note = ToNote(dbNote);
Notes.Add(id, note = Map(dbNote));
}
return note;
}
public IObservable<Note?> Watch(int id)
{
return _notesCache.WatchValue(id);
}
private NoteEntity ToEntity(Note note)
private NoteEntity Map(Note note)
{
return new()
{
@ -64,25 +56,26 @@ public class NoteStore
Created = note.CreatedTime,
Updated = note.UpdatedTime,
},
Parent = note.Parent switch
Parent = note.ParentId switch
{
{ Id: { } parentId } => new()
{ } parentId => new()
{
Id = parentId
},
_ => null,
}
_ => null
},
};
}
private Note ToNote(NoteEntity entity)
private Note Map(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;
return new()
{
Id = entity.Id,
Name = entity.Value.Name,
CreatedTime = entity.Value.Created,
UpdatedTime = entity.Value.Updated,
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;
public sealed class Workspace : IDisposable
public sealed class Workspace(IServiceScope scope) : IDisposable
{
private bool _disposedValue;
private IServiceScope? _scope;
private IServiceScope? _scope = scope;
public string Name { get; set; } = default!;
public LocalWorkspaceOptions Options { get; set; } = default!;
public IServiceProvider Services => _scope!.ServiceProvider;
public Workspace(IServiceScope scope)
{
_scope = scope;
}
public IServiceProvider Services { get; } = scope.ServiceProvider;
public void Dispose()
{
@ -33,14 +28,4 @@ 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

@ -1,6 +1,5 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using InkForge.Desktop;
@ -20,7 +19,6 @@ static class Program
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.UseReactiveUI()
.WithInterFont()
.LogToTrace();
@ -40,7 +38,7 @@ static class Program
_ = app.ApplicationLifetime switch
{
IClassicDesktopStyleApplicationLifetime desktop => desktop.MainWindow = new MainWindow(),
_ => throw new NotSupportedException(),
_ => throw new NotSupportedException(),
};
}

View file

@ -1,4 +1,5 @@
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
@ -52,7 +53,7 @@ public class TopLevels
// Register any new context
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 Dock.Model.ReactiveUI.Controls;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Dock.Model.Mvvm.Controls;
using InkForge.Desktop.Managers;
using InkForge.Desktop.Services;
using ReactiveUI;
namespace InkForge.Desktop.ViewModels.Documents;
public class WelcomePageDocumentViewModel : Document
public partial class WelcomePageDocumentViewModel : Document
{
private readonly WorkspaceManager _workspaceController;
public ReactiveCommand<Unit, Unit> CreateNew { get; }
public ReactiveCommand<Unit, Unit> OpenNew { get; }
public WelcomePageDocumentViewModel(WorkspaceManager workspaceController)
{
CanClose = false;
Title = "Welcome";
_workspaceController = workspaceController;
CreateNew = ReactiveCommand.CreateFromTask(OnCreateNew);
OpenNew = ReactiveCommand.CreateFromTask(OnOpenNew);
}
[RelayCommand]
private async Task OnCreateNew()
{
var storageProvider = this.GetStorageProvider()!;
@ -56,6 +51,7 @@ public class WelcomePageDocumentViewModel : Document
await _workspaceController.OpenWorkspace(filePath, true);
}
[RelayCommand]
private async Task OnOpenNew()
{
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 InkForge.Desktop.Managers;
using Microsoft.Extensions.DependencyInjection;
using ReactiveUI;
namespace InkForge.Desktop.ViewModels;
public class MainViewModel : ReactiveObject
public class MainViewModel : ObservableObject
{
private readonly DocumentManager _documentManager;
public IDock Layout { get; }
public MainViewModel(InkForgeFactory factory)
{
Layout = factory.CreateLayout();
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;
public record class RecentItemViewModel(
DateTimeOffset Created,
string Name,
DateTimeOffset LastUsed
) : ReactiveRecord;
public partial class RecentItemViewModel(
DateTimeOffset created,
string name,
DateTimeOffset lastUsed
) : ObservableObject
{
[ObservableProperty] private DateTimeOffset _created = created;
[ObservableProperty] private string _name = name;
[ObservableProperty] private DateTimeOffset _lastUsed = lastUsed;
}

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.Models;
using InkForge.Desktop.ViewModels.Workspaces;
using ReactiveUI;
namespace InkForge.Desktop.ViewModels.Tools;
public class WorkspaceTool : Tool
public partial class WorkspaceTool : Tool
{
private WorkspaceViewModel? _workspace;
public WorkspaceViewModel? Workspace
{
get => _workspace;
private set => this.RaiseAndSetIfChanged(ref _workspace, value);
}
private readonly IWorkspaceViewModelFactory _workspaceViewModelFactory;
[ObservableProperty] private WorkspaceViewModel? _workspace;
public WorkspaceTool(WorkspaceManager workspaceManager, IWorkspaceViewModelFactory workspaceViewModelFactory)
{
Title = "Workspace";
_workspaceViewModelFactory = workspaceViewModelFactory;
Title = "Explorer";
CanClose = false;
CanFloat = false;
CanPin = false;
workspaceManager.WhenAnyValue(v => v.Workspace,
v => v switch
{
{ } => workspaceViewModelFactory.Create(v),
_ => null
}).BindTo(this, v => v.Workspace);
workspaceManager.WhenValueChanged(v => v.Workspace).Subscribe(OnWorkspaceManagerWorkspaceChanged);
}
private void OnWorkspaceManagerWorkspaceChanged(Workspace? workspace)
{
Workspace = workspace switch
{
{ } v => _workspaceViewModelFactory.Create(v),
_ => null
};
}
}

View file

@ -1,3 +1,9 @@
using System.Collections.ObjectModel;
using Avalonia;
using DynamicData;
using InkForge.Desktop.Models;
using Microsoft.Extensions.DependencyInjection;
@ -6,30 +12,24 @@ namespace InkForge.Desktop.ViewModels.Workspaces
{
public class WorkspaceViewModel
{
private readonly Workspace _workspace;
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.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
@ -39,14 +39,14 @@ namespace InkForge.Desktop.ViewModels.Workspaces
namespace Internal
{
internal class WorkspaceViewModelFactory(IServiceProvider services) : IWorkspaceViewModelFactory
internal class WorkspaceViewModelFactory : IWorkspaceViewModelFactory
{
private static ObjectFactory<WorkspaceViewModel>? s_workspaceViewModelFactory;
public WorkspaceViewModel Create(Workspace workspace)
public static WorkspaceViewModel Create(Workspace 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);

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">
<MenuItem Header="Create New"
Command="{CompiledBinding CreateNew}" />
Command="{CompiledBinding CreateNewCommand}" />
<MenuItem Header="Open"
IsEnabled="False" />
<MenuItem Header="Open File"
Command="{CompiledBinding OpenNew}" />
Command="{CompiledBinding OpenNewCommand}" />
</Menu>
</Grid>
</UserControl>
</UserControl>

View file

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

View file

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

View file

@ -29,4 +29,4 @@
</Style>
</Style>
</UserControl.Styles>
</UserControl>
</UserControl>

View file

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

View file

@ -12,7 +12,8 @@
<Grid ColumnDefinitions="*, Auto"
RowDefinitions="Auto, *">
<TextBlock Grid.Column="0"
<TextBlock Text="{CompiledBinding Name}"
Grid.Column="0"
Grid.Row="0" />
<StackPanel Classes="WorkspaceToolbar"
@ -29,25 +30,9 @@
<Button>
<inkforge:FluentSymbolIcon Symbol="subtract_square_multiple" />
</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>
<TreeView Grid.ColumnSpan="2"
Grid.Row="1" />
</Grid>
</UserControl>
</UserControl>

View file

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