diff --git a/app/InkForge.Common/Controllers/WorkspaceController.cs b/app/InkForge.Common/Controllers/WorkspaceController.cs deleted file mode 100644 index 01c8be3..0000000 --- a/app/InkForge.Common/Controllers/WorkspaceController.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace InkForge.Common.Controllers; - -public class WorkspaceController -{ -} diff --git a/app/InkForge.Common/Data/NoteDbContextFactory.cs b/app/InkForge.Common/Data/NoteDbContextFactory.cs deleted file mode 100644 index 8de299e..0000000 --- a/app/InkForge.Common/Data/NoteDbContextFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -using InkForge.Data; - -using Microsoft.EntityFrameworkCore; - -namespace InkForge.Common.Data; - -public class NoteDbContextFactory : IDbContextFactory -{ - - public NoteDbContext CreateDbContext() - { - return new NoteDbContext(null); - } - -} diff --git a/app/InkForge.Common/InkForge.Common.csproj b/app/InkForge.Common/InkForge.Common.csproj deleted file mode 100644 index 1225e41..0000000 --- a/app/InkForge.Common/InkForge.Common.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - net8.0 - InkForge - enable - enable - true - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/InkForge.Common/Properties/Settings.json b/app/InkForge.Common/Properties/Settings.json deleted file mode 100644 index 9e26dfe..0000000 --- a/app/InkForge.Common/Properties/Settings.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/app/InkForge.Common/ViewModels/AppViewModel.cs b/app/InkForge.Common/ViewModels/AppViewModel.cs deleted file mode 100644 index 05adbf7..0000000 --- a/app/InkForge.Common/ViewModels/AppViewModel.cs +++ /dev/null @@ -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; - } -} diff --git a/app/InkForge.Common/ViewModels/Landing/CreateWorkspaceViewModel.cs b/app/InkForge.Common/ViewModels/Landing/CreateWorkspaceViewModel.cs deleted file mode 100644 index edc9254..0000000 --- a/app/InkForge.Common/ViewModels/Landing/CreateWorkspaceViewModel.cs +++ /dev/null @@ -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) - { - } -} diff --git a/app/InkForge.Common/ViewModels/Landing/LandingViewModelBase.cs b/app/InkForge.Common/ViewModels/Landing/LandingViewModelBase.cs deleted file mode 100644 index 4d5fff3..0000000 --- a/app/InkForge.Common/ViewModels/Landing/LandingViewModelBase.cs +++ /dev/null @@ -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; -} diff --git a/app/InkForge.Common/ViewModels/Landing/LandingViewModelFactory.cs b/app/InkForge.Common/ViewModels/Landing/LandingViewModelFactory.cs deleted file mode 100644 index e6b0866..0000000 --- a/app/InkForge.Common/ViewModels/Landing/LandingViewModelFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace InkForge.Common.ViewModels.Landing; - -public class LandingViewModelFactory(IServiceProvider serviceProvider) -{ - public T Create(LandingViewModel landing) where T : LandingViewModelBase - { - LandingViewModelsObjectParameters objectParameters = new(landing); - return TypeFactory.Create(objectParameters, serviceProvider); - } - - readonly record struct LandingViewModelsObjectParameters( - LandingViewModel Landing - ) : IObjectParameters - { - public static Type[] Types => [typeof(LandingViewModel)]; - - public static implicit operator object[](in LandingViewModelsObjectParameters self) => [self.Landing]; - } -} diff --git a/app/InkForge.Common/ViewModels/Landing/OpenRecentViewModel.cs b/app/InkForge.Common/ViewModels/Landing/OpenRecentViewModel.cs deleted file mode 100644 index 3d1464f..0000000 --- a/app/InkForge.Common/ViewModels/Landing/OpenRecentViewModel.cs +++ /dev/null @@ -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 recentItems; - - public ReadOnlyObservableCollection RecentItems => recentItems; - - public OpenRecentViewModel(LandingViewModel landing) : base(landing) - { - } -} diff --git a/app/InkForge.Common/ViewModels/LandingViewModel.cs b/app/InkForge.Common/ViewModels/LandingViewModel.cs deleted file mode 100644 index 7a1154c..0000000 --- a/app/InkForge.Common/ViewModels/LandingViewModel.cs +++ /dev/null @@ -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(this))) - .InvokeCommand(Router.NavigateAndReset); - } - - public void Navigate() where T : LandingViewModelBase - { - } -} diff --git a/app/InkForge.Common/Views/LandingView.axaml b/app/InkForge.Common/Views/LandingView.axaml deleted file mode 100644 index 1f0d60c..0000000 --- a/app/InkForge.Common/Views/LandingView.axaml +++ /dev/null @@ -1,19 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/InkForge.Common/Views/LandingViews/CreateWorkspaceView.axaml.cs b/app/InkForge.Common/Views/LandingViews/CreateWorkspaceView.axaml.cs deleted file mode 100644 index f9621f2..0000000 --- a/app/InkForge.Common/Views/LandingViews/CreateWorkspaceView.axaml.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Avalonia.ReactiveUI; - -using InkForge.Common.ViewModels.Landing; - -namespace InkForge.Common.Views.LandingViews; - -public partial class CreateWorkspaceView : ReactiveUserControl -{ - public CreateWorkspaceView() - { - InitializeComponent(); - } -} diff --git a/app/InkForge.Common/Views/LandingViews/OpenRecentView.axaml.cs b/app/InkForge.Common/Views/LandingViews/OpenRecentView.axaml.cs deleted file mode 100644 index 417e757..0000000 --- a/app/InkForge.Common/Views/LandingViews/OpenRecentView.axaml.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Avalonia.ReactiveUI; - -using InkForge.Common.ViewModels.Landing; - -namespace InkForge.Common.Views.LandingViews; - -public partial class OpenRecentView : ReactiveUserControl -{ - public OpenRecentView() - { - InitializeComponent(); - } -} diff --git a/app/InkForge.Common/App.axaml b/app/InkForge.Desktop/App.axaml similarity index 91% rename from app/InkForge.Common/App.axaml rename to app/InkForge.Desktop/App.axaml index 84f7844..72522b3 100644 --- a/app/InkForge.Common/App.axaml +++ b/app/InkForge.Desktop/App.axaml @@ -1,6 +1,6 @@ diff --git a/app/InkForge.Common/App.axaml.cs b/app/InkForge.Desktop/App.axaml.cs similarity index 90% rename from app/InkForge.Common/App.axaml.cs rename to app/InkForge.Desktop/App.axaml.cs index 93890c3..4fa264c 100644 --- a/app/InkForge.Common/App.axaml.cs +++ b/app/InkForge.Desktop/App.axaml.cs @@ -3,7 +3,7 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; -using InkForge.Common.ViewModels; +using InkForge.Desktop.ViewModels; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -14,7 +14,7 @@ using ReactiveUI; using Splat; using Splat.Microsoft.Extensions.DependencyInjection; -namespace InkForge.Common; +namespace InkForge.Desktop; public partial class App : Application { @@ -30,15 +30,15 @@ public partial class App : Application configuration.SetBasePath(AppContext.BaseDirectory); configuration.AddJsonFile( new ManifestEmbeddedFileProvider(typeof(App).Assembly), - "Properties/Settings.json", false, false); + "Properties/appsettings.json", false, false); configuration.AddJsonFile( Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.DoNotVerify), "InkForge", - "UserSettings.json"), true, true); - configuration.AddJsonFile("Settings.json", true, true); + "usersettings.json"), true, true); + configuration.AddJsonFile("appsettings.json", true, true); services.UseMicrosoftDependencyResolver(); Locator.CurrentMutable.InitializeSplat(); diff --git a/app/InkForge.Desktop/Controllers/WorkspaceController.cs b/app/InkForge.Desktop/Controllers/WorkspaceController.cs new file mode 100644 index 0000000..2ee9f0c --- /dev/null +++ b/app/InkForge.Desktop/Controllers/WorkspaceController.cs @@ -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 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(); + context.DbPath = path; + + var db = scopeServiceProvider.GetRequiredService(); + 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); + } +} diff --git a/app/InkForge.Desktop/Data/NoteDbContextFactory.cs b/app/InkForge.Desktop/Data/NoteDbContextFactory.cs new file mode 100644 index 0000000..9f069b9 --- /dev/null +++ b/app/InkForge.Desktop/Data/NoteDbContextFactory.cs @@ -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 +{ + private string? _connectionString; + + public NoteDbContext CreateDbContext() + { + _connectionString ??= Smart.Format(configuration.GetConnectionString("DefaultConnection")!, new + { + WorkspaceFile = context.DbPath + }); + + DbContextOptionsBuilder builder = new(); + builder.UseSqlite(_connectionString, o => o.MigrationsAssembly("InkForge.Sqlite")); + + return new NoteDbContext(builder.Options); + } +} diff --git a/app/InkForge.Desktop/InkForge.Desktop.csproj b/app/InkForge.Desktop/InkForge.Desktop.csproj index 04417cc..3540cfa 100644 --- a/app/InkForge.Desktop/InkForge.Desktop.csproj +++ b/app/InkForge.Desktop/InkForge.Desktop.csproj @@ -6,16 +6,32 @@ enable enable InkForge + InkForge.Desktop + true + + + + + + + + + - + + + + + + \ No newline at end of file diff --git a/app/InkForge.Common/Microsoft/Extensions/DependencyInjection/InkForgeServiceCollection.cs b/app/InkForge.Desktop/Microsoft/Extensions/DependencyInjection/InkForgeServiceCollection.cs similarity index 55% rename from app/InkForge.Common/Microsoft/Extensions/DependencyInjection/InkForgeServiceCollection.cs rename to app/InkForge.Desktop/Microsoft/Extensions/DependencyInjection/InkForgeServiceCollection.cs index a30e906..96dc72d 100644 --- a/app/InkForge.Common/Microsoft/Extensions/DependencyInjection/InkForgeServiceCollection.cs +++ b/app/InkForge.Desktop/Microsoft/Extensions/DependencyInjection/InkForgeServiceCollection.cs @@ -1,9 +1,11 @@ -using InkForge.Common.Controllers; -using InkForge.Common.Data; -using InkForge.Common.ViewModels; -using InkForge.Common.ViewModels.Landing; +using InkForge.Desktop.Controllers; +using InkForge.Desktop.Data; +using InkForge.Desktop.Services; +using InkForge.Desktop.ViewModels; using InkForge.Data; +using Microsoft.EntityFrameworkCore; + using ReactiveUI; using Splat; @@ -16,10 +18,12 @@ public static class InkForgeServiceCollections { services.AddHttpClient(); - services.AddDbContextFactory(); + services.AddScoped, NoteDbContextFactory>(); + services.AddScoped(s => s.GetRequiredService>().CreateDbContext()); + + services.AddScoped(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); Locator.CurrentMutable.RegisterViewsForViewModels(typeof(InkForgeServiceCollections).Assembly); diff --git a/app/InkForge.Common/Microsoft/Extensions/DependencyInjection/TypeFactories.cs b/app/InkForge.Desktop/Microsoft/Extensions/DependencyInjection/TypeFactories.cs similarity index 100% rename from app/InkForge.Common/Microsoft/Extensions/DependencyInjection/TypeFactories.cs rename to app/InkForge.Desktop/Microsoft/Extensions/DependencyInjection/TypeFactories.cs diff --git a/app/InkForge.Desktop/Models/Workspace.cs b/app/InkForge.Desktop/Models/Workspace.cs new file mode 100644 index 0000000..d7e422c --- /dev/null +++ b/app/InkForge.Desktop/Models/Workspace.cs @@ -0,0 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace InkForge.Desktop.Models; + +public class Workspace(IServiceScope scope) +{ + public IServiceProvider ServiceProvider => scope.ServiceProvider; +} diff --git a/app/InkForge.Desktop/Program.cs b/app/InkForge.Desktop/Program.cs index cfb5987..af21245 100644 --- a/app/InkForge.Desktop/Program.cs +++ b/app/InkForge.Desktop/Program.cs @@ -3,29 +3,18 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.ReactiveUI; using Avalonia.Threading; -using InkForge.Common; -using InkForge.Common.ViewModels; -using InkForge.Desktop.Views; +using InkForge.Desktop; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using ReactiveUI; - static class Program { - private static readonly ConfigurationManager Configuration = new(); - [STAThread] public static void Main(string[] args) => BuildAvaloniaApp() - .UseMicrosoftDependencyInjection() - .StartWithClassicDesktopLifetime(args, WithMicrosoftDependencyInjection); - - private static void WithMicrosoftDependencyInjection(IClassicDesktopStyleApplicationLifetime lifetime) - { - Configuration.AddCommandLine(lifetime.Args ?? []); - } + .UseMicrosoftDependencyInjection(out var configuration) + .StartWithClassicDesktopLifetime(args, configuration.WithMicrosoftDependencyInjection); public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() @@ -34,12 +23,7 @@ static class Program .WithInterFont() .LogToTrace(); - private static void ConfigureServices(IServiceCollection services) - { - services.AddTransient, MainWindow>(); - } - - private static void OnSetup(this IServiceCollection services, AppBuilder appBuilder) + private static void SetupApp(this IServiceCollection services, AppBuilder appBuilder) { var dispatcher = Dispatcher.UIThread; var app = appBuilder.Instance!; @@ -49,19 +33,52 @@ static class Program .AddSingleton(app.PlatformSettings!) .AddSingleton(dispatcher); - ConfigureServices(services); - var serviceProvider = services.BuildServiceProvider(); 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 = []; - App.Configure(services, Configuration); + services.AddSingleton(configuration); + App.Configure(services, configuration); - builder.AfterSetup(services.OnSetup); + builder.AfterSetup(services.SetupApp); 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 _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 + } + } } diff --git a/app/InkForge.Common/Properties/ApplicationSettings.cs b/app/InkForge.Desktop/Properties/ApplicationSettings.cs similarity index 67% rename from app/InkForge.Common/Properties/ApplicationSettings.cs rename to app/InkForge.Desktop/Properties/ApplicationSettings.cs index 3a71946..50047b5 100644 --- a/app/InkForge.Common/Properties/ApplicationSettings.cs +++ b/app/InkForge.Desktop/Properties/ApplicationSettings.cs @@ -1,4 +1,4 @@ -namespace InkForge.Common.Properties; +namespace InkForge.Desktop.Properties; public class ApplicationSettings { diff --git a/app/InkForge.Common/Properties/ConfigContext.cs b/app/InkForge.Desktop/Properties/ConfigContext.cs similarity index 88% rename from app/InkForge.Common/Properties/ConfigContext.cs rename to app/InkForge.Desktop/Properties/ConfigContext.cs index 621c320..730cecf 100644 --- a/app/InkForge.Common/Properties/ConfigContext.cs +++ b/app/InkForge.Desktop/Properties/ConfigContext.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace InkForge.Common.Properties; +namespace InkForge.Desktop.Properties; [JsonSerializable(typeof(ApplicationSettings))] [JsonSerializable(typeof(IDictionary))] diff --git a/app/InkForge.Desktop/Properties/appsettings.json b/app/InkForge.Desktop/Properties/appsettings.json new file mode 100644 index 0000000..280d31c --- /dev/null +++ b/app/InkForge.Desktop/Properties/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source={WorkspaceFile}" + } +} \ No newline at end of file diff --git a/app/InkForge.Common/ReactiveUI/RoutableReactiveObject.cs b/app/InkForge.Desktop/ReactiveUI/RoutableReactiveObject.cs similarity index 84% rename from app/InkForge.Common/ReactiveUI/RoutableReactiveObject.cs rename to app/InkForge.Desktop/ReactiveUI/RoutableReactiveObject.cs index 3b1e147..417591a 100644 --- a/app/InkForge.Common/ReactiveUI/RoutableReactiveObject.cs +++ b/app/InkForge.Desktop/ReactiveUI/RoutableReactiveObject.cs @@ -1,6 +1,6 @@ using ReactiveUI; -namespace InkForge.Common.ReactiveUI; +namespace InkForge.Desktop.ReactiveUI; public abstract class RoutableReactiveObject(IScreen screen) : ReactiveObject, IRoutableViewModel { diff --git a/app/InkForge.Common/ReactiveUI/ViewModelFactory.cs b/app/InkForge.Desktop/ReactiveUI/ViewModelFactory.cs similarity index 94% rename from app/InkForge.Common/ReactiveUI/ViewModelFactory.cs rename to app/InkForge.Desktop/ReactiveUI/ViewModelFactory.cs index bcbc69c..0cdca18 100644 --- a/app/InkForge.Common/ReactiveUI/ViewModelFactory.cs +++ b/app/InkForge.Desktop/ReactiveUI/ViewModelFactory.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -namespace InkForge.Common.ReactiveUI; +namespace InkForge.Desktop.ReactiveUI; public interface IViewModelFactory { diff --git a/app/InkForge.Desktop/Services/DialogHelper.cs b/app/InkForge.Desktop/Services/DialogHelper.cs new file mode 100644 index 0000000..7434151 --- /dev/null +++ b/app/InkForge.Desktop/Services/DialogHelper.cs @@ -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; + } +} diff --git a/app/InkForge.Desktop/Services/TopLevels.cs b/app/InkForge.Desktop/Services/TopLevels.cs new file mode 100644 index 0000000..e6925d9 --- /dev/null +++ b/app/InkForge.Desktop/Services/TopLevels.cs @@ -0,0 +1,54 @@ +using Avalonia; +using Avalonia.Controls; + +namespace InkForge.Desktop.Services; + +public class TopLevels +{ + public static readonly AttachedProperty RegisterProperty + = AvaloniaProperty.RegisterAttached("Register"); + + private static readonly Dictionary RegistrationMapper = []; + + static TopLevels() + { + RegisterProperty.Changed.AddClassHandler(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); + } + } +} diff --git a/app/InkForge.Desktop/Services/WorkspaceContext.cs b/app/InkForge.Desktop/Services/WorkspaceContext.cs new file mode 100644 index 0000000..1cb5043 --- /dev/null +++ b/app/InkForge.Desktop/Services/WorkspaceContext.cs @@ -0,0 +1,6 @@ +namespace InkForge.Desktop.Services; + +public class WorkspaceContext +{ + public string DbPath { get; set; } +} diff --git a/app/InkForge.Desktop/ViewModels/AppViewModel.cs b/app/InkForge.Desktop/ViewModels/AppViewModel.cs new file mode 100644 index 0000000..c0e4100 --- /dev/null +++ b/app/InkForge.Desktop/ViewModels/AppViewModel.cs @@ -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? + }; + } +} diff --git a/app/InkForge.Desktop/ViewModels/LandingViewModel.cs b/app/InkForge.Desktop/ViewModels/LandingViewModel.cs new file mode 100644 index 0000000..6d65bc1 --- /dev/null +++ b/app/InkForge.Desktop/ViewModels/LandingViewModel.cs @@ -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 _recentItems; + private readonly WorkspaceController _workspaceController; + + public ReactiveCommand CreateNew { get; } + + public ReactiveCommand OpenNew { get; } + + public ReadOnlyObservableCollection 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); + } +} diff --git a/app/InkForge.Common/ViewModels/Landing/RecentItemViewModel.cs b/app/InkForge.Desktop/ViewModels/RecentItemViewModel.cs similarity index 75% rename from app/InkForge.Common/ViewModels/Landing/RecentItemViewModel.cs rename to app/InkForge.Desktop/ViewModels/RecentItemViewModel.cs index a15c938..c5dd596 100644 --- a/app/InkForge.Common/ViewModels/Landing/RecentItemViewModel.cs +++ b/app/InkForge.Desktop/ViewModels/RecentItemViewModel.cs @@ -1,6 +1,6 @@ using ReactiveUI; -namespace InkForge.Common.ViewModels.Landing; +namespace InkForge.Desktop.ViewModels; public record class RecentItemViewModel( DateTimeOffset Created, diff --git a/app/InkForge.Desktop/ViewModels/WorkspaceViewModel.cs b/app/InkForge.Desktop/ViewModels/WorkspaceViewModel.cs new file mode 100644 index 0000000..a48b48e --- /dev/null +++ b/app/InkForge.Desktop/ViewModels/WorkspaceViewModel.cs @@ -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; + } +} diff --git a/app/InkForge.Common/Views/LandingViews/OpenRecentView.axaml b/app/InkForge.Desktop/Views/LandingView.axaml similarity index 58% rename from app/InkForge.Common/Views/LandingViews/OpenRecentView.axaml rename to app/InkForge.Desktop/Views/LandingView.axaml index 8a7cbf4..43eeb9d 100644 --- a/app/InkForge.Common/Views/LandingViews/OpenRecentView.axaml +++ b/app/InkForge.Desktop/Views/LandingView.axaml @@ -2,17 +2,21 @@ 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.Common.ViewModels.Landing" + xmlns:reactiveui="http://reactiveui.net" + xmlns:vm="using:InkForge.Desktop.ViewModels" + xmlns:services="using:InkForge.Desktop.Services" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="InkForge.Common.Views.LandingViews.OpenRecentView" - x:DataType="vm:OpenRecentViewModel"> - + x:Class="InkForge.Desktop.Views.LandingView" + x:DataType="vm:LandingViewModel" + services:TopLevels.Register="{CompiledBinding}"> + \ No newline at end of file diff --git a/app/InkForge.Common/Views/LandingView.axaml.cs b/app/InkForge.Desktop/Views/LandingView.axaml.cs similarity index 69% rename from app/InkForge.Common/Views/LandingView.axaml.cs rename to app/InkForge.Desktop/Views/LandingView.axaml.cs index 5cc1310..0461eca 100644 --- a/app/InkForge.Common/Views/LandingView.axaml.cs +++ b/app/InkForge.Desktop/Views/LandingView.axaml.cs @@ -1,8 +1,8 @@ using Avalonia.ReactiveUI; -using InkForge.Common.ViewModels; +using InkForge.Desktop.ViewModels; -namespace InkForge.Common.Views; +namespace InkForge.Desktop.Views; public partial class LandingView : ReactiveUserControl { diff --git a/app/InkForge.Desktop/Views/MainWindow.axaml b/app/InkForge.Desktop/Views/MainWindow.axaml index 4312524..22b7e34 100644 --- a/app/InkForge.Desktop/Views/MainWindow.axaml +++ b/app/InkForge.Desktop/Views/MainWindow.axaml @@ -3,22 +3,16 @@ 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" + xmlns:vm="using:InkForge.Desktop.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="InkForge.Desktop.Views.MainWindow" - x:DataType="ifcvm:AppViewModel" - Title="InkForge"> - - - - - + x:DataType="vm:AppViewModel" + Title="MainWindow"> + + - - - - - + + \ No newline at end of file diff --git a/app/InkForge.Desktop/Views/MainWindow.axaml.cs b/app/InkForge.Desktop/Views/MainWindow.axaml.cs index 65b2d21..4ad378f 100644 --- a/app/InkForge.Desktop/Views/MainWindow.axaml.cs +++ b/app/InkForge.Desktop/Views/MainWindow.axaml.cs @@ -1,6 +1,6 @@ using Avalonia.ReactiveUI; -using InkForge.Common.ViewModels; +using InkForge.Desktop.ViewModels; namespace InkForge.Desktop.Views; diff --git a/app/InkForge.Common/Views/LandingViews/CreateWorkspaceView.axaml b/app/InkForge.Desktop/Views/WorkspaceView.axaml similarity index 65% rename from app/InkForge.Common/Views/LandingViews/CreateWorkspaceView.axaml rename to app/InkForge.Desktop/Views/WorkspaceView.axaml index ce77e44..3be3f4d 100644 --- a/app/InkForge.Common/Views/LandingViews/CreateWorkspaceView.axaml +++ b/app/InkForge.Desktop/Views/WorkspaceView.axaml @@ -2,9 +2,12 @@ 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:vm="using:InkForge.Desktop.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="InkForge.Common.Views.LandingViews.CreateWorkspaceView"> + x:Class="InkForge.Desktop.Views.WorkspaceView" + x:DataType="vm:WorkspaceViewModel"> Welcome to Avalonia! \ No newline at end of file diff --git a/app/InkForge.Desktop/Views/WorkspaceView.axaml.cs b/app/InkForge.Desktop/Views/WorkspaceView.axaml.cs new file mode 100644 index 0000000..4166ebe --- /dev/null +++ b/app/InkForge.Desktop/Views/WorkspaceView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.ReactiveUI; + +using InkForge.Desktop.ViewModels; + +namespace InkForge.Desktop.Views; + +public partial class WorkspaceView : ReactiveUserControl +{ + public WorkspaceView() + { + InitializeComponent(); + } +}