From 38ffb3c7e1946cff1ba94f2f83a0674f103444ee Mon Sep 17 00:00:00 2001 From: Harrison Deng Date: Sat, 7 Aug 2021 17:20:46 -0500 Subject: [PATCH] Added logging to module framework Implemented logging to Adafruit and changed database loading behavior. --- .../Props.Shop/Adafruit/AdafruitShop.cs | 30 ++++--- .../Adafruit/Api/IProductListingManager.cs | 7 +- .../Adafruit/Api/LiveProductListingManager.cs | 71 ++++++++++++---- .../Props.Shop/Adafruit/Api/SearchManager.cs | 36 +++----- .../Props.Shop/Adafruit/Configuration.cs | 3 + .../Adafruit/Props.Shop.Adafruit.csproj | 1 - Props-Modules/Props.Shop/Framework/Filters.cs | 14 ++- Props-Modules/Props.Shop/Framework/IShop.cs | 8 +- .../Framework/Props.Shop.Framework.csproj | 4 + .../Adafruit.Tests/AdafruitShopTest.cs | 16 ++-- .../Api/FakeProductListingManager.cs | 66 ++++++++------- .../Adafruit.Tests/Api/SearchManagerTest.cs | 7 +- .../Props.Shop.Adafruit.Tests.csproj | 3 + Props/Options/ContentOptions.cs | 8 -- Props/Options/ModulesOptions.cs | 3 +- Props/Options/TextualOptions.cs | 8 ++ Props/Pages/Index.cshtml | 8 +- Props/Pages/Search.cshtml | 80 +++++++++++------- Props/Pages/Search.cshtml.cs | 12 +-- Props/Props.csproj | 9 -- Props/Services/Modules/ISearchManager.cs | 3 +- Props/Services/Modules/LiveSearchManager.cs | 10 +-- Props/Services/Modules/ModularShopManager.cs | 36 ++++---- .../Modules/ShopAssemblyLoadContext.cs | 2 + .../CachedTextualManager.cs} | 10 +-- .../ITextualManager.cs} | 2 +- Props/Startup.cs | 2 +- Props/TagHelpers/NavLinkTagHelper.cs | 1 - Props/appsettings.json | 7 +- Props/assets/js/specific/search.js | 41 ++------- ...rosoft.Extensions.Logging.Abstractions.dll | Bin 0 -> 52616 bytes Props/shops/Props.Shop.Adafruit.deps.json | 36 ++++---- Props/shops/Props.Shop.Adafruit.dll | Bin 18944 -> 22016 bytes Props/shops/Props.Shop.Framework.dll | Bin 0 -> 10752 bytes Props/{Content => textual}/Index.json | 0 Props/{Content => textual}/search.json | 0 36 files changed, 304 insertions(+), 240 deletions(-) delete mode 100644 Props/Options/ContentOptions.cs create mode 100644 Props/Options/TextualOptions.cs rename Props/Services/{Content/CachedContentManager.cs => Textual/CachedTextualManager.cs} (62%) rename Props/Services/{Content/IContentManager.cs => Textual/ITextualManager.cs} (61%) create mode 100644 Props/shops/Microsoft.Extensions.Logging.Abstractions.dll create mode 100644 Props/shops/Props.Shop.Framework.dll rename Props/{Content => textual}/Index.json (100%) rename Props/{Content => textual}/search.json (100%) diff --git a/Props-Modules/Props.Shop/Adafruit/AdafruitShop.cs b/Props-Modules/Props.Shop/Adafruit/AdafruitShop.cs index 650f8a5..4a3bcfc 100644 --- a/Props-Modules/Props.Shop/Adafruit/AdafruitShop.cs +++ b/Props-Modules/Props.Shop/Adafruit/AdafruitShop.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net.Http; -using System.Text.Json; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Props.Shop.Adafruit.Api; using Props.Shop.Framework; @@ -11,6 +10,8 @@ namespace Props.Shop.Adafruit { public class AdafruitShop : IShop { + private ILoggerFactory loggerFactory; + private ILogger logger; private SearchManager searchManager; private Configuration configuration; private HttpClient http; @@ -29,22 +30,27 @@ namespace Props.Shop.Adafruit false, true ); - public void Initialize(string workspaceDir) + public void Initialize(string workspaceDir, ILoggerFactory loggerFactory) { + this.loggerFactory = loggerFactory; http = new HttpClient(); http.BaseAddress = new Uri("http://www.adafruit.com/api/"); - configuration = new Configuration(); // TODO Implement config persistence. - } - - public async Task InitializeAsync(string workspaceDir) - { - ProductListingManager productListingManager = new ProductListingManager(http); + configuration = new Configuration(); + // TODO: Implement config persistence. + // TODO: Implement product listing persisted cache. + LiveProductListingManager productListingManager = new LiveProductListingManager(http, loggerFactory.CreateLogger(), configuration.MinDownloadInterval); this.searchManager = new SearchManager(productListingManager, configuration.Similarity); - await productListingManager.DownloadListings(); - productListingManager.StartUpdateTimer(); + productListingManager.StartUpdateTimer(delay: 0); + + logger = loggerFactory.CreateLogger(); } - public IEnumerable Search(string query, Filters filters) + public Task GetProductListingFromUrl(string url) + { + return searchManager.ProductListingManager.GetProductListingFromUrl(url); + } + + public IAsyncEnumerable Search(string query, Filters filters) { return searchManager.Search(query); } diff --git a/Props-Modules/Props.Shop/Adafruit/Api/IProductListingManager.cs b/Props-Modules/Props.Shop/Adafruit/Api/IProductListingManager.cs index 5caeaca..558e543 100644 --- a/Props-Modules/Props.Shop/Adafruit/Api/IProductListingManager.cs +++ b/Props-Modules/Props.Shop/Adafruit/Api/IProductListingManager.cs @@ -7,10 +7,11 @@ namespace Props.Shop.Adafruit.Api { public interface IProductListingManager : IDisposable { - public event EventHandler DataUpdateEvent; - public IDictionary> ActiveListings { get; } - public Task DownloadListings(); + public Task>> ProductListings { get; } + public void RefreshProductListings(); public void StartUpdateTimer(int delay = 1000 * 60 * 5, int period = 1000 * 60 * 5); public void StopUpdateTimer(); + + public Task GetProductListingFromUrl(string url); } } \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Adafruit/Api/LiveProductListingManager.cs b/Props-Modules/Props.Shop/Adafruit/Api/LiveProductListingManager.cs index 8cfb685..28e2fde 100644 --- a/Props-Modules/Props.Shop/Adafruit/Api/LiveProductListingManager.cs +++ b/Props-Modules/Props.Shop/Adafruit/Api/LiveProductListingManager.cs @@ -4,53 +4,94 @@ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Props.Shop.Framework; namespace Props.Shop.Adafruit.Api { - public class ProductListingManager : IProductListingManager + public class LiveProductListingManager : IProductListingManager { - public event EventHandler DataUpdateEvent; + private ILogger logger; private bool disposedValue; - private volatile Dictionary> activeListings; - public IDictionary> ActiveListings => activeListings; + private int minDownloadInterval; + private DateTime? lastDownload; + private object refreshLock = new object(); + private volatile Task>> productListingsTask; + + public Task>> ProductListings => productListingsTask; + private readonly ConcurrentDictionary activeProductListingUrls = new ConcurrentDictionary(); + private ProductListingsParser parser = new ProductListingsParser(); private HttpClient httpClient; private Timer updateTimer; - public ProductListingManager(HttpClient httpClient) + public LiveProductListingManager(HttpClient httpClient, ILogger logger, int minDownloadInterval = 5 * 60 * 1000) { + this.logger = logger; + this.minDownloadInterval = minDownloadInterval; this.httpClient = httpClient; } - public async Task DownloadListings() { + public void RefreshProductListings() + { + lock (refreshLock) + { + if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); + if ((lastDownload != null && DateTime.UtcNow - lastDownload <= TimeSpan.FromMilliseconds(minDownloadInterval)) || (productListingsTask != null && !productListingsTask.IsCompleted)) return; + lastDownload = DateTime.UtcNow; + logger.LogDebug("Refreshing listings ({0}).", lastDownload); + productListingsTask = DownloadListings(); + } + } + + public async Task GetProductListingFromUrl(string url) + { if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); + await productListingsTask; + return activeProductListingUrls[url]; + } + + private async Task>> DownloadListings() + { + if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); + logger.LogDebug("Beginning listing database download."); HttpResponseMessage responseMessage = await httpClient.GetAsync("products"); parser.BuildProductListings(responseMessage.Content.ReadAsStream()); - Dictionary> listings = new Dictionary>(); + logger.LogDebug("Listing database parsed."); + Dictionary> listingNames = new Dictionary>(); + activeProductListingUrls.Clear(); foreach (ProductListing product in parser.ProductListings) { - IList sameProducts = listings.GetValueOrDefault(product.Name); - if (sameProducts == null) { + activeProductListingUrls.TryAdd(product.URL, product); + IList sameProducts = listingNames.GetValueOrDefault(product.Name); + if (sameProducts == null) + { sameProducts = new List(); - listings.Add(product.Name, sameProducts); + listingNames.Add(product.Name, sameProducts); } sameProducts.Add(product); } - activeListings = listings; - DataUpdateEvent?.Invoke(this, null); + logger.LogDebug("Downloaded listings organized."); + return listingNames; } - public void StartUpdateTimer(int delay = 1000 * 60 * 5, int period = 1000 * 60 * 5) { + public void StartUpdateTimer(int delay = 1000 * 60 * 5, int period = 1000 * 60 * 5) + { if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); if (updateTimer != null) throw new InvalidOperationException("Update timer already started."); - updateTimer = new Timer(async (state) => await DownloadListings(), null, delay, period); + logger.LogInformation("Starting update timer."); + updateTimer = new Timer((state) => + { + RefreshProductListings(); + }, null, delay, period); } - public void StopUpdateTimer() { + public void StopUpdateTimer() + { if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); if (updateTimer != null) throw new InvalidOperationException("Update timer not started."); + logger.LogInformation("Stopping update timer."); updateTimer.Dispose(); updateTimer = null; } diff --git a/Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs b/Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs index 5b57379..ae73b21 100644 --- a/Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs +++ b/Props-Modules/Props.Shop/Adafruit/Api/SearchManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using FuzzySharp; using FuzzySharp.Extractor; using Props.Shop.Framework; @@ -11,41 +12,26 @@ namespace Props.Shop.Adafruit.Api public class SearchManager : IDisposable { public float Similarity { get; set; } - private readonly object searchLock = new object(); - private IDictionary> searched; - private IProductListingManager listingManager; + public IProductListingManager ProductListingManager { get; private set; } private bool disposedValue; public SearchManager(IProductListingManager productListingManager, float similarity = 0.8f) { - this.listingManager = productListingManager ?? throw new ArgumentNullException("productListingManager"); + this.ProductListingManager = productListingManager ?? throw new ArgumentNullException("productListingManager"); this.Similarity = similarity; - listingManager.DataUpdateEvent += OnDataUpdate; } - private void OnDataUpdate(object sender, EventArgs eventArgs) + public async IAsyncEnumerable Search(string query) { - BuildSearchIndex(); - } - - private void BuildSearchIndex() - { - lock (searchLock) - { - searched = new Dictionary>(listingManager.ActiveListings); + if (ProductListingManager.ProductListings == null) { + ProductListingManager.RefreshProductListings(); } - } - - public IEnumerable Search(string query) - { - lock (searchLock) + IDictionary> productListings = await ProductListingManager.ProductListings; + foreach (ExtractedResult listingNames in Process.ExtractAll(query, productListings.Keys, cutoff: (int)(Similarity * 100))) { - foreach (ExtractedResult listingNames in Process.ExtractAll(query, searched.Keys, cutoff: (int)(Similarity * 100))) + foreach (ProductListing same in productListings[listingNames.Value]) { - foreach (ProductListing same in searched[listingNames.Value]) - { - yield return same; - } + yield return same; } } } @@ -56,7 +42,7 @@ namespace Props.Shop.Adafruit.Api { if (disposing) { - listingManager.Dispose(); + ProductListingManager.Dispose(); } disposedValue = true; diff --git a/Props-Modules/Props.Shop/Adafruit/Configuration.cs b/Props-Modules/Props.Shop/Adafruit/Configuration.cs index 06e1407..25a1832 100644 --- a/Props-Modules/Props.Shop/Adafruit/Configuration.cs +++ b/Props-Modules/Props.Shop/Adafruit/Configuration.cs @@ -2,10 +2,13 @@ namespace Props.Shop.Adafruit { public class Configuration { + public int MinDownloadInterval { get; set; } + public float Similarity { get; set; } public Configuration() { + MinDownloadInterval = 5 * 60 * 1000; Similarity = 0.8f; } } diff --git a/Props-Modules/Props.Shop/Adafruit/Props.Shop.Adafruit.csproj b/Props-Modules/Props.Shop/Adafruit/Props.Shop.Adafruit.csproj index 4ed8f78..7f0c8f5 100644 --- a/Props-Modules/Props.Shop/Adafruit/Props.Shop.Adafruit.csproj +++ b/Props-Modules/Props.Shop/Adafruit/Props.Shop.Adafruit.csproj @@ -7,7 +7,6 @@ - diff --git a/Props-Modules/Props.Shop/Framework/Filters.cs b/Props-Modules/Props.Shop/Framework/Filters.cs index 72c354d..9d73e97 100644 --- a/Props-Modules/Props.Shop/Framework/Filters.cs +++ b/Props-Modules/Props.Shop/Framework/Filters.cs @@ -5,7 +5,19 @@ namespace Props.Shop.Framework public class Filters { public Currency Currency { get; set; } = Currency.CAD; - public float MinRating { get; set; } = 0.8f; + private float minRatingNormalized; + public int MinRating + { + get + { + return (int)(minRatingNormalized * 100); + } + set + { + if (value < 0 || value > 100) return; + minRatingNormalized = value / 100f; + } + } public bool KeepUnrated { get; set; } = true; public bool EnableUpperPrice { get; set; } = false; private int upperPrice; diff --git a/Props-Modules/Props.Shop/Framework/IShop.cs b/Props-Modules/Props.Shop/Framework/IShop.cs index 3e60f9d..341ffd0 100644 --- a/Props-Modules/Props.Shop/Framework/IShop.cs +++ b/Props-Modules/Props.Shop/Framework/IShop.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; namespace Props.Shop.Framework { @@ -12,10 +13,11 @@ namespace Props.Shop.Framework string ShopDescription { get; } string ShopModuleAuthor { get; } - public IEnumerable Search(string query, Filters filters); + public IAsyncEnumerable Search(string query, Filters filters); - void Initialize(string workspaceDir); - Task InitializeAsync(string workspaceDir); + public Task GetProductListingFromUrl(string url); + + void Initialize(string workspaceDir, ILoggerFactory loggerFactory); public SupportedFeatures SupportedFeatures { get; } } } \ No newline at end of file diff --git a/Props-Modules/Props.Shop/Framework/Props.Shop.Framework.csproj b/Props-Modules/Props.Shop/Framework/Props.Shop.Framework.csproj index f208d30..c906632 100644 --- a/Props-Modules/Props.Shop/Framework/Props.Shop.Framework.csproj +++ b/Props-Modules/Props.Shop/Framework/Props.Shop.Framework.csproj @@ -4,4 +4,8 @@ net5.0 + + + + diff --git a/Props-Modules/test/Props.Shop/Adafruit.Tests/AdafruitShopTest.cs b/Props-Modules/test/Props.Shop/Adafruit.Tests/AdafruitShopTest.cs index 46f8e1c..cf37472 100644 --- a/Props-Modules/test/Props.Shop/Adafruit.Tests/AdafruitShopTest.cs +++ b/Props-Modules/test/Props.Shop/Adafruit.Tests/AdafruitShopTest.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Props.Shop.Framework; using Xunit; @@ -8,16 +10,12 @@ namespace Props.Shop.Adafruit.Tests public class AdafruitShopTest { [Fact] - public async Task TestSearch() { + public void TestSearch() { AdafruitShop mockAdafruitShop = new AdafruitShop(); - mockAdafruitShop.Initialize(null); - await mockAdafruitShop.InitializeAsync(null); - int count = 0; - foreach (ProductListing listing in mockAdafruitShop.Search("raspberry pi", new Filters())) - { - count += 1; - } - Assert.True(count > 0); + mockAdafruitShop.Initialize(null, LoggerFactory.Create(builder => { + builder.AddXUnit(); + })); + Assert.NotEmpty(mockAdafruitShop.Search("raspberry pi", new Filters()).ToEnumerable()); } } } \ No newline at end of file diff --git a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs index b7e3ac8..61ad595 100644 --- a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs +++ b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/FakeProductListingManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Threading; @@ -10,58 +11,65 @@ namespace Props.Shop.Adafruit.Tests.Api { public class FakeProductListingManager : IProductListingManager { - private ProductListingsParser parser; - private Timer updateTimer; + private Timer refreshTimer; private bool disposedValue; + private volatile Task>> activeListings; + private DateTime? lastDownload; + private ProductListingsParser parser = new ProductListingsParser(); + private readonly ConcurrentDictionary activeProductListingUrls = new ConcurrentDictionary(); - private volatile Dictionary> activeListings; - public IDictionary> ActiveListings => activeListings; + public Task>> ProductListings => activeListings; - public event EventHandler DataUpdateEvent; - - public FakeProductListingManager() + public async Task GetProductListingFromUrl(string url) { - parser = new ProductListingsParser(); + if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); + await activeListings; + return activeProductListingUrls[url]; } - public Task DownloadListings() + public void RefreshProductListings() { - if (disposedValue) throw new ObjectDisposedException("FakeProductListingManager"); - using (Stream stream = File.OpenRead("./Assets/products.json")) - { - parser.BuildProductListings(stream); - } + if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); + if ((lastDownload != null && DateTime.UtcNow - lastDownload <= TimeSpan.FromMilliseconds(5 * 60 * 1000)) || (activeListings != null && !activeListings.IsCompleted)) return; + activeListings = DownloadListings(); + } - Dictionary> results = new Dictionary>(); + private Task>> DownloadListings() { + if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); + lastDownload = DateTime.UtcNow; + parser.BuildProductListings(File.OpenRead("./Assets/products.json")); + Dictionary> listingNames = new Dictionary>(); + activeProductListingUrls.Clear(); foreach (ProductListing product in parser.ProductListings) { - IList sameProducts = results.GetValueOrDefault(product.Name); + activeProductListingUrls.TryAdd(product.URL, product); + IList sameProducts = listingNames.GetValueOrDefault(product.Name); if (sameProducts == null) { sameProducts = new List(); - results.Add(product.Name, sameProducts); + listingNames.Add(product.Name, sameProducts); } sameProducts.Add(product); } - activeListings = results; - DataUpdateEvent?.Invoke(this, null); - return Task.CompletedTask; + return Task.FromResult>>(listingNames); } public void StartUpdateTimer(int delay = 300000, int period = 300000) { - if (disposedValue) throw new ObjectDisposedException("FakeProductListingManager"); - if (updateTimer != null) throw new InvalidOperationException("Update timer already started."); - updateTimer = new Timer((state) => DownloadListings(), null, delay, period); + if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); + if (refreshTimer != null) throw new InvalidOperationException("Refresh timer already running."); + refreshTimer = new Timer((state) => { + RefreshProductListings(); + }, null, delay, period); } public void StopUpdateTimer() { - if (disposedValue) throw new ObjectDisposedException("FakeProductListingManager"); - if (updateTimer == null) throw new InvalidOperationException("Update timer not started."); - updateTimer.Dispose(); - updateTimer = null; + if (disposedValue) throw new ObjectDisposedException("ProductListingManager"); + if (refreshTimer == null) throw new InvalidOperationException("Refresh timer not running."); + refreshTimer.Dispose(); + refreshTimer = null; } protected virtual void Dispose(bool disposing) @@ -70,8 +78,8 @@ namespace Props.Shop.Adafruit.Tests.Api { if (disposing) { - updateTimer?.Dispose(); - updateTimer = null; + refreshTimer?.Dispose(); + refreshTimer = null; } disposedValue = true; diff --git a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/SearchManagerTest.cs b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/SearchManagerTest.cs index a733d03..c7b1399 100644 --- a/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/SearchManagerTest.cs +++ b/Props-Modules/test/Props.Shop/Adafruit.Tests/Api/SearchManagerTest.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Threading.Tasks; using Props.Shop.Adafruit.Api; using Xunit; @@ -7,13 +8,13 @@ namespace Props.Shop.Adafruit.Tests.Api public class SearchManagerTest { [Fact] - public async Task SearchTest() + public void SearchTest() { FakeProductListingManager stubProductListingManager = new FakeProductListingManager(); SearchManager searchManager = new SearchManager(stubProductListingManager); - await stubProductListingManager.DownloadListings(); + stubProductListingManager.RefreshProductListings(); searchManager.Similarity = 0.8f; - Assert.NotEmpty(searchManager.Search("Raspberry Pi")); + Assert.NotEmpty(searchManager.Search("Raspberry Pi").ToEnumerable()); searchManager.Dispose(); } } diff --git a/Props-Modules/test/Props.Shop/Adafruit.Tests/Props.Shop.Adafruit.Tests.csproj b/Props-Modules/test/Props.Shop/Adafruit.Tests/Props.Shop.Adafruit.Tests.csproj index 11e4d36..5e9fcbf 100644 --- a/Props-Modules/test/Props.Shop/Adafruit.Tests/Props.Shop.Adafruit.Tests.csproj +++ b/Props-Modules/test/Props.Shop/Adafruit.Tests/Props.Shop.Adafruit.Tests.csproj @@ -7,7 +7,10 @@ + + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Props/Options/ContentOptions.cs b/Props/Options/ContentOptions.cs deleted file mode 100644 index 065dbf2..0000000 --- a/Props/Options/ContentOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Props.Options -{ - public class ContentOptions - { - public const string Content = "Content"; - public string Dir { get; set; } - } -} \ No newline at end of file diff --git a/Props/Options/ModulesOptions.cs b/Props/Options/ModulesOptions.cs index 3b53328..c52a479 100644 --- a/Props/Options/ModulesOptions.cs +++ b/Props/Options/ModulesOptions.cs @@ -3,7 +3,8 @@ namespace Props.Options public class ModulesOptions { public const string Modules = "Modules"; - public string ShopsDir { get; set; } + public string ModulesDir { get; set; } + public string ModuleDataDir { get; set; } public bool RecursiveLoad { get; set; } public string ShopRegex { get; set; } } diff --git a/Props/Options/TextualOptions.cs b/Props/Options/TextualOptions.cs new file mode 100644 index 0000000..7b7e806 --- /dev/null +++ b/Props/Options/TextualOptions.cs @@ -0,0 +1,8 @@ +namespace Props.Options +{ + public class TextualOptions + { + public const string Textual = "Textual"; + public string Dir { get; set; } + } +} \ No newline at end of file diff --git a/Props/Pages/Index.cshtml b/Props/Pages/Index.cshtml index c732a78..dbfcf9c 100644 --- a/Props/Pages/Index.cshtml +++ b/Props/Pages/Index.cshtml @@ -1,14 +1,15 @@ @page @using Props.Services.Content @model IndexModel -@inject IContentManager ContentManager +@inject ITextualManager ContentManager @{ ViewData["Title"] = "Home page"; }
- Props logo + Props logo

Props

@@ -24,7 +25,8 @@

@ContentManager.Json.help.title

- +
diff --git a/Props/Pages/Search.cshtml b/Props/Pages/Search.cshtml index 8a135ae..5c4d682 100644 --- a/Props/Pages/Search.cshtml +++ b/Props/Pages/Search.cshtml @@ -1,28 +1,26 @@ @page @using Props.Services.Content @model SearchModel -@inject IContentManager ContentManager +@inject ITextualManager ContentManager @{ ViewData["Title"] = "Search"; ViewData["Specific"] = "Search"; } -
-
+
+
+ aria-label="Search" aria-describedby="search-btn" id="search-bar" value="@Model.SearchQuery" name="q"> - +
-
-
-
-
+
+

Configuration

@@ -47,7 +48,9 @@
$ - + .00
@@ -55,17 +58,22 @@
- +
$ - + .00
+ checked="@Model.ActiveSearchOutline.Filters.KeepUnknownShipping" + name="ActiveSearchOutline.Filters.KeepUnknownShipping">
@@ -75,28 +83,34 @@
- + Purchases
+ checked="@Model.ActiveSearchOutline.Filters.KeepUnknownPurchaseCount" + name="ActiveSearchOutline.Filters.KeepUnknownPurchaseCount">
- + Reviews
+ checked="@Model.ActiveSearchOutline.Filters.KeepUnknownReviewCount" + name="ActiveSearchOutline.Filters.KeepUnknownReviewCount">
@@ -104,12 +118,15 @@
-
Minimum rating: %
+ value="@Model.ActiveSearchOutline.Filters.MinRating" + name="ActiveSearchOutline.Filters.MinRating"> +
- +
@@ -117,20 +134,20 @@

Shops Enabled

- + }
-
+