diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Assets/LockScreenLogo.scale-200.png b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000..67dacd6442 Binary files /dev/null and b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/LockScreenLogo.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Assets/SplashScreen.scale-200.png b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000000..81065653d3 Binary files /dev/null and b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/SplashScreen.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Assets/Square150x150Logo.scale-200.png b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..d735d9bac7 Binary files /dev/null and b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/Square150x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Assets/Square44x44Logo.scale-200.png b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000..4e6ec9fbc2 Binary files /dev/null and b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/Square44x44Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000000..5f414a8a6d Binary files /dev/null and b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Assets/StoreLogo.png b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/StoreLogo.png new file mode 100644 index 0000000000..b32ed0a8d7 Binary files /dev/null and b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/StoreLogo.png differ diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Assets/Wide310x150Logo.scale-200.png b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000..e6d439a3e6 Binary files /dev/null and b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/Wide310x150Logo.scale-200.png differ diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Assets/youtube_logo_icon.png b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/youtube_logo_icon.png new file mode 100644 index 0000000000..4452536d00 Binary files /dev/null and b/src/modules/cmdpal/Exts/YouTubeExtension/Assets/youtube_logo_icon.png differ diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Helper/LinkAction.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Helper/LinkAction.cs new file mode 100644 index 0000000000..38a9ee295e --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Helper/LinkAction.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace YouTubeExtension.Helper; + +internal sealed partial class LinkAction : InvokableCommand +{ + private readonly YouTubeVideo _video; + + internal LinkAction(YouTubeVideo video) + { + this._video = video; + this.Name = "Open link"; + this.Icon = new("\uE8A7"); + } + + public override CommandResult Invoke() + { + Process.Start(new ProcessStartInfo(_video.Link) { UseShellExecute = true }); + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeHelper.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeHelper.cs new file mode 100644 index 0000000000..1f06df2f14 --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeHelper.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YouTubeExtension.Helper; + +internal sealed class YouTubeHelper +{ + internal static string StateJsonPath() + { + // Get the path to our exe + var path = System.Reflection.Assembly.GetExecutingAssembly().Location; + + // Get the directory of the exe + var directory = Path.GetDirectoryName(path) ?? string.Empty; + + // now, the state is just next to the exe + return Path.Combine(directory, "state.json"); + } +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeVideo.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeVideo.cs new file mode 100644 index 0000000000..1ed45d0166 --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeVideo.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YouTubeExtension.Helper; + +public sealed class YouTubeVideo +{ + public string Title { get; init; } = string.Empty; + + public string Link { get; init; } = string.Empty; + + public string Author { get; init; } = string.Empty; +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Package.appxmanifest b/src/modules/cmdpal/Exts/YouTubeExtension/Package.appxmanifest new file mode 100644 index 0000000000..9c9f402de0 --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Package.appxmanifest @@ -0,0 +1,78 @@ + + + + + + + + YouTube extension for cmdpal + A Lone Developer + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIForm.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIForm.cs new file mode 100644 index 0000000000..5dc092b2de --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIForm.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.CmdPal.Extensions.Helpers; +using YouTubeExtension.Helper; + +namespace YouTubeExtension.Pages; + +internal sealed partial class YouTubeAPIForm : Form +{ + public override string TemplateJson() + { + var json = $$""" + { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "Input.Text", + "style": "text", + "id": "apiKey", + "label": "API Key", + "isRequired": true, + "errorMessage": "API Key required" + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": "Save", + "data": { + "apiKey": "apiKey" + } + } + ] + } + """; + return json; + } + + public override string DataJson() => throw new NotImplementedException(); + + public override string StateJson() => throw new NotImplementedException(); + + public override CommandResult SubmitForm(string payload) + { + var formInput = JsonNode.Parse(payload); + if (formInput == null) + { + return CommandResult.GoHome(); + } + + // get the name and url out of the values + var formApiKey = formInput["apiKey"] ?? string.Empty; + + // Construct a new json blob with the name and url + var json = $$""" + { + "apiKey": "{{formApiKey}}" + } + """; + + File.WriteAllText(YouTubeHelper.StateJsonPath(), json); + return CommandResult.GoHome(); + } +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIPage.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIPage.cs new file mode 100644 index 0000000000..1a4f4a557b --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIPage.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace YouTubeExtension.Pages; + +internal sealed partial class YouTubeAPIPage : FormPage +{ + private readonly YouTubeAPIForm apiForm = new(); + + public override IForm[] Forms() => [apiForm]; + + public YouTubeAPIPage() + { + Name = "YouTube API"; + Icon = new("https://www.youtube.com/favicon.ico"); + } +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeVideosPage.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeVideosPage.cs new file mode 100644 index 0000000000..97971774a7 --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeVideosPage.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using YouTubeExtension.Helper; + +namespace YouTubeExtension; + +internal sealed partial class YouTubeVideosPage : DynamicListPage +{ + public YouTubeVideosPage() + { + Icon = new("https://www.youtube.com/favicon.ico"); + Name = "YouTube"; + } + + public override ISection[] GetItems(string query) + { + return DoGetItems(query).GetAwaiter().GetResult(); // Fetch and await the task synchronously + } + + private async Task DoGetItems(string query) + { + // Fetch YouTube videos based on the query + List items = await GetYouTubeVideos(query); + + // Create a section and populate it with the video results + var section = new ListSection() + { + Title = "Search Results", + Items = items.Select(video => new ListItem(new LinkAction(video)) + { + Title = video.Title, + Subtitle = $"{video.Author}", + }).ToArray(), + }; + + return new[] { section }; // Properly return an array of sections + } + + // Method to fetch videos from YouTube API + private static async Task> GetYouTubeVideos(string query) + { + var state = File.ReadAllText(YouTubeHelper.StateJsonPath()); + var jsonState = JsonNode.Parse(state); + var apiKey = jsonState["apiKey"]?.ToString() ?? string.Empty; + + var videos = new List(); + + using HttpClient client = new HttpClient(); + { + try + { + var response = await client.GetStringAsync($"https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&q={query}&key={apiKey}"); + var json = JsonNode.Parse(response); + + if (json?["items"] is JsonArray itemsArray) + { + foreach (var item in itemsArray) + { + videos.Add(new YouTubeVideo + { + Title = item["snippet"]?["title"]?.ToString() ?? string.Empty, + Link = $"https://www.youtube.com/watch?v={item["id"]?["videoId"]?.ToString()}", + Author = item["snippet"]?["channelTitle"]?.ToString() ?? string.Empty, + }); + } + } + } + catch (Exception ex) + { + // Handle any errors from the API call or parsing + videos.Add(new YouTubeVideo + { + Title = "Error fetching data", + Link = string.Empty, + Author = $"Error: {ex.Message}", + }); + } + } + + return videos; + } +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Program.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Program.cs new file mode 100644 index 0000000000..3bc4589d61 --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Program.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using Microsoft.CmdPal.Extensions; + +namespace YouTubeExtension; + +public class Program +{ + [MTAThread] + public static void Main(string[] args) + { + if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer") + { + using ExtensionServer server = new(); + var extensionDisposedEvent = new ManualResetEvent(false); + var extensionInstance = new SampleExtension(extensionDisposedEvent); + + // We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called. + // This makes sure that only one instance of SampleExtension is alive, which is returned every time the host asks for the IExtension object. + // If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate. + server.RegisterExtension(() => extensionInstance); + + // This will make the main thread wait until the event is signalled by the extension class. + // Since we have single instance of the extension object, we exit as sooon as it is disposed. + extensionDisposedEvent.WaitOne(); + } + else + { + Console.WriteLine("Not being launched as a Extension... exiting."); + } + } +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Properties/launchSettings.json b/src/modules/cmdpal/Exts/YouTubeExtension/Properties/launchSettings.json new file mode 100644 index 0000000000..5a6a96b58e --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "YouTubeExtension (Package)": { + "commandName": "MsixPackage", + "commandLineArgs": "-RegisterProcessAsComServer" + }, + "YouTubeExtension (Unpackaged)": { + "commandName": "Project" + } + } +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/SampleExtension.cs b/src/modules/cmdpal/Exts/YouTubeExtension/SampleExtension.cs new file mode 100644 index 0000000000..e4e21e341d --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/SampleExtension.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.CmdPal.Extensions; + +namespace YouTubeExtension; + +[ComVisible(true)] +[Guid("95696eff-5c44-41ad-8f64-c2182ec9e3fd")] +[ComDefaultInterface(typeof(IExtension))] +public sealed partial class SampleExtension : IExtension +{ + private readonly ManualResetEvent _extensionDisposedEvent; + + public SampleExtension(ManualResetEvent extensionDisposedEvent) + { + this._extensionDisposedEvent = extensionDisposedEvent; + } + + public object GetProvider(ProviderType providerType) + { + switch (providerType) + { + case ProviderType.Commands: + return new YouTubeExtensionActionsProvider(); + default: + return null; + } + } + + public void Dispose() + { + this._extensionDisposedEvent.Set(); + } +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/YouTubeExtension.csproj b/src/modules/cmdpal/Exts/YouTubeExtension/YouTubeExtension.csproj new file mode 100644 index 0000000000..cd8089d212 --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/YouTubeExtension.csproj @@ -0,0 +1,53 @@ + + + + WinExe + YouTubeExtension + app.manifest + win-$(Platform).pubxml + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/YouTubeExtensionCommandsProvider.cs b/src/modules/cmdpal/Exts/YouTubeExtension/YouTubeExtensionCommandsProvider.cs new file mode 100644 index 0000000000..a88c3c7ee2 --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/YouTubeExtensionCommandsProvider.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using Windows.UI.ApplicationSettings; +using YouTubeExtension.Helper; +using YouTubeExtension.Pages; + +namespace YouTubeExtension; + +public partial class YouTubeExtensionActionsProvider : ICommandProvider +{ + public string DisplayName => $"YouTube"; + + public IconDataType Icon => new(string.Empty); + + private readonly IListItem[] _commands = [ + new ListItem(new YouTubeVideosPage()) { Title = "Search Videos", Subtitle = "YouTube" }, + ]; + + private readonly YouTubeAPIPage apiPage = new(); + +#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize + public void Dispose() => throw new NotImplementedException(); +#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize + + public IListItem[] TopLevelCommands() + { + return TopLevelCommandsAsync().GetAwaiter().GetResult(); + } + + public async Task TopLevelCommandsAsync() + { + var settingsPath = YouTubeHelper.StateJsonPath(); + + // Check if the settings file exists + if (!File.Exists(settingsPath)) + { + return new[] { new ListItem(apiPage) { Title = "YouTube", Subtitle = "Enter your API key." } }; + } + + // Read the file and parse the API key + var state = File.ReadAllText(settingsPath); + var jsonState = System.Text.Json.Nodes.JsonNode.Parse(state); + var apiKey = jsonState?["apiKey"]?.ToString() ?? string.Empty; + + // Validate the API key using YouTube API + if (string.IsNullOrWhiteSpace(apiKey) || !await IsApiKeyValid(apiKey)) + { + return new[] { new ListItem(apiPage) { Title = "YouTube", Subtitle = "Current API Invalid. Please enter a new API key." } }; + } + + // If file exists and API key is valid, return commands + return _commands; + } + + // Method to check if the API key is valid by making a simple request to the YouTube API + private static async Task IsApiKeyValid(string apiKey) + { + using HttpClient client = new HttpClient(); + { + try + { + // Make a simple request to verify the API key, such as fetching a video + var response = await client.GetAsync($"https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&q=test&key={apiKey}"); + + // If the response status code is 200, the API key is valid + if (response.IsSuccessStatusCode) + { + return true; + } + + // Optionally, handle other status codes and log errors + return false; + } + catch + { + // If any exception occurs (e.g., network error), consider the API key invalid + return false; + } + } + } +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/app.manifest b/src/modules/cmdpal/Exts/YouTubeExtension/app.manifest new file mode 100644 index 0000000000..bcafb9bc5b --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/app.manifest @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + PerMonitorV2 + + + diff --git a/src/modules/cmdpal/WindowsCommandPalette.sln b/src/modules/cmdpal/WindowsCommandPalette.sln index 0b74e7135d..5a3dfc00af 100644 --- a/src/modules/cmdpal/WindowsCommandPalette.sln +++ b/src/modules/cmdpal/WindowsCommandPalette.sln @@ -42,6 +42,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SSHKeychainExtension", "ext EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SamplePagesExtension", "exts\SamplePagesExtension\SamplePagesExtension.csproj", "{399B53F1-5AA6-4FE0-8C3C-66E07B84E6F1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YouTubeExtension", "Exts\YouTubeExtension\YouTubeExtension.csproj", "{276243F6-4B25-411C-B110-1E7DB575F13D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -198,6 +200,18 @@ Global {399B53F1-5AA6-4FE0-8C3C-66E07B84E6F1}.Release|x64.ActiveCfg = Release|x64 {399B53F1-5AA6-4FE0-8C3C-66E07B84E6F1}.Release|x64.Build.0 = Release|x64 {399B53F1-5AA6-4FE0-8C3C-66E07B84E6F1}.Release|x64.Deploy.0 = Release|x64 + {276243F6-4B25-411C-B110-1E7DB575F13D}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {276243F6-4B25-411C-B110-1E7DB575F13D}.Debug|ARM64.Build.0 = Debug|ARM64 + {276243F6-4B25-411C-B110-1E7DB575F13D}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {276243F6-4B25-411C-B110-1E7DB575F13D}.Debug|x64.ActiveCfg = Debug|x64 + {276243F6-4B25-411C-B110-1E7DB575F13D}.Debug|x64.Build.0 = Debug|x64 + {276243F6-4B25-411C-B110-1E7DB575F13D}.Debug|x64.Deploy.0 = Debug|x64 + {276243F6-4B25-411C-B110-1E7DB575F13D}.Release|ARM64.ActiveCfg = Release|ARM64 + {276243F6-4B25-411C-B110-1E7DB575F13D}.Release|ARM64.Build.0 = Release|ARM64 + {276243F6-4B25-411C-B110-1E7DB575F13D}.Release|ARM64.Deploy.0 = Release|ARM64 + {276243F6-4B25-411C-B110-1E7DB575F13D}.Release|x64.ActiveCfg = Release|x64 + {276243F6-4B25-411C-B110-1E7DB575F13D}.Release|x64.Build.0 = Release|x64 + {276243F6-4B25-411C-B110-1E7DB575F13D}.Release|x64.Deploy.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -219,6 +233,7 @@ Global {7F6796A4-4233-4CEC-914F-95EC7A5283A0} = {272D0E9A-8FC3-49F5-8FAD-79ABAE8AB1E4} {77D99BE0-F69C-4F27-8153-951CEC5110FE} = {B7FF739F-7716-4FC3-B622-705486187B87} {399B53F1-5AA6-4FE0-8C3C-66E07B84E6F1} = {B7FF739F-7716-4FC3-B622-705486187B87} + {276243F6-4B25-411C-B110-1E7DB575F13D} = {B7FF739F-7716-4FC3-B622-705486187B87} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BC94BFC2-A741-4978-B6A4-9E01B7660E6B}