From c0510ea93cd163dd6e54ea1c00ddcf3f8f9fa67b Mon Sep 17 00:00:00 2001 From: radoslawik Date: Fri, 21 May 2021 16:59:16 +0200 Subject: [PATCH 1/7] Add support for docx and docx Update options and stats models, rework validation implementation --- InDepthSearch.Core/InDepthSearch.Core.csproj | 3 +- InDepthSearch.Core/Models/ResultStats.cs | 5 +- InDepthSearch.Core/Models/SearchOptions.cs | 9 +- .../ViewModels/MainViewModel.cs | 127 +++++++++++------- .../Assets/Resources/StringsENG.xaml | 1 + .../Assets/Resources/StringsFRA.xaml | 3 +- .../Assets/Resources/StringsPOL.xaml | 1 + InDepthSearch.UI/Views/MainWindow.axaml | 23 +++- 8 files changed, 108 insertions(+), 64 deletions(-) diff --git a/InDepthSearch.Core/InDepthSearch.Core.csproj b/InDepthSearch.Core/InDepthSearch.Core.csproj index 5cd0bf2..2ebd522 100644 --- a/InDepthSearch.Core/InDepthSearch.Core.csproj +++ b/InDepthSearch.Core/InDepthSearch.Core.csproj @@ -8,9 +8,10 @@ + - + diff --git a/InDepthSearch.Core/Models/ResultStats.cs b/InDepthSearch.Core/Models/ResultStats.cs index 0d5f86b..f454150 100644 --- a/InDepthSearch.Core/Models/ResultStats.cs +++ b/InDepthSearch.Core/Models/ResultStats.cs @@ -10,10 +10,9 @@ namespace InDepthSearch.Core.Models { public class ResultStats : ReactiveObject { - public ResultStats(string filesAnalyzed, bool isReady, int pagesAnalyzed, string executionTime) + public ResultStats(string filesAnalyzed, int pagesAnalyzed, string executionTime) { FilesAnalyzed = filesAnalyzed; - IsReady = isReady; PagesAnalyzed = pagesAnalyzed; ExecutionTime = executionTime; } @@ -21,8 +20,6 @@ public ResultStats(string filesAnalyzed, bool isReady, int pagesAnalyzed, string [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..c957f19 100644 --- a/InDepthSearch.Core/Models/SearchOptions.cs +++ b/InDepthSearch.Core/Models/SearchOptions.cs @@ -7,7 +7,7 @@ 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) + bool caseSensitive, bool useOCR, bool useSubfolders, bool usePDF, bool useDOCX, bool useODT, bool useDOC) { Path = path; Keyword = keyword; @@ -19,6 +19,7 @@ public class SearchOptions : ReactiveObject UsePDF = usePDF; UseDOCX = useDOCX; UseODT = useODT; + UseDOCX = useDOC; } [Reactive] @@ -28,10 +29,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/ViewModels/MainViewModel.cs b/InDepthSearch.Core/ViewModels/MainViewModel.cs index e345ac4..c5f3aa7 100644 --- a/InDepthSearch.Core/ViewModels/MainViewModel.cs +++ b/InDepthSearch.Core/ViewModels/MainViewModel.cs @@ -14,13 +14,12 @@ using InDepthSearch.Core.Models; using InDepthSearch.Core.Types; using System.Threading; -using ReactiveUI.Validation.Helpers; -using ReactiveUI.Validation.Extensions; using InDepthSearch.Core.Services.Interfaces; +using DocumentFormat.OpenXml.Packaging; namespace InDepthSearch.Core.ViewModels { - public class MainViewModel : ReactiveValidationObject + public class MainViewModel : ReactiveObject { public string Logo => "avares://InDepthSearch.UI/Assets/Images/ids-logo.png"; @@ -41,10 +40,10 @@ public class MainViewModel : ReactiveValidationObject [Reactive] public string ResultInfo { get; set; } - [Reactive] - public bool KeywordErrorVisible { get; set; } - [Reactive] - public bool PathErrorVisible { get; set; } + public bool KeywordErrorVisible => string.IsNullOrWhiteSpace(Options.Keyword); + public bool PathErrorVisible => !Directory.Exists(Options.Path); + public bool FormatsErrorVisible => !(Options.UseDOC || Options.UseDOCX || Options.UseODT || Options.UsePDF); + public bool CanExecute => !KeywordErrorVisible && !PathErrorVisible && !FormatsErrorVisible; [Reactive] public string AppVersion { get; set; } [Reactive] @@ -56,6 +55,7 @@ public class MainViewModel : ReactiveValidationObject [Reactive] public string StatusName { get; set; } + private Thread? _th; private readonly IDocLib _docLib; private readonly IOptionService _optionService; @@ -71,22 +71,19 @@ public MainViewModel() 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); + false, true, false, true, false, false, false); Results = new ObservableCollection(); - Stats = new ResultStats("0/0", true, 0, "0"); + Stats = new ResultStats("0/0", 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!"); + this.WhenAnyValue(x => x.Options.Keyword, x => x.Options.Path).Subscribe(x => ValidateSelection()); 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, @@ -105,7 +102,7 @@ public MainViewModel() _th = new Thread(() => StartReading()); _th.IsBackground = true; _th.Start(); - }, this.IsValid()); + }); ChangeTheme = ReactiveCommand.Create(() => { themeService.ChangeTheme(); @@ -122,23 +119,29 @@ public MainViewModel() 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); + false, true, false, true, false, false, false); Results = new ObservableCollection(); - Stats = new ResultStats("0/0", true, 0, "0"); + Stats = new ResultStats("0/0", 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!"); + // 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()); // Get assembly version AppVersion = infoService.GetVersion(); + } + private void ValidateSelection() + { + this.RaisePropertyChanged(nameof(PathErrorVisible)); + this.RaisePropertyChanged(nameof(KeywordErrorVisible)); + this.RaisePropertyChanged(nameof(FormatsErrorVisible)); + this.RaisePropertyChanged(nameof(CanExecute)); } private void UpdateStringResources() @@ -148,20 +151,13 @@ private void UpdateStringResources() 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; + ItemsReady = false; Stats.FilesAnalyzed = "0/0"; Stats.PagesAnalyzed = 0; Stats.ExecutionTime = "..."; @@ -171,8 +167,22 @@ void StartReading() if (!string.IsNullOrWhiteSpace(searchOptions.Keyword)) { - List discoveredFiles = searchOptions.UseSubfolders ? Directory.GetFiles(searchOptions.Path, "*.pdf", SearchOption.AllDirectories).ToList() - : Directory.GetFiles(searchOptions.Path, "*.pdf").ToList(); + 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) { @@ -184,34 +194,52 @@ void StartReading() ResultInfo = _infoService.GetSearchInfo(SearchInfo.Init); Stats.FilesAnalyzed = "0/" + discoveredFiles.Count.ToString(); - foreach (var pdf in discoveredFiles) + foreach (var file in discoveredFiles) { - System.Diagnostics.Debug.WriteLine("Checking " + pdf); - using var docReader = _docLib.GetDocReader(pdf, _optionService.TranslatePrecision(searchOptions.SelectedPrecisionOCR).Item1); + System.Diagnostics.Debug.WriteLine("Checking " + file); - for (var i = 0; i < docReader.GetPageCount(); i++) + if(file.EndsWith(".pdf")) { - using var pageReader = docReader.GetPageReader(i); - var parsedText = pageReader.GetText().ToString(); + using var docReader = _docLib.GetDocReader(file, _optionService.TranslatePrecision(searchOptions.SelectedPrecisionOCR).Item1); - if (searchOptions.UseOCR && string.IsNullOrWhiteSpace(parsedText)) + for (var i = 0; i < docReader.GetPageCount(); i++) { - 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); + using var pageReader = docReader.GetPageReader(i); + var parsedText = pageReader.GetText().ToString(); - AddBytes(bmp, rawBytes); - using var stream = new MemoryStream(); - bmp.Save(stream, _optionService.TranslatePrecision(searchOptions.SelectedPrecisionOCR).Item4); + 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); - parsedText = ImageToText(stream.ToArray(), searchOptions.SelectedLanguageOCR, searchOptions.SelectedPrecisionOCR); - } + 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, file, i, searchOptions.CaseSensitive); + Stats.PagesAnalyzed += 1; - SearchPage(parsedText, searchOptions.Keyword, pdf, i, searchOptions.CaseSensitive); - Stats.PagesAnalyzed += 1; + } + } + else if(file.EndsWith(".docx") || file.EndsWith(".doc")) + { + using WordprocessingDocument wordDocument = WordprocessingDocument.Open(file, false); + var paragraphs = wordDocument.MainDocumentPart?.Document?.Body?.ChildElements; + var parsedString = ""; + if(paragraphs!=null) + { + foreach (var paragraph in paragraphs) + parsedString = parsedString + paragraph.InnerText + "\r"; + SearchPage(parsedString, searchOptions.Keyword, file, 0, searchOptions.CaseSensitive); + } } + fileCounter += 1; Stats.FilesAnalyzed = fileCounter.ToString() + "/" + discoveredFiles.Count.ToString(); } @@ -221,7 +249,6 @@ void StartReading() 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()) { 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/Views/MainWindow.axaml b/InDepthSearch.UI/Views/MainWindow.axaml index a0f4bdf..3d2acea 100644 --- a/InDepthSearch.UI/Views/MainWindow.axaml +++ b/InDepthSearch.UI/Views/MainWindow.axaml @@ -45,7 +45,7 @@ - + @@ -89,11 +89,20 @@ - - - - - + + + + + + + + + + + + + @@ -117,7 +126,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" Width="{Binding #PrecisionGrid.Bounds.Width}"/> - - + + From eba6be14d9f3557328760a267f7f9fddcea14cde Mon Sep 17 00:00:00 2001 From: radoslawik Date: Fri, 28 May 2021 14:28:43 +0200 Subject: [PATCH 3/7] Implement OCR for DOCX and DOC --- .../ViewModels/MainViewModel.cs | 100 +++++++++++------- InDepthSearch.UI/Views/MainWindow.axaml | 2 +- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/InDepthSearch.Core/ViewModels/MainViewModel.cs b/InDepthSearch.Core/ViewModels/MainViewModel.cs index 61fac92..603e015 100644 --- a/InDepthSearch.Core/ViewModels/MainViewModel.cs +++ b/InDepthSearch.Core/ViewModels/MainViewModel.cs @@ -198,47 +198,10 @@ void StartReading() { System.Diagnostics.Debug.WriteLine("Checking " + file); - if(file.EndsWith(".pdf")) - { - 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(); - 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, file, i, searchOptions.CaseSensitive); - Stats.PagesAnalyzed += 1; - - } - } - else if(file.EndsWith(".docx") || file.EndsWith(".doc")) - { - using WordprocessingDocument wordDocument = WordprocessingDocument.Open(file, false); - var paragraphs = wordDocument.MainDocumentPart?.Document?.Body?.ChildElements; - var parsedString = ""; - if(paragraphs!=null) - { - foreach (var paragraph in paragraphs) - parsedString = parsedString + paragraph.InnerText + "\r"; - - SearchPage(parsedString, searchOptions.Keyword, file, 0, searchOptions.CaseSensitive); - } - } + if (file.EndsWith(".pdf")) + HandlePDF(file, searchOptions); + else if (file.EndsWith(".docx") || file.EndsWith(".doc")) + HandleDOCX(file, searchOptions); fileCounter += 1; Stats.FilesAnalyzed = fileCounter.ToString() + "/" + discoveredFiles.Count.ToString(); @@ -321,6 +284,61 @@ async void BrowseDirectory() Options.Path = newDir; } + 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(); + 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, file, i, searchOptions.CaseSensitive); + 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); + } + } + } } diff --git a/InDepthSearch.UI/Views/MainWindow.axaml b/InDepthSearch.UI/Views/MainWindow.axaml index 829b95c..f09789e 100644 --- a/InDepthSearch.UI/Views/MainWindow.axaml +++ b/InDepthSearch.UI/Views/MainWindow.axaml @@ -180,7 +180,7 @@ - + From b2706b438f74e7836a2f3346876ffa97ef560d8c Mon Sep 17 00:00:00 2001 From: radoslawik Date: Fri, 28 May 2021 15:42:17 +0200 Subject: [PATCH 4/7] Add a button with a link to the repo --- .../ViewModels/MainViewModel.cs | 17 +++++++++-- InDepthSearch.UI/Assets/Resources/Icons.xaml | 11 ++++--- .../Themes/Styles/ComboBoxStyle.xaml | 5 +++- .../Themes/Styles/TextBlockStyle.xaml | 2 +- InDepthSearch.UI/Views/MainWindow.axaml | 30 ++++++++++++------- 5 files changed, 46 insertions(+), 19 deletions(-) diff --git a/InDepthSearch.Core/ViewModels/MainViewModel.cs b/InDepthSearch.Core/ViewModels/MainViewModel.cs index 603e015..049abd4 100644 --- a/InDepthSearch.Core/ViewModels/MainViewModel.cs +++ b/InDepthSearch.Core/ViewModels/MainViewModel.cs @@ -16,6 +16,7 @@ using System.Threading; using InDepthSearch.Core.Services.Interfaces; using DocumentFormat.OpenXml.Packaging; +using System.Diagnostics; namespace InDepthSearch.Core.ViewModels { @@ -37,6 +38,7 @@ public class MainViewModel : ReactiveObject public ReactiveCommand GetDirectory { get; } public ReactiveCommand ChangeTheme { get; } public ReactiveCommand ChangeLanguage { get; } + public ReactiveCommand OpenUrl { get; } [Reactive] public string ResultInfo { get; set; } @@ -86,7 +88,7 @@ public MainViewModel() #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, + public MainViewModel(IOptionService optionService, IDirectoryService directoryService, IAppService infoService, IThemeService themeService) { // Initialize services @@ -97,7 +99,7 @@ public MainViewModel() _infoService = infoService; // Initialize commands - GetDirectory = ReactiveCommand.Create(BrowseDirectory); + GetDirectory = ReactiveCommand.Create(BrowseDirectory); ReadPDF = ReactiveCommand.Create(() => { _th = new Thread(() => StartReading()); _th.IsBackground = true; @@ -114,6 +116,17 @@ public MainViewModel() 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 PrecisionOCR = new ObservableCollection(Enum.GetValues(typeof(RecognitionPrecision)).Cast()); 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/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 0a6e7c0..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/Views/MainWindow.axaml b/InDepthSearch.UI/Views/MainWindow.axaml index f09789e..e50c8fd 100644 --- a/InDepthSearch.UI/Views/MainWindow.axaml +++ b/InDepthSearch.UI/Views/MainWindow.axaml @@ -190,30 +190,38 @@ - - - - - + From b16431a6c775f27e98c8a4c4ec5c78ebab6d6873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Pude=C5=82ko?= <55437425+radoslawik@users.noreply.github.com> Date: Mon, 7 Jun 2021 16:40:17 +0200 Subject: [PATCH 5/7] Update README with docx support --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b657f05..320292b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ InDepthSearch is a multi-platform desktop search engine to find the keywords ins - Linux (x64) - macOS (x64) -**NOTE:** Currently the project is in pre-release state, some functionalities are not implemented yet and some of them work only on a specific platform. +**NOTE:** Currently the project is in pre-release state, some functionalities are not implemented yet and some of them work only on a specific platform. For more information see the section below. ## ✔️ Features The table below lists the features and their compatibility with the supported platforms: @@ -22,7 +22,7 @@ The table below lists the features and their compatibility with the supported pl | ------------- |:---------------:|:---------------:|:---------------:| | Multiple keywords search* | - | - | - | | PDF support | + | + | + | -| DOC/DOCX support** | - | - | - | +| DOC/DOCX support** | + | + | + | | ODT support** | - | - | - | | Search in subfolders | + | + | + | | Case-sensitive search | + | + | + | @@ -30,9 +30,9 @@ The table below lists the features and their compatibility with the supported pl * Currently the user is allowed to search only for one word/phrase. Support for multiple search entries is provisioned for next pre-release. -** Only PDF format is supported at the moment, but in the future it is planned to extend it to DOC, DOCX and ODT. +** ODT format is not available yet. -** Optical Character Recognition (OCR) libraries are compatible only with Windows, therefore search in images is disabled on Linux and macOS for the moment. +*** Optical Character Recognition (OCR) libraries are compatible only with Windows, therefore search in images is disabled on Linux and macOS for the moment. ## 🚀 Quick start! If you just want to run the application follow the instructions below. If you want to build the project first, go to to **Build & run** section. @@ -57,7 +57,7 @@ The code in this repository is licensed under the **Apache License 2.0** **NOTE:** The project would not exist without following resources: - - [Avalonia](https://github.com/AvaloniaUI/Avalonia), [ReactiveUI](https://github.com/reactiveui/ReactiveUI), [DocNET](https://github.com/GowenGit/docnet) licensed under MIT. + - [Avalonia](https://github.com/AvaloniaUI/Avalonia), [ReactiveUI](https://github.com/reactiveui/ReactiveUI), [DocNET](https://github.com/GowenGit/docnet) and [Open-XML-SDK](https://github.com/OfficeDev/Open-XML-SDK) licensed under MIT. - [tesseract](https://github.com/charlesw/tesseract) licensed under the Apache License 2.0. From 94da39da725a536fe83d28506373c1e49d62e131 Mon Sep 17 00:00:00 2001 From: radoslawik Date: Fri, 25 Mar 2022 10:54:31 +0100 Subject: [PATCH 6/7] Rework the app to use dependency injection Separate .NET Core part from Avalonia, move most of the logic from main view model into services, create separate view models for results and options. Hell lots of changes in one commit, I am not proud of it (: --- InDepthSearch.Core/Enums/AppLanguage.cs | 10 + InDepthSearch.Core/Enums/ImageExtension.cs | 10 + InDepthSearch.Core/Enums/MatchConfidence.cs | 11 + .../Enums/RecognitionLanguage.cs | 11 + .../Enums/RecognitionPrecision.cs | 11 + InDepthSearch.Core/Enums/SearchInfo.cs | 11 + InDepthSearch.Core/Enums/SearchStatus.cs | 11 + InDepthSearch.Core/Enums/Theme.cs | 10 + InDepthSearch.Core/InDepthSearch.Core.csproj | 1 - .../Managers/Interfaces/IResultManager.cs | 16 + InDepthSearch.Core/Models/QueryResult.cs | 2 +- InDepthSearch.Core/Models/ResultStats.cs | 8 +- InDepthSearch.Core/Models/SearchOptions.cs | 22 +- .../Services/Interfaces/IAppService.cs | 2 +- .../Services/Interfaces/IOptionService.cs | 10 +- .../Services/Interfaces/ISearchService.cs | 10 + .../Services/Interfaces/IThemeService.cs | 2 +- InDepthSearch.Core/Types/AppLanguage.cs | 15 - InDepthSearch.Core/Types/MatchConfidence.cs | 16 - .../Types/RecognitionLanguage.cs | 16 - .../Types/RecognitionPrecision.cs | 16 - InDepthSearch.Core/Types/SearchInfo.cs | 16 - InDepthSearch.Core/Types/SearchStatus.cs | 16 - InDepthSearch.Core/Types/Theme.cs | 15 - .../ViewModels/MainViewModel.cs | 357 ------------------ .../ViewModels/MainWindowViewModel.cs | 152 ++++++++ .../ViewModels/OptionsViewModel.cs | 71 ++++ .../ViewModels/ResultsViewModel.cs | 38 ++ .../ViewModels/ViewModelBase.cs | 8 + .../Services/OptionServiceTest.cs | 4 +- .../ViewModels/MainViewModelTest.cs | 6 +- InDepthSearch.UI/App.axaml | 2 +- InDepthSearch.UI/App.axaml.cs | 8 +- .../Assets/Resources/BaseResources.xaml | 7 + InDepthSearch.UI/AvaloniaStartup.cs | 2 - .../{Helpers => Controls}/FluentWindow.cs | 6 +- InDepthSearch.UI/Helpers/Conversion.cs | 21 ++ InDepthSearch.UI/InDepthSearch.UI.csproj | 1 + InDepthSearch.UI/Managers/ResultManager.cs | 36 ++ .../Services/AppService.cs | 48 ++- .../Services/DirectoryService.cs | 2 +- .../Services/OptionService.cs | 12 +- InDepthSearch.UI/Services/SearchService.cs | 152 ++++++++ .../Services/ThemeService.cs | 11 +- InDepthSearch.UI/{Helpers => }/ViewLocator.cs | 8 +- InDepthSearch.UI/ViewModelLocator.cs | 46 +++ InDepthSearch.UI/Views/MainWindow.axaml | 270 +++---------- InDepthSearch.UI/Views/MainWindow.axaml.cs | 1 + InDepthSearch.UI/Views/OptionsView.axaml | 73 ++++ InDepthSearch.UI/Views/OptionsView.axaml.cs | 19 + InDepthSearch.UI/Views/ResultsView.axaml | 41 ++ InDepthSearch.UI/Views/ResultsView.axaml.cs | 19 + 52 files changed, 923 insertions(+), 766 deletions(-) create mode 100644 InDepthSearch.Core/Enums/AppLanguage.cs create mode 100644 InDepthSearch.Core/Enums/ImageExtension.cs create mode 100644 InDepthSearch.Core/Enums/MatchConfidence.cs create mode 100644 InDepthSearch.Core/Enums/RecognitionLanguage.cs create mode 100644 InDepthSearch.Core/Enums/RecognitionPrecision.cs create mode 100644 InDepthSearch.Core/Enums/SearchInfo.cs create mode 100644 InDepthSearch.Core/Enums/SearchStatus.cs create mode 100644 InDepthSearch.Core/Enums/Theme.cs create mode 100644 InDepthSearch.Core/Managers/Interfaces/IResultManager.cs create mode 100644 InDepthSearch.Core/Services/Interfaces/ISearchService.cs delete mode 100644 InDepthSearch.Core/Types/AppLanguage.cs delete mode 100644 InDepthSearch.Core/Types/MatchConfidence.cs delete mode 100644 InDepthSearch.Core/Types/RecognitionLanguage.cs delete mode 100644 InDepthSearch.Core/Types/RecognitionPrecision.cs delete mode 100644 InDepthSearch.Core/Types/SearchInfo.cs delete mode 100644 InDepthSearch.Core/Types/SearchStatus.cs delete mode 100644 InDepthSearch.Core/Types/Theme.cs delete mode 100644 InDepthSearch.Core/ViewModels/MainViewModel.cs create mode 100644 InDepthSearch.Core/ViewModels/MainWindowViewModel.cs create mode 100644 InDepthSearch.Core/ViewModels/OptionsViewModel.cs create mode 100644 InDepthSearch.Core/ViewModels/ResultsViewModel.cs create mode 100644 InDepthSearch.Core/ViewModels/ViewModelBase.cs create mode 100644 InDepthSearch.UI/Assets/Resources/BaseResources.xaml rename InDepthSearch.UI/{Helpers => Controls}/FluentWindow.cs (95%) create mode 100644 InDepthSearch.UI/Helpers/Conversion.cs create mode 100644 InDepthSearch.UI/Managers/ResultManager.cs rename {InDepthSearch.Core => InDepthSearch.UI}/Services/AppService.cs (52%) rename {InDepthSearch.Core => InDepthSearch.UI}/Services/DirectoryService.cs (95%) rename {InDepthSearch.Core => InDepthSearch.UI}/Services/OptionService.cs (75%) create mode 100644 InDepthSearch.UI/Services/SearchService.cs rename {InDepthSearch.Core => InDepthSearch.UI}/Services/ThemeService.cs (91%) rename InDepthSearch.UI/{Helpers => }/ViewLocator.cs (83%) create mode 100644 InDepthSearch.UI/ViewModelLocator.cs create mode 100644 InDepthSearch.UI/Views/OptionsView.axaml create mode 100644 InDepthSearch.UI/Views/OptionsView.axaml.cs create mode 100644 InDepthSearch.UI/Views/ResultsView.axaml create mode 100644 InDepthSearch.UI/Views/ResultsView.axaml.cs 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 2ebd522..360514c 100644 --- a/InDepthSearch.Core/InDepthSearch.Core.csproj +++ b/InDepthSearch.Core/InDepthSearch.Core.csproj @@ -13,7 +13,6 @@ - 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 f454150..068f49a 100644 --- a/InDepthSearch.Core/Models/ResultStats.cs +++ b/InDepthSearch.Core/Models/ResultStats.cs @@ -10,11 +10,11 @@ namespace InDepthSearch.Core.Models { public class ResultStats : ReactiveObject { - public ResultStats(string filesAnalyzed, int pagesAnalyzed, string executionTime) + public ResultStats() { - FilesAnalyzed = filesAnalyzed; - PagesAnalyzed = pagesAnalyzed; - ExecutionTime = executionTime; + FilesAnalyzed = ""; + PagesAnalyzed = 0; + ExecutionTime = ""; } [Reactive] diff --git a/InDepthSearch.Core/Models/SearchOptions.cs b/InDepthSearch.Core/Models/SearchOptions.cs index c957f19..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,20 +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, bool useDOC) + public SearchOptions() { - Path = path; - Keyword = keyword; - SelectedPrecisionOCR = selectedPrecisionOCR; - SelectedLanguageOCR = selectedLanguageOCR; - CaseSensitive = caseSensitive; - UseOCR = useOCR; - UseSubfolders = useSubfolders; - UsePDF = usePDF; - UseDOCX = useDOCX; - UseODT = useODT; - UseDOCX = useDOC; + UseOCR = true; + UsePDF = true; + Path = ""; + Keyword = ""; + SelectedLanguageOCR = RecognitionLanguage.Default; + SelectedPrecisionOCR = RecognitionPrecision.Default; } [Reactive] 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 049abd4..0000000 --- a/InDepthSearch.Core/ViewModels/MainViewModel.cs +++ /dev/null @@ -1,357 +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 InDepthSearch.Core.Services.Interfaces; -using DocumentFormat.OpenXml.Packaging; -using System.Diagnostics; - -namespace InDepthSearch.Core.ViewModels -{ - public class MainViewModel : ReactiveObject - { - 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; } - public ReactiveCommand OpenUrl { get; } - - [Reactive] - public string ResultInfo { get; set; } - public bool KeywordErrorVisible => string.IsNullOrWhiteSpace(Options.Keyword); - public bool PathErrorVisible => !Directory.Exists(Options.Path); - public bool FormatsErrorVisible => !(Options.UseDOC || Options.UseDOCX || Options.UseODT || Options.UsePDF); - public bool CanExecute => !KeywordErrorVisible && !PathErrorVisible && !FormatsErrorVisible; - [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, false); - Results = new ObservableCollection(); - Stats = new ResultStats("0/0", 0, "0"); - StatusName = SearchStatus.Ready.ToString(); - ResultInfo = "Click search button to start"; - CurrentThemeName = Theme.Default.ToString().ToUpper(); - CurrentLanguageName = AppLanguage.English.ToString().ToUpper(); - ItemsReady = false; - this.WhenAnyValue(x => x.Options.Keyword, x => x.Options.Path).Subscribe(x => ValidateSelection()); - - 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(); - }); - 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 - 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, false); - Results = new ObservableCollection(); - Stats = new ResultStats("", 0, ""); - ResultInfo = infoService.GetSearchInfo(SearchInfo.Init); - StatusName = infoService.GetSearchStatus(SearchStatus.Ready); - CurrentThemeName = themeService.GetCurrentThemeName(); - CurrentLanguageName = infoService.GetCurrentLanguage(); - ItemsReady = false; - - // 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()); - - // Get assembly version - AppVersion = infoService.GetVersion(); - } - - private void ValidateSelection() - { - this.RaisePropertyChanged(nameof(PathErrorVisible)); - this.RaisePropertyChanged(nameof(KeywordErrorVisible)); - this.RaisePropertyChanged(nameof(FormatsErrorVisible)); - this.RaisePropertyChanged(nameof(CanExecute)); - } - - private void UpdateStringResources() - { - CurrentThemeName = _themeService.GetCurrentThemeName(); - StatusName = _infoService.GetSearchStatus(); - ResultInfo = _infoService.GetSearchInfo(); - } - - void StartReading() - { - var searchOptions = Options; - - Results.Clear(); - StatusName = _infoService.GetSearchStatus(SearchStatus.Initializing); - ItemsReady = false; - Stats.FilesAnalyzed = "0/0"; - Stats.PagesAnalyzed = 0; - Stats.ExecutionTime = ""; - - 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); - ResultInfo = _infoService.GetSearchInfo(SearchInfo.Init); - Stats.FilesAnalyzed = "0/" + discoveredFiles.Count.ToString(); - - foreach (var file in discoveredFiles) - { - System.Diagnostics.Debug.WriteLine("Checking " + file); - - if (file.EndsWith(".pdf")) - HandlePDF(file, searchOptions); - else if (file.EndsWith(".docx") || file.EndsWith(".doc")) - HandleDOCX(file, searchOptions); - - 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(); - 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; - } - - 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(); - 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, file, i, searchOptions.CaseSensitive); - 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); - } - } - - } - -} 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/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..b1aad36 100644 --- a/InDepthSearch.UI/InDepthSearch.UI.csproj +++ b/InDepthSearch.UI/InDepthSearch.UI.csproj @@ -11,6 +11,7 @@ + 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/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 e50c8fd..f4b8cad 100644 --- a/InDepthSearch.UI/Views/MainWindow.axaml +++ b/InDepthSearch.UI/Views/MainWindow.axaml @@ -3,228 +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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +