From 2727bb4ccb7e5f7892d6a047ca2c25ff90f60f6c Mon Sep 17 00:00:00 2001 From: Gitii Date: Mon, 27 Mar 2023 17:31:07 +0200 Subject: [PATCH 1/4] Improve numbers app. Simplify numbers ui and ux. Add start page --- Numbers.Core.Services/ExplorerShellImpl.cs | 47 ++++++++ .../Numbers.Core.Services.csproj | 3 +- .../ModelViews/RecentlyUsedFileViewModel.cs | 66 ++++++++++++ Numbers.Core/ModelViews/StartViewModel.cs | 102 ++++++++++++++++++ Numbers.Core/Services/IExplorerShell.cs | 7 ++ Numbers.Core/Services/RecentlyUsedFile.cs | 8 ++ Numbers/App.xaml.cs | 4 +- Numbers/Controls/RecentlyUsedFileView.xaml | 50 +++++++++ Numbers/Controls/RecentlyUsedFileView.xaml.cs | 35 ++++++ Numbers/MainWindow.xaml | 74 ++++--------- Numbers/MainWindow.xaml.cs | 61 ++++++----- Numbers/Numbers.csproj | 20 ++-- Numbers/Pages/Preparation.xaml | 1 - Numbers/Pages/Start.xaml | 49 +++++++++ Numbers/Pages/Start.xaml.cs | 89 +++++++++++++++ Numbers/Pages/Table.xaml.cs | 6 +- Numbers/Program.cs | 6 ++ Numbers/Properties/launchSettings.json | 3 +- 18 files changed, 531 insertions(+), 100 deletions(-) create mode 100644 Numbers.Core.Services/ExplorerShellImpl.cs create mode 100644 Numbers.Core/ModelViews/RecentlyUsedFileViewModel.cs create mode 100644 Numbers.Core/ModelViews/StartViewModel.cs create mode 100644 Numbers.Core/Services/IExplorerShell.cs create mode 100644 Numbers.Core/Services/RecentlyUsedFile.cs create mode 100644 Numbers/Controls/RecentlyUsedFileView.xaml create mode 100644 Numbers/Controls/RecentlyUsedFileView.xaml.cs create mode 100644 Numbers/Pages/Start.xaml create mode 100644 Numbers/Pages/Start.xaml.cs diff --git a/Numbers.Core.Services/ExplorerShellImpl.cs b/Numbers.Core.Services/ExplorerShellImpl.cs new file mode 100644 index 0000000..a4a8060 --- /dev/null +++ b/Numbers.Core.Services/ExplorerShellImpl.cs @@ -0,0 +1,47 @@ +using ShellLink; + +namespace Numbers.Core.Services; + +public class ExplorerShellImpl : IExplorerShell +{ + public IEnumerable GetRecentlyUsedFiles(params string[] extensionFilter) + { + var rufs = Directory.EnumerateFiles( + Environment.GetFolderPath(Environment.SpecialFolder.Recent) + ); + + return rufs.Where( + (f) => + { + const string LINK_FILE_EXTENSION = ".lnk"; + var cleanFileExtension = Path.GetExtension( + f.Substring(0, f.Length - LINK_FILE_EXTENSION.Length) + ); + + return extensionFilter.Length == 0 + || extensionFilter.Contains( + cleanFileExtension, + StringComparer.OrdinalIgnoreCase + ); + } + ) + .Select( + (f) => + { + var realFile = GetLinkTarget(f); + + return new RecentlyUsedFile() + { + FileName = Path.GetFileName(realFile), + FullPath = realFile, + AccessTime = File.GetLastAccessTime(realFile), + }; + } + ); + } + + public string GetLinkTarget(string pathToLnkFile) + { + return Shortcut.ReadFromFile(pathToLnkFile).LinkTargetIDList.Path; + } +} diff --git a/Numbers.Core.Services/Numbers.Core.Services.csproj b/Numbers.Core.Services/Numbers.Core.Services.csproj index 6c2a98e..f27a6ba 100644 --- a/Numbers.Core.Services/Numbers.Core.Services.csproj +++ b/Numbers.Core.Services/Numbers.Core.Services.csproj @@ -8,7 +8,8 @@ win-x86;win-x64;win-arm64 - + + diff --git a/Numbers.Core/ModelViews/RecentlyUsedFileViewModel.cs b/Numbers.Core/ModelViews/RecentlyUsedFileViewModel.cs new file mode 100644 index 0000000..c1ca578 --- /dev/null +++ b/Numbers.Core/ModelViews/RecentlyUsedFileViewModel.cs @@ -0,0 +1,66 @@ +using System.Reactive; +using Numbers.Core.Services; +using ReactiveUI; +using Sextant; +using Shared.Misc; + +namespace Numbers.Core.ModelViews; + +public class RecentlyUsedFileViewModel : ReactiveObject, IViewModel +{ + private readonly IParameterViewStackService _viewStackService; + + public string Id { get; } = nameof(RecentlyUsedFileViewModel); + + public RecentlyUsedFileViewModel(IParameterViewStackService viewStackService) + { + _viewStackService = viewStackService; + + OpenDocument = ReactiveCommand.Create(Open); + _fileName = _path = _formatedAccessDate = String.Empty; + } + + private void Open() + { + var navParams = new PreparationViewModel.NavigationParameter() + { + Arguments = new[] { Path }, + }; + + _viewStackService + .PushPage(navParams.ToNavigationParameter()) + .Subscribe(); + } + + public RecentlyUsedFileViewModel Set(RecentlyUsedFile file) + { + FileName = file.FileName; + Path = file.FullPath; + FormatedAccessDate = file.AccessTime.ToShortDateString(); + + return this; + } + + string _fileName; + public string FileName + { + get { return _fileName; } + private set { this.RaiseAndSetIfChanged(ref _fileName, value); } + } + + string _formatedAccessDate; + public string FormatedAccessDate + { + get { return _formatedAccessDate; } + private set { this.RaiseAndSetIfChanged(ref _formatedAccessDate, value); } + } + + string _path; + public string Path + { + get { return _path; } + private set { this.RaiseAndSetIfChanged(ref _path, value); } + } + + public ReactiveCommand OpenDocument { get; } +} diff --git a/Numbers.Core/ModelViews/StartViewModel.cs b/Numbers.Core/ModelViews/StartViewModel.cs new file mode 100644 index 0000000..488ef99 --- /dev/null +++ b/Numbers.Core/ModelViews/StartViewModel.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Immutable; +using System.Reactive; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Numbers.Core.Services; +using ReactiveUI; +using Sextant; +using Shared.Misc; + +namespace Numbers.Core.Services { } + +namespace Numbers.Core.ModelViews +{ + public class StartViewModel : ReactiveObject, IViewModel, INavigable + { + public string Id { get; } = nameof(StartViewModel); + + private readonly IExplorerShell _explorerShellService; + private readonly IParameterViewStackService _viewStackService; + private readonly IServiceProvider _serviceProvider; + private readonly Interaction _pickCsvDocumentInteraction; + + public StartViewModel( + IExplorerShell explorerShellService, + IParameterViewStackService viewStackService, + IServiceProvider serviceProvider + ) + { + _explorerShellService = explorerShellService; + _viewStackService = viewStackService; + _serviceProvider = serviceProvider; + _recentlyUsedFiles = ImmutableList.Empty; + + OpenFileCommand = ReactiveCommand.CreateFromTask(OpenFileAsync); + _pickCsvDocumentInteraction = new Interaction(RxApp.MainThreadScheduler); + } + + private async Task OpenFileAsync() + { + var filePath = await _pickCsvDocumentInteraction + .Handle(Unit.Default) + .ToTask() + .ConfigureAwait(true); + + if (string.IsNullOrEmpty(filePath)) + { + return; + } + + var navParams = new PreparationViewModel.NavigationParameter() + { + Arguments = new[] { filePath }, + }; + + _viewStackService + .PushPage(navParams.ToNavigationParameter()) + .Subscribe(); + } + + public IObservable WhenNavigatedTo(INavigationParameter parameter) + { + return Observable.Return(Unit.Default); + } + + public IObservable WhenNavigatedFrom(INavigationParameter parameter) + { + return Observable.Return(Unit.Default); + } + + public IObservable WhenNavigatingTo(INavigationParameter parameter) + { + return ProcessAsync().ToObservable(RxApp.MainThreadScheduler); + } + + ImmutableList _recentlyUsedFiles; + + public ImmutableList RecentlyUsedFiles + { + get { return _recentlyUsedFiles; } + private set { this.RaiseAndSetIfChanged(ref _recentlyUsedFiles, value); } + } + + public async Task ProcessAsync() + { + await Task.Delay(10).ConfigureAwait(true); + + RecentlyUsedFiles = _explorerShellService + .GetRecentlyUsedFiles(".csv") + .OrderByDescending((i) => i.AccessTime) + .Select( + (i) => _serviceProvider.GetRequiredService().Set(i) + ) + .ToImmutableList(); + } + + public ReactiveCommand OpenFileCommand { get; } + + public Interaction PickCsvDocumentInteraction => _pickCsvDocumentInteraction; + } +} diff --git a/Numbers.Core/Services/IExplorerShell.cs b/Numbers.Core/Services/IExplorerShell.cs new file mode 100644 index 0000000..720b8c0 --- /dev/null +++ b/Numbers.Core/Services/IExplorerShell.cs @@ -0,0 +1,7 @@ +namespace Numbers.Core.Services; + +public interface IExplorerShell +{ + public IEnumerable GetRecentlyUsedFiles(params string[] extensionFilter); + public string GetLinkTarget(string pathToLnkFile); +} diff --git a/Numbers.Core/Services/RecentlyUsedFile.cs b/Numbers.Core/Services/RecentlyUsedFile.cs new file mode 100644 index 0000000..d59242e --- /dev/null +++ b/Numbers.Core/Services/RecentlyUsedFile.cs @@ -0,0 +1,8 @@ +namespace Numbers.Core.Services; + +public class RecentlyUsedFile +{ + public string FileName { get; set; } = String.Empty; + public string FullPath { get; set; } = String.Empty; + public DateTime AccessTime { get; set; } = DateTime.Now; +} diff --git a/Numbers/App.xaml.cs b/Numbers/App.xaml.cs index 77348ea..1b9ccdd 100644 --- a/Numbers/App.xaml.cs +++ b/Numbers/App.xaml.cs @@ -27,5 +27,7 @@ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs ar _window.Activate(); } - private MainWindow? _window; + private static MainWindow? _window = null; + + public static MainWindow? Window => _window; } diff --git a/Numbers/Controls/RecentlyUsedFileView.xaml b/Numbers/Controls/RecentlyUsedFileView.xaml new file mode 100644 index 0000000..f5da79e --- /dev/null +++ b/Numbers/Controls/RecentlyUsedFileView.xaml @@ -0,0 +1,50 @@ + + + diff --git a/Numbers/Controls/RecentlyUsedFileView.xaml.cs b/Numbers/Controls/RecentlyUsedFileView.xaml.cs new file mode 100644 index 0000000..ce5f24c --- /dev/null +++ b/Numbers/Controls/RecentlyUsedFileView.xaml.cs @@ -0,0 +1,35 @@ +using System.Reactive.Disposables; +using Numbers.Core.ModelViews; +using ReactiveUI; + +namespace Numbers.Controls; + +[System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "MA0048:File name must match type name", + Justification = "Workaround: Xaml doesn't support generic types." +)] +public class ReactiveControlRecentlyUsedFileView + : ReactiveUserControl { } + +public sealed partial class RecentlyUsedFileView +{ + public RecentlyUsedFileView() + { + this.InitializeComponent(); + + this.WhenActivated( + (disposable) => + { + this.OneWayBind(ViewModel, (vm) => vm.FileName, (v) => v.TitleField.Text) + .DisposeWith(disposable); + this.OneWayBind(ViewModel, (vm) => vm.FormatedAccessDate, (v) => v.DateField.Text) + .DisposeWith(disposable); + this.OneWayBind(ViewModel, (vm) => vm.Path, (v) => v.PathField.Text) + .DisposeWith(disposable); + this.BindCommand(ViewModel, (vm) => vm.OpenDocument, (v) => v.Button) + .DisposeWith(disposable); + } + ); + } +} diff --git a/Numbers/MainWindow.xaml b/Numbers/MainWindow.xaml index 2ab3eb4..8beefa9 100644 --- a/Numbers/MainWindow.xaml +++ b/Numbers/MainWindow.xaml @@ -18,66 +18,30 @@ > + - - - - - - - - - - - - - - - - + + + - - + + + + diff --git a/Numbers/MainWindow.xaml.cs b/Numbers/MainWindow.xaml.cs index 522d480..ff86223 100644 --- a/Numbers/MainWindow.xaml.cs +++ b/Numbers/MainWindow.xaml.cs @@ -26,6 +26,8 @@ namespace Numbers; public sealed partial class MainWindow : DesktopWindow { + private const string DEFAULT_APP_TITLE = "Numbers"; + private const string DEFAULT_NO_TITLE = ""; private readonly IParameterViewStackService _viewStackService; private readonly INavigationService _navigationService; private readonly IApplicationLifeCycle _lifeCycle; @@ -44,9 +46,9 @@ IApplicationLifeCycle lifeCycle this.InitializeComponent(); - Title = "Winstrumenta Numbers"; - ExtendsContentIntoTitleBar = true; - SetTitleBar(TitleBarContainer); // do not set the title bar to use a 100% custom one. + Title = AppTitle.Text = DEFAULT_APP_TITLE; + ExtendsContentIntoTitleBar = true; // enable custom titlebar + SetTitleBar(AppTitleBar); // set user ui element as titlebar } public void SetLaunchArgs(string arguments) @@ -81,7 +83,7 @@ private void MainWindow_OnActivated(object sender, WindowLoadedEventArgs args) ) .NotifyOn(RxApp.MainThreadScheduler); - _navigationService.SetAdapter(new NavigationViewAdapter(Content, this, NavigationView)); + _navigationService.SetAdapter(new FrameNavigationViewAdapter(Content, this)); ForceUpdateTheme(); _viewStackService.PageStack.Subscribe(OnPageChanged); @@ -96,11 +98,21 @@ private void MainWindow_OnActivated(object sender, WindowLoadedEventArgs args) } } - var navParams = new PreparationViewModel.NavigationParameter() { Arguments = arguments, }; + if (arguments.Length == 0) + { + _viewStackService.PushPage(new NavigationParameter()).Subscribe(); + } + else + { + var navParams = new PreparationViewModel.NavigationParameter() + { + Arguments = arguments, + }; - _viewStackService - .PushPage(navParams.ToNavigationParameter()) - .Subscribe(); + _viewStackService + .PushPage(navParams.ToNavigationParameter()) + .Subscribe(); + } } private void OnPageChanged(IImmutableList stack) @@ -113,6 +125,12 @@ private void OnPageChanged(IImmutableList stack) if (stack.Last() is IWindowInterface ui) { + if (_windowUiDisposable != null) + { + _windowUiDisposable.Dispose(); + _windowUiDisposable = null; + } + var disposable = new CompositeDisposable(); ui.WhenAnyValue(x => x.WindowTitle) @@ -120,7 +138,7 @@ private void OnPageChanged(IImmutableList stack) .Subscribe( (title) => { - CurrentDocument.Content = TitleBar.Text = title ?? TitleBar.Text; + Title = AppTitle.Text = title ?? DEFAULT_NO_TITLE; } ) .DisposeWith(disposable); @@ -130,6 +148,8 @@ private void OnPageChanged(IImmutableList stack) .Subscribe( (items) => { + CommandBar.Visibility = + items?.Count == 0 ? Visibility.Collapsed : Visibility.Visible; CommandBar.PrimaryCommands.Clear(); CommandBar.PrimaryCommands.AddRange( items?.Select(ToAppBarButton) ?? Array.Empty() @@ -147,7 +167,7 @@ private AppBarButton ToAppBarButton(CommandDescription arg) return ( new AppBarButton() { - Content = arg.Label, + Label = arg.Label, Icon = arg.Icon != null ? new FontIcon() @@ -186,27 +206,6 @@ private void ResetWindow() CommandBar.SecondaryCommands.Clear(); } - private void NavigationView_OnSelectionChanged( - NavigationView sender, - NavigationViewSelectionChangedEventArgs args - ) - { - switch (args.SelectedItemContainer.Tag) - { - case "Document": - _viewStackService - .PushPage(new Sextant.NavigationParameter(), resetStack: true) - .Subscribe(); - break; - - case "Save": - _viewStackService - .PushPage(new Sextant.NavigationParameter(), resetStack: true) - .Subscribe(); - break; - } - } - private void MainWindow_OnClosing(object? sender, WindowClosingEventArgs e) { _lifeCycle.Exit(0); diff --git a/Numbers/Numbers.csproj b/Numbers/Numbers.csproj index 6e1d3d5..774cae0 100644 --- a/Numbers/Numbers.csproj +++ b/Numbers/Numbers.csproj @@ -1,4 +1,4 @@ - + WinExe net6.0-windows10.0.20348.0 @@ -20,10 +20,13 @@ + + + @@ -34,6 +37,7 @@ + @@ -65,9 +69,7 @@ - + @@ -78,6 +80,12 @@ MSBuild:Compile + + $(DefaultXamlRuntime) + + + MSBuild:Compile + MSBuild:Compile @@ -105,9 +113,7 @@ Explorer "Package and Publish" context menu entry to be enabled for this project even if the Windows App SDK Nuget package has not yet been restored. --> - + true diff --git a/Numbers/Pages/Preparation.xaml b/Numbers/Pages/Preparation.xaml index e7a853a..b7cc33c 100644 --- a/Numbers/Pages/Preparation.xaml +++ b/Numbers/Pages/Preparation.xaml @@ -2,7 +2,6 @@ x:Class="Numbers.Pages.Preparation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:core="using:PackageInstaller.Core" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:reactiveUi="using:ReactiveUI" diff --git a/Numbers/Pages/Start.xaml b/Numbers/Pages/Start.xaml new file mode 100644 index 0000000..bfed0cd --- /dev/null +++ b/Numbers/Pages/Start.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + There are no recently used csv documents... + + + + + + + diff --git a/Numbers/Pages/Start.xaml.cs b/Numbers/Pages/Start.xaml.cs new file mode 100644 index 0000000..7b96698 --- /dev/null +++ b/Numbers/Pages/Start.xaml.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Immutable; +using System.Reactive; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Microsoft.UI.Xaml; +using Numbers.Core.ModelViews; +using Numbers.Core.Services; +using ReactiveUI; +using Windows.Storage.Pickers; +using WinRT.Interop; + +namespace Numbers.Pages; + +[System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "MA0048:File name must match type name", + Justification = "Workaround: Xaml doesn't support generic types." +)] +public class ReactivePageStart : ReactivePage { } + +public sealed partial class Start +{ + public Start() + { + this.InitializeComponent(); + + this.WhenActivated( + (disposable) => + { + this.OneWayBind( + ViewModel, + (vm) => vm.RecentlyUsedFiles, + (v) => v.RecentlyUsedFiles.ItemsSource + ) + .DisposeWith(disposable); + + this.ViewModel + .WhenAnyValue((x) => x.RecentlyUsedFiles) + .Subscribe( + (ImmutableList list) => + { + RecentlyUsedFiles.Visibility = list.IsEmpty + ? Visibility.Collapsed + : Visibility.Visible; + RecentlyUsedFilesEmpty.Visibility = list.IsEmpty + ? Visibility.Visible + : Visibility.Collapsed; + } + ) + .DisposeWith(disposable); + + this.BindCommand(ViewModel, (vm) => vm.OpenFileCommand, (v) => v.OpenFile) + .DisposeWith(disposable); + + ViewModel!.PickCsvDocumentInteraction.RegisterHandler(PickCsvDocumentAsync); + } + ); + } + + private async Task PickCsvDocumentAsync(InteractionContext obj) + { + // Create a file picker + var openPicker = new Windows.Storage.Pickers.FileOpenPicker(); + + // Retrieve the window handle (HWND) of the current WinUI 3 window. + var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(App.Window); + + // Initialize the file picker with the window handle (HWND). + WinRT.Interop.InitializeWithWindow.Initialize(openPicker, hWnd); + + // Set options for your file picker + openPicker.ViewMode = PickerViewMode.List; + openPicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary; + openPicker.FileTypeFilter.Add(".csv"); + + // Open the picker for the user to pick a file + var file = await openPicker.PickSingleFileAsync(); + + if (file != null) + { + obj.SetOutput(file.Path); + } + else + { + obj.SetOutput(null); + } + } +} diff --git a/Numbers/Pages/Table.xaml.cs b/Numbers/Pages/Table.xaml.cs index e55cf2d..bd1df23 100644 --- a/Numbers/Pages/Table.xaml.cs +++ b/Numbers/Pages/Table.xaml.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reactive.Disposables; +using System.Reactive.Linq; using CommunityToolkit.WinUI.UI.Controls; using DynamicData; using Microsoft.UI.Xaml; @@ -28,6 +29,7 @@ public Table() { this.ViewModel .WhenAnyValue((vm) => vm.Columns) + .ObserveOn(RxApp.MainThreadScheduler) .Subscribe( (columns) => { @@ -50,8 +52,8 @@ public Table() ) .DisposeWith(disposable); - this.OneWayBind(ViewModel, (vm) => vm.Rows, (v) => v.DataGrid.ItemsSource) - .DisposeWith(disposable); + this.ViewModel.WhenAnyValue((vm) => vm.Rows).ObserveOn(RxApp.MainThreadScheduler) + .BindTo(this, (v) => v.DataGrid.ItemsSource).DisposeWith(disposable); } ); } diff --git a/Numbers/Program.cs b/Numbers/Program.cs index 80836e1..6bbd0c4 100644 --- a/Numbers/Program.cs +++ b/Numbers/Program.cs @@ -2,6 +2,7 @@ using Community.Sextant.WinUI.Microsoft.Extensions.DependencyInjection; using CommunityToolkit.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; +using Numbers.Controls; using Numbers.Core.ModelViews; using Numbers.Core.Services; using Numbers.Pages; @@ -94,6 +95,11 @@ private static void ConfigureModelViews(IServiceCollection collection) viewBuilder.RegisterViewAndViewModel(); viewBuilder.RegisterViewAndViewModel(); viewBuilder.RegisterViewAndViewModel(); + viewBuilder.RegisterViewAndViewModel(); + viewBuilder.RegisterViewAndViewModel< + RecentlyUsedFileView, + RecentlyUsedFileViewModel + >(); } ); } diff --git a/Numbers/Properties/launchSettings.json b/Numbers/Properties/launchSettings.json index 200f265..4bb0a22 100644 --- a/Numbers/Properties/launchSettings.json +++ b/Numbers/Properties/launchSettings.json @@ -2,11 +2,10 @@ "profiles": { "Numbers (Package)": { "commandName": "MsixPackage", - "commandLineArgs": "\"C:\\Users\\Germi\\Downloads\\cities.csv\"", "nativeDebugging": true }, "Numbers (Unpackaged)": { "commandName": "Project" } } -} +} \ No newline at end of file From ff812a6f4b40b2faa507766add397820756a3a04 Mon Sep 17 00:00:00 2001 From: Gitii Date: Tue, 28 Mar 2023 11:57:00 +0200 Subject: [PATCH 2/4] Improve csv handling --- NativeInterop.Win32.Xaml/DesktopWindow.cs | 3 - NativeInterop.Win32/ParentProcessUtilities.cs | 2 - NativeInterop.Win32/Pinvoke.cs | 2 - .../Csv.cs | 7 +- .../CsvRow.cs | 2 +- .../CsvRow100.cs | 2 +- .../CsvTable.cs | 2 +- .../DefaultTypeGenerator.cs | 2 +- .../ExcelImpl.cs | 120 +++++++++++++++++ .../ExplorerShellImpl.cs | 18 ++- .../GlobalContext.cs | 2 +- ...mbers.Core.Services.Implementations.csproj | 0 Numbers.Core/ModelViews/StartViewModel.cs | 3 +- Numbers.Core/ModelViews/TableViewModel.cs | 127 +++++++++++++++++- Numbers.Core/Services/ExcelImpl.cs | 9 ++ Numbers.Core/Services/IExcel.cs | 6 + Numbers/MainWindow.xaml | 1 + Numbers/MainWindow.xaml.cs | 1 - Numbers/Numbers.csproj | 16 ++- Numbers/Pages/Start.xaml.cs | 2 - Numbers/Pages/Table.xaml | 54 +++++++- Numbers/Pages/Table.xaml.cs | 27 +++- Numbers/Program.cs | 1 + PackageInstaller/MainWindow.xaml.cs | 3 +- .../ClipboardManagerImpl.cs | 6 +- .../Launcher.cs | 20 +++ Shared.Services/IClipboardManager.cs | 8 +- Shared.Services/ILauncher.cs | 3 + Winstrumenta.sln | 3 +- 29 files changed, 391 insertions(+), 61 deletions(-) rename {Numbers.Core.Services => Numbers.Core.Services.Implementations}/Csv.cs (98%) rename {Numbers.Core.Services => Numbers.Core.Services.Implementations}/CsvRow.cs (74%) rename {Numbers.Core.Services => Numbers.Core.Services.Implementations}/CsvRow100.cs (99%) rename {Numbers.Core.Services => Numbers.Core.Services.Implementations}/CsvTable.cs (89%) rename {Numbers.Core.Services => Numbers.Core.Services.Implementations}/DefaultTypeGenerator.cs (92%) create mode 100644 Numbers.Core.Services.Implementations/ExcelImpl.cs rename {Numbers.Core.Services => Numbers.Core.Services.Implementations}/ExplorerShellImpl.cs (74%) rename {Numbers.Core.Services => Numbers.Core.Services.Implementations}/GlobalContext.cs (78%) rename Numbers.Core.Services/Numbers.Core.Services.csproj => Numbers.Core.Services.Implementations/Numbers.Core.Services.Implementations.csproj (100%) create mode 100644 Numbers.Core/Services/ExcelImpl.cs create mode 100644 Numbers.Core/Services/IExcel.cs diff --git a/NativeInterop.Win32.Xaml/DesktopWindow.cs b/NativeInterop.Win32.Xaml/DesktopWindow.cs index 96794be..d236e98 100644 --- a/NativeInterop.Win32.Xaml/DesktopWindow.cs +++ b/NativeInterop.Win32.Xaml/DesktopWindow.cs @@ -1,9 +1,6 @@ using System; -using System.Diagnostics; using System.Runtime.InteropServices; using Windows.UI; -using Windows.Win32; -using Microsoft.UI; using Microsoft.UI.Xaml; using WinRT; diff --git a/NativeInterop.Win32/ParentProcessUtilities.cs b/NativeInterop.Win32/ParentProcessUtilities.cs index 760166c..32f51e0 100644 --- a/NativeInterop.Win32/ParentProcessUtilities.cs +++ b/NativeInterop.Win32/ParentProcessUtilities.cs @@ -2,8 +2,6 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System; -using System.Linq; -using System.Management; namespace NativeInterop.Win32; diff --git a/NativeInterop.Win32/Pinvoke.cs b/NativeInterop.Win32/Pinvoke.cs index fc13c5a..090ef84 100644 --- a/NativeInterop.Win32/Pinvoke.cs +++ b/NativeInterop.Win32/Pinvoke.cs @@ -1,8 +1,6 @@ using System; -using System.Drawing; using System.Runtime.InteropServices; using Windows.Win32.Foundation; -using Windows.Win32.Graphics.Gdi; using Windows.Win32.UI.WindowsAndMessaging; // ReSharper disable once CheckNamespace diff --git a/Numbers.Core.Services/Csv.cs b/Numbers.Core.Services.Implementations/Csv.cs similarity index 98% rename from Numbers.Core.Services/Csv.cs rename to Numbers.Core.Services.Implementations/Csv.cs index cbc4048..aad21b0 100644 --- a/Numbers.Core.Services/Csv.cs +++ b/Numbers.Core.Services.Implementations/Csv.cs @@ -1,11 +1,11 @@ using System.Collections; -using CsvHelper; -using CsvHelper.Configuration; using System.Globalization; using System.Reflection; +using CsvHelper; +using CsvHelper.Configuration; using CsvHelper.TypeConversion; -namespace Numbers.Core.Services; +namespace Numbers.Core.Services.Implementations; public class Csv : ICsv { @@ -49,6 +49,7 @@ public async Task SaveAsAsync(ICsvTable data, FileEncoding encoding, string file Quote = encoding.QuoteCharacter, BadDataFound = null, TrimOptions = TrimOptions.Trim, + ShouldQuote = _ => encoding.QuoteCharacter != '\0' }; using var writer = new StreamWriter( diff --git a/Numbers.Core.Services/CsvRow.cs b/Numbers.Core.Services.Implementations/CsvRow.cs similarity index 74% rename from Numbers.Core.Services/CsvRow.cs rename to Numbers.Core.Services.Implementations/CsvRow.cs index e11a6f8..0033ee6 100644 --- a/Numbers.Core.Services/CsvRow.cs +++ b/Numbers.Core.Services.Implementations/CsvRow.cs @@ -1,4 +1,4 @@ -namespace Numbers.Core.Services; +namespace Numbers.Core.Services.Implementations; public class CsvRow : ICsvRow { diff --git a/Numbers.Core.Services/CsvRow100.cs b/Numbers.Core.Services.Implementations/CsvRow100.cs similarity index 99% rename from Numbers.Core.Services/CsvRow100.cs rename to Numbers.Core.Services.Implementations/CsvRow100.cs index 0bbf7c2..2b67f60 100644 --- a/Numbers.Core.Services/CsvRow100.cs +++ b/Numbers.Core.Services.Implementations/CsvRow100.cs @@ -1,7 +1,7 @@ using System.ComponentModel; using System.Runtime.CompilerServices; -namespace Numbers.Core.Services; +namespace Numbers.Core.Services.Implementations; class CsvRow100 : ICsvRowCells { diff --git a/Numbers.Core.Services/CsvTable.cs b/Numbers.Core.Services.Implementations/CsvTable.cs similarity index 89% rename from Numbers.Core.Services/CsvTable.cs rename to Numbers.Core.Services.Implementations/CsvTable.cs index 81e3743..b9f8e5a 100644 --- a/Numbers.Core.Services/CsvTable.cs +++ b/Numbers.Core.Services.Implementations/CsvTable.cs @@ -1,4 +1,4 @@ -namespace Numbers.Core.Services; +namespace Numbers.Core.Services.Implementations; class CsvTable : ICsvTable { diff --git a/Numbers.Core.Services/DefaultTypeGenerator.cs b/Numbers.Core.Services.Implementations/DefaultTypeGenerator.cs similarity index 92% rename from Numbers.Core.Services/DefaultTypeGenerator.cs rename to Numbers.Core.Services.Implementations/DefaultTypeGenerator.cs index de6ca11..c2405a0 100644 --- a/Numbers.Core.Services/DefaultTypeGenerator.cs +++ b/Numbers.Core.Services.Implementations/DefaultTypeGenerator.cs @@ -1,4 +1,4 @@ -namespace Numbers.Core.Services; +namespace Numbers.Core.Services.Implementations; public class DefaultTypeGenerator : ITypeGenerator { diff --git a/Numbers.Core.Services.Implementations/ExcelImpl.cs b/Numbers.Core.Services.Implementations/ExcelImpl.cs new file mode 100644 index 0000000..5461183 --- /dev/null +++ b/Numbers.Core.Services.Implementations/ExcelImpl.cs @@ -0,0 +1,120 @@ +using System.Globalization; +using Microsoft.Win32; +using Shared.Services; + +namespace Numbers.Core.Services.Implementations; + +public class ExcelImpl : IExcel +{ + private readonly ILauncher _launcher; + + public ExcelImpl(ILauncher launcher) + { + _launcher = launcher; + } + +#pragma warning disable MA0051 + private string? FindExcelPath() +#pragma warning restore MA0051 + { + RegistryKey? excelKey = null; + + try + { + // First, try to find the Excel path for Office 2019 and later versions. + excelKey = Registry.ClassesRoot.OpenSubKey("Excel.Application\\CurVer", false); + if (excelKey != null) + { + var version = excelKey + .GetValue("") + ?.ToString() + ?.Replace(".", "_", StringComparison.OrdinalIgnoreCase); + excelKey = Registry.ClassesRoot.OpenSubKey( + $"Excel.Application\\{version}\\InstallRoot", + false + ); + + if (TryValidateExcelPath(excelKey?.GetValue("Path"), out var path)) + { + return path; + } + } + + // Next, try for office 365 + excelKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Office"); + if (excelKey != null) + { + var officeVersionKeys = excelKey.GetSubKeyNames().Where((key) => float.TryParse(key, NumberStyles.Float, CultureInfo.InvariantCulture, out _)).ToArray(); + + foreach (string officeSubKey in officeVersionKeys) + { + var officeKey = excelKey.OpenSubKey(officeSubKey, false)!; + + foreach (string subKeyName in officeKey.GetSubKeyNames()) + { + if (subKeyName.StartsWith("Excel", StringComparison.OrdinalIgnoreCase)) + { + var subKey = officeKey.OpenSubKey($"{subKeyName}\\InstallRoot", false); + + if (TryValidateExcelPath(subKey?.GetValue("Path"), out var path)) + { + return path; + } + } + } + } + } + + // Next, try to find the Excel path for Office 2016 and earlier versions. + excelKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Office"); + if (excelKey != null) + { + foreach (string subKeyName in excelKey.GetSubKeyNames()) + { + if (subKeyName.StartsWith("Excel")) + { + var subKey = excelKey.OpenSubKey($"{subKeyName}\\InstallRoot", false); + + if (TryValidateExcelPath(subKey?.GetValue("Path"), out var path)) + { + return path; + } + } + } + } + } + catch (Exception) + { + return null; + } + finally + { + excelKey?.Close(); + } + + return null; + + bool TryValidateExcelPath(object? officePath, out string fullExcelPath) + { + fullExcelPath = String.Empty; + + if (officePath is not string strOfficePath || string.IsNullOrWhiteSpace(strOfficePath)) + { + return false; + } + + fullExcelPath = Path.Combine(strOfficePath, "Excel.exe"); + + return File.Exists(fullExcelPath); + } + } + + public async Task OpenAsync(string filePath) + { + var excelPath = FindExcelPath(); + if (excelPath != null) + { + await _launcher.LaunchWithAssociatedAppAsync(filePath, excelPath).ConfigureAwait(false); + } + } +} diff --git a/Numbers.Core.Services/ExplorerShellImpl.cs b/Numbers.Core.Services.Implementations/ExplorerShellImpl.cs similarity index 74% rename from Numbers.Core.Services/ExplorerShellImpl.cs rename to Numbers.Core.Services.Implementations/ExplorerShellImpl.cs index a4a8060..a500628 100644 --- a/Numbers.Core.Services/ExplorerShellImpl.cs +++ b/Numbers.Core.Services.Implementations/ExplorerShellImpl.cs @@ -1,6 +1,6 @@ using ShellLink; -namespace Numbers.Core.Services; +namespace Numbers.Core.Services.Implementations; public class ExplorerShellImpl : IExplorerShell { @@ -25,18 +25,16 @@ public IEnumerable GetRecentlyUsedFiles(params string[] extens ); } ) + .Select(GetLinkTarget) + .Where(File.Exists) .Select( (f) => - { - var realFile = GetLinkTarget(f); - - return new RecentlyUsedFile() + new RecentlyUsedFile() { - FileName = Path.GetFileName(realFile), - FullPath = realFile, - AccessTime = File.GetLastAccessTime(realFile), - }; - } + FileName = Path.GetFileName(f), + FullPath = f, + AccessTime = File.GetLastAccessTime(f), + } ); } diff --git a/Numbers.Core.Services/GlobalContext.cs b/Numbers.Core.Services.Implementations/GlobalContext.cs similarity index 78% rename from Numbers.Core.Services/GlobalContext.cs rename to Numbers.Core.Services.Implementations/GlobalContext.cs index fe24815..bbac1bf 100644 --- a/Numbers.Core.Services/GlobalContext.cs +++ b/Numbers.Core.Services.Implementations/GlobalContext.cs @@ -1,4 +1,4 @@ -namespace Numbers.Core.Services; +namespace Numbers.Core.Services.Implementations; public class GlobalContext : IGlobalContext { diff --git a/Numbers.Core.Services/Numbers.Core.Services.csproj b/Numbers.Core.Services.Implementations/Numbers.Core.Services.Implementations.csproj similarity index 100% rename from Numbers.Core.Services/Numbers.Core.Services.csproj rename to Numbers.Core.Services.Implementations/Numbers.Core.Services.Implementations.csproj diff --git a/Numbers.Core/ModelViews/StartViewModel.cs b/Numbers.Core/ModelViews/StartViewModel.cs index 488ef99..b5e310b 100644 --- a/Numbers.Core/ModelViews/StartViewModel.cs +++ b/Numbers.Core/ModelViews/StartViewModel.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Immutable; +using System.Collections.Immutable; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; diff --git a/Numbers.Core/ModelViews/TableViewModel.cs b/Numbers.Core/ModelViews/TableViewModel.cs index 64e510a..d1b5fef 100644 --- a/Numbers.Core/ModelViews/TableViewModel.cs +++ b/Numbers.Core/ModelViews/TableViewModel.cs @@ -2,10 +2,12 @@ using System.Reactive; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; +using System.Text; using System.Windows.Input; using Numbers.Core.Services; using ReactiveUI; using Sextant; +using Shared.Misc; using Shared.Services; namespace Numbers.Core.ModelViews; @@ -15,6 +17,7 @@ public class TableViewModel : ReactiveObject, IViewModel, INavigable, IWindowInt private readonly IFile _file; private readonly ICsv _csv; private readonly IGlobalContext _context; + private readonly IExcel _excel; private readonly IParameterViewStackService _viewStackService; public string Id { get; } = nameof(TableViewModel); @@ -35,22 +38,65 @@ public ImmutableList Rows private set { this.RaiseAndSetIfChanged(ref _rows, value); } } + string _delimiter = String.Empty; + + public string Delimiter + { + get { return _delimiter; } + set { this.RaiseAndSetIfChanged(ref _delimiter, value); } + } + + ImmutableList _delimiterOptions = ImmutableList.Empty; + + public ImmutableList DelimiterOptions + { + get { return _delimiterOptions; } + set { this.RaiseAndSetIfChanged(ref _delimiterOptions, value); } + } + + char _quoteCharacter = '\0'; + + public char QuoteCharacter + { + get { return _quoteCharacter; } + set { this.RaiseAndSetIfChanged(ref _quoteCharacter, value); } + } + + Encoding _encoding = Encoding.Default; + + public Encoding Encoding + { + get { return _encoding; } + set { this.RaiseAndSetIfChanged(ref _encoding, value); } + } + + ImmutableList _encodingOptions = ImmutableList.Empty; + + public ImmutableList EncodingOptions + { + get { return _encodingOptions; } + set { this.RaiseAndSetIfChanged(ref _encodingOptions, value); } + } + public TableViewModel( IParameterViewStackService viewStackService, IFile file, ICsv csv, - IGlobalContext context + IGlobalContext context, + IExcel excel ) { _viewStackService = viewStackService; _file = file; _csv = csv; _context = context; + _excel = excel; _columns = ImmutableList<(string header, string fieldName)>.Empty; _rows = ImmutableList.Empty; SaveCommand = ReactiveCommand.CreateFromTask(SaveAsync); + OpenInExcelCommand = ReactiveCommand.CreateFromTask(OpenInExcelAsync); Commands = ImmutableList.Create( new CommandDescription() @@ -61,17 +107,61 @@ IGlobalContext context KeyboardAcceleratorModifier = "Control", Command = SaveCommand }, - new CommandDescription() { Label = "Open in Excel", Command = SaveCommand } + new CommandDescription() + { + Label = "Open in Excel", + Icon = "\uE8A7", + KeyboardAcceleratorKey = "E", + KeyboardAcceleratorModifier = "Control", + Command = OpenInExcelCommand + } + ); + + EncodingOptions = ImmutableList.Empty.AddRange( + new[] + { + System.Text.Encoding.ASCII, + Encoding.BigEndianUnicode, + Encoding.Latin1, + Encoding.UTF32, + Encoding.UTF8, + Encoding.Unicode + } + ); + DelimiterOptions = ImmutableList.Empty.AddRange(new[] { ",", ";", "\t", " " }); + } + + private Task OpenInExcelAsync() + { + return _excel.OpenAsync( + _context.FilePath ?? throw new Exception("Global context is missing file path") ); } private Task SaveAsync() { + if (this.Delimiter.Length == 0 || this.QuoteCharacter == '\0') + { + return Task.CompletedTask; + } + + UpdateContext( + _context.Table!, + new FileEncoding() + { + Delimiter = this.Delimiter, + Encoding = Encoding, + QuoteCharacter = QuoteCharacter, + } + ); + return _csv.SaveAsAsync(_context); } public ICommand SaveCommand { get; } + public ICommand OpenInExcelCommand { get; } + public IObservable WhenNavigatedTo(INavigationParameter parameter) { return Observable.Return(Unit.Default); @@ -106,6 +196,18 @@ private IObservable RestoreFromContext(ICsvTable data) Columns = data.HeaderNames.Zip(data.FieldNames).ToImmutableList(); Rows = data.Rows.ToImmutableList(); + var contextFileEncoding = + _context.FileEncoding + ?? throw new Exception("Expected global context to have file encoding"); + + Delimiter = + DelimiterOptions.FirstOrDefault( + (del) => + del.Equals(contextFileEncoding.Delimiter, StringComparison.OrdinalIgnoreCase) + ) ?? contextFileEncoding.Delimiter; + QuoteCharacter = contextFileEncoding.QuoteCharacter; + Encoding = contextFileEncoding.Encoding; + WindowTitle = Path.GetFileName(_context.FilePath ?? "unknown.csv"); return Observable.Return(Unit.Default); @@ -113,14 +215,25 @@ private IObservable RestoreFromContext(ICsvTable data) public async Task ProcessAsync(string filePath) { - await Task.Delay(10).ConfigureAwait(true); + try + { + await Task.Delay(10).ConfigureAwait(true); - await using var stream = _file.OpenRead(filePath); + await using var stream = _file.OpenRead(filePath); - var (data, encoding) = await _csv.ParseAsync(stream).ConfigureAwait(true); + var (data, encoding) = await _csv.ParseAsync(stream).ConfigureAwait(true); - UpdateContext(data, encoding); - RestoreFromContext(data); + UpdateContext(data, encoding); + RestoreFromContext(data); + } + catch (Exception e) + { + var navParms = new ErrorViewModel.NavigationParameter() { Exception = e }; + + _viewStackService + .PushPage(navParms.ToNavigationParameter()) + .Subscribe(); + } } private void UpdateContext(ICsvTable data, FileEncoding encoding) diff --git a/Numbers.Core/Services/ExcelImpl.cs b/Numbers.Core/Services/ExcelImpl.cs new file mode 100644 index 0000000..4d11e8b --- /dev/null +++ b/Numbers.Core/Services/ExcelImpl.cs @@ -0,0 +1,9 @@ +namespace Numbers.Core.Services; + +public class ExcelImpl : IExcel +{ + public Task OpenAsync(string filePath) + { + throw new NotImplementedException(); + } +} diff --git a/Numbers.Core/Services/IExcel.cs b/Numbers.Core/Services/IExcel.cs new file mode 100644 index 0000000..c2a8dba --- /dev/null +++ b/Numbers.Core/Services/IExcel.cs @@ -0,0 +1,6 @@ +namespace Numbers.Core.Services; + +public interface IExcel +{ + public Task OpenAsync(string filePath); +} diff --git a/Numbers/MainWindow.xaml b/Numbers/MainWindow.xaml index 8beefa9..4c43553 100644 --- a/Numbers/MainWindow.xaml +++ b/Numbers/MainWindow.xaml @@ -14,6 +14,7 @@ UseMicaSystemBackdrop="True" SystemBackdropEnabled="True" Closing="MainWindow_OnClosing" + StartupLocation="CenterParentProcess" mc:Ignorable="d" > diff --git a/Numbers/MainWindow.xaml.cs b/Numbers/MainWindow.xaml.cs index ff86223..8c56cf4 100644 --- a/Numbers/MainWindow.xaml.cs +++ b/Numbers/MainWindow.xaml.cs @@ -5,7 +5,6 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading; -using System.Windows.Input; using Windows.System; using Community.Sextant.WinUI; using Community.Sextant.WinUI.Adapters; diff --git a/Numbers/Numbers.csproj b/Numbers/Numbers.csproj index 774cae0..83e1ca6 100644 --- a/Numbers/Numbers.csproj +++ b/Numbers/Numbers.csproj @@ -1,4 +1,4 @@ - + WinExe net6.0-windows10.0.20348.0 @@ -66,10 +66,14 @@ - + - + @@ -81,7 +85,7 @@ MSBuild:Compile - $(DefaultXamlRuntime) + $(DefaultXamlRuntime) MSBuild:Compile @@ -113,7 +117,9 @@ Explorer "Package and Publish" context menu entry to be enabled for this project even if the Windows App SDK Nuget package has not yet been restored. --> - + true diff --git a/Numbers/Pages/Start.xaml.cs b/Numbers/Pages/Start.xaml.cs index 7b96698..27b6db0 100644 --- a/Numbers/Pages/Start.xaml.cs +++ b/Numbers/Pages/Start.xaml.cs @@ -5,10 +5,8 @@ using System.Threading.Tasks; using Microsoft.UI.Xaml; using Numbers.Core.ModelViews; -using Numbers.Core.Services; using ReactiveUI; using Windows.Storage.Pickers; -using WinRT.Interop; namespace Numbers.Pages; diff --git a/Numbers/Pages/Table.xaml b/Numbers/Pages/Table.xaml index 7bee114..180b265 100644 --- a/Numbers/Pages/Table.xaml +++ b/Numbers/Pages/Table.xaml @@ -6,7 +6,59 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls" + xmlns:system="using:System" + xmlns:text="using:System.Text" mc:Ignorable="d" > - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Numbers/Pages/Table.xaml.cs b/Numbers/Pages/Table.xaml.cs index bd1df23..d18683a 100644 --- a/Numbers/Pages/Table.xaml.cs +++ b/Numbers/Pages/Table.xaml.cs @@ -52,8 +52,31 @@ public Table() ) .DisposeWith(disposable); - this.ViewModel.WhenAnyValue((vm) => vm.Rows).ObserveOn(RxApp.MainThreadScheduler) - .BindTo(this, (v) => v.DataGrid.ItemsSource).DisposeWith(disposable); + this.ViewModel + .WhenAnyValue((vm) => vm.Rows) + .ObserveOn(RxApp.MainThreadScheduler) + .BindTo(this, (v) => v.DataGrid.ItemsSource) + .DisposeWith(disposable); + + this.Bind(ViewModel, (vm) => vm.Delimiter, (v) => v.Delimiter.Text) + .DisposeWith(disposable); + this.Bind(ViewModel, (vm) => vm.QuoteCharacter, (v) => v.Quote.Text) + .DisposeWith(disposable); + this.Bind(ViewModel, (vm) => vm.Encoding, (v) => v.FileEncoding.SelectedItem) + .DisposeWith(disposable); + + this.OneWayBind( + ViewModel, + (vm) => vm.DelimiterOptions, + (v) => v.Delimiter.ItemsSource + ) + .DisposeWith(disposable); + this.OneWayBind( + ViewModel, + (vm) => vm.EncodingOptions, + (v) => v.FileEncoding.ItemsSource + ) + .DisposeWith(disposable); } ); } diff --git a/Numbers/Program.cs b/Numbers/Program.cs index 6bbd0c4..f8a4a88 100644 --- a/Numbers/Program.cs +++ b/Numbers/Program.cs @@ -5,6 +5,7 @@ using Numbers.Controls; using Numbers.Core.ModelViews; using Numbers.Core.Services; +using Numbers.Core.Services.Implementations; using Numbers.Pages; using ReactiveUI; using Shared.Services; diff --git a/PackageInstaller/MainWindow.xaml.cs b/PackageInstaller/MainWindow.xaml.cs index 7ca54fe..3106dbb 100644 --- a/PackageInstaller/MainWindow.xaml.cs +++ b/PackageInstaller/MainWindow.xaml.cs @@ -1,5 +1,4 @@ -using Microsoft.UI.Xaml; -using System; +using System; using System.Linq; using System.Reactive; using System.Threading; diff --git a/Shared.Services.Implementations.Winui/ClipboardManagerImpl.cs b/Shared.Services.Implementations.Winui/ClipboardManagerImpl.cs index ece6ce9..673bfbd 100644 --- a/Shared.Services.Implementations.Winui/ClipboardManagerImpl.cs +++ b/Shared.Services.Implementations.Winui/ClipboardManagerImpl.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Threading.Tasks; using Windows.ApplicationModel.DataTransfer; namespace Shared.Services.Implementations.WinUI; diff --git a/Shared.Services.Implementations.Winui/Launcher.cs b/Shared.Services.Implementations.Winui/Launcher.cs index 914666d..47c9031 100644 --- a/Shared.Services.Implementations.Winui/Launcher.cs +++ b/Shared.Services.Implementations.Winui/Launcher.cs @@ -1,5 +1,7 @@ using System; using System.Threading.Tasks; +using Windows.Storage; +using System.Diagnostics; namespace Shared.Services.Implementations.WinUI; @@ -14,4 +16,22 @@ public Task LaunchFolderAsync(string folderPath) { return Windows.System.Launcher.LaunchFolderPathAsync(folderPath).AsTask(); } + + public Task LaunchWithAssociatedAppAsync(string filePath) + { + return LaunchWithAssociatedAppAsync(filePath, null); + } + + public async Task LaunchWithAssociatedAppAsync(string filePath, string? appPath) + { + if (appPath != null) + { + Process.Start(appPath, filePath).Dispose(); + } + else + { + var file = await StorageFile.GetFileFromPathAsync(filePath); + await Windows.System.Launcher.LaunchFileAsync(file).AsTask().ConfigureAwait(false); + } + } } diff --git a/Shared.Services/IClipboardManager.cs b/Shared.Services/IClipboardManager.cs index a17d724..93ff009 100644 --- a/Shared.Services/IClipboardManager.cs +++ b/Shared.Services/IClipboardManager.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Shared.Services; +namespace Shared.Services; public interface IClipboardManager { diff --git a/Shared.Services/ILauncher.cs b/Shared.Services/ILauncher.cs index 3b35e25..adc93ff 100644 --- a/Shared.Services/ILauncher.cs +++ b/Shared.Services/ILauncher.cs @@ -5,4 +5,7 @@ public interface ILauncher public Task LaunchAsync(Uri uri); public Task LaunchFolderAsync(string folderPath); + + public Task LaunchWithAssociatedAppAsync(string filePath); + public Task LaunchWithAssociatedAppAsync(string filePath, string appPath); } diff --git a/Winstrumenta.sln b/Winstrumenta.sln index e75f36a..b45089b 100644 --- a/Winstrumenta.sln +++ b/Winstrumenta.sln @@ -37,7 +37,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Numbers", "Numbers\Numbers. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Numbers.Core", "Numbers.Core\Numbers.Core.csproj", "{CBDCC5AB-D42D-44A5-8B85-BAC8290EEB5B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Numbers.Core.Services", "Numbers.Core.Services\Numbers.Core.Services.csproj", "{874B0591-89AE-4F53-A5FB-7F796661C21A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Numbers.Core.Services.Implementations", "Numbers.Core.Services.Implementations\Numbers.Core.Services.Implementations.csproj", "{874B0591-89AE-4F53-A5FB-7F796661C21A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{B54C9C76-05BD-49DC-9EB8-F31950E887F0}" EndProject @@ -432,7 +432,6 @@ Global GlobalSection(NestedProjects) = preSolution {C27DB2E0-A0B7-4FBB-B375-4F8D197BF3C0} = {0936AB9E-97C3-4BC9-A48E-13544E1DB94E} {918B8A53-8281-41EA-AB86-D83199CAE19E} = {0936AB9E-97C3-4BC9-A48E-13544E1DB94E} - {63302E70-F7C8-4B5D-8749-BDB631663D27} = {0936AB9E-97C3-4BC9-A48E-13544E1DB94E} {E1398C43-BEED-4D92-A194-BC3BAF8BA847} = {0936AB9E-97C3-4BC9-A48E-13544E1DB94E} {AE919A53-9DFB-41F6-ADC8-E62B766B7EE7} = {EBDAA242-5E9F-4BEF-B2CE-0306995C8534} {938B834D-5C5D-4A53-BCBA-F8BF1CC63196} = {EBDAA242-5E9F-4BEF-B2CE-0306995C8534} From a110cb9e31ef9b22120f8f96cba0560ac7159dce Mon Sep 17 00:00:00 2001 From: Gitii Date: Tue, 28 Mar 2023 12:06:59 +0200 Subject: [PATCH 3/4] . --- Numbers.Core/ModelViews/TableViewModel.cs | 33 ++++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/Numbers.Core/ModelViews/TableViewModel.cs b/Numbers.Core/ModelViews/TableViewModel.cs index d1b5fef..cbb9eb9 100644 --- a/Numbers.Core/ModelViews/TableViewModel.cs +++ b/Numbers.Core/ModelViews/TableViewModel.cs @@ -145,17 +145,30 @@ private Task SaveAsync() return Task.CompletedTask; } - UpdateContext( - _context.Table!, - new FileEncoding() - { - Delimiter = this.Delimiter, - Encoding = Encoding, - QuoteCharacter = QuoteCharacter, - } - ); + try + { + UpdateContext( + _context.Table!, + new FileEncoding() + { + Delimiter = this.Delimiter, + Encoding = Encoding, + QuoteCharacter = QuoteCharacter, + } + ); + + return _csv.SaveAsAsync(_context); + } + catch (Exception e) + { + var navParms = new ErrorViewModel.NavigationParameter() { Exception = e }; - return _csv.SaveAsAsync(_context); + _viewStackService + .PushPage(navParms.ToNavigationParameter()) + .Subscribe(); + + return Task.CompletedTask; + } } public ICommand SaveCommand { get; } From 33bae4c92419b1cd3c12b31acf36a0ca946b73be Mon Sep 17 00:00:00 2001 From: Gitii Date: Tue, 28 Mar 2023 12:10:14 +0200 Subject: [PATCH 4/4] Cleanup --- Numbers.Core.Services.Implementations/Csv.cs | 33 ++++++++++---------- Numbers.Core/ModelViews/SaveViewModel.cs | 21 +++++-------- PackageInstaller/Pages/PackageActions.xaml | 3 -- 3 files changed, 24 insertions(+), 33 deletions(-) diff --git a/Numbers.Core.Services.Implementations/Csv.cs b/Numbers.Core.Services.Implementations/Csv.cs index aad21b0..e409867 100644 --- a/Numbers.Core.Services.Implementations/Csv.cs +++ b/Numbers.Core.Services.Implementations/Csv.cs @@ -68,23 +68,22 @@ await csv.WriteRecordsAsync(data.Rows.Select((r) => r.Cells) as IEnumerable) public Task SaveAsAsync(IGlobalContext context) { - if (context.FilePath is null) - { - throw new ArgumentException("file path in context must not be null", nameof(context)); - } - - var encoding = context.FileEncoding; - if (encoding is null) - { - throw new ArgumentException("encoding in context must not be null", nameof(context)); - } - - if (context.Table is null) - { - throw new ArgumentException("csv data in context must not be null", nameof(context)); - } - - return SaveAsAsync(context.Table, encoding.Value, context.FilePath); + var filePath = + context.FilePath + ?? throw new ArgumentException( + "file path in context must not be null", + nameof(context) + ); + + var encoding = + context.FileEncoding + ?? throw new ArgumentException("encoding in context must not be null", nameof(context)); + + var table = + context.Table + ?? throw new ArgumentException("csv data in context must not be null", nameof(context)); + + return SaveAsAsync(table, encoding, filePath); } private async Task<(List, FileEncoding)> ReadRowsAsync( diff --git a/Numbers.Core/ModelViews/SaveViewModel.cs b/Numbers.Core/ModelViews/SaveViewModel.cs index c3d381d..bf77143 100644 --- a/Numbers.Core/ModelViews/SaveViewModel.cs +++ b/Numbers.Core/ModelViews/SaveViewModel.cs @@ -137,21 +137,16 @@ public IObservable WhenNavigatedFrom(INavigationParameter parameter) public IObservable WhenNavigatingTo(INavigationParameter parameter) { - var contextFileEncoding = _context.FileEncoding; - if (contextFileEncoding == null) - { - throw new Exception("Expected global context to have file encoding"); - } + var contextFileEncoding = + _context.FileEncoding + ?? throw new Exception("Expected global context to have file encoding"); - var filePath = _context.FilePath; - if (filePath == null) - { - throw new Exception("Expected global context to have file path"); - } + var filePath = + _context.FilePath ?? throw new Exception("Expected global context to have file path"); - Delimiter = contextFileEncoding.Value.Delimiter; - QuoteCharacter = contextFileEncoding.Value.QuoteCharacter; - Encoding = contextFileEncoding.Value.Encoding; + Delimiter = contextFileEncoding.Delimiter; + QuoteCharacter = contextFileEncoding.QuoteCharacter; + Encoding = contextFileEncoding.Encoding; FileName = Path.GetFileName(filePath); FilePath = Path.GetDirectoryName(filePath) ?? ""; diff --git a/PackageInstaller/Pages/PackageActions.xaml b/PackageInstaller/Pages/PackageActions.xaml index a3ad562..fd0691a 100644 --- a/PackageInstaller/Pages/PackageActions.xaml +++ b/PackageInstaller/Pages/PackageActions.xaml @@ -10,11 +10,8 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:media="using:Microsoft.UI.Xaml.Media.Animation" xmlns:modelViews="using:PackageInstaller.Core.ModelViews" - xmlns:reactiveUi="using:ReactiveUI" xmlns:xaml="using:Microsoft.UI.Xaml" xmlns:b="using:Microsoft.UI.Xaml.Data" - xmlns:w="using:System.Windows" - xmlns:ui="using:CommunityToolkit.WinUI.UI" mc:Ignorable="d" >