From 7307e9250a1f9c92837e9c0e61d16791d3c9dec0 Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Mon, 1 Mar 2021 22:20:40 +0000 Subject: [PATCH] Separator buttons #62 --- .DS_Store | Bin 14340 -> 14340 bytes Damselfly.Core/Services/SearchService.cs | 32 +++---- Damselfly.Core/Services/SelectionService.cs | 35 ++++---- Damselfly.Web/JsMethods.cs | 2 + Damselfly.Web/Shared/About.razor | 2 +- Damselfly.Web/Shared/ExportSettings.razor | 35 +------- Damselfly.Web/Shared/Images/ImageGrid.razor | 83 +++++++++++++------ Damselfly.Web/Shared/LocalFileExporter.razor | 69 +++++++++++++++ Damselfly.Web/wwwroot/css/site.css | 30 ++++++- 9 files changed, 194 insertions(+), 94 deletions(-) create mode 100644 Damselfly.Web/Shared/LocalFileExporter.razor diff --git a/.DS_Store b/.DS_Store index 312f2b80b19041d59d25251314075604488259db..ab8a695c71009f4ae60bea72ec73ce354d0367ec 100644 GIT binary patch delta 98 zcmZoEXeromTY&NS6AtaMQ5Eiq%5ls%03n0`1%9HRK zfGQb#do?yTEFdvCGJPx{doeIFH!L7FI52&E2?!S@F+E9OZFzcoe0_eJlNuu*1)Bf> h01lH75EYZnAP2L_A>anHs2E)bvj;Ht1hf1k2Lq-jDSQ9` diff --git a/Damselfly.Core/Services/SearchService.cs b/Damselfly.Core/Services/SearchService.cs index cdc313ff..da93da62 100644 --- a/Damselfly.Core/Services/SearchService.cs +++ b/Damselfly.Core/Services/SearchService.cs @@ -113,7 +113,22 @@ private async Task LoadMoreData(int first, int count) images = images.Include(x => x.Folder); - if( query.TagId != -1 ) + // Add in the ordering for the group by + switch (query.Grouping) + { + case GroupingType.None: + case GroupingType.Date: + images = images.OrderByDescending(x => x.SortDate); + break; + case GroupingType.Folder: + images = images.OrderBy(x => x.Folder.Path) + .ThenByDescending(x => x.SortDate); + break; + default: + throw new ArgumentException("Unexpected grouping type."); + } + + if ( query.TagId != -1 ) { images = images.Where(x => x.ImageTags.Any(y => y.TagId == query.TagId)); } @@ -171,21 +186,6 @@ private async Task LoadMoreData(int first, int count) images = images.Include(x => x.BasketEntry); - // Add in the ordering for the group by - switch( query.Grouping ) - { - case GroupingType.None: - case GroupingType.Date: - images = images.OrderByDescending(x => x.SortDate); - break; - case GroupingType.Folder: - images = images.OrderBy(x => x.Folder.Path) - .ThenByDescending(x => x.SortDate); - break; - default: - throw new ArgumentException("Unexpected grouping type."); - } - // Disable this for now - it's slow due to the EFCore subquery bug. // We mitigate it by loading the tags in a separate query below. // images = images.Include(x => x.ImageTags) diff --git a/Damselfly.Core/Services/SelectionService.cs b/Damselfly.Core/Services/SelectionService.cs index 51a53dab..554f56cd 100644 --- a/Damselfly.Core/Services/SelectionService.cs +++ b/Damselfly.Core/Services/SelectionService.cs @@ -53,6 +53,24 @@ public void SelectImages(List images) NotifyStateChanged(); } + /// + /// Add images into the selection + /// + /// + public void DeselectImages(List images) + { + bool removed = false; + + foreach (var img in images) + { + if (selectedImages.Remove(img.ImageId)) + removed = true; + } + + if (removed) + NotifyStateChanged(); + } + /// /// Add images into the selection /// @@ -74,27 +92,14 @@ public void ToggleSelection(List images) /// Add a single image into the selection /// /// - public void SelectImage(Image img) - { - if (selectedImages.TryAdd(img.ImageId, img) ) - NotifyStateChanged(); - } + public void SelectImage(Image img) => SelectImages(new List { img }); /// /// Remove an image from the selection /// /// /// - public bool DeselectImage(Image img) - { - if( selectedImages.Remove( img.ImageId ) ) - { - NotifyStateChanged(); - return true; - } - - return false; - } + public void DeselectImage(Image img) => DeselectImages(new List { img }); public int SelectionCount { get { return selectedImages.Count; } } diff --git a/Damselfly.Web/JsMethods.cs b/Damselfly.Web/JsMethods.cs index 2e8bc9ae..6ea4d805 100644 --- a/Damselfly.Web/JsMethods.cs +++ b/Damselfly.Web/JsMethods.cs @@ -6,6 +6,8 @@ namespace Damselfly.Web { public static class JsMethods { + public const string JSGetDesktopVersion = "getDesktopVersion"; + /// /// Called from the Javascript in the Desktop App. The Electron Node code /// calls the checkDesktopUpgrade in _Host.cshtml, passing in its version diff --git a/Damselfly.Web/Shared/About.razor b/Damselfly.Web/Shared/About.razor index 3823bc43..d27816b8 100644 --- a/Damselfly.Web/Shared/About.razor +++ b/Damselfly.Web/Shared/About.razor @@ -76,7 +76,7 @@ if (firstRender) { // Probe for a js function which indicates we're in the electron container - DesktopVersion = await JsRuntime.InvokeAsync("getDesktopVersion"); + DesktopVersion = await JsRuntime.InvokeAsync(JsMethods.JSGetDesktopVersion); isDesktopHosted = !string.IsNullOrEmpty(DesktopVersion); diff --git a/Damselfly.Web/Shared/ExportSettings.razor b/Damselfly.Web/Shared/ExportSettings.razor index 9d05e61a..c97ab735 100644 --- a/Damselfly.Web/Shared/ExportSettings.razor +++ b/Damselfly.Web/Shared/ExportSettings.razor @@ -5,6 +5,7 @@ @inject NavigationManager navManager
+
@@ -41,9 +42,9 @@
- @if (isDesktopHosted) + @if( FileExporter.IsDesktopHosted ) { - + }
@@ -52,6 +53,7 @@
@code { + private LocalFileExporter FileExporter; public ExportType[] exportTypes { get; private set; } = new ExportType[0]; public ExportSize[] exportSizes { get; private set; } = new ExportSize[0]; @@ -61,7 +63,6 @@ public ExportConfig selectedConfig = new ExportConfig { Name = "Default" }; public string StatusMessage { get; set; } public bool KeepFolders { get; set; } - public bool isDesktopHosted { get; set; } private void ChangeConfig(string name) { @@ -85,30 +86,6 @@ StateHasChanged(); } - private async Task ExportToLocalFS() - { - var baseUrl = new Uri(navManager.BaseUri); - - foreach (var image in basketService.SelectedImages) - { - var imageUrl = new Uri(baseUrl, image.RawImageUrl); - - // TODO: Save XMP and On1 sidecars with the images here. - - // Image folder is fully qualified, so we need to make it relative to the pictures folder - // The electron container will use its local settings to create the full path of where - // the image will be written locally on the client-side - string localPath = image.FullPath.MakePathRelativeTo(IndexingService.RootFolder); - - StatusService.Instance.StatusText = $"Writing {image.FileName} to {localPath}..."; - - // Now, shell out to Javascript, which will trigger the download in the Electron container - await JsRuntime.InvokeAsync("writeFileLocally", imageUrl, localPath); - } - - StatusService.Instance.StatusText = "Selected images saved locally."; - } - private void Export() { InvokeAsync(() => { _ = ProcessExport(); }); @@ -131,10 +108,6 @@ exportSizes = (ExportSize[])Enum.GetValues(typeof(ExportSize)); await LoadConfigs(); - // Probe for a js function which indicates we're in the electron container - var version = await JsRuntime.InvokeAsync("getDesktopVersion"); - isDesktopHosted = !string.IsNullOrEmpty( version ); - StateHasChanged(); } } diff --git a/Damselfly.Web/Shared/Images/ImageGrid.razor b/Damselfly.Web/Shared/Images/ImageGrid.razor index 520aede5..4bcf10ff 100644 --- a/Damselfly.Web/Shared/Images/ImageGrid.razor +++ b/Damselfly.Web/Shared/Images/ImageGrid.razor @@ -12,6 +12,7 @@ @using Damselfly.Web.Shared.Images @using Damselfly.Core.ImageProcessing +
@if (!gridImages.Any()) { @@ -34,19 +35,36 @@ if (!string.IsNullOrEmpty(grouping.Key)) {
- @grouping.Key - - +
+ @grouping.Key +
+
+ + + + + @if (FileExporter.IsDesktopHosted) + { + + } +
} - foreach( var image in grouping.Images ) + foreach (var image in grouping.Images) { var info = new SelectionInfo { image = image, index = allImages++ }; @@ -83,8 +101,8 @@ { } @@ -93,9 +111,10 @@
@code { + private LocalFileExporter FileExporter; const int scrollTriggerImages = 30; const int imagesPerPage = 250; - private bool showDateSeparators = ConfigService.Instance.GetBool( "DateSeparators", false ); + private bool showDateSeparators = ConfigService.Instance.GetBool("DateSeparators", false); bool IsLoading { get; set; } = false; bool endOfImages = false; @@ -103,7 +122,7 @@ ThumbSize CurrentThumbSize = ConfigService.Instance.Get("ThumbSize", ThumbSize.Small); - private void StoreImage( Image image ) + private void StoreImage(Image image) { // Todo - save an image to local storage } @@ -114,10 +133,20 @@ StatusService.Instance.StatusText = $"{grouping.Images.Count()} images added to the basket"; } + void RemoveGroupFromBasket(ImageGrouping grouping) + { + BasketService.Instance.SetBasketState(grouping.Images, false); + StatusService.Instance.StatusText = $"{grouping.Images.Count()} images removed from the basket"; + } + void SelectGroup(ImageGrouping grouping) { SelectionService.Instance.SelectImages(grouping.Images); - StatusService.Instance.StatusText = $"{grouping.Images.Count()} images added to the basket"; + } + + void DeselectGroup(ImageGrouping grouping) + { + SelectionService.Instance.DeselectImages(grouping.Images); } private List GroupedImages @@ -135,7 +164,7 @@ else if (SearchService.Instance.Grouping == SearchQuery.GroupingType.Date) { return gridImages.GroupBy(x => x.SortDate.Date) - .OrderByDescending( x => x.Key ) + .OrderByDescending(x => x.Key) .Select(x => new ImageGrouping { Key = x.Key.ToString("dddd, dd MMMM yyyy"), Images = x.ToList() }) .ToList(); } @@ -145,7 +174,7 @@ } - private void ChangeGroupType(SearchQuery.GroupingType newType ) + private void ChangeGroupType(SearchQuery.GroupingType newType) { SearchService.Instance.Grouping = newType; } @@ -174,15 +203,15 @@ { int initialLoadCount, scrollPos; - if ( !int.TryParse(ConfigService.Instance.Get("LoadedImages"), out initialLoadCount) || initialLoadCount < imagesPerPage) + if (!int.TryParse(ConfigService.Instance.Get("LoadedImages"), out initialLoadCount) || initialLoadCount < imagesPerPage) initialLoadCount = imagesPerPage; - if ( !int.TryParse(ConfigService.Instance.Get("ImageScrollTop"), out scrollPos)) + if (!int.TryParse(ConfigService.Instance.Get("ImageScrollTop"), out scrollPos)) scrollPos = 0; searchService.OnChange += SearchQueryChanged; - await LoadData( initialLoadCount ); - await InitJsListenerAsync( scrollPos ); + await LoadData(initialLoadCount); + await InitJsListenerAsync(scrollPos); } } @@ -213,22 +242,22 @@ _ = LoadData(imagesPerPage); } - protected async Task InitJsListenerAsync( int initialScrollPos ) + protected async Task InitJsListenerAsync(int initialScrollPos) { await JsRuntime.InvokeVoidAsync("InfiniteScroll.Init", "scroll-area", "list-end", DotNetObjectReference.Create(this), initialScrollPos); } - private void SaveScrollState( int scrollTop ) + private void SaveScrollState(int scrollTop) { - ConfigService.Instance.Set("ImageScrollTop", scrollTop.ToString() ); + ConfigService.Instance.Set("ImageScrollTop", scrollTop.ToString()); ConfigService.Instance.Set("LoadedImages", gridImages.Count.ToString()); } [JSInvokable] // Debugging method to help us differentiate between JS calls and other data loads - public void HandleScroll( int scrollTop ) + public void HandleScroll(int scrollTop) { - conflator.HandleEvent( (x) => { SaveScrollState( scrollTop ); } ); + conflator.HandleEvent((x) => { SaveScrollState(scrollTop); }); } [JSInvokable] @@ -238,7 +267,7 @@ await LoadData(imagesPerPage); } - public async Task LoadData( int imagesToLoad ) + public async Task LoadData(int imagesToLoad) { if (!IsLoading) { diff --git a/Damselfly.Web/Shared/LocalFileExporter.razor b/Damselfly.Web/Shared/LocalFileExporter.razor new file mode 100644 index 00000000..40421e3c --- /dev/null +++ b/Damselfly.Web/Shared/LocalFileExporter.razor @@ -0,0 +1,69 @@ + +@inject BasketService basketService +@inject ViewDataService ViewDataService +@inject IJSRuntime JsRuntime +@inject NavigationManager navManager + +@code { + public bool IsDesktopHosted { get; set; } + + public async Task ExportBasketToLocalFilesystem() + { + await ExportToLocalFS(basketService.SelectedImages); + } + + public async Task ExportImagesToLocalFilesystem(List images) + { + await ExportToLocalFS( images ); + } + + /// + /// Do the actual export + /// + /// + private async Task ExportToLocalFS(List images) + { + if (IsDesktopHosted) + { + var baseUrl = new Uri(navManager.BaseUri); + + foreach (var image in images) + { + var imageUrl = new Uri(baseUrl, image.RawImageUrl); + + // TODO: Save XMP and On1 sidecars with the images here. + + // Image folder is fully qualified, so we need to make it relative to the pictures folder + // The electron container will use its local settings to create the full path of where + // the image will be written locally on the client-side + string localPath = image.FullPath.MakePathRelativeTo(IndexingService.RootFolder); + + StatusService.Instance.StatusText = $"Writing {image.FileName} to {localPath}..."; + + // Now, shell out to Javascript, which will trigger the download in the Electron container + await JsRuntime.InvokeAsync("writeFileLocally", imageUrl, localPath); + } + + StatusService.Instance.StatusText = "Selected images saved locally."; + } + else + StatusService.Instance.StatusText = "Local save is not available outside the Desktop Client."; + } + + /// + /// Initialise the state to tell us if LocalFS is available. + /// + /// + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + // Probe for a js function which indicates we're in the electron container + var version = await JsRuntime.InvokeAsync("getDesktopVersion"); + IsDesktopHosted = !string.IsNullOrEmpty( version ); + + StateHasChanged(); + } + } +} \ No newline at end of file diff --git a/Damselfly.Web/wwwroot/css/site.css b/Damselfly.Web/wwwroot/css/site.css index 46424394..557cf2d6 100644 --- a/Damselfly.Web/wwwroot/css/site.css +++ b/Damselfly.Web/wwwroot/css/site.css @@ -83,9 +83,10 @@ div::-webkit-scrollbar-thumb { .btn-primary { color: #fff; - background-color: #1bc26e; + background-color: #1ba96e; border-color: #18ac61; - min-height: 30px; + height: 30px; + border-radius: 4px; } .image-grid-controls { @@ -861,11 +862,32 @@ f border: 1px; margin-top: 5px; margin-left: 3px; + max-height: 28px; + border-top: 1px solid rgba(130,155,130,0.75); + background-image: linear-gradient(90deg, rgb(50, 103, 64) 0%, rgb(10, 10, 10) 90%); + display: flex; +} + +.damselfly-imageseparatortitle { padding: 3px; + flex: 1 1 60%; padding-left: 15px; font-size: 14px; - border-top: 1px solid rgba(130,155,130,0.75); - background-image: linear-gradient(90deg, rgb(50, 103, 64) 0%, rgb(10, 10, 10) 90%); + text-wrap: none; +} + +.damselfly-imageseparatorbuttons { + width: 100%; + display: flex; + justify-content: flex-end; + align-items: center; + font-size: 12px; +} + +.damselfly-imageseparatorbutton button { + max-height: 22px; + height: 22px; + margin: 1px; } .InputAddOn {