From 77e41c9a331b6db39f4e0ebb816d31e3651a70ae Mon Sep 17 00:00:00 2001 From: Davide Giacometti Date: Sat, 20 Jan 2024 23:01:12 +0100 Subject: [PATCH] add support for multiple Edge profiles --- .../FavoriteQueryTest.cs | 42 ++++++-- .../DefaultFavoriteProvider.cs} | 35 +++--- .../Mocks/MultiProfileManager.cs | 18 ++++ .../Mocks/SingleProfileManager.cs | 18 ++++ .../Mocks/WorkFavoriteProvider.cs | 28 +++++ .../Helpers/EdgeHelpers.cs | 34 ++++++ .../Helpers/FavoriteProvider.cs | 69 +++++++----- .../Helpers/FavoriteQuery.cs | 44 +++++++- .../Helpers/IFavoriteQuery.cs | 4 +- .../Helpers/IProfileManager.cs | 14 +++ .../Helpers/ProfileManager.cs | 101 ++++++++++++++++++ .../Main.cs | 43 +++++--- .../Models/FavoriteItem.cs | 43 ++++---- .../Models/ProfileInfo.cs | 18 ++++ Directory.Build.props | 4 +- 15 files changed, 426 insertions(+), 89 deletions(-) rename Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/{MockFavoriteProvider.cs => Mocks/DefaultFavoriteProvider.cs} (52%) create mode 100644 Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/MultiProfileManager.cs create mode 100644 Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/SingleProfileManager.cs create mode 100644 Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/WorkFavoriteProvider.cs create mode 100644 Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/EdgeHelpers.cs create mode 100644 Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/IProfileManager.cs create mode 100644 Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/ProfileManager.cs create mode 100644 Community.PowerToys.Run.Plugin.EdgeFavorite/Models/ProfileInfo.cs diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/FavoriteQueryTest.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/FavoriteQueryTest.cs index 82cecf1..a312846 100644 --- a/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/FavoriteQueryTest.cs +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/FavoriteQueryTest.cs @@ -1,8 +1,10 @@ // Copyright (c) Davide Giacometti. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using System.Linq; using Community.PowerToys.Run.Plugin.EdgeFavorite.Helpers; +using Community.PowerToys.Run.Plugin.EdgeFavorite.Tests.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Tests @@ -10,21 +12,35 @@ namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Tests [TestClass] public class FavoriteQueryTest { - private readonly IFavoriteProvider _favoriteProvider; + private readonly IProfileManager _singleProfileManager; + private readonly FavoriteQuery _singleFavoriteQuery; + + private readonly IProfileManager _multiProfileManager; + private readonly FavoriteQuery _multiFavoriteQuery; public FavoriteQueryTest() { - _favoriteProvider = new MockFavoriteProvider(); + _singleProfileManager = new SingleProfileManager(); + _singleFavoriteQuery = new FavoriteQuery(_singleProfileManager); + + _multiProfileManager = new MultiProfileManager(); + _multiFavoriteQuery = new FavoriteQuery(_multiProfileManager); } [TestMethod] public void Should_Get_All_Urls() { - var sut = new FavoriteQuery(); - var result = sut.GetAll(_favoriteProvider.Root); + var result = _singleFavoriteQuery.GetAll(); Assert.AreEqual(result.Count(), 10); } + [TestMethod] + public void Should_Get_All_Urls_From_All_Profiles() + { + var result = _multiFavoriteQuery.GetAll(); + Assert.AreEqual(result.Count(), 14); + } + [DataTestMethod] [DataRow("", 5)] [DataRow("/", 0)] @@ -36,11 +52,25 @@ public void Should_Get_All_Urls() [DataRow("Coding/Tools", 1)] [DataRow("Coding/Tools/", 2)] [DataRow("coding/tools/j", 1)] + [DataRow("coding\\tools\\j", 1)] public void Should_Get_Expected_Result(string search, int expectedResult) { - var sut = new FavoriteQuery(); - var result = sut.Search(_favoriteProvider.Root, search.Split('/'), 0); + var result = _singleFavoriteQuery.Search(search); Assert.AreEqual(result.Count(), expectedResult); } + + [TestMethod] + public void Should_Get_Single_Folder() + { + var result = _multiFavoriteQuery.Search("Codi"); + Assert.AreEqual(result.Count(), 1); + } + + [TestMethod] + public void Should_Merge_Folders_Content() + { + var result = _multiFavoriteQuery.Search("Coding/"); + Assert.AreEqual(result.Count(), 7); + } } } diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/MockFavoriteProvider.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/DefaultFavoriteProvider.cs similarity index 52% rename from Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/MockFavoriteProvider.cs rename to Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/DefaultFavoriteProvider.cs index e67e196..fb44a6a 100644 --- a/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/MockFavoriteProvider.cs +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/DefaultFavoriteProvider.cs @@ -4,34 +4,35 @@ using Community.PowerToys.Run.Plugin.EdgeFavorite.Helpers; using Community.PowerToys.Run.Plugin.EdgeFavorite.Models; -namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Tests +namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Tests.Mocks { - public class MockFavoriteProvider : IFavoriteProvider + public class DefaultFavoriteProvider : IFavoriteProvider { + private readonly ProfileInfo _profileInfo = new("Default", "Default"); private readonly FavoriteItem _root; public FavoriteItem Root => _root; - public MockFavoriteProvider() + public DefaultFavoriteProvider() { - var coding = new FavoriteItem("Coding", null, "Coding", FavoriteType.Folder); - coding.AddChildren(new FavoriteItem("GitHub", "https://github.com/", "Coding/GitHub", FavoriteType.Url)); - coding.AddChildren(new FavoriteItem("Microsoft Azure", "https://portal.azure.com/", "Coding/Microsoft Azure", FavoriteType.Url)); - coding.AddChildren(new FavoriteItem("Microsoft Developer Blogs", "https://devblogs.microsoft.com/", "Coding/Microsoft Developer Blogs", FavoriteType.Url)); + var coding = new FavoriteItem("Coding", "Coding"); + coding.AddChildren(new FavoriteItem("GitHub", "https://github.com/", "Coding/GitHub", _profileInfo)); + coding.AddChildren(new FavoriteItem("Microsoft Azure", "https://portal.azure.com/", "Coding/Microsoft Azure", _profileInfo)); + coding.AddChildren(new FavoriteItem("Microsoft Developer Blogs", "https://devblogs.microsoft.com/", "Coding/Microsoft Developer Blogs", _profileInfo)); - var tools = new FavoriteItem("Tools", null, "Coding", FavoriteType.Folder); - tools.AddChildren(new FavoriteItem("JWT", "https://jwt.io/", "Coding/Tools/JWT", FavoriteType.Url)); - tools.AddChildren(new FavoriteItem("Pigment", "https://pigment.shapefactory.co/", "Coding/Tools/Pigment", FavoriteType.Url)); + var tools = new FavoriteItem("Tools", "Coding"); + tools.AddChildren(new FavoriteItem("JWT", "https://jwt.io/", "Coding/Tools/JWT", _profileInfo)); + tools.AddChildren(new FavoriteItem("Pigment", "https://pigment.shapefactory.co/", "Coding/Tools/Pigment", _profileInfo)); coding.AddChildren(tools); - var shopping = new FavoriteItem("Shopping", null, "Shopping", FavoriteType.Folder); - shopping.AddChildren(new FavoriteItem("Amazon", "https://www.amazon.com/", "Shopping/Amazon", FavoriteType.Url)); - shopping.AddChildren(new FavoriteItem("eBay", "https://www.ebay.com/", "Shopping/eBay", FavoriteType.Url)); + var shopping = new FavoriteItem("Shopping", "Shopping"); + shopping.AddChildren(new FavoriteItem("Amazon", "https://www.amazon.com/", "Shopping/Amazon", _profileInfo)); + shopping.AddChildren(new FavoriteItem("eBay", "https://www.ebay.com/", "Shopping/eBay", _profileInfo)); - _root = new FavoriteItem("Favorites bar", null, string.Empty, FavoriteType.Folder); - _root.AddChildren(new FavoriteItem("YouTube", "https://www.youtube.com/", "YouTube", FavoriteType.Url)); - _root.AddChildren(new FavoriteItem("Spotify", "https://open.spotify.com/", "Spotify", FavoriteType.Url)); - _root.AddChildren(new FavoriteItem("LinkedIn", "https://www.linkedin.com/", "LinkedIn", FavoriteType.Url)); + _root = new FavoriteItem("Favorites bar", string.Empty); + _root.AddChildren(new FavoriteItem("YouTube", "https://www.youtube.com/", "YouTube", _profileInfo)); + _root.AddChildren(new FavoriteItem("Spotify", "https://open.spotify.com/", "Spotify", _profileInfo)); + _root.AddChildren(new FavoriteItem("LinkedIn", "https://www.linkedin.com/", "LinkedIn", _profileInfo)); _root.AddChildren(coding); _root.AddChildren(shopping); } diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/MultiProfileManager.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/MultiProfileManager.cs new file mode 100644 index 0000000..17ec8b6 --- /dev/null +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/MultiProfileManager.cs @@ -0,0 +1,18 @@ +// Copyright (c) Davide Giacometti. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Community.PowerToys.Run.Plugin.EdgeFavorite.Helpers; + +namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Tests.Mocks +{ + public class MultiProfileManager : IProfileManager + { + public ReadOnlyCollection FavoriteProviders => (new IFavoriteProvider[] { new DefaultFavoriteProvider(), new WorkFavoriteProvider() }).AsReadOnly(); + + public void ReloadProfiles(bool all) + { + } + } +} diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/SingleProfileManager.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/SingleProfileManager.cs new file mode 100644 index 0000000..57152de --- /dev/null +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/SingleProfileManager.cs @@ -0,0 +1,18 @@ +// Copyright (c) Davide Giacometti. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Community.PowerToys.Run.Plugin.EdgeFavorite.Helpers; + +namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Tests.Mocks +{ + public class SingleProfileManager : IProfileManager + { + public ReadOnlyCollection FavoriteProviders => (new IFavoriteProvider[] { new DefaultFavoriteProvider() }).AsReadOnly(); + + public void ReloadProfiles(bool all) + { + } + } +} diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/WorkFavoriteProvider.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/WorkFavoriteProvider.cs new file mode 100644 index 0000000..3122bc3 --- /dev/null +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite.Tests/Mocks/WorkFavoriteProvider.cs @@ -0,0 +1,28 @@ +// Copyright (c) Davide Giacometti. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Community.PowerToys.Run.Plugin.EdgeFavorite.Helpers; +using Community.PowerToys.Run.Plugin.EdgeFavorite.Models; + +namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Tests.Mocks +{ + public class WorkFavoriteProvider : IFavoriteProvider + { + private readonly ProfileInfo _profileInfo = new("Work", "Profile 1"); + private readonly FavoriteItem _root; + + public FavoriteItem Root => _root; + + public WorkFavoriteProvider() + { + var coding = new FavoriteItem("Coding", "Coding"); + coding.AddChildren(new FavoriteItem("AWS", "https://aws.amazon.com/", "Coding/AWS", _profileInfo)); + coding.AddChildren(new FavoriteItem("Bitbucket", "https://bitbucket.org/", "Coding/Bitbucket", _profileInfo)); + coding.AddChildren(new FavoriteItem("Microsoft Azure", "https://portal.azure.com/", "Coding/Microsoft Azure", _profileInfo)); + + _root = new FavoriteItem("Favorites bar", string.Empty); + _root.AddChildren(new FavoriteItem("Gmail", "https://mail.google.com/", "Gmail", _profileInfo)); + _root.AddChildren(coding); + } + } +} diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/EdgeHelpers.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/EdgeHelpers.cs new file mode 100644 index 0000000..ad98f56 --- /dev/null +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/EdgeHelpers.cs @@ -0,0 +1,34 @@ +// Copyright (c) Davide Giacometti. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Community.PowerToys.Run.Plugin.EdgeFavorite.Models; +using Wox.Infrastructure; +using Wox.Plugin.Logger; + +namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Helpers +{ + public static class EdgeHelpers + { + public static void OpenInEdge(FavoriteItem favorite, bool inPrivate) + { + var args = $"{favorite.Url}"; + + if (inPrivate) + { + args += " -inprivate"; + } + + args += $" -profile-directory=\"{favorite.Profile.Directory}\""; + + try + { + Helper.OpenInShell(@"shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App", args); + } + catch (Exception ex) + { + Log.Exception("Failed to launch Microsoft Edge", ex, typeof(EdgeHelpers)); + } + } + } +} diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/FavoriteProvider.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/FavoriteProvider.cs index d971ff5..3052187 100644 --- a/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/FavoriteProvider.cs +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/FavoriteProvider.cs @@ -11,14 +11,18 @@ namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Helpers { public class FavoriteProvider : IFavoriteProvider { - private readonly string _path = Environment.ExpandEnvironmentVariables(@"%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\Bookmarks"); + private readonly string _path; private readonly FileSystemWatcher _watcher; private FavoriteItem _root; public FavoriteItem Root => _root; - public FavoriteProvider() + public ProfileInfo ProfileInfo { get; } + + public FavoriteProvider(string path, ProfileInfo profileInfo) { + _path = path; + ProfileInfo = profileInfo; _root = new FavoriteItem(); InitFavorites(); @@ -35,39 +39,46 @@ public FavoriteProvider() private void InitFavorites() { - if (!Path.Exists(_path)) + try { - Log.Warn($"Failed to find bookmarks file {_path}", typeof(FavoriteProvider)); - return; - } + if (!Path.Exists(_path)) + { + Log.Warn($"Failed to find Bookmarks file: {_path}", typeof(FavoriteProvider)); + return; + } - using var fs = new FileStream(_path, FileMode.Open, FileAccess.Read); - using var sr = new StreamReader(fs); - string json = sr.ReadToEnd(); - var parsed = JsonDocument.Parse(json); - parsed.RootElement.TryGetProperty("roots", out var rootElement); - if (rootElement.ValueKind != JsonValueKind.Object) - { - return; - } + using var fs = new FileStream(_path, FileMode.Open, FileAccess.Read); + using var sr = new StreamReader(fs); + string json = sr.ReadToEnd(); + var parsed = JsonDocument.Parse(json); + parsed.RootElement.TryGetProperty("roots", out var rootElement); + if (rootElement.ValueKind != JsonValueKind.Object) + { + return; + } - var newRoot = new FavoriteItem(); - rootElement.TryGetProperty("bookmark_bar", out var bookmarkBarElement); - if (bookmarkBarElement.ValueKind == JsonValueKind.Object) - { - ProcessFavorites(bookmarkBarElement, newRoot, string.Empty, true); - } + var newRoot = new FavoriteItem(); + rootElement.TryGetProperty("bookmark_bar", out var bookmarkBarElement); + if (bookmarkBarElement.ValueKind == JsonValueKind.Object) + { + ProcessFavorites(bookmarkBarElement, newRoot, string.Empty, true); + } + + rootElement.TryGetProperty("other", out var otherElement); + if (otherElement.ValueKind == JsonValueKind.Object) + { + ProcessFavorites(otherElement, newRoot, string.Empty, newRoot.Childrens.Count == 0); + } - rootElement.TryGetProperty("other", out var otherElement); - if (otherElement.ValueKind == JsonValueKind.Object) + _root = newRoot; + } + catch (Exception ex) { - ProcessFavorites(otherElement, newRoot, string.Empty, newRoot.Childrens.Count == 0); + Log.Exception($"Failed to read favorites: {_path}", ex, typeof(FavoriteProvider)); } - - _root = newRoot; } - private static void ProcessFavorites(JsonElement element, FavoriteItem parent, string path, bool root) + private void ProcessFavorites(JsonElement element, FavoriteItem parent, string path, bool root) { if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty("children", out var children)) { @@ -79,7 +90,7 @@ private static void ProcessFavorites(JsonElement element, FavoriteItem parent, s path += $"{(string.IsNullOrWhiteSpace(path) ? string.Empty : "/")}{name}"; } - var folder = new FavoriteItem(name, null, path, FavoriteType.Folder); + var folder = new FavoriteItem(name, path); if (root) { @@ -105,7 +116,7 @@ private static void ProcessFavorites(JsonElement element, FavoriteItem parent, s if (!string.IsNullOrWhiteSpace(name)) { path += $"{(string.IsNullOrWhiteSpace(path) ? string.Empty : "/")}{name}"; - var favorite = new FavoriteItem(name, url.GetString(), path, FavoriteType.Url); + var favorite = new FavoriteItem(name, url.GetString(), path, ProfileInfo); parent.AddChildren(favorite); } } diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/FavoriteQuery.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/FavoriteQuery.cs index b2da530..8994c8d 100644 --- a/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/FavoriteQuery.cs +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/FavoriteQuery.cs @@ -10,7 +10,47 @@ namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Helpers { public class FavoriteQuery : IFavoriteQuery { - public IEnumerable GetAll(FavoriteItem node) + private readonly IProfileManager _profileManager; + + public FavoriteQuery(IProfileManager profileManager) + { + _profileManager = profileManager; + } + + public IEnumerable GetAll() + { + foreach (var root in _profileManager.FavoriteProviders.Select(p => p.Root)) + { + foreach (var favorite in GetAll(root)) + { + yield return favorite; + } + } + } + + public IEnumerable Search(string query) + { + var path = query.Replace('\\', '/').Split('/'); + + if (_profileManager.FavoriteProviders.Count == 1) + { + return Search(_profileManager.FavoriteProviders[0].Root, path, 0); + } + else + { + var results = new List(); + + foreach (var root in _profileManager.FavoriteProviders.Select(p => p.Root)) + { + results.AddRange(Search(root, path, 0)); + } + + // Flatten folders with same path for each profiles + return results.DistinctBy(f => new { f.Path, f.Type, f.Profile }); + } + } + + private static IEnumerable GetAll(FavoriteItem node) { if (node.Type == FavoriteType.Url) { @@ -28,7 +68,7 @@ public IEnumerable GetAll(FavoriteItem node) } } - public IEnumerable Search(FavoriteItem node, string[] path, int depth) + private static IEnumerable Search(FavoriteItem node, string[] path, int depth) { if (depth == path.Length - 1) { diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/IFavoriteQuery.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/IFavoriteQuery.cs index e5254f8..3b56dee 100644 --- a/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/IFavoriteQuery.cs +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/IFavoriteQuery.cs @@ -8,8 +8,8 @@ namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Helpers { public interface IFavoriteQuery { - IEnumerable GetAll(FavoriteItem node); + IEnumerable GetAll(); - IEnumerable Search(FavoriteItem node, string[] path, int depth); + IEnumerable Search(string query); } } diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/IProfileManager.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/IProfileManager.cs new file mode 100644 index 0000000..6b737f7 --- /dev/null +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/IProfileManager.cs @@ -0,0 +1,14 @@ +// Copyright (c) Davide Giacometti. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.ObjectModel; + +namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Helpers +{ + public interface IProfileManager + { + ReadOnlyCollection FavoriteProviders { get; } + + void ReloadProfiles(bool all); + } +} diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/ProfileManager.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/ProfileManager.cs new file mode 100644 index 0000000..2fb5338 --- /dev/null +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite/Helpers/ProfileManager.cs @@ -0,0 +1,101 @@ +// Copyright (c) Davide Giacometti. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using Community.PowerToys.Run.Plugin.EdgeFavorite.Models; +using Wox.Plugin.Logger; + +namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Helpers +{ + public class ProfileManager : IProfileManager + { + private static readonly string _userDataPath = Environment.ExpandEnvironmentVariables(@"%LOCALAPPDATA%\Microsoft\Edge\User Data"); + private readonly List _favoriteProviders = new(); + + public ReadOnlyCollection FavoriteProviders => _favoriteProviders.AsReadOnly(); + + public ProfileManager(bool defaultOnly) + { + ReloadProfiles(defaultOnly); + } + + public void ReloadProfiles(bool defaultOnly) + { + if (_favoriteProviders.Count > 0) + { + _favoriteProviders.Clear(); + } + + foreach (var path in Directory.GetFiles(_userDataPath, "Bookmarks", new EnumerationOptions { RecurseSubdirectories = true, MaxRecursionDepth = 2 })) + { + var directory = Directory.GetParent(path); + + if (directory == null) + { + continue; + } + + // Guest profile doesn't allow favorites + if (directory.Name.Equals("Guest Profile", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (defaultOnly && !directory.Name.Equals("Default", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var profile = new ProfileInfo(directory.Name, directory.Name); + TrySetProfileName(profile, directory.FullName); + _favoriteProviders.Add(new FavoriteProvider(path, profile)); + } + } + + private static void TrySetProfileName(ProfileInfo profileInfo, string directoryPath) + { + _ = Task.Run(() => + { + try + { + var preferencesPath = Path.Combine(directoryPath, "Preferences"); + if (!File.Exists(preferencesPath)) + { + Log.Error($"Failed to set profile name: {preferencesPath} files not found.", typeof(ProfileManager)); + return; + } + + using var fs = new FileStream(preferencesPath, FileMode.Open, FileAccess.Read); + using var sr = new StreamReader(fs); + string json = sr.ReadToEnd(); + var parsed = JsonDocument.Parse(json); + parsed.RootElement.TryGetProperty("profile", out var profileElement); + profileElement.TryGetProperty("name", out var nameElement); + if (nameElement.ValueKind != JsonValueKind.String) + { + Log.Error("Failed to set profile name: name property is not a string.", typeof(ProfileManager)); + return; + } + + var name = nameElement.GetString(); + if (string.IsNullOrWhiteSpace(name)) + { + Log.Error("Failed to set profile name: name property is empty.", typeof(ProfileManager)); + return; + } + + profileInfo.Name = name; + } + catch (Exception ex) + { + Log.Exception("Failed to set profile name", ex, typeof(ProfileManager)); + } + }); + } + } +} diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite/Main.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite/Main.cs index 2153dc7..98744a6 100644 --- a/Community.PowerToys.Run.Plugin.EdgeFavorite/Main.cs +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite/Main.cs @@ -19,11 +19,14 @@ public class Main : IPlugin, ISettingProvider, IContextMenu public static string PluginID => "D73A7EF0633F4C82A14454FFD848F447"; private const string SearchTree = nameof(SearchTree); + private const string DefaultOnly = nameof(DefaultOnly); private const bool SearchTreeDefault = false; - private readonly IFavoriteProvider _favoriteProvider; - private readonly IFavoriteQuery _favoriteQuery; + private const bool DefaultOnlyDefault = false; + private readonly ProfileManager _profileManager; + private readonly FavoriteQuery _favoriteQuery; private PluginInitContext? _context; private bool _searchTree; + private bool _defaultOnly; public string Name => "Edge Favorite"; @@ -31,19 +34,26 @@ public class Main : IPlugin, ISettingProvider, IContextMenu public IEnumerable AdditionalOptions => new List { - new PluginAdditionalOption + new() { Key = SearchTree, Value = SearchTreeDefault, DisplayLabel = "Search as tree", - DisplayDescription = "Navigate the original directory tree when searching.", + DisplayDescription = "Navigate the folder tree when searching.", + }, + new() + { + Key = DefaultOnly, + Value = DefaultOnlyDefault, + DisplayLabel = "Default profile only", + DisplayDescription = "Show favorites only from the default Microsoft Edge profile.", }, }; public Main() { - _favoriteProvider = new FavoriteProvider(); - _favoriteQuery = new FavoriteQuery(); + _profileManager = new ProfileManager(_defaultOnly); + _favoriteQuery = new FavoriteQuery(_profileManager); } public void Init(PluginInitContext context) @@ -55,27 +65,27 @@ public void Init(PluginInitContext context) public List Query(Query query) { - var search = query.Search.Replace('\\', '/').Split('/'); + var showProfileName = _profileManager.FavoriteProviders.Count > 1; if (_searchTree) { return _favoriteQuery - .Search(_favoriteProvider.Root, search, 0) + .Search(query.Search) .OrderBy(f => f.Type) .ThenBy(f => f.Name) - .Select(f => f.CreateResult()) + .Select(f => f.CreateResult(showProfileName)) .ToList(); } else { var results = new List(); - foreach (var favorite in _favoriteQuery.GetAll(_favoriteProvider.Root)) + foreach (var favorite in _favoriteQuery.GetAll()) { var score = StringMatcher.FuzzySearch(query.Search, favorite.Name); if (string.IsNullOrWhiteSpace(query.Search) || score.Score > 0) { - var result = favorite.CreateResult(); + var result = favorite.CreateResult(showProfileName); result.Score = score.Score; result.TitleHighlightData = score.MatchData; results.Add(result); @@ -93,13 +103,22 @@ public Control CreateSettingPanel() public void UpdateSettings(PowerLauncherPluginSettings settings) { + var oldDefaultOnly = _defaultOnly; + if (settings != null && settings.AdditionalOptions != null) { _searchTree = settings.AdditionalOptions.FirstOrDefault(x => x.Key == SearchTree)?.Value ?? SearchTreeDefault; + _defaultOnly = settings.AdditionalOptions.FirstOrDefault(x => x.Key == DefaultOnly)?.Value ?? DefaultOnlyDefault; } else { _searchTree = SearchTreeDefault; + _defaultOnly = DefaultOnlyDefault; + } + + if (oldDefaultOnly != _defaultOnly) + { + _profileManager.ReloadProfiles(_defaultOnly); } } @@ -107,7 +126,7 @@ public List LoadContextMenus(Result selectedResult) { if (selectedResult.ContextData is not FavoriteItem favorite) { - return new List(); + return new(); } return favorite.CreateContextMenuResult(); diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite/Models/FavoriteItem.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite/Models/FavoriteItem.cs index e3e1126..887fcb3 100644 --- a/Community.PowerToys.Run.Plugin.EdgeFavorite/Models/FavoriteItem.cs +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite/Models/FavoriteItem.cs @@ -7,8 +7,8 @@ using System.Reflection; using System.Windows; using System.Windows.Input; +using Community.PowerToys.Run.Plugin.EdgeFavorite.Helpers; using ManagedCommon; -using Wox.Infrastructure; using Wox.Plugin; using Wox.Plugin.Logger; @@ -16,6 +16,7 @@ namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Models { public class FavoriteItem { + private static readonly ProfileInfo _emptyProfile = new(string.Empty, string.Empty); private static readonly string _pluginName = Assembly.GetExecutingAssembly().GetName().Name ?? string.Empty; private static string? _folderIcoPath; private static string? _urlIcoPath; @@ -29,6 +30,8 @@ public class FavoriteItem public FavoriteType Type { get; } + public ProfileInfo Profile { get; } + public ReadOnlyCollection Childrens => _childrens.AsReadOnly(); public FavoriteItem() @@ -36,14 +39,24 @@ public FavoriteItem() Name = string.Empty; Path = string.Empty; Type = FavoriteType.Folder; + Profile = _emptyProfile; // Folders are profile agnostic + } + + public FavoriteItem(string name, string path) + { + Name = name; + Path = path; + Type = FavoriteType.Folder; + Profile = _emptyProfile; // Folders are profile agnostic } - public FavoriteItem(string name, string? url, string path, FavoriteType type) + public FavoriteItem(string name, string? url, string path, ProfileInfo profile) { Name = name; Url = url; Path = path; - Type = type; + Type = FavoriteType.Url; + Profile = profile; } public void AddChildren(FavoriteItem item) @@ -51,7 +64,7 @@ public void AddChildren(FavoriteItem item) _childrens.Add(item); } - public Result CreateResult() + public Result CreateResult(bool showProfileName) { return Type switch { @@ -66,12 +79,12 @@ public Result CreateResult() FavoriteType.Url => new Result { Title = Name, - SubTitle = $"Favorite: {Path}", + SubTitle = showProfileName ? $"Favorite: {Path} - {Profile.Name}" : $"Favorite: {Path}", IcoPath = _urlIcoPath, QueryTextDisplay = Path, Action = _ => { - Helper.OpenInShell($"microsoft-edge:{Url}"); + EdgeHelpers.OpenInEdge(this, false); return true; }, ToolTipData = new ToolTipData(Name, Url), @@ -85,9 +98,9 @@ public List CreateContextMenuResult() { if (Type == FavoriteType.Url) { - return new List + return new() { - new ContextMenuResult + new() { Title = "Copy URL (Ctrl+C)", Glyph = "\xE8C8", @@ -109,7 +122,7 @@ public List CreateContextMenuResult() return true; }, }, - new ContextMenuResult + new() { Title = "Open InPrivate (Ctrl+P)", Glyph = "\xE727", @@ -119,22 +132,14 @@ public List CreateContextMenuResult() PluginName = _pluginName, Action = _ => { - try - { - Helper.OpenInShell(@"shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App", $"-inprivate {Url}"); - } - catch (Exception ex) - { - Log.Exception("Failed to launch Microsoft Edge", ex, typeof(FavoriteItem)); - } - + EdgeHelpers.OpenInEdge(this, true); return true; }, }, }; } - return new List(); + return new(); } public static void SetIcons(Theme theme) diff --git a/Community.PowerToys.Run.Plugin.EdgeFavorite/Models/ProfileInfo.cs b/Community.PowerToys.Run.Plugin.EdgeFavorite/Models/ProfileInfo.cs new file mode 100644 index 0000000..96b448f --- /dev/null +++ b/Community.PowerToys.Run.Plugin.EdgeFavorite/Models/ProfileInfo.cs @@ -0,0 +1,18 @@ +// Copyright (c) Davide Giacometti. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Community.PowerToys.Run.Plugin.EdgeFavorite.Models +{ + public class ProfileInfo + { + public string Name { get; set; } + + public string Directory { get; private set; } + + public ProfileInfo(string name, string directory) + { + Name = name; + Directory = directory; + } + } +} diff --git a/Directory.Build.props b/Directory.Build.props index 922a8cd..b254331 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 0.2.1 + 0.3.0 @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive