Browse Source

Added PdfRenderer

frankvandenbos 10 months ago
parent
commit
59c93aa046
31 changed files with 420 additions and 71 deletions
  1. 3 7
      InABox.Avalonia.Platform.Android/AppVersion.Android.cs
  2. 3 3
      InABox.Avalonia.Platform.Android/DeviceID.Android.cs
  3. 2 2
      InABox.Avalonia.Platform.Android/ImageTools.Android.cs
  4. 2 1
      InABox.Avalonia.Platform.Android/InABox.Avalonia.Platform.Android.csproj
  5. 25 0
      InABox.Avalonia.Platform.Android/PdfRenderer.Android.cs
  6. 19 0
      InABox.Avalonia.Platform.Desktop/InABox.Avalonia.Platform.Desktop.csproj
  7. 24 0
      InABox.Avalonia.Platform.Desktop/PDFRenderer.Desktop.cs
  8. 1 1
      InABox.Avalonia.Platform.iOS/AppVersion.iOS.cs
  9. 1 1
      InABox.Avalonia.Platform.iOS/DeviceID.iOS.cs
  10. 1 1
      InABox.Avalonia.Platform.iOS/ImageTools.iOS.cs
  11. 1 0
      InABox.Avalonia.Platform.iOS/InABox.Avalonia.Platform.iOS.csproj
  12. 15 0
      InABox.Avalonia.Platform.iOS/PDFRenderer.iOS.cs
  13. 1 1
      InABox.Avalonia.Platform/AppVersion/DefaultAppVersion.cs
  14. 1 1
      InABox.Avalonia.Platform/DeviceId/DefaultDeviceId.cs
  15. 1 1
      InABox.Avalonia.Platform/ILoggable.cs
  16. 1 1
      InABox.Avalonia.Platform/ImageTools/DefaultImageTools.cs
  17. 15 0
      InABox.Avalonia.Platform/PDFRenderer/DefaultPdfRenderer.cs
  18. 7 0
      InABox.Avalonia.Platform/PDFRenderer/IPdfRenderer.cs
  19. 10 0
      InABox.Avalonia.Platform/PlatformTools.cs
  20. 17 0
      InABox.Avalonia/Converters/ByteArrayToImageSourceConverter.cs
  21. 88 0
      InABox.Avalonia/Converters/DateTimeToAgeConverter.cs
  22. 9 0
      InABox.Avalonia/Converters/EmptyConverter.cs
  23. 16 0
      InABox.Avalonia/Converters/ListToBooleanConverter.cs
  24. 117 39
      InABox.Avalonia/DataModels/CoreRepository.cs
  25. 8 0
      InABox.Avalonia/DataModels/ICoreRepository.cs
  26. 2 0
      InABox.Avalonia/DataModels/IShell.cs
  27. 14 0
      InABox.Avalonia/DataModels/Shell.cs
  28. 6 6
      InABox.Avalonia/Router/HistoryRouter.cs
  29. 6 4
      InABox.Avalonia/Router/Router.cs
  30. 3 0
      InABox.Core/CoreFilterDefinition.cs
  31. 1 2
      InABox.Mobile/InABox.Mobile.Shared/DataModels/CoreRepository.cs

+ 3 - 7
InABox.Avalonia.Platform.Android/AppVersion.cs → InABox.Avalonia.Platform.Android/AppVersion.Android.cs

