Merge Common to Desktop

This commit is contained in:
Jöran Malek 2024-02-16 02:23:58 +01:00
parent 26915defe1
commit e9c6e14965
40 changed files with 447 additions and 282 deletions

View file

@ -1,5 +0,0 @@
namespace InkForge.Common.Controllers;
public class WorkspaceController
{
}

View file

@ -1,15 +0,0 @@
using InkForge.Data;
using Microsoft.EntityFrameworkCore;
namespace InkForge.Common.Data;
public class NoteDbContextFactory : IDbContextFactory<NoteDbContext>
{
public NoteDbContext CreateDbContext()
{
return new NoteDbContext(null);
}
}

View file

@ -1,31 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>InkForge</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Controls.DataGrid" />
<PackageReference Include="Avalonia.Fonts.Inter" />
<PackageReference Include="Avalonia.ReactiveUI" />
<PackageReference Include="Avalonia.Themes.Fluent" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Splat.Microsoft.Extensions.DependencyInjection" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\shared\InkForge.Data\InkForge.Data.csproj" />
<ProjectReference Include="..\..\shared\migrations\InkForge.Sqlite\InkForge.Sqlite.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\Settings.json" />
</ItemGroup>
</Project>

View file

@ -1 +0,0 @@
{}

View file

@ -1,21 +0,0 @@
using InkForge.Common.Controllers;
using ReactiveUI;
namespace InkForge.Common.ViewModels;
public class AppViewModel : ReactiveObject
{
private object _view;
public object View
{
get => _view;
set => this.RaiseAndSetIfChanged(ref _view, value);
}
public AppViewModel(WorkspaceController workspace, LandingViewModel landingViewModel)
{
View = landingViewModel;
}
}

View file

@ -1,23 +0,0 @@
using InkForge.Common.Controllers;
using InkForge.Common.ReactiveUI;
using ReactiveUI;
namespace InkForge.Common.ViewModels.Landing;
public class CreateWorkspaceViewModel : LandingViewModelBase
{
public override string? UrlPathSegment => null;
private string workspaceName;
public string WorkspaceName
{
get => workspaceName;
set => this.RaiseAndSetIfChanged(ref workspaceName, value);
}
public CreateWorkspaceViewModel(LandingViewModel landing, WorkspaceController workspace) : base(landing)
{
}
}

View file

@ -1,8 +0,0 @@
using InkForge.Common.ReactiveUI;
namespace InkForge.Common.ViewModels.Landing;
public abstract class LandingViewModelBase(LandingViewModel landing) : RoutableReactiveObject(landing)
{
public LandingViewModel Landing => landing;
}

View file

@ -1,21 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
namespace InkForge.Common.ViewModels.Landing;
public class LandingViewModelFactory(IServiceProvider serviceProvider)
{
public T Create<T>(LandingViewModel landing) where T : LandingViewModelBase
{
LandingViewModelsObjectParameters objectParameters = new(landing);
return TypeFactory<LandingViewModelsObjectParameters, T>.Create(objectParameters, serviceProvider);
}
readonly record struct LandingViewModelsObjectParameters(
LandingViewModel Landing
) : IObjectParameters<LandingViewModelsObjectParameters>
{
public static Type[] Types => [typeof(LandingViewModel)];
public static implicit operator object[](in LandingViewModelsObjectParameters self) => [self.Landing];
}
}

View file

@ -1,17 +0,0 @@
using System.Collections.ObjectModel;
using InkForge.Common.ReactiveUI;
namespace InkForge.Common.ViewModels.Landing;
public class OpenRecentViewModel : LandingViewModelBase
{
public override string? UrlPathSegment => null;
private readonly ReadOnlyObservableCollection<RecentItemViewModel> recentItems;
public ReadOnlyObservableCollection<RecentItemViewModel> RecentItems => recentItems;
public OpenRecentViewModel(LandingViewModel landing) : base(landing)
{
}
}

View file

