From d817bbc47adbcd5d2556ca35cdb35a16f085a2e3 Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Mon, 30 Aug 2021 17:32:08 +0100 Subject: [PATCH 01/21] Better write exception --- Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs b/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs index 9b42c9e8..37f016e2 100644 --- a/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs +++ b/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs @@ -267,10 +267,10 @@ public async Task SaveChangesAsync(string contextDesc) } catch (Exception ex) { + Logging.Log("Exception - DB WRITE FAILED: {0}", ex); + if (ex.InnerException != null) Logging.Log("Exception - DB WRITE FAILED. InnerException: {0}", ex.InnerException.Message); - else - Logging.Log("Exception - DB WRITE FAILED: {0}", ex.Message); return 0; } From 77e0d5672e411884142c744ae786ea86939492bb Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Mon, 30 Aug 2021 17:34:14 +0100 Subject: [PATCH 02/21] Sleep sleep baby --- Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs b/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs index 37f016e2..58ada42e 100644 --- a/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs +++ b/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs @@ -272,6 +272,11 @@ public async Task SaveChangesAsync(string contextDesc) if (ex.InnerException != null) Logging.Log("Exception - DB WRITE FAILED. InnerException: {0}", ex.InnerException.Message); + if (ex.Message.Contains("database is locked")) + { + Logging.LogWarning("Database locked - sleeping for 5s..."); + await Task.Delay(5 * 1000); + } return 0; } } From 6d24fb27f721e64d4fc829dacd10e0a73a8ab3bc Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Mon, 30 Aug 2021 17:41:04 +0100 Subject: [PATCH 03/21] Lock retries --- .../DBAbstractions/BaseModel.cs | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs b/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs index 58ada42e..b2476a4a 100644 --- a/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs +++ b/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs @@ -250,33 +250,41 @@ public async Task SaveChangesAsync(string contextDesc) return 1; } - try - { - // Write to the DB - var watch = new Stopwatch("SaveChanges" + contextDesc); - - LogChangeSummary(); + int retriesRemaining = 3; - int written = await base.SaveChangesAsync(); + while ( retriesRemaining > 0 ) + { + try + { + // Write to the DB + var watch = new Stopwatch("SaveChanges" + contextDesc); - Logging.LogTrace("{0} changes written to the DB", written); + LogChangeSummary(); - watch.Stop(); + int written = await base.SaveChangesAsync(); - return written; - } - catch (Exception ex) - { - Logging.Log("Exception - DB WRITE FAILED: {0}", ex); + Logging.LogTrace("{0} changes written to the DB", written); - if (ex.InnerException != null) - Logging.Log("Exception - DB WRITE FAILED. InnerException: {0}", ex.InnerException.Message); + watch.Stop(); - if (ex.Message.Contains("database is locked")) + return written; + } + catch (Exception ex) { - Logging.LogWarning("Database locked - sleeping for 5s..."); - await Task.Delay(5 * 1000); + if (ex.Message.Contains("database is locked") && retriesRemaining > 0 ) + { + Logging.LogWarning($"Database locked - sleeping for 5s and retying {retriesRemaining}..."); + retriesRemaining--; + await Task.Delay(5 * 1000); + } + else + { + Logging.LogError("Exception - DB WRITE FAILED: {0}", ex); + if (ex.InnerException != null) + Logging.LogError("Exception - DB WRITE FAILED. InnerException: {0}", ex.InnerException.Message); + } } + return 0; } } From 5b1a4582b5dc264507a6784cdefa632f2aa7207a Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Mon, 30 Aug 2021 22:44:30 +0100 Subject: [PATCH 04/21] Processing time limit for AI --- .../DBAbstractions/BaseModel.cs | 11 ++-- .../Constants/ConfigSettings.cs | 1 + .../Utils/DateTimeExtensions.cs | 33 ++++++++++++ .../Services/ImageRecognitionService.cs | 50 ++++++++++++++++++- Damselfly.Web/Shared/Config.razor | 30 ++++++++++- Damselfly.Web/wwwroot/css/site.css | 2 + 6 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 Damselfly.Core.Utils/Utils/DateTimeExtensions.cs diff --git a/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs b/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs index b2476a4a..1b92bbe0 100644 --- a/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs +++ b/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs @@ -251,6 +251,7 @@ public async Task SaveChangesAsync(string contextDesc) } int retriesRemaining = 3; + int recordsWritten = 0; while ( retriesRemaining > 0 ) { @@ -261,13 +262,13 @@ public async Task SaveChangesAsync(string contextDesc) LogChangeSummary(); - int written = await base.SaveChangesAsync(); + recordsWritten = await base.SaveChangesAsync(); - Logging.LogTrace("{0} changes written to the DB", written); + Logging.LogTrace("{0} changes written to the DB", recordsWritten); watch.Stop(); - return written; + break; } catch (Exception ex) { @@ -282,11 +283,13 @@ public async Task SaveChangesAsync(string contextDesc) Logging.LogError("Exception - DB WRITE FAILED: {0}", ex); if (ex.InnerException != null) Logging.LogError("Exception - DB WRITE FAILED. InnerException: {0}", ex.InnerException.Message); + } } - return 0; } + + return recordsWritten; } /// diff --git a/Damselfly.Core.Utils/Constants/ConfigSettings.cs b/Damselfly.Core.Utils/Constants/ConfigSettings.cs index 0246f358..9b831d23 100644 --- a/Damselfly.Core.Utils/Constants/ConfigSettings.cs +++ b/Damselfly.Core.Utils/Constants/ConfigSettings.cs @@ -16,6 +16,7 @@ public class ConfigSettings public const string ImportSidecarKeywords = "ImportSidecarKeywords"; + public const string AIProcessingTimeRange = "AIProcessingTimeRange"; public const string AzureEndpoint = "AzureEndpoint"; public const string AzureApiKey = "AzureApiKey"; public const string AzureDetectionType = "AzureDetectionType"; diff --git a/Damselfly.Core.Utils/Utils/DateTimeExtensions.cs b/Damselfly.Core.Utils/Utils/DateTimeExtensions.cs new file mode 100644 index 00000000..c5f6803c --- /dev/null +++ b/Damselfly.Core.Utils/Utils/DateTimeExtensions.cs @@ -0,0 +1,33 @@ +using System; + +namespace Damselfly.Core.Utils +{ + public static class DateTimeExtensions + { + public static TimeSpan? LocalTimeSpanToUTC(this TimeSpan? ts) + { + if (ts.HasValue) + { + DateTime dt = DateTime.Now.Date.Add(ts.Value); + DateTime dtUtc = dt.ToUniversalTime(); + TimeSpan tsUtc = dtUtc.TimeOfDay; + return tsUtc; + } + + return null; + } + + public static TimeSpan? UTCTimeSpanToLocal(this TimeSpan? tsUtc) + { + if (tsUtc.HasValue) + { + DateTime dtUtc = DateTime.UtcNow.Date.Add(tsUtc.Value); + DateTime dt = dtUtc.ToLocalTime(); + TimeSpan ts = dt.TimeOfDay; + return ts; + } + + return null; + } + } +} diff --git a/Damselfly.Core/Services/ImageRecognitionService.cs b/Damselfly.Core/Services/ImageRecognitionService.cs index fcc168cd..7d814dee 100644 --- a/Damselfly.Core/Services/ImageRecognitionService.cs +++ b/Damselfly.Core/Services/ImageRecognitionService.cs @@ -8,6 +8,7 @@ using Damselfly.Core.ImageProcessing; using Damselfly.Core.Models; using Damselfly.Core.Utils; +using Damselfly.Core.Utils.Constants; using Damselfly.Core.Utils.ML; using Damselfly.ML.Face.Accord; using Damselfly.ML.Face.Azure; @@ -26,13 +27,14 @@ public class ImageRecognitionService private readonly StatusService _statusService; private readonly IndexingService _indexingService; private readonly ThumbnailService _thumbService; + private readonly ConfigService _configService; private IDictionary _peopleCache; public static bool EnableImageRecognition { get; set; } = true; public ImageRecognitionService(StatusService statusService, ObjectDetector objectDetector, IndexingService indexingService, AzureFaceService azureFace, AccordFaceService accordFace, EmguFaceService emguService, - ThumbnailService thumbs) + ThumbnailService thumbs, ConfigService configService) { _thumbService = thumbs; _accordFaceService = accordFace; @@ -41,6 +43,7 @@ public ImageRecognitionService(StatusService statusService, ObjectDetector objec _objectDetector = objectDetector; _indexingService = indexingService; _emguFaceService = emguService; + _configService = configService; } public ImageRecognitionService() @@ -52,6 +55,26 @@ public List GetCachedPeople() return _peopleCache.Values.OrderBy(x => x.Name).ToList(); } + public (TimeSpan? start,TimeSpan? end) GetProcessingTimeRange() + { + TimeSpan? aiStartTime = null, aiEndTime = null; + + string aiTimeRange = _configService.Get(ConfigSettings.AIProcessingTimeRange); + + if (!string.IsNullOrEmpty(aiTimeRange)) + { + var settings = aiTimeRange.Split("-"); + + if (settings.Length == 2) + { + aiStartTime = TimeSpan.Parse(settings[0]); + aiEndTime = TimeSpan.Parse(settings[1]); + } + } + + return( aiStartTime, aiEndTime ); + } + /// /// Initialise the in-memory cache of people. /// @@ -524,7 +547,24 @@ private async Task ProcessImage(ImageMetaData metadata) { metadata.AILastUpdated = DateTime.UtcNow; await DetectObjects(metadata.Image); - await Task.Delay(500); + } + + private bool WithinProcessingTimeRange() + { + var timeRange = GetProcessingTimeRange(); + + if (timeRange.start != null && timeRange.end != null) + { + var now = DateTime.UtcNow.TimeOfDay; + + if (now < timeRange.start && now > timeRange.end) + { + // AI scans are disabled at this time. + return false; + } + } + + return true; } /// @@ -541,6 +581,12 @@ private async Task ProcessAIScan() while (!complete) { + if( ! WithinProcessingTimeRange() ) + { + Logging.LogVerbose("AI Processing disabled at this time."); + return; + } + Logging.LogVerbose("Querying DB for pending AI scans..."); var watch = new Stopwatch("GetAIQueue"); diff --git a/Damselfly.Web/Shared/Config.razor b/Damselfly.Web/Shared/Config.razor index ae021503..95afea37 100644 --- a/Damselfly.Web/Shared/Config.razor +++ b/Damselfly.Web/Shared/Config.razor @@ -12,6 +12,7 @@ @inject UserStatusService statusService @inject WordpressService wpService @inject TaskService taskScheduler +@inject ImageRecognitionService imageService @inject AzureFaceService azureService
@@ -88,11 +89,18 @@ - + +

AI Processing

+ +
+ + +
+

Azure Cognitive Services Face Recognition

@@ -219,6 +227,9 @@ private bool useSmtp = true; private bool forceLogin = false; private bool anableAuthAndRoles = false; + TimeSpan? aiStartTime = new TimeSpan(23, 00, 00); + TimeSpan? aiEndTime = new TimeSpan(04, 30, 00); + private bool enableAITimeLimit = false; private void HandleValidSubmit() { @@ -249,6 +260,14 @@ themeService.CurrentTheme = selectedTheme; } + string aiSettings = string.Empty; + + if( enableAITimeLimit ) + { + string aiTimeRange = $"{aiStartTime.LocalTimeSpanToUTC()}-{aiEndTime.LocalTimeSpanToUTC()}"; + configService.Set(ConfigSettings.AIProcessingTimeRange, aiTimeRange); + } + statusService.StatusText = "Settings saved."; UpdateServicesWithNewSettings(); @@ -297,6 +316,15 @@ importSidecarKeywords = configService.GetBool(ConfigSettings.ImportSidecarKeywords); selectedTheme = themeService.CurrentTheme; + var range = imageService.GetProcessingTimeRange(); + + if( range.start != null && range.end != null ) + { + aiStartTime = range.start.UTCTimeSpanToLocal(); + aiEndTime = range.end.UTCTimeSpanToLocal(); + enableAITimeLimit = true; + } + base.OnInitialized(); } diff --git a/Damselfly.Web/wwwroot/css/site.css b/Damselfly.Web/wwwroot/css/site.css index 613c9dd2..00acd2c9 100644 --- a/Damselfly.Web/wwwroot/css/site.css +++ b/Damselfly.Web/wwwroot/css/site.css @@ -1210,11 +1210,13 @@ padding: 0.4em 0.65em; flex: 1 1 auto; display: flex; flex-direction: column; + padding: 3px; } .damselfly-configsetting { display: flex; flex-direction: row; + padding: 3px; } .damselfly-configfield { From e546b7d87dfe09c9104c26d0757a30ca6fb2b84a Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Mon, 30 Aug 2021 22:52:16 +0100 Subject: [PATCH 05/21] NOWARN --- Accord/Directory.Build.targets | 2 +- Directory.Build.props | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Accord/Directory.Build.targets b/Accord/Directory.Build.targets index 67be0f84..abf805a5 100644 --- a/Accord/Directory.Build.targets +++ b/Accord/Directory.Build.targets @@ -1,6 +1,6 @@ 0 - 1701;1702;3245;0003;0436; + CA1416,MSB3245,SYSLIB0011 \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index eac64816..98cf4899 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,7 @@ - true - net6.0 + true + net6.0 + CA1416 \ No newline at end of file From ff505a717105b443814f6bce31f4d6b327d680f7 Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Mon, 30 Aug 2021 22:54:53 +0100 Subject: [PATCH 06/21] NOWARN --- Accord/Directory.Build.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Accord/Directory.Build.targets b/Accord/Directory.Build.targets index abf805a5..9b9d2a04 100644 --- a/Accord/Directory.Build.targets +++ b/Accord/Directory.Build.targets @@ -1,6 +1,6 @@ 0 - CA1416,MSB3245,SYSLIB0011 + CA1416,MSB3245,SYSLIB0011,IL2026,IL3000 \ No newline at end of file From 860531bb31d7e01a8bccf41858993dac0134e2a7 Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Mon, 30 Aug 2021 22:55:51 +0100 Subject: [PATCH 07/21] MOAR NOWARN --- Accord/Directory.Build.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Accord/Directory.Build.targets b/Accord/Directory.Build.targets index 9b9d2a04..bbdef107 100644 --- a/Accord/Directory.Build.targets +++ b/Accord/Directory.Build.targets @@ -1,6 +1,6 @@ 0 - CA1416,MSB3245,SYSLIB0011,IL2026,IL3000 + CA1416,MSB3245,SYSLIB0011,IL2026,IL3000,CS0436,MSB3245 \ No newline at end of file From 749aa5a7fa4c16b27e3e2f0abe655f3525f0c8c8 Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Mon, 30 Aug 2021 23:00:14 +0100 Subject: [PATCH 08/21] EXTRA NOWARN --- Accord/Directory.Build.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Accord/Directory.Build.targets b/Accord/Directory.Build.targets index bbdef107..892691ec 100644 --- a/Accord/Directory.Build.targets +++ b/Accord/Directory.Build.targets @@ -1,6 +1,6 @@ 0 - CA1416,MSB3245,SYSLIB0011,IL2026,IL3000,CS0436,MSB3245 + CA1416,MSB3245,SYSLIB0011,IL2026,IL3000,CS0436,MSB3245,SYSLIB0003,SYSLIB0006,SYSLIB0014,CS0108 \ No newline at end of file From b5d1c75f7f1241d41a6a4c87317d7748a5cd7d9a Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Mon, 30 Aug 2021 23:28:11 +0100 Subject: [PATCH 09/21] Docs --- README.md | 1 + docs/Technical.md | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b74ed918..22c7322b 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ editing etc. * Baskets can be user-specific, or shared with other users * Server-based deployment, with a web-based front-end UI, so the image library can be accessed via multiple devices without having to copy catalogues or other DBs to local device storage. +* Exclude images from Damselfly scanning by adding a `.nomedia` file in any folder. * Themes * Completely automated background indexing of images, so that the collection is automatically and quickly updated when new images are added or updated diff --git a/docs/Technical.md b/docs/Technical.md index 6f019f3f..b84408a4 100644 --- a/docs/Technical.md +++ b/docs/Technical.md @@ -35,25 +35,30 @@ etc - these will be coming. At startup, Damselfly will run a full index on the folder of images passed in as the only mandatory parameter (or the volume mapped to /pictures if you're running in Docker). The process runs as follows: -1. Damselfly will scan the entire folder tree for any images (currently JPEG, PNG, etc; HEIC and others will be added when Google's image processing -library Skia supports them). This process is normally very quick - on my Synology NAS, around half a million images can be scanned in an hour or two). -During this process, a filesystem iNotify watcher will be set up for each folder in the tree to watch for changes. +1. Damselfly will scan the entire folder tree for any images (currently JPG, PNG, HEIC, TIFF, Webp, BMP and DNG/CR2 RAW). This process is normally +very quick - on my Synology NAS, around half a million images can be scanned in an hour or two). During this process, a filesystem iNotify watcher +will be set up for each folder in the tree to watch for changes. 2. Next, Damselfly will then go back through all of the images, in newest (by file-modified-date) order first, and run the metadata scan. This is more time-consuming, but will pull in all of the metadata such as the resolution, keyword tags, other EXIF data, when the photo was taken, etc. 3. Lastly, the thumbnail generation process will run - generating the small previews used when browsing. This process is CPU-intensive, and can take some time; for my 4TB+ collection of half a million photos, it'll take 5+ days to run on my Synology NAS - processing around 100 images per minute. It'll also hammer the CPU while it runs, so be aware of that. -4. Image/object/face detection will run on the thumbnails after they've been generated. Thumbnails are used (rather than original images) because -image recognition models usually work faster with lower-res images. +4. Image/object/face detection will run on the thumbnails after they've been generated. Thumbnails are used (rather than original +images) because image recognition models usually work faster with lower-res images. Note that AI processing can be extremely CPU-intensive; +for this reason you may want to configure the time-range for processing so that AI processing will only happen during a period when your +NAS isn't being used for much else. Once step #2 finishes, the first full-index is complete. From that point onwards, changes to the image library should be picked up almost instantly and processed quickly; adding a new folder of photos after a day out should only take a few seconds to be indexed, and a couple of minutes for the thumbnails to be generated. +Note that if you want to exclude any folders from Damselfly's indexing process, just create a file named `.nomedia` in the folder, and it will +be skipped. + ## How does Damselfly's Image/Object/Face Recognition Work? -The latest version of Damselfly includes machine-learning functionality to find objects and faces in your images, and tag -them. If you're interested in the technical design/implementation of this feature, there's an article about it +The latest version of Damselfly includes machine-learning functionality to find objects and faces in your images, and tag them. +If you're interested in the technical design/implementation of this feature, there's an article about it [here](https://damselfly.info/face-recognition-in-net-the-good-the-bad-and-the-ugly/). Faces and objects that are recognised will be displayed as special tags, alongside normal keyword-tags, when browsing the From 69e13615d2b6d898ff1f7f9948a051e555674c6e Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Tue, 31 Aug 2021 22:01:27 +0100 Subject: [PATCH 10/21] Startup logging improvements --- Damselfly.Core.Utils/Utils/Logging.cs | 69 +++++----- Damselfly.Web/Program.cs | 173 ++++++++++++++------------ Damselfly.Web/Shared/Config.razor | 6 - Damselfly.Web/Shared/Keywords.razor | 1 + README.md | 2 +- 5 files changed, 138 insertions(+), 113 deletions(-) diff --git a/Damselfly.Core.Utils/Utils/Logging.cs b/Damselfly.Core.Utils/Utils/Logging.cs index 5c8f5081..dad3d6ad 100644 --- a/Damselfly.Core.Utils/Utils/Logging.cs +++ b/Damselfly.Core.Utils/Utils/Logging.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Threading; using Serilog; using Serilog.Core; @@ -45,34 +46,44 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) /// public static Logger InitLogs() { - if (!Directory.Exists(LogFolder)) - Directory.CreateDirectory(LogFolder); - - logLevel.MinimumLevel = LogEventLevel.Information; - - if (Verbose) - logLevel.MinimumLevel = LogEventLevel.Verbose; - - if (Trace) - logLevel.MinimumLevel = LogEventLevel.Debug; - - string logFilePattern = Path.Combine(LogFolder, "Damselfly-.log"); - - logger = new LoggerConfiguration() - .MinimumLevel.ControlledBy(logLevel) - .Enrich.With( new ThreadIDEnricher() ) - .WriteTo.Console(outputTemplate: template) - .WriteTo.File( logFilePattern, - outputTemplate: template, - rollingInterval: RollingInterval.Day, - fileSizeLimitBytes:104857600, - retainedFileCountLimit:10) - .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) - .CreateLogger(); - - logger.Information("=== Damselfly Log Started ==="); - logger.Information("Log folder: {0}", LogFolder); - logger.Information("LogLevel: {0}", logLevel.MinimumLevel); + try + { + if (!Directory.Exists(LogFolder)) + { + Console.WriteLine($"Creating log folder {LogFolder}"); + Directory.CreateDirectory(LogFolder); + } + + logLevel.MinimumLevel = LogEventLevel.Information; + + if (Verbose) + logLevel.MinimumLevel = LogEventLevel.Verbose; + + if (Trace) + logLevel.MinimumLevel = LogEventLevel.Debug; + + string logFilePattern = Path.Combine(LogFolder, "Damselfly-.log"); + + logger = new LoggerConfiguration() + .MinimumLevel.ControlledBy(logLevel) + .Enrich.With(new ThreadIDEnricher()) + .WriteTo.Console(outputTemplate: template) + .WriteTo.File(logFilePattern, + outputTemplate: template, + rollingInterval: RollingInterval.Day, + fileSizeLimitBytes: 104857600, + retainedFileCountLimit: 10) + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .CreateLogger(); + + logger.Information("=== Damselfly Log Started ==="); + logger.Information("Log folder: {0}", LogFolder); + logger.Information("LogLevel: {0}", logLevel.MinimumLevel); + } + catch( Exception ex ) + { + Console.WriteLine($"Unable to initialise logs: {ex}"); + } return logger; } diff --git a/Damselfly.Web/Program.cs b/Damselfly.Web/Program.cs index d867b6c8..029e710a 100644 --- a/Damselfly.Web/Program.cs +++ b/Damselfly.Web/Program.cs @@ -67,85 +67,104 @@ public class DamselflyOptions public static void Main(string[] args) { - Parser.Default.ParseArguments(args).WithParsed( o => - { - Logging.Verbose = o.Verbose; - Logging.Trace = o.Trace; - - if (Directory.Exists(o.SourceDirectory)) - { - if (!Directory.Exists(o.ConfigPath)) - Directory.CreateDirectory(o.ConfigPath); - - Logging.LogFolder = Path.Combine(o.ConfigPath, "logs"); - - Log.Logger = Logging.InitLogs(); - - if (o.ReadOnly) - { - o.NoEnableIndexing = true; - o.NoGenerateThumbnails = true; - } - - // TODO: Do away with static members here. We should pass this - // through to the config service and pick them up via DI - IndexingService.EnableIndexing = ! o.NoEnableIndexing; - IndexingService.RootFolder = o.SourceDirectory; - ThumbnailService.PicturesRoot = o.SourceDirectory; - ThumbnailService.Synology = o.Synology; - ThumbnailService.SetThumbnailRoot(o.ThumbPath); - ThumbnailService.EnableThumbnailGeneration = !o.NoGenerateThumbnails; - - Logging.Log("Startup State:"); - Logging.Log($" Damselfly Ver: {Assembly.GetExecutingAssembly().GetName().Version}"); - Logging.Log($" CLR Ver: {Environment.Version}"); - Logging.Log($" OS: {Environment.OSVersion}"); - Logging.Log($" CPU Arch: {RuntimeInformation.ProcessArchitecture}"); - Logging.Log($" Processor Count: {Environment.ProcessorCount}"); - Logging.Log($" Read-only mode: {o.ReadOnly}"); - Logging.Log($" Synology = {o.Synology}"); - Logging.Log($" Indexing = {!o.NoEnableIndexing}"); - Logging.Log($" ThumbGen = {!o.NoGenerateThumbnails}"); - Logging.Log($" Images Root set as {o.SourceDirectory}"); - - IDataBase dbType = null; - - if (! o.UsePostgresDB ) - { - string dbFolder = Path.Combine(o.ConfigPath, "db"); - - if (!Directory.Exists(dbFolder)) - { - Logging.Log(" Created DB folder: {0}", dbFolder); - Directory.CreateDirectory(dbFolder); - } - - string dbPath = Path.Combine(dbFolder, "damselfly.db"); - dbType = new SqlLiteModel(dbPath); - Logging.Log(" Sqlite Database location: {0}", dbPath); - } - else // Postgres - { - // READ Postgres config json - dbType = PostgresModel.ReadSettings("settings.json"); - Logging.Log(" Postgres Database location: {0}"); - } - - // TODO: https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers?tabs=dotnet-core-cli - BaseDBModel.InitDB(dbType, o.ReadOnly); - - // Make ourselves low-priority. - System.Diagnostics.Process.GetCurrentProcess().PriorityClass = System.Diagnostics.ProcessPriorityClass.Idle; - - StartWebServer(o.Port, args); - - Logging.Log("Shutting down."); - } - else - Logging.Log("Folder {0} did not exist. Exiting.", o.SourceDirectory); - }); + try + { + Parser.Default.ParseArguments(args).WithParsed(o => + { + Startup(o, args); + }); + } + catch( Exception ex ) + { + Console.WriteLine($"Startup exception: {ex}"); + } } + ///

