@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.
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}"