diff --git a/CHANGELOG.md b/CHANGELOG.md index 176fc71..b620244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [1.0.0] - 25/03/2022 + +### Added + - Support for docx and doc + +### Changed + - Improved status bar responsivity + ## [0.2.0] - 15/05/2021 ### Added diff --git a/InDepthSearch.Core/Enums/AppLanguage.cs b/InDepthSearch.Core/Enums/AppLanguage.cs new file mode 100644 index 0000000..802cdb3 --- /dev/null +++ b/InDepthSearch.Core/Enums/AppLanguage.cs @@ -0,0 +1,10 @@ + +namespace InDepthSearch.Core.Enums +{ + public enum AppLanguage + { + English, + Polski, + Francais + } +} diff --git a/InDepthSearch.Core/Enums/ImageExtension.cs b/InDepthSearch.Core/Enums/ImageExtension.cs new file mode 100644 index 0000000..a4d5d84 --- /dev/null +++ b/InDepthSearch.Core/Enums/ImageExtension.cs @@ -0,0 +1,10 @@ + +namespace InDepthSearch.Core.Enums +{ + public enum ImageExtension + { + Jpg, + Png, + Bmp + } +} diff --git a/InDepthSearch.Core/Enums/MatchConfidence.cs b/InDepthSearch.Core/Enums/MatchConfidence.cs new file mode 100644 index 0000000..3430d7f --- /dev/null +++ b/InDepthSearch.Core/Enums/MatchConfidence.cs @@ -0,0 +1,11 @@ + +namespace InDepthSearch.Core.Enums +{ + public enum MatchConfidence + { + High, // found exact expression + Medium, // found the all the words from the expression + Low // found part of the words from the expression + } + +} diff --git a/InDepthSearch.Core/Enums/RecognitionLanguage.cs b/InDepthSearch.Core/Enums/RecognitionLanguage.cs new file mode 100644 index 0000000..fec1bc9 --- /dev/null +++ b/InDepthSearch.Core/Enums/RecognitionLanguage.cs @@ -0,0 +1,11 @@ + +namespace InDepthSearch.Core.Enums +{ + public enum RecognitionLanguage + { + Default, + English, + French, + Polish + } +} diff --git a/InDepthSearch.Core/Enums/RecognitionPrecision.cs b/InDepthSearch.Core/Enums/RecognitionPrecision.cs new file mode 100644 index 0000000..6d3fc5b --- /dev/null +++ b/InDepthSearch.Core/Enums/RecognitionPrecision.cs @@ -0,0 +1,11 @@ + +namespace InDepthSearch.Core.Enums +{ + public enum RecognitionPrecision + { + Default, + High, + Medium, + Low + } +} diff --git a/InDepthSearch.Core/Enums/SearchInfo.cs b/InDepthSearch.Core/Enums/SearchInfo.cs new file mode 100644 index 0000000..4566418 --- /dev/null +++ b/InDepthSearch.Core/Enums/SearchInfo.cs @@ -0,0 +1,11 @@ + +namespace InDepthSearch.Core.Enums +{ + public enum SearchInfo + { + Unknown, + Init, + Run, + NoResults, + } +} diff --git a/InDepthSearch.Core/Enums/SearchStatus.cs b/InDepthSearch.Core/Enums/SearchStatus.cs new file mode 100644 index 0000000..69cb18f --- /dev/null +++ b/InDepthSearch.Core/Enums/SearchStatus.cs @@ -0,0 +1,11 @@ + +namespace InDepthSearch.Core.Enums +{ + public enum SearchStatus + { + Unknown, + Ready, + Initializing, + Running, + } +} diff --git a/InDepthSearch.Core/Enums/Theme.cs b/InDepthSearch.Core/Enums/Theme.cs new file mode 100644 index 0000000..d198055 --- /dev/null +++ b/InDepthSearch.Core/Enums/Theme.cs @@ -0,0 +1,10 @@ + +namespace InDepthSearch.Core.Enums +{ + public enum Theme + { + Default, + Light, + Dark, + } +} diff --git a/InDepthSearch.Core/InDepthSearch.Core.csproj b/InDepthSearch.Core/InDepthSearch.Core.csproj index 5cd0bf2..1b49eb8 100644 --- a/InDepthSearch.Core/InDepthSearch.Core.csproj +++ b/InDepthSearch.Core/InDepthSearch.Core.csproj @@ -4,15 +4,15 @@ Library enable net5.0 - 0.2.0 + 1.0.0 + - + - diff --git a/InDepthSearch.Core/Managers/Interfaces/IResultManager.cs b/InDepthSearch.Core/Managers/Interfaces/IResultManager.cs new file mode 100644 index 0000000..bda4e57 --- /dev/null +++ b/InDepthSearch.Core/Managers/Interfaces/IResultManager.cs @@ -0,0 +1,16 @@ + +using InDepthSearch.Core.Models; +using System.Collections.ObjectModel; + +namespace InDepthSearch.Core.Managers.Interfaces +{ + public interface IResultManager + { + ObservableCollection Results { get; } + ResultStats Stats { get; } + bool ItemsReady { get; set; } + void Reinitialize(); + void AddResult(QueryResult res); + void SetItemsReady(bool ready); + } +} diff --git a/InDepthSearch.Core/Models/QueryResult.cs b/InDepthSearch.Core/Models/QueryResult.cs index 2e996a0..6029a32 100644 --- a/InDepthSearch.Core/Models/QueryResult.cs +++ b/InDepthSearch.Core/Models/QueryResult.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using InDepthSearch.Core.Types; +using InDepthSearch.Core.Enums; namespace InDepthSearch.Core.Models { diff --git a/InDepthSearch.Core/Models/ResultStats.cs b/InDepthSearch.Core/Models/ResultStats.cs index 0d5f86b..068f49a 100644 --- a/InDepthSearch.Core/Models/ResultStats.cs +++ b/InDepthSearch.Core/Models/ResultStats.cs @@ -10,19 +10,16 @@ namespace InDepthSearch.Core.Models { public class ResultStats : ReactiveObject { - public ResultStats(string filesAnalyzed, bool isReady, int pagesAnalyzed, string executionTime) + public ResultStats() { - FilesAnalyzed = filesAnalyzed; - IsReady = isReady; - PagesAnalyzed = pagesAnalyzed; - ExecutionTime = executionTime; + FilesAnalyzed = ""; + PagesAnalyzed = 0; + ExecutionTime = ""; } [Reactive] public string FilesAnalyzed { get; set; } [Reactive] - public bool IsReady { get; set; } - [Reactive] public int PagesAnalyzed { get; set; } [Reactive] public string ExecutionTime { get; set; } diff --git a/InDepthSearch.Core/Models/SearchOptions.cs b/InDepthSearch.Core/Models/SearchOptions.cs index e36490d..b4cc1b2 100644 --- a/InDepthSearch.Core/Models/SearchOptions.cs +++ b/InDepthSearch.Core/Models/SearchOptions.cs @@ -1,4 +1,4 @@ -using InDepthSearch.Core.Types; +using InDepthSearch.Core.Enums; using ReactiveUI; using ReactiveUI.Fody.Helpers; @@ -6,19 +6,14 @@ namespace InDepthSearch.Core.Models { public class SearchOptions : ReactiveObject { - public SearchOptions(string path, string keyword, RecognitionPrecision selectedPrecisionOCR, RecognitionLanguage selectedLanguageOCR, - bool caseSensitive, bool useOCR, bool useSubfolders, bool usePDF, bool useDOCX, bool useODT) + public SearchOptions() { - Path = path; - Keyword = keyword; - SelectedPrecisionOCR = selectedPrecisionOCR; - SelectedLanguageOCR = selectedLanguageOCR; - CaseSensitive = caseSensitive; - UseOCR = useOCR; - UseSubfolders = useSubfolders; - UsePDF = usePDF; - UseDOCX = useDOCX; - UseODT = useODT; + UseOCR = true; + UsePDF = true; + Path = ""; + Keyword = ""; + SelectedLanguageOCR = RecognitionLanguage.Default; + SelectedPrecisionOCR = RecognitionPrecision.Default; } [Reactive] @@ -28,10 +23,16 @@ public class SearchOptions : ReactiveObject public RecognitionPrecision SelectedPrecisionOCR { get; set; } public RecognitionLanguage SelectedLanguageOCR { get; set; } public bool CaseSensitive { get; set; } + [Reactive] public bool UseOCR { get; set; } public bool UseSubfolders { get; set; } + [Reactive] public bool UsePDF { get; set; } + [Reactive] public bool UseDOCX { get; set; } + [Reactive] public bool UseODT { get; set; } + [Reactive] + public bool UseDOC { get; set; } } } diff --git a/InDepthSearch.Core/Services/Interfaces/IAppService.cs b/InDepthSearch.Core/Services/Interfaces/IAppService.cs index 49d362b..52e7ed6 100644 --- a/InDepthSearch.Core/Services/Interfaces/IAppService.cs +++ b/InDepthSearch.Core/Services/Interfaces/IAppService.cs @@ -1,4 +1,4 @@ -using InDepthSearch.Core.Types; +using InDepthSearch.Core.Enums; using System; using System.Collections.Generic; using System.Linq; diff --git a/InDepthSearch.Core/Services/Interfaces/IOptionService.cs b/InDepthSearch.Core/Services/Interfaces/IOptionService.cs index a533577..30df04d 100644 --- a/InDepthSearch.Core/Services/Interfaces/IOptionService.cs +++ b/InDepthSearch.Core/Services/Interfaces/IOptionService.cs @@ -1,17 +1,11 @@ using Docnet.Core.Models; -using InDepthSearch.Core.Types; -using System; -using System.Collections.Generic; -using System.Drawing.Imaging; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using InDepthSearch.Core.Enums; namespace InDepthSearch.Core.Services.Interfaces { public interface IOptionService { - public (PageDimensions, RenderFlags, PixelFormat, ImageFormat) TranslatePrecision(RecognitionPrecision rp); + public (PageDimensions, RenderFlags, ImageExtension) TranslatePrecision(RecognitionPrecision rp); public string TranslateLanguage(RecognitionLanguage rl); } } diff --git a/InDepthSearch.Core/Services/Interfaces/ISearchService.cs b/InDepthSearch.Core/Services/Interfaces/ISearchService.cs new file mode 100644 index 0000000..a871e69 --- /dev/null +++ b/InDepthSearch.Core/Services/Interfaces/ISearchService.cs @@ -0,0 +1,10 @@ +using InDepthSearch.Core.Enums; +using InDepthSearch.Core.Models; + +namespace InDepthSearch.Core.Services.Interfaces +{ + public interface ISearchService + { + void Search(string file, SearchOptions searchOptions); + } +} diff --git a/InDepthSearch.Core/Services/Interfaces/IThemeService.cs b/InDepthSearch.Core/Services/Interfaces/IThemeService.cs index 44cb773..7aa3941 100644 --- a/InDepthSearch.Core/Services/Interfaces/IThemeService.cs +++ b/InDepthSearch.Core/Services/Interfaces/IThemeService.cs @@ -1,4 +1,4 @@ -using InDepthSearch.Core.Types; +using InDepthSearch.Core.Enums; using System; using System.Collections.Generic; using System.Linq; diff --git a/InDepthSearch.Core/Types/AppLanguage.cs b/InDepthSearch.Core/Types/AppLanguage.cs deleted file mode 100644 index e4955fc..0000000 --- a/InDepthSearch.Core/Types/AppLanguage.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace InDepthSearch.Core.Types -{ - public enum AppLanguage: int - { - English, - Polski, - Francais - } -} diff --git a/InDepthSearch.Core/Types/MatchConfidence.cs b/InDepthSearch.Core/Types/MatchConfidence.cs deleted file mode 100644 index e29de82..0000000 --- a/InDepthSearch.Core/Types/MatchConfidence.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace InDepthSearch.Core.Types -{ - public enum MatchConfidence : int - { - High, // found exact expression - Medium, // found the all the words from the expression - Low // found part of the words from the expression - } - -} diff --git a/InDepthSearch.Core/Types/RecognitionLanguage.cs b/InDepthSearch.Core/Types/RecognitionLanguage.cs deleted file mode 100644 index f50f5d8..0000000 --- a/InDepthSearch.Core/Types/RecognitionLanguage.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace InDepthSearch.Core.Types -{ - public enum RecognitionLanguage : int - { - Default, - English, - French, - Polish - } -} diff --git a/InDepthSearch.Core/Types/RecognitionPrecision.cs b/InDepthSearch.Core/Types/RecognitionPrecision.cs deleted file mode 100644 index 15bd441..0000000 --- a/InDepthSearch.Core/Types/RecognitionPrecision.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace InDepthSearch.Core.Types -{ - public enum RecognitionPrecision : int - { - Default, - High, - Medium, - Low - } -} diff --git a/InDepthSearch.Core/Types/SearchInfo.cs b/InDepthSearch.Core/Types/SearchInfo.cs deleted file mode 100644 index ba0b054..0000000 --- a/InDepthSearch.Core/Types/SearchInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace InDepthSearch.Core.Types -{ - public enum SearchInfo - { - Unknown, - Init, - Run, - NoResults, - } -} diff --git a/InDepthSearch.Core/Types/SearchStatus.cs b/InDepthSearch.Core/Types/SearchStatus.cs deleted file mode 100644 index 001e508..0000000 --- a/InDepthSearch.Core/Types/SearchStatus.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace InDepthSearch.Core.Types -{ - public enum SearchStatus : int - { - Unknown, - Ready, - Initializing, - Running, - } -} diff --git a/InDepthSearch.Core/Types/Theme.cs b/InDepthSearch.Core/Types/Theme.cs deleted file mode 100644 index 191c4c7..0000000 --- a/InDepthSearch.Core/Types/Theme.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace InDepthSearch.Core.Types -{ - public enum Theme : int - { - Default, - Light, - Dark, - } -} diff --git a/InDepthSearch.Core/ViewModels/MainViewModel.cs b/InDepthSearch.Core/ViewModels/MainViewModel.cs deleted file mode 100644 index e345ac4..0000000 --- a/InDepthSearch.Core/ViewModels/MainViewModel.cs +++ /dev/null @@ -1,299 +0,0 @@ -using Docnet.Core; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using System.Reactive; -using System.Runtime.InteropServices; -using Tesseract; -using System.Linq; -using System.Collections.ObjectModel; -using InDepthSearch.Core.Models; -using InDepthSearch.Core.Types; -using System.Threading; -using ReactiveUI.Validation.Helpers; -using ReactiveUI.Validation.Extensions; -using InDepthSearch.Core.Services.Interfaces; - -namespace InDepthSearch.Core.ViewModels -{ - public class MainViewModel : ReactiveValidationObject - { - public string Logo => "avares://InDepthSearch.UI/Assets/Images/ids-logo.png"; - - [Reactive] - public ObservableCollection Results { get; set; } - [Reactive] - public SearchOptions Options { get; set; } - [Reactive] - public ResultStats Stats { get; set; } - [Reactive] - public ObservableCollection PrecisionOCR { get; set; } - [Reactive] - public ObservableCollection LanguageOCR { get; set; } - public ReactiveCommand ReadPDF { get; } - public ReactiveCommand GetDirectory { get; } - public ReactiveCommand ChangeTheme { get; } - public ReactiveCommand ChangeLanguage { get; } - - [Reactive] - public string ResultInfo { get; set; } - [Reactive] - public bool KeywordErrorVisible { get; set; } - [Reactive] - public bool PathErrorVisible { get; set; } - [Reactive] - public string AppVersion { get; set; } - [Reactive] - public string CurrentThemeName { get; set; } - [Reactive] - public string CurrentLanguageName { get; set; } - [Reactive] - public bool ItemsReady { get; set; } - [Reactive] - public string StatusName { get; set; } - - private Thread? _th; - private readonly IDocLib _docLib; - private readonly IOptionService _optionService; - private readonly IDirectoryService _directoryService; - private readonly IThemeService _themeService; - private readonly IAppService _infoService; - - #region Empty constructor only for the designer -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public MainViewModel() - { - // Initialize variables - PrecisionOCR = new ObservableCollection(Enum.GetValues(typeof(RecognitionPrecision)).Cast()); - LanguageOCR = new ObservableCollection(Enum.GetValues(typeof(RecognitionLanguage)).Cast()); - Options = new SearchOptions("", "", PrecisionOCR.FirstOrDefault(), LanguageOCR.FirstOrDefault(), - false, true, false, true, false, false); - Results = new ObservableCollection(); - Stats = new ResultStats("0/0", true, 0, "0"); - StatusName = SearchStatus.Ready.ToString(); - ResultInfo = "Click search button to start"; - CurrentThemeName = Theme.Default.ToString().ToUpper(); - CurrentLanguageName = AppLanguage.English.ToString().ToUpper(); - ItemsReady = false; - - // Subscribe for events and set validation rules - ErrorsChanged += OnValidationErrorsChanged; - this.ValidationRule(x => x.Options.Keyword, key => !string.IsNullOrEmpty(key), "Keyword cannot be empty!"); - this.ValidationRule(x => x.Options.Path, key => !string.IsNullOrEmpty(key) && Directory.Exists(key), "Path has to be valid!"); - - AppVersion = "x.x.x"; - } -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - #endregion - public MainViewModel(IOptionService optionService, IDirectoryService directoryService, - IAppService infoService, IThemeService themeService) - { - // Initialize services - _docLib = DocLib.Instance; - _optionService = optionService; - _directoryService = directoryService; - _themeService = themeService; - _infoService = infoService; - - // Initialize commands - GetDirectory = ReactiveCommand.Create(BrowseDirectory); - ReadPDF = ReactiveCommand.Create(() => { - _th = new Thread(() => StartReading()); - _th.IsBackground = true; - _th.Start(); - }, this.IsValid()); - ChangeTheme = ReactiveCommand.Create(() => - { - themeService.ChangeTheme(); - CurrentThemeName = themeService.GetCurrentThemeName(); - }); - ChangeLanguage = ReactiveCommand.Create(() => - { - infoService.ChangeLanguage(); - CurrentLanguageName = infoService.GetCurrentLanguage(); - UpdateStringResources(); - }); - - // Initialize variables - PrecisionOCR = new ObservableCollection(Enum.GetValues(typeof(RecognitionPrecision)).Cast()); - LanguageOCR = new ObservableCollection(Enum.GetValues(typeof(RecognitionLanguage)).Cast()); - Options = new SearchOptions("", "", PrecisionOCR.FirstOrDefault(), LanguageOCR.FirstOrDefault(), - false, true, false, true, false, false); - Results = new ObservableCollection(); - Stats = new ResultStats("0/0", true, 0, "0"); - ResultInfo = infoService.GetSearchInfo(SearchInfo.Init); - StatusName = infoService.GetSearchStatus(SearchStatus.Ready); - CurrentThemeName = themeService.GetCurrentThemeName(); - CurrentLanguageName = infoService.GetCurrentLanguage(); - ItemsReady = false; - - // Subscribe for events and set validation rules - ErrorsChanged += OnValidationErrorsChanged; - this.ValidationRule(x => x.Options.Keyword, key => !string.IsNullOrEmpty(key), "Keyword cannot be empty!"); - this.ValidationRule(x => x.Options.Path, key => !string.IsNullOrEmpty(key) && Directory.Exists(key), "Path has to be valid!"); - - // Get assembly version - AppVersion = infoService.GetVersion(); - - } - - private void UpdateStringResources() - { - CurrentThemeName = _themeService.GetCurrentThemeName(); - StatusName = _infoService.GetSearchStatus(); - ResultInfo = _infoService.GetSearchInfo(); - } - - private void OnValidationErrorsChanged(object? sender, System.ComponentModel.DataErrorsChangedEventArgs e) - { - KeywordErrorVisible = string.IsNullOrWhiteSpace(Options.Keyword); - PathErrorVisible = !Directory.Exists(Options.Path); - } - - void StartReading() - { - var searchOptions = Options; - - Results.Clear(); - StatusName = _infoService.GetSearchStatus(SearchStatus.Initializing); - ItemsReady = false; - Stats.IsReady = false; - Stats.FilesAnalyzed = "0/0"; - Stats.PagesAnalyzed = 0; - Stats.ExecutionTime = "..."; - - var fileCounter = 0; - var watch = System.Diagnostics.Stopwatch.StartNew(); - - if (!string.IsNullOrWhiteSpace(searchOptions.Keyword)) - { - List discoveredFiles = searchOptions.UseSubfolders ? Directory.GetFiles(searchOptions.Path, "*.pdf", SearchOption.AllDirectories).ToList() - : Directory.GetFiles(searchOptions.Path, "*.pdf").ToList(); - - if (discoveredFiles == null) - { - System.Diagnostics.Debug.WriteLine("No files found... "); - return; - } - - StatusName = _infoService.GetSearchStatus(SearchStatus.Running); - ResultInfo = _infoService.GetSearchInfo(SearchInfo.Init); - Stats.FilesAnalyzed = "0/" + discoveredFiles.Count.ToString(); - - foreach (var pdf in discoveredFiles) - { - System.Diagnostics.Debug.WriteLine("Checking " + pdf); - using var docReader = _docLib.GetDocReader(pdf, _optionService.TranslatePrecision(searchOptions.SelectedPrecisionOCR).Item1); - - for (var i = 0; i < docReader.GetPageCount(); i++) - { - using var pageReader = docReader.GetPageReader(i); - var parsedText = pageReader.GetText().ToString(); - - if (searchOptions.UseOCR && string.IsNullOrWhiteSpace(parsedText)) - { - var rawBytes = pageReader.GetImage(_optionService.TranslatePrecision(searchOptions.SelectedPrecisionOCR).Item2); - var width = pageReader.GetPageWidth(); - var height = pageReader.GetPageHeight(); - using var bmp = new Bitmap(width, height, _optionService.TranslatePrecision(searchOptions.SelectedPrecisionOCR).Item3); - - AddBytes(bmp, rawBytes); - using var stream = new MemoryStream(); - bmp.Save(stream, _optionService.TranslatePrecision(searchOptions.SelectedPrecisionOCR).Item4); - - parsedText = ImageToText(stream.ToArray(), searchOptions.SelectedLanguageOCR, searchOptions.SelectedPrecisionOCR); - } - - SearchPage(parsedText, searchOptions.Keyword, pdf, i, searchOptions.CaseSensitive); - Stats.PagesAnalyzed += 1; - - } - fileCounter += 1; - Stats.FilesAnalyzed = fileCounter.ToString() + "/" + discoveredFiles.Count.ToString(); - } - } - - watch.Stop(); - var elapsedMs = watch.ElapsedMilliseconds; - System.Diagnostics.Debug.WriteLine("Total execution " + elapsedMs); - Stats.ExecutionTime = (elapsedMs / 1000.0).ToString() + " " + _infoService.GetSecondsString(); - Stats.IsReady = true; - StatusName = _infoService.GetSearchStatus(SearchStatus.Ready); - if (!Results.Any()) - { - ResultInfo = _infoService.GetSearchInfo(SearchInfo.NoResults); - ItemsReady = false; - } - } - - private static void AddBytes(Bitmap bmp, byte[] rawBytes) - { - var rect = new Rectangle(0, 0, bmp.Width, bmp.Height); - - var bmpData = bmp.LockBits(rect, ImageLockMode.WriteOnly, bmp.PixelFormat); - var pNative = bmpData.Scan0; - - Marshal.Copy(rawBytes, 0, pNative, rawBytes.Length); - bmp.UnlockBits(bmpData); - } - - string ImageToText(byte[] imageBytes, RecognitionLanguage rl, RecognitionPrecision rp) - { - try - { - using var engine = new TesseractEngine(@"./Files", _optionService.TranslateLanguage(rl), EngineMode.Default); - using var img = _optionService.TranslatePrecision(rp).Item4 == System.Drawing.Imaging.ImageFormat.Tiff ? - Pix.LoadTiffFromMemory(imageBytes) : Pix.LoadFromMemory(imageBytes); - using var pager = engine.Process(img); - return pager.GetText().ToString(); - //System.Diagnostics.Debug.WriteLine("Mean confidence: {0}", pager.GetMeanConfidence()); - //System.Diagnostics.Debug.WriteLine("Text {0}", text); - } - catch (Exception ee) - { - System.Diagnostics.Debug.WriteLine("Unexpected Error: " + ee.Message); - System.Diagnostics.Debug.WriteLine("Details: "); - System.Diagnostics.Debug.WriteLine(ee.ToString()); - } - - return ""; - } - - void SearchPage(string rawText, string keyword, string filePath, int pageNum, bool isCaseSensitive) - { - - var searchIndex = 0; - var at = 0; - string textBefore, textFound, textAfter; - var offset = 30; - var text = rawText.Replace("\n", " ").Replace("\r", " "); - - while (at > -1) - { - if (!ItemsReady) ItemsReady = true; - at = isCaseSensitive ? text.IndexOf(keyword, searchIndex) : text.ToLower().IndexOf(keyword.ToLower(), searchIndex); - if (at == -1) break; - System.Diagnostics.Debug.WriteLine("Found the keyword " + keyword + " in doc: " + filePath + " on page " + pageNum + " at " + at + " position!"); - textBefore = "..." + text.Substring(Math.Max(0, at - offset), at < offset ? at : offset); - textAfter = text.Substring(at + keyword.Length, at + keyword.Length + offset > text.Length ? text.Length - at - keyword.Length : offset) + "..."; - textFound = text.Substring(at, keyword.Length); - Results.Add(new QueryResult(MatchConfidence.High, filePath, textBefore, - textFound, textAfter, pageNum)); - searchIndex = at + keyword.Length; - } - } - - async void BrowseDirectory() - { - var newDir = await _directoryService.ChooseDirectory(); - if (!string.IsNullOrEmpty(newDir)) - Options.Path = newDir; - } - - } - -} diff --git a/InDepthSearch.Core/ViewModels/MainWindowViewModel.cs b/InDepthSearch.Core/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..d7c971a --- /dev/null +++ b/InDepthSearch.Core/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,152 @@ +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reactive; +using System.Runtime.InteropServices; +using System.Linq; +using System.Collections.ObjectModel; +using InDepthSearch.Core.Models; +using InDepthSearch.Core.Enums; +using System.Threading; +using InDepthSearch.Core.Services.Interfaces; +using System.Diagnostics; +using InDepthSearch.Core.Managers.Interfaces; + +namespace InDepthSearch.Core.ViewModels +{ + public class MainWindowViewModel: ViewModelBase + { + private readonly IThemeService _themeService; + private readonly IAppService _infoService; + private readonly ISearchService _searchService; + + public MainWindowViewModel(IAppService infoService, IThemeService themeService, IResultManager resultManager, ISearchService searchService, + ResultsViewModel.Factory resultsFactory, OptionsViewModel.Factory optionsFactory) + { + // Initialize services + _themeService = themeService; + _infoService = infoService; + _searchService = searchService; + ResultManager = resultManager; + + ResultsPage = resultsFactory(); + OptionsMenu = optionsFactory(StartReading); + + ChangeTheme = ReactiveCommand.Create(() => + { + themeService.ChangeTheme(); + CurrentThemeName = themeService.GetCurrentThemeName(); + }); + ChangeLanguage = ReactiveCommand.Create(() => + { + infoService.ChangeLanguage(); + CurrentLanguageName = infoService.GetCurrentLanguage(); + UpdateStringResources(); + }); + OpenUrl = ReactiveCommand.Create(() => + { + var url = "https://github.com/radoslawik/InDepthSearch"; + using var process = Process.Start(new ProcessStartInfo + { + FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open", + Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"-e {url}" : "", + CreateNoWindow = true, + UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + }); + }); + + // Initialize variables + StatusName = infoService.GetSearchStatus(SearchStatus.Ready); + CurrentThemeName = themeService.GetCurrentThemeName(); + CurrentLanguageName = infoService.GetCurrentLanguage(); + + // Get assembly version + AppVersion = infoService.GetVersion(); + } + public IResultManager ResultManager { get; } + public ResultsViewModel ResultsPage { get; } + public OptionsViewModel OptionsMenu { get; } + + public ReactiveCommand ChangeTheme { get; } + public ReactiveCommand ChangeLanguage { get; } + public ReactiveCommand OpenUrl { get; } + + [Reactive] + public string AppVersion { get; set; } + [Reactive] + public string CurrentThemeName { get; set; } + [Reactive] + public string CurrentLanguageName { get; set; } + [Reactive] + public string StatusName { get; set; } + + private void UpdateStringResources() + { + CurrentThemeName = _themeService.GetCurrentThemeName(); + StatusName = _infoService.GetSearchStatus(); + ResultsPage.UpdateResultInfo(_infoService.GetSearchInfo()); + } + + private void StartReading(SearchOptions searchOptions) + { + StatusName = _infoService.GetSearchStatus(SearchStatus.Initializing); + ResultManager.Reinitialize(); + + var fileCounter = 0; + var watch = System.Diagnostics.Stopwatch.StartNew(); + + if (!string.IsNullOrWhiteSpace(searchOptions.Keyword)) + { + var allowedExtensions = new List(); + + if (searchOptions.UsePDF) + allowedExtensions.Add(".pdf"); + if (searchOptions.UseDOCX) + allowedExtensions.Add(".docx"); + if (searchOptions.UseDOC) + allowedExtensions.Add(".doc"); + if (searchOptions.UseODT) + allowedExtensions.Add(".odt"); + + List discoveredFiles = searchOptions.UseSubfolders ? + Directory.GetFiles(searchOptions.Path, "*.*", SearchOption.AllDirectories) + .Where(file => allowedExtensions.Any(file.ToLower().EndsWith)).ToList() : + Directory.GetFiles(searchOptions.Path) + .Where(file => allowedExtensions.Any(file.ToLower().EndsWith)).ToList(); + + if (discoveredFiles == null) + { + System.Diagnostics.Debug.WriteLine("No files found... "); + return; + } + + StatusName = _infoService.GetSearchStatus(SearchStatus.Running); + ResultsPage.UpdateResultInfo(_infoService.GetSearchInfo(SearchInfo.Init)); + ResultManager.Stats.FilesAnalyzed = "0/" + discoveredFiles.Count.ToString(); + + foreach (var file in discoveredFiles) + { + System.Diagnostics.Debug.WriteLine("Checking " + file); + + _searchService.Search(file, searchOptions); + fileCounter += 1; + ResultManager.Stats.FilesAnalyzed = fileCounter.ToString() + "/" + discoveredFiles.Count.ToString(); + } + } + + watch.Stop(); + var elapsedMs = watch.ElapsedMilliseconds; + System.Diagnostics.Debug.WriteLine("Total execution " + elapsedMs); + ResultManager.Stats.ExecutionTime = (elapsedMs / 1000.0).ToString() + " " + _infoService.GetSecondsString(); + StatusName = _infoService.GetSearchStatus(SearchStatus.Ready); + if (!ResultManager.Results.Any()) + { + ResultsPage.UpdateResultInfo(_infoService.GetSearchInfo(SearchInfo.NoResults)); + ResultManager.SetItemsReady(false); + } + } + } + +} diff --git a/InDepthSearch.Core/ViewModels/OptionsViewModel.cs b/InDepthSearch.Core/ViewModels/OptionsViewModel.cs new file mode 100644 index 0000000..17a41bf --- /dev/null +++ b/InDepthSearch.Core/ViewModels/OptionsViewModel.cs @@ -0,0 +1,71 @@ +using InDepthSearch.Core.Enums; +using InDepthSearch.Core.Models; +using InDepthSearch.Core.Services.Interfaces; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace InDepthSearch.Core.ViewModels +{ + public class OptionsViewModel : ViewModelBase + { + private Thread? _th; + public delegate OptionsViewModel Factory(Action startSearch); + public OptionsViewModel(IDirectoryService dirService, Action startSearch) + { + PrecisionOCR = new(Enum.GetValues(typeof(RecognitionPrecision)).Cast()); + LanguageOCR = new(Enum.GetValues(typeof(RecognitionLanguage)).Cast()); + Options = new(); + + // Initialize commands + GetDirectory = ReactiveCommand.CreateFromTask(async() => + { + var newDir = await dirService.ChooseDirectory(); + if (!string.IsNullOrEmpty(newDir)) + { + Options.Path = newDir; + } + }); + + ReadPDF = ReactiveCommand.Create(() => + { + _th = new(() => startSearch(Options)); + _th.IsBackground = true; + _th.Start(); + }); + + // Subscribe to validation values + this.WhenAnyValue(x => x.Options.Keyword, x => x.Options.Path, x => x.Options.UseDOC, x => x.Options.UseDOCX, + x => x.Options.UsePDF, x => x.Options.UseODT).Subscribe(x => ValidateSelection()); + } + [Reactive] + public SearchOptions Options { get; set; } + [Reactive] + public ObservableCollection PrecisionOCR { get; set; } + [Reactive] + public ObservableCollection LanguageOCR { get; set; } + public ReactiveCommand ReadPDF { get; } + public ReactiveCommand GetDirectory { get; } + public bool KeywordErrorVisible => string.IsNullOrWhiteSpace(Options.Keyword); + public bool PathErrorVisible => !Directory.Exists(Options.Path); + public bool CanExecute => !KeywordErrorVisible && !PathErrorVisible; + public static string Logo => "avares://InDepthSearch.UI/Assets/Images/ids-logo.png"; + + private void ValidateSelection() + { + this.RaisePropertyChanged(nameof(PathErrorVisible)); + this.RaisePropertyChanged(nameof(KeywordErrorVisible)); + this.RaisePropertyChanged(nameof(CanExecute)); + } + } + + +} diff --git a/InDepthSearch.Core/ViewModels/ResultsViewModel.cs b/InDepthSearch.Core/ViewModels/ResultsViewModel.cs new file mode 100644 index 0000000..cb11ad0 --- /dev/null +++ b/InDepthSearch.Core/ViewModels/ResultsViewModel.cs @@ -0,0 +1,38 @@ +using InDepthSearch.Core.Enums; +using InDepthSearch.Core.Managers.Interfaces; +using InDepthSearch.Core.Services.Interfaces; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace InDepthSearch.Core.ViewModels +{ + public class ResultsViewModel : ViewModelBase + { + public delegate ResultsViewModel Factory(); + public ResultsViewModel(IResultManager resultManager, IAppService appService) + { + ResultManager = resultManager; + ResultInfo = appService.GetSearchInfo(SearchInfo.Init); + ResultManager.Results.CollectionChanged += (s, e) => Teestc(); + } + + private void Teestc() + { + var test = ResultManager.Results.Count; + this.RaisePropertyChanged(nameof(ResultManager)); + } + + public IResultManager ResultManager { get; } + [Reactive] + public string ResultInfo { get; set; } + public void UpdateResultInfo(string info) + { + ResultInfo = info; + } + } +} diff --git a/InDepthSearch.Core/ViewModels/ViewModelBase.cs b/InDepthSearch.Core/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..7a40aab --- /dev/null +++ b/InDepthSearch.Core/ViewModels/ViewModelBase.cs @@ -0,0 +1,8 @@ +using ReactiveUI; + +namespace InDepthSearch.Core.ViewModels +{ + public abstract class ViewModelBase : ReactiveObject + { + } +} diff --git a/InDepthSearch.Tests/Services/OptionServiceTest.cs b/InDepthSearch.Tests/Services/OptionServiceTest.cs index 2797545..fdd41a3 100644 --- a/InDepthSearch.Tests/Services/OptionServiceTest.cs +++ b/InDepthSearch.Tests/Services/OptionServiceTest.cs @@ -1,12 +1,13 @@ using InDepthSearch.Core.Services; using InDepthSearch.Core.Services.Interfaces; -using InDepthSearch.Core.Types; +using InDepthSearch.Core.Enums; using Xunit; namespace InDepthSearch.Tests.Services { public class OptionServiceTest { + /* private readonly IOptionService _optionService; public OptionServiceTest() @@ -19,6 +20,7 @@ public void TranslateLanguageTest() { Assert.Equal("eng", _optionService.TranslateLanguage(RecognitionLanguage.Default)); } + */ } } diff --git a/InDepthSearch.Tests/ViewModels/MainViewModelTest.cs b/InDepthSearch.Tests/ViewModels/MainViewModelTest.cs index 8a23015..c10afb6 100644 --- a/InDepthSearch.Tests/ViewModels/MainViewModelTest.cs +++ b/InDepthSearch.Tests/ViewModels/MainViewModelTest.cs @@ -8,11 +8,12 @@ namespace InDepthSearch.Tests.ViewModels { public class MainViewModelTest { - private readonly MainViewModel _mainViewModel; + /* + private readonly MainWindowViewModel _mainViewModel; public MainViewModelTest() { - _mainViewModel = new MainViewModel(); // TODO implement mocked services + _mainViewModel = new MainWindowViewModel(); // TODO implement mocked services } [Fact] @@ -23,5 +24,6 @@ public void ReadPdfTest() _mainViewModel.ReadPDF.Execute().Subscribe(); Assert.Empty(_mainViewModel.Results); } + */ } } diff --git a/InDepthSearch.UI/App.axaml b/InDepthSearch.UI/App.axaml index 01c8f40..fc2e263 100644 --- a/InDepthSearch.UI/App.axaml +++ b/InDepthSearch.UI/App.axaml @@ -16,8 +16,8 @@ + - diff --git a/InDepthSearch.UI/App.axaml.cs b/InDepthSearch.UI/App.axaml.cs index 3ae1c49..b76b564 100644 --- a/InDepthSearch.UI/App.axaml.cs +++ b/InDepthSearch.UI/App.axaml.cs @@ -2,9 +2,10 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using InDepthSearch.Core.Services; -using InDepthSearch.Core.Types; +using InDepthSearch.Core.Enums; using InDepthSearch.Core.ViewModels; using InDepthSearch.UI.Views; +using Avalonia.Controls; namespace InDepthSearch.UI { @@ -21,11 +22,10 @@ public override void OnFrameworkInitializationCompleted() { desktop.MainWindow = new MainWindow { - DataContext = new MainViewModel(new OptionService(), new DirectoryService(), - new AppService(AppLanguage.English), new ThemeService(Theme.Light)) + DataContext = (Current.FindResource("vmLocator") as ViewModelLocator)!.MainWindow }; + desktop.MainWindow.Closed += (s, e) => (Current.FindResource("vmLocator") as ViewModelLocator)!.Dispose(); } - base.OnFrameworkInitializationCompleted(); } } diff --git a/InDepthSearch.UI/Assets/Resources/BaseResources.xaml b/InDepthSearch.UI/Assets/Resources/BaseResources.xaml new file mode 100644 index 0000000..f73f567 --- /dev/null +++ b/InDepthSearch.UI/Assets/Resources/BaseResources.xaml @@ -0,0 +1,7 @@ + + + + diff --git a/InDepthSearch.UI/Assets/Resources/Icons.xaml b/InDepthSearch.UI/Assets/Resources/Icons.xaml index 5cca975..6e3e668 100644 --- a/InDepthSearch.UI/Assets/Resources/Icons.xaml +++ b/InDepthSearch.UI/Assets/Resources/Icons.xaml @@ -10,9 +10,12 @@ M13,13H11V7H13M13,17H11V15H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z - M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,2L14.39,5.42C13.65,5.15 12.84,5 12,5C11.16,5 10.35,5.15 9.61,5.42L12,2M3.34,7L7.5,6.65C6.9,7.16 6.36,7.78 5.94,8.5C5.5,9.24 5.25,10 5.11,10.79L3.34,7M3.36,17L5.12,13.23C5.26,14 5.53,14.78 5.95,15.5C6.37,16.24 6.91,16.86 7.5,17.37L3.36,17M20.65,7L18.88,10.79C18.74,10 18.47,9.23 18.05,8.5C17.63,7.78 17.1,7.15 16.5,6.64L20.65,7M20.64,17L16.5,17.36C17.09,16.85 17.62,16.22 18.04,15.5C18.46,14.77 18.73,14 18.87,13.21L20.64,17M12,22L9.59,18.56C10.33,18.83 11.14,19 12,19C12.82,19 13.63,18.83 14.37,18.56L12,22Z + M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z + + + M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z + + + M11,8v5l4.25,2.52l0.77-1.28l-3.52-2.09V8H11z M21,10V3l-2.64,2.64C16.74,4.01,14.49,3,12,3c-4.97,0-9,4.03-9,9 s4.03,9,9,9s9-4.03,9-9h-2c0,3.86-3.14,7-7,7s-7-3.14-7-7s3.14-7,7-7c1.93,0,3.68,0.79,4.95,2.05L14,10H21z - - M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z - \ No newline at end of file diff --git a/InDepthSearch.UI/Assets/Resources/StringsENG.xaml b/InDepthSearch.UI/Assets/Resources/StringsENG.xaml index 4177d90..13cc270 100644 --- a/InDepthSearch.UI/Assets/Resources/StringsENG.xaml +++ b/InDepthSearch.UI/Assets/Resources/StringsENG.xaml @@ -38,6 +38,7 @@ Choose precision of OCR engine. Setting better precision may cause the search to take more time. Keyword cannot be empty! Path doesn't exist! + Choose at least one format! Word or phrase to find diff --git a/InDepthSearch.UI/Assets/Resources/StringsFRA.xaml b/InDepthSearch.UI/Assets/Resources/StringsFRA.xaml index c1a81b8..cb46138 100644 --- a/InDepthSearch.UI/Assets/Resources/StringsFRA.xaml +++ b/InDepthSearch.UI/Assets/Resources/StringsFRA.xaml @@ -37,7 +37,8 @@ Choisissez la précision du moteur OCR. La définition d'une meilleure précision peut entraîner une durée de recherche plus longue. Le mot-clé ne peut pas être vide! Le dossier n'existe pas! - + Choisissez au moins un format! + Mot ou phrase à trouver Dossier à rechercher diff --git a/InDepthSearch.UI/Assets/Resources/StringsPOL.xaml b/InDepthSearch.UI/Assets/Resources/StringsPOL.xaml index 2ff6e73..c88f8b3 100644 --- a/InDepthSearch.UI/Assets/Resources/StringsPOL.xaml +++ b/InDepthSearch.UI/Assets/Resources/StringsPOL.xaml @@ -37,6 +37,7 @@ Wybierz precyzję silnika OCR. Lepsza precyzja może pogorszyć czas szukania. Słowo kluczowe nie może być puste! Wybrany folder nie istnieje! + Wybierz co najmniej jeden format! Słowo albo wyrażenie do wyszukania diff --git a/InDepthSearch.UI/AvaloniaStartup.cs b/InDepthSearch.UI/AvaloniaStartup.cs index 85cc455..8571f87 100644 --- a/InDepthSearch.UI/AvaloniaStartup.cs +++ b/InDepthSearch.UI/AvaloniaStartup.cs @@ -1,7 +1,5 @@ using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; using Avalonia.ReactiveUI; -using System; namespace InDepthSearch.UI { diff --git a/InDepthSearch.UI/Helpers/FluentWindow.cs b/InDepthSearch.UI/Controls/FluentWindow.cs similarity index 95% rename from InDepthSearch.UI/Helpers/FluentWindow.cs rename to InDepthSearch.UI/Controls/FluentWindow.cs index 7f4d1e9..18c8edd 100644 --- a/InDepthSearch.UI/Helpers/FluentWindow.cs +++ b/InDepthSearch.UI/Controls/FluentWindow.cs @@ -1,12 +1,12 @@ using Avalonia.Styling; using System; -using System.Collections.Generic; -using System.Text; using Avalonia.Platform; using Avalonia.Controls.Primitives; using Avalonia.Media; +using Avalonia.Controls; +using Avalonia; -namespace Avalonia.Controls +namespace InDepthSearch.UI.Controls { public class FluentWindow : Window, IStyleable { diff --git a/InDepthSearch.UI/Helpers/Conversion.cs b/InDepthSearch.UI/Helpers/Conversion.cs new file mode 100644 index 0000000..9e686f8 --- /dev/null +++ b/InDepthSearch.UI/Helpers/Conversion.cs @@ -0,0 +1,21 @@ +using InDepthSearch.Core.Enums; +using System; +using System.Collections.Generic; +using System.Drawing.Imaging; + +namespace InDepthSearch.UI.Helpers +{ + public static class Conversion + { + public static ImageFormat ImageExtensionToFormat(ImageExtension ie) + { + return ie switch + { + ImageExtension.Jpg => ImageFormat.Jpeg, + ImageExtension.Png => ImageFormat.Png, + ImageExtension.Bmp => ImageFormat.Bmp, + _ => ImageFormat.Png, + }; + } + } +} diff --git a/InDepthSearch.UI/InDepthSearch.UI.csproj b/InDepthSearch.UI/InDepthSearch.UI.csproj index 7965cc9..833ce9f 100644 --- a/InDepthSearch.UI/InDepthSearch.UI.csproj +++ b/InDepthSearch.UI/InDepthSearch.UI.csproj @@ -4,13 +4,14 @@ net5.0 enable Assets\Images\ids-icon.ico - 0.2.0 + 1.0.0 + diff --git a/InDepthSearch.UI/Managers/ResultManager.cs b/InDepthSearch.UI/Managers/ResultManager.cs new file mode 100644 index 0000000..e03588c --- /dev/null +++ b/InDepthSearch.UI/Managers/ResultManager.cs @@ -0,0 +1,36 @@ +using InDepthSearch.Core.Managers.Interfaces; +using InDepthSearch.Core.Models; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System.Collections.ObjectModel; + +namespace InDepthSearch.UI.Managers +{ + public class ResultManager : ReactiveObject, IResultManager + { + public ResultManager() + { + Results = new(); + Stats = new(); + } + + [Reactive] + public ObservableCollection Results { get; } + [Reactive] + public ResultStats Stats { get; set; } + [Reactive] + public bool ItemsReady { get; set; } + + public void Reinitialize() + { + Results.Clear(); + ItemsReady = false; + Stats.FilesAnalyzed = "0/0"; + Stats.PagesAnalyzed = 0; + Stats.ExecutionTime = ""; + } + + public void AddResult(QueryResult res) => Results.Add(res); + public void SetItemsReady(bool ready) => ItemsReady = ready; + } +} diff --git a/InDepthSearch.Core/Services/AppService.cs b/InDepthSearch.UI/Services/AppService.cs similarity index 52% rename from InDepthSearch.Core/Services/AppService.cs rename to InDepthSearch.UI/Services/AppService.cs index ac5e8a7..674ab4b 100644 --- a/InDepthSearch.Core/Services/AppService.cs +++ b/InDepthSearch.UI/Services/AppService.cs @@ -1,35 +1,29 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml.MarkupExtensions; using InDepthSearch.Core.Services.Interfaces; -using InDepthSearch.Core.Types; +using InDepthSearch.Core.Enums; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Avalonia.Controls.ApplicationLifetimes; -namespace InDepthSearch.Core.Services +namespace InDepthSearch.UI.Services { public class AppService : IAppService { private readonly List _languages; - private int currentLanguage; - private SearchStatus status; - private SearchInfo info; + private int currentLanguage = 0; + private SearchStatus status = SearchStatus.Ready; + private SearchInfo info = SearchInfo.Init; - public AppService(AppLanguage lang) + public AppService() { + var basePath = "avares://InDepthSearch.UI/Assets/Resources/"; _languages = new List() { - new ResourceInclude() { Source = new Uri("avares://InDepthSearch.UI/Assets/Resources/StringsENG.xaml") }, - new ResourceInclude() { Source = new Uri("avares://InDepthSearch.UI/Assets/Resources/StringsPOL.xaml") }, - new ResourceInclude() { Source = new Uri("avares://InDepthSearch.UI/Assets/Resources/StringsFRA.xaml") }, + new() { Source = new Uri($"{basePath}StringsENG.xaml") }, + new() { Source = new Uri($"{basePath}StringsPOL.xaml") }, + new() { Source = new Uri($"{basePath}StringsFRA.xaml") }, }; - - currentLanguage = (int)lang; - status = SearchStatus.Ready; - info = SearchInfo.Init; - } public string GetVersion() @@ -37,7 +31,6 @@ public string GetVersion() var assemblyVer = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; return assemblyVer != null ? assemblyVer.Major.ToString() + "." + assemblyVer.Minor.ToString() + "." + assemblyVer.Build.ToString() : "x.x.x"; - } public void ChangeLanguage() @@ -59,9 +52,9 @@ public string GetSearchStatus(SearchStatus ss = SearchStatus.Unknown) return ss switch { - SearchStatus.Ready => (string?)Avalonia.Application.Current.FindResource("StatusReady") ?? ss.ToString().ToUpper(), - SearchStatus.Initializing => (string?)Avalonia.Application.Current.FindResource("StatusInitializing") ?? ss.ToString().ToUpper(), - SearchStatus.Running => (string?)Avalonia.Application.Current.FindResource("StatusRunning") ?? ss.ToString().ToUpper(), + SearchStatus.Ready => GetResourceString("StatusReady") ?? ss.ToString().ToUpper(), + SearchStatus.Initializing => GetResourceString("StatusInitializing") ?? ss.ToString().ToUpper(), + SearchStatus.Running => GetResourceString("StatusRunning") ?? ss.ToString().ToUpper(), _ => ss.ToString().ToUpper() }; } @@ -73,16 +66,21 @@ public string GetSearchInfo(SearchInfo si = SearchInfo.Unknown) return si switch { - SearchInfo.Init => (string?)Avalonia.Application.Current.FindResource("InfoInit") ?? si.ToString(), - SearchInfo.Run => (string?)Avalonia.Application.Current.FindResource("InfoRun") ?? si.ToString(), - SearchInfo.NoResults => (string?)Avalonia.Application.Current.FindResource("InfoNoResults") ?? si.ToString(), + SearchInfo.Init => GetResourceString("InfoInit") ?? si.ToString(), + SearchInfo.Run => GetResourceString("InfoRun") ?? si.ToString(), + SearchInfo.NoResults => GetResourceString("InfoNoResults") ?? si.ToString(), _ => si.ToString() }; } public string GetSecondsString() { - return (string?)Avalonia.Application.Current.FindResource("TimeSeconds") ?? "seconds"; + return GetResourceString("TimeSeconds") ?? "seconds"; + } + + private string? GetResourceString(string key) + { + return (string?)Avalonia.Application.Current.FindResource(key); } } diff --git a/InDepthSearch.Core/Services/DirectoryService.cs b/InDepthSearch.UI/Services/DirectoryService.cs similarity index 95% rename from InDepthSearch.Core/Services/DirectoryService.cs rename to InDepthSearch.UI/Services/DirectoryService.cs index 1536fb4..03feca5 100644 --- a/InDepthSearch.Core/Services/DirectoryService.cs +++ b/InDepthSearch.UI/Services/DirectoryService.cs @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace InDepthSearch.Core.Services +namespace InDepthSearch.UI.Services { public class DirectoryService : IDirectoryService { diff --git a/InDepthSearch.Core/Services/OptionService.cs b/InDepthSearch.UI/Services/OptionService.cs similarity index 75% rename from InDepthSearch.Core/Services/OptionService.cs rename to InDepthSearch.UI/Services/OptionService.cs index fccf7b9..cbd15d5 100644 --- a/InDepthSearch.Core/Services/OptionService.cs +++ b/InDepthSearch.UI/Services/OptionService.cs @@ -1,6 +1,6 @@ using Docnet.Core.Models; using InDepthSearch.Core.Services.Interfaces; -using InDepthSearch.Core.Types; +using InDepthSearch.Core.Enums; using System; using System.Collections.Generic; using System.Drawing.Imaging; @@ -12,17 +12,17 @@ namespace InDepthSearch.Core.Services { public class OptionService : IOptionService { - public (PageDimensions, RenderFlags, PixelFormat, ImageFormat) TranslatePrecision(RecognitionPrecision rp) + public (PageDimensions, RenderFlags, ImageExtension) TranslatePrecision(RecognitionPrecision rp) { return rp switch { RecognitionPrecision.High or RecognitionPrecision.Default => - (new PageDimensions(1080, 1920), RenderFlags.RenderAnnotations, PixelFormat.Format32bppArgb, ImageFormat.Png), + (new PageDimensions(1080, 1920), RenderFlags.RenderAnnotations, ImageExtension.Png), RecognitionPrecision.Medium => - (new PageDimensions(720, 1280), RenderFlags.RenderAnnotations, PixelFormat.Format32bppArgb, ImageFormat.Jpeg), + (new PageDimensions(720, 1280), RenderFlags.RenderAnnotations, ImageExtension.Jpg), RecognitionPrecision.Low => - (new PageDimensions(720, 1280), RenderFlags.RenderAnnotations | RenderFlags.OptimizeTextForLcd | RenderFlags.Grayscale, PixelFormat.Format32bppRgb, ImageFormat.Bmp), - _ => (new PageDimensions(1080, 1920), RenderFlags.RenderAnnotations, PixelFormat.Format32bppArgb, ImageFormat.Png), + (new PageDimensions(720, 1280), RenderFlags.RenderAnnotations | RenderFlags.OptimizeTextForLcd | RenderFlags.Grayscale, ImageExtension.Bmp), + _ => (new PageDimensions(1080, 1920), RenderFlags.RenderAnnotations, ImageExtension.Png), }; } diff --git a/InDepthSearch.UI/Services/SearchService.cs b/InDepthSearch.UI/Services/SearchService.cs new file mode 100644 index 0000000..e7c098c --- /dev/null +++ b/InDepthSearch.UI/Services/SearchService.cs @@ -0,0 +1,152 @@ + +using Docnet.Core; +using DocumentFormat.OpenXml.Packaging; +using InDepthSearch.Core.Enums; +using InDepthSearch.Core.Managers.Interfaces; +using InDepthSearch.Core.Models; +using InDepthSearch.Core.Services.Interfaces; +using InDepthSearch.UI.Helpers; +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; +using Tesseract; + +namespace InDepthSearch.UI.Services +{ + public class SearchService : ISearchService + { + private readonly IDocLib _docLib; + private readonly IOptionService _optionService; + private readonly IResultManager _resultManager; + public SearchService(IOptionService optionService, IResultManager resultManager) + { + _docLib = DocLib.Instance; + _optionService = optionService; + _resultManager = resultManager; + } + + public void Search(string file, SearchOptions searchOptions) + { + if(file.EndsWith(".pdf")) + { + HandlePDF(file, searchOptions); + } + else if (file.EndsWith(".docx") || file.EndsWith(".doc")) + { + HandleDOCX(file, searchOptions); + } + } + + private void HandlePDF(string file, SearchOptions searchOptions) + { + using var docReader = _docLib.GetDocReader(file, _optionService.TranslatePrecision(searchOptions.SelectedPrecisionOCR).Item1); + + for (var i = 0; i < docReader.GetPageCount(); i++) + { + using var pageReader = docReader.GetPageReader(i); + var parsedText = pageReader.GetText().ToString(); + + if (searchOptions.UseOCR && string.IsNullOrWhiteSpace(parsedText)) + { + var rawBytes = pageReader.GetImage(_optionService.TranslatePrecision(searchOptions.SelectedPrecisionOCR).Item2); + var width = pageReader.GetPageWidth(); + var height = pageReader.GetPageHeight(); + var imFormat = Conversion.ImageExtensionToFormat(_optionService.TranslatePrecision(searchOptions.SelectedPrecisionOCR).Item3); + using var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb); + + AddBytes(bmp, rawBytes); + using var stream = new MemoryStream(); + bmp.Save(stream, Conversion.ImageExtensionToFormat(_optionService.TranslatePrecision(searchOptions.SelectedPrecisionOCR).Item3)); + + parsedText = ImageToText(stream.ToArray(), searchOptions.SelectedLanguageOCR, searchOptions.SelectedPrecisionOCR); + } + + SearchPage(parsedText, searchOptions.Keyword, file, i, searchOptions.CaseSensitive); + _resultManager.Stats.PagesAnalyzed += 1; + } + } + private void HandleDOCX(string file, SearchOptions searchOptions) + { + using WordprocessingDocument wordDocument = WordprocessingDocument.Open(file, false); + var paragraphs = wordDocument.MainDocumentPart?.Document?.Body?.ChildElements; + var images = wordDocument.MainDocumentPart?.ImageParts; + if (searchOptions.UseOCR && images != null) + { + foreach (var image in images) + { + var docStream = wordDocument.Package.GetPart(image.Uri).GetStream(); + using var stream = new MemoryStream(); + docStream.CopyTo(stream); + var parsedText = ImageToText(stream.ToArray(), searchOptions.SelectedLanguageOCR, searchOptions.SelectedPrecisionOCR); + SearchPage(parsedText, searchOptions.Keyword, file, -1, searchOptions.CaseSensitive); + } + } + + if (paragraphs != null) + { + var parsedString = ""; + foreach (var paragraph in paragraphs) + parsedString = parsedString + paragraph.InnerText + "\r"; + + SearchPage(parsedString, searchOptions.Keyword, file, -1, searchOptions.CaseSensitive); + } + } + + private static void AddBytes(Bitmap bmp, byte[] rawBytes) + { + var rect = new Rectangle(0, 0, bmp.Width, bmp.Height); + + var bmpData = bmp.LockBits(rect, ImageLockMode.WriteOnly, bmp.PixelFormat); + var pNative = bmpData.Scan0; + + Marshal.Copy(rawBytes, 0, pNative, rawBytes.Length); + bmp.UnlockBits(bmpData); + } + + string ImageToText(byte[] imageBytes, RecognitionLanguage rl, RecognitionPrecision rp) + { + try + { + using var engine = new TesseractEngine(@"./Files", _optionService.TranslateLanguage(rl), EngineMode.Default); + using var img = Pix.LoadFromMemory(imageBytes); + using var pager = engine.Process(img); + return pager.GetText().ToString(); + //System.Diagnostics.Debug.WriteLine("Mean confidence: {0}", pager.GetMeanConfidence()); + //System.Diagnostics.Debug.WriteLine("Text {0}", text); + } + catch (Exception ee) + { + System.Diagnostics.Debug.WriteLine("Unexpected Error: " + ee.Message); + System.Diagnostics.Debug.WriteLine("Details: "); + System.Diagnostics.Debug.WriteLine(ee.ToString()); + } + + return ""; + } + + void SearchPage(string rawText, string keyword, string filePath, int pageNum, bool isCaseSensitive) + { + + var searchIndex = 0; + var at = 0; + string textBefore, textFound, textAfter; + var offset = 30; + var text = rawText.Replace("\n", " ").Replace("\r", " "); + + while (at > -1) + { + if (!_resultManager.ItemsReady) _resultManager.SetItemsReady(true); + at = isCaseSensitive ? text.IndexOf(keyword, searchIndex) : text.ToLower().IndexOf(keyword.ToLower(), searchIndex); + if (at == -1) break; + System.Diagnostics.Debug.WriteLine("Found the keyword " + keyword + " in doc: " + filePath + " on page " + pageNum + " at " + at + " position!"); + textBefore = "..." + text.Substring(Math.Max(0, at - offset), at < offset ? at : offset); + textAfter = text.Substring(at + keyword.Length, at + keyword.Length + offset > text.Length ? text.Length - at - keyword.Length : offset) + "..."; + textFound = text.Substring(at, keyword.Length); + _resultManager.AddResult(new QueryResult(MatchConfidence.High, filePath, textBefore, textFound, textAfter, pageNum)); + searchIndex = at + keyword.Length; + } + } + } +} diff --git a/InDepthSearch.Core/Services/ThemeService.cs b/InDepthSearch.UI/Services/ThemeService.cs similarity index 91% rename from InDepthSearch.Core/Services/ThemeService.cs rename to InDepthSearch.UI/Services/ThemeService.cs index a2df7a4..cb9cf8c 100644 --- a/InDepthSearch.Core/Services/ThemeService.cs +++ b/InDepthSearch.UI/Services/ThemeService.cs @@ -2,14 +2,10 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml.Styling; using InDepthSearch.Core.Services.Interfaces; -using InDepthSearch.Core.Types; +using InDepthSearch.Core.Enums; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace InDepthSearch.Core.Services +namespace InDepthSearch.UI.Services { public class ThemeService : IThemeService { @@ -18,11 +14,10 @@ public class ThemeService : IThemeService private Theme currentTheme = Theme.Light; private Window? _window = null; - public ThemeService(Theme theme) + public ThemeService() { _lightTheme = CreateStyle("avares://InDepthSearch.UI/Themes/Light.xaml"); _darkTheme = CreateStyle("avares://InDepthSearch.UI/Themes/Dark.xaml"); - currentTheme = theme; } private void InitDynamicThemes(Theme theme) diff --git a/InDepthSearch.UI/Themes/Styles/ComboBoxStyle.xaml b/InDepthSearch.UI/Themes/Styles/ComboBoxStyle.xaml index ecb3562..c995596 100644 --- a/InDepthSearch.UI/Themes/Styles/ComboBoxStyle.xaml +++ b/InDepthSearch.UI/Themes/Styles/ComboBoxStyle.xaml @@ -3,7 +3,7 @@ + diff --git a/InDepthSearch.UI/Themes/Styles/TextBlockStyle.xaml b/InDepthSearch.UI/Themes/Styles/TextBlockStyle.xaml index 51e1349..e566a03 100644 --- a/InDepthSearch.UI/Themes/Styles/TextBlockStyle.xaml +++ b/InDepthSearch.UI/Themes/Styles/TextBlockStyle.xaml @@ -19,7 +19,7 @@ diff --git a/InDepthSearch.UI/Helpers/ViewLocator.cs b/InDepthSearch.UI/ViewLocator.cs similarity index 83% rename from InDepthSearch.UI/Helpers/ViewLocator.cs rename to InDepthSearch.UI/ViewLocator.cs index 15dfe4d..970316e 100644 --- a/InDepthSearch.UI/Helpers/ViewLocator.cs +++ b/InDepthSearch.UI/ViewLocator.cs @@ -1,4 +1,4 @@ -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Controls.Templates; using InDepthSearch.Core.ViewModels; using System; @@ -7,11 +7,9 @@ namespace InDepthSearch.UI { public class ViewLocator : IDataTemplate { - public bool SupportsRecycling => false; - public IControl Build(object data) { - var name = data.GetType().FullName!.Replace("ViewModel", "View"); + var name = data.GetType().FullName!.Replace("ViewModel", "View").Replace(".Core.", ".UI."); var type = Type.GetType(name); if (type != null) @@ -26,7 +24,7 @@ public IControl Build(object data) public bool Match(object data) { - return data is MainViewModel; + return data is ViewModelBase; } } } diff --git a/InDepthSearch.UI/ViewModelLocator.cs b/InDepthSearch.UI/ViewModelLocator.cs new file mode 100644 index 0000000..7ab0d95 --- /dev/null +++ b/InDepthSearch.UI/ViewModelLocator.cs @@ -0,0 +1,46 @@ +using Autofac; +using InDepthSearch.Core.ViewModels; +using System; +using System.Linq; +using System.Reflection; + +namespace InDepthSearch.UI +{ + public class ViewModelLocator : IDisposable + { + private readonly IContainer _container; + public MainWindowViewModel MainWindow { get; } + public ViewModelLocator() + { + var ui = Assembly.GetCallingAssembly(); + var core = Assembly.GetAssembly(typeof(ViewModelBase))!; + var builder = new ContainerBuilder(); + + builder.RegisterAssemblyTypes(core).Where(name => name.Name.EndsWith("ViewModel")) + .AsSelf().InstancePerDependency(); + builder.RegisterAssemblyTypes(core, ui).Where(name => name.Name.EndsWith("Service")) + .AsImplementedInterfaces().SingleInstance(); + builder.RegisterAssemblyTypes(core, ui).Where(name => name.Name.EndsWith("Manager")) + .AsImplementedInterfaces().SingleInstance(); + + if (Avalonia.Controls.Design.IsDesignMode) + { + builder.RegisterAssemblyTypes(core, ui).Where(name => name.Name.EndsWith("Designer")).AsImplementedInterfaces().InstancePerDependency(); + } + + builder.RegisterType().AsSelf().SingleInstance(); + _container = builder.Build(); + MainWindow = _container.Resolve(); + + if (Avalonia.Controls.Design.IsDesignMode) + { + // Set parameters needed for designer if any + } + } + + public void Dispose() + { + _container.Dispose(); + } + } +} diff --git a/InDepthSearch.UI/Views/MainWindow.axaml b/InDepthSearch.UI/Views/MainWindow.axaml index a0f4bdf..f4b8cad 100644 --- a/InDepthSearch.UI/Views/MainWindow.axaml +++ b/InDepthSearch.UI/Views/MainWindow.axaml @@ -3,211 +3,76 @@ xmlns:vm="using:InDepthSearch.Core.ViewModels" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:conv="clr-namespace:InDepthSearch.UI.Converters" mc:Ignorable="d" Width="1220" Height="780" MinWidth="1000" MinHeight="600" x:Class="InDepthSearch.UI.Views.MainWindow" x:Name="Main" Icon="/Assets/Images/ids-icon.ico" - Title="InDepthSearch"> - - - - - - - - + Title="InDepthSearch" + Design.DataContext="{Binding Path=MainWindow, Source={StaticResource vmLocator}}"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + - - + + + - - diff --git a/InDepthSearch.UI/Views/MainWindow.axaml.cs b/InDepthSearch.UI/Views/MainWindow.axaml.cs index 085a6e5..e6261c6 100644 --- a/InDepthSearch.UI/Views/MainWindow.axaml.cs +++ b/InDepthSearch.UI/Views/MainWindow.axaml.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using InDepthSearch.UI.Controls; namespace InDepthSearch.UI.Views { diff --git a/InDepthSearch.UI/Views/OptionsView.axaml b/InDepthSearch.UI/Views/OptionsView.axaml new file mode 100644 index 0000000..0610a6a --- /dev/null +++ b/InDepthSearch.UI/Views/OptionsView.axaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +