diff --git a/.DS_Store b/.DS_Store index 1e4a1d6a..d3297681 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/workflows/damselfly-actions.yml b/.github/workflows/damselfly-actions.yml index ab671ccc..94b26ab3 100644 --- a/.github/workflows/damselfly-actions.yml +++ b/.github/workflows/damselfly-actions.yml @@ -180,12 +180,14 @@ jobs: uses: actions/download-artifact@v3 with: name: Desktop-Dist + path: Damselfly.Web.Client/wwwroot/desktop - name: Download Server Artifacts if: github.ref == 'refs/heads/master' uses: actions/download-artifact@v3 with: name: Server-Dist + path: server - name: Upload Server Artifacts if: github.ref == 'refs/heads/master' diff --git a/Damselfly.Core.Constants/ConfigSettings.cs b/Damselfly.Core.Constants/ConfigSettings.cs index 18a890a6..e425e021 100644 --- a/Damselfly.Core.Constants/ConfigSettings.cs +++ b/Damselfly.Core.Constants/ConfigSettings.cs @@ -16,6 +16,8 @@ public class ConfigSettings public const string FlatView = "FlatView"; public const string FolderSortAscending = "FolderSortAscending"; public const string FolderSortMode = "FolderSortMode"; + public const string IncludeChildFolders = "IncludeChildFolders"; + public const string TrashcanFolderName = "TrashcanFolderName"; public const string WordpressURL = "WordpressURL"; public const string WordpressUser = "WordpressUser"; diff --git a/Damselfly.Core.DbModels/Damselfly.Core.DbModels.csproj b/Damselfly.Core.DbModels/Damselfly.Core.DbModels.csproj index 9cc4efe7..955be2e6 100644 --- a/Damselfly.Core.DbModels/Damselfly.Core.DbModels.csproj +++ b/Damselfly.Core.DbModels/Damselfly.Core.DbModels.csproj @@ -1,21 +1,21 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + diff --git a/Damselfly.Core.DbModels/Interfaces/IFileService.cs b/Damselfly.Core.DbModels/Interfaces/IFileService.cs new file mode 100644 index 00000000..d6123c7f --- /dev/null +++ b/Damselfly.Core.DbModels/Interfaces/IFileService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Damselfly.Core.DbModels.Models.APIModels; + +namespace Damselfly.Core.ScopedServices.Interfaces; + +public interface IFileService +{ + public Task MoveImages( ImageMoveRequest req ); + public Task DeleteImages( MultiImageRequest req ); +} + diff --git a/Damselfly.Core.DbModels/Interfaces/ISearchService.cs b/Damselfly.Core.DbModels/Interfaces/ISearchService.cs index 9483c78d..2de2a4f1 100644 --- a/Damselfly.Core.DbModels/Interfaces/ISearchService.cs +++ b/Damselfly.Core.DbModels/Interfaces/ISearchService.cs @@ -27,6 +27,7 @@ public interface ISearchService bool TagsOnly { get; set; } bool IncludeAITags { get; set; } bool UntaggedImages { get; set; } + bool IncludeChildFolders { get; set; } FaceSearchType? FaceSearch { get; set; } GroupingType Grouping { get; set; } SortOrderType SortOrder { get; set; } diff --git a/Damselfly.Core.DbModels/Models/API Models/ImageMoveRequest.cs b/Damselfly.Core.DbModels/Models/API Models/ImageMoveRequest.cs new file mode 100644 index 00000000..d1979105 --- /dev/null +++ b/Damselfly.Core.DbModels/Models/API Models/ImageMoveRequest.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Damselfly.Core.Models; + +namespace Damselfly.Core.DbModels.Models.APIModels; + +public class MultiImageRequest +{ + public ICollection ImageIDs { get; set; } +} + +public class ImageMoveRequest : MultiImageRequest +{ + public Folder Destination { get; set; } + public bool Move { get; set; } +} + diff --git a/Damselfly.Core.DbModels/Models/Entities/SearchQuery.cs b/Damselfly.Core.DbModels/Models/Entities/SearchQuery.cs index 8e65981f..79ede341 100644 --- a/Damselfly.Core.DbModels/Models/Entities/SearchQuery.cs +++ b/Damselfly.Core.DbModels/Models/Entities/SearchQuery.cs @@ -14,6 +14,7 @@ public class SearchQuery public bool TagsOnly { get; set; } = false; public bool IncludeAITags { get; set; } = true; public bool UntaggedImages { get; set; } = false; + public bool IncludeChildFolders { get; set; } = true; public int? MaxSizeKB { get; set; } = null; public int? MinSizeKB { get; set; } = null; public int? CameraId { get; set; } = null; @@ -42,6 +43,7 @@ public void Reset() SearchText = string.Empty; TagsOnly = false; IncludeAITags = true; + IncludeChildFolders = true; UntaggedImages = false; MaxSizeKB = null; MinSizeKB = null; @@ -62,7 +64,7 @@ public void Reset() public override string ToString() { return - $"Filter: T={SearchText}, F={Folder?.FolderId}, Tag={Tag?.TagId}, Max={MaxDate}, Min={MinDate}, Max={MaxSizeKB}KB, Rating={MinRating}, Min={MinSizeKB}KB, Tags={TagsOnly}, Grouping={Grouping}, Sort={SortOrder}, Face={FaceSearch}, Person={Person?.Name}, SimilarTo={SimilarToId}"; + $"Filter: T={SearchText}, F={Folder?.FolderId}, ChildFolders={IncludeChildFolders}, Tag={Tag?.TagId}, Max={MaxDate}, Min={MinDate}, Max={MaxSizeKB}KB, Rating={MinRating}, Min={MinSizeKB}KB, Tags={TagsOnly}, Grouping={Grouping}, Sort={SortOrder}, Face={FaceSearch}, Person={Person?.Name}, SimilarTo={SimilarToId}"; } } @@ -76,6 +78,7 @@ public class SearchQueryDTO public bool TagsOnly { get; set; } = false; public bool IncludeAITags { get; set; } = true; public bool UntaggedImages { get; set; } = false; + public bool IncludeChildFolders { get; set; } = true; public int? MaxSizeKB { get; set; } = null; public int? MinSizeKB { get; set; } = null; public int? CameraId { get; set; } = null; diff --git a/Damselfly.Core.ImageProcessing/Damselfly.Core.ImageProcessing.csproj b/Damselfly.Core.ImageProcessing/Damselfly.Core.ImageProcessing.csproj index cbab3b1c..9e22e1f5 100644 --- a/Damselfly.Core.ImageProcessing/Damselfly.Core.ImageProcessing.csproj +++ b/Damselfly.Core.ImageProcessing/Damselfly.Core.ImageProcessing.csproj @@ -6,16 +6,16 @@ - - - + + + - - + + - - + + diff --git a/Damselfly.Core.ImageProcessing/ImageSharpProcessor.cs b/Damselfly.Core.ImageProcessing/ImageSharpProcessor.cs index cd19885f..f60e64c4 100644 --- a/Damselfly.Core.ImageProcessing/ImageSharpProcessor.cs +++ b/Damselfly.Core.ImageProcessing/ImageSharpProcessor.cs @@ -7,9 +7,11 @@ using Damselfly.Core.Interfaces; using Damselfly.Core.Utils; using Damselfly.Shared.Utils; +using Org.BouncyCastle.Utilities.Zlib; using SixLabors.Fonts; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -62,9 +64,15 @@ public async Task CreateThumbs(FileInfo source, IDictionary IImageProcessResult result = new ImageProcessResult(); var load = new Stopwatch("ImageSharpLoad"); + var largest = destFiles.Values + .OrderByDescending( x => x.width ) + .First(); + + DecoderOptions options = new() { TargetSize = new( width: largest.width, height: largest.height ) }; + // Image.Load(string path) is a shortcut for our default type. // Other pixel formats use Image.Load(string path)) - using var image = await Image.LoadAsync(source.FullName); + using var image = await Image.LoadAsync(options, source.FullName); load.Stop(); @@ -120,7 +128,7 @@ public async Task GetCroppedFile(FileInfo source, int x, int y, int width, int h // Image.Load(string path) is a shortcut for our default type. // Other pixel formats use Image.Load(string path)) - using var image = Image.Load(source.FullName); + using var image = await Image.LoadAsync(source.FullName); var rect = new Rectangle(x, y, width, height); image.Mutate(x => x.AutoOrient()); @@ -136,7 +144,7 @@ public async Task CropImage(FileInfo source, int x, int y, int width, int height // Image.Load(string path) is a shortcut for our default type. // Other pixel formats use Image.Load(string path)) - using var image = Image.Load(source.FullName); + using var image = await Image.LoadAsync(source.FullName); var rect = new Rectangle(x, y, width, height); image.Mutate(x => x.AutoOrient()); @@ -157,7 +165,9 @@ public async Task TransformDownloadImage(string input, Stream output, IExportSet { Logging.Log($" Running image transform for Watermark: {config.WatermarkText}"); - using var img = Image.Load(input, out var fmt); + DecoderOptions options = new() { TargetSize = new( width: config.MaxImageSize, height: config.MaxImageSize ) }; + + using var img = await Image.LoadAsync(options, input); if ( config.Size != ExportSize.FullRes ) { @@ -188,7 +198,7 @@ public async Task TransformDownloadImage(string input, Stream output, IExportSet img.Mutate(context => ApplyWaterMark(context, font, config.WatermarkText, Color.White)); } - await img.SaveAsync(output, fmt); + await img.SaveAsync( output, img.Metadata.DecodedImageFormat ); } /// diff --git a/Damselfly.Core.Interfaces/Damselfly.Core.Interfaces.csproj b/Damselfly.Core.Interfaces/Damselfly.Core.Interfaces.csproj index 14a3caf0..08f45741 100644 --- a/Damselfly.Core.Interfaces/Damselfly.Core.Interfaces.csproj +++ b/Damselfly.Core.Interfaces/Damselfly.Core.Interfaces.csproj @@ -4,7 +4,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Damselfly.Core.ScopedServices/BaseConfigService.cs b/Damselfly.Core.ScopedServices/BaseConfigService.cs index 380af240..29358772 100644 --- a/Damselfly.Core.ScopedServices/BaseConfigService.cs +++ b/Damselfly.Core.ScopedServices/BaseConfigService.cs @@ -77,7 +77,7 @@ public void Set(string name, string value) SetSetting(name, new ConfigSetting { Name = name, Value = value }); } - public string Get(string name, string defaultIfNotExists = null) + public string Get(string name, string? defaultIfNotExists = null) { var existing = GetSetting(name); diff --git a/Damselfly.Core.ScopedServices/BaseSearchService.cs b/Damselfly.Core.ScopedServices/BaseSearchService.cs index 6f58ff84..860ce092 100644 --- a/Damselfly.Core.ScopedServices/BaseSearchService.cs +++ b/Damselfly.Core.ScopedServices/BaseSearchService.cs @@ -124,7 +124,7 @@ public bool TagsOnly } } } - + public bool IncludeAITags { get => Query.IncludeAITags; @@ -138,6 +138,19 @@ public bool IncludeAITags } } + public bool IncludeChildFolders + { + get => Query.IncludeChildFolders; + set + { + if( Query.IncludeChildFolders != value ) + { + Query.IncludeChildFolders = value; + QueryChanged(); + } + } + } + public bool UntaggedImages { get => Query.UntaggedImages; @@ -368,7 +381,10 @@ public string SearchBreadcrumbs hints.Add($"Lens: {lens.Model}"); } - if ( hints.Any() ) + if( !IncludeChildFolders ) + hints.Add( $"Exclude child folders" ); + + if( hints.Any() ) return string.Join(", ", hints); return "No Filter"; diff --git a/Damselfly.Core.ScopedServices/Client Services/ApiAuthenticationStateProvider.cs b/Damselfly.Core.ScopedServices/Client Services/ApiAuthenticationStateProvider.cs index 79c5f742..b298050f 100644 --- a/Damselfly.Core.ScopedServices/Client Services/ApiAuthenticationStateProvider.cs +++ b/Damselfly.Core.ScopedServices/Client Services/ApiAuthenticationStateProvider.cs @@ -54,32 +54,38 @@ private IEnumerable ParseClaimsFromJwt(string jwt) var jsonBytes = ParseBase64WithoutPadding(payload); var keyValuePairs = JsonSerializer.Deserialize>(jsonBytes); - keyValuePairs.TryGetValue(ClaimTypes.Role, out var roles); - - if ( roles != null ) + if( keyValuePairs != null ) { - if ( roles.ToString().Trim().StartsWith("[") ) - { - var parsedRoles = JsonSerializer.Deserialize(roles.ToString()); + keyValuePairs.TryGetValue( ClaimTypes.Role, out var roles ); - _logger.LogInformation("Parsed roles from JWT:"); - foreach ( var parsedRole in parsedRoles ) + if( roles != null ) + { + if( roles.ToString().Trim().StartsWith( "[" ) ) { - claims.Add(new Claim(ClaimTypes.Role, parsedRole)); - _logger.LogTrace($" [{roles}]"); + var parsedRoles = JsonSerializer.Deserialize( roles.ToString() ); + + if( parsedRoles != null ) + { + _logger.LogInformation( "Parsed roles from JWT:" ); + foreach( var parsedRole in parsedRoles ) + { + claims.Add( new Claim( ClaimTypes.Role, parsedRole ) ); + _logger.LogTrace( $" [{roles}]" ); + } + } } - } - else - { - claims.Add(new Claim(ClaimTypes.Role, roles.ToString())); - _logger.LogTrace($"Parsed role from JWT: [{roles}]"); + else + { + claims.Add( new Claim( ClaimTypes.Role, roles.ToString() ) ); + _logger.LogTrace( $"Parsed role from JWT: [{roles}]" ); + } + + keyValuePairs.Remove( ClaimTypes.Role ); } - keyValuePairs.Remove(ClaimTypes.Role); + claims.AddRange( keyValuePairs.Select( kvp => new Claim( kvp.Key, kvp.Value.ToString() ) ) ); } - claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()))); - _logger.LogTrace( $"Parsed Claims from JWT: {string.Join(", ", keyValuePairs.Select(x => $"{x.Key} = {x.Value}"))}"); diff --git a/Damselfly.Core.ScopedServices/Client Services/AuthService.cs b/Damselfly.Core.ScopedServices/Client Services/AuthService.cs index 43c2f32b..df536627 100644 --- a/Damselfly.Core.ScopedServices/Client Services/AuthService.cs +++ b/Damselfly.Core.ScopedServices/Client Services/AuthService.cs @@ -33,7 +33,7 @@ public async Task Login(LoginModel loginModel) var loginResult = await _httpClient.CustomPostAsJsonAsync("api/Login", loginModel); var provider = _authenticationStateProvider as ApiAuthenticationStateProvider; - if ( loginResult.Successful ) + if ( loginResult != null && provider != null &&loginResult.Successful ) { await _localStorage.SetItemAsync("authToken", loginResult.Token); diff --git a/Damselfly.Core.ScopedServices/Client Services/ClientBasketService.cs b/Damselfly.Core.ScopedServices/Client Services/ClientBasketService.cs index 655a28c5..365b185f 100644 --- a/Damselfly.Core.ScopedServices/Client Services/ClientBasketService.cs +++ b/Damselfly.Core.ScopedServices/Client Services/ClientBasketService.cs @@ -72,21 +72,22 @@ public async Task SwitchToBasket(int basketId) } catch ( Exception ex ) { - _logger.LogError($"Attempted to switch to unknown basket ID {basketId}"); + _logger.LogError($"Attempted to switch to unknown basket ID {basketId}: {ex}"); throw; } } public async Task SwitchToDefaultBasket(int? userId) { - Basket basket; + Basket? basket; if ( userId is null ) basket = await httpClient.CustomGetFromJsonAsync("/api/basketdefault"); else basket = await httpClient.CustomGetFromJsonAsync($"/api/basketdefault/{userId}"); - await SetCurrentBasket(basket); + if( basket != null ) + await SetCurrentBasket(basket); return basket; } diff --git a/Damselfly.Core.ScopedServices/Client Services/ClientConfigService.cs b/Damselfly.Core.ScopedServices/Client Services/ClientConfigService.cs index c80f856e..7e952b43 100644 --- a/Damselfly.Core.ScopedServices/Client Services/ClientConfigService.cs +++ b/Damselfly.Core.ScopedServices/Client Services/ClientConfigService.cs @@ -78,7 +78,7 @@ protected override async Task PersistSetting(ConfigSetRequest saveRequest) protected override async Task> LoadAllSettings() { - List allSettings; + List? allSettings; try { if ( _userId.HasValue ) diff --git a/Damselfly.Core.ScopedServices/Client Services/ClientDataService.cs b/Damselfly.Core.ScopedServices/Client Services/ClientDataService.cs index 37acdabc..fca9560a 100644 --- a/Damselfly.Core.ScopedServices/Client Services/ClientDataService.cs +++ b/Damselfly.Core.ScopedServices/Client Services/ClientDataService.cs @@ -16,7 +16,7 @@ public class ClientDataService : ICachedDataService private readonly List _lenses = new(); private readonly ILogger _logger; private readonly RestClient httpClient; - private StaticData _staticData; + private StaticData? _staticData; public ClientDataService(RestClient client, ILogger logger) { @@ -24,8 +24,8 @@ public ClientDataService(RestClient client, ILogger logger) _logger = logger; } - public string ImagesRootFolder => _staticData.ImagesRootFolder; - public string ExifToolVer => _staticData.ExifToolVer; + public string ImagesRootFolder => _staticData?.ImagesRootFolder; + public string ExifToolVer => _staticData?.ExifToolVer; public ICollection Cameras => _cameras; public ICollection Lenses => _lenses; diff --git a/Damselfly.Core.ScopedServices/Client Services/ClientDownloadService.cs b/Damselfly.Core.ScopedServices/Client Services/ClientDownloadService.cs index d826bfa8..e3ef34f8 100644 --- a/Damselfly.Core.ScopedServices/Client Services/ClientDownloadService.cs +++ b/Damselfly.Core.ScopedServices/Client Services/ClientDownloadService.cs @@ -24,8 +24,11 @@ public async Task CreateDownloadZipAsync(ICollection imageIds, Expo { var request = new DownloadRequest { ImageIds = imageIds, Config = config }; - var response = - await httpClient.CustomPostAsJsonAsync("/api/download/images", request); - return response.DownloadUrl; + var response = await httpClient.CustomPostAsJsonAsync("/api/download/images", request); + + if( response != null ) + return response.DownloadUrl; + + return null; } } \ No newline at end of file diff --git a/Damselfly.Core.ScopedServices/Client Services/ClientFileService.cs b/Damselfly.Core.ScopedServices/Client Services/ClientFileService.cs new file mode 100644 index 00000000..54d0b8e0 --- /dev/null +++ b/Damselfly.Core.ScopedServices/Client Services/ClientFileService.cs @@ -0,0 +1,54 @@ + +using Damselfly.Core.DbModels.Models.APIModels; +using Damselfly.Core.ScopedServices.ClientServices; +using Damselfly.Core.ScopedServices.Interfaces; +using Microsoft.Extensions.Logging; + +namespace Damselfly.Core.ScopedServices; + +public class ClientFileService : IFileService +{ + private readonly ILogger _logger; + private readonly NotificationsService _notifications; + private readonly RestClient _restClient; + + public ClientFileService( NotificationsService notifications, RestClient restClient, IUserService userService, + ILogger logger) + { + _restClient = restClient; + _notifications = notifications; + _logger = logger; + } + + public async Task DeleteImages(MultiImageRequest req) + { + try + { + var response = await _restClient.CustomPostAsJsonAsync( "/api/files/delete", req ); + + return response; + } + catch( Exception ex ) + { + _logger.LogError( $"Exception during image delete API call: {ex}" ); + } + + return false; + } + + public async Task MoveImages(ImageMoveRequest req) + { + try + { + var response = await _restClient.CustomPostAsJsonAsync( "/api/files/move", req ); + + return response; + } + catch( Exception ex ) + { + _logger.LogError( $"Exception during image move API call: {ex}" ); + } + + return false; + } +} \ No newline at end of file diff --git a/Damselfly.Core.ScopedServices/Client Services/ClientFolderService.cs b/Damselfly.Core.ScopedServices/Client Services/ClientFolderService.cs index f8db69e1..31601738 100644 --- a/Damselfly.Core.ScopedServices/Client Services/ClientFolderService.cs +++ b/Damselfly.Core.ScopedServices/Client Services/ClientFolderService.cs @@ -17,10 +17,10 @@ public ClientFolderService(RestClient client, NotificationsService notificationS httpClient = client; _logger = logger; _notificationService = notificationService; + OnChange = null; } - public event Action OnChange; - + public event Action? OnChange; public async Task> GetFolders() { diff --git a/Damselfly.Core.ScopedServices/Client Services/ClientImageCacheService.cs b/Damselfly.Core.ScopedServices/Client Services/ClientImageCacheService.cs index bb3778a3..324da03c 100644 --- a/Damselfly.Core.ScopedServices/Client Services/ClientImageCacheService.cs +++ b/Damselfly.Core.ScopedServices/Client Services/ClientImageCacheService.cs @@ -29,7 +29,7 @@ public ClientImageCacheService(RestClient client, IMemoryCache cache, Notificati httpClient = client; _cacheOptions = new MemoryCacheEntryOptions() .SetSize(1) - .SetSlidingExpiration(TimeSpan.FromHours(4)); + .SetAbsoluteExpiration(TimeSpan.FromHours(4)); _notifications.SubscribeToNotification(NotificationType.CacheEvict, Evict); } @@ -158,10 +158,12 @@ private async Task> GetImages(ICollection imgIds) { var req = new ImageRequest { ImageIds = imgIds.ToList() }; var response = await httpClient.CustomPostAsJsonAsync("/api/images/batch", req); - return response.Images; + + if( response != null ) + return response.Images; } - else - return new List(); + + return new List(); } private async Task LoadAndCacheImage(int imageId) diff --git a/Damselfly.Core.ScopedServices/Client Services/ClientPeopleService.cs b/Damselfly.Core.ScopedServices/Client Services/ClientPeopleService.cs index 0e29937e..2494fc99 100644 --- a/Damselfly.Core.ScopedServices/Client Services/ClientPeopleService.cs +++ b/Damselfly.Core.ScopedServices/Client Services/ClientPeopleService.cs @@ -38,6 +38,6 @@ public async Task UpdateName(ImageObject theObject, string newName) public async Task UpdatePerson(Person thePerson, string newName) { throw new NotImplementedException(); - await httpClient.CustomPutAsJsonAsync($"/api/people/name/{thePerson.PersonId}", newName); + //await httpClient.CustomPutAsJsonAsync($"/api/people/name/{thePerson.PersonId}", newName); } } \ No newline at end of file diff --git a/Damselfly.Core.ScopedServices/Client Services/ClientTagService.cs b/Damselfly.Core.ScopedServices/Client Services/ClientTagService.cs index 53423b48..fe0bd4fb 100644 --- a/Damselfly.Core.ScopedServices/Client Services/ClientTagService.cs +++ b/Damselfly.Core.ScopedServices/Client Services/ClientTagService.cs @@ -13,8 +13,8 @@ public class ClientTagService : ITagService, IRecentTagService, ITagSearchServic private readonly RestClient httpClient; private readonly ILogger _logger; - private ICollection _favouriteTags; - private ICollection _recentTags; + private ICollection? _favouriteTags; + private ICollection? _recentTags; public event Action OnFavouritesChanged; public event Action> OnUserTagsAdded; diff --git a/Damselfly.Core.ScopedServices/Client Services/ClientThemeService.cs b/Damselfly.Core.ScopedServices/Client Services/ClientThemeService.cs index 15e2bb2b..fee41be6 100644 --- a/Damselfly.Core.ScopedServices/Client Services/ClientThemeService.cs +++ b/Damselfly.Core.ScopedServices/Client Services/ClientThemeService.cs @@ -10,8 +10,6 @@ namespace Damselfly.Core.ScopedServices; public class ClientThemeService : IThemeService, IDisposable { - private readonly AuthenticationStateProvider _authProvider; - private readonly IUserConfigService _configService; private readonly ILogger _logger; private readonly RestClient httpClient; @@ -84,9 +82,10 @@ public async Task ApplyTheme(string themeName) await ApplyTheme(themeConfig); } - public async Task ApplyTheme(ThemeConfig newTheme) + public Task ApplyTheme(ThemeConfig newTheme) { OnChangeTheme?.Invoke(newTheme); + return Task.CompletedTask; } private void SettingsLoaded(ICollection newSettings) diff --git a/Damselfly.Core.ScopedServices/Client Services/ClientUserMgmtService.cs b/Damselfly.Core.ScopedServices/Client Services/ClientUserMgmtService.cs index d8d077cc..07d4a34a 100644 --- a/Damselfly.Core.ScopedServices/Client Services/ClientUserMgmtService.cs +++ b/Damselfly.Core.ScopedServices/Client Services/ClientUserMgmtService.cs @@ -49,7 +49,7 @@ public async Task SetUserPasswordAsync(AppIdentityUser user, strin } public async Task CreateNewUser(AppIdentityUser newUser, string password, - ICollection roles = null) + ICollection? roles = null) { // /api/users var req = new UserRequest { User = newUser, Password = password, Roles = roles }; diff --git a/Damselfly.Core.ScopedServices/Damselfly.Core.ScopedServices.csproj b/Damselfly.Core.ScopedServices/Damselfly.Core.ScopedServices.csproj index bf41239c..767a3cd5 100644 --- a/Damselfly.Core.ScopedServices/Damselfly.Core.ScopedServices.csproj +++ b/Damselfly.Core.ScopedServices/Damselfly.Core.ScopedServices.csproj @@ -22,10 +22,10 @@ - - - - - + + + + + diff --git a/Damselfly.Core.ScopedServices/NavigationService.cs b/Damselfly.Core.ScopedServices/NavigationService.cs index e3bea682..fd09207b 100644 --- a/Damselfly.Core.ScopedServices/NavigationService.cs +++ b/Damselfly.Core.ScopedServices/NavigationService.cs @@ -56,9 +56,10 @@ private void NotifyStateChanged(Image newImage) /// /// /// - public async Task GetNextImage(bool next) + public Task GetNextImage(bool next) { var navigationItems = new List(); + int result = -1; if ( Context == NavigationContexts.Basket ) navigationItems.AddRange(_basketService.BasketImages.Select(x => x.ImageId)); @@ -81,10 +82,10 @@ public async Task GetNextImage(bool next) else currentIndex = currentIndex % navigationItems.Count; - return navigationItems[currentIndex]; + result = navigationItems[currentIndex]; } } - return -1; + return Task.FromResult( result ); } } \ No newline at end of file diff --git a/Damselfly.Core.ScopedServices/NotificationsService.cs b/Damselfly.Core.ScopedServices/NotificationsService.cs index 6c565db4..cc9afbaf 100644 --- a/Damselfly.Core.ScopedServices/NotificationsService.cs +++ b/Damselfly.Core.ScopedServices/NotificationsService.cs @@ -61,14 +61,16 @@ async ValueTask IAsyncDisposable.DisposeAsync() public event Action OnConnectionChanged; - private async Task ConnectionOpened(string? arg) + private Task ConnectionOpened(string? arg) { OnConnectionChanged?.Invoke(); + return Task.CompletedTask; } - private async Task ConnectionClosed(Exception? arg) + private Task ConnectionClosed(Exception? arg) { OnConnectionChanged?.Invoke(); + return Task.CompletedTask; } /// @@ -98,7 +100,7 @@ public void SubscribeToNotificationAsync(NotificationType type, Func _logger.LogInformation($"Received {methodName} - calling async action {payloadLog}"); await action(theObj); } - catch ( Exception ex ) + catch ( Exception ) { _logger.LogError($"Error processing serialized object for {methodName}: {payload}."); } @@ -134,7 +136,7 @@ public void SubscribeToNotification(NotificationType type, Action action) _logger.LogInformation($"Received {methodName} - calling action {payloadLog}"); action.Invoke(theObj); } - catch ( Exception ex ) + catch ( Exception ) { _logger.LogError($"Error processing serialized object for {methodName}: {payload}."); } diff --git a/Damselfly.Core.ScopedServices/ServiceRegistrations.cs b/Damselfly.Core.ScopedServices/ServiceRegistrations.cs index cf02b8de..3a183e9e 100644 --- a/Damselfly.Core.ScopedServices/ServiceRegistrations.cs +++ b/Damselfly.Core.ScopedServices/ServiceRegistrations.cs @@ -31,6 +31,7 @@ public static IServiceCollection AddDamselflyUIServices(this IServiceCollection services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -48,6 +49,7 @@ public static IServiceCollection AddDamselflyUIServices(this IServiceCollection services.AddSingleton(x => x.GetRequiredService()); services.AddSingleton(x => x.GetRequiredService()); services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton( x => x.GetRequiredService() ); services.AddSingleton(x => x.GetRequiredService()); services.AddSingleton(x => x.GetRequiredService()); services.AddSingleton(x => x.GetRequiredService()); diff --git a/Damselfly.Core.ScopedServices/UserFolderService.cs b/Damselfly.Core.ScopedServices/UserFolderService.cs index 23adf2ae..f2efc380 100644 --- a/Damselfly.Core.ScopedServices/UserFolderService.cs +++ b/Damselfly.Core.ScopedServices/UserFolderService.cs @@ -16,7 +16,7 @@ public class UserFolderService : IDisposable, IUserFolderService private readonly IFolderService _folderService; private readonly NotificationsService _notifications; private readonly ISearchService _searchService; - private ICollection folderItems; + private ICollection? folderItems; private IDictionary folderStates; public UserFolderService(IFolderService folderService, ISearchService searchService, IConfigService configService, @@ -108,12 +108,13 @@ public async Task> GetFilteredFolders(string filterTerm) return items.Where(x => x.ParentFolders.All(x => IsExpanded(x))).ToList(); } - public async Task GetFolder(int folderId) + public Task GetFolder(int folderId) { + Folder? result = null; if ( folderStates.TryGetValue(folderId, out var folderState) ) - return folderState.Folder; + result = folderState.Folder; - return null; + return Task.FromResult( result ); } /// diff --git a/Damselfly.Core.ScopedServices/ViewDataService.cs b/Damselfly.Core.ScopedServices/ViewDataService.cs index f2ed281c..61aa12fc 100644 --- a/Damselfly.Core.ScopedServices/ViewDataService.cs +++ b/Damselfly.Core.ScopedServices/ViewDataService.cs @@ -42,7 +42,7 @@ public class SideBarState public bool ShowImageProps { get; set; } = false; public bool HideSideBar { get; set; } = false; - public override bool Equals(object obj) + public override bool Equals(object? obj) { var other = obj as SideBarState; if ( other != null ) diff --git a/Damselfly.Core.Utils/Damselfly.Core.Utils.csproj b/Damselfly.Core.Utils/Damselfly.Core.Utils.csproj index a890f1a6..8cd700f4 100644 --- a/Damselfly.Core.Utils/Damselfly.Core.Utils.csproj +++ b/Damselfly.Core.Utils/Damselfly.Core.Utils.csproj @@ -1,14 +1,14 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/Damselfly.Core.Utils/Utils/EventConflator.cs b/Damselfly.Core.Utils/Utils/EventConflator.cs index 50834c5e..d9906e14 100644 --- a/Damselfly.Core.Utils/Utils/EventConflator.cs +++ b/Damselfly.Core.Utils/Utils/EventConflator.cs @@ -33,12 +33,7 @@ public void HandleEvent(TimerCallback callback) { theCallback = callback; - if ( eventTimer != null ) - { - var oldTimer = eventTimer; - eventTimer = null; - oldTimer.Dispose(); - } + ClearTimer(); eventTimer = new Timer(TimerCallback, null, intervalMS, Timeout.Infinite); } @@ -50,11 +45,16 @@ public void HandleEvent(TimerCallback callback) /// /// private void TimerCallback(object state) + { + ClearTimer(); + theCallback(state); + } + + private void ClearTimer() { var oldTimer = eventTimer; eventTimer = null; - if ( oldTimer != null ) + if( oldTimer != null ) oldTimer.Dispose(); - theCallback(state); } } \ No newline at end of file diff --git a/Damselfly.Core/Damselfly.Core.csproj b/Damselfly.Core/Damselfly.Core.csproj index 55c06657..559b5ea7 100644 --- a/Damselfly.Core/Damselfly.Core.csproj +++ b/Damselfly.Core/Damselfly.Core.csproj @@ -5,33 +5,33 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + - - + + - - + + - - + + - - + + - + diff --git a/Damselfly.Core/Database/BaseModel.cs b/Damselfly.Core/Database/BaseModel.cs index 940544ec..aea31b12 100644 --- a/Damselfly.Core/Database/BaseModel.cs +++ b/Damselfly.Core/Database/BaseModel.cs @@ -407,6 +407,28 @@ public void FlushDBWriteCache() ExecutePragma(this, "PRAGMA schema.wal_checkpoint;"); } + /// + /// Use SQLite recursion to find all the child folders under a particular parent. + /// + /// + /// + /// + /// + public Task> GetChildFolderIds( DbSet resultSet, int rootId ) where T : class + { + string sql = @"with recursive children(folderId, parentId) as ( + select p.FolderID, p.ParentID + from Folders p where FolderID = {0} + union all + select f.folderId, f.ParentID + from folders f + join children c on c.FolderID = f.ParentId ) + select r.* from folders r + join children x on r.FolderID = x.FolderID;"; + + return Task.FromResult( resultSet.FromSqlRaw( sql, rootId ) ); + } + // Can this be made async? public Task> ImageSearch(DbSet resultSet, string query, bool includeAITags) where T : class { diff --git a/Damselfly.Core/ScopedServices/UserTagFavouritesService.cs b/Damselfly.Core/ScopedServices/UserTagFavouritesService.cs index ed749c67..0c4db85f 100644 --- a/Damselfly.Core/ScopedServices/UserTagFavouritesService.cs +++ b/Damselfly.Core/ScopedServices/UserTagFavouritesService.cs @@ -23,7 +23,12 @@ public UserTagRecentsService(ExifService exifService, IConfigService configServi var recents = configService.Get(ConfigSettings.RecentTags); - if ( !string.IsNullOrEmpty(recents) ) recentTags.AddRange(recents.Split(",").Select(x => x.Trim()).ToList()); + if (!string.IsNullOrEmpty(recents)) + { + recentTags.AddRange(recents.Split(",") + .Select(x => x.Trim()) + .ToList()); + } } public void Dispose() diff --git a/Damselfly.Core/Services/ExifService.cs b/Damselfly.Core/Services/ExifService.cs index 45990414..38e8df08 100644 --- a/Damselfly.Core/Services/ExifService.cs +++ b/Damselfly.Core/Services/ExifService.cs @@ -131,7 +131,8 @@ public async Task UpdateTagsAsync(ICollection imageIds, ICollection UserId = userId })); - changeDesc += $"added: {string.Join(',', tagsToAdd)}"; + var tagsAdded = string.Join( ',', tagsToAdd ); + changeDesc += $"added: {tagsAdded}"; } if ( removeTags != null ) @@ -550,8 +551,8 @@ public async Task CleanUpKeywordOperations(TimeSpan cleanupFreq) using var scope = _scopeFactory.CreateScope(); using var db = scope.ServiceProvider.GetService(); - // Clean up completed operations older than 24hrs - var cutOff = DateTime.UtcNow.AddDays(-1); + // Clean up completed operations older than 6 months + var cutOff = DateTime.UtcNow.AddMonths(-6); try { @@ -708,6 +709,7 @@ private async Task LoadFavouriteTagsAsync() // TODO: Clear the tag cache and reload, and get this from the cache var faves = await Task.FromResult(db.Tags .Where(x => x.Favourite) + .Distinct() .OrderBy(x => x.Keyword) .ToList()); diff --git a/Damselfly.Core/Services/FileService.cs b/Damselfly.Core/Services/FileService.cs new file mode 100644 index 00000000..2d049de5 --- /dev/null +++ b/Damselfly.Core/Services/FileService.cs @@ -0,0 +1,154 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Damselfly.Core.Constants; +using Damselfly.Core.DbModels.Models.APIModels; +using Damselfly.Core.Models; +using Damselfly.Core.ScopedServices.Interfaces; +using Microsoft.Extensions.Logging; + +namespace Damselfly.Core.Services; + +public class FileService : IFileService +{ + private readonly ILogger _logger; + private readonly IStatusService _statusService; + private readonly IImageCacheService _imageCache; + private readonly IConfigService _configService; + private readonly ICachedDataService _cachedDataService; + + public FileService( IStatusService statusService, IImageCacheService imageCacheService, + ICachedDataService cachedDataService, IConfigService configService, + ILogger logger ) + { + _statusService = statusService; + _imageCache = imageCacheService; + _cachedDataService = cachedDataService; + _configService = configService; + _logger = logger; + } + + /// + /// Delete images + /// TODO: Based on a config setting we should either have + /// 1. Move to Damselfly Trashcan + /// 2. Move to OS trashcan + /// 3. Actually delete the actual file + /// + /// + /// + public async Task DeleteImages( MultiImageRequest req ) + { + var trashFolder = _configService.Get( ConfigSettings.TrashcanFolderName, "DamselflyTrashcan" ); + + // TODO - allow users to configure the delete folder name + var destfolder = Path.Combine( _cachedDataService.ImagesRootFolder, trashFolder ); + var success = true; + var images = await _imageCache.GetCachedImages( req.ImageIDs ); + + if( !Directory.Exists( destfolder ) ) + { + try + { + // Store the setting + _configService.Set( ConfigSettings.TrashcanFolderName, trashFolder ); + + var dir = Directory.CreateDirectory( destfolder ); + // Hide this here? + // dir.Attributes = FileAttributes.Directory | FileAttributes.Hidden; + _logger.LogInformation( $"Created trashcan folder: {destfolder}" ); + } + catch( Exception ex ) + { + _logger.LogError( $"Unable to create folder {destfolder}: {ex}" ); + return false; + } + } + + foreach( var image in images ) + { + var dest = Path.Combine( destfolder, image.FileName ); + + if( File.Exists( dest ) ) + { + // If there's a collision, create a unique filename + dest = GetUniqueFilename( dest ); + } + + if( !SafeCopyOrMove( image, dest, true ) ) + success = false; + } + + return success; + } + + + /// + /// Create a unique filename for the given filename + /// + /// A full filename, e.g., C:\temp\myfile.tmp + /// A filename like C:\temp\myfile_633822247336197902.tmp + public string GetUniqueFilename( string filename ) + { + string basename = Path.Combine( Path.GetDirectoryName( filename ), + Path.GetFileNameWithoutExtension( filename ) ); + string uniquefilename = string.Format( "{0}_{1}{2}", + basename, + DateTime.Now.Ticks, + Path.GetExtension( filename ) ); + return uniquefilename; + } + + /// + /// Move or copy images to a different location + /// + /// + /// + public async Task MoveImages(ImageMoveRequest req) + { + var success = true; + var images = await _imageCache.GetCachedImages( req.ImageIDs ); + + foreach( var image in images ) + { + var dest = Path.Combine(req.Destination.Path, image.FileName); + + if( !SafeCopyOrMove( image, dest, req.Move ) ) + success = false; + } + + return success; + } + + private bool SafeCopyOrMove(Image image, string destFilename, bool move) + { + var source = image.FullPath; + + if( File.Exists(source) && !File.Exists( destFilename ) ) + { + try + { + // Note, we *never* overwrite. + if( move ) + { + File.Move(source, destFilename, false); + _statusService.UpdateStatus($"Moved {image.FileName} to {destFilename}"); + } + else + { + File.Copy(source, destFilename, false); + _statusService.UpdateStatus($"Copied {image.FileName} to {destFilename}"); + } + + return true; + } + catch( Exception ex ) + { + _logger.LogError($"Unable to move file to {destFilename}: {ex}"); + } + } + + return false; + } +} + diff --git a/Damselfly.Core/Services/FolderWatcherService.cs b/Damselfly.Core/Services/FolderWatcherService.cs index 5b2144d3..c854404e 100644 --- a/Damselfly.Core/Services/FolderWatcherService.cs +++ b/Damselfly.Core/Services/FolderWatcherService.cs @@ -155,12 +155,20 @@ private void EnqueueFolderChangeForRescan(FileInfo file, WatcherChangeTypes chan var folder = file.Directory.FullName; - // If it's hidden, or already in the queue, ignore it. - if ( file.IsHidden() || folderQueue.Contains(folder) ) + if( changeType != WatcherChangeTypes.Deleted ) + { + // This check isn't relevant for deleted files, which don't have valid attributes + // If it's hidden, ignore it + if( file.IsHidden() ) + return; + } + + // If it's already in the queue, ignore it. + if( folderQueue.Contains( folder ) ) return; // Ignore non images, and hidden files/folders. - if ( file.IsDirectory() || _imageProcessService.IsImageFileType(file) || file.IsSidecarFileType() ) + if( file.IsDirectory() || _imageProcessService.IsImageFileType(file) || file.IsSidecarFileType() ) { Logging.Log($"FileWatcher: adding to queue: {folder} {changeType}"); folderQueue.Enqueue(folder); diff --git a/Damselfly.Core/Services/ImageCache.cs b/Damselfly.Core/Services/ImageCache.cs index af2ab9e7..f9c97674 100644 --- a/Damselfly.Core/Services/ImageCache.cs +++ b/Damselfly.Core/Services/ImageCache.cs @@ -48,7 +48,7 @@ public ImageCache(IMemoryCache memoryCache, IServiceScopeFactory scopeFactory, S _memoryCache = memoryCache; _cacheOptions = new MemoryCacheEntryOptions() .SetSize(1) - .SetSlidingExpiration(TimeSpan.FromDays(2)); + .SetAbsoluteExpiration(TimeSpan.FromDays(2)); } /// diff --git a/Damselfly.Core/Services/ImageRecognitionService.cs b/Damselfly.Core/Services/ImageRecognitionService.cs index 4bc022a1..3e8d5f28 100644 --- a/Damselfly.Core/Services/ImageRecognitionService.cs +++ b/Damselfly.Core/Services/ImageRecognitionService.cs @@ -105,9 +105,6 @@ public async Task UpdateName(ImageObject faceObject, string name) if (!faceObject.IsFace) throw new ArgumentException("Image object passed to name update."); - if (faceObject.Person == null) - throw new Exception("Person object did not have person included"); - using var scope = _scopeFactory.CreateScope(); using var db = scope.ServiceProvider.GetService(); @@ -228,7 +225,7 @@ private int GetPersonIDFromCache(Guid? azurePersonId) if ( azurePersonId.HasValue ) { // TODO Await - LoadPersonCache(); + LoadPersonCache().Wait(); if ( _peopleCache.TryGetValue(azurePersonId.ToString(), out var person) ) return person.PersonId; @@ -727,4 +724,4 @@ public override string ToString() return Description; } } -} \ No newline at end of file +} diff --git a/Damselfly.Core/Services/IndexingService.cs b/Damselfly.Core/Services/IndexingService.cs index cdbcb35f..5639c7e5 100644 --- a/Damselfly.Core/Services/IndexingService.cs +++ b/Damselfly.Core/Services/IndexingService.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using Damselfly.Core.Constants; using Damselfly.Core.Interfaces; using Damselfly.Core.Models; using Damselfly.Core.ScopedServices.Interfaces; @@ -29,10 +30,9 @@ public class IndexingService : IProcessJobFactory, IRescanProvider private readonly WorkService _workService; private bool _fullIndexComplete; - public IndexingService(IServiceScopeFactory scopeFactory, IStatusService statusService, - ImageProcessService imageService, - ConfigService config, ImageCache imageCache, FolderWatcherService watcherService, - WorkService workService) + public IndexingService( IServiceScopeFactory scopeFactory, IStatusService statusService, + ImageProcessService imageService, ConfigService config, ImageCache imageCache, + FolderWatcherService watcherService, WorkService workService) { _scopeFactory = scopeFactory; _statusService = statusService; @@ -143,6 +143,19 @@ public async Task MarkImagesForScan(ICollection images) public event Action OnFoldersChanged; + private bool IsIndexedFolder( DirectoryInfo dir ) + { + var trashFolder = _configService.Get( ConfigSettings.TrashcanFolderName ); + + if( !string.IsNullOrEmpty( trashFolder ) ) + { + if( dir.Name == trashFolder ) + return false; + } + + return dir.IsMonitoredFolder(); + } + private void NotifyFolderChanged() { Logging.LogVerbose("Folders changed."); @@ -167,7 +180,7 @@ public async Task IndexFolder(DirectoryInfo folder, Folder parent) // Get all the sub-folders on the disk, but filter out // ones we're not interested in. var subFolders = folder.SafeGetSubDirectories() - .Where(x => x.IsMonitoredFolder()) + .Where(IsIndexedFolder ) .ToList(); try @@ -222,7 +235,8 @@ public async Task IndexFolder(DirectoryInfo folder, Folder parent) } // Scan subdirs recursively. - foreach ( var sub in subFolders ) await IndexFolder(sub, folderToScan); + foreach ( var sub in subFolders ) + await IndexFolder(sub, folderToScan); } /// @@ -247,7 +261,7 @@ private async Task RemoveMissingChildDirs(ImageContext db, Folder folderTo // ...and then look for any DB folders that aren't included in the list of sub-folders. // That means they've been removed from the disk, and should be removed from the DB. - var missingDirs = dbChildDirs.Where(f => !new DirectoryInfo(f.Path).IsMonitoredFolder()).ToList(); + var missingDirs = dbChildDirs.Where(f => !IsIndexedFolder( new DirectoryInfo(f.Path))).ToList(); if ( missingDirs.Any() ) { diff --git a/Damselfly.Core/Services/MetaDataService.cs b/Damselfly.Core/Services/MetaDataService.cs index b40994ac..038f6b7e 100644 --- a/Damselfly.Core/Services/MetaDataService.cs +++ b/Damselfly.Core/Services/MetaDataService.cs @@ -161,6 +161,8 @@ public Task> SearchTags(string text) var tags = CachedTags .Where(x => x.Keyword.Contains(searchText, StringComparison.OrdinalIgnoreCase) && !x.Keyword.Equals(searchText, StringComparison.OrdinalIgnoreCase)) + // The closer to the start of the text, the earlier in the list + .OrderBy( x => x.Keyword.IndexOf( text, StringComparison.OrdinalIgnoreCase ) ) .OrderBy(x => x.Favourite ? 0 : 1) // Favourites first .ThenBy(x => x.Keyword) // Then order alphabetically .Take(30); // Don't go mad with the number we return @@ -519,13 +521,12 @@ public async Task ScanMetaData(int imageId) { var lastWriteTime = File.GetLastWriteTimeUtc(img.FullPath); - if ( lastWriteTime > DateTime.UtcNow.AddSeconds(-10) ) + if (lastWriteTime < DateTime.UtcNow.AddMinutes( 1 ) && lastWriteTime > DateTime.UtcNow.AddSeconds(-10)) { - // If the last-write time is within 30s of now, - // skip it, as it's possible it might still be - // mid-copy. + // If the last-write time is within 30s of now, but it's not a time far in the future + // we skip it, as it's possible it might still be mid-copy. // TODO: We need a better way of managing this - Logging.Log($"Skipping metadata scan for {img.FileName} - write time is too recent."); + Logging.LogWarning($"Skipping metadata scan for {img.FileName} - write time is too recent."); return; } @@ -582,7 +583,7 @@ public async Task ScanMetaData(int imageId) var changesSaved = await db.SaveChangesAsync("ImageMetaDataSave"); if ( changesSaved == 0 ) - Logging.LogError($"No changed saved after metadata scan for image {img.ImageId}"); + Logging.LogError($"No changes saved after metadata scan for image {img.ImageId}"); } // Now save the tags diff --git a/Damselfly.Core/Services/SearchQueryService.cs b/Damselfly.Core/Services/SearchQueryService.cs index eacce3bc..e0d4c20b 100644 --- a/Damselfly.Core/Services/SearchQueryService.cs +++ b/Damselfly.Core/Services/SearchQueryService.cs @@ -144,7 +144,16 @@ private async Task LoadMoreData(SearchQuery query, int first, in if ( query.Folder?.FolderId >= 0 ) { - var descendants = query.Folder.Subfolders.ToList(); + IEnumerable descendants; + + if( query.IncludeChildFolders ) + { + descendants = await db.GetChildFolderIds( db.Folders, query.Folder.FolderId ); + } + else + { + descendants = query.Folder.Subfolders.ToList(); + } // Filter by folderID images = images.Where(x => descendants.Select(x => x.FolderId).Contains(x.FolderId)); diff --git a/Damselfly.Core/Utils/ServiceRegistrations.cs b/Damselfly.Core/Utils/ServiceRegistrations.cs index 122ab126..3b82ab45 100644 --- a/Damselfly.Core/Utils/ServiceRegistrations.cs +++ b/Damselfly.Core/Utils/ServiceRegistrations.cs @@ -75,9 +75,11 @@ public static IServiceCollection AddHostedBlazorBackEndServices(this IServiceCol services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton( x => x.GetRequiredService() ); services.AddSingleton(); services.AddSingleton(x => x.GetRequiredService()); diff --git a/Damselfly.Desktop/package.json b/Damselfly.Desktop/package.json index 23fffaa5..0a2129ca 100644 --- a/Damselfly.Desktop/package.json +++ b/Damselfly.Desktop/package.json @@ -1,6 +1,6 @@ { "name": "Damselfly", - "version": "4.0.1", + "version": "4.0.4", "description": "Damselfy Desktop App", "main": "main.js", "scripts": { diff --git a/Damselfly.ML.AzureFace/AzureFaceService.cs b/Damselfly.ML.AzureFace/AzureFaceService.cs index 4a86ccbe..fd98bfc3 100644 --- a/Damselfly.ML.AzureFace/AzureFaceService.cs +++ b/Damselfly.ML.AzureFace/AzureFaceService.cs @@ -27,7 +27,6 @@ public class AzureFaceService // Assumine the newer/bigger numbers are better. private const string RECOGNITION_MODEL = RecognitionModel.Recognition04; private const string DETECTION_MODEL = DetectionModel.Detection03; - private readonly IList _attributes; private readonly IConfigService _configService; private readonly ITransactionThrottle _transThrottle; private FaceClient _faceClient; @@ -38,21 +37,6 @@ public AzureFaceService(IConfigService configService, ITransactionThrottle trans { _configService = configService; _transThrottle = transThrottle; - - _attributes = new[] - { - FaceAttributeType.Gender, - FaceAttributeType.Age, - FaceAttributeType.Smile, - FaceAttributeType.Emotion, - FaceAttributeType.Glasses, - FaceAttributeType.Hair, - FaceAttributeType.FacialHair, - FaceAttributeType.HeadPose - // These are currently not supported. - // FaceAttributeType.Mask - // FaceAttributeType.Makeup, - }; } public AzureDetection DetectionType { get; private set; } @@ -121,7 +105,8 @@ public async Task StartService() { InitFromConfig(); - if ( _faceClient != null && DetectionType != AzureDetection.Disabled ) await InitializeAzureService(); + if ( _faceClient != null && DetectionType != AzureDetection.Disabled ) + await InitializeAzureService(); } catch ( Exception ex ) { @@ -199,7 +184,7 @@ private async Task> AzureDetect(string imagePath) Logging.LogVerbose("Calling Azure Face service..."); var detectedFaces = await _transThrottle.Call("Detect", - _faceClient.Face.DetectWithStreamAsync(fileStream, true, true, _attributes, RECOGNITION_MODEL)); + _faceClient.Face.DetectWithStreamAsync(fileStream, returnFaceId: true, recognitionModel: RECOGNITION_MODEL)); Logging.LogVerbose("Azure Face service call complete."); diff --git a/Damselfly.ML.AzureFace/Damselfly.ML.AzureFace.csproj b/Damselfly.ML.AzureFace/Damselfly.ML.AzureFace.csproj index b1733b19..0e3b4ef6 100644 --- a/Damselfly.ML.AzureFace/Damselfly.ML.AzureFace.csproj +++ b/Damselfly.ML.AzureFace/Damselfly.ML.AzureFace.csproj @@ -1,17 +1,17 @@ - - - + + + - - - + + + - - + + diff --git a/Damselfly.ML.ImageClassification/Damselfly.ML.ImageClassification.csproj b/Damselfly.ML.ImageClassification/Damselfly.ML.ImageClassification.csproj index b1da0bc3..1b6d92fa 100644 --- a/Damselfly.ML.ImageClassification/Damselfly.ML.ImageClassification.csproj +++ b/Damselfly.ML.ImageClassification/Damselfly.ML.ImageClassification.csproj @@ -6,22 +6,22 @@ - + - + - - - - - + + + + + - + diff --git a/Damselfly.ML.ObjectDetection.ML/Damselfly.ML.ObjectDetection.csproj b/Damselfly.ML.ObjectDetection.ML/Damselfly.ML.ObjectDetection.csproj index 3a1a8612..30cee4f7 100644 --- a/Damselfly.ML.ObjectDetection.ML/Damselfly.ML.ObjectDetection.csproj +++ b/Damselfly.ML.ObjectDetection.ML/Damselfly.ML.ObjectDetection.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/Damselfly.ML.ObjectDetection.ML/ObjectDetector.cs b/Damselfly.ML.ObjectDetection.ML/ObjectDetector.cs index fca9f704..aee0aa20 100644 --- a/Damselfly.ML.ObjectDetection.ML/ObjectDetector.cs +++ b/Damselfly.ML.ObjectDetection.ML/ObjectDetector.cs @@ -37,31 +37,37 @@ public void InitScorer() /// /// /// - public async Task> DetectObjects(Image image) + public Task> DetectObjects(Image image) { + IList result = null; try { - if ( scorer == null ) - return new List(); + if( scorer != null ) + { + + var watch = new Stopwatch( "DetectObjects" ); - var watch = new Stopwatch("DetectObjects"); + var predictions = scorer.Predict( image ); - var predictions = scorer.Predict(image); + watch.Stop(); - watch.Stop(); + var objectsFound = predictions.Where( x => x.Score > predictionThreshold ) + .Select( x => MakeResult( x ) ) + .ToList(); - var objectsFound = predictions.Where(x => x.Score > predictionThreshold) - .Select(x => MakeResult(x)) - .ToList(); + result = objectsFound; - return objectsFound; + } } catch ( Exception ex ) { Logging.LogError($"Error during object detection: {ex.Message}"); } - return new List(); + if( result == null ) + result = new List(); + + return Task.FromResult( result ); } private ImageDetectResult MakeResult(YoloPrediction prediction) diff --git a/Damselfly.Migrations.Sqlite/Damselfly.Migrations.Sqlite.csproj b/Damselfly.Migrations.Sqlite/Damselfly.Migrations.Sqlite.csproj index e1cf3617..5df8e54f 100644 --- a/Damselfly.Migrations.Sqlite/Damselfly.Migrations.Sqlite.csproj +++ b/Damselfly.Migrations.Sqlite/Damselfly.Migrations.Sqlite.csproj @@ -8,11 +8,11 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/Damselfly.Shared.Utils/Damselfly.Shared.Utils.csproj b/Damselfly.Shared.Utils/Damselfly.Shared.Utils.csproj index 6e4a8790..955bc06b 100644 --- a/Damselfly.Shared.Utils/Damselfly.Shared.Utils.csproj +++ b/Damselfly.Shared.Utils/Damselfly.Shared.Utils.csproj @@ -13,7 +13,6 @@ - diff --git a/Damselfly.Web.Client/ClientVersion.cs b/Damselfly.Web.Client/ClientVersion.cs new file mode 100644 index 00000000..1bc4efec --- /dev/null +++ b/Damselfly.Web.Client/ClientVersion.cs @@ -0,0 +1,2 @@ +namespace Damselfly.Web.Client; public static class Client { public static string ClientVersion = "{{CACHE_VERSION}}"; } + diff --git a/Damselfly.Web.Client/Damselfly.Web.Client.csproj b/Damselfly.Web.Client/Damselfly.Web.Client.csproj index f60f5627..e2212916 100644 --- a/Damselfly.Web.Client/Damselfly.Web.Client.csproj +++ b/Damselfly.Web.Client/Damselfly.Web.Client.csproj @@ -15,24 +15,24 @@ - - - + + + - - - - - + + + + + - - - - + + + + - + diff --git a/Damselfly.Web.Client/Extensions/ImageGridBase.cs b/Damselfly.Web.Client/Extensions/ImageGridBase.cs index ea40f7df..0ebb4935 100644 --- a/Damselfly.Web.Client/Extensions/ImageGridBase.cs +++ b/Damselfly.Web.Client/Extensions/ImageGridBase.cs @@ -16,7 +16,7 @@ public class ImageGridBase : ComponentBase // Grid images is a list of lists of images. protected readonly List gridImages = new(); - private SelectionInfo prevSelection; + private SelectionInfo? prevSelection = null; [Inject] protected SelectionService selectionService { get; init; } diff --git a/Damselfly.Web.Client/Extensions/ScrollView.cs b/Damselfly.Web.Client/Extensions/ScrollView.cs index 53a96d1b..c9184a97 100644 --- a/Damselfly.Web.Client/Extensions/ScrollView.cs +++ b/Damselfly.Web.Client/Extensions/ScrollView.cs @@ -14,7 +14,7 @@ public override string ToString() return $"ClientHeight: {ClientHeight}, Top: {ScrollTop}"; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { var other = obj as ScrollView; @@ -36,7 +36,7 @@ public class ScrollViewResult public int SkipItems { get; set; } public int TakeItems { get; set; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { var other = obj as ScrollViewResult; diff --git a/Damselfly.Shared.Utils/SyncfusionLicence.cs b/Damselfly.Web.Client/Extensions/SyncfusionLicence.cs similarity index 55% rename from Damselfly.Shared.Utils/SyncfusionLicence.cs rename to Damselfly.Web.Client/Extensions/SyncfusionLicence.cs index 6694a55d..131eb37f 100644 --- a/Damselfly.Shared.Utils/SyncfusionLicence.cs +++ b/Damselfly.Web.Client/Extensions/SyncfusionLicence.cs @@ -1,12 +1,12 @@ using Syncfusion.Licensing; -namespace Damselfly.Shared.Utils; +namespace Damselfly.Web.Client.Extensions; public static class SyncfusionLicence { public static void RegisterSyncfusionLicence() { SyncfusionLicenseProvider.RegisterLicense( - "NzU4MTU3QDMyMzAyZTMzMmUzMEEyTEJYL0I4R2RrOHdhUzJPdmh6bDJ5ZHo5bnNzNTRkYmZVRHBiMG5vdXc9"); + "MjUxMDA2OEAzMjMyMmUzMDJlMzBJd1EzZDZXdElNbGJURU9OT2FxYURPenhjZDhWQWJNKzY0YzJHVmdZTjhFPQ==" ); } } \ No newline at end of file diff --git a/Damselfly.Web.Client/Pages/HomePage.razor b/Damselfly.Web.Client/Pages/HomePage.razor index 65b0f717..bc9bcb0f 100644 --- a/Damselfly.Web.Client/Pages/HomePage.razor +++ b/Damselfly.Web.Client/Pages/HomePage.razor @@ -121,7 +121,7 @@ await ApplyQueryParams(); } - void HandleLocationChanged(object sender, LocationChangedEventArgs e) + void HandleLocationChanged(object? sender, LocationChangedEventArgs e) { StateHasChanged(); diff --git a/Damselfly.Web.Client/Pages/ImagePage.razor b/Damselfly.Web.Client/Pages/ImagePage.razor index b3cff053..8d617e8a 100644 --- a/Damselfly.Web.Client/Pages/ImagePage.razor +++ b/Damselfly.Web.Client/Pages/ImagePage.razor @@ -71,8 +71,8 @@ public string ImageID { get; set; } private Image CurrentImage; - private int nextImage; - private int prevImage; + private int? nextImage; + private int? prevImage; private ElementReference ImageView; private bool ShowObjects => theImagePreview == null ? false : theImagePreview.ShowObjects; @@ -91,9 +91,9 @@ private string PrevImageIDUrl => $"/image/{prevImage}"; private string NextImageIDUrl => $"/image/{nextImage}"; - private async Task ZoomIn() => await theImagePreview?.ZoomIn(); - private async Task ZoomOut() => await theImagePreview?.ZoomOut(); - private async Task ResetZoom() => await theImagePreview?.ResetZoom(); + private async Task ZoomIn() => await theImagePreview!.ZoomIn(); + private async Task ZoomOut() => await theImagePreview!.ZoomOut(); + private async Task ResetZoom() => await theImagePreview!.ResetZoom(); private double ZoomRange { get { return theImagePreview == null ? 0 : theImagePreview.ZoomRangeValue; } set { if (theImagePreview != null) theImagePreview.ZoomRangeValue = value; } } private bool CanZoomIn => ZoomRange < 5.0 && !ZoomDisabled; private bool CanZoomOut => ZoomRange > 1.0 && !ZoomDisabled; diff --git a/Damselfly.Web.Client/Pages/PeoplePage.razor b/Damselfly.Web.Client/Pages/PeoplePage.razor index d35676bc..a1f986d1 100644 --- a/Damselfly.Web.Client/Pages/PeoplePage.razor +++ b/Damselfly.Web.Client/Pages/PeoplePage.razor @@ -128,7 +128,7 @@ filteredPeople = names.Where(x => FilterFunc(x)) .ToList(); - await InvokeAsync(StateHasChanged); + StateHasChanged(); } protected override async Task OnAfterRenderAsync(bool firstRender) diff --git a/Damselfly.Web.Client/Pages/TagPage.razor b/Damselfly.Web.Client/Pages/TagPage.razor index 45eaaef9..7da97136 100644 --- a/Damselfly.Web.Client/Pages/TagPage.razor +++ b/Damselfly.Web.Client/Pages/TagPage.razor @@ -72,13 +72,13 @@ statusService.UpdateStatus("Loading tags..."); var results = await tagSearchService.GetAllTags(); Tags = results.ToList(); - await InvokeAsync(StateHasChanged); + StateHasChanged(); } private void DoSearch(string searchTerm) { searchText = searchTerm; - InvokeAsync(StateHasChanged); + StateHasChanged(); } diff --git a/Damselfly.Web.Client/Program.cs b/Damselfly.Web.Client/Program.cs index bd9fdd5a..aafa5121 100644 --- a/Damselfly.Web.Client/Program.cs +++ b/Damselfly.Web.Client/Program.cs @@ -5,6 +5,7 @@ using Damselfly.Core.ScopedServices.ClientServices; using Damselfly.Core.ScopedServices.Interfaces; using Damselfly.Shared.Utils; +using Damselfly.Web.Client.Extensions; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; @@ -50,15 +51,15 @@ public static async Task Main(string[] args) builder.Services.AddAuthorizationCore(config => config.SetupPolicies(builder.Services)); builder.Services.AddMudServices(); - builder.Services.AddSyncfusionBlazor(options => { options.IgnoreScriptIsolation = true; }); + builder.Services.AddSyncfusionBlazor(); builder.Services.AddBlazoredLocalStorage(); builder.Services.AddBlazorPanzoomServices(); builder.Services.AddScoped(); builder.Services.AddSingleton(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddDamselflyUIServices(); diff --git a/Damselfly.Web.Client/Shared/AIObject.razor b/Damselfly.Web.Client/Shared/AIObject.razor index d0fc9d54..5b184fc1 100644 --- a/Damselfly.Web.Client/Shared/AIObject.razor +++ b/Damselfly.Web.Client/Shared/AIObject.razor @@ -74,11 +74,11 @@ return; var menuList = new List -{ + { new() { Text = $"Find more photos containing this person", Value = 0 }, }; - contextMenuService.Open(args, menuList, async args => + contextMenuService.Open(args, menuList, args => { contextMenuService.Close(); switch (args.Value) @@ -107,7 +107,7 @@ var dialog = DialogService.Show("Add Name", parameters); var result = await dialog.Result; - if( !dialog.Result.IsCanceled) + if (!dialog.Result.IsCanceled) { // Reset the tag Editing = false; diff --git a/Damselfly.Web.Client/Shared/About.razor b/Damselfly.Web.Client/Shared/About.razor index 002c7d06..dcdd1aa3 100644 --- a/Damselfly.Web.Client/Shared/About.razor +++ b/Damselfly.Web.Client/Shared/About.razor @@ -1,13 +1,14 @@ @using System.Reflection @inject ICachedDataService dataService +@inject IJSRuntime jsRuntime
- +
-

Damselfly v@Version © 2019-@DateTime.Now.Year Mark Otway, All rights reserved.

+

Damselfly v@Version © 2019-@DateTime.Now.Year Mark Otway, All rights reserved.

Server-based Digital Asset Management system

-
+

Credits/Thanks

Powered by Blazor. @@ -15,11 +16,11 @@ Icons by Font-Awesome. Image Processing by SkiaSharp and SixLabors ImageSharp - and ImageMagick . + and ImageMagick . Face Detection by EmguCV. Facial recognition by Azure Cognitive Services Face API. Object Detection based on MentalStack's code. - ExifTool @dataService.ExifToolVer by Phil Harvey. + ExifTool @dataService.ExifToolVer by Phil Harvey. Dominant Colour calculation by Jelle Vergeer. Pan & Zoom by Ronnie Tran Maps by OpenStreetMap and Syncfusion. diff --git a/Damselfly.Web.Client/Shared/AdvancedSearchPanel.razor b/Damselfly.Web.Client/Shared/AdvancedSearchPanel.razor index e3bbf71b..8cd00040 100644 --- a/Damselfly.Web.Client/Shared/AdvancedSearchPanel.razor +++ b/Damselfly.Web.Client/Shared/AdvancedSearchPanel.razor @@ -23,7 +23,9 @@

- + @foreach( var choice in monthChoices ) { @@ -33,7 +35,7 @@
- + @foreach( var choice in Enumerable.Range(1, 5).Reverse() ) { @@ -43,7 +45,7 @@
- + @foreach( var choice in faceTypes ) { @@ -53,7 +55,7 @@
- + @foreach( var choice in orientationTypes ) { @@ -63,7 +65,7 @@
- + @foreach( var cam in cachedData.Cameras ) { @@ -73,7 +75,7 @@
- + @foreach( var lens in cachedData.Lenses ) { @@ -83,7 +85,7 @@
- + @foreach( var choice in fileSizeChoices ) { @@ -93,7 +95,7 @@
- + @foreach( var choice in fileSizeChoices ) { @@ -118,6 +120,11 @@ List> DateRanges = new(); DateRange filterRange = new(); + private Task FilterChanged() + { + return Task.CompletedTask; + } + DateRange DateRange { get => filterRange; @@ -154,7 +161,7 @@ CreateShortcuts(); filterRange = new DateRange(searchService.MinDate, searchService.MaxDate); - // TODO: Can we do better than do this every time the advanced Search panel opens? + // TODO: Can we do better than do this every time the advanced Search panel opens? cachedData.InitialiseData(); base.OnInitialized(); @@ -197,7 +204,8 @@ private void OnSearchChanged() { filterRange = new DateRange(searchService.MinDate, searchService.MaxDate); - InvokeAsync(StateHasChanged); + + StateHasChanged(); } private void AddRangeShortcut(string name, DateRange range) @@ -207,7 +215,7 @@ private void CreateShortcuts() { - // TODO: Add more here + // TODO: Add more here var Jan1ThisYear = new DateTime(DateTime.UtcNow.Year, 1, 1); AddRangeShortcut("This year", new DateRange { Start = Jan1ThisYear, End = DateTime.UtcNow }); diff --git a/Damselfly.Web.Client/Shared/BasketManager.razor b/Damselfly.Web.Client/Shared/BasketManager.razor index 4c50fce1..a366bae0 100644 --- a/Damselfly.Web.Client/Shared/BasketManager.razor +++ b/Damselfly.Web.Client/Shared/BasketManager.razor @@ -115,7 +115,7 @@ watch.Stop(); - await InvokeAsync(StateHasChanged); + StateHasChanged(); } } diff --git a/Damselfly.Web.Client/Shared/ConflatedTextBox.razor b/Damselfly.Web.Client/Shared/ConflatedTextBox.razor index 7779442e..17dca2e2 100644 --- a/Damselfly.Web.Client/Shared/ConflatedTextBox.razor +++ b/Damselfly.Web.Client/Shared/ConflatedTextBox.razor @@ -63,7 +63,7 @@ } } - private void SearchCallback(object state) + private void SearchCallback(object? state) { if( OnValueChanged != null ) OnValueChanged(tempValue); diff --git a/Damselfly.Web.Client/Shared/ConnectionStatus.razor b/Damselfly.Web.Client/Shared/ConnectionStatus.razor index 778a8eb7..62ee579c 100644 --- a/Damselfly.Web.Client/Shared/ConnectionStatus.razor +++ b/Damselfly.Web.Client/Shared/ConnectionStatus.razor @@ -28,10 +28,11 @@ HubConnectionState.Connected => "color:green;", HubConnectionState.Reconnecting => "color:darkorange;", HubConnectionState.Connecting => "color:darkorange;", - HubConnectionState.Disconnected => "color:darkred;" - } + " text-shadow: 1px 1px 1px #666;"; + HubConnectionState.Disconnected => "color:darkred;", + _ => "color:darkred;" + } + " text-shadow: 1px 1px 1px #666;"; - InvokeAsync(StateHasChanged); + StateHasChanged(); } protected override async Task OnInitializedAsync() @@ -41,7 +42,7 @@ await base.OnInitializedAsync(); } - protected override async Task OnAfterRenderAsync(bool firstRender) + protected override void OnAfterRender(bool firstRender) { if( firstRender ) { diff --git a/Damselfly.Web.Client/Shared/Dialogs/BasketDialog.razor b/Damselfly.Web.Client/Shared/Dialogs/BasketDialog.razor index 060eca24..c18c1f43 100644 --- a/Damselfly.Web.Client/Shared/Dialogs/BasketDialog.razor +++ b/Damselfly.Web.Client/Shared/Dialogs/BasketDialog.razor @@ -72,8 +72,6 @@ { await basketService.Delete(Basket.BasketId); - int? userId = null; - if( model.IsPublic ) statusService.UpdateGlobalStatus($"Basket '{model.BasketName}' was deleted."); else diff --git a/Damselfly.Web.Client/Shared/Dialogs/NameDialog.razor b/Damselfly.Web.Client/Shared/Dialogs/NameDialog.razor index f8be1e79..c537eb56 100644 --- a/Damselfly.Web.Client/Shared/Dialogs/NameDialog.razor +++ b/Damselfly.Web.Client/Shared/Dialogs/NameDialog.razor @@ -35,7 +35,10 @@ protected override void OnInitialized() { - TypeAheadName = theObject.Person.Name; + if( theObject.Person != null && theObject.Person.Name != null ) + { + TypeAheadName = theObject.Person.Name; + } } private async Task> SearchNames(string text) diff --git a/Damselfly.Web.Client/Shared/ExportConfigManager.razor b/Damselfly.Web.Client/Shared/ExportConfigManager.razor index 3f37fed1..96d233e8 100644 --- a/Damselfly.Web.Client/Shared/ExportConfigManager.razor +++ b/Damselfly.Web.Client/Shared/ExportConfigManager.razor @@ -28,7 +28,7 @@ readonly List configs = new(); [Parameter] - public ExportConfig CurrentConfig { get; set; } + public ExportConfig? CurrentConfig { get; set; } [Parameter] public EventCallback OnValueChanged { get; set; } @@ -84,6 +84,6 @@ await OnValueChanged.InvokeAsync(new ChangeEventArgs { Value = CurrentConfig }); } - await InvokeAsync(StateHasChanged); + StateHasChanged(); } } \ No newline at end of file diff --git a/Damselfly.Web.Client/Shared/FolderList.razor b/Damselfly.Web.Client/Shared/FolderList.razor index 307d3ce2..ddafee71 100644 --- a/Damselfly.Web.Client/Shared/FolderList.razor +++ b/Damselfly.Web.Client/Shared/FolderList.razor @@ -1,43 +1,46 @@ @inject ISearchService searchService @inject IUserFolderService folderService @inject ContextMenuService contextMenuService +@inject SelectionService selectionService +@inject IUserStatusService statusService @inject IUserConfigService configService @inject IDialogService DialogService +@inject IFileService fileService @inject ILogger logger @using DialogOptions = MudBlazor.DialogOptions @implements IDisposable - +
- + + +
- - - +
- @if( folderItems == null ) + @if (folderItems == null) {

Loading...

- + } else {
ShowContextMenu(args, null)) @oncontextmenu:preventDefault="true"> - All Folders + All Folders
@@ -53,6 +56,7 @@ Folder SelectedItem { get; set; } bool FlatView => configService.GetBool(ConfigSettings.FlatView, true); bool Ascending => configService.GetBool(ConfigSettings.FolderSortAscending); + bool IncludeChildFolders => configService.GetBool(ConfigSettings.IncludeChildFolders, true); string SortMode => configService.Get(ConfigSettings.FolderSortMode, "Date"); string SortClass => SortMode == "Date" ? Ascending ? "fa-sort-amount-down" : "fa-sort-amount-up" : Ascending ? "fa-arrow-down-a-z" : "fa-arrow-up-a-z"; string SortModeClass => SortMode == "Date" ? "fa-calendar" : "fa-folder-closed"; @@ -64,59 +68,110 @@ var selectText = "Select Folder"; var viewText = FlatView ? "Tree View" : "Flat View"; var sortDirection = "Sort Ascending"; + var includeChildFolders = "Show sub-folder Images"; var sortText = "Sort by Date"; - if( SortMode == "Date" ) + if (SortMode == "Date") sortText = "Sort by Name"; - if( Ascending ) + if (Ascending) sortDirection = "Sort Descending"; - if( folder == null ) + if (IncludeChildFolders) + includeChildFolders = "Hide sub-folder Images"; + + if (folder == null) { refreshText = "Refresh All Folders"; selectText = "Clear Folder Selection"; } - contextMenuService.Open(args, - new List - { - new() { Text = selectText, Value = 0 }, - new() { Text = refreshText, Value = 1 }, - new() { Text = viewText, Value = 3 }, - new() { Text = sortText, Value = 4 }, - new() { Text = sortDirection, Value = 5 } - }, args => - { - contextMenuService.Close(); - switch( args.Value ) - { - case 0: - if( folder == null ) ResetFilterFolder(); - else SetFilterFolder(folder); - break; - case 1: - _ = ShowRescanDialog(folder); - break; - case 3: - _ = ToggleFlatView(); - break; - case 4: - _ = ToggleSortMode(); - break; - case 5: - _ = ToggleSortAscendingView(); - break; - } - }); + Action OnMenuItemClick = async (x) => await MenuSelected(x, folder); + + contextMenuService.Open(args, ds => + @ + + + +
+ + + + +
+ + @if (selectionService.Selection.Any()) + { + + + } + +
+
); + } + + private async Task MenuSelected(MenuItemEventArgs args, Folder folder) + { + contextMenuService.Close(); + switch (args.Value) + { + case 0: + if (folder == null) + ResetFilterFolder(); + else + SetFilterFolder(folder); + break; + case 1: + await ShowRescanDialog(folder); + break; + case 3: + await ToggleFlatView(); + break; + case 4: + await ToggleSortMode(); + break; + case 5: + await ToggleSortAscendingView(); + break; + case 6: + await ToggleIncludeChildFolder(); + break; + case 7: + await MoveSelectedImagesToFolder(folder, true); + break; + case 8: + await MoveSelectedImagesToFolder(folder, false); + break; + } + } + + /// + /// + /// + /// + /// True if the files are to be moved. False for them to be copied + /// + private async Task MoveSelectedImagesToFolder(Folder folder, bool move) + { + var req = new ImageMoveRequest + { + Destination = folder, + ImageIDs = selectionService.Selection.Select( x => x.ImageId ).ToArray(), + Move = move + }; + + if (await fileService.MoveImages(req)) + { + statusService.UpdateStatus($"Files moved to {folder.Path} successfully."); + } } - private string FolderDisplayName( Folder folder ) + private string FolderDisplayName(Folder folder) { string display = folder.MetaData.DisplayName; if (FlatView) - { + { if (display.Length < 10 && folder.Parent != null) display = folder.Parent.MetaData.DisplayName + $" {System.IO.Path.DirectorySeparatorChar} " + display; } @@ -137,7 +192,7 @@ { var newMode = "Date"; - if( SortMode == "Date" ) + if (SortMode == "Date") newMode = "Name"; configService.SetForUser(ConfigSettings.FolderSortMode, newMode); @@ -145,6 +200,18 @@ await ProcessUpdatedFilter(); } + private Task ToggleIncludeChildFolder() + { + var newState = !IncludeChildFolders; + + searchService.IncludeChildFolders = newState; + StateHasChanged(); + + configService.SetForUser(ConfigSettings.IncludeChildFolders, newState.ToString()); + + return Task.CompletedTask; + } + private async Task ToggleSortAscendingView() { var newState = !Ascending; @@ -158,11 +225,12 @@ { var parameters = new DialogParameters { { "allimages", true } }; - if( folder != null ) + if (folder != null) parameters = new DialogParameters { { "folder", folder }, { "count", folder.MetaData.ImageCount } }; var options = new DialogOptions { MaxWidth = MaxWidth.ExtraSmall, DisableBackdropClick = true }; - var dialog = DialogService.Show("Re-scan Images", parameters, options); + var dialog = DialogService.Show + ("Re-scan Images", parameters, options); var result = await dialog.Result; } @@ -173,11 +241,11 @@ string FolderIcon(Folder folder) { - if( !FlatView ) + if (!FlatView) { - if( folderService.IsExpanded(folder) ) + if (folderService.IsExpanded(folder)) return "fa-folder-open"; - if( folder.HasSubFolders ) + if (folder.HasSubFolders) return "fa-folder-plus"; } return "fa-folder"; @@ -185,7 +253,7 @@ string IndentMargin(Folder folder) { - if( FlatView ) + if (FlatView) return string.Empty; return $"margin-left:{folder.MetaData.Depth * 10}px;"; @@ -193,10 +261,10 @@ string FolderStyle(int folderId) { - if( folderId == -1 && searchService.Folder == null ) + if (folderId == -1 && searchService.Folder == null) return "folder-entry-selected"; - if( searchService.Folder?.FolderId == folderId ) + if (searchService.Folder?.FolderId == folderId) return "folder-entry-selected"; return string.Empty; @@ -204,7 +272,7 @@ private void DoFilter(string searchTerm) { - if( FilterTerm != searchTerm ) + if (FilterTerm != searchTerm) { FilterTerm = searchTerm; OnFoldersChanged(); @@ -225,7 +293,7 @@ protected async Task ToggleExpand(Folder item) { - if( item.HasSubFolders ) + if (item.HasSubFolders) { folderService.ToggleExpand(item); @@ -240,12 +308,12 @@ logger.LogInformation($"Retrieved {folders.Count} folders"); folderItems = folders.ToList(); - await InvokeAsync(StateHasChanged); + StateHasChanged(); } protected override async void OnAfterRender(bool firstRender) { - if( firstRender ) + if (firstRender) { folderService.OnFoldersChanged += OnFoldersChanged; searchService.OnSearchQueryChanged += OnSearchChanged; @@ -269,7 +337,7 @@ private void OnSearchChanged() { - InvokeAsync(StateHasChanged); + StateHasChanged(); } -} \ No newline at end of file + } diff --git a/Damselfly.Web.Client/Shared/GridImage.razor b/Damselfly.Web.Client/Shared/GridImage.razor index 5a5df394..a0c6b988 100644 --- a/Damselfly.Web.Client/Shared/GridImage.razor +++ b/Damselfly.Web.Client/Shared/GridImage.razor @@ -5,6 +5,7 @@ @inject ISearchService searchService @inject NavigationService navContext @inject IUserBasketService basketService +@inject IFileService fileService @inject IDownloadService downloadService @inject ILogger logger @inject IDialogService DialogService @@ -122,8 +123,10 @@ else } - + +
+
); @@ -173,9 +176,18 @@ else case 8: FilterByImageDate(); break; + case 9: + await DeleteImages(ContextSelection.Select(x => x.ImageId).ToList()); + break; } } + private async Task DeleteImages( ICollection imageIds ) + { + var req = new MultiImageRequest { ImageIDs = imageIds }; + await fileService.DeleteImages(req); + } + private async Task ShowRescanDialog() { var parameters = new DialogParameters { { "images", ContextSelection } }; diff --git a/Damselfly.Web.Client/Shared/ImageGrid.razor b/Damselfly.Web.Client/Shared/ImageGrid.razor index 7918bbe2..b5bbe295 100644 --- a/Damselfly.Web.Client/Shared/ImageGrid.razor +++ b/Damselfly.Web.Client/Shared/ImageGrid.razor @@ -14,7 +14,7 @@ @if (!gridImages.Any()) {
- @ResultsMessage +
@ResultsMessage
} else @@ -67,7 +67,7 @@ if (!endOfImages) {
-
+
} else @@ -89,13 +89,7 @@ private ScrollMonitor ScrollMonitor; [Parameter] - public ThumbSize CurrentThumbSize - { - get => _currentThumbSize; - set => ChangeThumbSize(value); - } - - private ThumbSize _currentThumbSize; + public ThumbSize CurrentThumbSize { get; set; } public string ResultsMessage { get; set; } @@ -123,6 +117,12 @@ selectionService.DeselectImages(grouping.Images); } + protected override void OnParametersSet() + { + ChangeThumbSize(CurrentThumbSize); + base.OnParametersSet(); + } + private List GroupedImages { get @@ -175,31 +175,26 @@ protected void ChangeThumbSize(ThumbSize newSize) { - if (_currentThumbSize != newSize) + switch (newSize) { - _currentThumbSize = newSize; - - switch (newSize) - { - case ThumbSize.Medium: - WrapStyle = "wrapping-table-medium"; - break; - case ThumbSize.Large: - WrapStyle = "wrapping-table-large"; - break; - case ThumbSize.ExtraLarge: - WrapStyle = "wrapping-table-x-large"; - break; - case ThumbSize.Small: - WrapStyle = "wrapping-table-small"; - break; - default: - WrapStyle = "wrapping-table-small"; - break; - } - - configService.Set(ConfigSettings.ThumbSize, newSize.ToString()); + case ThumbSize.Medium: + WrapStyle = "wrapping-table-medium"; + break; + case ThumbSize.Large: + WrapStyle = "wrapping-table-large"; + break; + case ThumbSize.ExtraLarge: + WrapStyle = "wrapping-table-x-large"; + break; + case ThumbSize.Small: + WrapStyle = "wrapping-table-small"; + break; + default: + WrapStyle = "wrapping-table-small"; + break; } + + configService.Set(ConfigSettings.ThumbSize, newSize.ToString()); } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -226,7 +221,7 @@ protected void SelectedImagesChanged() { - InvokeAsync(StateHasChanged); + StateHasChanged(); } public void Dispose() @@ -241,7 +236,7 @@ protected void BasketChanged(BasketChanged changeType) { - InvokeAsync(StateHasChanged); + StateHasChanged(); } protected void SearchResultsChanged(SearchResponse response) @@ -279,7 +274,7 @@ if (searchService.SearchResults.Any()) { ResultsMessage = $"Loading {searchService.SearchResults.Count()} images..."; - await InvokeAsync(StateHasChanged); + StateHasChanged(); var images = await imageCache.GetCachedImages(searchService.SearchResults); @@ -301,7 +296,7 @@ // Flag the 'more' div if we loaded at least as many as we requested. endOfImages = !moreDataAvaiable; - await InvokeAsync(StateHasChanged); + StateHasChanged(); } } \ No newline at end of file diff --git a/Damselfly.Web.Client/Shared/ImagePreview.razor b/Damselfly.Web.Client/Shared/ImagePreview.razor index af47a713..f987e962 100644 --- a/Damselfly.Web.Client/Shared/ImagePreview.razor +++ b/Damselfly.Web.Client/Shared/ImagePreview.razor @@ -43,33 +43,25 @@ of the visible image is updated which should update it instantly. { [Parameter] [EditorRequired] - public Image Image - { - get => theImage; - set => ChangeImage(value); - } + public Image Image { get; set; } [Parameter] - public bool ShowObjects - { - get => showObjects; - set => SetShowObjects(value); - } + public bool ShowObjects { get; set; } [CascadingParameter] private Task authenticationStateTask { get; set; } - private bool showObjects; - private Image theImage; private LocalFileExporter FileExporter; private double _rangeValue = 1.0; private Panzoom _panzoom; public double ZoomLevel => _rangeValue; - private string ImgKey => $"prev{theImage.ImageId}"; - private string ImgPreviewKey => $"{theImage.ImageId}"; + private string ImgKey => $"prev{Image.ImageId}"; + private string ImgPreviewKey => $"{Image.ImageId}"; private string ImageUrl { get; set; } - private string ImageUrlHighRes => theImage.ThumbUrl(ThumbSize.ExtraLarge); + private string ImageUrlHighRes => Image.ThumbUrl(ThumbSize.ExtraLarge); + private int currentImageId = -1; + // TODO: This will change to ZoomWithWheel - but only when that can be changed dynamically. private BlazorPanzoom.WheelMode WheelMode => ShowObjects ? BlazorPanzoom.WheelMode.None : BlazorPanzoom.WheelMode.None; @@ -86,11 +78,19 @@ of the visible image is updated which should update it instantly. private readonly List browserRenderedExtensions = new() { ".jpg", ".jpeg", ".png", ".svg", ".webp", ".gif" }; + protected override async Task OnParametersSetAsync() + { + await ChangeImage(); + SetShowObjects(ShowObjects); + + await base.OnParametersSetAsync(); + } + private IList Objects { get { - var objects = theImage?.ImageObjects ?? new List(); + var objects = Image?.ImageObjects ?? new List(); // Sort objects first, then faces, and largest first, so that smaller // objects will appear on top, so they can be easily accessed. @@ -104,8 +104,8 @@ of the visible image is updated which should update it instantly. { Task.Run(async () => { - this.theImage = await imageCacheService.GetCachedImage(imgObject.ImageId); - await InvokeAsync(StateHasChanged); + this.Image = await imageCacheService.GetCachedImage(imgObject.ImageId); + StateHasChanged(); }); } @@ -141,7 +141,7 @@ of the visible image is updated which should update it instantly. await ShowRescanDialog(); break; case 3: - await FileExporter.ExportImagesToLocalFilesystem(new List { theImage }); + await FileExporter.ExportImagesToLocalFilesystem(new List { Image }); break; case 4: GoToImageFolder(); @@ -155,12 +155,12 @@ of the visible image is updated which should update it instantly. private void GoToImageFolder() { - NavigationManager.NavigateTo($"/?folderId={theImage.Folder.FolderId}"); + NavigationManager.NavigateTo($"/?folderId={Image.Folder.FolderId}"); } private void FilterByImageDate() { - var url = $"/?date={theImage.SortDate:dd-MMM-yyyy}"; + var url = $"/?date={Image.SortDate:dd-MMM-yyyy}"; NavigationManager.NavigateTo(url); } @@ -176,7 +176,7 @@ of the visible image is updated which should update it instantly. private async Task ShowRescanDialog() { - var parameters = new DialogParameters { { "images", new List { theImage } } }; + var parameters = new DialogParameters { { "images", new List { Image } } }; var options = new DialogOptions { MaxWidth = MaxWidth.ExtraSmall, DisableBackdropClick = true }; var dialog = DialogService.Show("Re-scan Images", parameters, options); var result = await dialog.Result; @@ -186,7 +186,7 @@ of the visible image is updated which should update it instantly. { try { - var downloadFile = theImage.DownloadImageUrl; + var downloadFile = Image.DownloadImageUrl; await JsRuntime.InvokeAsync("downloadFile", downloadFile); } catch (Exception ex) @@ -197,64 +197,62 @@ of the visible image is updated which should update it instantly. public void ToggleShowObjects() { - SetShowObjects(!showObjects); + SetShowObjects(!ShowObjects); } private void SetShowObjects(bool newState) { - if (newState == showObjects) + if (newState == ShowObjects) return; if (ZoomRangeValue != 1.0) _ = ResetZoom(); - showObjects = newState; - userConfigService.SetForUser(ConfigSettings.ShowObjects, showObjects.ToString()); + ShowObjects = newState; + userConfigService.SetForUser(ConfigSettings.ShowObjects, ShowObjects.ToString()); StateHasChanged(); - if (showObjects) + if (ShowObjects) DoObjectBox(); } - protected override async Task OnAfterRenderAsync(bool firstRender) + protected override void OnAfterRender(bool firstRender) { if (firstRender) { - if (showObjects) + if (ShowObjects) DoObjectBox(); } } - private void ChangeImage(Image newImage) + private async Task ChangeImage() { - if (newImage == null) - throw new ArgumentException("Image cannot be null"); - - if (theImage == null || theImage.ImageId != newImage.ImageId) + if (currentImageId != Image.ImageId) { - theImage = newImage; + currentImageId = Image.ImageId; HiResImageLoaded = false; - ImageUrl = $"/thumb/{ThumbSize.Medium}/{theImage.ImageId}"; + ImageUrl = $"/thumb/{ThumbSize.Medium}/{Image.ImageId}"; statusService.UpdateStatus("Loading hi-res image..."); StateHasChanged(); + + await ResetZoom(); } } protected override void OnInitialized() { ShowObjects = userConfigService.GetBool(ConfigSettings.ShowObjects); - base.OnInitialized(); } - protected async Task ReplaceUrl(ProgressEventArgs args) + protected void ReplaceUrl(ProgressEventArgs args) { ImageUrl = ImageUrlHighRes; HiResImageLoaded = true; - statusService.UpdateStatus("Hi-res image loaded."); StateHasChanged(); + statusService.UpdateStatus("Hi-res image loaded."); DoObjectBox(); @@ -301,13 +299,21 @@ of the visible image is updated which should update it instantly. public async Task ResetZoom() { - await _panzoom.ResetAsync(); - await UpdateSlider(); + if (_panzoom != null) + { + await _panzoom.ResetAsync(); + await UpdateSlider(); + } } public async Task UpdateSlider() { var scale = await _panzoom.GetScaleAsync(); - _rangeValue = scale; + if (scale != _rangeValue) + { + _rangeValue = scale; + StateHasChanged(); + } } + } \ No newline at end of file diff --git a/Damselfly.Web.Client/Shared/ImageProperties.razor b/Damselfly.Web.Client/Shared/ImageProperties.razor index 3352c63b..29619506 100644 --- a/Damselfly.Web.Client/Shared/ImageProperties.razor +++ b/Damselfly.Web.Client/Shared/ImageProperties.razor @@ -41,7 +41,7 @@ else
@@ -142,19 +142,17 @@ else private async Task SetBasketState(bool newState) { await basketService.SetImageBasketState(newState, new[] { CurrentImage.ImageId }); - + // Notify the image list that the selection has changed StateHasChanged(); } - private async Task OnRatingChanged(int newValue) + private async Task OnRatingChanged() { - if( newValue != CurrentImage.MetaData.Rating ) - { - await tagService.SetExifFieldAsync(new[] { CurrentImage.ImageId }, ExifOperation.ExifType.Rating, newValue.ToString(), userService.UserId); - CurrentImage.MetaData.Rating = newValue; - StateHasChanged(); - } + var newValue = CurrentImage.MetaData.Rating; + await tagService.SetExifFieldAsync(new[] { CurrentImage.ImageId }, ExifOperation.ExifType.Rating, newValue.ToString(), userService.UserId); + CurrentImage.MetaData.Rating = newValue; + StateHasChanged(); } private async void CaptionChanged(ChangeEventArgs args) @@ -212,7 +210,7 @@ else protected void BasketStateChanged(BasketChanged change) { - InvokeAsync(StateHasChanged); + StateHasChanged(); } protected void NavigationChanged(Image image) @@ -221,7 +219,7 @@ else { CurrentImage = image; - InvokeAsync(StateHasChanged); + StateHasChanged(); } } diff --git a/Damselfly.Web.Client/Shared/LogView.razor b/Damselfly.Web.Client/Shared/LogView.razor index 102f262b..0050d681 100644 --- a/Damselfly.Web.Client/Shared/LogView.razor +++ b/Damselfly.Web.Client/Shared/LogView.razor @@ -73,7 +73,7 @@ if( response.LogEntries != null ) { logLines.AddRange(response.LogEntries); - await InvokeAsync(StateHasChanged); + StateHasChanged(); statusService.UpdateStatus($"Loaded {logLines.Count()} log lines..."); } } @@ -82,7 +82,7 @@ private async Task DownloadLogFile() { - // TODO: Download log file here + // TODO: Download log file here await Task.Delay(500); } diff --git a/Damselfly.Web.Client/Shared/MainLayout.razor b/Damselfly.Web.Client/Shared/MainLayout.razor index 9b00d5d9..9a5fb473 100644 --- a/Damselfly.Web.Client/Shared/MainLayout.razor +++ b/Damselfly.Web.Client/Shared/MainLayout.razor @@ -73,9 +73,12 @@ { var authState = await authStateTask; - ShowLogin = ! authState.User.Identity.IsAuthenticated; + if( authState != null ) + { + ShowLogin = ! authState.User.Identity.IsAuthenticated; - StateHasChanged(); + StateHasChanged(); + } } public void Dispose() diff --git a/Damselfly.Web.Client/Shared/MapView.razor b/Damselfly.Web.Client/Shared/MapView.razor index e58d5032..4601d849 100644 --- a/Damselfly.Web.Client/Shared/MapView.razor +++ b/Damselfly.Web.Client/Shared/MapView.razor @@ -87,7 +87,7 @@ else else showMap = false; - await InvokeAsync(StateHasChanged); + StateHasChanged(); } protected override async Task OnAfterRenderAsync(bool firstRender) diff --git a/Damselfly.Web.Client/Shared/SearchBar.razor b/Damselfly.Web.Client/Shared/SearchBar.razor index 995c77ae..7e5850f2 100644 --- a/Damselfly.Web.Client/Shared/SearchBar.razor +++ b/Damselfly.Web.Client/Shared/SearchBar.razor @@ -148,7 +148,7 @@ protected void SearchQueryChanged() { - _ = InvokeAsync(StateHasChanged); + StateHasChanged(); } private void DoSearch(string searchTerm) diff --git a/Damselfly.Web.Client/Shared/SelectedImages.razor b/Damselfly.Web.Client/Shared/SelectedImages.razor index 22473c89..df7fc11b 100644 --- a/Damselfly.Web.Client/Shared/SelectedImages.razor +++ b/Damselfly.Web.Client/Shared/SelectedImages.razor @@ -132,7 +132,7 @@ else protected void SelectedImagesChanged() { - InvokeAsync(StateHasChanged); + StateHasChanged(); } protected void BasketImagesChanged(BasketChanged change) @@ -142,14 +142,14 @@ else private async Task LoadData() { - // Marshall onto the dispatcher thread + // Marshall onto the dispatcher thread var watch = new Stopwatch("SelectedLoadData"); gridImages.Clear(); gridImages.AddRange(basketService.BasketImages); watch.Stop(); - await InvokeAsync(StateHasChanged); + StateHasChanged(); } } \ No newline at end of file diff --git a/Damselfly.Web.Client/Shared/SideBar.razor b/Damselfly.Web.Client/Shared/SideBar.razor index 22477669..cbb98e73 100644 --- a/Damselfly.Web.Client/Shared/SideBar.razor +++ b/Damselfly.Web.Client/Shared/SideBar.razor @@ -72,7 +72,7 @@ private void OnSideBarStateChanged(ViewDataService.SideBarState state) { - InvokeAsync(StateHasChanged); + StateHasChanged(); } } \ No newline at end of file diff --git a/Damselfly.Web.Client/Shared/Spinner.razor b/Damselfly.Web.Client/Shared/Spinner.razor deleted file mode 100644 index daa23ebb..00000000 --- a/Damselfly.Web.Client/Shared/Spinner.razor +++ /dev/null @@ -1,5 +0,0 @@ -
-
- Loading... -
-
\ No newline at end of file diff --git a/Damselfly.Web.Client/Shared/StarRating.razor b/Damselfly.Web.Client/Shared/StarRating.razor index 476b840a..3d26937a 100644 --- a/Damselfly.Web.Client/Shared/StarRating.razor +++ b/Damselfly.Web.Client/Shared/StarRating.razor @@ -3,13 +3,10 @@ { - + - @if( star <= Rating ) - { - - } + } @@ -18,33 +15,23 @@ @code { [Parameter] - public int Rating - { - get => proxyValue; - set => ValueChanged(value); - } + public int Rating { get; set; } [Parameter] - public bool Editable { get; set; } + public EventCallback RatingChanged { get; set; } [Parameter] - public Func OnValueChanged { get; set; } - - private int proxyValue = 0; + public bool Editable { get; set; } - private string StarStyle(int star) + private string StarStyle(int star, bool canEdit) { - return (star <= Rating ? "fas" : "far") + " fa-star damselfly-ratingstar damselfly-ratingstarmouse"; + return (star <= Rating ? "fas" : "far") + " fa-star damselfly-ratingstar" + (canEdit ? " damselfly-ratingstarmouse" : string.Empty); } private void ValueChanged(int value) { - if( proxyValue != value ) - { - proxyValue = value; - - _ = OnValueChanged?.Invoke(value); - } + Rating = value; + RatingChanged.InvokeAsync(Rating); } } \ No newline at end of file diff --git a/Damselfly.Web.Client/Shared/Stats.razor b/Damselfly.Web.Client/Shared/Stats.razor index fb6ee680..7db988c8 100644 --- a/Damselfly.Web.Client/Shared/Stats.razor +++ b/Damselfly.Web.Client/Shared/Stats.razor @@ -102,7 +102,7 @@ if( firstRender ) { statistics = await dataService.GetStatistics(); - await InvokeAsync(StateHasChanged); + StateHasChanged(); } } diff --git a/Damselfly.Web.Client/Shared/Statusbar.razor b/Damselfly.Web.Client/Shared/Statusbar.razor index d108adfe..646cadc1 100644 --- a/Damselfly.Web.Client/Shared/Statusbar.razor +++ b/Damselfly.Web.Client/Shared/Statusbar.razor @@ -25,14 +25,11 @@ private void UpdateStatus(string newText) { - InvokeAsync(() => + if (StatusText != newText) { - if (StatusText != newText) - { - StatusText = newText; - StateHasChanged(); - } - }); + StatusText = newText; + StateHasChanged(); + } } protected override void OnAfterRender(bool firstRender) diff --git a/Damselfly.Web.Client/Shared/TagAutoComplete.razor b/Damselfly.Web.Client/Shared/TagAutoComplete.razor index 30ca21b7..32bd7223 100644 --- a/Damselfly.Web.Client/Shared/TagAutoComplete.razor +++ b/Damselfly.Web.Client/Shared/TagAutoComplete.razor @@ -5,17 +5,18 @@
@code { - private string tagText; - private string TypeAheadTag { get { return tagText; } set { CreateNewTags(value); } } + private string TypeAheadTag { get; set; } [Parameter] public bool IsDisabled { get; set; } = false; @@ -31,18 +32,19 @@ base.OnParametersSet(); } - private void CreateNewTags(string tag) + private void CreateNewTags() { - if (!string.IsNullOrEmpty(tag)) + if (!string.IsNullOrEmpty(TypeAheadTag)) { - Logging.Log("Saving new tag: " + tag); + var newTag = TypeAheadTag; + Logging.Log($"Saving new tag: {newTag}"); // Reset the tag TypeAheadTag = string.Empty; StateHasChanged(); // Call the callback - OnAddNewtag(tag); + OnAddNewtag(newTag); } } diff --git a/Damselfly.Web.Client/Shared/TagList.razor b/Damselfly.Web.Client/Shared/TagList.razor index ce3f95b6..84607422 100644 --- a/Damselfly.Web.Client/Shared/TagList.razor +++ b/Damselfly.Web.Client/Shared/TagList.razor @@ -169,7 +169,7 @@ ///
/// /// - private async Task WriteNewTags(List tagsToAdd, List tagsToDelete = null) + private async Task WriteNewTags(List? tagsToAdd, List? tagsToDelete = null) { await tagService.UpdateTagsAsync(CurrentImages.Select(x => x.ImageId).ToList(), tagsToAdd, tagsToDelete, userService.UserId); @@ -305,14 +305,18 @@ protected async Task RefreshFavourites() { + var favourites = await tagService.GetFavouriteTags(); + var recents = await recentTagService.GetRecentTags(); + var uniqueRecents = recents.Except(favourites.Select(x => x.Keyword)); + faveTags.Clear(); - recentTags.Clear(); + faveTags.AddRange(favourites); - faveTags.AddRange(await tagService.GetFavouriteTags()); - recentTags.AddRange(await recentTagService.GetRecentTags()); + recentTags.Clear(); + recentTags.AddRange( recents ); // We might not be called from the dispatcher thread. - await InvokeAsync(StateHasChanged); + StateHasChanged(); } protected override async Task OnParametersSetAsync() diff --git a/Damselfly.Web.Client/Shared/TimedStatus.razor b/Damselfly.Web.Client/Shared/TimedStatus.razor index d1a19ff6..3024d242 100644 --- a/Damselfly.Web.Client/Shared/TimedStatus.razor +++ b/Damselfly.Web.Client/Shared/TimedStatus.razor @@ -8,28 +8,25 @@ public int DisplayIntervalSecs { get; set; } = 5; [Parameter] - public string StatusText + public string StatusText { get; set; } + + private Timer? searchTimer; + + protected override async Task OnParametersSetAsync() { - get => _text; - set => SetStatusText(value); - } + SetStatusText(StatusText); - private string _text; - private Timer searchTimer; + await base.OnParametersSetAsync(); + } private void SetStatusText(string value) { - if( _text != value ) - { - KillTimer(); + KillTimer(); + + if( ! string.IsNullOrEmpty( value )) searchTimer = new Timer(TimerCallback, null, DisplayIntervalSecs * 1000, Timeout.Infinite); - InvokeAsync(() => - { - _text = value; - StateHasChanged(); - }); - } + StateHasChanged(); } private void KillTimer() @@ -40,15 +37,9 @@ oldTimer.Dispose(); } - private void TimerCallback(object state) + private void TimerCallback(object? state) { - KillTimer(); - - if( !string.IsNullOrEmpty(_text) ) - { - _text = string.Empty; - InvokeAsync(StateHasChanged); - } + SetStatusText(string.Empty); } } \ No newline at end of file diff --git a/Damselfly.Web.Client/Shared/UserManagement.razor b/Damselfly.Web.Client/Shared/UserManagement.razor index d8a1faef..b0c0d2f5 100644 --- a/Damselfly.Web.Client/Shared/UserManagement.razor +++ b/Damselfly.Web.Client/Shared/UserManagement.razor @@ -46,7 +46,7 @@ var dialog = DialogService.Show("Edit User", parameters); var result = await dialog.Result; - if (!result.Cancelled) + if (!result.Canceled) { await LoadUsers(); } diff --git a/Damselfly.Web.Client/wwwroot/index.html b/Damselfly.Web.Client/wwwroot/index.html index 8dd27203..cf7c98cb 100644 --- a/Damselfly.Web.Client/wwwroot/index.html +++ b/Damselfly.Web.Client/wwwroot/index.html @@ -40,6 +40,7 @@ + @@ -70,6 +71,10 @@ return element.setSelectionRange(start, end); } +function getClientVersion(){ + return CACHE_VERSION; +} + function clearFocus() { if (document.activeElement instanceof HTMLElement) document.activeElement.blur(); diff --git a/Damselfly.Web.Client/wwwroot/service-worker.published.js b/Damselfly.Web.Client/wwwroot/service-worker.published.js index b51c27b2..63f4fdc3 100644 --- a/Damselfly.Web.Client/wwwroot/service-worker.published.js +++ b/Damselfly.Web.Client/wwwroot/service-worker.published.js @@ -14,6 +14,7 @@ const offlineAssetsExclude = [ /^service-worker\.js$/ ]; async function onInstall(event) { console.info('Service worker: Install'); + // Fetch and cache all matching items from the assets manifest const assetsRequests = self.assetsManifest.assets .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) @@ -29,6 +30,8 @@ async function onInstall(event) { async function onActivate(event) { console.info('Service worker: Activate'); + self.skipWaiting(); + // Delete unused caches const cacheKeys = await caches.keys(); await Promise.all(cacheKeys diff --git a/Damselfly.Web.Client/wwwroot/version.js b/Damselfly.Web.Client/wwwroot/version.js index 6e387611..c194b652 100644 --- a/Damselfly.Web.Client/wwwroot/version.js +++ b/Damselfly.Web.Client/wwwroot/version.js @@ -1 +1 @@ -const CACHE_VERSION='4.0.2-20221209173718' +const CACHE_VERSION='4.0.5-20230711213350' diff --git a/Damselfly.Web.Server/Areas/Identity/Pages/Account/Login.cshtml.cs b/Damselfly.Web.Server/Areas/Identity/Pages/Account/Login.cshtml.cs index c378eb7d..60cc14f2 100644 --- a/Damselfly.Web.Server/Areas/Identity/Pages/Account/Login.cshtml.cs +++ b/Damselfly.Web.Server/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -31,13 +31,13 @@ public LoginModel(SignInManager signInManager, public IList ExternalLogins { get; set; } - public string ReturnUrl { get; set; } + public string? ReturnUrl { get; set; } [TempData] public string ErrorMessage { get; set; } public bool CanRegister => _userService.AllowPublicRegistration; - public async Task OnGetAsync(string returnUrl = null) + public async Task OnGetAsync(string? returnUrl = null) { if ( !string.IsNullOrEmpty(ErrorMessage) ) ModelState.AddModelError(string.Empty, ErrorMessage); @@ -51,7 +51,7 @@ public async Task OnGetAsync(string returnUrl = null) ReturnUrl = returnUrl; } - public async Task OnPostAsync(string returnUrl = null) + public async Task OnPostAsync(string? returnUrl = null) { returnUrl ??= Url.Content("~/"); diff --git a/Damselfly.Web.Server/Areas/Identity/Pages/Account/Register.cshtml.cs b/Damselfly.Web.Server/Areas/Identity/Pages/Account/Register.cshtml.cs index 41fb2ce3..7b46b3a3 100644 --- a/Damselfly.Web.Server/Areas/Identity/Pages/Account/Register.cshtml.cs +++ b/Damselfly.Web.Server/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -35,13 +35,13 @@ public RegisterModel( [BindProperty] public InputModel Input { get; set; } - public string ReturnUrl { get; set; } + public string? ReturnUrl { get; set; } public bool CanRegister => _userService.AllowPublicRegistration; public IList ExternalLogins { get; set; } - public async Task OnGetAsync(string returnUrl = null) + public async Task OnGetAsync(string? returnUrl = null) { ReturnUrl = returnUrl; ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); @@ -52,7 +52,7 @@ public async Task OnGetAsync(string returnUrl = null) ///
/// /// - public async Task OnPostAsync(string returnUrl = null) + public async Task OnPostAsync(string? returnUrl = null) { if ( !_userService.AllowPublicRegistration ) { diff --git a/Damselfly.Web.Server/Controllers/FileController.cs b/Damselfly.Web.Server/Controllers/FileController.cs new file mode 100644 index 00000000..deacb028 --- /dev/null +++ b/Damselfly.Web.Server/Controllers/FileController.cs @@ -0,0 +1,33 @@ +using Damselfly.Core.DbModels.Models.APIModels; +using Damselfly.Core.Models; +using Damselfly.Core.ScopedServices.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace Damselfly.Web.Server.Controllers; + +//[Authorize(Policy = PolicyDefinitions.s_IsLoggedIn)] +[ApiController] +[Route("/api/files")] +public class FileController : ControllerBase +{ + private readonly ILogger _logger; + private readonly IFileService _service; + + public FileController( IFileService service, ILogger logger) + { + _service = service; + _logger = logger; + } + + [HttpPost( "/api/files/delete" )] + public async Task DeleteImages( MultiImageRequest req ) + { + return await _service.DeleteImages( req ); + } + + [HttpPost("/api/files/move")] + public async Task MoveImages( ImageMoveRequest req) + { + return await _service.MoveImages(req); + } +} \ No newline at end of file diff --git a/Damselfly.Web.Server/Controllers/ImageController.cs b/Damselfly.Web.Server/Controllers/ImageController.cs index 12031e8f..b620d4cb 100644 --- a/Damselfly.Web.Server/Controllers/ImageController.cs +++ b/Damselfly.Web.Server/Controllers/ImageController.cs @@ -49,7 +49,7 @@ public async Task Image(string imageId, CancellationToken cancel, if ( image != null ) { - string downloadFilename = null; + string? downloadFilename = null; if ( isDownload ) downloadFilename = image.FileName; diff --git a/Damselfly.Web.Server/Damselfly.Web.Server.csproj b/Damselfly.Web.Server/Damselfly.Web.Server.csproj index d1f15046..11cc0fd2 100644 --- a/Damselfly.Web.Server/Damselfly.Web.Server.csproj +++ b/Damselfly.Web.Server/Damselfly.Web.Server.csproj @@ -13,9 +13,9 @@ 4 - - - + + + @@ -29,16 +29,15 @@ - - - - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - diff --git a/Damselfly.Web.Server/Program.cs b/Damselfly.Web.Server/Program.cs index 56e8d450..027f7505 100644 --- a/Damselfly.Web.Server/Program.cs +++ b/Damselfly.Web.Server/Program.cs @@ -230,30 +230,33 @@ private static void StartWebServer(DamselflyOptions cmdLineOptions, string[] arg app.Run(); } - private static void InitialiseDB(WebApplication app, DamselflyOptions options) + private static void InitialiseDB( WebApplication app, DamselflyOptions options ) { using var scope = app.Services.CreateScope(); using var db = scope.ServiceProvider.GetService(); - try + if( db != null ) { - Logging.Log("Running Sqlite DB migrations..."); - db.Database.Migrate(); - } - catch ( Exception ex ) - { - Logging.LogWarning($"Migrations failed with exception: {ex}"); + try + { + Logging.Log( "Running Sqlite DB migrations..." ); + db.Database.Migrate(); + } + catch( Exception ex ) + { + Logging.LogWarning( $"Migrations failed with exception: {ex}" ); - if ( ex.InnerException != null ) - Logging.LogWarning($"InnerException: {ex.InnerException}"); + if( ex.InnerException != null ) + Logging.LogWarning( $"InnerException: {ex.InnerException}" ); - Logging.Log("Creating DB."); - db.Database.EnsureCreated(); - } + Logging.Log( "Creating DB." ); + db.Database.EnsureCreated(); + } - db.IncreasePerformance(); + db.IncreasePerformance(); - BaseDBModel.ReadOnly = options.ReadOnly; + BaseDBModel.ReadOnly = options.ReadOnly; + } } private static void SetupIdentity(IServiceCollection services) diff --git a/NuGet.config b/NuGet.config index d82209db..dca8b86b 100644 --- a/NuGet.config +++ b/NuGet.config @@ -2,7 +2,6 @@ - @@ -10,9 +9,6 @@ - - - diff --git a/README.md b/README.md index 8f9eba6d..a6ff66ea 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ editing etc. * Image Classification * Facial Recognition (Temporarily unavailable - see note below) * Full-text search with multi-phrase partial-word searches +* Image re-organisation - move/copy images between folders, and delete images (via a trashcan folder) * Advanced search - filter by: * Find visually similar images * Date ranges diff --git a/VERSION b/VERSION index aa31e71b..8b2dd6c3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.3 \ No newline at end of file +4.0.5 \ No newline at end of file