diff --git a/.DS_Store b/.DS_Store index cd8b372c..12a692b0 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Accord/Accord.Imaging/Accord.Imaging.csproj b/Accord/Accord.Imaging/Accord.Imaging.csproj index f906b353..1a2593e0 100644 --- a/Accord/Accord.Imaging/Accord.Imaging.csproj +++ b/Accord/Accord.Imaging/Accord.Imaging.csproj @@ -30,7 +30,7 @@ - + diff --git a/Damselfly.Core.DbModels/Damselfly.Core.DbModels.csproj b/Damselfly.Core.DbModels/Damselfly.Core.DbModels.csproj index 25df6ef9..6797c751 100644 --- a/Damselfly.Core.DbModels/Damselfly.Core.DbModels.csproj +++ b/Damselfly.Core.DbModels/Damselfly.Core.DbModels.csproj @@ -12,7 +12,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/Damselfly.Core.ImageProcessing/ImageMagickProcessor.cs b/Damselfly.Core.ImageProcessing/ImageMagickProcessor.cs index e15ec8fc..783bb81d 100644 --- a/Damselfly.Core.ImageProcessing/ImageMagickProcessor.cs +++ b/Damselfly.Core.ImageProcessing/ImageMagickProcessor.cs @@ -182,7 +182,7 @@ private static void Process_OutputDataReceived(object sender, DataReceivedEventA Logging.LogVerbose(e.Data); } - public void TransformDownloadImage(string input, Stream output, IExportSettings config) + public Task TransformDownloadImage(string input, Stream output, IExportSettings config) { throw new NotImplementedException(); } diff --git a/Damselfly.Core.ImageProcessing/ImageSharpProcessor.cs b/Damselfly.Core.ImageProcessing/ImageSharpProcessor.cs index 3f884850..69bb2e6f 100644 --- a/Damselfly.Core.ImageProcessing/ImageSharpProcessor.cs +++ b/Damselfly.Core.ImageProcessing/ImageSharpProcessor.cs @@ -223,7 +223,7 @@ public async Task GetCroppedFile( FileInfo source, int x, int y, int width, int /// /// /// - public void TransformDownloadImage(string input, Stream output, IExportSettings config) + public async Task TransformDownloadImage(string input, Stream output, IExportSettings config) { Logging.Log($" Running image transform for Watermark: {config.WatermarkText}"); @@ -257,7 +257,7 @@ public void TransformDownloadImage(string input, Stream output, IExportSettings img.Mutate(context => ApplyWaterMark(context, font, config.WatermarkText, Color.White)); } - img.Save(output, fmt); + await img.SaveAsync(output, fmt); } diff --git a/Damselfly.Core.ImageProcessing/SkiaSharpProcessor.cs b/Damselfly.Core.ImageProcessing/SkiaSharpProcessor.cs index 889ab126..2a4feea8 100644 --- a/Damselfly.Core.ImageProcessing/SkiaSharpProcessor.cs +++ b/Damselfly.Core.ImageProcessing/SkiaSharpProcessor.cs @@ -365,13 +365,25 @@ private static SKBitmap AutoOrient(SKBitmap original, SKEncodedOrigin origin) return rotated; } + /// + /// Async wrapper - note that Skia Sharp doesn't support Async yet. + /// + /// + /// + /// + /// + public async Task TransformDownloadImage(string input, Stream output, IExportSettings config) + { + await Task.Run(() => TransformDownloadImageSync(input, output, config)); + } + /// /// Transform the images ready for download, optionally adding a watermark. /// /// /// /// - public void TransformDownloadImage(string input, Stream output, IExportSettings config) + public void TransformDownloadImageSync(string input, Stream output, IExportSettings config) { using SKImage img = SKImage.FromEncodedData(input); using var bitmap = SKBitmap.FromImage(img); diff --git a/Damselfly.Core.Interfaces/IImageProcessor.cs b/Damselfly.Core.Interfaces/IImageProcessor.cs index b6ea8e99..4eb047f9 100644 --- a/Damselfly.Core.Interfaces/IImageProcessor.cs +++ b/Damselfly.Core.Interfaces/IImageProcessor.cs @@ -22,7 +22,7 @@ public interface IImageProcessor { Task CreateThumbs(FileInfo source, IDictionary destFiles ); Task GetCroppedFile(FileInfo source, int x, int y, int width, int height, FileInfo destFile); - void TransformDownloadImage(string input, Stream output, IExportSettings exportConfig); + Task TransformDownloadImage(string input, Stream output, IExportSettings exportConfig); static ICollection SupportedFileExtensions { get; } } diff --git a/Damselfly.Core.Utils/Constants/ConfigSettings.cs b/Damselfly.Core.Utils/Constants/ConfigSettings.cs index bd91e979..7822a3ec 100644 --- a/Damselfly.Core.Utils/Constants/ConfigSettings.cs +++ b/Damselfly.Core.Utils/Constants/ConfigSettings.cs @@ -10,6 +10,8 @@ public class ConfigSettings public const string ShowZoom = "ShowZoom"; public const string ShowObjects = "ShowObjects"; public const string SelectedBasketId = "SelectedBasketId"; + public const string SideBarWidth = "SideBarWidth"; + public const string SideBarCollapsed = "SideBarCollapsed"; public const string WordpressURL = "WordpressURL"; public const string WordpressUser = "WordpressUser"; diff --git a/Damselfly.Core.Utils/Constants/ExportTypes.cs b/Damselfly.Core.Utils/Constants/ExportTypes.cs index cd66d812..61ef998d 100644 --- a/Damselfly.Core.Utils/Constants/ExportTypes.cs +++ b/Damselfly.Core.Utils/Constants/ExportTypes.cs @@ -17,6 +17,7 @@ public enum ExportSize Large = 2, Medium = 3, Small = 4, + ExtraLarge = 5 }; } diff --git a/Damselfly.Core.Utils/Damselfly.Core.Utils.csproj b/Damselfly.Core.Utils/Damselfly.Core.Utils.csproj index 75dd8604..798e435e 100644 --- a/Damselfly.Core.Utils/Damselfly.Core.Utils.csproj +++ b/Damselfly.Core.Utils/Damselfly.Core.Utils.csproj @@ -1,6 +1,6 @@ - + diff --git a/Damselfly.Core/Damselfly.Core.csproj b/Damselfly.Core/Damselfly.Core.csproj index acef1469..1e977d83 100644 --- a/Damselfly.Core/Damselfly.Core.csproj +++ b/Damselfly.Core/Damselfly.Core.csproj @@ -23,18 +23,18 @@ - - + + - - + + - + diff --git a/Damselfly.Core/Models/ImageContext.cs b/Damselfly.Core/Models/ImageContext.cs index c549da98..6f906d10 100644 --- a/Damselfly.Core/Models/ImageContext.cs +++ b/Damselfly.Core/Models/ImageContext.cs @@ -676,19 +676,19 @@ public class ExportConfig : IExportSettings public bool KeepFolders { get; set; } public string WatermarkText { get; set; } - public int MaxImageSize - { - get + public int MaxImageSize => MaxSize(Size); + public string SizeDesc() => SizeDesc(Size); + + public static string SizeDesc(ExportSize size) => $"{size.Humanize()}" + (size == ExportSize.FullRes ? "" : $" (max {MaxSize(size)}x{MaxSize(size)})"); + public static int MaxSize( ExportSize size ) => + size switch { - return Size switch - { - ExportSize.Large => 1600, - ExportSize.Medium => 1024, - ExportSize.Small => 800, - _ => int.MaxValue, - }; - } - } + ExportSize.ExtraLarge => 1920, + ExportSize.Large => 1600, + ExportSize.Medium => 1024, + ExportSize.Small => 800, + _ => int.MaxValue, + }; } /// diff --git a/Damselfly.Core/ScopedServices/SearchService.cs b/Damselfly.Core/ScopedServices/SearchService.cs index 66aa7de5..6dd62a91 100644 --- a/Damselfly.Core/ScopedServices/SearchService.cs +++ b/Damselfly.Core/ScopedServices/SearchService.cs @@ -42,7 +42,6 @@ public class SearchResponse private readonly MetaDataService _metadataService; private readonly SearchQuery query = new SearchQuery(); public List SearchResults { get; private set; } = new List(); - private const double s_similarityThreshold = 0.75; public void NotifyStateChanged() { @@ -97,8 +96,11 @@ public void SetDateRange( DateTime? min, DateTime? max ) private void QueryChanged() { - SearchResults.Clear(); - NotifyStateChanged(); + Task.Run(() => + { + SearchResults.Clear(); + NotifyStateChanged(); + }); } /// @@ -395,6 +397,9 @@ public string SearchBreadcrumbs if (Person != null) hints.Add($"Person: {Person.Name}"); + if (MinRating != null) + hints.Add($"Rating: at least {MinRating} stars"); + if (SimilarTo != null) hints.Add($"Looks Like: {SimilarTo.FileName}"); diff --git a/Damselfly.Core/Services/DownloadService.cs b/Damselfly.Core/Services/DownloadService.cs index ddc12cdf..85e23a76 100644 --- a/Damselfly.Core/Services/DownloadService.cs +++ b/Damselfly.Core/Services/DownloadService.cs @@ -210,8 +210,7 @@ public async Task CreateDownloadZipAsync(FileInfo[] filesToZip, ExportCo { // Run the transform - note we do this in-memory and directly on the stream so the // transformed file is never actually written to disk other than in the zip. - await Task.Run(() => _imageProcessingService.TransformDownloadImage(imagePath.FullName, - zipStream, config)); + await _imageProcessingService.TransformDownloadImage(imagePath.FullName, zipStream, config); } } } diff --git a/Damselfly.Core/Services/ExifService.cs b/Damselfly.Core/Services/ExifService.cs index a8f661c0..da45aef9 100644 --- a/Damselfly.Core/Services/ExifService.cs +++ b/Damselfly.Core/Services/ExifService.cs @@ -112,7 +112,11 @@ public async Task UpdateTagsAsync(Image image, List addTags, List public async Task UpdateFaceDataAsync(Image[] images, List faces, AppIdentityUser user = null) { - // TODO: Split tags with commas here? +#if ! DEBUG + // Not supported yet.... + return; +# endif + var timestamp = DateTime.UtcNow; var changeDesc = string.Empty; @@ -292,8 +296,7 @@ private async Task ProcessExifOperations(int imageId, List Logging.LogVerbose("Updating tags for file {0}", image.FullPath); string args = string.Empty; - List processedOps = new List(); - bool needExecuteExifTool = false; + List opsToProcess = new List(); foreach (var op in exifOperations) { @@ -321,45 +324,38 @@ private async Task ProcessExifOperations(int imageId, List if (op.Operation == ExifOperation.OperationType.Remove) { Logging.LogVerbose($" Removing keyword {operationText} from {op.Image.FileName}"); - processedOps.Add(op); + opsToProcess.Add(op); } else if (op.Operation == ExifOperation.OperationType.Add) { Logging.LogVerbose($" Adding keyword '{operationText}' to {op.Image.FileName}"); args += $" -keywords+=\"{operationText}\" "; - processedOps.Add(op); + opsToProcess.Add(op); } - - needExecuteExifTool = true; } else if( op.Type == ExifOperation.ExifType.Caption ) { args += $" -iptc:Caption-Abstract=\"{op.Text}\""; - processedOps.Add(op); - needExecuteExifTool = true; + opsToProcess.Add(op); } else if (op.Type == ExifOperation.ExifType.Description) { args += $" -Exif:ImageDescription=\"{op.Text}\""; - processedOps.Add(op); - needExecuteExifTool = true; + opsToProcess.Add(op); } else if (op.Type == ExifOperation.ExifType.Copyright) { args += $" -Copyright=\"{op.Text}\""; args += $" -iptc:CopyrightNotice=\"{op.Text}\""; - processedOps.Add(op); - needExecuteExifTool = true; + opsToProcess.Add(op); } else if (op.Type == ExifOperation.ExifType.Rating) { args += $" -exif:Rating=\"{op.Text}\""; - processedOps.Add(op); - needExecuteExifTool = true; + opsToProcess.Add(op); } else if (op.Type == ExifOperation.ExifType.Face) { -#if DEBUG var imageObject = JsonSerializer.Deserialize(op.Text); // Face tags using MGW standard @@ -367,32 +363,31 @@ private async Task ProcessExifOperations(int imageId, List // -xmp-mwg-rs:RegionAreaX=0.319270833 -xmp-mwg-rs:RegionAreaY=0.21015625 -xmp-mwg-rs:RegionAreaW=0.165104167 -xmp-mwg-rs:RegionAreaH=0.30390625 // -xmp-mwg-rs:RegionName=John -xmp-mwg-rs:RegionRotation=0 -xmp-mwg-rs:RegionType="Face" myfile.xmp - // TODO: How to add multiple faces? - args += $" -xmp-mwg-rs:RegionType=\"Face\""; - args += $" -xmp-mwg-rs:RegionAppliedToDimensionsUnit=\"pixel\""; - args += $" -xmp-mwg-rs:RegionAppliedToDimensionsH=4000"; - args += $" -xmp-mwg-rs:RegionAppliedToDimensionsW=6000"; - args += $" -xmp-mwg-rs:RegionAreaX=0.319270833 -xmp-mwg-rs:RegionAreaY=0.21015625"; - args += $" -xmp-mwg-rs:RegionAreaW=0.165104167 -xmp-mwg-rs:RegionAreaH=0.30390625"; - args += $" -xmp-mwg-rs:RegionRotation=0"; - - if (imageObject.Person != null) + if (System.Diagnostics.Debugger.IsAttached) { - args += $" -xmp-mwg-rs:RegionName={imageObject.Person.Name}"; - } + // TODO: How to add multiple faces? + args += $" -xmp-mwg-rs:RegionType=\"Face\""; + args += $" -xmp-mwg-rs:RegionAppliedToDimensionsUnit=\"pixel\""; + args += $" -xmp-mwg-rs:RegionAppliedToDimensionsH=4000"; + args += $" -xmp-mwg-rs:RegionAppliedToDimensionsW=6000"; + args += $" -xmp-mwg-rs:RegionAreaX=0.319270833 -xmp-mwg-rs:RegionAreaY=0.21015625"; + args += $" -xmp-mwg-rs:RegionAreaW=0.165104167 -xmp-mwg-rs:RegionAreaH=0.30390625"; + args += $" -xmp-mwg-rs:RegionRotation=0"; + + if (imageObject.Person != null) + { + args += $" -xmp-mwg-rs:RegionName={imageObject.Person.Name}"; + } - processedOps.Add(op); - needExecuteExifTool = true; -#else - op.State = ExifOperation.FileWriteState.Failed; - Logging.Log("Writing Face EXIF data is not supported at this time."); -#endif + opsToProcess.Add(op); + } } } - var db = new ImageContext(); + // Assume they've all failed unless we succeed below. + exifOperations.ForEach(x => x.State = ExifOperation.FileWriteState.Failed); - if (needExecuteExifTool) + if (opsToProcess.Any() ) { // Note: we could do this to preserve the last-mod-time: // args += " -P -overwrite_original_in_place"; @@ -419,7 +414,7 @@ private async Task ProcessExifOperations(int imageId, List if (success) { - processedOps.ForEach(x => x.State = ExifOperation.FileWriteState.Written); + opsToProcess.ForEach(x => x.State = ExifOperation.FileWriteState.Written); // Updating the timestamp on the image to newer than its metadata will // trigger its metadata and tags to be refreshed during the next scan @@ -427,25 +422,23 @@ private async Task ProcessExifOperations(int imageId, List } else { - processedOps.ForEach(x => x.State = ExifOperation.FileWriteState.Failed); Logging.LogError("ExifTool Tag update failed for image: {0}", image.FullPath); - RestoreTempExifImage(image.FullPath); } } + using var db = new ImageContext(); + // Now write the updates - await db.BulkUpdate(db.KeywordOperations, processedOps); + await db.BulkUpdate(db.KeywordOperations, exifOperations); - if( needExecuteExifTool ) - { - var totals = string.Join(", ", exifOperations.GroupBy(x => x.State) - .Select(x => $"{x.Key}: {x.Count()}") - .ToList()); + // Now write a summary of how many succeeded and failed. + var totals = string.Join(", ", exifOperations.GroupBy(x => x.State) + .Select(x => $"{x.Key}: {x.Count()}") + .ToList()); - _statusService.StatusText = $"EXIF data written for {image.FileName}. {totals}"; - } - + _statusService.StatusText = $"EXIF data written for {image.FileName}. {totals}"; + return success; } @@ -588,7 +581,7 @@ private async Task>> ConflateOperations(Lis if (discardedOps.Any()) { - var db = new ImageContext(); + using var db = new ImageContext(); // Mark the ops as discarded, and save them. discardedOps.ForEach(x => x.State = ExifOperation.FileWriteState.Discarded); @@ -690,7 +683,7 @@ public async Task Process() public async Task> GetPendingJobs(int maxCount) { - var db = new ImageContext(); + using var db = new ImageContext(); // We skip any operations where the timestamp is more recent than 30s var timeThreshold = DateTime.UtcNow.AddSeconds(-1 * s_exifWriteDelay); diff --git a/Damselfly.Core/Services/ImageCache.cs b/Damselfly.Core/Services/ImageCache.cs index 39aca7ae..4acae0c6 100644 --- a/Damselfly.Core/Services/ImageCache.cs +++ b/Damselfly.Core/Services/ImageCache.cs @@ -55,7 +55,7 @@ public async Task WarmUp() Logging.Log($"Warming up image cache with up to {warmupCount} most recent images."); - var db = new ImageContext(); + using var db = new ImageContext(); var warmupIds = await db.Images.OrderByDescending(x => x.SortDate) .Take(warmupCount) diff --git a/Damselfly.Core/Services/ImageProcessService.cs b/Damselfly.Core/Services/ImageProcessService.cs index d21742aa..fee05eb5 100644 --- a/Damselfly.Core/Services/ImageProcessService.cs +++ b/Damselfly.Core/Services/ImageProcessService.cs @@ -73,14 +73,14 @@ public async Task CreateThumbs(FileInfo source, IDictionary< /// /// /// TODO: Async - public void TransformDownloadImage(string input, Stream output, IExportSettings exportConfig) + public async Task TransformDownloadImage(string input, Stream output, IExportSettings exportConfig) { var ext = Path.GetExtension(input); var processor = _factory.GetProcessor(ext); if (processor != null) - processor.TransformDownloadImage(input, output, exportConfig); + await processor.TransformDownloadImage(input, output, exportConfig); } /// diff --git a/Damselfly.Core/Services/ImageRecognitionService.cs b/Damselfly.Core/Services/ImageRecognitionService.cs index 48a433b4..d59d3e5b 100644 --- a/Damselfly.Core/Services/ImageRecognitionService.cs +++ b/Damselfly.Core/Services/ImageRecognitionService.cs @@ -82,16 +82,15 @@ private void LoadPersonCache(bool force = false) { var watch = new Stopwatch("LoadPersonCache"); - using (var db = new ImageContext()) - { - // Pre-cache tags from DB. - _peopleCache = new ConcurrentDictionary(db.People - .Where(x => !string.IsNullOrEmpty(x.AzurePersonId)) - .AsNoTracking() - .ToDictionary(k => k.AzurePersonId, v => v)); - if (_peopleCache.Any()) - Logging.LogTrace("Pre-loaded cach with {0} people.", _peopleCache.Count()); - } + using var db = new ImageContext(); + + // Pre-cache tags from DB. + _peopleCache = new ConcurrentDictionary(db.People + .Where(x => !string.IsNullOrEmpty(x.AzurePersonId)) + .AsNoTracking() + .ToDictionary(k => k.AzurePersonId, v => v)); + if (_peopleCache.Any()) + Logging.LogTrace("Pre-loaded cach with {0} people.", _peopleCache.Count()); watch.Stop(); } @@ -602,7 +601,7 @@ public void StartService() /// private async Task DetectObjects(int imageId) { - var db = new ImageContext(); + using var db = new ImageContext(); var image = await _imageCache.GetCachedImage(imageId); db.Attach(image); @@ -672,7 +671,7 @@ public async Task Process() public async Task> GetPendingJobs( int maxJobs ) { - var db = new ImageContext(); + using var db = new ImageContext(); var images = await db.ImageMetaData.Where(x => x.AILastUpdated == null && x.ThumbLastUpdated != null) .OrderByDescending(x => x.LastUpdated) diff --git a/Damselfly.Core/Services/IndexingService.cs b/Damselfly.Core/Services/IndexingService.cs index 438a4b39..cf9a7c78 100644 --- a/Damselfly.Core/Services/IndexingService.cs +++ b/Damselfly.Core/Services/IndexingService.cs @@ -77,39 +77,37 @@ public async Task IndexFolder(DirectoryInfo folder, Folder parent ) try { - using (var db = new ImageContext()) - { - // Load the existing folder and its images from the DB - folderToScan = await db.Folders - .Where(x => x.Path.Equals(folder.FullName)) - .Include(x => x.Images) - .FirstOrDefaultAsync(); - - if (folderToScan == null) - { - Logging.LogVerbose("Scanning new folder: {0}\\{1}", folder.Parent.Name, folder.Name); - folderToScan = new Folder { Path = folder.FullName }; - } - else - Logging.LogVerbose("Scanning existing folder: {0}\\{1} ({2} images in DB)", folder.Parent.Name, folder.Name, folderToScan.Images.Count()); + using var db = new ImageContext(); + // Load the existing folder and its images from the DB + folderToScan = await db.Folders + .Where(x => x.Path.Equals(folder.FullName)) + .Include(x => x.Images) + .FirstOrDefaultAsync(); - if (folderToScan.FolderId == 0) - { - Logging.Log($"Adding new folder: {folderToScan.Path}"); + if (folderToScan == null) + { + Logging.LogVerbose("Scanning new folder: {0}\\{1}", folder.Parent.Name, folder.Name); + folderToScan = new Folder { Path = folder.FullName }; + } + else + Logging.LogVerbose("Scanning existing folder: {0}\\{1} ({2} images in DB)", folder.Parent.Name, folder.Name, folderToScan.Images.Count()); - if (parent != null) - folderToScan.ParentFolderId = parent.FolderId; + if (folderToScan.FolderId == 0) + { + Logging.Log($"Adding new folder: {folderToScan.Path}"); - // New folder, add it. - db.Folders.Add(folderToScan); - await db.SaveChangesAsync("AddFolders"); - foldersChanged = true; - } + if (parent != null) + folderToScan.ParentFolderId = parent.FolderId; - // Now, check for missing folders, and clean up if appropriate. - foldersChanged = await RemoveMissingChildDirs(db, folderToScan) || foldersChanged; + // New folder, add it. + db.Folders.Add(folderToScan); + await db.SaveChangesAsync("AddFolders"); + foldersChanged = true; } + // Now, check for missing folders, and clean up if appropriate. + foldersChanged = await RemoveMissingChildDirs(db, folderToScan) || foldersChanged; + _watcherService.CreateFileWatcher(folder); // Now scan the images. If there's changes it could mean the folder @@ -482,7 +480,7 @@ public async Task> GetPendingJobs( int maxCount ) { if (_fullIndexComplete) { - var db = new ImageContext(); + using var db = new ImageContext(); // Now, see if there's any folders that have a null scan date. var folders = await db.Folders.Where(x => x.FolderScanDate == null) diff --git a/Damselfly.Core/Services/MetaDataService.cs b/Damselfly.Core/Services/MetaDataService.cs index a606e49b..d14678a5 100644 --- a/Damselfly.Core/Services/MetaDataService.cs +++ b/Damselfly.Core/Services/MetaDataService.cs @@ -372,7 +372,7 @@ public async Task ScanMetaData( int imageId ) Stopwatch watch = new Stopwatch("ScanMetadata"); var writeSideCarTagsToImages = _configService.GetBool(ConfigSettings.ImportSidecarKeywords); - var db = new ImageContext(); + using var db = new ImageContext(); var updateTimeStamp = DateTime.UtcNow; var imageKeywords = new List(); List sideCarTags = new List(); @@ -479,7 +479,7 @@ private async Task WriteXMPFaces(Image image, List xmpFaces) var names = xmpFaces.Select(x => x.Person.Name) .ToList(); - var db = new ImageContext(); + using var db = new ImageContext(); var peopleLookup = db.People.Where(x => names.Contains(x.Name)) .ToDictionary(x => x.Name, y => y.PersonId); @@ -804,15 +804,14 @@ private void LoadTagCache(bool force = false) { var watch = new Stopwatch("LoadTagCache"); - using (var db = new ImageContext()) - { - // Pre-cache tags from DB. - _tagCache = new ConcurrentDictionary(db.Tags - .AsNoTracking() - .ToDictionary(k => k.Keyword, v => v)); - if (_tagCache.Any()) - Logging.LogTrace("Pre-loaded cach with {0} tags.", _tagCache.Count()); - } + using var db = new ImageContext(); + + // Pre-cache tags from DB. + _tagCache = new ConcurrentDictionary(db.Tags + .AsNoTracking() + .ToDictionary(k => k.Keyword, v => v)); + if (_tagCache.Any()) + Logging.LogTrace("Pre-loaded cach with {0} tags.", _tagCache.Count()); watch.Stop(); } @@ -955,7 +954,7 @@ public async Task Process() public async Task> GetPendingJobs(int maxJobs) { - var db = new ImageContext(); + using var db = new ImageContext(); // Find all images where there's either no metadata, or where the image or sidecar file // was updated more recently than the image metadata diff --git a/Damselfly.Core/Services/ThumbnailService.cs b/Damselfly.Core/Services/ThumbnailService.cs index a6baf8c6..07588594 100644 --- a/Damselfly.Core/Services/ThumbnailService.cs +++ b/Damselfly.Core/Services/ThumbnailService.cs @@ -344,7 +344,7 @@ public async Task CreateThumbs(ImageMetaData sourceImage, bo /// public async Task CreateThumb(int imageId) { - var db = new ImageContext(); + using var db = new ImageContext(); var image = await _imageCache.GetCachedImage(imageId); @@ -371,7 +371,7 @@ public async Task AddHashToImage( Image image, ImageProcessResult processResult { try { - var db = new ImageContext(); + using var db = new ImageContext(); Hash hash = image.Hash; if (hash == null) @@ -650,7 +650,7 @@ public async Task> GetPendingJobs( int maxJobs ) if (!EnableThumbnailGeneration) return new ThumbProcess[0]; - var db = new ImageContext(); + using var db = new ImageContext(); var images = await db.ImageMetaData.Where(x => x.ThumbLastUpdated == null) .OrderByDescending(x => x.LastUpdated) diff --git a/Damselfly.Core/Services/WorkService.cs b/Damselfly.Core/Services/WorkService.cs index 5c0b2d46..63658d63 100644 --- a/Damselfly.Core/Services/WorkService.cs +++ b/Damselfly.Core/Services/WorkService.cs @@ -218,6 +218,9 @@ private bool PopulateJobsForService(IProcessJobFactory source, int maxCount) { _jobQueue.Enqueue(job, (int)job.Priority); newJobs = true; + + // TODO: HACK + Thread.Sleep(100); } } catch (Exception ex) diff --git a/Damselfly.ML.AccordFace/Damselfly.ML.AccordFace.csproj b/Damselfly.ML.AccordFace/Damselfly.ML.AccordFace.csproj index e82541be..54ae46a9 100644 --- a/Damselfly.ML.AccordFace/Damselfly.ML.AccordFace.csproj +++ b/Damselfly.ML.AccordFace/Damselfly.ML.AccordFace.csproj @@ -6,7 +6,7 @@ - + diff --git a/Damselfly.ML.AzureFace/Damselfly.ML.AzureFace.csproj b/Damselfly.ML.AzureFace/Damselfly.ML.AzureFace.csproj index c3fa53bb..55777739 100644 --- a/Damselfly.ML.AzureFace/Damselfly.ML.AzureFace.csproj +++ b/Damselfly.ML.AzureFace/Damselfly.ML.AzureFace.csproj @@ -12,6 +12,6 @@ - + diff --git a/Damselfly.ML.ObjectDetection.ML/Damselfly.ML.ObjectDetection.csproj b/Damselfly.ML.ObjectDetection.ML/Damselfly.ML.ObjectDetection.csproj index b5b596e0..86c3c55f 100644 --- a/Damselfly.ML.ObjectDetection.ML/Damselfly.ML.ObjectDetection.csproj +++ b/Damselfly.ML.ObjectDetection.ML/Damselfly.ML.ObjectDetection.csproj @@ -6,11 +6,11 @@ - + - + diff --git a/Damselfly.Migrations.Postgres/Damselfly.Migrations.Postgres.csproj b/Damselfly.Migrations.Postgres/Damselfly.Migrations.Postgres.csproj index 15035620..38d809bc 100644 --- a/Damselfly.Migrations.Postgres/Damselfly.Migrations.Postgres.csproj +++ b/Damselfly.Migrations.Postgres/Damselfly.Migrations.Postgres.csproj @@ -1,7 +1,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Damselfly.Migrations.Sqlite/Damselfly.Migrations.Sqlite.csproj b/Damselfly.Migrations.Sqlite/Damselfly.Migrations.Sqlite.csproj index 32079394..d9343b17 100644 --- a/Damselfly.Migrations.Sqlite/Damselfly.Migrations.Sqlite.csproj +++ b/Damselfly.Migrations.Sqlite/Damselfly.Migrations.Sqlite.csproj @@ -12,7 +12,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/Damselfly.Web/Damselfly.Web.csproj b/Damselfly.Web/Damselfly.Web.csproj index 05d5cc16..dfc6f993 100644 --- a/Damselfly.Web/Damselfly.Web.csproj +++ b/Damselfly.Web/Damselfly.Web.csproj @@ -52,6 +52,7 @@ + @@ -63,6 +64,7 @@ + @@ -77,13 +79,13 @@ - + - + - + @@ -98,6 +100,6 @@ - + \ No newline at end of file diff --git a/Damselfly.Web/Pages/ExportPage.razor b/Damselfly.Web/Pages/ExportPage.razor index e60ec354..0feb92db 100644 --- a/Damselfly.Web/Pages/ExportPage.razor +++ b/Damselfly.Web/Pages/ExportPage.razor @@ -46,7 +46,7 @@ { var watch = new Stopwatch("ExportLoadData"); images.Clear(); - images.AddRange(basketService.BasketImages.Select(x => new ListableImage(x, ThumbSize.Small))); + images.AddRange(basketService.BasketImages.Select(x => new ListableImage(x, ThumbSize.Medium))); watch.Stop(); return Task.FromResult(images); diff --git a/Damselfly.Web/Shared/BasketDialog.razor b/Damselfly.Web/Shared/Dialogs/BasketDialog.razor similarity index 98% rename from Damselfly.Web/Shared/BasketDialog.razor rename to Damselfly.Web/Shared/Dialogs/BasketDialog.razor index 15fe987b..d84a26db 100644 --- a/Damselfly.Web/Shared/BasketDialog.razor +++ b/Damselfly.Web/Shared/Dialogs/BasketDialog.razor @@ -73,7 +73,7 @@ async Task DeleteBasket() { IStatusService statusAudience = model.IsPublic ? statusService : userStatusService; - var db = new ImageContext(); + using var db = new ImageContext(); var existingBasket = db.Baskets.Where(x => x.BasketId == Basket.BasketId); @@ -89,7 +89,7 @@ async Task Save() { - var db = new ImageContext(); + using var db = new ImageContext(); IStatusService statusAudience = model.IsPublic ? statusService : userStatusService; try diff --git a/Damselfly.Web/Shared/BasketMoveDialog.razor b/Damselfly.Web/Shared/Dialogs/BasketMoveDialog.razor similarity index 100% rename from Damselfly.Web/Shared/BasketMoveDialog.razor rename to Damselfly.Web/Shared/Dialogs/BasketMoveDialog.razor diff --git a/Damselfly.Web/Shared/Dialogs/ExportConfigDialog.razor b/Damselfly.Web/Shared/Dialogs/ExportConfigDialog.razor new file mode 100644 index 00000000..0cf66e7c --- /dev/null +++ b/Damselfly.Web/Shared/Dialogs/ExportConfigDialog.razor @@ -0,0 +1,146 @@ + +@using System.ComponentModel.DataAnnotations +@using Damselfly.Core.Interfaces + +@inject BasketService basketService +@inject UserService userService +@inject UserStatusService statusService +@inject ConfigService configService + + + + @(Mode == "Edit" ? $"Edit Config '{Config.Name}'" : "Create New Export Config") + + + + + + @if (!string.IsNullOrEmpty(errorMsg)) + { +

@errorMsg

+ } + + @foreach (var choice in exportSizes) + { + + @ExportConfig.SizeDesc( choice ) + + } + + + @foreach (var choice in exportTypes) + { + + @choice.ToString() + + } + + + +
+
+ + @if (Mode == "Edit") + { + + + Delete Config + Cancel + Save + + + } + else + { + Cancel + Add Config + } + +
+ +@code { + [CascadingParameter] + MudDialogInstance MudDialog { get; set; } + + [Parameter] + public ExportConfig Config { get; set; } = new ExportConfig(); + + [Parameter] + public string Mode { get; set; } + + public ExportType[] exportTypes { get; private set; } = new ExportType[0]; + public ExportSize[] exportSizes { get; private set; } = new ExportSize[0]; + + private async Task OnValidSubmit(EditContext context) + { + await Save(); + } + + async Task DeleteConfig() + { + using var db = new ImageContext(); + + var existingConfig = db.DownloadConfigs.Where(x => x.ExportConfigId == Config.ExportConfigId); + + await db.BatchDelete(existingConfig); + + statusService.StatusText = $"Config '{Config.Name}' was deleted."; + + MudDialog.Close(DialogResult.Ok(true)); + } + + async Task Save() + { + using var db = new ImageContext(); + + try + { + if (Mode == "Add") + { + if (db.DownloadConfigs.Any(x => x.Name.Equals(Config.Name) )) + { + DisplayError($"Config '{Config.Name}' already exists. Please choose another name."); + return; + } + else + { + db.DownloadConfigs.Add(Config); + await db.SaveChangesAsync("SaveExportConfig"); + + statusService.StatusText = $"New download config '{Config.Name}' was created."; + } + } + else if (Mode == "Edit") + { + db.DownloadConfigs.Update(Config); + await db.SaveChangesAsync("SaveExportConfig"); + + + statusService.StatusText = $"Config '{Config.Name}' saved."; + } + + MudDialog.Close(DialogResult.Ok(true)); + } + catch (Exception ex) + { + Logging.LogError($"Unexpected error saving export config: {ex}"); + DisplayError($"Unexpected error saving export config {ex.Message}"); + } + } + + private string errorMsg; + + private void DisplayError(string errorText) + { + errorMsg = errorText; + StateHasChanged(); + } + + void Cancel() => MudDialog.Cancel(); + + protected override void OnInitialized() + { + exportTypes = (ExportType[])Enum.GetValues(typeof(ExportType)); + exportSizes = (ExportSize[])Enum.GetValues(typeof(ExportSize)); + } +} \ No newline at end of file diff --git a/Damselfly.Web/Shared/RescanDialog.razor b/Damselfly.Web/Shared/Dialogs/RescanDialog.razor similarity index 100% rename from Damselfly.Web/Shared/RescanDialog.razor rename to Damselfly.Web/Shared/Dialogs/RescanDialog.razor diff --git a/Damselfly.Web/Shared/UserDialog.razor b/Damselfly.Web/Shared/Dialogs/UserDialog.razor similarity index 100% rename from Damselfly.Web/Shared/UserDialog.razor rename to Damselfly.Web/Shared/Dialogs/UserDialog.razor diff --git a/Damselfly.Web/Shared/ExportConfigManager.razor b/Damselfly.Web/Shared/ExportConfigManager.razor index d99dfdd1..96c1a549 100644 --- a/Damselfly.Web/Shared/ExportConfigManager.razor +++ b/Damselfly.Web/Shared/ExportConfigManager.razor @@ -1,66 +1,62 @@ @inject ThumbnailService thumbService @inject NavigationManager NavigationManager @inject WordpressService wpService; +@inject IDialogService DialogService -
- @if (!AddingConfig) - { - - - } - else + -@code { + +@code +{ readonly List configs = new List(); - bool AddingConfig { get; set; } - string NewConfigName { get; set; } [Parameter] public ExportConfig CurrentConfig { get; set; } - private void SaveConfig() - { - AddingConfig = false; + [Parameter] + public EventCallback OnValueChanged { get; set; } - using var db = new ImageContext(); + async Task ConfigChanged(ChangeEventArgs e) + { + int id = Convert.ToInt32(e.Value); - if( CurrentConfig.ExportConfigId == -1 ) - db.DownloadConfigs.Add(CurrentConfig); - else - db.DownloadConfigs.Update(CurrentConfig); - db.SaveChanges("AddExportConfig"); + if (id >= 0) + CurrentConfig = configs.FirstOrDefault(x => x.ExportConfigId == id); - StateHasChanged(); + await OnValueChanged.InvokeAsync( new ChangeEventArgs { Value = CurrentConfig }); } - private void CancelAdding() + private async Task OpenAddBasketDialog() { - AddingConfig = false; - StateHasChanged(); - } + var newConfig = new ExportConfig { Name = "New Config" }; - private void AddBasket() - { - AddingConfig = true; - StateHasChanged(); + var parameters = new DialogParameters { { "Config", newConfig }, { "mode", "Add" } }; + var dialog = DialogService.Show("Add New Config", parameters); + var result = await dialog.Result; + + await LoadData(); } - private void ConfigChanged(ChangeEventArgs e) + private async Task OpenEditBasketDialog() { - StateHasChanged(); - } + var parameters = new DialogParameters { { "Config", CurrentConfig }, { "mode", "Edit" } }; + var dialog = DialogService.Show("Edit Config", parameters); + var result = await dialog.Result; + await LoadData(); + } protected override async Task OnAfterRenderAsync(bool firstRender) { @@ -78,6 +74,12 @@ this.configs.AddRange(db.DownloadConfigs); watch.Stop(); + if (CurrentConfig == null) + { + CurrentConfig = configs.FirstOrDefault(); + await OnValueChanged.InvokeAsync(new ChangeEventArgs { Value = CurrentConfig }); + } + await InvokeAsync(StateHasChanged); } } diff --git a/Damselfly.Web/Shared/ExportSettings.razor b/Damselfly.Web/Shared/ExportSettings.razor index 1462dc08..0d7ce667 100644 --- a/Damselfly.Web/Shared/ExportSettings.razor +++ b/Damselfly.Web/Shared/ExportSettings.razor @@ -4,55 +4,41 @@ @inject DownloadService downloadService @inject IJSRuntime JsRuntime @inject NavigationManager navManager + @implements IDisposable
- -
-
-

Export Settings

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- @if (FileExporter != null && FileExporter.IsDesktopHosted) - { - - } - -
-
- + + @if (selectedConfig != null) + { +
+
+

Export Settings

+
+
+ +
+
+ +
+
+ +
+
+ @if (FileExporter != null && FileExporter.IsDesktopHosted) + { + + } + +
+
+ } + else + { +

Please create an Export config.

+ }
@code { @@ -60,31 +46,23 @@ public ExportType[] exportTypes { get; private set; } = new ExportType[0]; public ExportSize[] exportSizes { get; private set; } = new ExportSize[0]; - // TODO: Size etc are not binding. - public string CurrentConfigName { get { return selectedConfig.Name; } set { ChangeConfig(value); } } - public List configs = new List(); - public ExportConfig selectedConfig = new ExportConfig { Name = "Default" }; + private ExportConfig selectedConfig; public string StatusMessage { get; set; } - private void ChangeConfig(string name) + private void ConfigChanged( ChangeEventArgs args) { - using var db = new ImageContext(); + selectedConfig = args.Value as ExportConfig; - var config = db.DownloadConfigs.FirstOrDefault(x => x.Name.Equals(name)); + if( selectedConfig != null ) + StatusMessage = $"Loaded Config for '{selectedConfig.Name}'"; - if (config != null) - { - selectedConfig = config; - StatusMessage = $"Loaded Config for '{name}'"; - StateHasChanged(); - } + StateHasChanged(); } private async Task SaveConfig() { await downloadService.SaveDownloadConfig(selectedConfig); StatusMessage = $"Saved Config for '{selectedConfig.Name}'"; - await LoadConfigs(); StateHasChanged(); } @@ -110,19 +88,11 @@ exportTypes = (ExportType[])Enum.GetValues(typeof(ExportType)); exportSizes = (ExportSize[])Enum.GetValues(typeof(ExportSize)); - await LoadConfigs(); StateHasChanged(); } } - private async Task> LoadConfigs() - { - using var db = new ImageContext(); - configs = await Task.FromResult(db.DownloadConfigs.ToList()); - return configs; - } - public void Dispose() { FileExporter.OnChange -= StateHasChanged; diff --git a/Damselfly.Web/Shared/Images/ImageProperties.razor b/Damselfly.Web/Shared/Images/ImageProperties.razor index 6d84a638..5df81204 100644 --- a/Damselfly.Web/Shared/Images/ImageProperties.razor +++ b/Damselfly.Web/Shared/Images/ImageProperties.razor @@ -137,7 +137,7 @@ else private async Task SaveProperty( Action propertyUpdate ) { - var db = new ImageContext(); + using var db = new ImageContext(); var metadata = CurrentImage.MetaData; db.Attach(metadata); propertyUpdate.Invoke(CurrentImage.MetaData); diff --git a/Damselfly.Web/Shared/MainLayout.razor b/Damselfly.Web/Shared/MainLayout.razor index 85c68c1b..479b9ca6 100644 --- a/Damselfly.Web/Shared/MainLayout.razor +++ b/Damselfly.Web/Shared/MainLayout.razor @@ -1,4 +1,5 @@  +@inject UserConfigService configService @inherits LayoutComponentBase @@ -11,8 +12,14 @@
- - @Body + + + + + + @Body + +
diff --git a/Damselfly.Web/Shared/SplitView.razor b/Damselfly.Web/Shared/SplitView.razor new file mode 100644 index 00000000..1b6eedc1 --- /dev/null +++ b/Damselfly.Web/Shared/SplitView.razor @@ -0,0 +1,61 @@ + +@inject UserConfigService configService +@inherits LayoutComponentBase + + + + @LeftPane + + + @RightPane + + + +@code{ + [Parameter] + public RenderFragment LeftPane { get; set; } + + [Parameter] + public RenderFragment RightPane { get; set; } + + private string SideBarSize { get; set; } + private bool Collapsed { get; set; } + + protected override void OnInitialized() + { + base.OnInitialized(); + + SideBarSize = configService.Get(ConfigSettings.SideBarWidth, "20%"); + Collapsed = configService.GetBool(ConfigSettings.SideBarCollapsed, false); + } + + void OnResize(RadzenSplitterResizeEventArgs args) + { + if( args.PaneIndex == 0 ) + { + var newSize = $"{(int)args.NewSize}%"; + configService.Set(ConfigSettings.SideBarWidth, newSize); + configService.Set(ConfigSettings.SideBarCollapsed, "false"); + } + } + + void OnCollapse(RadzenSplitterEventArgs args) + { + if (args.PaneIndex == 0) + { + Collapsed = true; + configService.Set(ConfigSettings.SideBarCollapsed, Collapsed.ToString()); + } + } + + void OnExpand(RadzenSplitterEventArgs args) + { + if (args.PaneIndex == 0) + { + Collapsed = false; + configService.Set("SideBarCollapsed", Collapsed.ToString()); + } + } +} + + diff --git a/Damselfly.Web/Shared/Stats.razor b/Damselfly.Web/Shared/Stats.razor index 55505535..a7017806 100644 --- a/Damselfly.Web/Shared/Stats.razor +++ b/Damselfly.Web/Shared/Stats.razor @@ -67,34 +67,33 @@ protected async Task GetStatsSync() { - using (var db = new ImageContext()) - { - TotalImages = await db.Images.CountAsync(); - TotalTags = await db.Tags.CountAsync(); - TotalFolders = await db.Folders.CountAsync(); - TotalImagesSizeBytes = await db.Images.SumAsync( x => (long)x.FileSizeBytes ); - PeopleFound = await db.People.CountAsync(); - PeopleIdentified = await db.People.Where( x => x.Name != "Unknown" ).CountAsync(); - ObjectsRecognised = await db.ImageObjects.CountAsync(); - PendingAIScans = await db.ImageMetaData.Where(x => !x.AILastUpdated.HasValue).CountAsync(); - PendingThumbs = await db.ImageMetaData.Where(x => !x.ThumbLastUpdated.HasValue).CountAsync(); - PendingImages = await db.Images.Where(x => x.MetaData == null || x.LastUpdated > x.MetaData.LastUpdated ).Include( x => x.MetaData ).CountAsync(); - PendingKeywordOps = await db.KeywordOperations.Where(x => x.State == ExifOperation.FileWriteState.Pending).CountAsync(); - PendingKeywordImages = await db.KeywordOperations.Where(x => x.State == ExifOperation.FileWriteState.Pending) - .Select(x => x.ImageId ) - .Distinct().CountAsync(); + using var db = new ImageContext(); + + TotalImages = await db.Images.CountAsync(); + TotalTags = await db.Tags.CountAsync(); + TotalFolders = await db.Folders.CountAsync(); + TotalImagesSizeBytes = await db.Images.SumAsync( x => (long)x.FileSizeBytes ); + PeopleFound = await db.People.CountAsync(); + PeopleIdentified = await db.People.Where( x => x.Name != "Unknown" ).CountAsync(); + ObjectsRecognised = await db.ImageObjects.CountAsync(); + PendingAIScans = await db.ImageMetaData.Where(x => !x.AILastUpdated.HasValue).CountAsync(); + PendingThumbs = await db.ImageMetaData.Where(x => !x.ThumbLastUpdated.HasValue).CountAsync(); + PendingImages = await db.Images.Where(x => x.MetaData == null || x.LastUpdated > x.MetaData.LastUpdated ).Include( x => x.MetaData ).CountAsync(); + PendingKeywordOps = await db.KeywordOperations.Where(x => x.State == ExifOperation.FileWriteState.Pending).CountAsync(); + PendingKeywordImages = await db.KeywordOperations.Where(x => x.State == ExifOperation.FileWriteState.Pending) + .Select(x => x.ImageId ) + .Distinct().CountAsync(); - // TODO: Should pull this out of the TransThrottle instance. - var now = DateTime.UtcNow; - var monthStart = new DateTime( now.Year, now.Month, 1, 0, 0, 1 ); - var monthEnd = monthStart.AddMonths(1).AddSeconds( -1 ); - var totalTrans = await db.CloudTransactions.Where(x => x.Date >= monthStart && x.Date <= monthEnd).SumAsync(x => x.TransCount); + // TODO: Should pull this out of the TransThrottle instance. + var now = DateTime.UtcNow; + var monthStart = new DateTime( now.Year, now.Month, 1, 0, 0, 1 ); + var monthEnd = monthStart.AddMonths(1).AddSeconds( -1 ); + var totalTrans = await db.CloudTransactions.Where(x => x.Date >= monthStart && x.Date <= monthEnd).SumAsync(x => x.TransCount); - if( totalTrans > 0 ) - AzureMonthlyTransactions = $"{totalTrans} (during {monthStart:MMM})"; + if( totalTrans > 0 ) + AzureMonthlyTransactions = $"{totalTrans} (during {monthStart:MMM})"; - StatsReady = true; - await InvokeAsync( StateHasChanged ); - }; + StatsReady = true; + await InvokeAsync( StateHasChanged ); } } \ No newline at end of file diff --git a/Damselfly.Web/Startup.cs b/Damselfly.Web/Startup.cs index 11e9d03f..86f69c59 100644 --- a/Damselfly.Web/Startup.cs +++ b/Damselfly.Web/Startup.cs @@ -322,10 +322,9 @@ private static void StartTaskScheduler(TaskService taskScheduler, DownloadServic ExecutionFrequency = new TimeSpan(2, 0, 0), WorkMethod = () => { - using (var db = new ImageContext()) - { - db.FlushDBWriteCache(); - } + using var db = new ImageContext(); + + db.FlushDBWriteCache(); } }); */ diff --git a/Damselfly.Web/_Imports.razor b/Damselfly.Web/_Imports.razor index 5dab716e..7a7837f5 100644 --- a/Damselfly.Web/_Imports.razor +++ b/Damselfly.Web/_Imports.razor @@ -22,9 +22,10 @@ @using Damselfly.Web @using Damselfly.Web.Data @using Damselfly.Web.Extensions -@using Damselfly.Web.Shared @using Damselfly.Web.Components +@using Damselfly.Web.Shared @using Damselfly.Web.Shared.Images +@using Damselfly.Web.Shared.Dialogs @using Radzen @using Radzen.Blazor @using MudBlazor diff --git a/Damselfly.Web/wwwroot/css/site.css b/Damselfly.Web/wwwroot/css/site.css index 90b556cf..45fdbf82 100644 --- a/Damselfly.Web/wwwroot/css/site.css +++ b/Damselfly.Web/wwwroot/css/site.css @@ -501,7 +501,9 @@ f order: 1; flex: 0 0 350px; overflow-y: auto; + overflow-x: hidden; display: flex; + height: 100%; flex-direction: column; } @@ -510,6 +512,7 @@ f display: flex; flex: 1; flex-direction: column; + height: 100%; overflow-y: auto; } @@ -561,7 +564,7 @@ f flex-direction: column; flex: 1 1 15px; overflow-y: auto; - overflow-x: hidden; + overflow-x: auto; } .damselfly-selectedimages { @@ -1442,11 +1445,34 @@ padding: 0.4em 0.65em; } .rz-menu:not(.rz-profile-menu) .rz-navigation-item-wrapper { - padding: 2px 5px; + padding: 2px 5px !important; } .rz-navigation-item-link { - padding: 1px 1px; + padding: 1px 1px !important; +} + + +.rz-splitter-horizontal > .rz-splitter-bar { + width: 8px; +} + +.rz-splitter > .rz-splitter-bar { + color: var(--tool-window-shadow); +/* background-color: var(--tool-window-bg); */ + background-image: linear-gradient(90deg, var(--statusbar-gradstart), var(--statusbar-gradend) ); + opacity: 0.7; +} + +.rz-splitter:hover > .rz-splitter-bar:hover { + opacity: 1; + color: var(--tool-window-bg); + background-image: linear-gradient(90deg, var(--statusbar-gradstart), var(--statusbar-gradend) ); +} + +.rz-splitter:active > .rz-splitter-bar:active { + opacity: 1; + background-color: var(--tool-window-title-bg); } hr.separator { diff --git a/Dockerfile b/Dockerfile index 785c36f1..104e8f8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ +ARG BASE_IMAGE=webreaper/damselfly-base:latest -FROM webreaper/damselfly-base:latest AS final +FROM $BASE_IMAGE as final WORKDIR /app COPY /publish . diff --git a/README.md b/README.md index 8964d0a8..805a4510 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ do this for you) where the server will re-index them to pick up your changes. ### Suggested workflow. 1. Images are copied onto a laptop for initial sorting, quality checks, and IPTC tagging using Picasa or Digikam -2. [Rclone](www.rclone.org) script syncs the new images across the LAN to the network share +2. [Rclone](https://rclone.org/) script syncs the new images across the LAN to the network share 3. Damselfly automatically picks up the new images and indexes them (and generates thumbnails) within 30 minutes 4. Images are now searchable in Damselfly and can be added to the Damselfly 'basket' with a single click 5. Images in the basket can be copied back to the desktop/laptop for local editing in Lightroom/On1/Digikam/etc. diff --git a/docs/Installation.md b/docs/Installation.md index bf31a337..b7677b5a 100644 --- a/docs/Installation.md +++ b/docs/Installation.md @@ -9,6 +9,8 @@ - [FileWatcher INotify Limits](#filewatcher-inotify-limits) - [Setting up the Desktop Client](#setting-up-the-desktop-client) - [Installing the Desktop Client](#installing-the-desktop-client) + - [Can I Run Damselfly Without Docker?](#can-i-run-damselfly-without-docker) + - [Dependencies for Damselfly without Docker](#dependencies-for-damselfly-without-docker) ## Docker @@ -55,7 +57,8 @@ Damselfly uses OS-level filewatcher triggers to monitor your library for changes photo library. For MacOS and Linus, the number of inotify watchers availalbe to the OS may be set very low (a few hundred) so you may need to increase -the number of inotify instances as follows (where 524288 is any large number that's big enough for one watcher per folder), [for linux](https://unix.stackexchange.com/questions/13751/kernel-inotify-watch-limit-reached). +the number of inotify instances as follows (where 524288 is any large number that's big enough for one watcher per folder), +[for linux](https://unix.stackexchange.com/questions/13751/kernel-inotify-watch-limit-reached). ``` echo fs.inotify.max_user_instances=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p @@ -95,3 +98,40 @@ photos to sync locally. Configuring Damselfly Desktop Once you've entered the correct details, click `Save` and the Web UI should be displayed. + +## Can I run Damselfly without Docker? + +Damselfly can be run without docker, but it will be harder to set up the AI components. Please note: I cannot provide support for installations +that don't use docker. This is for experts only. + +Note that Damselfly is a 64-bit app, so you'll need at 64-bit OS in order to run it, whether or not you use Docker. + +To run without docker, you'll need to download the appropriate server binaries from the release (something like `damselfly-server-linux-2.9.0.zip` +or the Windows/Mac equivalent). Note that I don't produce non-docker binary assets with every release; if you need them for a release and they're +not there, +please email and ask. + +Once you've downloaded the binaries, extract them into a folder, and from the command-line within that folder run Damselfly. There is only one +mandatory command-line parameter, which is the path to where your photo collection can be found, so something like: + +``` +./Damselfly.Web /path/to/my/photos +``` + +### Dependencies for Damselfly without Docker + +**Note: I cannot support non-docker installations; there are too many variations across all the different OS flavours/types, and I simply don't +have time. I recommend you run Damselfly in Docker.** + +Damselfly relies on various dependencies being present for all functionality to work. These are bundled with the Docker image, but if you're running +outside docker you'll need to manage them yourself. + +Many of the depenendencies may be available on Windows already. On linux/OSX, you'll need to install these by hand, probably via a package manager. +Dependencies you will require are: +* Exiftool (used for keyword and other metadata write operations to images) +* Fonts (used for watermarking images on export) +* libgomp1 / libdgiplus / libc6-dev - for ML/ONNX functionality for object recognition +* Various dependencies for the EMGUCV AI libraries for face-recognition etc. + +To see the full set of dependencies required by Damselfly, see the +[Dockerfile for the base image](https://github.com/Webreaper/Damselfly-Base-Image/blob/main/Dockerfile). diff --git a/scripts/makedocker.sh b/scripts/makedocker.sh index d978e9de..a9efb61b 100644 --- a/scripts/makedocker.sh +++ b/scripts/makedocker.sh @@ -2,14 +2,18 @@ if [ -z "$1" ]; then echo 'No docker tag specified. Pushing to dev' DOCKERTAG='dev' + + echo "**** Building Docker Damselfly using dev base image" + docker build -t damselfly --build-arg BASE_IMAGE=webreaper/damselfly-base:dev . else version=`cat VERSION` DOCKERTAG="${version}-beta" echo "Master specified - creating tag: ${DOCKERTAG}" + + echo "**** Building Docker Damselfly" + docker build -t damselfly . fi -echo "**** Building Docker Damselfly" -docker build -t damselfly . echo "*** Pushing docker image to webreaper/damselfly:${DOCKERTAG}"