@ -1,29 +0,0 @@
using System.Reactive.Linq;
using InkForge.Common.ViewModels.Landing;
using Microsoft.Extensions.DependencyInjection;
using ReactiveUI;
namespace InkForge.Common.ViewModels;
public class LandingViewModel : ReactiveObject, IScreen
{
private readonly LandingViewModelFactory _factory;
public RoutingState Router { get; } = new();
public LandingViewModel(LandingViewModelFactory factory)
{
_factory = factory;
Router.CurrentViewModel.Where(x => x is null)
.SelectMany(Observable.Return(factory.Create<OpenRecentViewModel>(this)))
.InvokeCommand<IRoutableViewModel>(Router.NavigateAndReset);
}
public void Navigate<T>() where T : LandingViewModelBase
{
}
}

View file

@ -1,19 +0,0 @@
<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:reactiveui="http://reactiveui.net"
xmlns:ifcvm="using:InkForge.Common.ViewModels"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:Class="InkForge.Common.Views.LandingView"
x:DataType="ifcvm:LandingViewModel">
<Grid RowDefinitions="Auto, *, Auto">
<Label Content="InkForge"
Grid.Row="0" />
<reactiveui:RoutedViewHost Router="{CompiledBinding Router}"
Grid.Row="1" />
</Grid>
</UserControl>

View file

@ -1,13 +0,0 @@
using Avalonia.ReactiveUI;
using InkForge.Common.ViewModels.Landing;
namespace InkForge.Common.Views.LandingViews;
public partial class CreateWorkspaceView : ReactiveUserControl<CreateWorkspaceViewModel>
{
public CreateWorkspaceView()
{
InitializeComponent();
}
}

View file

@ -1,13 +0,0 @@
using Avalonia.ReactiveUI;
using InkForge.Common.ViewModels.Landing;
namespace InkForge.Common.Views.LandingViews;
public partial class OpenRecentView : ReactiveUserControl<OpenRecentViewModel>
{
public OpenRecentView()
{
InitializeComponent();
}
}

View file

@ -1,6 +1,6 @@
<Application xmlns="https://github.com/avaloniaui" <Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="InkForge.Common.App" x:Class="InkForge.Desktop.App"
RequestedThemeVariant="Default"> RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. --> <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->

View file

