diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index 17c2dc511..ce067e8a0 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -127,5 +127,81 @@ namespace Flow.Launcher.Plugin.SharedCommands #endif } } + + /// + /// This checks whether a given string is a directory path or network location string. + /// It does not check if location actually exists. + /// + public static bool IsLocationPathString(string querySearchString) + { + if (string.IsNullOrEmpty(querySearchString)) + return false; + + // // shared folder location, and not \\\location\ + if (querySearchString.Length >= 3 + && querySearchString.StartsWith(@"\\") + && char.IsLetter(querySearchString[2])) + return true; + + // c:\ + if (querySearchString.Length == 3 + && char.IsLetter(querySearchString[0]) + && querySearchString[1] == ':' + && querySearchString[2] == '\\') + return true; + + // c:\\ + if (querySearchString.Length >= 4 + && char.IsLetter(querySearchString[0]) + && querySearchString[1] == ':' + && querySearchString[2] == '\\' + && char.IsLetter(querySearchString[3])) + return true; + + return false; + } + + /// + /// Gets the previous level directory from a path string. + /// Checks that previous level directory exists and returns it + /// as a path string, or empty string if doesn't exit + /// + public static string GetPreviousExistingDirectory(Func locationExists, string path) + { + var previousDirectoryPath = ""; + var index = path.LastIndexOf('\\'); + if (index > 0 && index < (path.Length - 1)) + { + previousDirectoryPath = path.Substring(0, index + 1); + if (!locationExists(previousDirectoryPath)) + { + return ""; + } + } + else + { + return ""; + } + + return previousDirectoryPath; + } + + /// + /// Returns the previous level directory if path incomplete (does not end with '\'). + /// Does not check if previous level directory exists. + /// Returns passed in string if is complete path + /// + public static string ReturnPreviousDirectoryIfIncompleteString(string path) + { + if (!path.EndsWith("\\")) + { + // not full path, get previous level directory string + var indexOfSeparator = path.LastIndexOf('\\'); + + return path.Substring(0, indexOfSeparator + 1); + } + + return path; + } } } diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj index eb26fccf2..e970c47b9 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -39,6 +39,7 @@ + diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs new file mode 100644 index 000000000..0a1dece4f --- /dev/null +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -0,0 +1,279 @@ +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.Explorer; +using Flow.Launcher.Plugin.Explorer.Search; +using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo; +using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex; +using Flow.Launcher.Plugin.SharedCommands; +using NUnit.Framework; +using System; +using System.Collections.Generic; + +namespace Flow.Launcher.Test.Plugins +{ + /// + /// These tests require the use of CSearchManager class from Microsoft.Search.Interop. + /// Windows Search service needs to be running to complete the tests + /// + [TestFixture] + public class ExplorerTest + { + private List MethodWindowsIndexSearchReturnsZeroResults(Query dummyQuery, string dummyString) + { + return new List(); + } + + private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummyQuery, string dummyString) + { + return new List + { + new Result + { + Title="Result 1" + }, + + new Result + { + Title="Result 2" + } + }; + } + + private bool PreviousLocationExistsReturnsTrue(string dummyString) => true; + + private bool PreviousLocationNotExistReturnsFalse(string dummyString) => false; + + [TestCase("C:\\SomeFolder\\", "directory='file:C:\\SomeFolder\\'")] + public void GivenWindowsIndexSearch_WhenProvidedFolderPath_ThenQueryWhereRestrictionsShouldUseDirectoryString(string path, string expectedString) + { + // Given + var queryConstructor = new QueryConstructor(new Settings()); + + // When + var folderPath = path; + var result = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(folderPath); + + // Then + Assert.IsTrue(result == expectedString, + $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + + $"Actual: {result}{Environment.NewLine}"); + } + + [TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemPathDisplay, System.ItemType FROM SystemIndex WHERE directory='file:C:\\'")] + [TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemPathDisplay, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\'")] + public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString) + { + // Given + var queryConstructor = new QueryConstructor(new Settings()); + + //When + var queryString = queryConstructor.QueryForTopLevelDirectorySearch(folderPath); + + // Then + Assert.IsTrue(queryString == expectedString, + $"Expected string: {expectedString}{Environment.NewLine} " + + $"Actual string was: {queryString}{Environment.NewLine}"); + } + + [TestCase("C:\\SomeFolder\\flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemPathDisplay, System.ItemType " + + "FROM SystemIndex WHERE (System.FileName LIKE 'flow.launcher.sln%' " + + "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033))" + + " AND directory='file:C:\\SomeFolder'")] + public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString( + string userSearchString, string expectedString) + { + // Given + var queryConstructor = new QueryConstructor(new Settings()); + + //When + var queryString = queryConstructor.QueryForTopLevelDirectorySearch(userSearchString); + + // Then + Assert.IsTrue(queryString == expectedString, + $"Expected string: {expectedString}{Environment.NewLine} " + + $"Actual string was: {queryString}{Environment.NewLine}"); + } + + [TestCase("C:\\SomeFolder\\SomeApp", "(System.FileName LIKE 'SomeApp%' " + + "OR CONTAINS(System.FileName,'\"SomeApp*\"',1033))" + + " AND directory='file:C:\\SomeFolder'")] + public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryWhereRestrictionsShouldUseDirectoryString( + string userSearchString, string expectedString) + { + // Given + var queryConstructor = new QueryConstructor(new Settings()); + + //When + var queryString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(userSearchString); + + // Then + Assert.IsTrue(queryString == expectedString, + $"Expected string: {expectedString}{Environment.NewLine} " + + $"Actual string was: {queryString}{Environment.NewLine}"); + } + + [TestCase("scope='file:'")] + public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString) + { + // Given + var queryConstructor = new QueryConstructor(new Settings()); + + //When + var resultString = queryConstructor.QueryWhereRestrictionsForAllFilesAndFoldersSearch(); + + // Then + Assert.IsTrue(resultString == expectedString, + $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + + $"Actual string was: {resultString}{Environment.NewLine}"); + } + + [TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemPathDisplay\", \"System.ItemType\" " + + "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " + + "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:'")] + public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString( + string userSearchString, string expectedString) + { + // Given + var queryConstructor = new QueryConstructor(new Settings()); + + //When + var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString); + + // Then + Assert.IsTrue(resultString == expectedString, + $"Expected query string: {expectedString}{Environment.NewLine} " + + $"Actual string was: {resultString}{Environment.NewLine}"); + } + + [TestCase] + public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldContinueDirectoryInfoClassSearch() + { + // Given + var searchManager = new SearchManager(new Settings(), new PluginInitContext()); + + // When + var results = searchManager.TopLevelDirectorySearchBehaviour( + MethodWindowsIndexSearchReturnsZeroResults, + MethodDirectoryInfoClassSearchReturnsTwoResults, + false, + new Query(), + "string not used"); + + // Then + Assert.IsTrue(results.Count == 2, + $"Expected to have 2 results from DirectoryInfoClassSearch {Environment.NewLine} " + + $"Actual number of results is {results.Count} {Environment.NewLine}"); + } + + [TestCase] + public void GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldNotContinueDirectoryInfoClassSearch() + { + // Given + var searchManager = new SearchManager(new Settings(), new PluginInitContext()); + + // When + var results = searchManager.TopLevelDirectorySearchBehaviour( + MethodWindowsIndexSearchReturnsZeroResults, + MethodDirectoryInfoClassSearchReturnsTwoResults, + true, + new Query(), + "string not used"); + + // Then + Assert.IsTrue(results.Count == 0, + $"Expected to have 0 results because location is indexed {Environment.NewLine} " + + $"Actual number of results is {results.Count} {Environment.NewLine}"); + } + + [TestCase(@"c:\\", false)] + [TestCase(@"i:\", true)] + [TestCase(@"\c:\", false)] + [TestCase(@"cc:\", false)] + [TestCase(@"\\\SomeNetworkLocation\", false)] + [TestCase("RandomFile", false)] + public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString(string querySearchString, bool expectedResult) + { + // When, Given + var result = FilesFolders.IsLocationPathString(querySearchString); + + //Then + Assert.IsTrue(result == expectedResult, + $"Expected query search string check result is: {expectedResult} {Environment.NewLine} " + + $"Actual check result is {result} {Environment.NewLine}"); + + } + + [TestCase(@"C:\SomeFolder\SomeApp", true, @"C:\SomeFolder\")] + [TestCase(@"C:\SomeFolder\SomeApp\SomeFile", true, @"C:\SomeFolder\SomeApp\")] + [TestCase(@"C:\NonExistentFolder\SomeApp", false, "")] + public void GivenAPartialPath_WhenPreviousLevelDirectoryExists_ThenShouldReturnThePreviousDirectoryPathString( + string path, bool previousDirectoryExists, string expectedString) + { + // When + Func previousLocationExists = null; + if (previousDirectoryExists) + { + previousLocationExists = PreviousLocationExistsReturnsTrue; + } + else + { + previousLocationExists = PreviousLocationNotExistReturnsFalse; + } + + // Given + var previousDirectoryPath = FilesFolders.GetPreviousExistingDirectory(previousLocationExists, path); + + //Then + Assert.IsTrue(previousDirectoryPath == expectedString, + $"Expected path string: {expectedString} {Environment.NewLine} " + + $"Actual path string is {previousDirectoryPath} {Environment.NewLine}"); + } + + [TestCase(@"C:\NonExistentFolder\SomeApp", @"C:\NonExistentFolder\")] + [TestCase(@"C:\NonExistentFolder\SomeApp\", @"C:\NonExistentFolder\SomeApp\")] + public void WhenGivenAPath_ThenShouldReturnThePreviousDirectoryPathIfIncompleteOrOriginalString( + string path, string expectedString) + { + var returnedPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path); + + //Then + Assert.IsTrue(returnedPath == expectedString, + $"Expected path string: {expectedString} {Environment.NewLine} " + + $"Actual path string is {returnedPath} {Environment.NewLine}"); + } + + [TestCase("c:\\SomeFolder\\>", "scope='file:c:\\SomeFolder'")] + [TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' " + + "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND " + + "scope='file:c:\\SomeFolder'")] + public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString) + { + // Given + var queryConstructor = new QueryConstructor(new Settings()); + + //When + var resultString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path); + + // Then + Assert.IsTrue(resultString == expectedString, + $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + + $"Actual string was: {resultString}{Environment.NewLine}"); + } + + [TestCase("c:\\somefolder\\>somefile","*somefile*")] + [TestCase("c:\\somefolder\\somefile", "somefile*")] + [TestCase("c:\\somefolder\\", "*")] + public void GivenDirectoryInfoSearch_WhenSearchPatternHotKeyIsSearchAll_ThenSearchCriteriaShouldUseCriteriaString(string path, string expectedString) + { + // Given + var criteriaConstructor = new DirectoryInfoSearch(new PluginInitContext()); + + //When + var resultString = criteriaConstructor.ConstructSearchCriteria(path); + + // Then + Assert.IsTrue(resultString == expectedString, + $"Expected criteria string: {expectedString}{Environment.NewLine} " + + $"Actual criteria string was: {resultString}{Environment.NewLine}"); + } + } +} diff --git a/Flow.Launcher.sln b/Flow.Launcher.sln index f668b2764..b244c97de 100644 --- a/Flow.Launcher.sln +++ b/Flow.Launcher.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29806.167 @@ -18,9 +17,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher", "Flow.Launc {1EE20B48-82FB-48A2-8086-675D6DDAB4F0} = {1EE20B48-82FB-48A2-8086-675D6DDAB4F0} {0B9DE348-9361-4940-ADB6-F5953BFFCCEC} = {0B9DE348-9361-4940-ADB6-F5953BFFCCEC} {FDB3555B-58EF-4AE6-B5F1-904719637AB4} = {FDB3555B-58EF-4AE6-B5F1-904719637AB4} + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F} = {F9C4C081-4CC3-4146-95F1-E102B4E10A5F} {59BD9891-3837-438A-958D-ADC7F91F6F7E} = {59BD9891-3837-438A-958D-ADC7F91F6F7E} {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0} = {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0} - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4} = {787B8AA6-CA93-4C84-96FE-DF31110AD1C4} {F35190AA-4758-4D9E-A193-E3BDF6AD3567} = {F35190AA-4758-4D9E-A193-E3BDF6AD3567} {9B130CC5-14FB-41FF-B310-0A95B6894C37} = {9B130CC5-14FB-41FF-B310-0A95B6894C37} {FDED22C8-B637-42E8-824A-63B5B6E05A3A} = {FDED22C8-B637-42E8-824A-63B5B6E05A3A} @@ -41,8 +40,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.WebSea EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.ControlPanel", "Plugins\Flow.Launcher.Plugin.ControlPanel\Flow.Launcher.Plugin.ControlPanel.csproj", "{1EE20B48-82FB-48A2-8086-675D6DDAB4F0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Folder", "Plugins\Flow.Launcher.Plugin.Folder\Flow.Launcher.Plugin.Folder.csproj", "{787B8AA6-CA93-4C84-96FE-DF31110AD1C4}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.PluginIndicator", "Plugins\Flow.Launcher.Plugin.PluginIndicator\Flow.Launcher.Plugin.PluginIndicator.csproj", "{FDED22C8-B637-42E8-824A-63B5B6E05A3A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Sys", "Plugins\Flow.Launcher.Plugin.Sys\Flow.Launcher.Plugin.Sys.csproj", "{0B9DE348-9361-4940-ADB6-F5953BFFCCEC}" @@ -58,11 +55,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .gitattributes = .gitattributes .gitignore = .gitignore appveyor.yml = appveyor.yml + Scripts\flowlauncher.nuspec = Scripts\flowlauncher.nuspec LICENSE = LICENSE Scripts\post_build.ps1 = Scripts\post_build.ps1 README.md = README.md SolutionAssemblyInfo.cs = SolutionAssemblyInfo.cs - Scripts\flowlauncher.nuspec = Scripts\flowlauncher.nuspec EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloWorldCSharp", "Plugins\HelloWorldCSharp\HelloWorldCSharp.csproj", "{03FFA443-5F50-48D5-8869-F3DF316803AA}" @@ -75,6 +72,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Calcul EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "HelloWorldFSharp", "Plugins\HelloWorldFSharp\HelloWorldFSharp.fsproj", "{30DDA7D9-3712-44F4-BD18-DC1C05B2DD9E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Explorer", "Plugins\Flow.Launcher.Plugin.Explorer\Flow.Launcher.Plugin.Explorer.csproj", "{F9C4C081-4CC3-4146-95F1-E102B4E10A5F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -193,18 +192,6 @@ Global {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Release|x64.Build.0 = Release|Any CPU {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Release|x86.ActiveCfg = Release|Any CPU {1EE20B48-82FB-48A2-8086-675D6DDAB4F0}.Release|x86.Build.0 = Release|Any CPU - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|x64.ActiveCfg = Debug|Any CPU - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|x64.Build.0 = Debug|Any CPU - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|x86.ActiveCfg = Debug|Any CPU - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|x86.Build.0 = Debug|Any CPU - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|Any CPU.Build.0 = Release|Any CPU - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|x64.ActiveCfg = Release|Any CPU - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|x64.Build.0 = Release|Any CPU - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|x86.ActiveCfg = Release|Any CPU - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|x86.Build.0 = Release|Any CPU {FDED22C8-B637-42E8-824A-63B5B6E05A3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FDED22C8-B637-42E8-824A-63B5B6E05A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU {FDED22C8-B637-42E8-824A-63B5B6E05A3A}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -326,6 +313,18 @@ Global {30DDA7D9-3712-44F4-BD18-DC1C05B2DD9E}.Release|x64.Build.0 = Release|Any CPU {30DDA7D9-3712-44F4-BD18-DC1C05B2DD9E}.Release|x86.ActiveCfg = Release|Any CPU {30DDA7D9-3712-44F4-BD18-DC1C05B2DD9E}.Release|x86.Build.0 = Release|Any CPU + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Debug|x64.ActiveCfg = Debug|Any CPU + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Debug|x64.Build.0 = Debug|Any CPU + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Debug|x86.ActiveCfg = Debug|Any CPU + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Debug|x86.Build.0 = Debug|Any CPU + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Release|Any CPU.Build.0 = Release|Any CPU + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Release|x64.ActiveCfg = Release|Any CPU + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Release|x64.Build.0 = Release|Any CPU + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Release|x86.ActiveCfg = Release|Any CPU + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -335,7 +334,6 @@ Global {FDB3555B-58EF-4AE6-B5F1-904719637AB4} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {403B57F2-1856-4FC7-8A24-36AB346B763E} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {1EE20B48-82FB-48A2-8086-675D6DDAB4F0} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {FDED22C8-B637-42E8-824A-63B5B6E05A3A} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {0B9DE348-9361-4940-ADB6-F5953BFFCCEC} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {A3DCCBCA-ACC1-421D-B16E-210896234C26} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} @@ -346,6 +344,7 @@ Global {9B130CC5-14FB-41FF-B310-0A95B6894C37} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {59BD9891-3837-438A-958D-ADC7F91F6F7E} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} {30DDA7D9-3712-44F4-BD18-DC1C05B2DD9E} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} + {F9C4C081-4CC3-4146-95F1-E102B4E10A5F} = {3A73F5A7-0335-40D8-BF7C-F20BE5D0BA87} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F26ACB50-3F6C-4907-B0C9-1ADACC1D0DED} diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 6fe1b817d..987a685ac 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -83,9 +83,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs new file mode 100644 index 000000000..90a3fb2e8 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using System.Windows; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Plugin.SharedCommands; +using Flow.Launcher.Plugin.Explorer.Search; +using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; +using System.Linq; +using System.Reflection; + +namespace Flow.Launcher.Plugin.Explorer +{ + internal class ContextMenu : IContextMenu + { + private PluginInitContext Context { get; set; } + + private Settings Settings { get; set; } + + public ContextMenu(PluginInitContext context, Settings settings) + { + Context = context; + Settings = settings; + } + + public List LoadContextMenus(Result selectedResult) + { + var contextMenus = new List(); + if (selectedResult.ContextData is SearchResult record) + { + if (record.Type == ResultType.File) + contextMenus.Add(CreateOpenWithEditorResult(record)); + + if (record.Type == ResultType.Folder && record.WindowsIndexed) + contextMenus.Add(CreateAddToIndexSearchExclusionListResult(record)); + + contextMenus.Add(CreateOpenContainingFolderResult(record)); + + contextMenus.Add(CreateOpenWindowsIndexingOptions()); + + if (record.ShowIndexState) + contextMenus.Add(new Result {Title = "From index search: " + (record.WindowsIndexed ? "Yes" : "No"), + SubTitle = "Location: " + record.FullPath, + Score = 501, IcoPath = Constants.IndexImagePath}); + + var icoPath = (record.Type == ResultType.File) ? Constants.FileImagePath : Constants.FolderImagePath; + var fileOrFolder = (record.Type == ResultType.File) ? "file" : "folder"; + contextMenus.Add(new Result + { + Title = Context.API.GetTranslation("plugin_explorer_copypath"), + SubTitle = $"Copy the current {fileOrFolder} path to clipboard", + Action = (context) => + { + try + { + Clipboard.SetText(record.FullPath); + return true; + } + catch (Exception e) + { + var message = "Fail to set text in clipboard"; + LogException(message, e); + Context.API.ShowMsg(message); + return false; + } + }, + IcoPath = Constants.CopyImagePath + }); + + contextMenus.Add(new Result + { + Title = Context.API.GetTranslation("plugin_explorer_copyfilefolder") + $" {fileOrFolder}", + SubTitle = $"Copy the {fileOrFolder} to clipboard", + Action = (context) => + { + try + { + Clipboard.SetFileDropList(new System.Collections.Specialized.StringCollection { record.FullPath }); + return true; + } + catch (Exception e) + { + var message = $"Fail to set {fileOrFolder} in clipboard"; + LogException(message, e); + Context.API.ShowMsg(message); + return false; + } + + }, + IcoPath = icoPath + }); + + if (record.Type == ResultType.File || record.Type == ResultType.Folder) + contextMenus.Add(new Result + { + Title = Context.API.GetTranslation("plugin_explorer_deletefilefolder") + $" {fileOrFolder}", + SubTitle = Context.API.GetTranslation("plugin_explorer_deletefilefolder_subtitle") + $" {fileOrFolder}", + Action = (context) => + { + try + { + if (record.Type == ResultType.File) + File.Delete(record.FullPath); + else + Directory.Delete(record.FullPath, true); + } + catch (Exception e) + { + var message = $"Fail to delete {fileOrFolder} at {record.FullPath}"; + LogException(message, e); + Context.API.ShowMsg(message); + return false; + } + + return true; + }, + IcoPath = Constants.DeleteFileFolderImagePath + }); + + if (record.Type == ResultType.File && CanRunAsDifferentUser(record.FullPath)) + contextMenus.Add(new Result + { + Title = Context.API.GetTranslation("plugin_explorer_runasdifferentuser"), + SubTitle = Context.API.GetTranslation("plugin_explorer_runasdifferentuser_subtitle"), + Action = (context) => + { + try + { + Task.Run(() => ShellCommand.RunAsDifferentUser(record.FullPath.SetProcessStartInfo())); + } + catch (FileNotFoundException e) + { + var name = "Plugin: Folder"; + var message = $"File not found: {e.Message}"; + Context.API.ShowMsg(name, message); + } + + return true; + }, + IcoPath = Constants.DifferentUserIconImagePath + }); + } + + return contextMenus; + } + + private Result CreateOpenContainingFolderResult(SearchResult record) + { + return new Result + { + Title = Context.API.GetTranslation("plugin_explorer_opencontainingfolder"), + SubTitle = Context.API.GetTranslation("plugin_explorer_opencontainingfolder_subtitle"), + Action = _ => + { + try + { + Process.Start("explorer.exe", $" /select,\"{record.FullPath}\""); + } + catch (Exception e) + { + var message = $"Fail to open file at {record.FullPath}"; + LogException(message, e); + Context.API.ShowMsg(message); + return false; + } + + return true; + }, + IcoPath = Constants.FolderImagePath + }; + } + + private Result CreateOpenWithEditorResult(SearchResult record) + { + string editorPath = "Notepad.exe"; // TODO add the ability to create a custom editor + + var name = Context.API.GetTranslation("plugin_explorer_openwitheditor") + + " " + Path.GetFileNameWithoutExtension(editorPath); + + return new Result + { + Title = name, + Action = _ => + { + try + { + Process.Start(editorPath, record.FullPath); + return true; + } + catch (Exception e) + { + var message = $"Failed to open editor for file at {record.FullPath}"; + LogException(message, e); + Context.API.ShowMsg(message); + return false; + } + }, + IcoPath = Constants.FileImagePath + }; + } + + private Result CreateAddToIndexSearchExclusionListResult(SearchResult record) + { + return new Result + { + Title = Context.API.GetTranslation("plugin_explorer_excludefromindexsearch"), + SubTitle = Context.API.GetTranslation("plugin_explorer_path") + " " + record.FullPath, + Action = _ => + { + if(!Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == record.FullPath)) + Settings.IndexSearchExcludedSubdirectoryPaths.Add(new FolderLink { Path = record.FullPath }); + + var pluginDirectory = Directory.GetParent(Assembly.GetExecutingAssembly().Location.ToString()); + + var iconPath = pluginDirectory + "\\" + Constants.ExplorerIconImagePath; + + Task.Run(() => + { + Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_excludedfromindexsearch_msg"), + Context.API.GetTranslation("plugin_explorer_path") + + " " + record.FullPath, iconPath); + + // so the new path can be persisted to storage and not wait till next ViewModel save. + Context.API.SaveAppAllSettings(); + }); + + return false; + }, + IcoPath = Constants.ExcludeFromIndexImagePath + }; + } + + private Result CreateOpenWindowsIndexingOptions() + { + return new Result + { + Title = Context.API.GetTranslation("plugin_explorer_openindexingoptions"), + SubTitle = Context.API.GetTranslation("plugin_explorer_openindexingoptions_subtitle"), + Action = _ => + { + try + { + var psi = new ProcessStartInfo + { + FileName = "control.exe", + UseShellExecute = true, + Arguments = "srchadmin.dll" + }; + + Process.Start(psi); + return true; + } + catch (Exception e) + { + var message = Context.API.GetTranslation("plugin_explorer_openindexingoptions_errormsg"); + LogException(message, e); + Context.API.ShowMsg(message); + return false; + } + }, + IcoPath = Constants.IndexingOptionsIconImagePath + }; + } + + public void LogException(string message, Exception e) + { + Log.Exception($"|Flow.Launcher.Plugin.Folder.ContextMenu|{message}", e); + } + + private bool CanRunAsDifferentUser(string path) + { + switch (Path.GetExtension(path)) + { + case ".exe": + case ".bat": + case ".msi": + return true; + + default: + return false; + + } + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Flow.Launcher.Plugin.Folder.csproj b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj similarity index 64% rename from Plugins/Flow.Launcher.Plugin.Folder/Flow.Launcher.Plugin.Folder.csproj rename to Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj index a05b5d49d..bd2a047eb 100644 --- a/Plugins/Flow.Launcher.Plugin.Folder/Flow.Launcher.Plugin.Folder.csproj +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj @@ -1,37 +1,23 @@  + Library netcoreapp3.1 - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4} - Properties - Flow.Launcher.Plugin.Folder - Flow.Launcher.Plugin.Folder + true true false - false + + - - - true - full - false - ..\..\Output\Debug\Plugins\Flow.Launcher.Plugin.Folder\ - DEBUG;TRACE - prompt - 4 - false + + + ..\..\Output\Debug\Plugins\Flow.Launcher.Plugin.Explorer - - - pdbonly - true - ..\..\Output\Release\Plugins\Flow.Launcher.Plugin.Folder\ - TRACE - prompt - 4 - false + + + ..\..\Output\Release\Plugins\Flow.Launcher.Plugin.Explorer - + PreserveNewest @@ -39,69 +25,87 @@ - - MSBuild:Compile - Designer - + + PreserveNewest + + + + PreserveNewest + + + + PreserveNewest + + PreserveNewest + Always + PreserveNewest - - Always + + + PreserveNewest + + + PreserveNewest + + + + PreserveNewest + + MSBuild:Compile Designer PreserveNewest + MSBuild:Compile Designer PreserveNewest + MSBuild:Compile Designer PreserveNewest + MSBuild:Compile Designer PreserveNewest + MSBuild:Compile Designer PreserveNewest + MSBuild:Compile Designer PreserveNewest + + + + + - - - - PreserveNewest - - - - - - - - \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Images/copy.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/copy.png similarity index 100% rename from Plugins/Flow.Launcher.Plugin.Folder/Images/copy.png rename to Plugins/Flow.Launcher.Plugin.Explorer/Images/copy.png diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Images/deletefilefolder.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/deletefilefolder.png similarity index 100% rename from Plugins/Flow.Launcher.Plugin.Folder/Images/deletefilefolder.png rename to Plugins/Flow.Launcher.Plugin.Explorer/Images/deletefilefolder.png diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/excludeindexpath.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/excludeindexpath.png new file mode 100644 index 000000000..8578ac7fb Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/excludeindexpath.png differ diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png new file mode 100644 index 000000000..552b2c5bd Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/explorer.png differ diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Images/file.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/file.png similarity index 100% rename from Plugins/Flow.Launcher.Plugin.Folder/Images/file.png rename to Plugins/Flow.Launcher.Plugin.Explorer/Images/file.png diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Images/folder.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/folder.png similarity index 100% rename from Plugins/Flow.Launcher.Plugin.Folder/Images/folder.png rename to Plugins/Flow.Launcher.Plugin.Explorer/Images/folder.png diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/index.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/index.png new file mode 100644 index 000000000..c63fc8069 Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/index.png differ diff --git a/Plugins/Flow.Launcher.Plugin.Folder/Images/user.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/user.png similarity index 100% rename from Plugins/Flow.Launcher.Plugin.Folder/Images/user.png rename to Plugins/Flow.Launcher.Plugin.Explorer/Images/user.png diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/windowsindexingoptions.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/windowsindexingoptions.png new file mode 100644 index 000000000..78a015715 Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/windowsindexingoptions.png differ diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml new file mode 100644 index 000000000..2209b5bed --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/de.xaml @@ -0,0 +1,14 @@ + + + + Löschen + Bearbeiten + Hinzufügen + + + Bitte wähle eine Ordnerverknüpfung + Bist du sicher {0} zu löschen? + + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml new file mode 100644 index 000000000..bc3f5664a --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml @@ -0,0 +1,39 @@ + + + + Please select a folder link + Are you sure you want to delete {0}? + + + Delete + Edit + Add + Quick Folder Access Paths + Index Search Excluded Paths + Indexing Options + + + + Explorer + Search and manage files and folders. Explorer utilises Windows Index Search + + + Copy path + Copy + Delete + Path: + Delete the selected + Run as different user + Run the selected using a different user account + Open containing folder + Opens the location that contains the file or folder + Open With Editor: + Exclude current and sub-directories from Index Search + Excluded from Index Search + Open Windows Indexing Options + Manage indexed files and folders + Failed to open Windows Indexing Options + + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml new file mode 100644 index 000000000..9a6b38f6f --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/pl.xaml @@ -0,0 +1,14 @@ + + + + Usuń + Edytuj + Dodaj + + + Musisz wybrać któryś folder z listy + Czy jesteś pewien że chcesz usunąć {0}? + + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml new file mode 100644 index 000000000..d6744fdd4 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/tr.xaml @@ -0,0 +1,14 @@ + + + + Sil + Düzenle + Ekle + + + Lütfen bir klasör bağlantısı seçin + {0} bağlantısını silmek istediğinize emin misiniz? + + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml new file mode 100644 index 000000000..7f516861e --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-cn.xaml @@ -0,0 +1,14 @@ + + + + 删除 + 编辑 + 添加 + + + 请选择一个文件夹 + 你确定要删除{0}吗? + + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml new file mode 100644 index 000000000..1455aa676 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/zh-tw.xaml @@ -0,0 +1,14 @@ + + + + 刪除 + 編輯 + 新增 + + + 請選擇一個資料夾 + 你確認要刪除{0}嗎? + + diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs new file mode 100644 index 000000000..30a06e882 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -0,0 +1,58 @@ +using Flow.Launcher.Infrastructure.Storage; +using Flow.Launcher.Plugin.Explorer.Search; +using Flow.Launcher.Plugin.Explorer.ViewModels; +using Flow.Launcher.Plugin.Explorer.Views; +using System.Collections.Generic; +using System.Windows.Controls; + +namespace Flow.Launcher.Plugin.Explorer +{ + public class Main : ISettingProvider, IPlugin, ISavable, IContextMenu, IPluginI18n + { + internal PluginInitContext Context { get; set; } + + internal Settings Settings; + + private SettingsViewModel viewModel; + + private IContextMenu contextMenu; + + public Control CreateSettingPanel() + { + return new ExplorerSettings(viewModel); + } + + public void Init(PluginInitContext context) + { + Context = context; + viewModel = new SettingsViewModel(context); + Settings = viewModel.Settings; + contextMenu = new ContextMenu(Context, Settings); + } + + public List LoadContextMenus(Result selectedResult) + { + return contextMenu.LoadContextMenus(selectedResult); + } + + public List Query(Query query) + { + return new SearchManager(Settings, Context).Search(query); + } + + public void Save() + { + viewModel.Save(); + } + + public string GetTranslatedPluginTitle() + { + return Context.API.GetTranslation("plugin_explorer_plugin_name"); + } + + public string GetTranslatedPluginDescription() + { + return Context.API.GetTranslation("plugin_explorer_plugin_description"); + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs new file mode 100644 index 000000000..9c8a300bc --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Flow.Launcher.Plugin.Explorer.Search +{ + internal static class Constants + { + internal const string FolderImagePath = "Images\\folder.png"; + internal const string FileImagePath = "Images\\file.png"; + internal const string DeleteFileFolderImagePath = "Images\\deletefilefolder.png"; + internal const string CopyImagePath = "Images\\copy.png"; + internal const string IndexImagePath = "Images\\index.png"; + internal const string ExcludeFromIndexImagePath = "Images\\excludeindexpath.png"; + internal const string ExplorerIconImagePath = "Images\\explorer.png"; + internal const string DifferentUserIconImagePath = "Images\\user.png"; + internal const string IndexingOptionsIconImagePath = "Images\\windowsindexingoptions.png"; + + internal const string DefaultFolderSubtitleString = "Ctrl + Enter to open the directory"; + + internal const char AllFilesFolderSearchWildcard = '>'; + + internal const char DirectorySeperator = '\\'; + + internal const string WindowsIndexingOptions = "srchadmin.dll"; + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs new file mode 100644 index 000000000..b198395b8 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs @@ -0,0 +1,96 @@ +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Plugin.SharedCommands; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo +{ + public class DirectoryInfoSearch + { + private readonly ResultManager resultManager; + + public DirectoryInfoSearch(PluginInitContext context) + { + resultManager = new ResultManager(context); + } + + internal List TopLevelDirectorySearch(Query query, string search) + { + var criteria = ConstructSearchCriteria(search); + + if (search.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > search.LastIndexOf(Constants.DirectorySeperator)) + return DirectorySearch(SearchOption.AllDirectories, query, search, criteria); + + return DirectorySearch(SearchOption.TopDirectoryOnly, query, search, criteria); + } + + public string ConstructSearchCriteria(string search) + { + string incompleteName = ""; + + if (!search.EndsWith(Constants.DirectorySeperator)) + { + var indexOfSeparator = search.LastIndexOf(Constants.DirectorySeperator); + + incompleteName = search.Substring(indexOfSeparator + 1).ToLower(); + + if (incompleteName.StartsWith(Constants.AllFilesFolderSearchWildcard)) + incompleteName = "*" + incompleteName.Substring(1); + } + + incompleteName += "*"; + + return incompleteName; + } + + private List DirectorySearch(SearchOption searchOption, Query query, string search, string searchCriteria) + { + var results = new List(); + + var path = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(search); + + var folderList = new List(); + var fileList = new List(); + + try + { + var directoryInfo = new System.IO.DirectoryInfo(path); + var fileSystemInfos = directoryInfo.GetFileSystemInfos(searchCriteria, searchOption); + + foreach (var fileSystemInfo in fileSystemInfos) + { + if ((fileSystemInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) continue; + + if (fileSystemInfo is System.IO.DirectoryInfo) + { + folderList.Add(resultManager.CreateFolderResult(fileSystemInfo.Name, Constants.DefaultFolderSubtitleString, fileSystemInfo.FullName, query, true, false)); + } + else + { + fileList.Add(resultManager.CreateFileResult(fileSystemInfo.FullName, query, true, false)); + } + } + } + catch (Exception e) + { + if (e is UnauthorizedAccessException || e is ArgumentException) + { + results.Add(new Result { Title = e.Message, Score = 501 }); + + return results; + } + +#if DEBUG // Please investigate and handle error from DirectoryInfo search + throw e; +#else + Log.Exception($"|Flow.Launcher.Plugin.Explorer.DirectoryInfoSearch|Error from performing DirectoryInfoSearch", e); +#endif + } + + // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection. + return results.Concat(folderList.OrderBy(x => x.Title)).Concat(fileList.OrderBy(x => x.Title)).ToList(); + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs new file mode 100644 index 000000000..5ba76bd7d --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/EnvironmentVariables.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Flow.Launcher.Plugin.Explorer.Search +{ + public static class EnvironmentVariables + { + internal static bool IsEnvironmentVariableSearch(string search) + { + return LoadEnvironmentStringPaths().Count > 0 && search.StartsWith("%") && !search.Substring(1).Contains("%"); + } + + internal static Dictionary LoadEnvironmentStringPaths() + { + var envStringPaths = new Dictionary(); + + foreach (DictionaryEntry special in Environment.GetEnvironmentVariables()) + { + if (Directory.Exists(special.Value.ToString())) + { + envStringPaths.Add(special.Key.ToString().ToLower(), special.Value.ToString()); + } + } + + return envStringPaths; + } + + internal static string TranslateEnvironmentVariablePath(string environmentVariablePath) + { + var envStringPaths = LoadEnvironmentStringPaths(); + var splitSearch = environmentVariablePath.Substring(1).Split("%"); + var exactEnvStringPath = splitSearch[0]; + + // if there are more than 2 % characters in the query, don't bother + if (splitSearch.Length == 2 && envStringPaths.ContainsKey(exactEnvStringPath)) + { + var queryPartToReplace = $"%{exactEnvStringPath}%"; + var expandedPath = envStringPaths[exactEnvStringPath]; + // replace the %envstring% part of the query with its expanded equivalent + return environmentVariablePath.Replace(queryPartToReplace, expandedPath); + } + + return environmentVariablePath; + } + + internal static List GetEnvironmentStringPathSuggestions(string querySearch, Query query, PluginInitContext context) + { + var results = new List(); + + var environmentVariables = LoadEnvironmentStringPaths(); + var search = querySearch; + + if (querySearch.EndsWith("%") && search.Length > 1) + { + // query starts and ends with a %, find an exact match from env-string paths + search = querySearch.Substring(1, search.Length - 2); + + if (environmentVariables.ContainsKey(search)) + { + var expandedPath = environmentVariables[search]; + + results.Add(new ResultManager(context).CreateFolderResult($"%{search}%", expandedPath, expandedPath, query)); + + return results; + } + } + + if (querySearch == "%") + { + search = ""; // Get all paths + } + else + { + search = search.Substring(1); + } + + foreach (var p in environmentVariables) + { + if (p.Key.StartsWith(search)) + { + results.Add(new ResultManager(context).CreateFolderResult($"%{p.Key}%", p.Value, p.Value, query)); + } + } + return results; + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs new file mode 100644 index 000000000..379b5848f --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/FolderLink.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; +using System; +using System.Linq; + +namespace Flow.Launcher.Plugin.Explorer.Search.FolderLinks +{ + [JsonObject(MemberSerialization.OptIn)] + public class FolderLink + { + [JsonProperty] + public string Path { get; set; } + + public string Nickname + { + get + { + var path = Path.EndsWith(Constants.DirectorySeperator) ? Path[0..^1] : Path; + + if (path.EndsWith(':')) + return path[0..^1] + " Drive"; + + return path.Split(new[] { System.IO.Path.DirectorySeparatorChar }, StringSplitOptions.None) + .Last() + + " (" + System.IO.Path.GetDirectoryName(Path) + ")"; + } + } + } + +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs new file mode 100644 index 000000000..689c865c0 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/FolderLinks/QuickFolderAccess.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Flow.Launcher.Plugin.Explorer.Search.FolderLinks +{ + public class QuickFolderAccess + { + internal List FolderList(Query query, List folderLinks, PluginInitContext context) + { + if (string.IsNullOrEmpty(query.Search)) + return folderLinks + .Select(item => + new ResultManager(context) + .CreateFolderResult(item.Nickname, Constants.DefaultFolderSubtitleString, item.Path, query)) + .ToList(); + + string search = query.Search.ToLower(); + + var queriedFolderLinks = folderLinks.Where(x => x.Nickname.StartsWith(search, StringComparison.OrdinalIgnoreCase)); + + return queriedFolderLinks.Select(item => + new ResultManager(context) + .CreateFolderResult(item.Nickname, Constants.DefaultFolderSubtitleString, item.Path, query)) + .ToList(); + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs new file mode 100644 index 000000000..e2fd1c7da --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -0,0 +1,135 @@ +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Plugin.SharedCommands; +using System; +using System.IO; +using System.Linq; +using System.Windows; + +namespace Flow.Launcher.Plugin.Explorer.Search +{ + public class ResultManager + { + private readonly PluginInitContext context; + + public ResultManager(PluginInitContext context) + { + this.context = context; + } + internal Result CreateFolderResult(string title, string subtitle, string path, Query query, bool showIndexState = false, bool windowsIndexed = false) + { + return new Result + { + Title = title, + IcoPath = path, + SubTitle = subtitle, + TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData, + Action = c => + { + if (c.SpecialKeyState.CtrlPressed) + { + try + { + FilesFolders.OpenPath(path); + return true; + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Could not start " + path); + return false; + } + } + + string changeTo = path.EndsWith(Constants.DirectorySeperator) ? path : path + Constants.DirectorySeperator; + context.API.ChangeQuery(string.IsNullOrEmpty(query.ActionKeyword) ? + changeTo : + query.ActionKeyword + " " + changeTo); + return false; + }, + ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, ShowIndexState = showIndexState, WindowsIndexed = windowsIndexed } + }; + } + + internal Result CreateOpenCurrentFolderResult(string path, bool windowsIndexed = false) + { + var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path); + + var folderName = retrievedDirectoryPath.TrimEnd(Constants.DirectorySeperator).Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None).Last(); + + if (retrievedDirectoryPath.EndsWith(":\\")) + { + var driveLetter = path.Substring(0, 1).ToUpper(); + folderName = driveLetter + " drive"; + } + + var title = "Open current directory"; + + if (retrievedDirectoryPath != path) + title = "Open " + folderName; + + + var subtitleFolderName = folderName; + + // ie. max characters can be displayed without subtitle cutting off: "Program Files (x86)" + if (folderName.Length > 19) + subtitleFolderName = "the directory"; + + return new Result + { + Title = title, + SubTitle = $"Use > to search within {subtitleFolderName}, " + + $"* to search for file extensions or >* to combine both searches.", + IcoPath = retrievedDirectoryPath, + Score = 500, + Action = c => + { + FilesFolders.OpenPath(retrievedDirectoryPath); + return true; + }, + ContextData = new SearchResult { Type = ResultType.Folder, FullPath = retrievedDirectoryPath, ShowIndexState = true, WindowsIndexed = windowsIndexed } + }; + } + + internal Result CreateFileResult(string filePath, Query query, bool showIndexState = false, bool windowsIndexed = false) + { + var result = new Result + { + Title = Path.GetFileName(filePath), + SubTitle = filePath, + IcoPath = filePath, + TitleHighlightData = StringMatcher.FuzzySearch(query.Search, Path.GetFileName(filePath)).MatchData, + Action = c => + { + try + { + FilesFolders.OpenPath(filePath); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Could not start " + filePath); + } + + return true; + }, + ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, ShowIndexState = showIndexState, WindowsIndexed = windowsIndexed } + }; + return result; + } + } + + internal class SearchResult + { + public string FullPath { get; set; } + public ResultType Type { get; set; } + + public bool WindowsIndexed { get; set; } + + public bool ShowIndexState { get; set; } + } + + internal enum ResultType + { + Volume, + Folder, + File + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs new file mode 100644 index 000000000..4d3f11967 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -0,0 +1,126 @@ +using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo; +using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; +using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex; +using Flow.Launcher.Plugin.SharedCommands; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Flow.Launcher.Plugin.Explorer.Search +{ + public class SearchManager + { + private readonly PluginInitContext context; + + private readonly IndexSearch indexSearch; + + private readonly QuickFolderAccess quickFolderAccess = new QuickFolderAccess(); + + private readonly ResultManager resultManager; + + private readonly Settings settings; + + public SearchManager(Settings settings, PluginInitContext context) + { + this.context = context; + indexSearch = new IndexSearch(context); + resultManager = new ResultManager(context); + this.settings = settings; + } + + internal List Search(Query query) + { + var results = new List(); + + var querySearch = query.Search; + + var quickFolderLinks = quickFolderAccess.FolderList(query, settings.QuickFolderAccessLinks, context); + + if (quickFolderLinks.Count > 0) + return quickFolderLinks; + + if (string.IsNullOrEmpty(query.Search)) + return results; + + if (!FilesFolders.IsLocationPathString(querySearch)) + return WindowsIndexFilesAndFoldersSearch(query, querySearch); + + var locationPath = query.Search; + + if (EnvironmentVariables.IsEnvironmentVariableSearch(locationPath)) + return EnvironmentVariables.GetEnvironmentStringPathSuggestions(locationPath, query, context); + + // Query is a location path with a full environment variable, eg. %appdata%\somefolder\ + if (locationPath.Substring(1).Contains("%")) + locationPath = EnvironmentVariables.TranslateEnvironmentVariablePath(locationPath); + + if (!FilesFolders.LocationExists(FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath))) + return results; + + var useIndexSearch = UseWindowsIndexForDirectorySearch(locationPath); + + results.Add(resultManager.CreateOpenCurrentFolderResult(locationPath, useIndexSearch)); + + results.AddRange(TopLevelDirectorySearchBehaviour(WindowsIndexTopLevelFolderSearch, + DirectoryInfoClassSearch, + useIndexSearch, + query, + locationPath)); + + return results; + } + + private List DirectoryInfoClassSearch(Query query, string querySearch) + { + var directoryInfoSearch = new DirectoryInfoSearch(context); + + return directoryInfoSearch.TopLevelDirectorySearch(query, querySearch); + } + + public List TopLevelDirectorySearchBehaviour( + Func> windowsIndexSearch, + Func> directoryInfoClassSearch, + bool useIndexSearch, + Query query, + string querySearchString) + { + if (!useIndexSearch) + return directoryInfoClassSearch(query, querySearchString); + + return windowsIndexSearch(query, querySearchString); + } + + private List WindowsIndexFilesAndFoldersSearch(Query query, string querySearchString) + { + var queryConstructor = new QueryConstructor(settings); + + return indexSearch.WindowsIndexSearch(querySearchString, + queryConstructor.CreateQueryHelper().ConnectionString, + queryConstructor.QueryForAllFilesAndFolders, + query); + } + + private List WindowsIndexTopLevelFolderSearch(Query query, string path) + { + var queryConstructor = new QueryConstructor(settings); + + return indexSearch.WindowsIndexSearch(path, + queryConstructor.CreateQueryHelper().ConnectionString, + queryConstructor.QueryForTopLevelDirectorySearch, + query); + } + + private bool UseWindowsIndexForDirectorySearch(string locationPath) + { + if (!settings.UseWindowsIndexForDirectorySearch) + return false; + + if (settings.IndexSearchExcludedSubdirectoryPaths + .Any(x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath) + .StartsWith(x.Path, StringComparison.OrdinalIgnoreCase))) + return false; + + return indexSearch.PathIsIndexed(locationPath); + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs new file mode 100644 index 000000000..5401c2845 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs @@ -0,0 +1,117 @@ +using Flow.Launcher.Infrastructure.Logger; +using Microsoft.Search.Interop; +using System; +using System.Collections.Generic; +using System.Data.OleDb; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex +{ + internal class IndexSearch + { + private readonly object _lock = new object(); + + private OleDbConnection conn; + + private OleDbCommand command; + + private OleDbDataReader dataReaderResults; + + private readonly ResultManager resultManager; + + // Reserved keywords in oleDB + private readonly string reservedStringPattern = @"^[\/\\\$\%]+$"; + + internal IndexSearch(PluginInitContext context) + { + resultManager = new ResultManager(context); + } + + internal List ExecuteWindowsIndexSearch(string indexQueryString, string connectionString, Query query) + { + var folderResults = new List(); + var fileResults = new List(); + var results = new List(); + + try + { + using (conn = new OleDbConnection(connectionString)) + { + conn.Open(); + + using (command = new OleDbCommand(indexQueryString, conn)) + { + // Results return as an OleDbDataReader. + using (dataReaderResults = command.ExecuteReader()) + { + if (dataReaderResults.HasRows) + { + while (dataReaderResults.Read()) + { + if (dataReaderResults.GetValue(0) != DBNull.Value && dataReaderResults.GetValue(1) != DBNull.Value) + { + if (dataReaderResults.GetString(2) == "Directory") + { + folderResults.Add(resultManager.CreateFolderResult( + dataReaderResults.GetString(0), + Constants.DefaultFolderSubtitleString, + dataReaderResults.GetString(1), + query, true, true)); + } + else + { + fileResults.Add(resultManager.CreateFileResult(dataReaderResults.GetString(1), query, true, true)); + } + } + } + } + } + } + } + } + catch (InvalidOperationException e) + { + // Internal error from ExecuteReader(): Connection closed. + LogException("Internal error from ExecuteReader()", e); + } + catch (Exception e) + { + LogException("General error from performing index search", e); + } + + // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection. + return results.Concat(folderResults.OrderBy(x => x.Title)).Concat(fileResults.OrderBy(x => x.Title)).ToList(); ; + } + + internal List WindowsIndexSearch(string searchString, string connectionString, Func constructQuery, Query query) + { + var regexMatch = Regex.Match(searchString, reservedStringPattern); + + if (regexMatch.Success) + return new List(); + + lock (_lock) + { + var constructedQuery = constructQuery(searchString); + return ExecuteWindowsIndexSearch(constructedQuery, connectionString, query); + } + } + + internal bool PathIsIndexed(string path) + { + var csm = new CSearchManager(); + var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager(); + return indexManager.IncludedInCrawlScope(path) > 0; + } + + private void LogException(string message, Exception e) + { +#if DEBUG // Please investigate and handle error from index search + throw e; +#else + Log.Exception($"|Flow.Launcher.Plugin.Explorer.IndexSearch|{message}", e); +#endif + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs new file mode 100644 index 000000000..f4480eea7 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs @@ -0,0 +1,121 @@ +using Microsoft.Search.Interop; + +namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex +{ + public class QueryConstructor + { + private readonly Settings settings; + + private const string SystemIndex = "SystemIndex"; + + public QueryConstructor(Settings settings) + { + this.settings = settings; + } + + public CSearchQueryHelper CreateBaseQuery() + { + var baseQuery = CreateQueryHelper(); + + // Set the number of results we want. Don't set this property if all results are needed. + baseQuery.QueryMaxResults = settings.MaxResult; + + // Set list of columns we want to display, getting the path presently + baseQuery.QuerySelectColumns = "System.FileName, System.ItemPathDisplay, System.ItemType"; + + // Filter based on file name + baseQuery.QueryContentProperties = "System.FileName"; + + // Set sorting order + //baseQuery.QuerySorting = "System.ItemType DESC"; + + return baseQuery; + } + + internal CSearchQueryHelper CreateQueryHelper() + { + // This uses the Microsoft.Search.Interop assembly + var manager = new CSearchManager(); + + // SystemIndex catalog is the default catalog in Windows + var catalogManager = manager.GetCatalog(SystemIndex); + + // Get the ISearchQueryHelper which will help us to translate AQS --> SQL necessary to query the indexer + var queryHelper = catalogManager.GetQueryHelper(); + + return queryHelper; + } + + /// + /// Set the required WHERE clause restriction to search on the first level of a specified directory. + /// + public string QueryWhereRestrictionsForTopLevelDirectorySearch(string path) + { + var searchDepth = $"directory='file:"; + + return QueryWhereRestrictionsFromLocationPath(path, searchDepth); + } + + /// + /// Set the required WHERE clause restriction to search all files and subfolders of a specified directory. + /// + public string QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(string path) + { + var searchDepth = $"scope='file:"; + + return QueryWhereRestrictionsFromLocationPath(path, searchDepth); + } + + private string QueryWhereRestrictionsFromLocationPath(string path, string searchDepth) + { + if (path.EndsWith(Constants.DirectorySeperator)) + return searchDepth + $"{path}'"; + + var indexOfSeparator = path.LastIndexOf(Constants.DirectorySeperator); + + var itemName = path.Substring(indexOfSeparator + 1); + + if (itemName.StartsWith(Constants.AllFilesFolderSearchWildcard)) + itemName = itemName.Substring(1); + + var previousLevelDirectory = path.Substring(0, indexOfSeparator); + + if (string.IsNullOrEmpty(itemName)) + return searchDepth + $"{previousLevelDirectory}'"; + + return $"(System.FileName LIKE '{itemName}%' " + + $"OR CONTAINS(System.FileName,'\"{itemName}*\"',1033)) AND " + + searchDepth + $"{previousLevelDirectory}'"; + } + + /// + /// Search will be performed on all folders and files on the first level of a specified directory. + /// + public string QueryForTopLevelDirectorySearch(string path) + { + string query = "SELECT TOP " + settings.MaxResult + $" {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE "; + + if (path.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > path.LastIndexOf(Constants.DirectorySeperator)) + return query + QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path); + + return query + QueryWhereRestrictionsForTopLevelDirectorySearch(path); + } + + /// + /// Search will be performed on all folders and files based on user's search keywords. + /// + public string QueryForAllFilesAndFolders(string userSearchString) + { + // Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause + return CreateBaseQuery().GenerateSQLFromUserQuery(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch(); + } + + /// + /// Set the required WHERE clause restriction to search for all files and folders. + /// + public string QueryWhereRestrictionsForAllFilesAndFoldersSearch() + { + return $"scope='file:'"; + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs new file mode 100644 index 000000000..d4bfdbbfc --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs @@ -0,0 +1,21 @@ +using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Flow.Launcher.Plugin.Explorer +{ + public class Settings + { + [JsonProperty] + public int MaxResult { get; set; } = 100; + + [JsonProperty] + public List QuickFolderAccessLinks { get; set; } = new List(); + + [JsonProperty] + public bool UseWindowsIndexForDirectorySearch { get; set; } = true; + + [JsonProperty] + public List IndexSearchExcludedSubdirectoryPaths { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs new file mode 100644 index 000000000..a5e81716a --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -0,0 +1,44 @@ +using Flow.Launcher.Infrastructure.Storage; +using Flow.Launcher.Plugin.Explorer.Search; +using Flow.Launcher.Plugin.Explorer.Search.FolderLinks; +using System.Diagnostics; + +namespace Flow.Launcher.Plugin.Explorer.ViewModels +{ + public class SettingsViewModel + { + private readonly PluginJsonStorage storage; + + internal Settings Settings { get; set; } + + internal PluginInitContext Context { get; set; } + + public SettingsViewModel(PluginInitContext context) + { + Context = context; + storage = new PluginJsonStorage(); + Settings = storage.Load(); + } + + public void Save() + { + storage.Save(); + } + + internal void RemoveFolderLinkFromQuickFolders(FolderLink selectedRow) => Settings.QuickFolderAccessLinks.Remove(selectedRow); + + internal void RemoveFolderLinkFromExcludedIndexPaths(FolderLink selectedRow) => Settings.IndexSearchExcludedSubdirectoryPaths.Remove(selectedRow); + + internal void OpenWindowsIndexingOptions() + { + var psi = new ProcessStartInfo + { + FileName = "control.exe", + UseShellExecute = true, + Arguments = Constants.WindowsIndexingOptions + }; + + Process.Start(psi); + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml new file mode 100644 index 000000000..5ba1a063b --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +