From 16829af3301170f1d3cf76b86ee6fea6b83c74c5 Mon Sep 17 00:00:00 2001 From: Shankar Date: Sat, 14 Dec 2024 02:02:25 +0530 Subject: [PATCH 1/8] Miscellaneous - Improved ImageFileResource handling - Navigation changes in Symptum App --- src/Symptum.Common/Helpers/PackageHelper.cs | 3 +- src/Symptum.Common/Helpers/ResourceHelper.cs | 65 +++++++++++++++++-- src/Symptum.Common/Helpers/StorageHelper.cs | 2 +- src/Symptum.Editor/App.xaml.cs | 2 +- .../EditorPages/ImageViewerPage.xaml.cs | 1 + src/Symptum/App.xaml.cs | 2 +- src/Symptum/MainPage.xaml.cs | 2 +- src/Symptum/Navigation/NavigationInfo.cs | 4 +- src/Symptum/Navigation/NavigationManager.cs | 6 +- src/Symptum/Pages/DefaultPage.xaml | 23 +++++++ src/Symptum/Pages/DefaultPage.xaml.cs | 41 ++++++++++++ src/Symptum/Pages/SubjectViewPage.xaml | 26 ++++---- src/Symptum/Pages/SubjectViewPage.xaml.cs | 33 ++++++++++ src/Symptum/Pages/SubjectsPage.xaml | 4 +- 14 files changed, 186 insertions(+), 28 deletions(-) create mode 100644 src/Symptum/Pages/DefaultPage.xaml create mode 100644 src/Symptum/Pages/DefaultPage.xaml.cs diff --git a/src/Symptum.Common/Helpers/PackageHelper.cs b/src/Symptum.Common/Helpers/PackageHelper.cs index e2c34b8..da39774 100644 --- a/src/Symptum.Common/Helpers/PackageHelper.cs +++ b/src/Symptum.Common/Helpers/PackageHelper.cs @@ -150,10 +150,9 @@ public static async Task ExportPackageAsync(IPackageResource? package) if (zipFile != null && zipFile.FileType.Equals(PackageFileExtension, StringComparison.InvariantCultureIgnoreCase) && PackageCacheFolder != null && PackagesFolder != null) { - StorageFolder? targetFolder = await PackageCacheFolder.CreateFolderAsync(zipFile.DisplayName, CreationCollisionOption.ReplaceExisting); Stream zipStream; #if __WASM__ - var buffer = await FileIO.ReadBufferAsync(zipFile); // OpenStreamForReadAsync() crashes on WASM? + var buffer = await FileIO.ReadBufferAsync(zipFile); // NOTE: OpenStreamForReadAsync() crashes on WASM? zipStream = new MemoryStream(buffer.ToArray()); #else zipStream = await zipFile.OpenStreamForReadAsync(); diff --git a/src/Symptum.Common/Helpers/ResourceHelper.cs b/src/Symptum.Common/Helpers/ResourceHelper.cs index 4cae671..3b5f83a 100644 --- a/src/Symptum.Common/Helpers/ResourceHelper.cs +++ b/src/Symptum.Common/Helpers/ResourceHelper.cs @@ -305,6 +305,7 @@ private static async Task LoadImageFileResourceAsync(ImageFileResource imageReso { if (await GetResourceFileAsync(imageResource.FilePath) is StorageFile imgFile) { + imageResource.ImageType = imgFile.FileType.ToLower(); imageFileMap.TryAdd(imageResource.FilePath, imgFile); } } @@ -412,18 +413,29 @@ private static async Task SaveMarkdownFileAsync(MarkdownFileResource markd private static async Task SaveImageFileAsync(ImageFileResource imageResource, StorageFolder? targetFolder = null) { - return true; - if (imageResource == null) return false; //bool pathExists = await VerifyWorkFolderAsync(targetFolder); //if (!pathExists) return false; string subFolderPath = ResourceManager.GetResourceFolderPath(imageResource); string? fileName = ResourceManager.GetResourceFileName(imageResource); - imageResource.FilePath = subFolderPath + fileName + imageResource.ImageType; - //StorageFile? saveFile = await PickSaveFileAsync(fileName, imageResource.ImageType, "Image File", targetFolder, subFolderPath); + string filePath = subFolderPath + fileName + imageResource.ImageType; - return false; + if (filePath.Equals(imageResource.FilePath, StringComparison.InvariantCultureIgnoreCase) && + (targetFolder == null || targetFolder == _workFolder)) // Prevent writing the file onto itself. + return true; + + string? originalFilePath = imageResource.FilePath; + if (!string.IsNullOrEmpty(originalFilePath) && imageFileMap.TryGetValue(originalFilePath, out var imageFile)) + { + imageResource.FilePath = filePath; + + StorageFile? saveFile = await CopyFileAsync(imageFile, fileName, imageResource.ImageType, "Image File", targetFolder, subFolderPath); + imageFileMap.Remove(originalFilePath); + imageFileMap.TryAdd(filePath, saveFile); + } + + return true; } private static async Task SaveMetadataAsync(T resource, StorageFolder? targetFolder = null) where T : MetadataResource @@ -463,6 +475,9 @@ private static async Task SaveChildrenAsync(IResource? resource, StorageFo private static async Task PickSaveFileAsync(string name, string extension, string fileType, StorageFolder? targetFolder = null, string? subFolder = null) { + if (string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(extension) || + string.IsNullOrEmpty(fileType)) return null; + StorageFile saveFile; targetFolder ??= _workFolder; if (targetFolder != null && StorageHelper.IsFolderPickerSupported) @@ -486,6 +501,46 @@ private static async Task SaveChildrenAsync(IResource? resource, StorageFo return saveFile; } + private static async Task CopyFileAsync(StorageFile file, string name, string extension, string fileType, StorageFolder? targetFolder = null, string? subFolder = null) + { + StorageFile? saveFile = file; + targetFolder ??= _workFolder; + + if (string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(extension) || + string.IsNullOrEmpty(fileType)) return saveFile; + + if (targetFolder != null && StorageHelper.IsFolderPickerSupported) + { + var folder = await StorageHelper.CreateSubFoldersAsync(targetFolder, subFolder); + try + { + // NOTE: instead of doing this check here, do it before calling this function. + // This is to prevent copying the file onto itself. + + //StorageFolder parent = await file.GetParentAsync(); + //if (parent != null && folder != null && !parent.Path.Equals(folder?.Path, StringComparison.InvariantCultureIgnoreCase)) + saveFile = await file.CopyAsync(folder, name + extension, NameCollisionOption.ReplaceExisting); + } + catch { } + } + else + { + saveFile = await PickSaveFileAsync(name, extension, fileType, targetFolder, subFolder); + if (saveFile != null) + { + CachedFileManager.DeferUpdates(saveFile); + var source = await file.OpenStreamForReadAsync(); + var destination = await saveFile.OpenStreamForWriteAsync(); + await source.CopyToAsync(destination); + await source.FlushAsync(); + await destination.FlushAsync(); + await CachedFileManager.CompleteUpdatesAsync(saveFile); + } + } + + return saveFile; + } + #endregion #region Removing Resources diff --git a/src/Symptum.Common/Helpers/StorageHelper.cs b/src/Symptum.Common/Helpers/StorageHelper.cs index 22925e5..a3ce605 100644 --- a/src/Symptum.Common/Helpers/StorageHelper.cs +++ b/src/Symptum.Common/Helpers/StorageHelper.cs @@ -109,7 +109,7 @@ private static async Task UpdateArchiveAsync(ZipArchive archive, StorageFolder s { string filePath #if __WASM__ - = Path.Combine(sourceFolder.Path, file.Name); // file.Path returns the file's name and not it's actual path in WASM + = Path.Combine(sourceFolder.Path, file.Name); // NOTE: file.Path returns the file's name and not it's actual path in WASM #else = file.Path; #endif diff --git a/src/Symptum.Editor/App.xaml.cs b/src/Symptum.Editor/App.xaml.cs index 2964a8a..fb979e4 100644 --- a/src/Symptum.Editor/App.xaml.cs +++ b/src/Symptum.Editor/App.xaml.cs @@ -34,7 +34,7 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) WindowHelper.Initialize(MainWindow); #if DEBUG - MainWindow.EnableHotReload(); + MainWindow.UseStudio(); #endif // Do not repeat app initialization when the Window already has content, diff --git a/src/Symptum.Editor/EditorPages/ImageViewerPage.xaml.cs b/src/Symptum.Editor/EditorPages/ImageViewerPage.xaml.cs index 856198f..2fd786f 100644 --- a/src/Symptum.Editor/EditorPages/ImageViewerPage.xaml.cs +++ b/src/Symptum.Editor/EditorPages/ImageViewerPage.xaml.cs @@ -69,6 +69,7 @@ private async void ImageViewerPage_Loaded(object sender, RoutedEventArgs e) } else { + // NOTE: IRandomAccessStream doesn't seem to render in WASM? BitmapImage bitmap = new(); await bitmap.SetSourceAsync(stream); imagePreview.Source = bitmap; diff --git a/src/Symptum/App.xaml.cs b/src/Symptum/App.xaml.cs index 96c7584..598db42 100644 --- a/src/Symptum/App.xaml.cs +++ b/src/Symptum/App.xaml.cs @@ -28,7 +28,7 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) WindowHelper.Initialize(MainWindow); #if DEBUG - MainWindow.EnableHotReload(); + MainWindow.UseStudio(); #endif await ResourceHelper.SelectWorkFolderAsync(await StorageFolder.GetFolderFromPathAsync("E:\\BUS\\Temp\\T")); diff --git a/src/Symptum/MainPage.xaml.cs b/src/Symptum/MainPage.xaml.cs index ea6b011..5dc4819 100644 --- a/src/Symptum/MainPage.xaml.cs +++ b/src/Symptum/MainPage.xaml.cs @@ -47,7 +47,7 @@ private void NavView_Navigate(INavigable? navigable, NavigationTransitionInfo in navigable ??= NavigationManager.HomeNavInfo; Type? pageType = NavigationManager.GetPageTypeForNavigable(navigable); - if (pageType != null && ContentFrame.CurrentSourcePageType != pageType) + if (pageType != null && NavigationManager.CurrentNavigable != navigable) { ContentFrame.Navigate(pageType, navigable, info); } diff --git a/src/Symptum/Navigation/NavigationInfo.cs b/src/Symptum/Navigation/NavigationInfo.cs index 8d957e5..8fe6fae 100644 --- a/src/Symptum/Navigation/NavigationInfo.cs +++ b/src/Symptum/Navigation/NavigationInfo.cs @@ -5,7 +5,7 @@ namespace Symptum.Navigation; // This will be used for data binding items to NavigationView -public class NavigationInfo(Uri? uri, string? title, Type? pageType, IconSource? iconSource = null) : INavigable +public class NavigationInfo(Uri? uri, string? title, Type? pageType, IconSource? iconSource = null, INavigable? backingNavigable = null) : INavigable { public Uri? Uri { get; set; } = uri; @@ -16,4 +16,6 @@ public class NavigationInfo(Uri? uri, string? title, Type? pageType, IconSource? public IconSource? IconSource { get; set; } = iconSource; public ObservableCollection Children { get; } = []; + + public INavigable BackingNavigable { get; set; } = backingNavigable; } diff --git a/src/Symptum/Navigation/NavigationManager.cs b/src/Symptum/Navigation/NavigationManager.cs index 7836b56..8e096a7 100644 --- a/src/Symptum/Navigation/NavigationManager.cs +++ b/src/Symptum/Navigation/NavigationManager.cs @@ -88,8 +88,8 @@ public static void Navigate(INavigable? navigable) { if (navigable is NavigationInfo navInfo) return navInfo.PageType; - else if (navigable is NavigableResource resource) - return typeof(SubjectViewPage); + else if (navigable is NavigableResource) + return typeof(DefaultPage); else return null; } @@ -98,7 +98,7 @@ public static void Navigate(INavigable? navigable) { return navigable switch { - Subject => new(navigable.Uri, navigable.Title, typeof(SubjectViewPage), new FontIconSource() { Glyph = "\uE82D" }), + Subject => new(navigable.Uri, navigable.Title, typeof(SubjectViewPage), new FontIconSource() { Glyph = "\uE82D" }, navigable), _ => null, }; } diff --git a/src/Symptum/Pages/DefaultPage.xaml b/src/Symptum/Pages/DefaultPage.xaml new file mode 100644 index 0000000..f505ce2 --- /dev/null +++ b/src/Symptum/Pages/DefaultPage.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/src/Symptum/Pages/DefaultPage.xaml.cs b/src/Symptum/Pages/DefaultPage.xaml.cs new file mode 100644 index 0000000..2a90f74 --- /dev/null +++ b/src/Symptum/Pages/DefaultPage.xaml.cs @@ -0,0 +1,41 @@ +using Symptum.Core.Management.Navigation; +using Symptum.Navigation; + +namespace Symptum.Pages; + +public sealed partial class DefaultPage : NavigablePage +{ + public DefaultPage() + { + InitializeComponent(); + } + + #region Properties + + public static readonly DependencyProperty NavigableResourceProperty = DependencyProperty.Register( + nameof(NavigableResource), + typeof(NavigableResource), + typeof(DefaultPage), + new(null)); + + public NavigableResource NavigableResource + { + get => (NavigableResource)GetValue(NavigableResourceProperty); + set => SetValue(NavigableResourceProperty, value); + } + + #endregion + + protected override void OnNavigableChanged(INavigable? navigable) + { + if (navigable is NavigableResource resource) + { + NavigableResource = resource; + } + } + + private void Button_Click(object sender, RoutedEventArgs e) + { + NavigationManager.Navigate((sender as Button).Tag as INavigable); + } +} diff --git a/src/Symptum/Pages/SubjectViewPage.xaml b/src/Symptum/Pages/SubjectViewPage.xaml index a5e837d..894fb69 100644 --- a/src/Symptum/Pages/SubjectViewPage.xaml +++ b/src/Symptum/Pages/SubjectViewPage.xaml @@ -2,18 +2,22 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Symptum.Pages" + xmlns:res="using:Symptum.Core.Management.Resources" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - - - - + + + + + + + + + + + + diff --git a/src/Symptum/Pages/SubjectViewPage.xaml.cs b/src/Symptum/Pages/SubjectViewPage.xaml.cs index e894183..acde2b4 100644 --- a/src/Symptum/Pages/SubjectViewPage.xaml.cs +++ b/src/Symptum/Pages/SubjectViewPage.xaml.cs @@ -1,3 +1,7 @@ +using Symptum.Core.Management.Navigation; +using Symptum.Core.Subjects; +using Symptum.Navigation; + namespace Symptum.Pages; public sealed partial class SubjectViewPage : NavigablePage @@ -6,4 +10,33 @@ public SubjectViewPage() { InitializeComponent(); } + + #region Properties + + public static readonly DependencyProperty SubjectProperty = DependencyProperty.Register( + nameof(Subject), + typeof(Subject), + typeof(SubjectViewPage), + new(null)); + + public Subject Subject + { + get => (Subject)GetValue(SubjectProperty); + set => SetValue(SubjectProperty, value); + } + + #endregion + + protected override void OnNavigableChanged(INavigable? navigable) + { + if (navigable is NavigationInfo navInfo && navInfo.BackingNavigable is Subject subject) + { + Subject = subject; + } + } + + private void Button_Click(object sender, RoutedEventArgs e) + { + NavigationManager.Navigate((sender as Button).Tag as INavigable); + } } diff --git a/src/Symptum/Pages/SubjectsPage.xaml b/src/Symptum/Pages/SubjectsPage.xaml index 3e95a5e..ee06e52 100644 --- a/src/Symptum/Pages/SubjectsPage.xaml +++ b/src/Symptum/Pages/SubjectsPage.xaml @@ -14,8 +14,8 @@ - From bcd480223e0cfc79db25c89479b58d710bf540ed Mon Sep 17 00:00:00 2001 From: Shankar Date: Sat, 14 Dec 2024 12:31:53 +0530 Subject: [PATCH 2/8] Resource Handling Improvements - Introduced TextFileResource, MediaFileResource and AudioFileResource - Moved ImageFileResource into FileResource.cs - Refactored ResourceHelper - Nutrient Source Generators: removed unused descriptions - Editor MainPage: added shortcuts for tab management --- src/Symptum.Common/Helpers/ResourceHelper.cs | 234 +++++++----------- .../NutrientQuantityGenerator.cs | 23 +- src/Symptum.Core/Helpers/FileHelper.cs | 11 + .../Management/Resources/ContentFileType.cs | 7 +- .../Management/Resources/CsvFileResource.cs | 9 +- .../Management/Resources/FileResource.cs | 104 ++++---- .../Management/Resources/IContent.cs | 7 + .../Management/Resources/ImageFileResource.cs | 18 -- .../Resources/MarkdownFileResource.cs | 12 +- .../Management/Resources/ResourceManager.cs | 4 +- .../NutrientEditorUIGenerator.cs | 5 +- .../EditorPages/FoodGroupEditorPage.xaml | 2 +- .../EditorPages/ImageViewerPage.xaml | 2 +- .../EditorPages/ImageViewerPage.xaml.cs | 4 +- .../EditorPages/MarkdownEditorPage.xaml | 2 +- .../EditorPages/QuestionTopicEditorPage.xaml | 2 +- .../ReferenceValueGroupEditorPage.xaml | 2 +- src/Symptum.Editor/MainPage.xaml | 31 +++ src/Symptum.Editor/MainPage.xaml.cs | 77 +++++- .../Markdown/TextElements/ImageElement.cs | 82 +++--- 20 files changed, 345 insertions(+), 293 deletions(-) delete mode 100644 src/Symptum.Core/Management/Resources/ImageFileResource.cs diff --git a/src/Symptum.Common/Helpers/ResourceHelper.cs b/src/Symptum.Common/Helpers/ResourceHelper.cs index 3b5f83a..e99d5ad 100644 --- a/src/Symptum.Common/Helpers/ResourceHelper.cs +++ b/src/Symptum.Common/Helpers/ResourceHelper.cs @@ -88,14 +88,14 @@ public static async Task SelectWorkFolderAsync(StorageFolder? folder = nul #endregion - #region Image Resource Handling + #region Resource File Handling - private static readonly Dictionary imageFileMap = []; + private static readonly Dictionary fileMap = []; - public static async Task OpenImageFileForReadAsync(ImageFileResource imageFileResource) + public static async Task OpenFileForReadAsync(FileResource fileResource) { - if (!string.IsNullOrEmpty(imageFileResource.FilePath) && - imageFileMap.TryGetValue(imageFileResource.FilePath, out StorageFile? file)) + if (!string.IsNullOrEmpty(fileResource.FilePath) && + fileMap.TryGetValue(fileResource, out StorageFile? file)) { return await file.OpenReadAsync(); } @@ -135,20 +135,22 @@ public static async Task LoadResourcesFromFilesAsync(IEnumerable? f return await LoadPackageResourceFromFileAsync(file); else if (ImageFileExtensions.Any(ext => ext.Equals(file.FileType, StringComparison.InvariantCultureIgnoreCase))) return await LoadImageFileResourceFromFileAsync(file, parent); + else if (AudioFileExtensions.Any(ext => ext.Equals(file.FileType, StringComparison.InvariantCultureIgnoreCase))) + return await LoadAudioFileResourceFromFileAsync(file, parent); return null; } private static async Task LoadCsvFileResourceFromFileAsync(StorageFile? file, IResource? parent) { - if (file != null /*&& file.FileType.Equals(CsvFileExtension, StringComparison.InvariantCultureIgnoreCase)*/) + if (file != null) { string csv = await FileIO.ReadTextAsync(file); if (CsvResourceHelper.TryGetCsvResourceType(csv, out Type? csvType) && Activator.CreateInstance(csvType) is CsvFileResource csvFileResource) { csvFileResource.Title = file.DisplayName; - ResourceManager.LoadResourceFile(csvFileResource, csv); + ResourceManager.LoadResourceFileText(csvFileResource, csv); if (parent != null && parent.CanAddChildResourceType(csvType)) parent.AddChildResource(csvFileResource); @@ -164,7 +166,7 @@ public static async Task LoadResourcesFromFilesAsync(IEnumerable? f private static async Task LoadMarkdownFileResourceFromFileAsync(StorageFile? file, IResource? parent) { - if (file != null /*&& file.FileType.Equals(MarkdownFileExtension, StringComparison.InvariantCultureIgnoreCase)*/) + if (file != null) { string md = await FileIO.ReadTextAsync(file); @@ -172,7 +174,7 @@ public static async Task LoadResourcesFromFilesAsync(IEnumerable? f { Title = file.DisplayName }; - ResourceManager.LoadResourceFile(markdownFileResource, md); + ResourceManager.LoadResourceFileText(markdownFileResource, md); if (parent != null && parent.CanAddChildResourceType(typeof(MarkdownFileResource))) parent.AddChildResource(markdownFileResource); @@ -192,11 +194,11 @@ public static async Task LoadResourcesFromFilesAsync(IEnumerable? f ImageFileResource imageFileResource = new() { Title = file.DisplayName, - ImageType = file.FileType.ToLower(), FilePath = file.Path }; + imageFileResource.SetMediaFileExtension(file.FileType.ToLower()); - imageFileMap.TryAdd(file.Path, file); + fileMap.TryAdd(imageFileResource, file); if (parent != null && parent.CanAddChildResourceType(typeof(ImageFileResource))) parent.AddChildResource(imageFileResource); @@ -209,9 +211,33 @@ public static async Task LoadResourcesFromFilesAsync(IEnumerable? f return null; } + private static async Task LoadAudioFileResourceFromFileAsync(StorageFile file, IResource? parent) + { + if (file != null) + { + AudioFileResource audioFileResource = new() + { + Title = file.DisplayName, + FilePath = file.Path + }; + audioFileResource.SetMediaFileExtension(file.FileType.ToLower()); + + fileMap.TryAdd(audioFileResource, file); + + if (parent != null && parent.CanAddChildResourceType(typeof(AudioFileResource))) + parent.AddChildResource(audioFileResource); + else + ResourceManager.Resources.Add(audioFileResource); + + return audioFileResource; + } + + return null; + } + internal static async Task LoadPackageResourceFromFileAsync(StorageFile? file) { - if (file != null /*&& file.FileType.Equals(JsonFileExtension, StringComparison.InvariantCultureIgnoreCase)*/) + if (file != null) { string json = await FileIO.ReadTextAsync(file); var package = ResourceManager.LoadPackageFromMetadata(json); @@ -229,19 +255,16 @@ public static async Task LoadResourcesFromFilesAsync(IEnumerable? f public static async Task LoadResourceAsync(IResource resource, IResource? parent = null) { - if (resource is CsvFileResource csvResource) - { - await LoadCSVFileResourceAsync(csvResource); - resource.InitializeResource(parent); - } - else if (resource is MarkdownFileResource markdownResource) + if (resource is TextFileResource textResource) { - await LoadMarkdownFileResourceAsync(markdownResource); + // CSVs and Markdowns + await LoadTextFileResourceAsync(textResource); resource.InitializeResource(parent); } - else if (resource is ImageFileResource imageResource) + else if (resource is MediaFileResource mediaResource) { - await LoadImageFileResourceAsync(imageResource); + // Images and Audios + await LoadMediaFileResourceAsync(mediaResource); resource.InitializeResource(parent); } else @@ -283,30 +306,21 @@ private static async Task LoadChildrenResourcesAsync(IResource resource) return null; } - private static async Task LoadCSVFileResourceAsync(CsvFileResource csvResource) + private static async Task LoadTextFileResourceAsync(TextFileResource textResource) { - if (await GetResourceFileAsync(csvResource.FilePath) is StorageFile csvFile) + if (await GetResourceFileAsync(textResource.FilePath) is StorageFile textFile) { - string text = await FileIO.ReadTextAsync(csvFile); - ResourceManager.LoadResourceFile(csvResource, text); + string text = await FileIO.ReadTextAsync(textFile); + ResourceManager.LoadResourceFileText(textResource, text); } } - private static async Task LoadMarkdownFileResourceAsync(MarkdownFileResource markdownResource) + private static async Task LoadMediaFileResourceAsync(MediaFileResource mediaResource) { - if (await GetResourceFileAsync(markdownResource.FilePath) is StorageFile mdFile) + if (await GetResourceFileAsync(mediaResource.FilePath) is StorageFile mediaFile) { - string text = await FileIO.ReadTextAsync(mdFile); - ResourceManager.LoadResourceFile(markdownResource, text); - } - } - - private static async Task LoadImageFileResourceAsync(ImageFileResource imageResource) - { - if (await GetResourceFileAsync(imageResource.FilePath) is StorageFile imgFile) - { - imageResource.ImageType = imgFile.FileType.ToLower(); - imageFileMap.TryAdd(imageResource.FilePath, imgFile); + mediaResource.SetMediaFileExtension(mediaFile.FileType.ToLower()); + fileMap.TryAdd(mediaResource, mediaFile); } } @@ -342,17 +356,15 @@ public static async Task SaveResourceAsync(IResource resource, StorageFold { if (resource == null) return false; - if (resource is CsvFileResource csvResource) - { - return await SaveCSVFileAsync(csvResource, targetFolder); - } - else if (resource is MarkdownFileResource markdownResource) + if (resource is TextFileResource textResource) { - return await SaveMarkdownFileAsync(markdownResource, targetFolder); + // CSVs and Markdowns + return await SaveTextFileResourceAsync(textResource, targetFolder); } - else if (resource is ImageFileResource imageResource) + else if (resource is MediaFileResource mediaResource) { - return await SaveImageFileAsync(imageResource, targetFolder); + // Images and Audios + return await CopySaveFileResourceAsync(mediaResource, targetFolder); } else { @@ -371,68 +383,44 @@ public static async Task SaveResourceAsync(IResource resource, StorageFold } } - private static async Task SaveCSVFileAsync(CsvFileResource csvResource, StorageFolder? targetFolder = null) + private static async Task SaveTextFileResourceAsync(TextFileResource textResource, StorageFolder? targetFolder = null) { - if (csvResource == null) return false; - //bool pathExists = await VerifyWorkFolderAsync(targetFolder); - //if (!pathExists) return false; + if (textResource == null) return false; - string subFolderPath = ResourceManager.GetResourceFolderPath(csvResource); - string? fileName = ResourceManager.GetResourceFileName(csvResource); - csvResource.FilePath = subFolderPath + fileName + CsvFileExtension; - StorageFile? saveFile = await PickSaveFileAsync(fileName, CsvFileExtension, "CSV File", targetFolder, subFolderPath); + string subFolderPath = ResourceManager.GetResourceFolderPath(textResource); + string? fileName = ResourceManager.GetResourceFileName(textResource); + textResource.FilePath = subFolderPath + fileName + textResource.FileExtension; + StorageFile? saveFile = await PickSaveFileAsync(fileName, textResource.FileExtension, $"{textResource.FileType} File", targetFolder, subFolderPath); if (saveFile != null) { - string? text = ResourceManager.WriteResourceFileText(csvResource); - return await StorageHelper.WriteToFileAsync(saveFile, text); + string? text = ResourceManager.WriteResourceFileText(textResource); + if (!string.IsNullOrEmpty(text)) + return await StorageHelper.WriteToFileAsync(saveFile, text); } return false; } - private static async Task SaveMarkdownFileAsync(MarkdownFileResource markdownResource, StorageFolder? targetFolder = null) + private static async Task CopySaveFileResourceAsync(FileResource fileResource, StorageFolder? targetFolder = null) { - if (markdownResource == null) return false; - //bool pathExists = await VerifyWorkFolderAsync(targetFolder); - //if (!pathExists) return false; + if (fileResource == null) return false; - string subFolderPath = ResourceManager.GetResourceFolderPath(markdownResource); - string? fileName = ResourceManager.GetResourceFileName(markdownResource); - markdownResource.FilePath = subFolderPath + fileName + MarkdownFileExtension; - StorageFile? saveFile = await PickSaveFileAsync(fileName, MarkdownFileExtension, "Markdown File", targetFolder, subFolderPath); + string subFolderPath = ResourceManager.GetResourceFolderPath(fileResource); + string? fileName = ResourceManager.GetResourceFileName(fileResource); + string filePath = subFolderPath + fileName + fileResource.FileExtension; - if (saveFile != null) - { - string? text = ResourceManager.WriteResourceFileText(markdownResource); - return await StorageHelper.WriteToFileAsync(saveFile, text); - } - - return false; - } - - private static async Task SaveImageFileAsync(ImageFileResource imageResource, StorageFolder? targetFolder = null) - { - if (imageResource == null) return false; - //bool pathExists = await VerifyWorkFolderAsync(targetFolder); - //if (!pathExists) return false; - - string subFolderPath = ResourceManager.GetResourceFolderPath(imageResource); - string? fileName = ResourceManager.GetResourceFileName(imageResource); - string filePath = subFolderPath + fileName + imageResource.ImageType; - - if (filePath.Equals(imageResource.FilePath, StringComparison.InvariantCultureIgnoreCase) && + if (filePath.Equals(fileResource.FilePath, StringComparison.InvariantCultureIgnoreCase) && (targetFolder == null || targetFolder == _workFolder)) // Prevent writing the file onto itself. return true; - string? originalFilePath = imageResource.FilePath; - if (!string.IsNullOrEmpty(originalFilePath) && imageFileMap.TryGetValue(originalFilePath, out var imageFile)) + if (fileMap.TryGetValue(fileResource, out var file)) { - imageResource.FilePath = filePath; + fileResource.FilePath = filePath; - StorageFile? saveFile = await CopyFileAsync(imageFile, fileName, imageResource.ImageType, "Image File", targetFolder, subFolderPath); - imageFileMap.Remove(originalFilePath); - imageFileMap.TryAdd(filePath, saveFile); + StorageFile? saveFile = await CopyFileAsync(file, fileName, fileResource.FileExtension, $"{fileResource.FileType} File", targetFolder, subFolderPath); + fileMap.Remove(fileResource); + if (saveFile != null) fileMap.TryAdd(fileResource, saveFile); } return true; @@ -441,8 +429,6 @@ private static async Task SaveImageFileAsync(ImageFileResource imageResour private static async Task SaveMetadataAsync(T resource, StorageFolder? targetFolder = null) where T : MetadataResource { if (resource == null) return false; - //bool pathExists = await VerifyWorkFolderAsync(targetFolder); - //if (!pathExists) return false; string subFolderPath = ResourceManager.GetResourceFolderPath(resource); string? fileName = ResourceManager.GetResourceFileName(resource); @@ -473,17 +459,17 @@ private static async Task SaveChildrenAsync(IResource? resource, StorageFo return true; } - private static async Task PickSaveFileAsync(string name, string extension, string fileType, StorageFolder? targetFolder = null, string? subFolder = null) + private static async Task PickSaveFileAsync(string? name, string? extension, string? fileType, StorageFolder? targetFolder = null, string? subFolder = null) { if (string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(extension) || string.IsNullOrEmpty(fileType)) return null; - StorageFile saveFile; + StorageFile? saveFile = null; targetFolder ??= _workFolder; if (targetFolder != null && StorageHelper.IsFolderPickerSupported) { var folder = await StorageHelper.CreateSubFoldersAsync(targetFolder, subFolder); - saveFile = await folder?.CreateFileAsync(name + extension, CreationCollisionOption.ReplaceExisting); + if (folder != null) saveFile = await folder.CreateFileAsync(name + extension, CreationCollisionOption.ReplaceExisting); } else { @@ -501,12 +487,12 @@ private static async Task SaveChildrenAsync(IResource? resource, StorageFo return saveFile; } - private static async Task CopyFileAsync(StorageFile file, string name, string extension, string fileType, StorageFolder? targetFolder = null, string? subFolder = null) + private static async Task CopyFileAsync(StorageFile? file, string? name, string? extension, string? fileType, StorageFolder? targetFolder = null, string? subFolder = null) { StorageFile? saveFile = file; targetFolder ??= _workFolder; - if (string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(extension) || + if (file == null || string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(extension) || string.IsNullOrEmpty(fileType)) return saveFile; if (targetFolder != null && StorageHelper.IsFolderPickerSupported) @@ -519,7 +505,7 @@ private static async Task SaveChildrenAsync(IResource? resource, StorageFo //StorageFolder parent = await file.GetParentAsync(); //if (parent != null && folder != null && !parent.Path.Equals(folder?.Path, StringComparison.InvariantCultureIgnoreCase)) - saveFile = await file.CopyAsync(folder, name + extension, NameCollisionOption.ReplaceExisting); + if (folder != null) saveFile = await file.CopyAsync(folder, name + extension, NameCollisionOption.ReplaceExisting); } catch { } } @@ -551,12 +537,12 @@ public static async Task RemoveResourceAsync(IResource? resource, bool delete = await RemoveChildrenAsync(resource, delete); - if (delete && resource is CsvFileResource csvResource) - await DeleteCSVFileAsync(csvResource); - else if (delete && resource is MarkdownFileResource markdownResource) - await DeleteMarkdownFileAsync(markdownResource); - else if (delete && resource is ImageFileResource imageResource) - await DeleteImageFileAsync(imageResource); + if (resource is FileResource fileResource) + { + if (fileResource is MediaFileResource) + fileMap.Remove(fileResource); // Images and Audios + if (delete) await DeleteResourceFileAsync(fileResource); + } else if (delete && resource is MetadataResource metadataResource) await DeleteMetadataAsync(metadataResource); @@ -570,48 +556,18 @@ public static async Task RemoveResourceAsync(IResource? resource, bool delete = ResourceManager.UnregisterResource(resource); } - private static async Task DeleteCSVFileAsync(CsvFileResource? csvResource) + private static async Task DeleteResourceFileAsync(FileResource? fileResource) { - if (csvResource == null) return; - if (_workFolder != null && StorageHelper.IsFolderPickerSupported) - { - try - { - string path = ResourceManager.GetResourceFolderPath(csvResource); - var folder = await StorageHelper.GetSubFolderAsync(_workFolder, path); - IStorageItem? file = await folder?.TryGetItemAsync(ResourceManager.GetResourceFileName(csvResource) + CsvFileExtension); - if (file != null) await file.DeleteAsync(); - } - catch { } - } - } - - private static async Task DeleteMarkdownFileAsync(MarkdownFileResource? markdownResource) - { - if (markdownResource == null) return; - if (_workFolder != null && StorageHelper.IsFolderPickerSupported) - { - try - { - string path = ResourceManager.GetResourceFolderPath(markdownResource); - var folder = await StorageHelper.GetSubFolderAsync(_workFolder, path); - IStorageItem? file = await folder?.TryGetItemAsync(ResourceManager.GetResourceFileName(markdownResource) + MarkdownFileExtension); - if (file != null) await file.DeleteAsync(); - } - catch { } - } - } + if (fileResource == null) return; - private static async Task DeleteImageFileAsync(ImageFileResource? imageResource) - { - if (imageResource == null) return; if (_workFolder != null && StorageHelper.IsFolderPickerSupported) { try { - string path = ResourceManager.GetResourceFolderPath(imageResource); + string path = ResourceManager.GetResourceFolderPath(fileResource); var folder = await StorageHelper.GetSubFolderAsync(_workFolder, path); - IStorageItem? file = await folder?.TryGetItemAsync(ResourceManager.GetResourceFileName(imageResource) + imageResource.ImageType); + if (folder == null) return; + IStorageItem? file = await folder.TryGetItemAsync(ResourceManager.GetResourceFileName(fileResource) + fileResource.FileExtension); if (file != null) await file.DeleteAsync(); } catch { } diff --git a/src/Symptum.Core.SourceGenerators/NutrientQuantityGenerator.cs b/src/Symptum.Core.SourceGenerators/NutrientQuantityGenerator.cs index 2fb462c..a5a3357 100644 --- a/src/Symptum.Core.SourceGenerators/NutrientQuantityGenerator.cs +++ b/src/Symptum.Core.SourceGenerators/NutrientQuantityGenerator.cs @@ -14,8 +14,8 @@ public class NutrientQuantityGenerator : ISourceGenerator private static (string? fieldName, string? propertyName, string? header, string? unit, string? comment, bool isComment) ParseLine(string line) { - List fieldName = []; - List propertyName = []; + StringBuilder fieldName = new(); + StringBuilder propertyName = new(); string? header = null; string? unit = null; string? comment = null; @@ -39,20 +39,19 @@ private static (string? fieldName, string? propertyName, string? header, string? unitIndex = i; else if (char.IsLetter(ch)) { - fieldName.Add(firstChar ? char.ToLower(ch) : ch); - propertyName.Add(firstChar ? char.ToUpper(ch) : ch); + fieldName.Append(firstChar ? char.ToLower(ch) : ch); + propertyName.Append(firstChar ? char.ToUpper(ch) : ch); firstChar = false; } else if (char.IsDigit(ch)) { if (firstChar) { - fieldName.Add('_'); - fieldName.Add('_'); // Add "__" to field for distinguishment - propertyName.Add('_'); + fieldName.Append("__"); // Add "__" to field for distinguishment + propertyName.Append('_'); } - fieldName.Add(ch); - propertyName.Add(ch); + fieldName.Append(ch); + propertyName.Append(ch); firstChar = false; } } @@ -76,7 +75,7 @@ private static (string? fieldName, string? propertyName, string? header, string? return firstChar ? (null, null, null, null, comment, true) : - (new([.. fieldName]), new([.. propertyName]), header, unit, comment, false); + (fieldName.ToString(), propertyName.ToString(), header, unit, comment, false); } public void Execute(GeneratorExecutionContext context) @@ -103,10 +102,12 @@ public partial class Food (string? fieldName, string? propertyName, string? header, string? unit, string? comment, bool isComment) = ParseLine(line); if (isComment) continue; + string desc = !string.IsNullOrEmpty(comment) ? $@", Description = ""{comment}""" : string.Empty; + source.Append($@" private Quantity? {fieldName}; - [GenerateUI(Header = ""{header} (in {unit})"", Description = ""{comment}"")] + [GenerateUI(Header = ""{header} (in {unit})""{desc})] [TypeConverter(typeof(QuantityCsvConverter))] public Quantity? {propertyName} {{ diff --git a/src/Symptum.Core/Helpers/FileHelper.cs b/src/Symptum.Core/Helpers/FileHelper.cs index 841889b..7f88210 100644 --- a/src/Symptum.Core/Helpers/FileHelper.cs +++ b/src/Symptum.Core/Helpers/FileHelper.cs @@ -37,6 +37,17 @@ public static class FileHelper #endregion + #region Audio File Extensions + + public static readonly string[] AudioFileExtensions = + [ + Mp3FileExtension + ]; + + public const string Mp3FileExtension = ".mp3"; + + #endregion + public static (string folder, string fileName, string extension) GetDetailsFromFilePath(string? filePath) { string folder = string.Empty; diff --git a/src/Symptum.Core/Management/Resources/ContentFileType.cs b/src/Symptum.Core/Management/Resources/ContentFileType.cs index 627c0cb..6d9c933 100644 --- a/src/Symptum.Core/Management/Resources/ContentFileType.cs +++ b/src/Symptum.Core/Management/Resources/ContentFileType.cs @@ -1,8 +1,9 @@ -namespace Symptum.Core.Management.Resources; +namespace Symptum.Core.Management.Resources; public enum ContentFileType { Markdown, Csv, - Image -} \ No newline at end of file + Image, + Audio +} diff --git a/src/Symptum.Core/Management/Resources/CsvFileResource.cs b/src/Symptum.Core/Management/Resources/CsvFileResource.cs index 802c6ad..4a1b76c 100644 --- a/src/Symptum.Core/Management/Resources/CsvFileResource.cs +++ b/src/Symptum.Core/Management/Resources/CsvFileResource.cs @@ -1,16 +1,21 @@ using System.Text.Json.Serialization; +using static Symptum.Core.Helpers.FileHelper; namespace Symptum.Core.Management.Resources; -public abstract class CsvFileResource : FileResource +public abstract partial class CsvFileResource : TextFileResource { + public CsvFileResource() + { + FileExtension = CsvFileExtension; + } [JsonIgnore] public override ContentFileType FileType { get; } = ContentFileType.Csv; protected override void OnReadFileText(string text) => OnReadCSV(text); - protected override string OnWriteFileText() => OnWriteCSV(); + protected override string? OnWriteFileText() => OnWriteCSV(); protected abstract void OnReadCSV(string csv); diff --git a/src/Symptum.Core/Management/Resources/FileResource.cs b/src/Symptum.Core/Management/Resources/FileResource.cs index 38cf5ab..7e83254 100644 --- a/src/Symptum.Core/Management/Resources/FileResource.cs +++ b/src/Symptum.Core/Management/Resources/FileResource.cs @@ -1,80 +1,43 @@ using System.Text.Json.Serialization; +using CommunityToolkit.Mvvm.ComponentModel; using Symptum.Core.Data; using Symptum.Core.Management.Navigation; namespace Symptum.Core.Management.Resources; -public abstract class FileResource : NavigableResource, IContent +public abstract partial class FileResource : NavigableResource, IContent { #region Properties - #region IContent + [ObservableProperty] + public partial string? FilePath { get; set; } [JsonIgnore] - public abstract ContentFileType FileType { get; } - - private string? description; - - public string? Description - { - get => description; - set => SetProperty(ref description, value); - } - - private IList? authors; - - public IList? Authors - { - get => authors; - set => SetProperty(ref authors, value); - } - - private DateOnly? dateModified; - - public DateOnly? DateModified - { - get => dateModified; - set => SetProperty(ref dateModified, value); - } + [ObservableProperty] + public partial string? FileExtension { get; protected set; } - private IList? tags; - - public IList? Tags - { - get => tags; - set => SetProperty(ref tags, value); - } + [JsonIgnore] + public abstract ContentFileType FileType { get; } - private IList? seeAlso; + [ObservableProperty] + public partial string? Description { get; set; } - public IList? SeeAlso - { - get => seeAlso; - set => SetProperty(ref seeAlso, value); - } + [ObservableProperty] + public partial IList? Authors { get; set; } - #endregion + [ObservableProperty] + public partial DateOnly? DateModified { get; set; } - private string? filePath; + [ObservableProperty] + public partial IList? Tags { get; set; } - public string? FilePath - { - get => filePath; - set => SetProperty(ref filePath, value); - } + [ObservableProperty] + public partial IList? SeeAlso { get; set; } #endregion protected override void OnInitializeResource(IResource? parent) { } - internal void ReadFileText(string content) => OnReadFileText(content); - - internal string WriteFileText() => OnWriteFileText(); - - protected abstract void OnReadFileText(string content); - - protected abstract string OnWriteFileText(); - #region Ignore // Since the instances of FileResource are always the end resources (i.e. no children), @@ -93,3 +56,34 @@ protected override void OnInitializeResource(IResource? parent) { } #endregion } + +public abstract class TextFileResource : FileResource +{ + internal void ReadFileText(string content) => OnReadFileText(content); + + internal string? WriteFileText() => OnWriteFileText(); + + protected abstract void OnReadFileText(string content); + + protected abstract string? OnWriteFileText(); +} + +public abstract class MediaFileResource : FileResource +{ + public void SetMediaFileExtension(string? extension) + { + FileExtension = extension; + } +} + +public sealed partial class ImageFileResource : MediaFileResource +{ + [JsonIgnore] + public override ContentFileType FileType { get; } = ContentFileType.Image; +} + +public sealed partial class AudioFileResource : MediaFileResource +{ + [JsonIgnore] + public override ContentFileType FileType { get; } = ContentFileType.Audio; +} diff --git a/src/Symptum.Core/Management/Resources/IContent.cs b/src/Symptum.Core/Management/Resources/IContent.cs index 6f9719b..173fce1 100644 --- a/src/Symptum.Core/Management/Resources/IContent.cs +++ b/src/Symptum.Core/Management/Resources/IContent.cs @@ -1,9 +1,16 @@ +using System.Text.Json.Serialization; using Symptum.Core.Data; namespace Symptum.Core.Management.Resources; public interface IContent : IResource { + public string? FilePath { get; set; } + + [JsonIgnore] + public string? FileExtension { get; } + + [JsonIgnore] public abstract ContentFileType FileType { get; } public string? Description { get; set; } diff --git a/src/Symptum.Core/Management/Resources/ImageFileResource.cs b/src/Symptum.Core/Management/Resources/ImageFileResource.cs deleted file mode 100644 index e100342..0000000 --- a/src/Symptum.Core/Management/Resources/ImageFileResource.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json.Serialization; -using CommunityToolkit.Mvvm.ComponentModel; - -namespace Symptum.Core.Management.Resources; - -public sealed partial class ImageFileResource : FileResource -{ - [JsonIgnore] - public override ContentFileType FileType { get; } = ContentFileType.Image; - - [JsonIgnore] - [ObservableProperty] - public partial string ImageType { get; set; } - - protected override void OnReadFileText(string content) => throw new NotImplementedException(); - - protected override string OnWriteFileText() => throw new NotImplementedException(); -} diff --git a/src/Symptum.Core/Management/Resources/MarkdownFileResource.cs b/src/Symptum.Core/Management/Resources/MarkdownFileResource.cs index 943ea1d..3b115b5 100644 --- a/src/Symptum.Core/Management/Resources/MarkdownFileResource.cs +++ b/src/Symptum.Core/Management/Resources/MarkdownFileResource.cs @@ -1,18 +1,24 @@ using System.Text.Json.Serialization; using CommunityToolkit.Mvvm.ComponentModel; +using static Symptum.Core.Helpers.FileHelper; namespace Symptum.Core.Management.Resources; -public sealed partial class MarkdownFileResource : FileResource +public sealed partial class MarkdownFileResource : TextFileResource { + public MarkdownFileResource() + { + FileExtension = MarkdownFileExtension; + } + [JsonIgnore] public override ContentFileType FileType { get; } = ContentFileType.Markdown; [JsonIgnore] [ObservableProperty] - public partial string Markdown { get; set; } + public partial string? Markdown { get; set; } protected override void OnReadFileText(string content) => Markdown = content; - protected override string OnWriteFileText() => Markdown; + protected override string? OnWriteFileText() => Markdown; } diff --git a/src/Symptum.Core/Management/Resources/ResourceManager.cs b/src/Symptum.Core/Management/Resources/ResourceManager.cs index bd33bf2..e9f7efd 100644 --- a/src/Symptum.Core/Management/Resources/ResourceManager.cs +++ b/src/Symptum.Core/Management/Resources/ResourceManager.cs @@ -40,9 +40,9 @@ public static string GetResourceFilePath(IResource? resource, string? extension) return path + GetResourceFileName(resource) + extension; } - public static void LoadResourceFile(FileResource? fileResource, string text) => fileResource?.ReadFileText(text); + public static void LoadResourceFileText(TextFileResource? fileResource, string text) => fileResource?.ReadFileText(text); - public static string? WriteResourceFileText(FileResource? fileResource) => fileResource?.WriteFileText(); + public static string? WriteResourceFileText(TextFileResource? fileResource) => fileResource?.WriteFileText(); public static PackageResource? LoadPackageFromMetadata(string metadata) => JsonSerializer.Deserialize(metadata); diff --git a/src/Symptum.Editor.SourceGenerators/NutrientEditorUIGenerator.cs b/src/Symptum.Editor.SourceGenerators/NutrientEditorUIGenerator.cs index 8187d8d..b490b9f 100644 --- a/src/Symptum.Editor.SourceGenerators/NutrientEditorUIGenerator.cs +++ b/src/Symptum.Editor.SourceGenerators/NutrientEditorUIGenerator.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Xml.Linq; using Microsoft.CodeAnalysis; namespace Symptum.Editor.SourceGenerators; @@ -67,9 +68,11 @@ private void GenerateControls(StringBuilder source, IEnumerable description = data.NamedArguments.FirstOrDefault(x => x.Key == "Description").Value.Value?.ToString(); } + string desc = !string.IsNullOrEmpty(description) ? $@", Description = ""{description}""" : string.Empty; + // Creating fields source.Append($@" - private readonly TextBox {controlName} = new() {{ Header = ""{header}"", Description = ""{description}"" }};"); + private readonly TextBox {controlName} = new() {{ Header = ""{header}""{desc} }};"); // We are doing the other gens in a single loop; addControls.Append($@" diff --git a/src/Symptum.Editor/EditorPages/FoodGroupEditorPage.xaml b/src/Symptum.Editor/EditorPages/FoodGroupEditorPage.xaml index f8bbb11..e682abc 100644 --- a/src/Symptum.Editor/EditorPages/FoodGroupEditorPage.xaml +++ b/src/Symptum.Editor/EditorPages/FoodGroupEditorPage.xaml @@ -99,7 +99,7 @@ - + diff --git a/src/Symptum.Editor/EditorPages/ImageViewerPage.xaml b/src/Symptum.Editor/EditorPages/ImageViewerPage.xaml index dfc394f..de09f21 100644 --- a/src/Symptum.Editor/EditorPages/ImageViewerPage.xaml +++ b/src/Symptum.Editor/EditorPages/ImageViewerPage.xaml @@ -42,7 +42,7 @@ HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"> - + diff --git a/src/Symptum.Editor/EditorPages/ImageViewerPage.xaml.cs b/src/Symptum.Editor/EditorPages/ImageViewerPage.xaml.cs index 2fd786f..de88da2 100644 --- a/src/Symptum.Editor/EditorPages/ImageViewerPage.xaml.cs +++ b/src/Symptum.Editor/EditorPages/ImageViewerPage.xaml.cs @@ -56,11 +56,11 @@ private async void ImageViewerPage_Loaded(object sender, RoutedEventArgs e) _imageFileResource = imageFileResource; - var stream = await ResourceHelper.OpenImageFileForReadAsync(imageFileResource); + var stream = await ResourceHelper.OpenFileForReadAsync(imageFileResource); if (stream == null) return; Vector2 availableSize = scrollViewer.ActualSize; - if (imageFileResource.ImageType.Equals(SvgFileExtension, StringComparison.InvariantCultureIgnoreCase)) + if (SvgFileExtension.Equals(imageFileResource.FileExtension, StringComparison.InvariantCultureIgnoreCase)) { SvgImageSource svg = new(); await svg.SetSourceAsync(stream); diff --git a/src/Symptum.Editor/EditorPages/MarkdownEditorPage.xaml b/src/Symptum.Editor/EditorPages/MarkdownEditorPage.xaml index b925bf3..ac5bffd 100644 --- a/src/Symptum.Editor/EditorPages/MarkdownEditorPage.xaml +++ b/src/Symptum.Editor/EditorPages/MarkdownEditorPage.xaml @@ -320,7 +320,7 @@ - + diff --git a/src/Symptum.Editor/EditorPages/QuestionTopicEditorPage.xaml b/src/Symptum.Editor/EditorPages/QuestionTopicEditorPage.xaml index beb6d71..ec280dc 100644 --- a/src/Symptum.Editor/EditorPages/QuestionTopicEditorPage.xaml +++ b/src/Symptum.Editor/EditorPages/QuestionTopicEditorPage.xaml @@ -118,7 +118,7 @@ - + diff --git a/src/Symptum.Editor/EditorPages/ReferenceValueGroupEditorPage.xaml b/src/Symptum.Editor/EditorPages/ReferenceValueGroupEditorPage.xaml index 41b56dc..cc07deb 100644 --- a/src/Symptum.Editor/EditorPages/ReferenceValueGroupEditorPage.xaml +++ b/src/Symptum.Editor/EditorPages/ReferenceValueGroupEditorPage.xaml @@ -95,7 +95,7 @@ - + diff --git a/src/Symptum.Editor/MainPage.xaml b/src/Symptum.Editor/MainPage.xaml index 8bfeead..f071793 100644 --- a/src/Symptum.Editor/MainPage.xaml +++ b/src/Symptum.Editor/MainPage.xaml @@ -103,6 +103,25 @@ + + + + + + + + + + + + + + + + + + + @@ -176,6 +195,18 @@ + + + + + + + + + + + + diff --git a/src/Symptum.Editor/MainPage.xaml.cs b/src/Symptum.Editor/MainPage.xaml.cs index 29f7f95..f9db6d3 100644 --- a/src/Symptum.Editor/MainPage.xaml.cs +++ b/src/Symptum.Editor/MainPage.xaml.cs @@ -12,13 +12,13 @@ using CsvHelper; using System.Globalization; using Symptum.Core.Extensions; +using Microsoft.UI.Xaml.Input; namespace Symptum.Editor; public sealed partial class MainPage : Page { private bool _collapsed = false; - private bool _showResourcesPane = true; private readonly AddNewItemDialog addNewItemDialog = new(); private readonly QuestionBankContextConfigureDialog contextConfigureDialog = new(); @@ -79,6 +79,28 @@ public MainPage() SizeChanged += MainPage_SizeChanged; } + #region Properties + + public static DependencyProperty ShowResourcesPaneProperty = DependencyProperty.Register( + nameof(ShowResourcesPane), + typeof(bool), + typeof(MainPage), + new(true, OnShowResourcesPaneProperty)); + + private static void OnShowResourcesPaneProperty(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is MainPage page) + page.ShowOrHideResourcesPane((bool)e.NewValue); + } + + public bool ShowResourcesPane + { + get => (bool)GetValue(ShowResourcesPaneProperty); + set => SetValue(ShowResourcesPaneProperty, value); + } + + #endregion + private void MainPage_SizeChanged(object sender, SizeChangedEventArgs args) { bool collapsed = args.NewSize.Width switch @@ -90,7 +112,7 @@ private void MainPage_SizeChanged(object sender, SizeChangedEventArgs args) if (collapsed != _collapsed) { _collapsed = collapsed; - VisualStateManager.GoToState(this, collapsed || !_showResourcesPane ? "MinimalState" : "DefaultState", true); + VisualStateManager.GoToState(this, collapsed || !ShowResourcesPane ? "MinimalState" : "DefaultState", true); } } @@ -322,14 +344,6 @@ private void MultiSelectButton_Click(object sender, RoutedEventArgs e) UpdateDeleteButtonEnabled(); } - private void ShowResourcesPaneButton_Click(object sender, RoutedEventArgs e) - { - _showResourcesPane = !_showResourcesPane; - ToolTipService.SetToolTip(showResourcesPaneButton, _showResourcesPane ? "Unpin" : "Pin"); - resourcesPaneButtonSymbolIcon.Symbol = _showResourcesPane ? Symbol.UnPin : Symbol.Pin; - VisualStateManager.GoToState(this, _showResourcesPane && !_collapsed ? "DefaultState" : "MinimalState", true); - } - private void UpdateDeleteButtonEnabled() { int count = treeView.SelectedItems.Count; @@ -444,4 +458,47 @@ private async void MenuFlyoutItem_Click(object sender, RoutedEventArgs e) } } } + + private void ShowOrHideResourcesPane(bool showResourcesPane) + { + ToolTipService.SetToolTip(showResourcesPaneButton, showResourcesPane ? "Unpin" : "Pin"); + resourcesPaneButtonSymbolIcon.Symbol = showResourcesPane ? Symbol.UnPin : Symbol.Pin; + showResourcesPaneMenuItem.IsChecked = showResourcesPane; + VisualStateManager.GoToState(this, showResourcesPane && !_collapsed ? "DefaultState" : "MinimalState", true); + } + + private void ShowResourcesPaneButton_Click(object sender, RoutedEventArgs e) + { + ShowResourcesPane = !ShowResourcesPane; + } + + private void CloseAllTabs_Click(object sender, RoutedEventArgs e) + { + EditorPagesManager.ResetEditors(); + } + + private void CloseSelectedTabKeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) + { + EditorPagesManager.TryCloseEditor(editorsTabView.SelectedItem as IEditorPage); + } + + private void NavigateToNumberedTabKeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) + { + int max = editorsTabView.TabItems.Count - 1; + int tabToSelect = sender.Key switch + { + VirtualKey.Number1 => 0, + VirtualKey.Number2 => 1, + VirtualKey.Number3 => 2, + VirtualKey.Number4 => 3, + VirtualKey.Number5 => 4, + VirtualKey.Number6 => 5, + VirtualKey.Number7 => 6, + VirtualKey.Number8 => 7, + _ => max + }; + + tabToSelect = Math.Clamp(tabToSelect, 0, max); + editorsTabView.SelectedIndex = tabToSelect; + } } diff --git a/src/Symptum.UI/Markdown/TextElements/ImageElement.cs b/src/Symptum.UI/Markdown/TextElements/ImageElement.cs index 333d5b7..249c669 100644 --- a/src/Symptum.UI/Markdown/TextElements/ImageElement.cs +++ b/src/Symptum.UI/Markdown/TextElements/ImageElement.cs @@ -1,7 +1,6 @@ using Markdig.Syntax.Inlines; using Windows.Storage.Streams; using Microsoft.UI.Xaml.Media.Imaging; -using System.Runtime.InteropServices.WindowsRuntime; using Windows.Foundation; using HtmlAgilityPack; using System.Globalization; @@ -118,58 +117,57 @@ void imageLoaded(ImageSource source) } else if (_uri.Scheme == "file") { - // Load image from local file - StorageFile file = await StorageFile.GetFileFromPathAsync(_uri.LocalPath); - using IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.Read); - // Create a BitmapImage - BitmapImage bitmap = new(); - await bitmap.SetSourceAsync(stream); - _image.Source = bitmap; - _image.Width = bitmap.PixelWidth == 0 ? bitmap.DecodePixelWidth : bitmap.PixelWidth; - _image.Height = bitmap.PixelHeight == 0 ? bitmap.DecodePixelHeight : bitmap.PixelHeight; - imageLoaded(bitmap); + StorageFile? file = await StorageFile.GetFileFromPathAsync(_uri.LocalPath); + if (file != null) + { + using IRandomAccessStream? stream = await file.OpenAsync(FileAccessMode.Read); + BitmapImage bitmap = new(); + if (stream != null) await bitmap.SetSourceAsync(stream); + _image.Source = bitmap; + _image.Width = bitmap.PixelWidth == 0 ? bitmap.DecodePixelWidth : bitmap.PixelWidth; + _image.Height = bitmap.PixelHeight == 0 ? bitmap.DecodePixelHeight : bitmap.PixelHeight; + imageLoaded(bitmap); + } } else { HttpClient client = new(); - - // Download data from URL +//#if __WASM__ +// client.DefaultRequestHeaders.Add("Access-Control-Allow-Origin", "*"); +// client.DefaultRequestHeaders.Add("Access-Control-Allow-Methods", "*"); +// client.DefaultRequestHeaders.Add("Access-Control-Allow-Headers", "*"); +// client.DefaultRequestHeaders.Add("Access-Control-Max-Age", "86400"); +//#endif HttpResponseMessage response = await client.GetAsync(_uri); - - string? contentType = response.Content.Headers.ContentType.MediaType; - if (contentType == "image/svg+xml") + if (response != null) { - string? svgString = await response.Content.ReadAsStringAsync(); - ImageSource resImage = await _svgRenderer.SvgToImageSource(svgString); - if (resImage != null) + string? contentType = response.Content.Headers?.ContentType?.MediaType; + if (contentType == "image/svg+xml") { - _image.Source = resImage; - Size size = Extensions.GetSvgSize(svgString); - if (size.Width > 0) _image.Width = size.Width; - if (size.Height > 0) _image.Height = size.Height; - imageLoaded(resImage); + string? svgString = await response.Content.ReadAsStringAsync(); + ImageSource resImage = await _svgRenderer.SvgToImageSource(svgString); + if (resImage != null) + { + _image.Source = resImage; + Size size = Extensions.GetSvgSize(svgString); + if (size.Width > 0) _image.Width = size.Width; + if (size.Height > 0) _image.Height = size.Height; + imageLoaded(resImage); + } } - } - else - { - byte[] data = await response?.Content?.ReadAsByteArrayAsync(); - // Create a BitmapImage for other supported formats - BitmapImage bitmap = new(); - using (InMemoryRandomAccessStream stream = new()) + else { - // Write the data to the stream - await stream.WriteAsync(data.AsBuffer()); - stream.Seek(0); + using Stream? stream = await response.Content.ReadAsStreamAsync(); + BitmapImage bitmap = new(); - // Set the source of the BitmapImage - await bitmap.SetSourceAsync(stream); + if (stream != null) await bitmap.SetSourceAsync(stream.AsRandomAccessStream()); + + _image.Source = bitmap; + _image.Width = bitmap.PixelWidth == 0 ? bitmap.DecodePixelWidth : bitmap.PixelWidth; + _image.Height = bitmap.PixelHeight == 0 ? bitmap.DecodePixelHeight : bitmap.PixelHeight; + imageLoaded(bitmap); } - _image.Source = bitmap; - _image.Width = bitmap.PixelWidth == 0 ? bitmap.DecodePixelWidth : bitmap.PixelWidth; - _image.Height = bitmap.PixelHeight == 0 ? bitmap.DecodePixelHeight : bitmap.PixelHeight; - imageLoaded(bitmap); } - } } catch (Exception) { } @@ -187,7 +185,7 @@ void imageLoaded(ImageSource source) public void AddChild(IAddChild child) { - if (child !=null && child.TextElement is SInline inline) + if (child != null && child.TextElement is SInline inline) { _altText.Inlines.Add(inline.Inline); } From a2b34258caac056446ee375233bd2442c6dc978b Mon Sep 17 00:00:00 2001 From: Shankar Date: Sat, 14 Dec 2024 16:19:07 +0530 Subject: [PATCH 3/8] Breaking: BookReference to PresetBookReference - Brought back the old way of storing book references. - Removed the compatibility code for upgrading the CSVs to use the new ReferenceBase --- .../Data/Bibliography/BookReference.cs | 76 --------------- .../Data/Bibliography/PresetBookReference.cs | 93 +++++++++++++++++++ .../Data/Bibliography/ReferenceBase.cs | 8 +- .../Extensions/StringExtensions.cs | 7 ++ .../Management/Deployment/IPackageResource.cs | 2 - .../Subjects/QuestionBanks/QuestionEntry.cs | 27 ------ src/Symptum.Core/TypeConversion/Converters.cs | 13 --- ...er.xaml => PresetBookReferencePicker.xaml} | 2 +- ...l.cs => PresetBookReferencePicker.xaml.cs} | 50 +++++----- .../QuestionBankContextConfigureDialog.xaml | 2 +- ...QuestionBankContextConfigureDialog.xaml.cs | 6 +- .../Controls/QuestionEditorDialog.xaml | 6 +- .../Controls/QuestionEditorDialog.xaml.cs | 6 +- .../Controls/ReferenceBaseTemplateSelector.cs | 4 +- src/Symptum.Editor/Helpers/MarkdownHelper.cs | 9 +- .../Helpers/QuestionBankContextHelper.cs | 2 +- src/Symptum.Editor/MainPage.xaml | 1 - src/Symptum.Editor/MainPage.xaml.cs | 65 ------------- 18 files changed, 144 insertions(+), 235 deletions(-) create mode 100644 src/Symptum.Core/Data/Bibliography/PresetBookReference.cs rename src/Symptum.Editor/Controls/{BookReferencePicker.xaml => PresetBookReferencePicker.xaml} (98%) rename src/Symptum.Editor/Controls/{BookReferencePicker.xaml.cs => PresetBookReferencePicker.xaml.cs} (77%) diff --git a/src/Symptum.Core/Data/Bibliography/BookReference.cs b/src/Symptum.Core/Data/Bibliography/BookReference.cs index 0b5bf07..9ef23e8 100644 --- a/src/Symptum.Core/Data/Bibliography/BookReference.cs +++ b/src/Symptum.Core/Data/Bibliography/BookReference.cs @@ -1,86 +1,10 @@ -using Symptum.Core.Helpers; -using Symptum.Core.Subjects.Books; -using System.Diagnostics.CodeAnalysis; -using System.Web; - namespace Symptum.Core.Data.Bibliography; public record BookReference : LiteratureReference { - private static readonly string _bookId = "n"; - private static readonly string _bookEdition = "ed"; - private static readonly string _bookVolume = "vol"; - - #region Properties - public string? Section { get; init; } public int Edition { get; init; } public int ISBN { get; init; } - - #endregion - - // @book?n={id}&ed={edition}&vol={volume}#{pages} - public static bool TryParse(string? text, [NotNullWhen(true)] out BookReference? bookReference) - { - bool parsed = false; - bookReference = null; - if (!string.IsNullOrEmpty(text)) - { - var values = text.Split(ParserHelper.BookReferenceDelimiter); - if (values.Length == 2) - { - string bookString = values[0][0] == '@' && values[0].Length > 6 ? values[0].Remove(0, 6) : values[0]; - (Book? book, int edition, int volume) = ParseBookString(bookString); - - bookReference = new() - { - Id = book?.Id, - Title = book?.Title, - Authors = book?.Authors, - Edition = edition, - Volume = volume, - Pages = values[1] - }; - parsed = true; - } - } - - return parsed; - } - - private static (Book? book, int edition, int volume) ParseBookString(string bookString) - { - Book? book = null; - int edition = 0; - int volume = 0; - var col = HttpUtility.ParseQueryString(bookString); - if (col != null && col.Count > 0) - { - string? bookCode = col[_bookId]; - string? bookEdition = col[_bookEdition]; - string? bookVolume = col[_bookVolume]; - if (!string.IsNullOrEmpty(bookCode)) - book = BookStore.Books.FirstOrDefault(x => x.Id == bookCode); - if (int.TryParse(bookEdition, out int edNo)) - edition = edNo; - if (int.TryParse(bookVolume, out int volNo)) - volume = volNo; - } - - return (book, edition, volume); - } - - public override string ToString() - { - var col = HttpUtility.ParseQueryString(string.Empty); - col.Add(_bookId, Id ?? string.Empty); - col.Add(_bookEdition, Edition.ToString()); - col.Add(_bookVolume, Volume.ToString()); - return "@book?" + col.ToString() + ParserHelper.BookReferenceDelimiter + Pages; - } - - public override string GetPreviewText() => $"{Title} by {Authors}, " + - $"Edition: {Edition}, Volume: {Volume}, Pages: {Pages}"; } diff --git a/src/Symptum.Core/Data/Bibliography/PresetBookReference.cs b/src/Symptum.Core/Data/Bibliography/PresetBookReference.cs new file mode 100644 index 0000000..c3d58c5 --- /dev/null +++ b/src/Symptum.Core/Data/Bibliography/PresetBookReference.cs @@ -0,0 +1,93 @@ +using System.Diagnostics.CodeAnalysis; +using Symptum.Core.Helpers; +using Symptum.Core.Subjects.Books; +using System.Web; + +namespace Symptum.Core.Data.Bibliography; + +// This will be used for Question Banks (i.e. QuestionEntry). +// Question Banks tend to have the same book references repeat many times. +// Thus it will be efficient to hold the book's title and authors in a shared instance (i.e. Book class), +// rather than having the same thing in multiple instances (as they may only differ in "Pages"). +// TLDR: shared book instance, less memory. + +// For other use cases: for e.g. references in documents, use BookReference class +public record PresetBookReference : ReferenceBase +{ + private static readonly string _bookId = "n"; + private static readonly string _bookEdition = "ed"; + private static readonly string _bookVolume = "vol"; + + #region Properties + + public Book? Book { get; init; } + + public int Edition { get; init; } + + public int Volume { get; init; } + + public string? Pages { get; init; } + + #endregion + + // @book?n={id}&ed={edition}&vol={volume}#{pages} + public static bool TryParse(string? text, [NotNullWhen(true)] out PresetBookReference? bookReference) + { + bool parsed = false; + bookReference = null; + if (!string.IsNullOrEmpty(text)) + { + var values = text.Split(ParserHelper.BookReferenceDelimiter); + if (values.Length == 2) + { + string bookString = values[0][0] == '@' && values[0].Length > 6 ? values[0].Remove(0, 6) : values[0]; + (Book? book, int edition, int volume) = ParseBookString(bookString); + + bookReference = new() + { + Book = book, + Edition = edition, + Volume = volume, + Pages = values[1] + }; + parsed = true; + } + } + + return parsed; + } + + private static (Book? book, int edition, int volume) ParseBookString(string bookString) + { + Book? book = null; + int edition = 0; + int volume = 0; + var col = HttpUtility.ParseQueryString(bookString); + if (col != null && col.Count > 0) + { + string? bookCode = col[_bookId]; + string? bookEdition = col[_bookEdition]; + string? bookVolume = col[_bookVolume]; + if (!string.IsNullOrEmpty(bookCode)) + book = BookStore.Books.FirstOrDefault(x => x.Id == bookCode); + if (int.TryParse(bookEdition, out int edNo)) + edition = edNo; + if (int.TryParse(bookVolume, out int volNo)) + volume = volNo; + } + + return (book, edition, volume); + } + + public override string ToString() + { + var col = HttpUtility.ParseQueryString(string.Empty); + col.Add(_bookId, Book?.Id ?? string.Empty); + col.Add(_bookEdition, Edition.ToString()); + col.Add(_bookVolume, Volume.ToString()); + return "@book?" + col.ToString() + ParserHelper.BookReferenceDelimiter + Pages; + } + + public override string GetPreviewText() => $"{Book?.Title} by {Book?.Authors}, " + + $"Edition: {Edition}, Volume: {Volume}, Pages: {Pages}"; +} diff --git a/src/Symptum.Core/Data/Bibliography/ReferenceBase.cs b/src/Symptum.Core/Data/Bibliography/ReferenceBase.cs index 1493ab3..514cf89 100644 --- a/src/Symptum.Core/Data/Bibliography/ReferenceBase.cs +++ b/src/Symptum.Core/Data/Bibliography/ReferenceBase.cs @@ -4,12 +4,6 @@ namespace Symptum.Core.Data.Bibliography; public abstract record ReferenceBase { - #region Properties - - public string? Id { get; init; } - - #endregion - public static bool TryParse(string? text, [NotNullWhen(true)] out ReferenceBase? reference) { bool parsed = false; @@ -19,7 +13,7 @@ public static bool TryParse(string? text, [NotNullWhen(true)] out ReferenceBase? { if (text.StartsWith("@book?")) { - if (BookReference.TryParse(text, out BookReference? bookReference)) + if (PresetBookReference.TryParse(text, out PresetBookReference? bookReference)) { parsed = true; reference = bookReference; diff --git a/src/Symptum.Core/Extensions/StringExtensions.cs b/src/Symptum.Core/Extensions/StringExtensions.cs index 1ac33c1..1f01dbb 100644 --- a/src/Symptum.Core/Extensions/StringExtensions.cs +++ b/src/Symptum.Core/Extensions/StringExtensions.cs @@ -110,4 +110,11 @@ public static (int lineIndex, int columnIndex) GetLineAndColumnIndex(this string return (line, position - lastLineStart + 1); } + + public static string? ToNullIfEmpty(this string? text) + { + if (string.IsNullOrEmpty(text)) return null; + + return text; + } } diff --git a/src/Symptum.Core/Management/Deployment/IPackageResource.cs b/src/Symptum.Core/Management/Deployment/IPackageResource.cs index 9a8e7a5..2e4c76a 100644 --- a/src/Symptum.Core/Management/Deployment/IPackageResource.cs +++ b/src/Symptum.Core/Management/Deployment/IPackageResource.cs @@ -15,7 +15,5 @@ public interface IPackageResource : IResource public IList? DependencyIds { get; set; } - // public IList? Contents { get; set; } // Is this necessary? - public IList? Tags { get; set; } } diff --git a/src/Symptum.Core/Subjects/QuestionBanks/QuestionEntry.cs b/src/Symptum.Core/Subjects/QuestionBanks/QuestionEntry.cs index 6d6ac38..f89aeaa 100644 --- a/src/Symptum.Core/Subjects/QuestionBanks/QuestionEntry.cs +++ b/src/Symptum.Core/Subjects/QuestionBanks/QuestionEntry.cs @@ -130,30 +130,3 @@ public int CompareTo(object? obj) return CompareTo(obj as QuestionEntry); } } - -public class QuestionEntryO -{ - [TypeConverter(typeof(QuestionIdConverter))] - public QuestionId Id { get; set; } - - public string Title { get; set; } - - [TypeConverter(typeof(StringListConverter))] - public List Descriptions { get; set; } - - public bool HasPreviouslyBeenAsked { get; set; } - - public int Importance { get; set; } - - [TypeConverter(typeof(DateOnlyListConverter))] - public List YearsAsked { get; set; } - - [TypeConverter(typeof(StringListConverter))] - public List ProbableCases { get; set; } - - [TypeConverter(typeof(BookReferenceListConverter))] - public List BookReferences { get; set; } - - [TypeConverter(typeof(UriListConverter))] - public List LinkReferences { get; set; } -} diff --git a/src/Symptum.Core/TypeConversion/Converters.cs b/src/Symptum.Core/TypeConversion/Converters.cs index 2093696..36ec398 100644 --- a/src/Symptum.Core/TypeConversion/Converters.cs +++ b/src/Symptum.Core/TypeConversion/Converters.cs @@ -48,11 +48,6 @@ public class StringListConverter : ListConverter public override void ValidateData(string text, List list) => ListToStringConversion.ValidateDataForString(text, list); } -public class BookReferenceListConverter : ListConverter -{ - public override void ValidateData(string text, List list) => ListToStringConversion.ValidateDataForBookReference(text, list); -} - #endregion #region Reference Values @@ -163,14 +158,6 @@ public static void ValidateDataForString(string text, List list) } } - public static void ValidateDataForBookReference(string text, List list) - { - if (BookReference.TryParse(text, out BookReference? reference)) - { - list.Add(reference); - } - } - public static void ValidateDataForReference(string text, List list) { if (ReferenceBase.TryParse(text, out ReferenceBase? reference)) diff --git a/src/Symptum.Editor/Controls/BookReferencePicker.xaml b/src/Symptum.Editor/Controls/PresetBookReferencePicker.xaml similarity index 98% rename from src/Symptum.Editor/Controls/BookReferencePicker.xaml rename to src/Symptum.Editor/Controls/PresetBookReferencePicker.xaml index 732c75f..ed539fc 100644 --- a/src/Symptum.Editor/Controls/BookReferencePicker.xaml +++ b/src/Symptum.Editor/Controls/PresetBookReferencePicker.xaml @@ -1,5 +1,5 @@  (BookReference)GetValue(BookReferenceProperty); - set => SetValue(BookReferenceProperty, value); + get => (PresetBookReference)GetValue(PresetBookReferenceProperty); + set => SetValue(PresetBookReferenceProperty, value); } #endregion - public BookReferencePicker() + public PresetBookReferencePicker() { InitializeComponent(); #if HAS_UNO_WINUI @@ -128,26 +128,24 @@ private void SearchBook(string? queryText) private void LoadBookReference() { - if (BookReference == null) return; - selectedBook = BookStore.Books.FirstOrDefault(x => x.Id == BookReference.Id); + if (PresetBookReference == null) return; + selectedBook = BookStore.Books.FirstOrDefault(x => x.Id == PresetBookReference.Book?.Id); #if HAS_UNO_WINUI bookQueryBox.Text = selectedBook?.ToString(); #else bookList.SelectedItem = selectedBook; #endif - editionSelector.Value = BookReference.Edition; - volumeSelector.Value = BookReference.Volume; - pageNoSelector.Text = BookReference.Pages; + editionSelector.Value = PresetBookReference.Edition; + volumeSelector.Value = PresetBookReference.Volume; + pageNoSelector.Text = PresetBookReference.Pages; } private void UpdateBookReference() { - if (BookReference == null) return; - BookReference = BookReference with + if (PresetBookReference == null) return; + PresetBookReference = PresetBookReference with { - Id = selectedBook?.Id, - Title = selectedBook?.Title, - Authors = selectedBook?.Authors, + Book = selectedBook, Edition = (int)editionSelector.Value, Volume = (int)volumeSelector.Value, Pages = pageNoSelector.Text @@ -156,9 +154,9 @@ private void UpdateBookReference() private void UpdatePreviewText() { - if (BookReference == null) return; + if (PresetBookReference == null) return; - string previewText = BookReference.GetPreviewText(); + string previewText = PresetBookReference.GetPreviewText(); previewTextBlock.Text = previewText; ToolTipService.SetToolTip(previewButton, previewText); } diff --git a/src/Symptum.Editor/Controls/QuestionBankContextConfigureDialog.xaml b/src/Symptum.Editor/Controls/QuestionBankContextConfigureDialog.xaml index 4e0e670..52b8b4e 100644 --- a/src/Symptum.Editor/Controls/QuestionBankContextConfigureDialog.xaml +++ b/src/Symptum.Editor/Controls/QuestionBankContextConfigureDialog.xaml @@ -12,6 +12,6 @@ - + diff --git a/src/Symptum.Editor/Controls/QuestionBankContextConfigureDialog.xaml.cs b/src/Symptum.Editor/Controls/QuestionBankContextConfigureDialog.xaml.cs index 1d914a0..f9d6b64 100644 --- a/src/Symptum.Editor/Controls/QuestionBankContextConfigureDialog.xaml.cs +++ b/src/Symptum.Editor/Controls/QuestionBankContextConfigureDialog.xaml.cs @@ -27,7 +27,7 @@ private void QuestionBankContextConfigureDialog_Opened(ContentDialog sender, Con { datePicker.Date = new(dateOnly.ToDateTime(new TimeOnly(0))); } - bookRefPicker.BookReference = context.PreferredBook ?? new(); + bookRefPicker.PresetBookReference = context.PreferredBook ?? new(); } } @@ -37,7 +37,7 @@ private void QuestionBankContextConfigureDialog_PrimaryButtonClick(ContentDialog { context.SubjectCode = scCB.SelectedItem != null ? (SubjectList)scCB.SelectedItem : SubjectList.None; context.LastInputDate = DateOnly.FromDateTime(datePicker.SelectedDate?.Date ?? DateTime.Now); - context.PreferredBook = bookRefPicker.BookReference; + context.PreferredBook = bookRefPicker.PresetBookReference; } } @@ -45,6 +45,6 @@ private void QuestionBankContextConfigureDialog_SecondaryButtonClick(ContentDial { scCB.SelectedItem = null; datePicker.SelectedDate = null; - bookRefPicker.BookReference = null; + bookRefPicker.PresetBookReference = null; } } diff --git a/src/Symptum.Editor/Controls/QuestionEditorDialog.xaml b/src/Symptum.Editor/Controls/QuestionEditorDialog.xaml index 8a8ee7f..362271b 100644 --- a/src/Symptum.Editor/Controls/QuestionEditorDialog.xaml +++ b/src/Symptum.Editor/Controls/QuestionEditorDialog.xaml @@ -14,9 +14,9 @@ Style="{ThemeResource DefaultContentDialogStyle}" CloseButtonText="Cancel" PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"> - + - + @@ -25,7 +25,7 @@ diff --git a/src/Symptum.Editor/Controls/QuestionEditorDialog.xaml.cs b/src/Symptum.Editor/Controls/QuestionEditorDialog.xaml.cs index c8c1645..866ae73 100644 --- a/src/Symptum.Editor/Controls/QuestionEditorDialog.xaml.cs +++ b/src/Symptum.Editor/Controls/QuestionEditorDialog.xaml.cs @@ -311,7 +311,7 @@ private void HandleListEditors() rfLE.ItemsSource = references; rfLE.ItemTypes = [ - new ("Book Reference", typeof(BookReference)), + new ("Preset Book Reference", typeof(PresetBookReference)), new ("Journal Article Reference", typeof(JournalArticleReference)), new ("Link Reference", typeof(LinkReference)), ]; @@ -319,9 +319,9 @@ private void HandleListEditors() rfLE.ItemsSource = references; rfLE.AddItemRequested += (s, e) => { - if (e == typeof(BookReference)) + if (e == typeof(PresetBookReference)) { - BookReference bookReference = context?.PreferredBook ?? new(); + PresetBookReference bookReference = context?.PreferredBook ?? new(); references.Add(new() { Value = bookReference }); } else if (e == typeof(LinkReference)) diff --git a/src/Symptum.Editor/Controls/ReferenceBaseTemplateSelector.cs b/src/Symptum.Editor/Controls/ReferenceBaseTemplateSelector.cs index 5e5c861..3db99d2 100644 --- a/src/Symptum.Editor/Controls/ReferenceBaseTemplateSelector.cs +++ b/src/Symptum.Editor/Controls/ReferenceBaseTemplateSelector.cs @@ -4,7 +4,7 @@ namespace Symptum.Editor.Controls; public partial class ReferenceBaseTemplateSelector : DataTemplateSelector { - public DataTemplate? BookReferenceTemplate { get; set; } + public DataTemplate? PresetBookReferenceTemplate { get; set; } public DataTemplate? LinkReferenceTemplate { get; set; } @@ -16,7 +16,7 @@ protected override DataTemplate SelectTemplateCore(object item) { template = reference switch { - BookReference => BookReferenceTemplate, + PresetBookReference => PresetBookReferenceTemplate, LinkReference => LinkReferenceTemplate, _ => null, }; diff --git a/src/Symptum.Editor/Helpers/MarkdownHelper.cs b/src/Symptum.Editor/Helpers/MarkdownHelper.cs index fbfd086..ffdc494 100644 --- a/src/Symptum.Editor/Helpers/MarkdownHelper.cs +++ b/src/Symptum.Editor/Helpers/MarkdownHelper.cs @@ -54,12 +54,13 @@ private static string GetOrdinal(int num) }; } - private static string GetBookReference(BookReference? bookReference) + private static string GetReferences(ReferenceBase? reference) { - if (bookReference == null) return string.Empty; + if (reference is not PresetBookReference bookReference) return string.Empty; + string volString = bookReference.Volume > 0 ? $" Volume {bookReference.Volume}," : string.Empty; - return $"{bookReference.Id}, {GetOrdinal(bookReference.Edition)} Edition,{volString} Pg.No: {bookReference.Pages}"; + return $"{bookReference.Book?.Id}, {GetOrdinal(bookReference.Edition)} Edition,{volString} Pg.No: {bookReference.Pages}"; } public static void GenerateMarkdownForQuestionBankTopic(QuestionBankTopic topic, ref StringBuilder mdBuilder) @@ -125,7 +126,7 @@ public static void GenerateMarkdownForQuestionEntry(QuestionEntry? entry, int qu ListToStringConversion.ConvertToString(entry.YearsAsked, GetYear)); if (_includeBookReferences && entry.References != null && entry.References.Count > 0) mdBuilder.AppendFormat("\t({0})", - ListToStringConversion.ConvertToString(entry.References, GetBookReference)); + ListToStringConversion.ConvertToString(entry.References, GetReferences)); mdBuilder.AppendLine(); if (entry.Descriptions != null && entry.Descriptions.Count > 0) { diff --git a/src/Symptum.Editor/Helpers/QuestionBankContextHelper.cs b/src/Symptum.Editor/Helpers/QuestionBankContextHelper.cs index e25f402..56b26ed 100644 --- a/src/Symptum.Editor/Helpers/QuestionBankContextHelper.cs +++ b/src/Symptum.Editor/Helpers/QuestionBankContextHelper.cs @@ -14,5 +14,5 @@ internal class QuestionBankContext public DateOnly? LastInputDate { get; set; } - public BookReference? PreferredBook { get; set; } + public PresetBookReference? PreferredBook { get; set; } } diff --git a/src/Symptum.Editor/MainPage.xaml b/src/Symptum.Editor/MainPage.xaml index f071793..cfa651d 100644 --- a/src/Symptum.Editor/MainPage.xaml +++ b/src/Symptum.Editor/MainPage.xaml @@ -91,7 +91,6 @@ - diff --git a/src/Symptum.Editor/MainPage.xaml.cs b/src/Symptum.Editor/MainPage.xaml.cs index f9db6d3..872dd42 100644 --- a/src/Symptum.Editor/MainPage.xaml.cs +++ b/src/Symptum.Editor/MainPage.xaml.cs @@ -394,71 +394,6 @@ private async void AddNewFlyoutItem_Click(object sender, RoutedEventArgs e) } } - private async void MenuFlyoutItem_Click(object sender, RoutedEventArgs e) - { - FileOpenPicker fileOpenPicker = new(); - fileOpenPicker.FileTypeFilter.Add(CsvFileExtension); - -#if NET6_0_OR_GREATER && WINDOWS && !HAS_UNO - WinRT.Interop.InitializeWithWindow.Initialize(fileOpenPicker, WindowHelper.WindowHandle); -#endif - var pickedFiles = await fileOpenPicker.PickMultipleFilesAsync(); - if (pickedFiles.Count > 0) - { - foreach (StorageFile file in pickedFiles) - { - if (file != null && file.FileType.Equals(CsvFileExtension, StringComparison.InvariantCultureIgnoreCase)) - { - QuestionBankTopic topic = new(); - string csv = await FileIO.ReadTextAsync(file); - if (string.IsNullOrEmpty(csv)) return; - - using StringReader reader = new(csv); - using CsvReader csvReader = new(reader, CultureInfo.InvariantCulture); - var entries = csvReader.GetRecords().ToList(); - - List entriesn = []; - foreach (var entry in entries) - { - QuestionEntry entry1 = new() - { - Id = new() { QuestionType = entry.Id.QuestionType, SubjectCode = entry.Id.SubjectCode, CompetencyNumbers = entry.Id.CompetencyNumbers }, - Title = entry.Title, - Descriptions = entry.Descriptions.CloneList(), - HasPreviouslyBeenAsked = entry.HasPreviouslyBeenAsked, - Importance = entry.Importance, - YearsAsked = entry.YearsAsked.CloneList(), - ProbableCases = entry.ProbableCases.CloneList() - }; - - if (entry.BookReferences?.Count > 0) - { - entry1.References = new(entry.BookReferences); - } - entriesn.Add(entry1); - } - - topic.Entries = new(entriesn); - - using StringWriter writer = new(); - using CsvWriter csvW = new(writer, CultureInfo.InvariantCulture); - csvW.WriteHeader(); - csvW.NextRecord(); - if (entriesn != null) - { - foreach (var entry in entriesn) - { - csvW.WriteRecord(entry); - csvW.NextRecord(); - } - } - - await FileIO.WriteTextAsync(file, writer.ToString()); - } - } - } - } - private void ShowOrHideResourcesPane(bool showResourcesPane) { ToolTipService.SetToolTip(showResourcesPaneButton, showResourcesPane ? "Unpin" : "Pin"); From fc37201d64e1053faf878a5c8e1fd7138926a19e Mon Sep 17 00:00:00 2001 From: Shankar Date: Sun, 15 Dec 2024 18:07:01 +0530 Subject: [PATCH 4/8] Project System MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - This implementation was more difficult to do than getting a girlfriend 💀😭🙏. - Project System: - Symproj: an XML file with path entries of PackageResources. - ProjectFolder: resources can be placed in sub folders rather than in work folder. - Moved Books to Symptum.Common and added a common Bootstrapper. - Resources Pane and AddNewItemDialog now supports icons. - PropertiesEditor: Ability to generate Id and Uri from ancestors. - Icon for "New Project" based on Fluent System Icons and Symptum Logo? --- src/Symptum.Common/Bootstrapper.cs | 29 +++ src/Symptum.Common/Helpers/ResourceHelper.cs | 112 ++++------ src/Symptum.Common/ProjectSystem/Project.cs | 57 +++++ .../ProjectSystem/ProjectFolder.cs | 8 + .../ProjectSystem/ProjectSystemManager.cs | 211 ++++++++++++++++++ src/Symptum.Common/Symptum.Common.csproj | 4 + .../Extensions/StringExtensions.cs | 2 - src/Symptum.Core/Helpers/FileHelper.cs | 13 +- .../Management/Resources/ResourceManager.cs | 80 ++++++- src/Symptum.Core/Symptum.Core.csproj | 2 +- .../NutrientEditorUIGenerator.cs | 1 - src/Symptum.Editor/App.xaml.cs | 27 +-- .../Common/DefaultIconSources.cs | 12 + .../{Controls => Common}/NewItemType.cs | 4 +- ...ResourceBaseToIconSourceMarkupExtension.cs | 79 +++++++ .../Controls/AddNewItemDialog.xaml | 68 +++--- .../Controls/AddNewItemDialog.xaml.cs | 54 +++-- .../Controls/ListEditorControl.xaml | 17 +- .../Controls/ListEditorControl.xaml.cs | 2 + .../ResourcePropertiesEditorControl.xaml | 1 + .../ResourcePropertiesEditorControl.xaml.cs | 64 ++++-- .../EditorPages/EditorPageBase.cs | 4 + .../EditorPages/EditorPagesManager.cs | 8 + .../EditorPages/FoodGroupEditorPage.xaml.cs | 4 +- src/Symptum.Editor/EditorPages/IEditorPage.cs | 2 + .../EditorPages/ImageViewerPage.xaml.cs | 2 +- .../EditorPages/MarkdownEditorPage.xaml.cs | 10 +- .../QuestionTopicEditorPage.xaml.cs | 4 +- .../ReferenceValueGroupEditorPage.xaml.cs | 4 +- src/Symptum.Editor/MainPage.xaml | 15 +- src/Symptum.Editor/MainPage.xaml.cs | 21 +- src/Symptum.Editor/Symptum.Editor.csproj | 4 - src/Symptum/App.xaml.cs | 3 +- src/Symptum/MainPage.xaml | 4 +- 34 files changed, 724 insertions(+), 208 deletions(-) create mode 100644 src/Symptum.Common/Bootstrapper.cs create mode 100644 src/Symptum.Common/ProjectSystem/Project.cs create mode 100644 src/Symptum.Common/ProjectSystem/ProjectFolder.cs create mode 100644 src/Symptum.Common/ProjectSystem/ProjectSystemManager.cs rename src/Symptum.Editor/{Controls => Common}/NewItemType.cs (93%) create mode 100644 src/Symptum.Editor/Common/ResourceBaseToIconSourceMarkupExtension.cs diff --git a/src/Symptum.Common/Bootstrapper.cs b/src/Symptum.Common/Bootstrapper.cs new file mode 100644 index 0000000..62d51ad --- /dev/null +++ b/src/Symptum.Common/Bootstrapper.cs @@ -0,0 +1,29 @@ +using Symptum.Common.Helpers; +using Symptum.Core.Subjects.Books; + +namespace Symptum.Common; + +public class Bootstrapper +{ + public static async Task InitializeAsync() + { + await PackageHelper.InitializeAsync(); + StorageHelper.Initialize(); + try + { + var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Books/First Year Books.csv")); + string content = await FileIO.ReadTextAsync(file); + BookStore.LoadBooks(content); + file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Books/Second Year Books.csv")); + content = await FileIO.ReadTextAsync(file); + BookStore.LoadBooks(content); + file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Books/Third Year Books.csv")); + content = await FileIO.ReadTextAsync(file); + BookStore.LoadBooks(content); + file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Books/Final Year Books.csv")); + content = await FileIO.ReadTextAsync(file); + BookStore.LoadBooks(content); + } + catch { } + } +} diff --git a/src/Symptum.Common/Helpers/ResourceHelper.cs b/src/Symptum.Common/Helpers/ResourceHelper.cs index e99d5ad..00f7864 100644 --- a/src/Symptum.Common/Helpers/ResourceHelper.cs +++ b/src/Symptum.Common/Helpers/ResourceHelper.cs @@ -14,7 +14,7 @@ public class ResourceHelper public static StorageFolder? WorkFolder { get => _workFolder; - private set + internal set { _workFolder = value; WorkFolderChanged?.Invoke(null, _workFolder); @@ -23,37 +23,12 @@ private set #region Work Folder Handling - public static async Task OpenWorkFolderAsync(StorageFolder? folder = null) - { - bool result = await SelectWorkFolderAsync(folder); - if (result && StorageHelper.IsFolderPickerSupported) - { - ResourceManager.Resources.Clear(); - await LoadResourcesFromWorkPathAsync(); - return true; - } - - return false; - } - public static void CloseWorkFolder() { WorkFolder = null; ResourceManager.Resources.Clear(); } - private static async Task VerifyWorkFolderAsync(StorageFolder? targetFolder = null) - { - bool pathExists = true; - if (targetFolder == null) - { - if (_workFolder == null) - pathExists = await SelectWorkFolderAsync(); - } - - return pathExists; - } - public static async Task SelectWorkFolderAsync(StorageFolder? folder = null) { if (folder == null && StorageHelper.IsFolderPickerSupported) @@ -113,17 +88,17 @@ public static async Task LoadResourcesFromWorkPathAsync() await LoadResourcesFromFilesAsync(files); } - public static async Task LoadResourcesFromFilesAsync(IEnumerable? files, IResource? parent = null) + public static async Task LoadResourcesFromFilesAsync(IEnumerable? files, IResource? parent = null, StorageFolder? sourceFolder = null) { if (files == null) return; foreach (StorageFile file in files) { - await LoadResourceFromFileAsync(file, parent); + await LoadResourceFromFileAsync(file, parent, sourceFolder); } } - public static async Task LoadResourceFromFileAsync(StorageFile file, IResource? parent = null) + public static async Task LoadResourceFromFileAsync(StorageFile? file, IResource? parent = null, StorageFolder? sourceFolder = null) { if (file == null) return null; @@ -132,7 +107,7 @@ public static async Task LoadResourcesFromFilesAsync(IEnumerable? f else if (file.FileType.Equals(MarkdownFileExtension, StringComparison.InvariantCultureIgnoreCase)) return await LoadMarkdownFileResourceFromFileAsync(file, parent); else if (file.FileType.Equals(JsonFileExtension, StringComparison.InvariantCultureIgnoreCase)) - return await LoadPackageResourceFromFileAsync(file); + return await LoadPackageResourceFromFileAsync(file, parent, sourceFolder); else if (ImageFileExtensions.Any(ext => ext.Equals(file.FileType, StringComparison.InvariantCultureIgnoreCase))) return await LoadImageFileResourceFromFileAsync(file, parent); else if (AudioFileExtensions.Any(ext => ext.Equals(file.FileType, StringComparison.InvariantCultureIgnoreCase))) @@ -187,7 +162,7 @@ public static async Task LoadResourcesFromFilesAsync(IEnumerable? f return null; } - private static async Task LoadImageFileResourceFromFileAsync(StorageFile file, IResource? parent) + private static async Task LoadImageFileResourceFromFileAsync(StorageFile? file, IResource? parent) { if (file != null) { @@ -211,7 +186,7 @@ public static async Task LoadResourcesFromFilesAsync(IEnumerable? f return null; } - private static async Task LoadAudioFileResourceFromFileAsync(StorageFile file, IResource? parent) + private static async Task LoadAudioFileResourceFromFileAsync(StorageFile? file, IResource? parent) { if (file != null) { @@ -235,7 +210,7 @@ public static async Task LoadResourcesFromFilesAsync(IEnumerable? f return null; } - internal static async Task LoadPackageResourceFromFileAsync(StorageFile? file) + internal static async Task LoadPackageResourceFromFileAsync(StorageFile? file, IResource? parent = null, StorageFolder? sourceFolder = null) { if (file != null) { @@ -243,9 +218,12 @@ public static async Task LoadResourcesFromFilesAsync(IEnumerable? f var package = ResourceManager.LoadPackageFromMetadata(json); if (package != null) { - ResourceManager.Resources.Add(package); + if (parent != null && parent.CanAddChildResourceType(package.GetType())) + parent.AddChildResource(package); + else + ResourceManager.Resources.Add(package); ResourceManager.RegisterResource(package); - await LoadResourceAsync(package); + await LoadResourceAsync(package, parent, sourceFolder); return package; } } @@ -253,47 +231,48 @@ public static async Task LoadResourcesFromFilesAsync(IEnumerable? f return null; } - public static async Task LoadResourceAsync(IResource resource, IResource? parent = null) + public static async Task LoadResourceAsync(IResource? resource, IResource? parent = null, StorageFolder? sourceFolder = null) { if (resource is TextFileResource textResource) { // CSVs and Markdowns - await LoadTextFileResourceAsync(textResource); + await LoadTextFileResourceAsync(textResource, sourceFolder); resource.InitializeResource(parent); } else if (resource is MediaFileResource mediaResource) { // Images and Audios - await LoadMediaFileResourceAsync(mediaResource); + await LoadMediaFileResourceAsync(mediaResource, sourceFolder); resource.InitializeResource(parent); } else { if (resource is MetadataResource metadataResource && metadataResource.SplitMetadata) { - await LoadMetadataResourceAsync(metadataResource); + await LoadMetadataResourceAsync(metadataResource, sourceFolder); } resource.InitializeResource(parent); - await LoadChildrenResourcesAsync(resource); // Temporary + await LoadChildrenResourcesAsync(resource, sourceFolder); // Temporary } } - private static async Task LoadChildrenResourcesAsync(IResource resource) + private static async Task LoadChildrenResourcesAsync(IResource? resource, StorageFolder? sourceFolder = null) { if (resource != null && resource.CanHandleChildren && resource.ChildrenResources != null) { foreach (var child in resource.ChildrenResources) { - await LoadResourceAsync(child, resource); + await LoadResourceAsync(child, resource, sourceFolder); } } } - private static async Task GetResourceFileAsync(string? path) + private static async Task GetResourceFileAsync(string? path, StorageFolder? sourceFolder = null) { + sourceFolder ??= _workFolder; (string folderPath, string fileName, string extension) = GetDetailsFromFilePath(path); - StorageFolder? folder = await StorageHelper.GetSubFolderAsync(_workFolder, folderPath); + StorageFolder? folder = await StorageHelper.GetSubFolderAsync(sourceFolder, folderPath); if (folder != null) { try @@ -306,27 +285,27 @@ private static async Task LoadChildrenResourcesAsync(IResource resource) return null; } - private static async Task LoadTextFileResourceAsync(TextFileResource textResource) + private static async Task LoadTextFileResourceAsync(TextFileResource textResource, StorageFolder? sourceFolder = null) { - if (await GetResourceFileAsync(textResource.FilePath) is StorageFile textFile) + if (await GetResourceFileAsync(textResource.FilePath, sourceFolder) is StorageFile textFile) { string text = await FileIO.ReadTextAsync(textFile); ResourceManager.LoadResourceFileText(textResource, text); } } - private static async Task LoadMediaFileResourceAsync(MediaFileResource mediaResource) + private static async Task LoadMediaFileResourceAsync(MediaFileResource mediaResource, StorageFolder? sourceFolder = null) { - if (await GetResourceFileAsync(mediaResource.FilePath) is StorageFile mediaFile) + if (await GetResourceFileAsync(mediaResource.FilePath, sourceFolder) is StorageFile mediaFile) { mediaResource.SetMediaFileExtension(mediaFile.FileType.ToLower()); fileMap.TryAdd(mediaResource, mediaFile); } } - private static async Task LoadMetadataResourceAsync(MetadataResource resource) + private static async Task LoadMetadataResourceAsync(MetadataResource resource, StorageFolder? sourceFolder = null) { - if (await GetResourceFileAsync(resource.MetadataPath) is StorageFile jsonFile) + if (await GetResourceFileAsync(resource.MetadataPath, sourceFolder) is StorageFile jsonFile) { string text = await FileIO.ReadTextAsync(jsonFile); ResourceManager.LoadResourceMetadata(resource, text); @@ -337,21 +316,6 @@ private static async Task LoadMetadataResourceAsync(MetadataResource resource) #region Saving Resources - public static async Task SaveAllResourcesAsync(StorageFolder? targetFolder = null) - { - if (ResourceManager.Resources.Count > 0) - { - bool allSaved = true; - foreach (var resource in ResourceManager.Resources) - { - allSaved &= await SaveResourceAsync(resource, targetFolder); - } - return allSaved; - } - - return false; - } - public static async Task SaveResourceAsync(IResource resource, StorageFolder? targetFolder = null) { if (resource == null) return false; @@ -387,7 +351,7 @@ private static async Task SaveTextFileResourceAsync(TextFileResource textR { if (textResource == null) return false; - string subFolderPath = ResourceManager.GetResourceFolderPath(textResource); + string subFolderPath = ResourceManager.GetRelativeResourceFolderPath(textResource); string? fileName = ResourceManager.GetResourceFileName(textResource); textResource.FilePath = subFolderPath + fileName + textResource.FileExtension; StorageFile? saveFile = await PickSaveFileAsync(fileName, textResource.FileExtension, $"{textResource.FileType} File", targetFolder, subFolderPath); @@ -406,7 +370,7 @@ private static async Task CopySaveFileResourceAsync(FileResource fileResou { if (fileResource == null) return false; - string subFolderPath = ResourceManager.GetResourceFolderPath(fileResource); + string subFolderPath = ResourceManager.GetRelativeResourceFolderPath(fileResource); string? fileName = ResourceManager.GetResourceFileName(fileResource); string filePath = subFolderPath + fileName + fileResource.FileExtension; @@ -430,7 +394,7 @@ private static async Task SaveMetadataAsync(T resource, StorageFolder? { if (resource == null) return false; - string subFolderPath = ResourceManager.GetResourceFolderPath(resource); + string subFolderPath = ResourceManager.GetRelativeResourceFolderPath(resource); string? fileName = ResourceManager.GetResourceFileName(resource); resource.MetadataPath = subFolderPath + fileName + JsonFileExtension; StorageFile? saveFile = await PickSaveFileAsync(fileName, JsonFileExtension, "JSON File", targetFolder, subFolderPath); @@ -459,7 +423,7 @@ private static async Task SaveChildrenAsync(IResource? resource, StorageFo return true; } - private static async Task PickSaveFileAsync(string? name, string? extension, string? fileType, StorageFolder? targetFolder = null, string? subFolder = null) + internal static async Task PickSaveFileAsync(string? name, string? extension, string? fileType, StorageFolder? targetFolder = null, string? subFolder = null) { if (string.IsNullOrEmpty(name) || string.IsNullOrWhiteSpace(extension) || string.IsNullOrEmpty(fileType)) return null; @@ -559,12 +523,11 @@ public static async Task RemoveResourceAsync(IResource? resource, bool delete = private static async Task DeleteResourceFileAsync(FileResource? fileResource) { if (fileResource == null) return; - if (_workFolder != null && StorageHelper.IsFolderPickerSupported) { try { - string path = ResourceManager.GetResourceFolderPath(fileResource); + string path = ResourceManager.GetAbsoluteResourceFolderPath(fileResource); var folder = await StorageHelper.GetSubFolderAsync(_workFolder, path); if (folder == null) return; IStorageItem? file = await folder.TryGetItemAsync(ResourceManager.GetResourceFileName(fileResource) + fileResource.FileExtension); @@ -581,16 +544,17 @@ private static async Task DeleteMetadataAsync(MetadataResource? resource) { try { - string path = ResourceManager.GetResourceFolderPath(resource); + string path = ResourceManager.GetAbsoluteResourceFolderPath(resource); string? resourceName = ResourceManager.GetResourceFileName(resource); var folder = await StorageHelper.GetSubFolderAsync(_workFolder, path); + if (folder == null) return; if (resource.SplitMetadata || resource is PackageResource) { - IStorageItem? file = await folder?.TryGetItemAsync(resourceName + JsonFileExtension); + IStorageItem? file = await folder.TryGetItemAsync(resourceName + JsonFileExtension); if (file != null) await file.DeleteAsync(); } - IStorageItem? folder1 = await folder?.TryGetItemAsync(resourceName); + IStorageItem? folder1 = await folder.TryGetItemAsync(resourceName); if (folder1 != null) await folder1.DeleteAsync(); } catch { } diff --git a/src/Symptum.Common/ProjectSystem/Project.cs b/src/Symptum.Common/ProjectSystem/Project.cs new file mode 100644 index 0000000..463405a --- /dev/null +++ b/src/Symptum.Common/ProjectSystem/Project.cs @@ -0,0 +1,57 @@ +using System.Xml; +using System.Xml.Serialization; + +namespace Symptum.Common.ProjectSystem; + +public class Project +{ + [XmlIgnore] + public string? Name { get; set; } + + public List? Entries { get; set; } + + private static XmlSerializer _serializer = new(typeof(Project)); + + public static string Serialize(Project project) + { + var settings = new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true + }; + + var namespaces = new XmlSerializerNamespaces([XmlQualifiedName.Empty]); + using var stringWriter = new StringWriter(); + using XmlWriter xmlWriter = XmlWriter.Create(stringWriter, settings); + _serializer.Serialize(xmlWriter, project, namespaces); + return stringWriter.ToString(); + } + + public static Project? DeserializeProject(string xml) + { + if (string.IsNullOrWhiteSpace(xml)) return null; + try + { + return (Project?)_serializer.Deserialize(new StringReader(xml)); + } + catch { } + return null; + } +} + +public class ProjectEntry +{ + public ProjectEntry() { } + + public ProjectEntry(string path, string name) + { + Path = path; + Name = name; + } + + [XmlAttribute] + public string? Path { get; set; } + + [XmlAttribute] + public string? Name { get; set; } +} diff --git a/src/Symptum.Common/ProjectSystem/ProjectFolder.cs b/src/Symptum.Common/ProjectSystem/ProjectFolder.cs new file mode 100644 index 0000000..a4a98e1 --- /dev/null +++ b/src/Symptum.Common/ProjectSystem/ProjectFolder.cs @@ -0,0 +1,8 @@ +using Symptum.Core.Management.Resources; + +namespace Symptum.Common.ProjectSystem; + +public class ProjectFolder : CategoryResource +{ + protected override bool ChildRestraint(Type childResourceType) => childResourceType != typeof(Project); +} diff --git a/src/Symptum.Common/ProjectSystem/ProjectSystemManager.cs b/src/Symptum.Common/ProjectSystem/ProjectSystemManager.cs new file mode 100644 index 0000000..3c21d19 --- /dev/null +++ b/src/Symptum.Common/ProjectSystem/ProjectSystemManager.cs @@ -0,0 +1,211 @@ +using Symptum.Common.Helpers; +using Symptum.Core.Management.Resources; +using static Symptum.Core.Helpers.FileHelper; + +namespace Symptum.Common.ProjectSystem; + +public class ProjectSystemManager +{ + public static bool UseProjectManager { get; set; } = false; + + public static Project? CurrentProject { get; set; } + + public static async Task OpenWorkFolderAsync(StorageFolder? folder = null) + { + bool result = await ResourceHelper.SelectWorkFolderAsync(folder); + if (result && StorageHelper.IsFolderPickerSupported) + { + CurrentProject = null; + UseProjectManager = false; + ResourceManager.Resources.Clear(); + await ProcessFilesFromWorkPathAsync(); + return true; + } + + return false; + } + + private static async Task ProcessFilesFromWorkPathAsync() + { + var files = await ResourceHelper.GetFilesFromWorkPathAsync(); + if (files == null) return; + + foreach (StorageFile file in files) + { + await ProcessFileAsync(file); + } + } + + private static async Task ProcessFileAsync(StorageFile file) + { + if (file == null) return; + + if (file.FileType.Equals(ProjectFileExtension, StringComparison.InvariantCultureIgnoreCase)) + { + await LoadProjectFromFileAsync(file); + } + else + await ResourceHelper.LoadResourceFromFileAsync(file); + } + + private static ProjectFolder? CreateOrGetProjectFolderResources(string? path) + { + ProjectFolder? parent = null; + if (string.IsNullOrEmpty(path)) return null; + var folders = path.Split(PathSeparator); + + for (int i = 0; i < folders.Length; i++) + { + string folderName = folders[i]; + if (!string.IsNullOrWhiteSpace(folderName)) + { + var resources = parent?.ChildrenResources ?? ResourceManager.Resources; + if (resources.FirstOrDefault(x => + x is ProjectFolder f && folderName.Equals(f.Title, StringComparison.InvariantCultureIgnoreCase)) + is not ProjectFolder folder) + { + folder = new() { Title = folderName }; + if (parent != null) + parent.AddChildResource(folder); + else + ResourceManager.Resources.Add(folder); + + ((IResource)folder).InitializeResource(parent); + } + parent = folder; + } + } + + return parent; + } + + private static async Task LoadProjectFromFileAsync(StorageFile? file) + { + if (file == null) return; + + string xml = await FileIO.ReadTextAsync(file); + CurrentProject = Project.DeserializeProject(xml); + if (CurrentProject != null && CurrentProject.Entries != null) + { + CurrentProject.Name = file.DisplayName; + UseProjectManager = true; + foreach (ProjectEntry entry in CurrentProject.Entries) + { + // NOTE: To prevent reloading the resources in the same folder as the project file. + // They will be loaded regardless. + if (string.IsNullOrEmpty(entry.Path) || entry.Path == PathSeparator.ToString()) + continue; + + StorageFolder? sourceFolder = await StorageHelper.GetSubFolderAsync(ResourceHelper.WorkFolder, entry.Path); + if (sourceFolder != null) + { + StorageFile? sourceFile = await sourceFolder.TryGetItemAsync(entry.Name) as StorageFile; + IResource? parent = CreateOrGetProjectFolderResources(entry.Path); + await ResourceHelper.LoadResourceFromFileAsync(sourceFile, parent, sourceFolder); + } + } + } + } + + public static async Task SaveAllResourcesAsync(StorageFolder? targetFolder = null) + { + if (ResourceManager.Resources.Count > 0) + { + CurrentProject?.Entries?.Clear(); + + bool allSaved = true; + foreach (var resource in ResourceManager.Resources) + { + allSaved &= await SaveTopMostResourceAsync(resource); + } + + if (UseProjectManager && CurrentProject != null) + { + allSaved &= await SaveProjectFileAsync(); + } + + return allSaved; + } + + return false; + } + + private static async Task SaveTopMostResourceAsync(IResource resource, string? subFolder = null) + { + if (resource == null) return false; + + // If there are no active project loaded, then fallback to ResourceHelper and let it handle. + if (!UseProjectManager) + return await ResourceHelper.SaveResourceAsync(resource); + + // Only top-most resources (direct children of ProjectFolders and + // direct children of ResourceManager.Resources) are be handled here. + if (resource is ProjectFolder && resource.ChildrenResources != null) + { + string subFolderPath = ResourceManager.GetAbsoluteFolderPath(resource); + bool allChildrenSaved = true; + foreach (var child in resource.ChildrenResources) + { + allChildrenSaved &= await SaveTopMostResourceAsync(child, subFolderPath); + } + return allChildrenSaved; + } + else + { + string? extension = resource switch + { + MetadataResource => JsonFileExtension, + FileResource fileResource => fileResource.FileExtension, + _ => string.Empty + }; + + // NOTE: To prevent adding the resources to the project file. + // As they will be loaded regardless. + if (resource.ParentResource is ProjectFolder) + CurrentProject?.Entries?.Add(new(subFolder ?? PathSeparator.ToString(), resource.Title + extension)); + + // Only pass the targetFolder for PackageResources. For others, they will use the relative folder path + StorageFolder? targetFolder = null; + if (resource is PackageResource) + targetFolder = await StorageHelper.CreateSubFoldersAsync(ResourceHelper.WorkFolder, subFolder); + + return await ResourceHelper.SaveResourceAsync(resource, targetFolder); + } + } + + // This will find the ancestor project folder and savable metadata and save it as well + // (i.e. PackageResource or MetadataResource with SplitMetadata = true). + // NOTE: wouldn't it be problematic if this function also saves the sibling resources? + public static async Task SaveResourceAndAncestorAsync(IResource? resource) + { + if (UseProjectManager && ResourceManager.TryGetSavableParent(resource, out IMetadataResource? parent)) + { + StorageFolder? targetFolder = null; + // Checks if there are any parent ProjectFolder. + if (ResourceManager.TryGetParentOfType(parent, out ProjectFolder? folder)) + { + string subFolderPath = ResourceManager.GetAbsoluteFolderPath(folder); + targetFolder = await StorageHelper.CreateSubFoldersAsync(ResourceHelper.WorkFolder, subFolderPath); + } + + return await ResourceHelper.SaveResourceAsync(parent, targetFolder); + } + + // Rely on relative folder path + return await ResourceHelper.SaveResourceAsync(resource); + } + + private static async Task SaveProjectFileAsync() + { + if (CurrentProject == null) return false; + + StorageFile? saveFile = await ResourceHelper.PickSaveFileAsync(CurrentProject.Name, ProjectFileExtension, "Project File"); + if (saveFile != null) + { + string xml = Project.Serialize(CurrentProject); + return await StorageHelper.WriteToFileAsync(saveFile, xml); + } + + return false; + } +} diff --git a/src/Symptum.Common/Symptum.Common.csproj b/src/Symptum.Common/Symptum.Common.csproj index 7f06e79..25971ee 100644 --- a/src/Symptum.Common/Symptum.Common.csproj +++ b/src/Symptum.Common/Symptum.Common.csproj @@ -39,4 +39,8 @@ + + + + \ No newline at end of file diff --git a/src/Symptum.Core/Extensions/StringExtensions.cs b/src/Symptum.Core/Extensions/StringExtensions.cs index 1f01dbb..ca410d5 100644 --- a/src/Symptum.Core/Extensions/StringExtensions.cs +++ b/src/Symptum.Core/Extensions/StringExtensions.cs @@ -1,5 +1,3 @@ -using System; - namespace Symptum.Core.Extensions; public static class StringExtensions diff --git a/src/Symptum.Core/Helpers/FileHelper.cs b/src/Symptum.Core/Helpers/FileHelper.cs index 7f88210..840f8cb 100644 --- a/src/Symptum.Core/Helpers/FileHelper.cs +++ b/src/Symptum.Core/Helpers/FileHelper.cs @@ -6,6 +6,8 @@ public static class FileHelper public const char ExtensionSeparator = '.'; + public const string ProjectFileExtension = ".symproj"; + public const string JsonFileExtension = ".json"; public const string CsvFileExtension = ".csv"; @@ -74,10 +76,15 @@ public static (string folder, string fileName, string extension) GetDetailsFromF } } - if (dotIndex > 0 && slashIndex > 0) - { + if (slashIndex > 0) folder = filePath[..slashIndex]; - fileName = filePath[slashIndex..dotIndex]; + + if (dotIndex > 0) + { + if (slashIndex > 0) + fileName = filePath[slashIndex..dotIndex]; + else + fileName = filePath[..dotIndex]; extension = filePath[dotIndex..]; } diff --git a/src/Symptum.Core/Management/Resources/ResourceManager.cs b/src/Symptum.Core/Management/Resources/ResourceManager.cs index e9f7efd..d21e7e6 100644 --- a/src/Symptum.Core/Management/Resources/ResourceManager.cs +++ b/src/Symptum.Core/Management/Resources/ResourceManager.cs @@ -22,21 +22,67 @@ public class ResourceManager #region Resource File Handling + /// + /// Gets the file name of the resource. + /// + /// The resource to get the file name from. + /// The file name of the resource. public static string? GetResourceFileName(IResource? resource) => resource?.Title; - public static string GetResourceFolderPath(IResource? resource) + /// + /// Gets the absolute folder path of the resource including its path. + /// + /// The resource to get the path from. + /// The absolute folder path of the resource including its path. + public static string GetAbsoluteFolderPath(IResource? resource) + { + string _path = PathSeparator.ToString(); + if (resource != null) + { + _path = GetAbsoluteFolderPath(resource.ParentResource) + GetResourceFileName(resource) + PathSeparator; + } + return _path; + } + + /// + /// Gets the absolute folder path of the resource. + /// + /// The resource to get the path from. + /// The absolute folder path of the resource. + public static string GetAbsoluteResourceFolderPath(IResource? resource) { string _path = PathSeparator.ToString(); if (resource?.ParentResource is IResource parent) { - _path = GetResourceFolderPath(parent) + GetResourceFileName(parent) + PathSeparator; + _path = GetAbsoluteResourceFolderPath(parent) + GetResourceFileName(parent) + PathSeparator; + } + return _path; + } + + /// + /// Gets the folder path of the resource relative to its parent . + /// + /// The resource to get the path from. + /// The folder path of the resource relative to its parent + public static string GetRelativeResourceFolderPath(IResource? resource) + { + string _path = PathSeparator.ToString(); + if (resource is not PackageResource && resource?.ParentResource is IResource parent) + { + _path = GetRelativeResourceFolderPath(parent) + GetResourceFileName(parent) + PathSeparator; } return _path; } + /// + /// Gets the file path of the resource relative to its parent . + /// + /// The resource to get the path from. + /// The file extension of the resource. + /// The file path of the resource relative to its parent public static string GetResourceFilePath(IResource? resource, string? extension) { - string path = GetResourceFolderPath(resource); + string path = GetRelativeResourceFolderPath(resource); return path + GetResourceFileName(resource) + extension; } @@ -62,6 +108,34 @@ public static string GetResourceFilePath(IResource? resource, string? extension) #region Resource Fetching + #region Get Parent + + public static bool TryGetParentOfType(IResource? resource, [NotNullWhen(true)] out T? parent, + Func? condition = null) where T : IResource + { + parent = default; + if (resource == null) return false; + + if (resource.ParentResource is T p) + { + if (condition == null || condition(p)) + { + parent = p; + return true; + } + } + + return TryGetParentOfType(resource.ParentResource, out parent, condition); + } + + public static bool TryGetSavableParent(IResource? resource, [NotNullWhen(true)] out IMetadataResource? parent) => + TryGetParentOfType(resource, out parent, x => x is PackageResource || (x != null && x.SplitMetadata)); + + public static bool TryGetParentPackage(IResource? resource, [NotNullWhen(true)] out PackageResource? package) => + TryGetParentOfType(resource, out package); + + #endregion + #region From Id public static bool TryGetResourceFromId(string? id, [NotNullWhen(true)] out IResource? resource) => diff --git a/src/Symptum.Core/Symptum.Core.csproj b/src/Symptum.Core/Symptum.Core.csproj index 7be4f44..9b27426 100644 --- a/src/Symptum.Core/Symptum.Core.csproj +++ b/src/Symptum.Core/Symptum.Core.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Symptum.Editor.SourceGenerators/NutrientEditorUIGenerator.cs b/src/Symptum.Editor.SourceGenerators/NutrientEditorUIGenerator.cs index b490b9f..81cdda0 100644 --- a/src/Symptum.Editor.SourceGenerators/NutrientEditorUIGenerator.cs +++ b/src/Symptum.Editor.SourceGenerators/NutrientEditorUIGenerator.cs @@ -1,5 +1,4 @@ using System.Text; -using System.Xml.Linq; using Microsoft.CodeAnalysis; namespace Symptum.Editor.SourceGenerators; diff --git a/src/Symptum.Editor/App.xaml.cs b/src/Symptum.Editor/App.xaml.cs index fb979e4..218b481 100644 --- a/src/Symptum.Editor/App.xaml.cs +++ b/src/Symptum.Editor/App.xaml.cs @@ -1,6 +1,6 @@ +using Symptum.Common; using Symptum.Common.Helpers; using Symptum.Core.Management.Deployment; -using Symptum.Core.Subjects.Books; using Uno.Resizetizer; namespace Symptum.Editor; @@ -16,10 +16,7 @@ public App() protected override async void OnLaunched(LaunchActivatedEventArgs args) { - await PackageHelper.InitializeAsync(); - StorageHelper.Initialize(); - - await LoadAllBookListsAsync(); + await Bootstrapper.InitializeAsync(); PackageManager.StartDependencyResolution(); @@ -68,26 +65,6 @@ private void OnNavigationFailed(object sender, NavigationFailedEventArgs e) throw new InvalidOperationException($"Failed to load {e.SourcePageType.FullName}: {e.Exception}"); } - private async Task LoadAllBookListsAsync() - { - try - { - var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Books/First Year Books.csv")); - string content = await FileIO.ReadTextAsync(file); - BookStore.LoadBooks(content); - file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Books/Second Year Books.csv")); - content = await FileIO.ReadTextAsync(file); - BookStore.LoadBooks(content); - file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Books/Third Year Books.csv")); - content = await FileIO.ReadTextAsync(file); - BookStore.LoadBooks(content); - file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Books/Final Year Books.csv")); - content = await FileIO.ReadTextAsync(file); - BookStore.LoadBooks(content); - } - catch { } - } - /// /// Configures global Uno Platform logging /// diff --git a/src/Symptum.Editor/Common/DefaultIconSources.cs b/src/Symptum.Editor/Common/DefaultIconSources.cs index 371f742..136e192 100644 --- a/src/Symptum.Editor/Common/DefaultIconSources.cs +++ b/src/Symptum.Editor/Common/DefaultIconSources.cs @@ -8,5 +8,17 @@ public static class DefaultIconSources public static IconSource DocumentIconSource { get; } = new SymbolIconSource() { Symbol = Symbol.Document }; + public static IconSource GroupListIconSource { get; } = new FontIconSource() { Glyph = "\uF168" }; + + public static IconSource DictionaryIconSource { get; } = new FontIconSource() { Glyph = "\uE82D" }; + + public static IconSource PhotoIconSource { get; } = new FontIconSource() { Glyph = "\uE91B" }; + public static IconSource PicturesIconSource { get; } = new SymbolIconSource() { Symbol = Symbol.Pictures }; + + public static IconSource AudioIconSource { get; } = new SymbolIconSource() { Symbol = Symbol.Audio }; + + public static IconSource FolderIconSource { get; } = new SymbolIconSource() { Symbol = Symbol.Folder }; + + public static IconSource PackageIconSource { get; } = new FontIconSource() { Glyph = "\uE7B8" }; } diff --git a/src/Symptum.Editor/Controls/NewItemType.cs b/src/Symptum.Editor/Common/NewItemType.cs similarity index 93% rename from src/Symptum.Editor/Controls/NewItemType.cs rename to src/Symptum.Editor/Common/NewItemType.cs index 48dd520..2eb6ae7 100644 --- a/src/Symptum.Editor/Controls/NewItemType.cs +++ b/src/Symptum.Editor/Common/NewItemType.cs @@ -1,10 +1,11 @@ +using Symptum.Common.ProjectSystem; using Symptum.Core.Data.Nutrition; using Symptum.Core.Data.ReferenceValues; using Symptum.Core.Management.Resources; using Symptum.Core.Subjects; using Symptum.Core.Subjects.QuestionBanks; -namespace Symptum.Editor.Controls; +namespace Symptum.Editor.Common; public class NewItemType { @@ -26,6 +27,7 @@ public NewItemType(string displayName, Type type, string? groupName = null) public static List KnownTypes { get; } = [ + new("Folder", typeof(ProjectFolder), "Common"), new("Subject", typeof(Subject), "Subjects"), new("Category", typeof(CategoryResource), "Common"), new("Image Category", typeof(ImageCategoryResource), "Common"), diff --git a/src/Symptum.Editor/Common/ResourceBaseToIconSourceMarkupExtension.cs b/src/Symptum.Editor/Common/ResourceBaseToIconSourceMarkupExtension.cs new file mode 100644 index 0000000..8641fd0 --- /dev/null +++ b/src/Symptum.Editor/Common/ResourceBaseToIconSourceMarkupExtension.cs @@ -0,0 +1,79 @@ +using Symptum.Common.ProjectSystem; +using Symptum.Core.Management.Resources; +using Symptum.Core.Subjects; + +namespace Symptum.Editor.Common; + +internal class ResourceBaseToIconSourceExtension +{ + #region Resource + + public static readonly DependencyProperty ResourceProperty = DependencyProperty.RegisterAttached( + "Resource", + typeof(ResourceBase), + typeof(IconSourceElement), + new PropertyMetadata(null, OnResourcePropertyChanged)); + + public static ResourceBase GetResource(IconSourceElement obj) => (ResourceBase)obj.GetValue(ResourceProperty); + + public static void SetResource(IconSourceElement obj, ResourceBase value) => obj.SetValue(ResourceProperty, value); + + private static void OnResourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is IconSourceElement iconSourceElement) + { + if (e.NewValue is ResourceBase resource) + { + iconSourceElement.IconSource = resource switch + { + ProjectFolder => DefaultIconSources.FolderIconSource, + Subject => DefaultIconSources.DictionaryIconSource, + CsvFileResource => DefaultIconSources.DataGridIconSource, + ImageFileResource => DefaultIconSources.PhotoIconSource, + MarkdownFileResource => DefaultIconSources.DocumentIconSource, + ImageCategoryResource => DefaultIconSources.PicturesIconSource, + PackageResource => DefaultIconSources.PackageIconSource, + _ => DefaultIconSources.GroupListIconSource + }; + } + else + iconSourceElement.IconSource = null; + } + } + + #endregion + + #region ResourceType + + public static readonly DependencyProperty ResourceTypeProperty = DependencyProperty.RegisterAttached( + "ResourceType", + typeof(Type), + typeof(IconSourceElement), + new PropertyMetadata(null, OnResourceTypePropertyChanged)); + + public static Type GetResourceType(IconSourceElement obj) => (Type)obj.GetValue(ResourceTypeProperty); + + public static void SetResourceType(IconSourceElement obj, Type value) => obj.SetValue(ResourceTypeProperty, value); + + private static void OnResourceTypePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is IconSourceElement iconSourceElement) + { + if (e.NewValue is Type resourceType) + { + iconSourceElement.IconSource = typeof(ProjectFolder).IsAssignableFrom(resourceType) ? DefaultIconSources.FolderIconSource : + typeof(Subject).IsAssignableFrom(resourceType) ? DefaultIconSources.DictionaryIconSource : + typeof(CsvFileResource).IsAssignableFrom(resourceType) ? DefaultIconSources.DataGridIconSource : + typeof(ImageFileResource).IsAssignableFrom(resourceType) ? DefaultIconSources.PhotoIconSource : + typeof(MarkdownFileResource).IsAssignableFrom(resourceType) ? DefaultIconSources.DocumentIconSource : + typeof(ImageCategoryResource).IsAssignableFrom(resourceType) ? DefaultIconSources.PicturesIconSource : + typeof(PackageResource).IsAssignableFrom(resourceType) ? DefaultIconSources.PackageIconSource : + DefaultIconSources.GroupListIconSource; + } + else + iconSourceElement.IconSource = null; + } + } + + #endregion +} diff --git a/src/Symptum.Editor/Controls/AddNewItemDialog.xaml b/src/Symptum.Editor/Controls/AddNewItemDialog.xaml index 2a3eacc..b659ef2 100644 --- a/src/Symptum.Editor/Controls/AddNewItemDialog.xaml +++ b/src/Symptum.Editor/Controls/AddNewItemDialog.xaml @@ -1,36 +1,36 @@  - - - - - - - - - - - - - - - - - - - - - - - - + x:Class="Symptum.Editor.Controls.AddNewItemDialog" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:local="using:Symptum.Editor.Controls" + xmlns:common="using:Symptum.Editor.Common" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + Style="{ThemeResource DefaultContentDialogStyle}" + CloseButtonText="Cancel" DefaultButton="Primary"> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symptum.Editor/Controls/AddNewItemDialog.xaml.cs b/src/Symptum.Editor/Controls/AddNewItemDialog.xaml.cs index 740c377..69e6926 100644 --- a/src/Symptum.Editor/Controls/AddNewItemDialog.xaml.cs +++ b/src/Symptum.Editor/Controls/AddNewItemDialog.xaml.cs @@ -1,4 +1,5 @@ using Symptum.Core.Management.Resources; +using Symptum.Editor.Common; namespace Symptum.Editor.Controls; @@ -65,11 +66,11 @@ private void AddNewItemDialog_PrimaryButtonClick(ContentDialog sender, ContentDi { bool valid = Validate(); args.Cancel = !valid; - + errorInfoBar.IsOpen = !valid; if (valid) { Result = EditorResult.Create; - SelectedItemType = (newItemsLV.SelectedItem as NewItemType)?.Type; + if (!_newProject) SelectedItemType = (newItemsLV.SelectedItem as NewItemType)?.Type; ItemTitle = titleTextBox.Text; } } @@ -81,39 +82,54 @@ private void AddNewItemDialog_CloseButtonClick(ContentDialog sender, ContentDial private bool Validate() { - bool valid = false; - - if (newItemsLV.SelectedItem is NewItemType itemType) + if (!string.IsNullOrWhiteSpace(titleTextBox.Text)) { - valid = (ParentResource != null - && ParentResource.CanAddChildResourceType(itemType.Type)) - || ParentResource == null; + if (_newProject) return true; - if (valid) + if (newItemsLV.SelectedItem is NewItemType itemType) { - var title = titleTextBox.Text; - valid = !string.IsNullOrEmpty(title) - && !string.IsNullOrWhiteSpace(title); - - if (!valid) - errorInfoBar.Message = "Title must not be empty"; + if ((ParentResource != null + && ParentResource.CanAddChildResourceType(itemType.Type)) + || ParentResource == null) + return true; + else + errorInfoBar.Message = "Cannot add a new item to the parent item (" + ParentResource?.Title + ")"; } else - errorInfoBar.Message = "Cannot add a new item to the parent item (" + ParentResource?.Title + ")"; + errorInfoBar.Message = "An item type must be selected"; } else - errorInfoBar.Message = "An item type must be selected"; + errorInfoBar.Message = "Title must not be empty."; - errorInfoBar.IsOpen = !valid; - return valid; + return false; + } + + private bool _newProject = false; + + public async Task CreateProjectAsync() + { + _newProject = true; + parentInfo.Visibility = Visibility.Collapsed; + queryBox.Visibility = Visibility.Collapsed; + newItemsLV.Visibility = Visibility.Collapsed; + ItemTitle = titleTextBox.Text = string.Empty; + Title = "Create A New Project"; + PrimaryButtonText = "Create"; + await ShowAsync(ContentDialogPlacement.Popup); + return Result; } public async Task CreateAsync(IResource? parentResource) { + _newProject = false; + queryBox.Visibility = Visibility.Visible; + newItemsLV.Visibility = Visibility.Visible; ParentResource = parentResource; SetParentInfo(parentResource); FilterItems(parentResource); ItemTitle = titleTextBox.Text = string.Empty; + Title = "Add New Item"; + PrimaryButtonText = "Add"; await ShowAsync(ContentDialogPlacement.Popup); return Result; } diff --git a/src/Symptum.Editor/Controls/ListEditorControl.xaml b/src/Symptum.Editor/Controls/ListEditorControl.xaml index b7f839a..65c1f6e 100644 --- a/src/Symptum.Editor/Controls/ListEditorControl.xaml +++ b/src/Symptum.Editor/Controls/ListEditorControl.xaml @@ -1,11 +1,12 @@  + x:Class="Symptum.Editor.Controls.ListEditorControl" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:local="using:Symptum.Editor.Controls" + xmlns:common="using:Symptum.Editor.Common" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d"> @@ -18,7 +19,7 @@ - +