@ -3,7 +3,7 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using InkForge.Common.ViewModels; using InkForge.Desktop.ViewModels;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -14,7 +14,7 @@ using ReactiveUI;
using Splat; using Splat;
using Splat.Microsoft.Extensions.DependencyInjection; using Splat.Microsoft.Extensions.DependencyInjection;
namespace InkForge.Common; namespace InkForge.Desktop;
public partial class App : Application public partial class App : Application
{ {
@ -30,15 +30,15 @@ public partial class App : Application
configuration.SetBasePath(AppContext.BaseDirectory); configuration.SetBasePath(AppContext.BaseDirectory);
configuration.AddJsonFile( configuration.AddJsonFile(
new ManifestEmbeddedFileProvider(typeof(App).Assembly), new ManifestEmbeddedFileProvider(typeof(App).Assembly),
"Properties/Settings.json", false, false); "Properties/appsettings.json", false, false);
configuration.AddJsonFile( configuration.AddJsonFile(
Path.Combine( Path.Combine(
Environment.GetFolderPath( Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData, Environment.SpecialFolder.ApplicationData,
Environment.SpecialFolderOption.DoNotVerify), Environment.SpecialFolderOption.DoNotVerify),
"InkForge", "InkForge",
"UserSettings.json"), true, true); "usersettings.json"), true, true);
configuration.AddJsonFile("Settings.json", true, true); configuration.AddJsonFile("appsettings.json", true, true);
services.UseMicrosoftDependencyResolver(); services.UseMicrosoftDependencyResolver();
Locator.CurrentMutable.InitializeSplat(); Locator.CurrentMutable.InitializeSplat();

View file

@ -0,0 +1,68 @@
using InkForge.Data;
using InkForge.Desktop.Models;
using InkForge.Desktop.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ReactiveUI;
namespace InkForge.Desktop.Controllers;
public class WorkspaceController : ReactiveObject
{
private readonly IServiceProvider _serviceProvider;
private Workspace _workspace;
public Workspace Workspace
{
get => _workspace;
set => this.RaiseAndSetIfChanged(ref _workspace, value);
}
public WorkspaceController(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task OpenWorkspace(string path, bool createFile = false)
{
if (await CreateWorkspace(path, createFile) is { } workspace)
{
Workspace = workspace;
}
}
private async ValueTask<Workspace?> CreateWorkspace(string path, bool createFile)
{
FileInfo file = new(path);
if (!(createFile || file.Exists))
{
return null;
}
file.Directory!.Create();
var scope = _serviceProvider.CreateScope();
var scopeServiceProvider = scope.ServiceProvider;
var context = scope.ServiceProvider.GetRequiredService<WorkspaceContext>();
context.DbPath = path;
var db = scopeServiceProvider.GetRequiredService<NoteDbContext>();
await using (var transaction = await db.Database.BeginTransactionAsync().ConfigureAwait(false))
{
try
{
await db.Database.MigrateAsync().ConfigureAwait(false);
}
catch
{
await transaction.RollbackAsync().ConfigureAwait(false);
return null;
}
await transaction.CommitAsync().ConfigureAwait(false);
}
return new(scope);
}
}

View file

@ -0,0 +1,27 @@
using InkForge.Desktop.Services;
using InkForge.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using SmartFormat;
namespace InkForge.Desktop.Data;
public class NoteDbContextFactory(WorkspaceContext context, IConfiguration configuration) : IDbContextFactory<NoteDbContext>
{
private string? _connectionString;
public NoteDbContext CreateDbContext()
{
_connectionString ??= Smart.Format(configuration.GetConnectionString("DefaultConnection")!, new
{
WorkspaceFile = context.DbPath
});
DbContextOptionsBuilder<NoteDbContext> builder = new();
builder.UseSqlite(_connectionString, o => o.MigrationsAssembly("InkForge.Sqlite"));
return new NoteDbContext(builder.Options);
}
}

View file

@ -6,16 +6,32 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>InkForge</AssemblyName> <AssemblyName>InkForge</AssemblyName>
<RootNamespace>InkForge.Desktop</RootNamespace>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Controls.DataGrid" />
<PackageReference Include="Avalonia.Desktop" /> <PackageReference Include="Avalonia.Desktop" />
<PackageReference Include="Avalonia.Fonts.Inter" />
<PackageReference Include="Avalonia.ReactiveUI" />
<PackageReference Include="Avalonia.Themes.Fluent" />
<PackageReference Include="Dock.Avalonia" /> <PackageReference Include="Dock.Avalonia" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" /> <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.Microsoft.Extensions.DependencyInjection" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\InkForge.Common\InkForge.Common.csproj" /> <ProjectReference Include="..\..\shared\InkForge.Data\InkForge.Data.csproj" />
<ProjectReference Include="..\..\shared\migrations\InkForge.Sqlite\InkForge.Sqlite.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\appsettings.json" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,9 +1,11 @@
using InkForge.Common.Controllers; using InkForge.Desktop.Controllers;
using InkForge.Common.Data; using InkForge.Desktop.Data;
using InkForge.Common.ViewModels; using InkForge.Desktop.Services;
using InkForge.Common.ViewModels.Landing; using InkForge.Desktop.ViewModels;
using InkForge.Data; using InkForge.Data;
using Microsoft.EntityFrameworkCore;
using ReactiveUI; using ReactiveUI;
using Splat; using Splat;
@ -16,10 +18,12 @@ public static class InkForgeServiceCollections
{ {
services.AddHttpClient(); services.AddHttpClient();
services.AddDbContextFactory<NoteDbContext, NoteDbContextFactory>(); services.AddScoped<IDbContextFactory<NoteDbContext>, NoteDbContextFactory>();
services.AddScoped(s => s.GetRequiredService<IDbContextFactory<NoteDbContext>>().CreateDbContext());
services.AddScoped<WorkspaceContext>();
services.AddSingleton<LandingViewModel>(); services.AddSingleton<LandingViewModel>();
services.AddSingleton<LandingViewModelFactory>();
services.AddSingleton<WorkspaceController>(); services.AddSingleton<WorkspaceController>();
Locator.CurrentMutable.RegisterViewsForViewModels(typeof(InkForgeServiceCollections).Assembly); Locator.CurrentMutable.RegisterViewsForViewModels(typeof(InkForgeServiceCollections).Assembly);

View file

@ -0,0 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
namespace InkForge.Desktop.Models;
public class Workspace(IServiceScope scope)
{
public IServiceProvider ServiceProvider => scope.ServiceProvider;
}

View file

@ -3,29 +3,18 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading; using Avalonia.Threading;
using InkForge.Common; using InkForge.Desktop;
using InkForge.Common.ViewModels;
using InkForge.Desktop.Views;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using ReactiveUI;
static class Program static class Program
{ {
private static readonly ConfigurationManager Configuration = new();
[STAThread] [STAThread]
public static void Main(string[] args) public static void Main(string[] args)
=> BuildAvaloniaApp() => BuildAvaloniaApp()
.UseMicrosoftDependencyInjection() .UseMicrosoftDependencyInjection(out var configuration)
.StartWithClassicDesktopLifetime(args, WithMicrosoftDependencyInjection); .StartWithClassicDesktopLifetime(args, configuration.WithMicrosoftDependencyInjection);
private static void WithMicrosoftDependencyInjection(IClassicDesktopStyleApplicationLifetime lifetime)
{
Configuration.AddCommandLine(lifetime.Args ?? []);
}
public static AppBuilder BuildAvaloniaApp() public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>() => AppBuilder.Configure<App>()
@ -34,12 +23,7 @@ static class Program
.WithInterFont() .WithInterFont()
.LogToTrace(); .LogToTrace();
private static void ConfigureServices(IServiceCollection services) private static void SetupApp(this IServiceCollection services, AppBuilder appBuilder)
{
services.AddTransient<IViewFor<AppViewModel>, MainWindow>();
}
private static void OnSetup(this IServiceCollection services, AppBuilder appBuilder)
{ {
var dispatcher = Dispatcher.UIThread; var dispatcher = Dispatcher.UIThread;
var app = appBuilder.Instance!; var app = appBuilder.Instance!;
@ -49,19 +33,52 @@ static class Program
.AddSingleton(app.PlatformSettings!) .AddSingleton(app.PlatformSettings!)
.AddSingleton(dispatcher); .AddSingleton(dispatcher);
ConfigureServices(services);
var serviceProvider = services.BuildServiceProvider(); var serviceProvider = services.BuildServiceProvider();
app.SetValue(App.ServiceProviderProperty, serviceProvider); app.SetValue(App.ServiceProviderProperty, serviceProvider);
dispatcher.ShutdownFinished += (_, _) => serviceProvider.Dispose(); _ = new ServiceProviderDisposer(serviceProvider, dispatcher);
} }
private static AppBuilder UseMicrosoftDependencyInjection(this AppBuilder builder) private static AppBuilder UseMicrosoftDependencyInjection(this AppBuilder builder, out ConfigurationManager configuration)
{ {
configuration = new();
ServiceCollection services = []; ServiceCollection services = [];
App.Configure(services, Configuration); services.AddSingleton<IConfiguration>(configuration);
App.Configure(services, configuration);
builder.AfterSetup(services.OnSetup); builder.AfterSetup(services.SetupApp);
return builder; return builder;
} }
private static void WithMicrosoftDependencyInjection(this ConfigurationManager configuration, IClassicDesktopStyleApplicationLifetime lifetime)
{
configuration.AddCommandLine(lifetime.Args ?? []);
}
private class ServiceProviderDisposer
{
private readonly ServiceProvider _serviceProvider;
private readonly TaskCompletionSource<ValueTask> _shutdownTask = new();
public ServiceProviderDisposer(ServiceProvider serviceProvider, Dispatcher dispatcher)
{
dispatcher.ShutdownStarted += OnShutdownStarted;
dispatcher.ShutdownFinished += OnShutdownFinished;
_serviceProvider = serviceProvider;
}
private void OnShutdownFinished(object? sender, EventArgs e)
{
if (_shutdownTask.Task.Result is { IsCompleted: false } disposeTask)
{
disposeTask.GetAwaiter().GetResult();
}
}
private void OnShutdownStarted(object? sender, EventArgs e)
{
#pragma warning disable CA2012 // This will only ever be awaited once in ShutdownFinished
_shutdownTask.SetResult(_serviceProvider.DisposeAsync());
#pragma warning restore CA2012
}
}
} }

View file

@ -1,4 +1,4 @@
namespace InkForge.Common.Properties; namespace InkForge.Desktop.Properties;
public class ApplicationSettings public class ApplicationSettings
{ {

View file

@ -1,6 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace InkForge.Common.Properties; namespace InkForge.Desktop.Properties;
[JsonSerializable(typeof(ApplicationSettings))] [JsonSerializable(typeof(ApplicationSettings))]
[JsonSerializable(typeof(IDictionary<string, object>))] [JsonSerializable(typeof(IDictionary<string, object>))]

View file

@ -0,0 +1,5 @@
{
"ConnectionStrings": {
"DefaultConnection": "Data Source={WorkspaceFile}"
}
}

View file

@ -1,6 +1,6 @@
using ReactiveUI; using ReactiveUI;
namespace InkForge.Common.ReactiveUI; namespace InkForge.Desktop.ReactiveUI;
public abstract class RoutableReactiveObject(IScreen screen) : ReactiveObject, IRoutableViewModel public abstract class RoutableReactiveObject(IScreen screen) : ReactiveObject, IRoutableViewModel
{ {

View file

@ -1,6 +1,6 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace InkForge.Common.ReactiveUI; namespace InkForge.Desktop.ReactiveUI;
public interface IViewModelFactory<T, TCreator> public interface IViewModelFactory<T, TCreator>
{ {

View file

@ -0,0 +1,13 @@
using Avalonia.Platform.Storage;
namespace InkForge.Desktop.Services;
public static class StorageProviderExtensions
{
public static IStorageProvider? GetStorageProvider(this object? context)
{
ArgumentNullException.ThrowIfNull(context);
return TopLevels.GetTopLevelForContext(context)?.StorageProvider;
}
}

View file

@ -0,0 +1,54 @@
using Avalonia;
using Avalonia.Controls;
namespace InkForge.Desktop.Services;
public class TopLevels
{
public static readonly AttachedProperty<object?> RegisterProperty
= AvaloniaProperty.RegisterAttached<TopLevels, Visual, object?>("Register");
private static readonly Dictionary<object, Visual> RegistrationMapper = [];
static TopLevels()
{
RegisterProperty.Changed.AddClassHandler<Visual>(RegisterChanged);
}
public static object? GetRegister(AvaloniaObject element)
{
return element.GetValue(RegisterProperty);
}
public static TopLevel? GetTopLevelForContext(object context)
{
return TopLevel.GetTopLevel(GetVisualForContext(context));
}
public static Visual? GetVisualForContext(object context)
{
return RegistrationMapper.TryGetValue(context, out var result) ? result : null;
}
public static void SetRegister(AvaloniaObject element, object value)
{
element.SetValue(RegisterProperty, value);
}
private static void RegisterChanged(Visual sender, AvaloniaPropertyChangedEventArgs e)
{
ArgumentNullException.ThrowIfNull(sender);
// Unregister any old registered context
if (e.OldValue != null)
{
RegistrationMapper.Remove(e.OldValue);
}
// Register any new context
if (e.NewValue != null)
{
RegistrationMapper.Add(e.NewValue, sender);
}
}
}

View file

@ -0,0 +1,6 @@
namespace InkForge.Desktop.Services;
public class WorkspaceContext
{
public string DbPath { get; set; }
}

View file

@ -0,0 +1,36 @@
using InkForge.Desktop.Controllers;
using InkForge.Desktop.Models;
using ReactiveUI;
namespace InkForge.Desktop.ViewModels;
public class AppViewModel : ReactiveObject
{
private readonly LandingViewModel _landingViewModel;
private readonly WorkspaceController _workspace;
private object _view;
public object View
{
get => _view;
set => this.RaiseAndSetIfChanged(ref _view, value);
}
public AppViewModel(WorkspaceController workspace, LandingViewModel landingViewModel)
{
_workspace = workspace;
_landingViewModel = landingViewModel;
this.WhenAnyValue(v => v._workspace.Workspace).Subscribe(OnWorkspaceChanged);
}
private void OnWorkspaceChanged(Workspace workspace)
{
View = workspace switch
{
null => _landingViewModel,
{ } => new WorkspaceViewModel(workspace) // scoped?
};
}
}

View file

@ -0,0 +1,89 @@
using System.Collections.ObjectModel;
using System.Reactive;
using Avalonia.Platform.Storage;
using InkForge.Desktop.Controllers;
using InkForge.Desktop.Services;
using ReactiveUI;
namespace InkForge.Desktop.ViewModels;
public class LandingViewModel : ReactiveObject
{
private ReadOnlyObservableCollection<RecentItemViewModel> _recentItems;
private readonly WorkspaceController _workspaceController;
public ReactiveCommand<Unit, Unit> CreateNew { get; }
public ReactiveCommand<Unit, Unit> OpenNew { get; }
public ReadOnlyObservableCollection<RecentItemViewModel> RecentItems => _recentItems;
public LandingViewModel(WorkspaceController workspaceController)
{
_workspaceController = workspaceController;
CreateNew = ReactiveCommand.CreateFromTask(OnCreateNew);
OpenNew = ReactiveCommand.CreateFromTask(OnOpenNew);
}
private async Task OnCreateNew()
{
var storageProvider = this.GetStorageProvider()!;
var documents = await storageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents);
var file = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions()
{
DefaultExtension = ".ifdb",
FileTypeChoices =
[
new FilePickerFileType("InkForge Database File")
{
Patterns = [ "*.ifdb" ],
},
],
SuggestedStartLocation = documents,
Title = "Select InkForge Database Name",
});
if (file?.TryGetLocalPath() is not { } filePath)
{
return;
}
await _workspaceController.OpenWorkspace(filePath, true);
}
private async Task OnOpenNew()
{
var storageProvider = this.GetStorageProvider()!;
var documents = await storageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents);
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions()
{
AllowMultiple = false,
SuggestedStartLocation = documents,
FileTypeFilter =
[
new FilePickerFileType("InkForge Database File")
{
Patterns = [ "*.ifdb" ]
}
],
Title = "Open InkForge Database file"
});
if (files.Count != 1)
{
return;
}
if (files[0].TryGetLocalPath() is not { } filePath)
{
return;
}
await _workspaceController.OpenWorkspace(filePath, false);
}
}

View file

@ -1,6 +1,6 @@
using ReactiveUI; using ReactiveUI;
namespace InkForge.Common.ViewModels.Landing; namespace InkForge.Desktop.ViewModels;
public record class RecentItemViewModel( public record class RecentItemViewModel(
DateTimeOffset Created, DateTimeOffset Created,

View file

@ -0,0 +1,15 @@
using InkForge.Desktop.Models;
using ReactiveUI;
namespace InkForge.Desktop.ViewModels;
public class WorkspaceViewModel : ReactiveObject
{
private readonly Workspace _workspace;
public WorkspaceViewModel(Workspace workspace)
{
_workspace = workspace;
}
}

View file

@ -2,17 +2,21 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:InkForge.Common.ViewModels.Landing" xmlns:reactiveui="http://reactiveui.net"
xmlns:vm="using:InkForge.Desktop.ViewModels"
xmlns:services="using:InkForge.Desktop.Services"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignWidth="800" d:DesignWidth="800"
d:DesignHeight="450" d:DesignHeight="450"
x:Class="InkForge.Common.Views.LandingViews.OpenRecentView" x:Class="InkForge.Desktop.Views.LandingView"
x:DataType="vm:OpenRecentViewModel"> x:DataType="vm:LandingViewModel"
<Grid RowDefinitions="Auto, *"> services:TopLevels.Register="{CompiledBinding}">
<Grid RowDefinitions="Auto, *, Auto">
<Label Content="Open Recent" <Label Content="Open Recent"
Grid.Row="0" /> Grid.Row="0" />
<DataGrid IsReadOnly="true" <DataGrid IsEnabled="False"
IsReadOnly="True"
ItemsSource="{CompiledBinding RecentItems}" ItemsSource="{CompiledBinding RecentItems}"
Grid.Row="1"> Grid.Row="1">
<DataGrid.Columns> <DataGrid.Columns>
@ -25,5 +29,14 @@
Binding="{CompiledBinding LastUsed, StringFormat={}{0:d}}" /> Binding="{CompiledBinding LastUsed, StringFormat={}{0:d}}" />
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
<Menu Grid.Row="2">
<MenuItem Header="Create New"
Command="{CompiledBinding CreateNew}" />
<MenuItem Header="Open"
IsEnabled="False" />
<MenuItem Header="Open File"
Command="{CompiledBinding OpenNew}" />
</Menu>
</Grid> </Grid>
</UserControl> </UserControl>

View file

@ -1,8 +1,8 @@
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using InkForge.Common.ViewModels; using InkForge.Desktop.ViewModels;
namespace InkForge.Common.Views; namespace InkForge.Desktop.Views;
public partial class LandingView : ReactiveUserControl<LandingViewModel> public partial class LandingView : ReactiveUserControl<LandingViewModel>
{ {

View file

@ -3,19 +3,13 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:reactiveui="http://reactiveui.net" xmlns:reactiveui="http://reactiveui.net"
xmlns:ifcvm="using:InkForge.Common.ViewModels" xmlns:vm="using:InkForge.Desktop.ViewModels"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignWidth="800" d:DesignWidth="800"
d:DesignHeight="450" d:DesignHeight="450"
x:Class="InkForge.Desktop.Views.MainWindow" x:Class="InkForge.Desktop.Views.MainWindow"
x:DataType="ifcvm:AppViewModel" x:DataType="vm:AppViewModel"
Title="InkForge"> Title="MainWindow">
<NativeMenu.Menu>
<NativeMenu>
<NativeMenuItem Header="Test" />
</NativeMenu>
</NativeMenu.Menu>
<DockPanel> <DockPanel>
<NativeMenuBar /> <NativeMenuBar />

View file

@ -1,6 +1,6 @@
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using InkForge.Common.ViewModels; using InkForge.Desktop.ViewModels;
namespace InkForge.Desktop.Views; namespace InkForge.Desktop.Views;

View file

@ -2,9 +2,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:reactiveui="http://reactiveui.net"
xmlns:vm="using:InkForge.Desktop.ViewModels"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignWidth="800" d:DesignWidth="800"
d:DesignHeight="450" d:DesignHeight="450"
x:Class="InkForge.Common.Views.LandingViews.CreateWorkspaceView"> x:Class="InkForge.Desktop.Views.WorkspaceView"
x:DataType="vm:WorkspaceViewModel">
Welcome to Avalonia! Welcome to Avalonia!
</UserControl> </UserControl>

View file

@ -0,0 +1,13 @@
using Avalonia.ReactiveUI;
using InkForge.Desktop.ViewModels;
namespace InkForge.Desktop.Views;
public partial class WorkspaceView : ReactiveUserControl<WorkspaceViewModel>
{
public WorkspaceView()
{
InitializeComponent();
}
}