+ /// Process the startup args and initialise the logging. + /// + /// + /// + private static void Startup(DamselflyOptions o, string[] args) + { + Logging.Verbose = o.Verbose; + Logging.Trace = o.Trace; + + if (Directory.Exists(o.SourceDirectory)) + { + if (!Directory.Exists(o.ConfigPath)) + Directory.CreateDirectory(o.ConfigPath); + + Logging.LogFolder = Path.Combine(o.ConfigPath, "logs"); + + Log.Logger = Logging.InitLogs(); + + if (o.ReadOnly) + { + o.NoEnableIndexing = true; + o.NoGenerateThumbnails = true; + } + + // TODO: Do away with static members here. We should pass this + // through to the config service and pick them up via DI + IndexingService.EnableIndexing = !o.NoEnableIndexing; + IndexingService.RootFolder = o.SourceDirectory; + ThumbnailService.PicturesRoot = o.SourceDirectory; + ThumbnailService.Synology = o.Synology; + ThumbnailService.SetThumbnailRoot(o.ThumbPath); + ThumbnailService.EnableThumbnailGeneration = !o.NoGenerateThumbnails; + + Logging.Log("Startup State:"); + Logging.Log($" Damselfly Ver: {Assembly.GetExecutingAssembly().GetName().Version}"); + Logging.Log($" CLR Ver: {Environment.Version}"); + Logging.Log($" OS: {Environment.OSVersion}"); + Logging.Log($" CPU Arch: {RuntimeInformation.ProcessArchitecture}"); + Logging.Log($" Processor Count: {Environment.ProcessorCount}"); + Logging.Log($" Read-only mode: {o.ReadOnly}"); + Logging.Log($" Synology = {o.Synology}"); + Logging.Log($" Indexing = {!o.NoEnableIndexing}"); + Logging.Log($" ThumbGen = {!o.NoGenerateThumbnails}"); + Logging.Log($" Images Root set as {o.SourceDirectory}"); + + IDataBase dbType = null; + + if (!o.UsePostgresDB) + { + string dbFolder = Path.Combine(o.ConfigPath, "db"); + + if (!Directory.Exists(dbFolder)) + { + Logging.Log(" Created DB folder: {0}", dbFolder); + Directory.CreateDirectory(dbFolder); + } + + string dbPath = Path.Combine(dbFolder, "damselfly.db"); + dbType = new SqlLiteModel(dbPath); + Logging.Log(" Sqlite Database location: {0}", dbPath); + } + else // Postgres + { + // READ Postgres config json + dbType = PostgresModel.ReadSettings("settings.json"); + Logging.Log(" Postgres Database location: {0}"); + } + + // TODO: https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers?tabs=dotnet-core-cli + BaseDBModel.InitDB(dbType, o.ReadOnly); + + // Make ourselves low-priority. + System.Diagnostics.Process.GetCurrentProcess().PriorityClass = System.Diagnostics.ProcessPriorityClass.Idle; + + StartWebServer(o.Port, args); + + Logging.Log("Shutting down."); + } + else + { + Console.WriteLine("Folder {0} did not exist. Exiting.", o.SourceDirectory); + } + + } /// /// Main entry point. Creates a bunch of services, and then kicks off /// the webserver, which is a blocking call (since it's the dispatcher diff --git a/Damselfly.Web/Shared/Config.razor b/Damselfly.Web/Shared/Config.razor index 95afea37..b107b6be 100644 --- a/Damselfly.Web/Shared/Config.razor +++ b/Damselfly.Web/Shared/Config.razor @@ -11,7 +11,6 @@ @inject ThemeService themeService @inject UserStatusService statusService @inject WordpressService wpService -@inject TaskService taskScheduler @inject ImageRecognitionService imageService @inject AzureFaceService azureService @@ -327,9 +326,4 @@ base.OnInitialized(); } - - private void RunTask(ScheduledTask task) - { - taskScheduler.EnqueueTaskAsync(task); - } } diff --git a/Damselfly.Web/Shared/Keywords.razor b/Damselfly.Web/Shared/Keywords.razor index f4a6900a..cba04305 100644 --- a/Damselfly.Web/Shared/Keywords.razor +++ b/Damselfly.Web/Shared/Keywords.razor @@ -4,4 +4,5 @@
@code { + } diff --git a/README.md b/README.md index 22c7322b..c7d87837 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ be appreciated! * Synchronisation of local images back to the server * If you have ideas for other features - let me know by [raising an issue](https://github.com/Webreaper/Damselfly/issues)! -# How should we I Damselfly? What's the workflow? +# How should I use Damselfly? What's the workflow? The photos live in the library on the server, but whenever you want to work on a picture (e.g., in Photoshop, Digikam or your editing tool of choice) you use the Damselfly Deskop app to add the images to the basket, and choose Download => Save Locally, to sync the pictures across From 106976ee5ee9cf6758692071c4a26d214f03ec3c Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Tue, 31 Aug 2021 23:24:03 +0100 Subject: [PATCH 11/21] Fix dupe default baskets. --- Damselfly.Core/ScopedServices/BasketService.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Damselfly.Core/ScopedServices/BasketService.cs b/Damselfly.Core/ScopedServices/BasketService.cs index 396ba836..a09b0770 100644 --- a/Damselfly.Core/ScopedServices/BasketService.cs +++ b/Damselfly.Core/ScopedServices/BasketService.cs @@ -22,6 +22,7 @@ public class BasketService private readonly UserStatusService _statusService; private const string s_MyBasket = "My Basket"; + private const string s_DefaultBasket = "default"; public event Action OnBasketChanged; public Basket CurrentBasket { get; set; } @@ -123,7 +124,13 @@ public async Task> GetUserBaskets( AppIdentityUser user ) if( ! myBaskets.Any( x => x.UserId == userId )) { - var newBasketName = (user != null) ? s_MyBasket : "default"; + var newBasketName = (user != null) ? s_MyBasket : s_DefaultBasket; + + if( user == null && myBaskets.Any( x => x.Name.Equals(s_DefaultBasket))) + { + // Don't create another default basket if one already exists. + return myBaskets; + } // Create a default (user) basket if none exists. var userBasket = new Basket { Name = newBasketName, UserId = user?.Id }; From 5480ff6c1d9afb5317c3be102d9a7acbb482868d Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Wed, 1 Sep 2021 07:11:48 +0100 Subject: [PATCH 12/21] Better tooltips --- Damselfly.Web/Shared/Images/GridImage.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Damselfly.Web/Shared/Images/GridImage.razor b/Damselfly.Web/Shared/Images/GridImage.razor index fe9cef60..f1357956 100644 --- a/Damselfly.Web/Shared/Images/GridImage.razor +++ b/Damselfly.Web/Shared/Images/GridImage.razor @@ -51,7 +51,7 @@ else set { _ = SetBasketState(value); } } - private string ImgToolTip => $"{CurrentImage.FileName}\nTaken: {CurrentImage.SortDate.Display()}"; + private string ImgToolTip => $"{CurrentImage.FileName}\nTaken: {CurrentImage.SortDate.Display()}\nFolder: {CurrentImage.Folder.Path}"; private LocalFileExporter FileExporter; string ImageUrl => $"/thumb/{ThumbnailSize}/{CurrentImage.ImageId}"; string SelectStyle { get { return selectionService.IsSelected(CurrentImage) ? "grid-image-selected" : "grid-image-unselected"; } } From fdaf2241cd500d01d95632e6d9725dc2a5121af4 Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Wed, 1 Sep 2021 12:39:58 +0100 Subject: [PATCH 13/21] Fix null-ref --- Damselfly.Web/Shared/Images/ImageProperties.razor | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Damselfly.Web/Shared/Images/ImageProperties.razor b/Damselfly.Web/Shared/Images/ImageProperties.razor index b838edc2..736ed398 100644 --- a/Damselfly.Web/Shared/Images/ImageProperties.razor +++ b/Damselfly.Web/Shared/Images/ImageProperties.razor @@ -23,10 +23,13 @@ else {
-
Add to @basketService.CurrentBasket.Name:
+ @if( basketService.CurrentBasket != null ) + { +
Add to @basketService.CurrentBasket.Name:
+ }
Date Taken: @CurrentImage.SortDate.Display()
Filename: @CurrentImage.FileName
-
Folder: @CurrentImage.Folder.Name
+
Folder: @CurrentImage.Folder.Name
@if (CurrentImage.MetaData != null) { @if (!string.IsNullOrEmpty(CurrentImage.MetaData.Description)) From 3f8cddb5c3daab9d67262cbf5ae35eba0adf1916 Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Fri, 3 Sep 2021 23:58:16 +0100 Subject: [PATCH 14/21] Scaffolding for face page --- .DS_Store | Bin 16388 -> 16388 bytes .../ImageProcessing/ImageMagickProcessor.cs | 11 +++- .../ImageProcessing/ImageSharpProcessor.cs | 26 +++++++-- .../ImageProcessing/SkiaSharpProcessor.cs | 52 ++++++++++++++++-- Damselfly.Core/Interfaces/IImageProcessor.cs | 1 + Damselfly.Core/Models/SideCars/On1Sidecar.cs | 22 ++++---- .../Services/ImageProcessService.cs | 11 ++++ Damselfly.Web/Controllers/ImageController.cs | 43 +++++++++++++++ 8 files changed, 143 insertions(+), 23 deletions(-) diff --git a/.DS_Store b/.DS_Store index cd3f2d2482b87e3ba7a3bcfa50e0572a2eaa1a99..f7c6e2a38831e72200b1ffb13449a967d7422cca 100644 GIT binary patch delta 174 zcmZo^U~Fk%+#n<}xmvbm@^Led&G$ugxHoetC^5oCekz8sP1aFt;V?6|&`~flwwQd* zOm?z>iOOU}4du-any1+|a~fDdlo%PA0hJhcn&(C3)oG4;6`GSV; RA@+RSn?zpVJ=0%euW_e6BKH*+f}GU8M8 zLotkPa*fQn$qE|Eo9#4Dvu)-!uwa|4qj+ZW6=CVg+UA^-q)jIOmGPat**thNyM+yQ HIrRVly2&p7 diff --git a/Damselfly.Core/ImageProcessing/ImageMagickProcessor.cs b/Damselfly.Core/ImageProcessing/ImageMagickProcessor.cs index 51c8b5ba..751197a5 100644 --- a/Damselfly.Core/ImageProcessing/ImageMagickProcessor.cs +++ b/Damselfly.Core/ImageProcessing/ImageMagickProcessor.cs @@ -49,7 +49,7 @@ private void CheckToolStatus() ///
/// Source. /// Sizes. - public Task CreateThumbs(FileInfo source, IDictionary destFiles ) + public async Task CreateThumbs(FileInfo source, IDictionary destFiles ) { // This processor doesn't support hash creation ImageProcessResult result = new ImageProcessResult { ThumbsGenerated = false, ImageHash = string.Empty }; @@ -119,7 +119,7 @@ public Task CreateThumbs(FileInfo source, IDictionary CreateThumbs(FileInfo source, IDictionary image) return result; } + /// + /// Draw rectangles onto a file + /// + /// + /// + /// public static void DrawRects( string path, List rects, string output ) { using var image = Image.Load(path); @@ -115,14 +121,14 @@ public static void DrawRects( string path, List rects, string output ///
/// /// - public Task CreateThumbs(FileInfo source, IDictionary destFiles) + public async Task CreateThumbs(FileInfo source, IDictionary destFiles) { var result = new ImageProcessResult(); Stopwatch load = new Stopwatch("ImageSharpLoad"); // 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); // We've got the image in memory. Create the hash. result.ImageHash = GetHash(image); @@ -160,14 +166,26 @@ public Task CreateThumbs(FileInfo source, IDictionary x.Resize(opts)); - image.Save(dest.FullName); + await image.SaveAsync(dest.FullName); result.ThumbsGenerated = true; } thumbs.Stop(); - return Task.FromResult(result); + return result; + } + + public async Task GetCroppedFile( FileInfo source, int x, int y, int width, int height, FileInfo destFile ) + { + // 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); + + var rect = new Rectangle(x, y, width, height); + image.Mutate(x => x.AutoOrient()); + image.Mutate(x => x.Crop( rect )); + await image.SaveAsync(destFile.FullName); } /// diff --git a/Damselfly.Core/ImageProcessing/SkiaSharpProcessor.cs b/Damselfly.Core/ImageProcessing/SkiaSharpProcessor.cs index bb4af57e..676179aa 100644 --- a/Damselfly.Core/ImageProcessing/SkiaSharpProcessor.cs +++ b/Damselfly.Core/ImageProcessing/SkiaSharpProcessor.cs @@ -139,6 +139,39 @@ public Task CreateThumbs(FileInfo source, IDictionary + /// Crop a file + /// + /// + /// + /// + /// + /// + /// + /// + public Task GetCroppedFile(FileInfo source, int x, int y, int width, int height, FileInfo dest) + { + try + { + using var sourceBitmap = LoadOrientedBitmap(source, width); + + // setup crop rect + var cropRect = new SKRectI(x, y, x + width, y + height); + var cropped = Crop(sourceBitmap, cropRect); + using SKData data = cropped.Encode(SKEncodedImageFormat.Jpeg, 90); + + using (var stream = new FileStream(dest.FullName, FileMode.Create, FileAccess.Write)) + data.SaveTo(stream); + } + catch (Exception ex) + { + Logging.Log($"Exception during Crop processing: {ex.Message}"); + throw; + } + + return Task.CompletedTask; + } + /// /// Crop the image to fit within the dimensions specified. /// @@ -167,15 +200,26 @@ private SKBitmap Crop(SKBitmap original, SKSize maxSize) Bottom = original.Height - cropTopBottom + cropTopBottom / 2 }; - // crop - SKBitmap bitmap = new SKBitmap(cropRect.Width, cropRect.Height); - original.ExtractSubset(bitmap, cropRect); - return bitmap; + return Crop(original, cropRect); } else return original.Copy(); } + /// + /// Crop to a rectangle + /// + /// + /// + /// + private SKBitmap Crop( SKBitmap original, SKRectI cropRect) + { + // crop + SKBitmap bitmap = new SKBitmap(cropRect.Width, cropRect.Height); + original.ExtractSubset(bitmap, cropRect); + return bitmap; + } + /// /// Loads an image from a disk file, decoding for the optimal required /// size so that we don't load the entire image for a smaller target, diff --git a/Damselfly.Core/Interfaces/IImageProcessor.cs b/Damselfly.Core/Interfaces/IImageProcessor.cs index 83fff90c..a2d1f310 100644 --- a/Damselfly.Core/Interfaces/IImageProcessor.cs +++ b/Damselfly.Core/Interfaces/IImageProcessor.cs @@ -20,6 +20,7 @@ public interface IImageProcessor { Task CreateThumbs(FileInfo source, IDictionary destFiles ); void TransformDownloadImage(string input, Stream output, ExportConfig config); + Task GetCroppedFile(FileInfo source, int x, int y, int width, int height, FileInfo destFile); static ICollection SupportedFileExtensions { get; } } } diff --git a/Damselfly.Core/Models/SideCars/On1Sidecar.cs b/Damselfly.Core/Models/SideCars/On1Sidecar.cs index 4e2d1457..71fdffad 100644 --- a/Damselfly.Core/Models/SideCars/On1Sidecar.cs +++ b/Damselfly.Core/Models/SideCars/On1Sidecar.cs @@ -12,7 +12,7 @@ public class MetaData public List Keywords { get; set; } } - public class Guid + public class Photo { public bool guid_locked { get; set; } public MetaData metadata { get; set; } @@ -24,6 +24,8 @@ public class Guid /// public class On1Sidecar { + public Dictionary photos { get; set; } = new Dictionary(); + /// /// Load the on1 sidecar metadata for the image - if it exists. /// @@ -38,19 +40,15 @@ public static MetaData LoadMetadata(FileInfo sidecarPath) string json = File.ReadAllText( sidecarPath.FullName ); // Deserialize. - var list = JsonSerializer.Deserialize>(json); - - if (list.TryGetValue("photos", out var photos)) - { - // Unfortunately, On1 uses the slightly crazy method of a GUID as the field identifier, - // which means we have to deserialise as a dictionary, and then just pick the first kvp. - var guid = JsonSerializer.Deserialize>(photos.ToString()).First(); - - // Now we can deserialise the actual object, and get the metadata. - var data = JsonSerializer.Deserialize(guid.Value.ToString()); + var sideCar = JsonSerializer.Deserialize(json); + if( sideCar != null ) + { Logging.LogVerbose($"Successfully loaded on1 sidecar for {sidecarPath.FullName}"); - result = data.metadata; + var photo = sideCar.photos.Values.FirstOrDefault(); + + if( photo != null ) + result = photo.metadata; } } catch( Exception ex ) diff --git a/Damselfly.Core/Services/ImageProcessService.cs b/Damselfly.Core/Services/ImageProcessService.cs index 6253e5ae..389f3edf 100644 --- a/Damselfly.Core/Services/ImageProcessService.cs +++ b/Damselfly.Core/Services/ImageProcessService.cs @@ -57,6 +57,7 @@ public async Task CreateThumbs(FileInfo source, IDictionary< /// /// /// + /// TODO: Async public void TransformDownloadImage(string input, Stream output, ExportConfig config) { var ext = Path.GetExtension(input); @@ -83,5 +84,15 @@ public bool IsImageFileType(FileInfo filename) // If we have a valid processor, we're good. return processor != null; } + + public async Task GetCroppedFile(FileInfo source, int x, int y, int width, int height, FileInfo destFile) + { + var ext = Path.GetExtension(source.Name); + + var processor = _factory.GetProcessor(ext); + + if (processor != null) + await processor.GetCroppedFile(source, x, y, width, height, destFile); + } } } diff --git a/Damselfly.Web/Controllers/ImageController.cs b/Damselfly.Web/Controllers/ImageController.cs index 4964ece0..5034ef6c 100644 --- a/Damselfly.Web/Controllers/ImageController.cs +++ b/Damselfly.Web/Controllers/ImageController.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Damselfly.Core.ImageProcessing; @@ -10,6 +11,7 @@ using Damselfly.Core.Models; using Damselfly.Core.Utils; using Damselfly.Core.ScopedServices; +using Microsoft.EntityFrameworkCore; namespace Damselfly.Web.Controllers { @@ -177,5 +179,46 @@ public async Task Thumb(string thumbSize, string imageId, Cancell return result; } + + [HttpGet("/face/{faceId}")] + public async Task Face(string faceId, CancellationToken cancel, + [FromServices] ImageProcessService imageProcessor, [FromServices] ThumbnailService thumbService) + { + using var db = new ImageContext(); + + IActionResult result = Redirect("/no-image.png"); + + var query = db.ImageObjects + .Include(x => x.Image) + .ThenInclude(o => o.MetaData) + .Include(x => x.Person) + .OrderByDescending(x => x.Image.MetaData.Width) + .ThenByDescending(x => x.Image.MetaData.Height); + + ImageObject face = null; + + if ( int.TryParse( faceId, out var personId )) + { + face = await query.Where( x => x.Person.PersonId == personId ) + .FirstOrDefaultAsync(); + } + else + { + face = await query.Where(x => x.Person.AzurePersonId == faceId) + .FirstOrDefaultAsync(); + } + + var file = new FileInfo(face.Image.FullPath); + var imagePath = new FileInfo( thumbService.GetThumbPath(file, ThumbSize.Large) ); + var destFile = new FileInfo( "path" ); + + if (!destFile.Exists) + { + await imageProcessor.GetCroppedFile(imagePath, face.RectX, face.RectY, face.RectWidth, face.RectHeight, destFile ); + } + + return result; + } + } } \ No newline at end of file From 40170626bc8fbdbb8c0e0a9ab176b0bfb9df3ca6 Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Sat, 4 Sep 2021 16:10:34 +0100 Subject: [PATCH 15/21] Querystrings for Search --- .DS_Store | Bin 16388 -> 16388 bytes Damselfly.Web/Controllers/ImageController.cs | 2 + Damselfly.Web/Damselfly.Web.csproj | 4 + .../Extensions/QueryStringExtensions.cs | 125 ++++++++++++++++++ Damselfly.Web/Pages/HomePage.razor | 44 ++++-- .../Shared/Images/ImageProperties.razor | 2 +- Damselfly.Web/_Imports.razor | 1 + 7 files changed, 165 insertions(+), 13 deletions(-) create mode 100644 Damselfly.Web/Extensions/QueryStringExtensions.cs diff --git a/.DS_Store b/.DS_Store index f7c6e2a38831e72200b1ffb13449a967d7422cca..b5ec83df097c269c0072529de670f0da5d66d274 100644 GIT binary patch delta 1044 zcmb`_T}YEr7zgm@f86H#b`W1b%IVV0EUg?dE5CwlSYi2*rn4+aoB9T;HO-l3QM1x6 z`XV||Nk!B}MTG^+zAh?=C?SH%pd={hBFt`rC@PAMn-NAA5uKZd|HE?*=kPmSo#^Vs zrFKJBc73zg<8O612Eif>MpLAsj^jMa?5bY2ykS@y#xLb9U6 zt?@K8`Kmmd^*SxK&7-%MH@AA4eEwy6i(gh%%rz!3&c(^`Q^hu;4P2ZsI?8#5d77%Z z6_;Ob9EjCoqPd;VP&L2mTIcaKhC1dfK1* z2LsNU+cdJ$oYO;<(!zzv<#Vss)0~cQj{n&ntssSx$w7rwOtsWZ9khoI(FwXteRPHH z(nESqFKLM0(?|MDU+60gFd`BPVvqz2l97T;%nl$MIVeO4O0fjxaA6f{u^MiyK@*zM z0zW#i13S@;UD%JqID#IW!g*XkFD~LH?%_WA@dPjM219s@VSHdF7ReMA!zQySES+UA z2g_%LtcX>FFV8Ugh?NvQ=2^@wPC5#L!;!^GR_~u(twoC#7nhX&tLs@lSB*0OuN;pF|AJtM@N>GYZ3A%sG;F(yNnnO z`Exke4DwH?1D3=jGf(DDRn;goSU6uEqiDAjMM?VwANZg0{q&fg(JOi<<-a3BdMCh! zbl4?x0i2kR1z0GlD^Q855O>{Lsa=l-c;S=c8>IObY()T_*pA)Ui+v&Y9vsCn9LH&# z!C9O`5Pi6cYq*XZxPu3Hib1^o&9`ifF5!cC6qgeG6stxBxh-#ku%~vLa_qL5`PpKg iC0TI$WHAtD5t+HFfMu5WHnA)i%spWGi#ZX@So#ABMd^V6 delta 1004 zcmbu+Ur19?90&08J#O<}J=XQlbSk#lKuxF2|7Ja`vP?9WIn6NAO$UwUW=<3;nw2jR z;-5niQN8p~qCvF&QIQW}|A=0)KS~7Ii(yc2VfD~ycLqTZQTOF{zrTCVy}xrmg98{G zz?FVYajvV|>k0H)t#hDJ8m%r`l4D|7oWarLXl5;KNVU4UfB}YK)nMqLM zS27DLkd=V!*yr(ehC7BrRxBxeEV%q(vn^M> zb@-fJZrN?GN$Z0Y`u90c-fV?1Y$p`X@ECJ9#Mhs$+1U*t< zKqhjLhkO)Z9jdSyTd)JI*o{4CLnpf6LpSy#*oPnna1_UI9An@(iBmX_i@1bwT*V#S z#XU^n8D8NvX7L^$@dZoxif>dylW7Vy&`erFOKBM`r#4zc>#1EGoFa07U6C3ow8iD@ zX>Hu=?{_#|&HQObWoRLKn^b(^&zQ#Qt<^QG_P;O}6%S=b9+#RGG^D1bXRc;OMJtS? znV&H$TST4kQDdIPXjG_jT~ed9(8!c1fi8y^ms&(bB&@AuOwkB_c_uwI&A>94O_miB zUbu0Sc#Wh?Ns?q<{sCrN@E?$8$vjygpUHQDyaI`WHXBxyVZ9)&$96QrfhNJ)4i`GY zw0;5Gg8&ZUu&_NMbWdOi!x({m6lZW2=fcF}n7|}1;|6Zx7H;D{rtuJu@EA|<9B=Rr z^XlZkDC?F_qE;;Odd;d3O_bwC2Vz69rix_VY8{k{ipngeqQVkBm$QL4Xwv1*K!?}m W_j?JC&kcq` Face(string faceId, CancellationToken cancel, IActionResult result = Redirect("/no-image.png"); + // TODO: Use cache + var query = db.ImageObjects .Include(x => x.Image) .ThenInclude(o => o.MetaData) diff --git a/Damselfly.Web/Damselfly.Web.csproj b/Damselfly.Web/Damselfly.Web.csproj index 7f029d53..6d2494e5 100644 --- a/Damselfly.Web/Damselfly.Web.csproj +++ b/Damselfly.Web/Damselfly.Web.csproj @@ -45,12 +45,15 @@ + + + @@ -82,5 +85,6 @@ + \ No newline at end of file diff --git a/Damselfly.Web/Extensions/QueryStringExtensions.cs b/Damselfly.Web/Extensions/QueryStringExtensions.cs new file mode 100644 index 00000000..352d5e75 --- /dev/null +++ b/Damselfly.Web/Extensions/QueryStringExtensions.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; + +namespace Damselfly.Web.Extensions +{ + /// + /// from https://www.meziantou.net/bind-parameters-from-the-query-string-in-blazor.htm + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class QueryStringParameterAttribute : Attribute + { + public QueryStringParameterAttribute() + { + } + + public QueryStringParameterAttribute(string name) + { + Name = name; + } + + /// Name of the query string parameter. It uses the property name by default. + public string Name { get; } + } + + // Requires Microsoft.AspNetCore.WebUtilities to edit the query string + // + public static class QueryStringParameterExtensions + { + // Apply the values from the query string to the current component + public static void SetParametersFromQueryString(this T component, NavigationManager navigationManager) + where T : ComponentBase + { + if (!Uri.TryCreate(navigationManager.Uri, UriKind.RelativeOrAbsolute, out var uri)) + throw new InvalidOperationException("The current url is not a valid URI. Url: " + navigationManager.Uri); + + // Parse the query string + Dictionary queryString = QueryHelpers.ParseQuery(uri.Query); + + // Enumerate all properties of the component + foreach (var property in GetProperties()) + { + // Get the name of the parameter to read from the query string + var parameterName = GetQueryStringParameterName(property); + if (parameterName == null) + continue; // The property is not decorated by [QueryStringParameterAttribute] + + if (queryString.TryGetValue(parameterName, out var value)) + { + // Convert the value from string to the actual property type + var convertedValue = ConvertValue(value, property.PropertyType); + property.SetValue(component, convertedValue); + } + } + } + + // Apply the values from the component to the query string + public static void UpdateQueryString(this T component, NavigationManager navigationManager) + where T : ComponentBase + { + if (!Uri.TryCreate(navigationManager.Uri, UriKind.RelativeOrAbsolute, out var uri)) + throw new InvalidOperationException("The current url is not a valid URI. Url: " + navigationManager.Uri); + + // Fill the dictionary with the parameters of the component + Dictionary parameters = QueryHelpers.ParseQuery(uri.Query); + foreach (var property in GetProperties()) + { + var parameterName = GetQueryStringParameterName(property); + if (parameterName == null) + continue; + + var value = property.GetValue(component); + if (value is null) + { + parameters.Remove(parameterName); + } + else + { + var convertedValue = ConvertToString(value); + parameters[parameterName] = convertedValue; + } + } + + // Compute the new URL + var newUri = uri.GetComponents(UriComponents.Scheme | UriComponents.Host | UriComponents.Port | UriComponents.Path, UriFormat.UriEscaped); + foreach (var parameter in parameters) + { + foreach (var value in parameter.Value) + { + newUri = QueryHelpers.AddQueryString(newUri, parameter.Key, value); + } + } + + navigationManager.NavigateTo(newUri); + } + + private static object ConvertValue(StringValues value, Type type) + { + return Convert.ChangeType(value[0], type, CultureInfo.InvariantCulture); + } + + private static string ConvertToString(object value) + { + return Convert.ToString(value, CultureInfo.InvariantCulture); + } + + private static PropertyInfo[] GetProperties() + { + return typeof(T).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + } + + private static string GetQueryStringParameterName(PropertyInfo property) + { + var attribute = property.GetCustomAttribute(); + if (attribute == null) + return null; + + return attribute.Name ?? property.Name; + } + } +} diff --git a/Damselfly.Web/Pages/HomePage.razor b/Damselfly.Web/Pages/HomePage.razor index e3bcfb9b..3674ea1b 100644 --- a/Damselfly.Web/Pages/HomePage.razor +++ b/Damselfly.Web/Pages/HomePage.razor @@ -1,5 +1,4 @@ @page "/" -@page "/folder/{FolderId}" @using Damselfly.Web.Data @using Damselfly.Core.Services; @@ -11,7 +10,8 @@ @inject NavigationService navContext @inject SearchService searchService @inject UserStatusService statusService - +@inject NavigationManager navigationManager +
@@ -21,23 +21,43 @@
-@code { [Parameter] - public string FolderId { get; set; } - +@code +{ + [QueryStringParameter] + public int FolderId { get; set; } + + [QueryStringParameter] + public int TagId { get; set; } + + [QueryStringParameter] + public int PersonId { get; set; } + + [QueryStringParameter] + public DateTime Date { get; set; } + protected override async Task OnParametersSetAsync() { - if (int.TryParse(FolderId, out var fID)) + if( FolderId != 0 ) { - var folder = await ImageService.GetFolderAsync(fID); + var folder = await ImageService.GetFolderAsync(FolderId); if (folder != null) { statusService.StatusText = $"Selected folder {folder.Name}"; - searchService.Folder = folder; - } - } - } - + searchService.Folder = folder; + } + } + + //this.UpdateQueryString(navigationManager); + } + + public override Task SetParametersAsync(ParameterView parameters) + { + this.SetParametersFromQueryString(navigationManager); + + return base.SetParametersAsync(parameters); + } + protected override void OnInitialized() { base.OnInitialized(); diff --git a/Damselfly.Web/Shared/Images/ImageProperties.razor b/Damselfly.Web/Shared/Images/ImageProperties.razor index 736ed398..43aeeb4b 100644 --- a/Damselfly.Web/Shared/Images/ImageProperties.razor +++ b/Damselfly.Web/Shared/Images/ImageProperties.razor @@ -106,7 +106,7 @@ @code { private Image theImage; public Image CurrentImage { get { return theImage; } set { theImage = value; } } - public string CurrentFolderLink => $"/folder/{CurrentImage.Folder.FolderId}"; + public string CurrentFolderLink => $"/?folderId={CurrentImage.Folder.FolderId}"; public string Aperture => $"f{CurrentImage.MetaData.FNum}"; public string Exposure => $"{CurrentImage.MetaData.Exposure} {(CurrentImage.MetaData.FlashFired ? "(flash)" : string.Empty)}"; diff --git a/Damselfly.Web/_Imports.razor b/Damselfly.Web/_Imports.razor index d9f59a3f..fcbfa924 100644 --- a/Damselfly.Web/_Imports.razor +++ b/Damselfly.Web/_Imports.razor @@ -20,6 +20,7 @@ @using Damselfly.Core.DbModels; @using Damselfly.Web @using Damselfly.Web.Data +@using Damselfly.Web.Extensions @using Damselfly.Web.Shared @using Damselfly.Web.Components @using Damselfly.Web.Shared.Images From 5c69bf4a41688e0c75f55662e930541e6004643e Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Mon, 6 Sep 2021 09:41:34 +0100 Subject: [PATCH 16/21] Better DB logging --- Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs | 12 ++++++++---- Damselfly.Web/Damselfly.Web.csproj | 2 +- Damselfly.Web/Pages/HomePage.razor | 11 +++++++++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs b/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs index 1b92bbe0..5d2b943a 100644 --- a/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs +++ b/Damselfly.Core.DbModels/DBAbstractions/BaseModel.cs @@ -241,7 +241,10 @@ public override int SaveChanges() /// /// /// The number of entities written to the DB - public async Task SaveChangesAsync(string contextDesc) + public async Task SaveChangesAsync(string contextDesc, + [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", + [System.Runtime.CompilerServices.CallerMemberNameAttribute] string sourceMethod = "", + [System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0 ) { if (ReadOnly) { @@ -274,15 +277,16 @@ public async Task SaveChangesAsync(string contextDesc) { if (ex.Message.Contains("database is locked") && retriesRemaining > 0 ) { - Logging.LogWarning($"Database locked - sleeping for 5s and retying {retriesRemaining}..."); + Logging.LogWarning($"Database locked for {contextDesc} - sleeping for 5s and retying {retriesRemaining}..."); retriesRemaining--; await Task.Delay(5 * 1000); } else { - Logging.LogError("Exception - DB WRITE FAILED: {0}", ex); + Logging.LogError($"Exception - DB WRITE FAILED for {contextDesc}: {ex.Message}" ); + Logging.LogError($" Called from {sourceMethod} line {lineNumber} in {sourceFilePath}."); if (ex.InnerException != null) - Logging.LogError("Exception - DB WRITE FAILED. InnerException: {0}", ex.InnerException.Message); + Logging.LogError(" Exception - DB WRITE FAILED. InnerException: {0}", ex.InnerException.Message); } } diff --git a/Damselfly.Web/Damselfly.Web.csproj b/Damselfly.Web/Damselfly.Web.csproj index 6d2494e5..05a1c963 100644 --- a/Damselfly.Web/Damselfly.Web.csproj +++ b/Damselfly.Web/Damselfly.Web.csproj @@ -72,7 +72,7 @@ - + diff --git a/Damselfly.Web/Pages/HomePage.razor b/Damselfly.Web/Pages/HomePage.razor index 3674ea1b..5451139d 100644 --- a/Damselfly.Web/Pages/HomePage.razor +++ b/Damselfly.Web/Pages/HomePage.razor @@ -48,6 +48,17 @@ } } + if( TagId != 0 ) + { + searchService.TagId = TagId; + } + + if( Date != DateTime.MinValue ) + { + // searchService.MinDate + } + + // Don't need this yet //this.UpdateQueryString(navigationManager); } From 4c0c73cbf42e75095b32ed3d5d0ea36e5a671234 Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Tue, 7 Sep 2021 16:20:38 +0100 Subject: [PATCH 17/21] Deep linking and search breadcrumbs --- Damselfly.Core/Models/ImageContext.cs | 2 +- .../ScopedServices/SearchService.cs | 44 +++++++- Damselfly.Core/Services/IndexingService.cs | 23 ++++ Damselfly.Migrations.Sqlite/SqlLiteModel.cs | 3 + Damselfly.Web/Pages/HomePage.razor | 105 ++++++++++++------ Damselfly.Web/Pages/TagPage.razor | 98 +++++----------- Damselfly.Web/Shared/Images/ImageGrid.razor | 39 ++----- .../Shared/Images/SelectedImages.razor | 10 ++ Damselfly.Web/Shared/Images/TagList.razor | 2 +- Damselfly.Web/Shared/MainLayout.razor | 37 +++--- Damselfly.Web/wwwroot/css/site.css | 8 ++ 11 files changed, 217 insertions(+), 154 deletions(-) diff --git a/Damselfly.Core/Models/ImageContext.cs b/Damselfly.Core/Models/ImageContext.cs index c959b64b..2207d049 100644 --- a/Damselfly.Core/Models/ImageContext.cs +++ b/Damselfly.Core/Models/ImageContext.cs @@ -617,7 +617,7 @@ public enum GroupingType public bool TagsOnly { get; set; } = false; public bool IncludeAITags { get; set; } = true; public int CameraId { get; set; } = -1; - public int TagId { get; set; } = -1; + public Tag Tag { get; set; } = null; public int LensId { get; set; } = -1; public GroupingType Grouping { get; set; } = GroupingType.None; public SortOrderType SortOrder { get; set; } = SortOrderType.Descending; diff --git a/Damselfly.Core/ScopedServices/SearchService.cs b/Damselfly.Core/ScopedServices/SearchService.cs index 414fdefe..3799043a 100644 --- a/Damselfly.Core/ScopedServices/SearchService.cs +++ b/Damselfly.Core/ScopedServices/SearchService.cs @@ -54,7 +54,7 @@ public void NotifyStateChanged() public bool TagsOnly { get { return query.TagsOnly; } set { if (query.TagsOnly != value) { query.TagsOnly = value; QueryChanged(); } } } public bool IncludeAITags { get { return query.IncludeAITags; } set { if (query.IncludeAITags != value) { query.IncludeAITags = value; QueryChanged(); } } } public int CameraId { get { return query.CameraId; } set { if (query.CameraId != value) { query.CameraId = value; QueryChanged(); } } } - public int TagId { get { return query.TagId; } set { if (query.TagId != value) { query.TagId = value; QueryChanged(); } } } + public Tag Tag { get { return query.Tag; } set { if (query.Tag != value) { query.Tag = value; QueryChanged(); } } } public int LensId { get { return query.LensId; } set { if (query.LensId != value) { query.LensId = value; QueryChanged(); } } } public GroupingType Grouping { get { return query.Grouping; } set { if (query.Grouping != value) { query.Grouping = value; QueryChanged(); } } } public SortOrderType SortOrder { get { return query.SortOrder; } set { if (query.SortOrder != value) { query.SortOrder = value; QueryChanged(); } } } @@ -134,10 +134,10 @@ private async Task LoadMoreData(int first, int count) images = images.Include(x => x.Folder); - if ( query.TagId != -1 ) + if ( query.Tag != null ) { - var tagImages = images.Where(x => x.ImageTags.Any(y => y.TagId == query.TagId)); - var objImages = images.Where(x => x.ImageObjects.Any(y => y.TagId == query.TagId)); + var tagImages = images.Where(x => x.ImageTags.Any(y => y.TagId == query.Tag.TagId)); + var objImages = images.Where(x => x.ImageObjects.Any(y => y.TagId == query.Tag.TagId)); images = tagImages.Union(objImages); } @@ -255,5 +255,41 @@ public async Task GetQueryImagesAsync(int first, int count) return SearchResults.Skip(first).Take(count).ToArray(); } + + public string SearchBreadcrumbs + { + get + { + var hints = new List(); + + if (!string.IsNullOrEmpty(SearchText)) + hints.Add($"Text: {SearchText}"); + + if (Folder != null) + hints.Add($"Folder: {Folder.Name}"); + + if (Tag != null) + hints.Add($"Tag: {Tag.Keyword}"); + + string dateRange = string.Empty; + if (MinDate.Date != DateTime.MinValue.Date) + dateRange = $"{MinDate:dd-MMM-yyyy}"; + + if (MaxDate.Date != DateTime.MaxValue.Date && + MaxDate.Date != MinDate.Date) + { + if (!string.IsNullOrEmpty(dateRange)) + dateRange += " - "; + dateRange += $"{MaxDate:dd-MMM-yyyy}"; + } + + if (!string.IsNullOrEmpty(dateRange)) + hints.Add($"Date: {dateRange}"); + + // TODO: Need camera here. + + return string.Join(", ", hints); + } + } } } diff --git a/Damselfly.Core/Services/IndexingService.cs b/Damselfly.Core/Services/IndexingService.cs index 303b40c7..ce23ff93 100644 --- a/Damselfly.Core/Services/IndexingService.cs +++ b/Damselfly.Core/Services/IndexingService.cs @@ -386,6 +386,29 @@ private void LoadTagCache(bool force = false) Logging.LogError($"Unexpected exception loading tag cache: {ex.Message}"); } } + + /// + /// Return a tag by its ID. + /// TODO: Is this faster, or slower than a DB query, given it means iterating + /// a collection of, say, 10,000 tags. Probably faster, but perhaps we should + /// maintain a dict of ID => tag? + /// + /// + /// + public Models.Tag GetTag( int tagId ) + { + var tag = _tagCache.Values.Where(x => x.TagId == tagId).FirstOrDefault(); + + return tag; + } + + public Models.Tag GetTag(string keyword) + { + // TODO: Should we make the tag-cache key case-insensitive? What would happen?! + var tag = _tagCache.Values.Where(x => x.Keyword.Equals( keyword, StringComparison.OrdinalIgnoreCase) ).FirstOrDefault(); + + return tag; + } #endregion public async Task> CreateTagsFromStrings(IEnumerable tags) diff --git a/Damselfly.Migrations.Sqlite/SqlLiteModel.cs b/Damselfly.Migrations.Sqlite/SqlLiteModel.cs index 4459b580..486e1a0b 100644 --- a/Damselfly.Migrations.Sqlite/SqlLiteModel.cs +++ b/Damselfly.Migrations.Sqlite/SqlLiteModel.cs @@ -68,6 +68,9 @@ private void ExecutePragma(BaseDBModel db, string pragmaCommand) /// private void IncreasePerformance(BaseDBModel db) { + // Increase the timeout from the default (which I think is 30s) + // To help concurrency. + db.Database.SetCommandTimeout(60); // Enable journal mode - this will also improve // concurrent acces ExecutePragma(db, "PRAGMA journal_mode=WAL;"); diff --git a/Damselfly.Web/Pages/HomePage.razor b/Damselfly.Web/Pages/HomePage.razor index 5451139d..c611de71 100644 --- a/Damselfly.Web/Pages/HomePage.razor +++ b/Damselfly.Web/Pages/HomePage.razor @@ -3,78 +3,118 @@ @using Damselfly.Web.Data @using Damselfly.Core.Services; @using Damselfly.Web.Shared.Images; +@implements IDisposable @inject ImageService imageService @inject ThumbnailService thumbService @inject ViewDataService ViewDataService +@inject IndexingService indexingService @inject NavigationService navContext @inject SearchService searchService @inject UserStatusService statusService @inject NavigationManager navigationManager - +
- - - - - - + +
@code -{ - [QueryStringParameter] +{ + [QueryStringParameter] + public string S { get; set; } + + [QueryStringParameter] public int FolderId { get; set; } [QueryStringParameter] public int TagId { get; set; } [QueryStringParameter] - public int PersonId { get; set; } + public string Tag { get; set; } [QueryStringParameter] - public DateTime Date { get; set; } + public int PersonId { get; set; } + [QueryStringParameter] + public DateTime Date { get; set; } + protected override async Task OnParametersSetAsync() - { - if( FolderId != 0 ) - { - var folder = await ImageService.GetFolderAsync(FolderId); + { + await ApplyQueryParams(); + } - if (folder != null) - { - statusService.StatusText = $"Selected folder {folder.Name}"; + private async Task ApplyQueryParams() + { + if( ! string.IsNullOrEmpty( S ) ) + { + searchService.SearchText = S; + } + + if (FolderId != 0) + { + var folder = await ImageService.GetFolderAsync(FolderId); + + if (folder != null) + { + statusService.StatusText = $"Selected folder {folder.Name}"; searchService.Folder = folder; } } - if( TagId != 0 ) + if (TagId != 0) { - searchService.TagId = TagId; - } + var tag = indexingService.GetTag(TagId); - if( Date != DateTime.MinValue ) + if (tag != null) + searchService.Tag = tag; + } + else if( ! string.IsNullOrEmpty( Tag )) { - // searchService.MinDate + var tag = indexingService.GetTag(Tag); + + if (tag != null) + searchService.Tag = tag; } + if (Date != DateTime.MinValue) + { + // searchService.MinDate + } + // Don't need this yet //this.UpdateQueryString(navigationManager); } - - public override Task SetParametersAsync(ParameterView parameters) - { - this.SetParametersFromQueryString(navigationManager); - - return base.SetParametersAsync(parameters); - } - + + public override Task SetParametersAsync(ParameterView parameters) + { + this.SetParametersFromQueryString(navigationManager); + + return base.SetParametersAsync(parameters); + } + protected override void OnInitialized() { base.OnInitialized(); navContext.CurrentImage = null; ViewDataService.SetSideBarState(new ViewDataService.SideBarState { ShowFolderList = true, ShowBasket = true, ShowTags = true }); + + navigationManager.LocationChanged += HandleLocationChanged; + + } + + void HandleLocationChanged(object sender, LocationChangedEventArgs e) + { + this.SetParametersFromQueryString(navigationManager); + StateHasChanged(); + + _ = ApplyQueryParams(); + } + + public void Dispose() + { + navigationManager.LocationChanged -= HandleLocationChanged; } // TODO: Don't think we need this @@ -84,4 +124,5 @@ { Logging.Log($"Checking for update: {clientVersion}"); - } } \ No newline at end of file + } +} \ No newline at end of file diff --git a/Damselfly.Web/Pages/TagPage.razor b/Damselfly.Web/Pages/TagPage.razor index cd373a94..a0ce794b 100644 --- a/Damselfly.Web/Pages/TagPage.razor +++ b/Damselfly.Web/Pages/TagPage.razor @@ -1,9 +1,5 @@ @page "/tags" -@page "/tag/{TagName}" -@using Damselfly.Web.Data -@using System.IO; -@using Damselfly.Web.Shared.Images; @using Damselfly.Core.Services; @using Damselfly.Core.Models; @@ -12,65 +8,47 @@ @inject IndexingService indexingService
- @if (images == null) + @if (Tags == null) { - if (Tags == null) - { -

Loading tags...

- } - else - { - - - Keyword Tags - - - - - ID - Keyword - Favourite - Date Created - - - @context.TagId - - @context.Keyword - - @context.Favourite - @context.TimeStamp - - - - - - } +

Loading tags...

} else { -
-

Tag: @TagName

-
-
- @foreach (var image in images) - { -
- -
- } -
+ + + Keyword Tags + + + + + ID + Keyword + Favourite + Date Created + + + @context.TagId + + @context.Keyword + + @context.Favourite + @context.TimeStamp + + + + + }
@code { [Parameter] public string TagName { get; set; } - Image[] images; Tag selectedTag; string searchText; - private string TagLink( Tag tag ) => $"/tag/{tag.Keyword}"; + private string TagLink( Tag tag ) => $"/?tagid={tag.TagId}"; private IEnumerable Tags { @@ -96,24 +74,4 @@ ViewDataService.SetSideBarState(new ViewDataService.SideBarState { ShowBasket = true, ShowTags = true }); } } - - protected override async Task OnParametersSetAsync() - { - if (!string.IsNullOrEmpty(TagName)) - { - images = await ImageService.GetTagImagesAsync(TagName); - StateHasChanged(); - } - - StateHasChanged(); - } - - protected override void OnInitialized() - { - if (string.IsNullOrEmpty(TagName)) - { - ViewDataService.SetSideBarState(new ViewDataService.SideBarState { ShowBasket = true }); - images = null; - } - } } diff --git a/Damselfly.Web/Shared/Images/ImageGrid.razor b/Damselfly.Web/Shared/Images/ImageGrid.razor index be282e06..8475487a 100644 --- a/Damselfly.Web/Shared/Images/ImageGrid.razor +++ b/Damselfly.Web/Shared/Images/ImageGrid.razor @@ -12,26 +12,19 @@ @inherits ImageGridBase @using Damselfly.Web.Shared -@using Damselfly.Web.Shared.Images @using Damselfly.Core.ImageProcessing @using Damselfly.Core.Utils.Constants
-
- -
@foreach (var choice in new[] { ThumbSize.Small, ThumbSize.Medium, ThumbSize.Large }) { } @@ -43,8 +36,8 @@ { } @@ -57,13 +50,16 @@ { }
+
+ +
@@ -165,14 +161,6 @@ // Todo - save an image to local storage } - async Task AddSelectedToBasket() - { - var selected = selectionService.Selection.ToList(); - await basketService.SetBasketState(selected, true); - - statusService.StatusText = $"{selected.Count()} images added to the basket"; - } - async Task AddGroupToBasket(ImageGrouping grouping) { await basketService.SetBasketState(grouping.Images, true); @@ -302,12 +290,7 @@ { get { - if (searchService.Folder != null) - { - return $"No images were found in folder '{searchService.Folder.Name}' that match the current filter."; - } - - return "No images were found that match the current filter."; + return $"No images were found that match the filter:\n{searchService.SearchBreadcrumbs}"; } } @@ -322,6 +305,8 @@ gridImages.Clear(); endOfImages = false; + _ = InvokeAsync( StateHasChanged ); + _ = LoadData(imagesPerPage); } diff --git a/Damselfly.Web/Shared/Images/SelectedImages.razor b/Damselfly.Web/Shared/Images/SelectedImages.razor index b124f80b..30c9a601 100644 --- a/Damselfly.Web/Shared/Images/SelectedImages.razor +++ b/Damselfly.Web/Shared/Images/SelectedImages.razor @@ -45,6 +45,7 @@ else
+ @@ -62,6 +63,7 @@ else public bool ShowTags { get; set; } = true; public bool BasketHasImages => gridImages.Any(); + public bool ImagesSelected => selectionService.Selection.Any(); private void UploadToWordPress() { @@ -84,6 +86,14 @@ else } + async Task AddSelectedToBasket() + { + var selected = selectionService.Selection.ToList(); + await basketService.SetBasketState(selected, true); + + statusService.StatusText = $"{selected.Count()} images added to the basket"; + } + private void ShowDownloads() { try diff --git a/Damselfly.Web/Shared/Images/TagList.razor b/Damselfly.Web/Shared/Images/TagList.razor index 6857f3cc..b6de8eab 100644 --- a/Damselfly.Web/Shared/Images/TagList.razor +++ b/Damselfly.Web/Shared/Images/TagList.razor @@ -328,7 +328,7 @@ contextMenuService.Close(); switch (args.Value) { - case 0: NavigationManager.NavigateTo("/tag/" + tag.Keyword); break; + case 0: NavigationManager.NavigateTo($"/?tagid={tag.TagId}"); break; case 1: await iptcService.ToggleFavourite(tag); break; case 2: QuickAddTag(tag); break; case 3: DeleteTag(tag); break; diff --git a/Damselfly.Web/Shared/MainLayout.razor b/Damselfly.Web/Shared/MainLayout.razor index a2159821..85c68c1b 100644 --- a/Damselfly.Web/Shared/MainLayout.razor +++ b/Damselfly.Web/Shared/MainLayout.razor @@ -4,25 +4,24 @@ - - - -
-
- - - - - @Body - -
- - - -
- -
- + + + + + +
+
+ + @Body +
+ +
+
+
+ +

An error occurred. Please check the Damselfly logs and reload the page.

+
+
@code{ } diff --git a/Damselfly.Web/wwwroot/css/site.css b/Damselfly.Web/wwwroot/css/site.css index 00acd2c9..5c1fe4a6 100644 --- a/Damselfly.Web/wwwroot/css/site.css +++ b/Damselfly.Web/wwwroot/css/site.css @@ -674,6 +674,14 @@ f align-items: center; } +.damselfly-searchhint { + flex: 1 1 200px; + margin: 3px; + display: flex; + flex-flow: row; + align-items: center; +} + .damselfly-browsetoollabel { margin: 2px; } From 4663c48676e71407a05020ff753787d33d396358 Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Tue, 7 Sep 2021 17:06:08 +0100 Subject: [PATCH 18/21] Log loading --- Damselfly.Web/Pages/LogsPage.razor | 107 ++++++++++++++++++----------- 1 file changed, 68 insertions(+), 39 deletions(-) diff --git a/Damselfly.Web/Pages/LogsPage.razor b/Damselfly.Web/Pages/LogsPage.razor index b5a5066e..7cb53b73 100644 --- a/Damselfly.Web/Pages/LogsPage.razor +++ b/Damselfly.Web/Pages/LogsPage.razor @@ -1,36 +1,44 @@ @page "/logs" @inject ViewDataService ViewDataService +@inject UserStatusService statusService
-
-

@LogFileName

-
- - - - - - - - - - - - @foreach (var line in logLines) - { - - - - - - - } - - -
TimestampLog LevelEntryThread
@line.date@line.level@line.entry@line.thread
+ @if (logLines == null || !logLines.Any()) + { +

Loading log entries....

+ } + else + { +
+

@LogFileName

+
+ + + + + + + + + + + + @foreach (var line in logLines) + { + + + + + + + } + + +
TimestampLog LevelEntryThread
@line.date@line.level@line.entry@line.thread
+
-
+ }
@code { @@ -46,20 +54,23 @@ List logLines = new List(); private string LogFileName { get; set; } - - protected override async Task OnAfterRenderAsync(bool firstRender) + protected override void OnAfterRender(bool firstRender) { if (firstRender) { ViewDataService.SetSideBarState(new ViewDataService.SideBarState { ShowLogs = true }); - var lines = await GetLogLines(); - logLines.AddRange(lines); - StateHasChanged(); + _ = GetLogLines(); } } - private Task GetLogLines() + private async Task DownloadLogFile() + { + // TODO: Download log file here + await Task.Delay(500); + } + + private async Task GetLogLines() { LogEntry[] result = new LogEntry[0]; @@ -72,13 +83,31 @@ { LogFileName = file.Name; - result = File.ReadAllLines(file.FullName) - .Reverse() - .Select(x => CreateLogEntry(x)) - .ToArray(); - } + int page = 0, pageSize = 100; + var lines = File.ReadLines(file.FullName); - return Task.FromResult(result); + while (true) + { + var pageLines = lines.Skip(page * pageSize).Take(pageSize).Reverse(); + + if (pageLines.Any()) + { + var logPage = pageLines + .Select(x => CreateLogEntry(x)) + .ToArray(); + + logLines.AddRange(logPage); + + page++; + + await InvokeAsync(StateHasChanged); + await Task.Delay(100); + statusService.StatusText = $"Loaded {logLines.Count()} log lines..."; + } + else + break; + } + } } // TODO: Use a regex here From c3337282755a5456bd3db28fae1452f40e06e93d Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Tue, 7 Sep 2021 23:20:20 +0100 Subject: [PATCH 19/21] Date fixes --- Damselfly.Core/Models/ImageContext.cs | 4 ++-- .../ScopedServices/SearchService.cs | 20 ++++++++++--------- Damselfly.Web/Shared/DatePickerEx.razor | 2 +- Damselfly.Web/Shared/SearchBar.razor | 5 ++++- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Damselfly.Core/Models/ImageContext.cs b/Damselfly.Core/Models/ImageContext.cs index 2207d049..fbad3f44 100644 --- a/Damselfly.Core/Models/ImageContext.cs +++ b/Damselfly.Core/Models/ImageContext.cs @@ -609,8 +609,8 @@ public enum GroupingType }; public string SearchText { get; set; } - public DateTime MaxDate { get; set; } = DateTime.MaxValue; - public DateTime MinDate { get; set; } = DateTime.MinValue; + public DateTime? MaxDate { get; set; } = null; + public DateTime? MinDate { get; set; } = null; public ulong MaxSizeKB { get; set; } = ulong.MaxValue; public ulong MinSizeKB { get; set; } = ulong.MinValue; public Folder Folder { get; set; } = null; diff --git a/Damselfly.Core/ScopedServices/SearchService.cs b/Damselfly.Core/ScopedServices/SearchService.cs index 3799043a..50fbf932 100644 --- a/Damselfly.Core/ScopedServices/SearchService.cs +++ b/Damselfly.Core/ScopedServices/SearchService.cs @@ -46,8 +46,8 @@ public void NotifyStateChanged() public event Action OnChange; public string SearchText { get { return query.SearchText; } set { if (query.SearchText != value.Trim() ) { query.SearchText = value.Trim(); QueryChanged(); } } } - public DateTime MaxDate { get { return query.MaxDate; } set { if (query.MaxDate != value) { query.MaxDate = value; QueryChanged(); } } } - public DateTime MinDate { get { return query.MinDate; } set { if (query.MinDate != value) { query.MinDate = value; QueryChanged(); } } } + public DateTime? MaxDate { get { return query.MaxDate; } set { if (query.MaxDate != value) { query.MaxDate = value; QueryChanged(); } } } + public DateTime? MinDate { get { return query.MinDate; } set { if (query.MinDate != value) { query.MinDate = value; QueryChanged(); } } } public ulong MaxSizeKB { get { return query.MaxSizeKB; } set { if (query.MaxSizeKB != value) { query.MaxSizeKB = value; QueryChanged(); } } } public ulong MinSizeKB { get { return query.MinSizeKB; } set { if (query.MinSizeKB != value) { query.MinSizeKB = value; QueryChanged(); } } } public Folder Folder { get { return query.Folder; } set { if (query.Folder != value) { query.Folder = value; QueryChanged(); } } } @@ -59,7 +59,7 @@ public void NotifyStateChanged() public GroupingType Grouping { get { return query.Grouping; } set { if (query.Grouping != value) { query.Grouping = value; QueryChanged(); } } } public SortOrderType SortOrder { get { return query.SortOrder; } set { if (query.SortOrder != value) { query.SortOrder = value; QueryChanged(); } } } - public void SetDateRange( DateTime min, DateTime max ) + public void SetDateRange( DateTime? min, DateTime? max ) { if (query.MinDate != min || query.MaxDate != max) { @@ -179,12 +179,14 @@ private async Task LoadMoreData(int first, int count) throw new ArgumentException("Unexpected grouping type."); } - if (query.MinDate > DateTime.MinValue || query.MaxDate < DateTime.MaxValue) + if (query.MinDate.HasValue || query.MaxDate.HasValue) { + var minDate = query.MinDate.HasValue ? query.MinDate : DateTime.MinValue; + var maxDate = query.MaxDate.HasValue ? query.MaxDate : DateTime.MaxValue; // Always filter by date - because if there's no filter // set then they'll be set to min/max date. - images = images.Where(x => x.SortDate >= query.MinDate && - x.SortDate <= query.MaxDate); + images = images.Where(x => x.SortDate >= minDate && + x.SortDate <= maxDate); } if( query.MinSizeKB > ulong.MinValue ) @@ -272,11 +274,11 @@ public string SearchBreadcrumbs hints.Add($"Tag: {Tag.Keyword}"); string dateRange = string.Empty; - if (MinDate.Date != DateTime.MinValue.Date) + if (MinDate.HasValue) dateRange = $"{MinDate:dd-MMM-yyyy}"; - if (MaxDate.Date != DateTime.MaxValue.Date && - MaxDate.Date != MinDate.Date) + if (MaxDate.HasValue && + (! MinDate.HasValue || MaxDate.Value.Date != MinDate.Value.Date)) { if (!string.IsNullOrEmpty(dateRange)) dateRange += " - "; diff --git a/Damselfly.Web/Shared/DatePickerEx.razor b/Damselfly.Web/Shared/DatePickerEx.razor index b09f0d65..eb5e1f89 100644 --- a/Damselfly.Web/Shared/DatePickerEx.razor +++ b/Damselfly.Web/Shared/DatePickerEx.razor @@ -58,5 +58,5 @@ // Close the picker picker.Close(); // Fire OnRangeSelectEvent - picker.OnRangeSelect.InvokeAsync(new BlazorDateRangePicker.DateRange { Start = DateTime.MinValue, End = DateTime.MaxValue } ); + picker.OnRangeSelect.InvokeAsync(null); }} diff --git a/Damselfly.Web/Shared/SearchBar.razor b/Damselfly.Web/Shared/SearchBar.razor index 03752625..0dfe0836 100644 --- a/Damselfly.Web/Shared/SearchBar.razor +++ b/Damselfly.Web/Shared/SearchBar.razor @@ -38,7 +38,10 @@ public void OnRangeSelect(BlazorDateRangePicker.DateRange range) { - searchService.SetDateRange(range.Start.Date, range.End.Date); + if( range != null ) + searchService.SetDateRange(range.Start.Date, range.End.Date); + else + searchService.SetDateRange(null, null); } private void KeyChangedMinSize(string newText) From 1cbb72994f6335a5d8bc1dcb01c1b37f17eee277 Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Wed, 8 Sep 2021 08:19:25 +0100 Subject: [PATCH 20/21] Update DatePickerEx.razor --- Damselfly.Web/Shared/DatePickerEx.razor | 1 + 1 file changed, 1 insertion(+) diff --git a/Damselfly.Web/Shared/DatePickerEx.razor b/Damselfly.Web/Shared/DatePickerEx.razor index eb5e1f89..bcccf293 100644 --- a/Damselfly.Web/Shared/DatePickerEx.razor +++ b/Damselfly.Web/Shared/DatePickerEx.razor @@ -58,5 +58,6 @@ // Close the picker picker.Close(); // Fire OnRangeSelectEvent + picker.Reset(); picker.OnRangeSelect.InvokeAsync(null); }} From 1fe5008eb1a58ed702fa9e0c64e4b07569ef8141 Mon Sep 17 00:00:00 2001 From: Mark Otway Date: Wed, 8 Sep 2021 08:20:22 +0100 Subject: [PATCH 21/21] Version bump --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index ccbccc3d..c043eea7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.0 +2.2.1