@@ -6,18 +6,14 @@ using Net = Android.Net;
 
 namespace InABox.Avalonia.Platform.Android
 {
-    /// <summary>
-    /// <see cref="ILatestVersion"/> implementation for Android.
-    /// </summary>H
-    [Preserve(AllMembers = true)]
-    public class AppVersion : IAppVersion
+    public class Android_AppVersion : IAppVersion
     {
         string _packageName = "";
         string _versionName = "";
 
-        public Logger Logger { get; set; }
+        public Logger? Logger { get; set; }
 
-        public AppVersion() : base()
+        public Android_AppVersion() : base()
         {
             try
             {

+ 3 - 3
InABox.Avalonia.Platform.Android/DeviceID.cs → InABox.Avalonia.Platform.Android/DeviceID.Android.cs

@@ -3,14 +3,14 @@
 namespace InABox.Avalonia.Platform.Android
 {
 
-    public class DeviceID : IDeviceId
+    public class Android_DeviceId : IDeviceId
     {
         
-        public Logger Logger { get; set; }
+        public Logger? Logger { get; set; }
         
         public String GetDeviceId()
         {
-            return global::Android.OS.Build.Serial;
+            return global::Android.OS.Build.Serial ?? "";
         }
 
     }

+ 2 - 2
InABox.Avalonia.Platform.Android/ImageTools.cs → InABox.Avalonia.Platform.Android/ImageTools.Android.cs

@@ -12,10 +12,10 @@ using Stream = System.IO.Stream;
 
 namespace InABox.Avalonia.Platform.Android
 {
-    public class ImageTools : IImageTools
+    public class Android_ImageTools : IImageTools
     {
         
-        public Logger Logger { get; set; }
+        public Logger? Logger { get; set; }
 
         public byte[] CreateVideoThumbnail(byte[] video, int maxwidth, int maxheight)
         {

+ 2 - 1
InABox.Avalonia.Platform.Android/InABox.Avalonia.Platform.Android.csproj

@@ -4,7 +4,6 @@
         <TargetFramework>net8.0-android</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
-        <RootNamespace>InABox.Avalonia.Platform.Android</RootNamespace>
         <LangVersion>default</LangVersion>
     </PropertyGroup>
 
@@ -23,7 +22,9 @@
     
     <ItemGroup>
       <PackageReference Include="Avalonia" Version="11.2.3" />
+      <PackageReference Include="bblanchon.PDFium.Android" Version="134.0.6968" />
       <PackageReference Include="Microsoft.Maui.Essentials" />
+      <PackageReference Include="PDFtoImage" Version="5.0.0" />
     </ItemGroup>
 
 </Project>

+ 25 - 0
InABox.Avalonia.Platform.Android/PdfRenderer.Android.cs

@@ -0,0 +1,25 @@
+using InABox.Core;
+using PDFtoImage;
+using SkiaSharp;
+
+namespace InABox.Avalonia.Platform.Android;
+
+public class Android_PdfRenderer : IPdfRenderer
+{
+        
+    public Logger? Logger { get; set; }
+    
+    public byte[]? RenderPdf(byte[]? pdf, int page, int dpi)
+    {
+        if (pdf?.Any() != true)
+            return null;
+        var result = Conversion.ToImage(pdf, page, options: new RenderOptions(Dpi: dpi));
+        using var ms = new MemoryStream();
+        result.Encode(ms, SKEncodedImageFormat.Jpeg, 65);
+        return ms.ToArray();
+    }
+
+    public Task<byte[]?> RenderPdfAsync(byte[]? pdf, int page, int dpi)
+        => Task.Run(() => RenderPdf(pdf, page, dpi));
+
+}

+ 19 - 0
InABox.Avalonia.Platform.Desktop/InABox.Avalonia.Platform.Desktop.csproj

@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0-windows</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\InABox.Avalonia.Platform\InABox.Avalonia.Platform.csproj" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <PackageReference Include="PDFtoImage" Version="5.0.0" />
+      <PackageReference Include="System.ComponentModel.Composition" Version="9.0.1" />
+      <PackageReference Include="System.ComponentModel.Composition.Registration" Version="9.0.1" />
+    </ItemGroup>
+
+</Project>

+ 24 - 0
InABox.Avalonia.Platform.Desktop/PDFRenderer.Desktop.cs

@@ -0,0 +1,24 @@
+using InABox.Core;
+using PDFtoImage;
+using SkiaSharp;
+
+namespace InABox.Avalonia.Platform.Desktop;
+
+public class Desktop_PdfRenderer : IPdfRenderer
+{
+    public byte[]? RenderPdf(byte[]? pdf, int page, int dpi)
+    {
+        if (pdf?.Any() != true)
+            return null;
+        
+        var result = Conversion.ToImage(pdf, page, options: new RenderOptions(Dpi: dpi));
+        using var ms = new MemoryStream();
+        result.Encode(ms, SKEncodedImageFormat.Jpeg, 65);
+        return ms.ToArray();
+    }
+
+    public Task<byte[]?> RenderPdfAsync(byte[]? pdf, int page, int dpi)
+        => Task.Run(() => RenderPdf(pdf, page, dpi));
+
+    public Logger? Logger { get; set; }
+}

+ 1 - 1
InABox.Avalonia.Platform.iOS/AppVersion.cs → InABox.Avalonia.Platform.iOS/AppVersion.iOS.cs

@@ -5,7 +5,7 @@ using Newtonsoft.Json;
 namespace InABox.Avalonia.Platform.iOS
 {
     
-    public class AppVersion : IAppVersion
+    public class iOS_AppVersion : IAppVersion
     {
         string _bundleName => NSBundle.MainBundle.ObjectForInfoDictionary("CFBundleName").ToString();
         string _bundleIdentifier => NSBundle.MainBundle.ObjectForInfoDictionary("CFBundleIdentifier").ToString();

+ 1 - 1
InABox.Avalonia.Platform.iOS/DeviceID.cs → InABox.Avalonia.Platform.iOS/DeviceID.iOS.cs

@@ -3,7 +3,7 @@
 namespace InABox.Avalonia.Platform.iOS
 {
 
-    public class DeviceID : IDeviceId
+    public class iOS_DeviceId : IDeviceId
     {
         public Logger Logger { get; set; }
         

+ 1 - 1
InABox.Avalonia.Platform.iOS/ImageTools.cs → InABox.Avalonia.Platform.iOS/ImageTools.iOS.cs

@@ -9,7 +9,7 @@ using Microsoft.Maui.Storage;
 namespace InABox.Avalonia.Platform.iOS
 {
     
-    public class ImageTools : IImageTools
+    public class iOS_ImageTools : IImageTools
     {
         public Logger Logger { get; set; }
         

+ 1 - 0
InABox.Avalonia.Platform.iOS/InABox.Avalonia.Platform.iOS.csproj

@@ -22,6 +22,7 @@
 
     <ItemGroup>
       <PackageReference Include="Avalonia.iOS" Version="11.2.3" />
+      <PackageReference Include="bblanchon.PDFium.iOS" Version="134.0.6968" />
       <PackageReference Include="Microsoft.Maui.Essentials" />
       <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
     </ItemGroup>

+ 15 - 0
InABox.Avalonia.Platform.iOS/PDFRenderer.iOS.cs

@@ -0,0 +1,15 @@
+using InABox.Core;
+
+namespace InABox.Avalonia.Platform.iOS;
+
+public class iOS_PdfRenderer : IPdfRenderer
+{
+
+    public Logger? Logger { get; set; }
+
+    public byte[]? RenderPdf(byte[]? pdf, int page, int dpi)
+        => null;
+
+    public Task<byte[]?> RenderPdfAsync(byte[]? pdf, int page, int dpi)
+        => Task.Run(() => RenderPdf(pdf, page, dpi));
+}

+ 1 - 1
InABox.Avalonia.Platform/AppVersion/DefaultAppVersion.cs

@@ -5,7 +5,7 @@ namespace InABox.Avalonia.Platform;
 public class DefaultAppVersion : IAppVersion
 {
     
-    public Logger Logger { get; set; }
+    public Logger? Logger { get; set; }
     
     public string InstalledVersionNumber()
     {

+ 1 - 1
InABox.Avalonia.Platform/DeviceId/DefaultDeviceId.cs

@@ -5,7 +5,7 @@ namespace InABox.Avalonia.Platform;
 public class DefaultDeviceId : IDeviceId
 {
  
-    public Logger Logger { get; set; }
+    public Logger? Logger { get; set; }
 
     public String GetDeviceId()
     {

+ 1 - 1
InABox.Avalonia.Platform/ILoggable.cs

@@ -4,5 +4,5 @@ namespace InABox.Avalonia.Platform;
 
 public interface ILoggable
 { 
-    Logger Logger { get; set; }
+    Logger? Logger { get; set; }
 }

+ 1 - 1
InABox.Avalonia.Platform/ImageTools/DefaultImageTools.cs

@@ -6,7 +6,7 @@ namespace InABox.Avalonia.Platform;
 
 public class DefaultImageTools : IImageTools
 {
-    public Logger Logger { get; set; }
+    public Logger? Logger { get; set; }
 
     public byte[] CreateVideoThumbnail(byte[] video, int maxwidth, int maxheight)
     { 

+ 15 - 0
InABox.Avalonia.Platform/PDFRenderer/DefaultPdfRenderer.cs

@@ -0,0 +1,15 @@
+using InABox.Core;
+
+namespace InABox.Avalonia.Platform;
+
+public class DefaultPdfRenderer : IPdfRenderer
+{
+    public Logger? Logger { get; set; }
+ 
+    public byte[]? RenderPdf(byte[]? pdf, int page, int dpi) 
+        => null;
+
+    public Task<byte[]?> RenderPdfAsync(byte[]? pdf, int page, int dpi)
+        => Task.Run(() => RenderPdf(pdf, page, dpi));
+
+}

+ 7 - 0
InABox.Avalonia.Platform/PDFRenderer/IPdfRenderer.cs

@@ -0,0 +1,7 @@
+namespace InABox.Avalonia.Platform;
+
+public interface IPdfRenderer : ILoggable
+{
+    byte[]? RenderPdf(byte[]? pdf, int page, int dpi);
+    Task<byte[]?> RenderPdfAsync(byte[]? pdf, int page, int dpi);
+}

+ 10 - 0
InABox.Avalonia.Platform/PlatformTools.cs

@@ -39,6 +39,16 @@ public static class PlatformTools
         }
     }
     
+    private static IPdfRenderer? _pdfRenderer;
+    public static IPdfRenderer PdfRenderer
+    {
+        get
+        {
+            _pdfRenderer ??= Resolve<IPdfRenderer, DefaultPdfRenderer>();
+            return _pdfRenderer;
+        }
+    }
+    
     private static TInterface Resolve<TInterface, TDefault>() where TInterface : notnull, ILoggable where TDefault : TInterface, new() 
     {
         _container ??= _builder.Build();

+ 17 - 0
InABox.Avalonia/Converters/ByteArrayToImageSourceConverter.cs

@@ -0,0 +1,17 @@
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media.Imaging;
+
+namespace InABox.Avalonia.Converters;
+
+public class ByteArrayToImageSourceConverter : AbstractConverter<byte[], Bitmap?>
+{
+    protected override Bitmap? Convert(byte[]? value, object? parameter = null)
+    {
+        Bitmap? result = null;
+        if (value is byte[] bytes)
+            using (var stream = new MemoryStream(bytes))
+                result = new Bitmap(stream);
+        return result;
+    }
+}

+ 88 - 0
InABox.Avalonia/Converters/DateTimeToAgeConverter.cs

@@ -0,0 +1,88 @@
+using System;
+using InABox.Core;
+
+namespace InABox.Avalonia.Converters;
+
+public class DateTimeToAgeConverter : AbstractConverter<DateTime, String>
+{
+        
+    public static string FormatTime(DateTime date)
+    {
+        DateTime now = DateTime.Now;
+        String prefix = date > now ? "In" : "";
+        String suffix = date <= now ? "ago" : "";
+        TimeSpan span = date > now ? date - now : now - date;
+            
+        if (span.Days > 365)
+        {
+            int years = (span.Days / 365);
+            if (span.Days % 365 != 0)
+                years += 1;
+            return String.Format("{0} {1} {2} {3}", 
+                prefix,
+                years, 
+                years == 1 ? "year" : "years",
+                suffix).Trim();
+        }
+            
+        if (span.Days > 30)
+        {
+            int months = (span.Days / 30);
+            if (span.Days % 31 != 0)
+                months += 1;
+            return String.Format("{0} {1} {2} {3}", 
+                prefix,
+                months,
+                months == 1 ? "month" : "months",
+                suffix).Trim();
+        }
+            
+        if (span.Days > 0)
+            return String.Format("{0} {1} {2} {3}", 
+                prefix,
+                span.Days, 
+                span.Days == 1 ? "day" : "days",
+                suffix).Trim();
+            
+        if (span.Hours > 0)
+            return String.Format("{0} about {1} {2} {3}", 
+                prefix,
+                span.Hours, 
+                span.Hours == 1 ? "hour" : "hours",
+                suffix);
+            
+        if (span.Minutes > 0)
+            return String.Format("{0} {1} {2} {3}",
+                prefix,
+                span.Minutes, 
+                span.Minutes == 1 ? "minute" : "minutes",
+                suffix).Trim();
+            
+        if (span.Seconds > 5)
+            return String.Format("{0} {1} seconds {2}", 
+                prefix,
+                span.Seconds,
+                suffix).Trim();
+            
+        if (span.Seconds <= 5)
+            return date > now
+                ? "Imminently" 
+                : "Just now";
+            
+        return string.Empty;
+    }
+        
+    public string EmptyValue { get; set; } = String.Empty;
+
+    public string Prefix { get; set; } = string.Empty;
+    
+    protected override string Convert(DateTime value, object? parameter = null)
+    {
+        var result = value.IsEmpty()
+            ? EmptyValue
+            : string.IsNullOrWhiteSpace(Prefix)
+                ? FormatTime(value)
+                : $"{Prefix} {FormatTime(value)}";
+        return result;
+    }
+}

+ 9 - 0
InABox.Avalonia/Converters/EmptyConverter.cs

@@ -0,0 +1,9 @@
+namespace InABox.Avalonia.Converters;
+
+public class EmptyConverter : AbstractConverter<object, object>
+{
+    protected override object? Convert(object? value, object? parameter = null)
+    {
+        return value;
+    }
+}

+ 16 - 0
InABox.Avalonia/Converters/ListToBooleanConverter.cs

@@ -0,0 +1,16 @@
+using System.Collections;
+
+namespace InABox.Avalonia.Converters;
+
+public class ListToBooleanConverter : AbstractConverter<IList, bool>
+{
+    
+    public bool EmptyValue { get; set; }
+    
+    protected override bool Convert(IList? value, object? parameter = null)
+    {
+        return value?.Count == 0
+            ? EmptyValue
+            : !EmptyValue;
+    }
+}

+ 117 - 39
InABox.Avalonia/DataModels/CoreRepository.cs

@@ -1,8 +1,11 @@
 using System.Collections;
+using System.Collections.ObjectModel;
 using System.ComponentModel;
 using System.Linq.Expressions;
 using System.Runtime.CompilerServices;
+using Avalonia.Controls.Selection;
 using Avalonia.Threading;
+using CommunityToolkit.Mvvm.ComponentModel;
 using InABox.Clients;
 using InABox.Configuration;
 using InABox.Core;
@@ -23,7 +26,8 @@ namespace InABox.Avalonia
     }
 
     public delegate void CoreRepositoryItemCreatedEvent<TShell>(object sender, CoreRepositoryItemCreatedArgs<TShell> args);
-
+    
+    
     public abstract class CoreRepository
     {
         public static bool IsCached(string? filename) => 
@@ -36,6 +40,12 @@ namespace InABox.Avalonia
         public static string CacheFolder()
         {
             var result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+            if (OperatingSystem.IsWindows())
+            {
+                var assembly = Path.GetFileNameWithoutExtension(System.Diagnostics.Process.GetCurrentProcess().MainModule.ModuleName);
+                result = Path.Combine(result, assembly);
+            }
+
             if (CacheID != Guid.Empty)
                 result = Path.Combine(result,CacheID.ToString());
             if (!Directory.Exists(result))
@@ -44,14 +54,22 @@ namespace InABox.Avalonia
         }
 
         public static Guid CacheID { get; set; }
+        
+    }
 
-        public abstract string DefaultFileName();
+    public partial class CoreRepositoryFilter : ObservableObject
+    {
+        [ObservableProperty]
+        private string? _name;
+        
+        [ObservableProperty]
+        private string? _filter;
+        
+        [ObservableProperty]
+        private bool _selected;
 
     }
     
-    
-    
-    
     public abstract class CoreRepository<TParent, TItem, TEntity> : CoreRepository, ICoreRepository, IEnumerable<TItem>
         where TParent : CoreRepository<TParent, TItem, TEntity>
         where TEntity : Entity, IRemotable, IPersistent, new() 
@@ -65,11 +83,27 @@ namespace InABox.Avalonia
         
         public IModelHost Host { get; set; }
         
-        public DateTime LastUpdated { get; protected set; }
+        private DateTime _lastUpdated = DateTime.MinValue;
+        public DateTime LastUpdated
+        {
+            get => _lastUpdated;
+            protected set
+            {
+                _lastUpdated = value;
+                OnPropertyChanged();
+            }
+        }
         
         public Func<string>? FileName { get; }
 
-        public override string DefaultFileName() => typeof(TEntity).Name + ".db";
+        
+        private string DataFileName() => FileName != null
+            ? $"{FileName.Invoke()}.data"
+            : string.Empty;
+        
+        private string FilterFileName() => !string.IsNullOrWhiteSpace(FilterTag) && FileName != null
+            ? $"{FileName.Invoke()}.filter"
+            : string.Empty;
         
         protected CoreRepository(IModelHost host, Func<Filter<TEntity>> filter, Func<string>? filename = null)
         {
@@ -87,6 +121,7 @@ namespace InABox.Avalonia
             Host = host;
             Filter = filter;
             FileName = filename;
+
         }
 
         protected virtual void ItemsChanged(IEnumerable<TItem> items)
@@ -143,26 +178,25 @@ namespace InABox.Avalonia
         public bool HasImages() => Images.Any();
 
         #endregion
-
         
         protected virtual string FilterTag => typeof(TEntity).EntityName().Split('.').Last();
         
-        public CoreFilterDefinitions AvailableFilters()
-        {
-            return string.IsNullOrWhiteSpace(FilterTag)
-                ? new CoreFilterDefinitions()
-                : new GlobalConfiguration<CoreFilterDefinitions>(FilterTag).Load();
-        }
-
-        protected Filter<TEntity> SelectedFilter;
+        public CoreObservableCollection<CoreRepositoryFilter> AvailableFilters { get; } = new();
+        IEnumerable ICoreRepository.AvailableFilters => AvailableFilters;
         
-        public void SelectFilter(String name)
+        protected Filter<TEntity>? SelectedFilter =>
+            Serialization.Deserialize<Filter<TEntity>>(AvailableFilters.FirstOrDefault(x => x.Selected)?.Filter);
+        
+        public bool FiltersVisible => AvailableFilters.Any();
+        
+        public void SelectFilter(String? name)
         {
-            var definition = AvailableFilters().FirstOrDefault(x => String.Equals(x.Name, name));
-            SelectedFilter = definition?.AsFilter<TEntity>();
+            var definition = AvailableFilters.FirstOrDefault(x => String.Equals(x.Name, name));
+            foreach (var availableFilter in AvailableFilters)
+                availableFilter.Selected = definition == availableFilter;
+            OnPropertyChanged(nameof(AvailableFilters));
         }
-
-
+        
         protected Filter<TEntity> EffectiveFilter()
         {
             var filters = new Filters<TEntity>();
@@ -189,6 +223,8 @@ namespace InABox.Avalonia
             Images.Clear();
         }
         
+        
+        
         public bool Loaded { get; protected set; }
         
         private void DoRefresh(bool force)
@@ -197,8 +233,8 @@ namespace InABox.Avalonia
             Items.Clear();
             SelectedItems.Clear();
 
-            var filename = FileName?.Invoke();
-            if (!Loaded && CoreRepository.IsCached(filename))
+            var dataFileName = DataFileName();
+            if (!force && !Loaded && CoreRepository.IsCached(dataFileName))
             {
                 DoBeforeLoad();
                 if (LoadFromStorage())
@@ -216,15 +252,16 @@ namespace InABox.Avalonia
                 SelectedItems.AddRange(Items.Where(x=>curselected.Contains(x)));
                 return;
             }
-            
-            SelectedItems.AddRange(Items.Where(x=>curselected.Contains(x)));
- 
+
+            SelectedItems.AddRange(Items.Where(x => curselected.Contains(x)));
+
+
         }
 
         private void AfterRefresh()
         {
             Loaded = true;
-            Search();
+            Dispatcher.UIThread.Invoke(Search);
             NotifyChanged();
         }
 
@@ -252,7 +289,7 @@ namespace InABox.Avalonia
             );
         }
 
-        public Task RefreshAsync(bool force)
+        public Task<ICoreRepository> RefreshAsync(bool force)
         {
             return Task.Run(() => Refresh(force));
         }
@@ -272,6 +309,8 @@ namespace InABox.Avalonia
         private CoreTable _table = new CoreTable();
 
         public CoreObservableCollection<TItem> Items { get; private set; }
+
+        public int ItemCount => Items.Count;
         
         IEnumerable ICoreRepository.Items => Items;
         
@@ -373,6 +412,7 @@ namespace InABox.Avalonia
             
             Items.ReplaceRange(items);
             OnPropertyChanged(nameof(Items));
+            OnPropertyChanged(nameof(ItemCount));
             return this;
         }
  
@@ -416,7 +456,6 @@ namespace InABox.Avalonia
         
         protected virtual void AfterLoad(MultiQuery query)
         {
-      
         }
         
         protected void DoLoad()
@@ -424,13 +463,29 @@ namespace InABox.Avalonia
 
             try
             {
+                var selected = AvailableFilters.FirstOrDefault(x => x.Selected)?.Name;
+
+                if (!string.IsNullOrWhiteSpace(FilterTag))
+                {
+                    var filters = new GlobalConfiguration<CoreFilterDefinitions>(FilterTag).Load()
+                        .Where(x => x.Visibility == CoreFilterDefinitionVisibility.DesktopAndMobile)
+                        .Select(x => new CoreRepositoryFilter() { Name = x.Name, Filter = x.Filter, Selected = string.Equals(x.Name,selected) })
+                        .ToList();
+                    if (filters.Any())
+                        filters.Insert(0, new CoreRepositoryFilter() { Name="All", Filter = "", Selected = !filters.Any(x => string.Equals(x.Name, selected)) });
+                    AvailableFilters.ReplaceRange(filters);
+                    Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(FiltersVisible)));
+                }
+                
                 DoBeforeLoad();
                 BeforeLoad(_query);
+                
                 Task.Run(() =>
                 {
                     _query.Query();
                     DoAfterLoad();
                 }).Wait();
+                    
                 Search();
                 AfterLoad(_query);
                 LastUpdated = DateTime.Now;
@@ -515,18 +570,32 @@ namespace InABox.Avalonia
         
         protected bool LoadFromStorage()
         {
-            var filename = FileName?.Invoke();
-            if (String.IsNullOrWhiteSpace(filename))
+            
+            var filterFileName = FilterFileName();
+            if (!string.IsNullOrWhiteSpace(filterFileName))
+            {
+                filterFileName = CacheFileName(filterFileName);
+                if (File.Exists(filterFileName))
+                {
+                    var json = File.ReadAllText(filterFileName);
+                    var filters = Serialization.Deserialize<ObservableCollection<CoreRepositoryFilter>>(json) ?? new ObservableCollection<CoreRepositoryFilter>();
+                    AvailableFilters.ReplaceRange(filters);
+                    Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(FiltersVisible)));
+                }
+            }
+
+            var dataFileName = DataFileName();
+            if (String.IsNullOrWhiteSpace(dataFileName))
             {
                 InitializeTables();
                 return true;
             }
             
-            var file = CacheFileName(filename);
-            if (File.Exists(file))
+            dataFileName = CacheFileName(dataFileName);
+            if (File.Exists(dataFileName))
             {
-                LastUpdated = File.GetLastWriteTime(file);
-                using (var stream = new FileStream(file, FileMode.Open))
+                LastUpdated = File.GetLastWriteTime(dataFileName);
+                using (var stream = new FileStream(dataFileName, FileMode.Open))
                 {
                     QueryStorage storage = Serialization.ReadBinary<QueryStorage>(stream,
                         BinarySerializationSettings.Latest);
@@ -545,7 +614,7 @@ namespace InABox.Avalonia
             }
             else
                 InitializeTables();
-
+            
             return true;
         }
 
@@ -562,8 +631,17 @@ namespace InABox.Avalonia
 
         protected void SaveToStorage()
         {
-            var filename = FileName?.Invoke();
-            if (String.IsNullOrWhiteSpace(filename))
+            
+            var filterFileName = FilterFileName();
+            if (!string.IsNullOrWhiteSpace(filterFileName))
+            {
+                filterFileName = CacheFileName(filterFileName);
+                var json = Serialization.Serialize(AvailableFilters);
+                File.WriteAllText(filterFileName,json);
+            }
+            
+            var dataFileName = DataFileName();
+            if (String.IsNullOrWhiteSpace(dataFileName))
                 return;
             
             QueryStorage storage = new QueryStorage();
@@ -573,7 +651,7 @@ namespace InABox.Avalonia
             var data = storage.WriteBinary(BinarySerializationSettings.Latest);
             try
             {
-                var file = CacheFileName(filename);
+                var file = CacheFileName(dataFileName);
                 File.WriteAllBytes(file,data);
             }
             catch (Exception e)

+ 8 - 0
InABox.Avalonia/DataModels/ICoreRepository.cs

@@ -1,5 +1,7 @@
 using System.Collections;
+using System.Collections.ObjectModel;
 using System.ComponentModel;
+using InABox.Core;
 
 
 namespace InABox.Avalonia
@@ -15,12 +17,17 @@ namespace InABox.Avalonia
     {
         //IColumns GetColumns();
         
+        IEnumerable AvailableFilters { get; }
+        void SelectFilter(string? name);
+        bool FiltersVisible { get; }
+        
         byte[]? GetImageSource(Guid id);
         bool HasImages();
 
         bool Loaded { get; }
         
         ICoreRepository Refresh(bool force);
+        Task<ICoreRepository> RefreshAsync(bool force);
         void Refresh(bool force, Action loaded);
         
         DateTime LastUpdated { get; }
@@ -34,6 +41,7 @@ namespace InABox.Avalonia
         object AddItem();
         void DeleteItem(object item);
         IEnumerable Items { get; }
+        int ItemCount { get; }
         ICoreRepository Search();
         ICoreRepository Search(Func<object,bool> predicate);
         

+ 2 - 0
InABox.Avalonia/DataModels/IShell.cs

@@ -11,6 +11,8 @@ namespace InABox.Avalonia
         bool IsChanged();
         void Save(string auditmessage);
         void Cancel();
+
+        bool Match(string? text);
     }
 
     public interface IShell<out TEntity>

+ 14 - 0
InABox.Avalonia/DataModels/Shell.cs

@@ -74,6 +74,20 @@ namespace InABox.Avalonia
         ICoreRepository IShell.Parent => this.Parent;
 
         public Guid ID => Get<Guid>();
+
+        public virtual string[] TextSearchValues() => [];
+        
+        public bool Match(string? text)
+        {
+            if (string.IsNullOrWhiteSpace(text))
+                return true;
+            foreach (var value in TextSearchValues())
+            {
+                if (value.Contains(text, StringComparison.InvariantCultureIgnoreCase))
+                    return true;
+            }
+            return false;
+        }
         
         #region Row Get/Set Caching
         

+ 6 - 6
InABox.Avalonia/Router/HistoryRouter.cs

@@ -66,24 +66,24 @@ public class HistoryRouter<TViewModelBase>: Router<TViewModelBase> where TViewMo
     
     public TViewModelBase? Forward() => HasNext ? Go(1) : default;
 
-    private T InternalGoTo<T>(RouterDirection direction) where T: TViewModelBase
+    private T InternalGoTo<T>(RouterDirection direction, Action<T>? configure = null) where T: TViewModelBase
     {
-        var destination = InstantiateViewModel<T>();
+        var destination = InstantiateViewModel<T>(configure);
         CurrentViewModelChanging?.Invoke(destination, direction);
         CurrentViewModel = destination;
         Push(destination);
         return destination;
     }
     
-    public T Reset<T>() where T: TViewModelBase
+    public T Reset<T>(Action<T>? configure = null) where T: TViewModelBase
     {
         _historyIndex = -1;
         _history.Clear();
-        return InternalGoTo<T>(RouterDirection.Backward);
+        return InternalGoTo<T>(RouterDirection.Backward, configure);
     }
     
-    public override T GoTo<T>()
+    public override T GoTo<T>(Action<T>? configure = null)
     {
-        return InternalGoTo<T>(RouterDirection.Forward);
+        return InternalGoTo<T>(RouterDirection.Forward, configure);
     }
 }

+ 6 - 4
InABox.Avalonia/Router/Router.cs

@@ -29,16 +29,18 @@ public class Router<TViewModelBase> where TViewModelBase:class
         CurrentViewModelChanged?.Invoke(viewModel);
     }
 
-    public virtual T GoTo<T>() where T : TViewModelBase
+    public virtual T GoTo<T>(Action<T>? configure) where T : TViewModelBase
     {
-        var viewModel = InstantiateViewModel<T>();
+        var viewModel = InstantiateViewModel<T>(configure);
         CurrentViewModel = viewModel;
         return viewModel;
     }
 
-    protected T InstantiateViewModel<T>() where T:TViewModelBase
+    protected T InstantiateViewModel<T>(Action<T>? configure) where T:TViewModelBase
     {
-        return (T)Convert.ChangeType(CreateViewModel(typeof(T)), typeof(T));
+        var result = (T)Convert.ChangeType(CreateViewModel(typeof(T)), typeof(T));
+        configure?.Invoke(result);
+        return result;
     }
 
 }

+ 3 - 0
InABox.Core/CoreFilterDefinition.cs

@@ -59,6 +59,9 @@ namespace InABox.Core
         {
             return Serialization.Deserialize<Filter<T>>(Filter);
         }
+        
+        [NullEditor]
+        public bool IsSelected { get; set; }
     }
 
     public class CoreFilterDefinitions : List<CoreFilterDefinition>, IGlobalConfigurationSettings, IUserConfigurationSettings

+ 1 - 2
InABox.Mobile/InABox.Mobile.Shared/DataModels/CoreRepository.cs

@@ -164,8 +164,7 @@ namespace InABox.Mobile
             var definition = AvailableFilters().FirstOrDefault(x => String.Equals(x.Name, name));
             SelectedFilter = definition?.AsFilter<TEntity>();
         }
-
-
+        
         protected Filter<TEntity> EffectiveFilter()
         {
             var filters = new Filters<TEntity>();