diff --git a/src/Jarvis.Addin.Files/Constants.cs b/src/Jarvis.Addin.Files/Constants.cs new file mode 100644 index 0000000..1a8bfb9 --- /dev/null +++ b/src/Jarvis.Addin.Files/Constants.cs @@ -0,0 +1,26 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Jarvis.Addin.Files +{ + internal sealed class Constants + { + internal sealed class Settings + { + public const string Version = "Files.Version"; + + internal sealed class Include + { + public const string Folders = "Files.Include.Folders"; + public const string Extensions = "Files.Include.Extensions"; + } + + internal sealed class Exclude + { + public const string Folders = "Files.Exclude.Folders"; + public const string Patterns = "Files.Exclude.Patterns"; + } + } + } +} diff --git a/src/Jarvis.Addin.Files/FileAddin.cs b/src/Jarvis.Addin.Files/FileAddin.cs index c6db8db..2a21a1d 100644 --- a/src/Jarvis.Addin.Files/FileAddin.cs +++ b/src/Jarvis.Addin.Files/FileAddin.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using Autofac; -using Jarvis.Addin.Files.Drawing; +using Jarvis.Addin.Files.Icons; using Jarvis.Addin.Files.Indexing; using Jarvis.Addin.Files.Sources; using Jarvis.Addin.Files.Sources.Uwp; @@ -18,12 +18,14 @@ public void Configure(ContainerBuilder builder) { builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); // Sources builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); + // Utilities builder.RegisterType().SingleInstance(); builder.RegisterType().SingleInstance(); builder.RegisterType().As().SingleInstance(); diff --git a/src/Jarvis.Addin.Files/FileProvider.cs b/src/Jarvis.Addin.Files/FileProvider.cs index 71b26f4..5cc299f 100644 --- a/src/Jarvis.Addin.Files/FileProvider.cs +++ b/src/Jarvis.Addin.Files/FileProvider.cs @@ -7,19 +7,26 @@ using System.Diagnostics; using System.Linq; using System.Net; +using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Media; -using Jarvis.Addin.Files.Drawing; +using Jarvis.Addin.Files.Extensions; +using Jarvis.Addin.Files.Icons; using Jarvis.Addin.Files.Indexing; +using Jarvis.Addin.Files.Sources; +using Jarvis.Addin.Files.ViewModels; using Jarvis.Core; +using Jarvis.Core.Interop; using JetBrains.Annotations; +using Spectre.System.IO; using static Jarvis.Addin.Files.Sources.Uwp.ShellInterop; +using Path = System.IO.Path; namespace Jarvis.Addin.Files { [UsedImplicitly] - internal sealed class FileProvider : QueryProvider + internal sealed class FileProvider : QueryProvider { private readonly IFileIndex _index; private readonly IconLoader _loader; @@ -55,7 +62,36 @@ protected override Task ExecuteAsync(FileResult result) if (result.Path.Scheme == "shell") { - Process.Start(path); + if (path.EndsWith("lnk") && result.OriginalEntry is StartMenuIndexSourceEntry startMenuEntry) + { + path = WebUtility.UrlDecode(startMenuEntry.TargetPath.ToUri("shell").AbsolutePath).TrimStart('/'); + } + var existingProcess = Process + .GetProcesses() + .FirstOrDefault(process => + { + var processPath = new StringBuilder(1024); + var size = processPath.Capacity; + var processHandle = Win32.OpenProcess(0x1000, false, process.Id); + Win32.QueryFullProcessImageName(processHandle, 0, processPath, out size); + return processPath.ToString() == path; + }); + + if (existingProcess != null) + { + Win32.SetForegroundWindow(existingProcess.MainWindowHandle); + + var rect = new Win32.W32Rect(); + Win32.GetWindowRect(existingProcess.MainWindowHandle, ref rect); + if (rect.Top < 0 && rect.Right < 0 && rect.Bottom < 0 && rect.Left < 0) // Window is minimized + { + Win32.ShowWindow(existingProcess.MainWindowHandle, 1); + } + } + else + { + Process.Start(path); + } } else if (result.Path.Scheme == "uwp") { diff --git a/src/Jarvis.Addin.Files/FileResult.cs b/src/Jarvis.Addin.Files/FileResult.cs index 8e04eee..1b93316 100644 --- a/src/Jarvis.Addin.Files/FileResult.cs +++ b/src/Jarvis.Addin.Files/FileResult.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; +using Jarvis.Addin.Files.Indexing; using Jarvis.Core; namespace Jarvis.Addin.Files @@ -18,9 +19,10 @@ internal sealed class FileResult : IQueryResult, IEquatable public string Description { get; } public float Distance { get; } public float Score { get; } + public IndexedEntry OriginalEntry { get; } public FileResult(QueryResultType type, Uri path, Uri icon, - string title, string description, float distance, float score) + string title, string description, float distance, float score, IndexedEntry originalEntry) { Type = type; Path = path; @@ -29,6 +31,7 @@ public FileResult(QueryResultType type, Uri path, Uri icon, Description = description; Distance = distance; Score = score; + OriginalEntry = originalEntry; } public override bool Equals(object obj) diff --git a/src/Jarvis.Addin.Files/FileSettings.cs b/src/Jarvis.Addin.Files/FileSettings.cs new file mode 100644 index 0000000..ccd9c23 --- /dev/null +++ b/src/Jarvis.Addin.Files/FileSettings.cs @@ -0,0 +1,82 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Jarvis.Core; +using Spectre.System.IO; + +namespace Jarvis.Addin.Files +{ + internal sealed class FileSettings + { + public int Version { get; set; } + public HashSet IncludedFolders { get; } + public HashSet IncludedExtensions { get; } + public HashSet ExcludedFolders { get; } + public HashSet ExcludedPatterns { get; } + + public const int CurrentVersion = 1; + + public FileSettings() + { + IncludedFolders = new HashSet(new PathComparer(false)); + IncludedExtensions = new HashSet(StringComparer.Ordinal); + ExcludedFolders = new HashSet(new PathComparer(false)); + ExcludedPatterns = new HashSet(StringComparer.Ordinal); + } + + public static FileSettings Load(ISettingsStore settings) + { + var model = new FileSettings + { + Version = settings.Get(Constants.Settings.Version) + }; + + // Load included folders. + var includeFolders = settings.Get(Constants.Settings.Include.Folders); + if (includeFolders != null) + { + model.IncludedFolders.AddRange(includeFolders + .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => new DirectoryPath(s))); + } + + // Load included extensions. + var includeExtensions = settings.Get(Constants.Settings.Include.Extensions); + if (!string.IsNullOrWhiteSpace(includeExtensions)) + { + model.IncludedExtensions.AddRange(includeExtensions.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)); + } + + // Load excluded folders. + var excludeFolders = settings.Get(Constants.Settings.Exclude.Folders); + if (excludeFolders != null) + { + model.ExcludedFolders.AddRange(excludeFolders + .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => new DirectoryPath(s))); + } + + // Load excluded patterns. + var excludePatterns = settings.Get(Constants.Settings.Exclude.Patterns); + if (!string.IsNullOrWhiteSpace(excludePatterns)) + { + model.ExcludedPatterns.AddRange(excludePatterns.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)); + } + + return model; + } + + public void Save(ISettingsStore settings) + { + settings.Set(Constants.Settings.Version, CurrentVersion); + settings.Set(Constants.Settings.Include.Folders, string.Join("|", IncludedFolders.Select(p => p.FullPath))); + settings.Set(Constants.Settings.Include.Extensions, string.Join("|", IncludedExtensions)); + settings.Set(Constants.Settings.Exclude.Folders, string.Join("|", ExcludedFolders.Select(p => p.FullPath))); + settings.Set(Constants.Settings.Exclude.Patterns, string.Join("|", ExcludedPatterns)); + } + } +} diff --git a/src/Jarvis.Addin.Files/FileSettingsSeeder.cs b/src/Jarvis.Addin.Files/FileSettingsSeeder.cs new file mode 100644 index 0000000..bff7b8e --- /dev/null +++ b/src/Jarvis.Addin.Files/FileSettingsSeeder.cs @@ -0,0 +1,44 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Jarvis.Core; +using Spectre.System.IO; + +namespace Jarvis.Addin.Files +{ + internal sealed class FileSettingsSeeder : ISettingsSeeder + { + public void Seed(ISettingsStore store) + { + var model = FileSettings.Load(store); + + if (model.Version == 0) + { + Initialize(model); + model.Save(store); + } + } + + private static void Initialize(FileSettings model) + { + // Included folders. + model.IncludedFolders.Add(new DirectoryPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments))); + model.IncludedFolders.Add(new DirectoryPath(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic))); + model.IncludedFolders.Add(new DirectoryPath(Environment.GetFolderPath(Environment.SpecialFolder.MyVideos))); + model.IncludedFolders.Add(new DirectoryPath(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures))); + + // Included extensions. + model.IncludedExtensions.AddRange(new[] + { + "ai", "avi", "doc", "docx", "eps", "flv", "gif", "htm", "html", + "jpeg", "jpg", "mov", "mp3", "mp4", "mpg", "mpeg", "odt", "ogg", "ogv", "pdf", "png", "ppt", "psd", + "rar", "rtf", "svg", "txt", "wav", "wma", "xls", "xlsx", "zip" + }); + + // Excluded patterns. + model.ExcludedPatterns.AddRange(new[] { ".git", "node_modules", "packages" }); + } + } +} diff --git a/src/Jarvis.Addin.Files/Drawing/IconLoader.cs b/src/Jarvis.Addin.Files/Icons/IconLoader.cs similarity index 98% rename from src/Jarvis.Addin.Files/Drawing/IconLoader.cs rename to src/Jarvis.Addin.Files/Icons/IconLoader.cs index 6085a8e..70c79ab 100644 --- a/src/Jarvis.Addin.Files/Drawing/IconLoader.cs +++ b/src/Jarvis.Addin.Files/Icons/IconLoader.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; using System.Windows.Media; -namespace Jarvis.Addin.Files.Drawing +namespace Jarvis.Addin.Files.Icons { internal sealed class IconLoader { diff --git a/src/Jarvis.Addin.Files/Drawing/ShellIconLoader.cs b/src/Jarvis.Addin.Files/Icons/ShellIconLoader.cs similarity index 98% rename from src/Jarvis.Addin.Files/Drawing/ShellIconLoader.cs rename to src/Jarvis.Addin.Files/Icons/ShellIconLoader.cs index 7ad45cc..b62b093 100644 --- a/src/Jarvis.Addin.Files/Drawing/ShellIconLoader.cs +++ b/src/Jarvis.Addin.Files/Icons/ShellIconLoader.cs @@ -13,7 +13,7 @@ using System.Windows.Media.Imaging; using Jarvis.Core.Interop; -namespace Jarvis.Addin.Files.Drawing +namespace Jarvis.Addin.Files.Icons { internal static class ShellIconLoader { diff --git a/src/Jarvis.Addin.Files/Drawing/UwpIconLoader.cs b/src/Jarvis.Addin.Files/Icons/UwpIconLoader.cs similarity index 98% rename from src/Jarvis.Addin.Files/Drawing/UwpIconLoader.cs rename to src/Jarvis.Addin.Files/Icons/UwpIconLoader.cs index bf09cc6..57b24b5 100644 --- a/src/Jarvis.Addin.Files/Drawing/UwpIconLoader.cs +++ b/src/Jarvis.Addin.Files/Icons/UwpIconLoader.cs @@ -10,7 +10,7 @@ using ColorConverter = System.Windows.Media.ColorConverter; using Pen = System.Windows.Media.Pen; -namespace Jarvis.Addin.Files.Drawing +namespace Jarvis.Addin.Files.Icons { internal static class UwpIconLoader { diff --git a/src/Jarvis.Addin.Files/Indexing/FileIndexer.cs b/src/Jarvis.Addin.Files/Indexing/FileIndexer.cs index cc14242..530f665 100644 --- a/src/Jarvis.Addin.Files/Indexing/FileIndexer.cs +++ b/src/Jarvis.Addin.Files/Indexing/FileIndexer.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Caliburn.Micro; using Jarvis.Addin.Files.Collections; using Jarvis.Core; using Jarvis.Core.Diagnostics; @@ -19,25 +20,29 @@ namespace Jarvis.Addin.Files.Indexing { [UsedImplicitly] - internal sealed class FileIndexer : IBackgroundWorker, IFileIndex + internal sealed class FileIndexer : IBackgroundWorker, IFileIndex, IHandle { private readonly IJarvisLog _log; private readonly List _sources; private readonly HashSet _stopWords; private readonly ScoreComparer _comparer; private readonly IndexedEntryComparer _entryComparer; + private readonly ManualResetEvent _trigger; private Trie _trie; public string Name => "File indexing service"; - public FileIndexer(IEnumerable sources, IJarvisLog log) + public FileIndexer(IEventAggregator events, IEnumerable sources, IJarvisLog log) { _log = new LogDecorator("FileIndexer", log); _sources = new List(sources ?? Array.Empty()); _stopWords = new HashSet(StringComparer.OrdinalIgnoreCase) { "to", "the" }; _comparer = new ScoreComparer(); _entryComparer = new IndexedEntryComparer(); + _trigger = new ManualResetEvent(false); + + events.Subscribe(this); } public Task Run(CancellationToken token) @@ -46,8 +51,8 @@ public Task Run(CancellationToken token) { while (true) { - var st = new Stopwatch(); - st.Start(); + var indexingWatch = new Stopwatch(); + indexingWatch.Start(); var result = LoadResults(token); @@ -57,8 +62,8 @@ public Task Run(CancellationToken token) } _log.Debug("Updating index..."); - var st2 = new Stopwatch(); - st2.Start(); + var indexUpdateWatch = new Stopwatch(); + indexUpdateWatch.Start(); var trie = new Trie(); foreach (var file in result) @@ -86,8 +91,8 @@ public Task Run(CancellationToken token) } } - st2.Stop(); - _log.Debug($"Building trie took {st2.ElapsedMilliseconds}ms"); + indexUpdateWatch.Stop(); + _log.Debug($"Building trie took {indexUpdateWatch.ElapsedMilliseconds}ms"); _log.Debug("Writing index..."); Interlocked.Exchange(ref _trie, trie); @@ -95,22 +100,30 @@ public Task Run(CancellationToken token) _log.Verbose($"Nodes: {_trie.NodeCount}"); _log.Verbose($"Items: {_trie.ItemCount}"); - // Wait for a minute. - st.Stop(); - _log.Debug($"Indexing done. Took {st.ElapsedMilliseconds}ms"); + indexingWatch.Stop(); + _log.Debug($"Indexing done. Took {indexingWatch.ElapsedMilliseconds}ms"); - if (token.WaitHandle.WaitOne((int)TimeSpan.FromMinutes(5).TotalMilliseconds)) + // Wait for a while. + var index = WaitHandle.WaitAny(new[] { token.WaitHandle, _trigger }, (int)TimeSpan.FromMinutes(5).TotalMilliseconds); + if (index == 0) { _log.Information("We were instructed to stop (2)."); break; } + + // Triggered update? + if (index == 1) + { + _log.Information("A re-index was triggered."); + _trigger.Reset(); + } } return true; - }); + }, token); } - private List LoadResults(CancellationToken token) + private IEnumerable LoadResults(CancellationToken token) { // Ask all sources for files. return _sources.AsParallel() @@ -183,5 +196,10 @@ private static float CalculateScore(IndexedEntry entry, string query) return Math.Min(LevenshteinScorer.Score(entry.Title, query), LevenshteinScorer.Score(entry.Description ?? entry.Title, query)); } + + public void Handle(TriggerIndexMessage message) + { + _trigger.Set(); + } } } \ No newline at end of file diff --git a/src/Jarvis.Addin.Files/Indexing/TriggerIndexMessage.cs b/src/Jarvis.Addin.Files/Indexing/TriggerIndexMessage.cs new file mode 100644 index 0000000..cd80941 --- /dev/null +++ b/src/Jarvis.Addin.Files/Indexing/TriggerIndexMessage.cs @@ -0,0 +1,10 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Jarvis.Addin.Files.Indexing +{ + internal sealed class TriggerIndexMessage + { + } +} diff --git a/src/Jarvis.Addin.Files/Jarvis.Addin.Files.csproj b/src/Jarvis.Addin.Files/Jarvis.Addin.Files.csproj index 4909346..9cf7b1a 100644 --- a/src/Jarvis.Addin.Files/Jarvis.Addin.Files.csproj +++ b/src/Jarvis.Addin.Files/Jarvis.Addin.Files.csproj @@ -35,6 +35,15 @@ ..\packages\Autofac.4.6.2\lib\net45\Autofac.dll + + ..\packages\Caliburn.Micro.Core.3.2.0\lib\net45\Caliburn.Micro.dll + + + ..\packages\Caliburn.Micro.3.2.0\lib\net45\Caliburn.Micro.Platform.dll + + + ..\packages\Caliburn.Micro.3.2.0\lib\net45\Caliburn.Micro.Platform.Core.dll + ..\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll @@ -62,6 +71,10 @@ + + + C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7\System.Xaml.dll + @@ -84,13 +97,16 @@ - + + + + @@ -98,6 +114,7 @@ + @@ -112,11 +129,18 @@ - + - + + + + + + + + @@ -147,7 +171,28 @@ - + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + diff --git a/src/Jarvis.Addin.Files/Sources/DocumentIndexSource.cs b/src/Jarvis.Addin.Files/Sources/DocumentIndexSource.cs index 3f53adf..2906a48 100644 --- a/src/Jarvis.Addin.Files/Sources/DocumentIndexSource.cs +++ b/src/Jarvis.Addin.Files/Sources/DocumentIndexSource.cs @@ -4,9 +4,13 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; using Jarvis.Addin.Files.Extensions; using Jarvis.Addin.Files.Indexing; using Jarvis.Core; +using Jarvis.Core.Diagnostics; using JetBrains.Annotations; using Spectre.System.IO; @@ -16,34 +20,38 @@ namespace Jarvis.Addin.Files.Sources internal sealed class DocumentIndexSource : IFileIndexSource { private readonly IFileSystem _fileSystem; + private readonly ISettingsStore _settings; + private readonly IJarvisLog _log; public string Name => "User document"; - public DocumentIndexSource(IFileSystem fileSystem) + public DocumentIndexSource(IFileSystem fileSystem, ISettingsStore settings, IJarvisLog log) { _fileSystem = fileSystem; + _settings = settings; + _log = log; } public IEnumerable Index() { - var folders = new[] - { - new DirectoryPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)), - new DirectoryPath(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic)), - new DirectoryPath(Environment.GetFolderPath(Environment.SpecialFolder.MyVideos)), - new DirectoryPath(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)) - }; + var settings = FileSettings.Load(_settings); + var extensions = new HashSet(settings.IncludedExtensions, StringComparer.OrdinalIgnoreCase); + var regexes = settings.ExcludedPatterns.Select(s => new Regex(s, RegexOptions.Singleline)).ToList(); - foreach (var folder in folders) + foreach (var folder in settings.IncludedFolders) { - foreach (var entry in Index(folder)) + foreach (var entry in Index(folder, settings.ExcludedFolders, extensions, regexes)) { yield return entry; } } } - private IEnumerable Index(DirectoryPath path) + private IEnumerable Index( + DirectoryPath path, + HashSet excludedFolders, + HashSet includedExtensions, + List excludedPatterns) { var stack = new Stack(); stack.Push(path); @@ -52,6 +60,20 @@ private IEnumerable Index(DirectoryPath path) { var current = stack.Pop(); + // Folder excluded? + if (excludedFolders.Contains(current)) + { + _log.Debug($"Folder '{current.FullPath}' has been excluded."); + continue; + } + + // Folder name not allowed? + if (TryMatchPattern(current, excludedPatterns, out var pattern)) + { + _log.Debug($"Folder '{current.FullPath}' matched pattern '{pattern}' that has been excluded."); + continue; + } + var directory = _fileSystem.GetDirectorySafe(current); if (directory != null) { @@ -78,6 +100,18 @@ private IEnumerable Index(DirectoryPath path) // TODO: Temporary fix due to Spectre.System lib. continue; } + if ((file.Attributes & FileAttributes.System) == FileAttributes.System) + { + continue; + } + if ((file.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) + { + continue; + } + if (!includedExtensions.Contains(file.Path.GetExtension().Name)) + { + continue; + } yield return DocumentIndexSourceEntry.File(file.Path, file.Path.GetFilename().FullPath, file.Path.FullPath, @@ -90,5 +124,20 @@ private IEnumerable Index(DirectoryPath path) } } } + + private static bool TryMatchPattern(DirectoryPath current, IEnumerable excludedPatterns, out Regex pattern) + { + pattern = null; + var currentName = current.GetDirectoryName(); + foreach (var excludedPattern in excludedPatterns) + { + if (excludedPattern.IsMatch(currentName)) + { + pattern = excludedPattern; + return true; + } + } + return false; + } } } diff --git a/src/Jarvis.Addin.Files/Sources/DocumentIndexSourceEntry.cs b/src/Jarvis.Addin.Files/Sources/DocumentIndexSourceEntry.cs index f20d467..9daf8f1 100644 --- a/src/Jarvis.Addin.Files/Sources/DocumentIndexSourceEntry.cs +++ b/src/Jarvis.Addin.Files/Sources/DocumentIndexSourceEntry.cs @@ -42,7 +42,8 @@ public override FileResult GetFileResult(string query, float distance, float sco return new FileResult( IsDirectory ? QueryResultType.Folder : QueryResultType.File, Path.ToUri("shell"), - Icon, Title, Description, distance, score); + Icon, Title, Description, distance, score, + this); } protected override int GetEntryHashCode() diff --git a/src/Jarvis.Addin.Files/Sources/StartMenuIndexSourceEntry.cs b/src/Jarvis.Addin.Files/Sources/StartMenuIndexSourceEntry.cs index 36346be..349a85c 100644 --- a/src/Jarvis.Addin.Files/Sources/StartMenuIndexSourceEntry.cs +++ b/src/Jarvis.Addin.Files/Sources/StartMenuIndexSourceEntry.cs @@ -30,7 +30,8 @@ public override FileResult GetFileResult(string query, float distance, float sco QueryResultType.Application, Path.ToUri("shell"), Icon, - Title, Description, distance, score); + Title, Description, distance, score, + this); } protected override int GetEntryHashCode() diff --git a/src/Jarvis.Addin.Files/Sources/UwpIndexSourceEntry.cs b/src/Jarvis.Addin.Files/Sources/UwpIndexSourceEntry.cs index 628fc74..1f8604a 100644 --- a/src/Jarvis.Addin.Files/Sources/UwpIndexSourceEntry.cs +++ b/src/Jarvis.Addin.Files/Sources/UwpIndexSourceEntry.cs @@ -26,7 +26,8 @@ public override FileResult GetFileResult(string query, float distance, float sco return new FileResult( QueryResultType.Application, new Uri($"uwp:///{WebUtility.UrlEncode(Id)}?aumid={WebUtility.UrlEncode(AppUserModelId)}"), Icon, - Title, Description, distance, score); + Title, Description, distance, score, + this); } protected override int GetEntryHashCode() diff --git a/src/Jarvis.Addin.Files/ViewModels/ExcludeFolderViewModel.cs b/src/Jarvis.Addin.Files/ViewModels/ExcludeFolderViewModel.cs new file mode 100644 index 0000000..d7ecb4a --- /dev/null +++ b/src/Jarvis.Addin.Files/ViewModels/ExcludeFolderViewModel.cs @@ -0,0 +1,29 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Jarvis.Core; + +namespace Jarvis.Addin.Files.ViewModels +{ + internal sealed class ExcludeFolderViewModel : SelectFolderViewModel + { + protected override string Description => "Select a folder to exclude from indexing."; + + public override ValidationResult Validate() + { + return ValidationResult.Ok(); + } + + public override void Load(FileSettings settings) + { + Items.AddRange(settings.ExcludedFolders); + } + + public override void Populate(ref FileSettings settings) + { + settings.ExcludedFolders.Clear(); + settings.ExcludedFolders.AddRange(Items); + } + } +} diff --git a/src/Jarvis.Addin.Files/ViewModels/ExcludePatternViewModel.cs b/src/Jarvis.Addin.Files/ViewModels/ExcludePatternViewModel.cs new file mode 100644 index 0000000..0afe2f9 --- /dev/null +++ b/src/Jarvis.Addin.Files/ViewModels/ExcludePatternViewModel.cs @@ -0,0 +1,44 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using System.Linq; +using Jarvis.Core; + +namespace Jarvis.Addin.Files.ViewModels +{ + internal sealed class ExcludePatternViewModel : SelectPatternViewModel + { + public override ValidationResult Validate() + { + return ValidationResult.Ok(); + } + + public override string Process(string value) + { + return value?.Trim(); + } + + public override bool IsValid(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + var invalidCharacters = Path.GetInvalidPathChars(); + return value.All(character => !invalidCharacters.Contains(character)); + } + + public override void Load(FileSettings settings) + { + Items.AddRange(settings.ExcludedPatterns); + } + + public override void Populate(ref FileSettings settings) + { + settings.ExcludedPatterns.Clear(); + settings.ExcludedPatterns.AddRange(Items); + } + } +} diff --git a/src/Jarvis.Addin.Files/ViewModels/FileSettingsViewModel.cs b/src/Jarvis.Addin.Files/ViewModels/FileSettingsViewModel.cs new file mode 100644 index 0000000..8c6fbf6 --- /dev/null +++ b/src/Jarvis.Addin.Files/ViewModels/FileSettingsViewModel.cs @@ -0,0 +1,78 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Caliburn.Micro; +using Jarvis.Addin.Files.Indexing; +using Jarvis.Core; + +namespace Jarvis.Addin.Files.ViewModels +{ + internal sealed class FileSettingsViewModel : Screen, ISettings + { + private readonly IEventAggregator _eventAggregator; + private bool _hasChanges; + + public string Name => "File indexing"; + + public IncludeFolderViewModel IncludeFolders { get; } + public IncludeExtensionsViewModel IncludeExtensions { get; } + public ExcludeFolderViewModel ExcludeFolders { get; } + public ExcludePatternViewModel ExcludePatterns { get; } + + public FileSettingsViewModel( + IEventAggregator eventAggregator, + IncludeFolderViewModel includeFolders, + IncludeExtensionsViewModel includeExtensions, + ExcludeFolderViewModel excludeFolders, + ExcludePatternViewModel excludePatterns) + { + _eventAggregator = eventAggregator; + IncludeFolders = includeFolders; + IncludeExtensions = includeExtensions; + ExcludeFolders = excludeFolders; + ExcludePatterns = excludePatterns; + } + + public ValidationResult Validate() + { + return ValidationResult.Ok(); + } + + public void Load(ISettingsStore settings) + { + var model = FileSettings.Load(settings); + + IncludeFolders.Load(model); + IncludeExtensions.Load(model); + ExcludeFolders.Load(model); + ExcludePatterns.Load(model); + } + + public void Save(ISettingsStore settings) + { + var model = new FileSettings(); + + IncludeFolders.Populate(ref model); + IncludeExtensions.Populate(ref model); + ExcludeFolders.Populate(ref model); + ExcludePatterns.Populate(ref model); + + model.Save(settings); + + if (!_hasChanges) + { + _hasChanges = IncludeFolders.IsDirty || IncludeExtensions.IsDirty + || ExcludeFolders.IsDirty || ExcludePatterns.IsDirty; + } + } + + public void OnSaved() + { + if (_hasChanges) + { + _eventAggregator.PublishOnCurrentThread(new TriggerIndexMessage()); + } + } + } +} diff --git a/src/Jarvis.Addin.Files/ViewModels/IncludeExtensionsViewModel.cs b/src/Jarvis.Addin.Files/ViewModels/IncludeExtensionsViewModel.cs new file mode 100644 index 0000000..ba664e8 --- /dev/null +++ b/src/Jarvis.Addin.Files/ViewModels/IncludeExtensionsViewModel.cs @@ -0,0 +1,44 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using System.Linq; +using Jarvis.Core; + +namespace Jarvis.Addin.Files.ViewModels +{ + internal sealed class IncludeExtensionsViewModel : SelectPatternViewModel + { + public override ValidationResult Validate() + { + return ValidationResult.Ok(); + } + + public override string Process(string value) + { + return value?.TrimStart('.').Trim(); + } + + public override bool IsValid(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + var invalidCharacters = Path.GetInvalidFileNameChars(); + return value.All(character => !invalidCharacters.Contains(character)); + } + + public override void Load(FileSettings settings) + { + Items.AddRange(settings.IncludedExtensions); + } + + public override void Populate(ref FileSettings settings) + { + settings.IncludedExtensions.Clear(); + settings.IncludedExtensions.AddRange(Items); + } + } +} \ No newline at end of file diff --git a/src/Jarvis.Addin.Files/ViewModels/IncludeFolderViewModel.cs b/src/Jarvis.Addin.Files/ViewModels/IncludeFolderViewModel.cs new file mode 100644 index 0000000..1de7fb7 --- /dev/null +++ b/src/Jarvis.Addin.Files/ViewModels/IncludeFolderViewModel.cs @@ -0,0 +1,29 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Jarvis.Core; + +namespace Jarvis.Addin.Files.ViewModels +{ + internal sealed class IncludeFolderViewModel : SelectFolderViewModel + { + protected override string Description => "Select a folder to index."; + + public override ValidationResult Validate() + { + return ValidationResult.Ok(); + } + + public override void Load(FileSettings settings) + { + Items.AddRange(settings.IncludedFolders); + } + + public override void Populate(ref FileSettings settings) + { + settings.IncludedFolders.Clear(); + settings.IncludedFolders.AddRange(Items); + } + } +} \ No newline at end of file diff --git a/src/Jarvis.Addin.Files/ViewModels/SelectFolderViewModel.cs b/src/Jarvis.Addin.Files/ViewModels/SelectFolderViewModel.cs new file mode 100644 index 0000000..cdd2670 --- /dev/null +++ b/src/Jarvis.Addin.Files/ViewModels/SelectFolderViewModel.cs @@ -0,0 +1,53 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Windows.Forms; +using Caliburn.Micro; +using Jarvis.Core; +using Spectre.System.IO; + +namespace Jarvis.Addin.Files.ViewModels +{ + internal abstract class SelectFolderViewModel : Conductor.Collection.OneActive + { + public bool CanRemoveFolder => ActiveItem != null; + protected abstract string Description { get; } + public bool IsDirty { get; private set; } + + public void Toggle(DirectoryPath item) + { + NotifyOfPropertyChange(nameof(CanRemoveFolder)); + } + + public void AddFolder() + { + using (var dialog = new FolderBrowserDialog()) + { + dialog.Description = Description; + + var result = dialog.ShowDialog(); + if (result == DialogResult.OK) + { + Items.Add(new DirectoryPath(dialog.SelectedPath)); + IsDirty = true; + } + } + } + + public void RemoveFolder() + { + if (ActiveItem != null) + { + Items.Remove(ActiveItem); + ActiveItem = null; + NotifyOfPropertyChange(nameof(CanRemoveFolder)); + IsDirty = true; + } + } + + public abstract ValidationResult Validate(); + public abstract void Load(FileSettings settings); + public abstract void Populate(ref FileSettings settings); + } +} \ No newline at end of file diff --git a/src/Jarvis.Addin.Files/ViewModels/SelectPatternViewModel.cs b/src/Jarvis.Addin.Files/ViewModels/SelectPatternViewModel.cs new file mode 100644 index 0000000..cfc6480 --- /dev/null +++ b/src/Jarvis.Addin.Files/ViewModels/SelectPatternViewModel.cs @@ -0,0 +1,69 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Caliburn.Micro; +using Jarvis.Core; + +namespace Jarvis.Addin.Files.ViewModels +{ + internal abstract class SelectPatternViewModel : Conductor.Collection.OneActive + { + private string _pattern; + + public string Pattern + { + get => _pattern; + set + { + _pattern = value; + NotifyOfPropertyChange(nameof(Pattern)); + NotifyOfPropertyChange(nameof(CanAddPattern)); + NotifyOfPropertyChange(nameof(CanRemovePattern)); + } + } + + public bool CanRemovePattern => ActiveItem != null && Items.Contains(ActiveItem); + public bool CanAddPattern + { + get + { + var processedPattern = Process(Pattern); + return IsValid(processedPattern) && !Items.Contains(processedPattern); + } + } + + public bool IsDirty { get; private set; } + + public void Toggle(string item) + { + NotifyOfPropertyChange(nameof(CanRemovePattern)); + } + + public void AddPattern() + { + if (CanAddPattern) + { + Items.Add(Process(Pattern)); + Pattern = string.Empty; + IsDirty = true; + } + } + + public void RemovePattern() + { + if (CanRemovePattern) + { + Items.Remove(ActiveItem); + NotifyOfPropertyChange(nameof(CanRemovePattern)); + IsDirty = true; + } + } + + public abstract ValidationResult Validate(); + public abstract string Process(string value); + public abstract bool IsValid(string value); + public abstract void Load(FileSettings settings); + public abstract void Populate(ref FileSettings settings); + } +} \ No newline at end of file diff --git a/src/Jarvis.Addin.Files/Views/ExcludeFolderView.xaml b/src/Jarvis.Addin.Files/Views/ExcludeFolderView.xaml new file mode 100644 index 0000000..5abc0df --- /dev/null +++ b/src/Jarvis.Addin.Files/Views/ExcludeFolderView.xaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + - - - + + + + + + + + + + + + + diff --git a/src/Jarvis/Views/ShellView.xaml b/src/Jarvis/Views/ShellView.xaml index 4d62ac8..5331307 100644 --- a/src/Jarvis/Views/ShellView.xaml +++ b/src/Jarvis/Views/ShellView.xaml @@ -11,7 +11,8 @@ ResizeMode="NoResize" AllowsTransparency="True" ShowActivated="False" - Topmost="False" Visibility="Hidden" + Topmost="False" + Visibility="Hidden" cal:Message.Attach="[Event Deactivated] = [Action OnDeactivated()]; [Event Closing] = [Action OnClose($eventArgs)]" WindowStyle="None"> @@ -69,7 +70,7 @@ - + diff --git a/src/Jarvis/Views/UpdateView.xaml b/src/Jarvis/Views/UpdateView.xaml index 1dd8493..514033b 100644 --- a/src/Jarvis/Views/UpdateView.xaml +++ b/src/Jarvis/Views/UpdateView.xaml @@ -3,12 +3,14 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" + xmlns:cal="http://www.caliburnproject.org" mc:Ignorable="d" Topmost="True" WindowStyle="SingleBorderWindow" ResizeMode="NoResize" WindowStartupLocation="CenterScreen" - SizeToContent="WidthAndHeight" - Title="Update available"> + SizeToContent="WidthAndHeight" Width="300" + Title="Update available" d:DataContext="{d:DesignData UpdateViewModel}"> @@ -17,14 +19,44 @@ + A new version of Jarvis is available: - + + + + + + + + + + - -