From bde4e861c2705a377767bf80b2328d90f98faecb Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 11 Jun 2023 18:52:17 +0200 Subject: [PATCH] Initial workshop commit --- src/Artemis.UI/Artemis.UI.csproj | 1 + src/Artemis.UI/ArtemisBootstrapper.cs | 2 + .../Screens/Workshop/WorkshopView.axaml | 70 ++--- .../Screens/Workshop/WorkshopViewModel.cs | 78 +++--- src/Artemis.WebClient.Updating/.graphqlconfig | 15 -- .../Artemis.WebClient.Updating.csproj | 4 + .../graphql.config.yml | 8 + .../schema.extensions.graphql | 13 - src/Artemis.WebClient.Updating/schema.graphql | 29 ++- .../.config/dotnet-tools.json | 12 + .../.graphqlrc.json | 22 ++ .../Artemis.WebClient.Workshop.csproj | 20 ++ .../DryIoc/ContainerExtensions.cs | 26 ++ .../Queries/GetEntries.graphql | 9 + .../Queries/GetEntryById.graphql | 7 + .../graphql.config.yml | 8 + src/Artemis.WebClient.Workshop/schema.graphql | 244 ++++++++++++++++++ src/Artemis.sln | 6 + 18 files changed, 441 insertions(+), 133 deletions(-) delete mode 100644 src/Artemis.WebClient.Updating/.graphqlconfig create mode 100644 src/Artemis.WebClient.Updating/graphql.config.yml delete mode 100644 src/Artemis.WebClient.Updating/schema.extensions.graphql create mode 100644 src/Artemis.WebClient.Workshop/.config/dotnet-tools.json create mode 100644 src/Artemis.WebClient.Workshop/.graphqlrc.json create mode 100644 src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj create mode 100644 src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs create mode 100644 src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql create mode 100644 src/Artemis.WebClient.Workshop/Queries/GetEntryById.graphql create mode 100644 src/Artemis.WebClient.Workshop/graphql.config.yml create mode 100644 src/Artemis.WebClient.Workshop/schema.graphql diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index ce2eaaf18..f30f7879f 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Artemis.UI/ArtemisBootstrapper.cs b/src/Artemis.UI/ArtemisBootstrapper.cs index 52cab9df9..dbed7a0f4 100644 --- a/src/Artemis.UI/ArtemisBootstrapper.cs +++ b/src/Artemis.UI/ArtemisBootstrapper.cs @@ -12,6 +12,7 @@ using Artemis.UI.Shared.DryIoc; using Artemis.UI.Shared.Services; using Artemis.VisualScripting.DryIoc; using Artemis.WebClient.Updating.DryIoc; +using Artemis.WebClient.Workshop.DryIoc; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -45,6 +46,7 @@ public static class ArtemisBootstrapper _container.RegisterUI(); _container.RegisterSharedUI(); _container.RegisterUpdatingClient(); + _container.RegisterWorkshopClient(); _container.RegisterNoStringEvaluating(); configureServices?.Invoke(_container); diff --git a/src/Artemis.UI/Screens/Workshop/WorkshopView.axaml b/src/Artemis.UI/Screens/Workshop/WorkshopView.axaml index 4cd4ef63f..485e308ca 100644 --- a/src/Artemis.UI/Screens/Workshop/WorkshopView.axaml +++ b/src/Artemis.UI/Screens/Workshop/WorkshopView.axaml @@ -10,62 +10,26 @@ xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" xmlns:gradientPicker="clr-namespace:Artemis.UI.Shared.Controls.GradientPicker;assembly=Artemis.UI.Shared" xmlns:materialIconPicker="clr-namespace:Artemis.UI.Shared.MaterialIconPicker;assembly=Artemis.UI.Shared" + xmlns:workshop1="clr-namespace:Artemis.WebClient.Workshop;assembly=Artemis.WebClient.Workshop" mc:Ignorable="d" d:DesignWidth="800" x:Class="Artemis.UI.Screens.Workshop.WorkshopView" x:DataType="workshop:WorkshopViewModel"> - - - - - Notification tests - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs b/src/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs index 32df9585c..44fbb4226 100644 --- a/src/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs @@ -1,70 +1,52 @@ -using System.Reactive; +using System; +using System.Collections.ObjectModel; +using System.Reactive; using System.Reactive.Linq; +using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.Builders; +using Artemis.WebClient.Workshop; using Avalonia.Input; using ReactiveUI; using SkiaSharp; +using StrawberryShake; namespace Artemis.UI.Screens.Workshop; public class WorkshopViewModel : MainScreenViewModel { - private readonly ObservableAsPropertyHelper _cursor; - private readonly INotificationService _notificationService; + private readonly IWorkshopClient _workshopClient; - private ColorGradient _colorGradient = new() + public WorkshopViewModel(IScreen hostScreen, IWorkshopClient workshopClient) : base(hostScreen, "workshop") { - new ColorGradientStop(new SKColor(0xFFFF6D00), 0f), - new ColorGradientStop(new SKColor(0xFFFE6806), 0.2f), - new ColorGradientStop(new SKColor(0xFFEF1788), 0.4f), - new ColorGradientStop(new SKColor(0xFFEF1788), 0.6f), - new ColorGradientStop(new SKColor(0xFF00FCCC), 0.8f), - new ColorGradientStop(new SKColor(0xFF00FCCC), 1f) - }; - - private StandardCursorType _selectedCursor; - private double _testValue; - - public WorkshopViewModel(IScreen hostScreen, INotificationService notificationService) : base(hostScreen, "workshop") - { - _notificationService = notificationService; - _cursor = this.WhenAnyValue(vm => vm.SelectedCursor).Select(c => new Cursor(c)).ToProperty(this, vm => vm.Cursor); - + _workshopClient = workshopClient; DisplayName = "Workshop"; - ShowNotification = ReactiveCommand.Create(ExecuteShowNotification); + + Task.Run(() => GetEntries()); } - public ReactiveCommand ShowNotification { get; set; } + public ObservableCollection Test { get; set; } = new(); - public StandardCursorType SelectedCursor + private async Task GetEntries() { - get => _selectedCursor; - set => RaiseAndSetIfChanged(ref _selectedCursor, value); - } - public Cursor Cursor => _cursor.Value; - - public ColorGradient ColorGradient - { - get => _colorGradient; - set => RaiseAndSetIfChanged(ref _colorGradient, value); - } - - public double TestValue - { - get => _testValue; - set => RaiseAndSetIfChanged(ref _testValue, value); - } - - public void CreateRandomGradient() - { - ColorGradient = ColorGradient.GetRandom(6); - } - - private void ExecuteShowNotification(NotificationSeverity severity) - { - _notificationService.CreateNotification().WithTitle("Test title").WithMessage("Test message").WithSeverity(severity).Show(); + try + { + IOperationResult entries = await _workshopClient.GetEntries.ExecuteAsync(); + if (entries.Data?.Entries?.Nodes == null) + return; + + foreach (IGetEntries_Entries_Nodes getEntriesEntriesNodes in entries.Data.Entries.Nodes) + { + Console.WriteLine(getEntriesEntriesNodes); + Test.Add(getEntriesEntriesNodes); + } + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/.graphqlconfig b/src/Artemis.WebClient.Updating/.graphqlconfig deleted file mode 100644 index 727ec86a0..000000000 --- a/src/Artemis.WebClient.Updating/.graphqlconfig +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "Untitled GraphQL Schema", - "schemaPath": "schema.graphql", - "extensions": { - "endpoints": { - "Default GraphQL Endpoint": { - "url": "https://updating.artemis-rgb.com/graphql", - "headers": { - "user-agent": "JS GraphQL" - }, - "introspect": true - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/Artemis.WebClient.Updating.csproj b/src/Artemis.WebClient.Updating/Artemis.WebClient.Updating.csproj index c31da3c25..0b6943ddc 100644 --- a/src/Artemis.WebClient.Updating/Artemis.WebClient.Updating.csproj +++ b/src/Artemis.WebClient.Updating/Artemis.WebClient.Updating.csproj @@ -13,4 +13,8 @@ + + + + diff --git a/src/Artemis.WebClient.Updating/graphql.config.yml b/src/Artemis.WebClient.Updating/graphql.config.yml new file mode 100644 index 000000000..784e49e2e --- /dev/null +++ b/src/Artemis.WebClient.Updating/graphql.config.yml @@ -0,0 +1,8 @@ +schema: schema.graphql +extensions: + endpoints: + Default GraphQL Endpoint: + url: https://updating.artemis-rgb.com/graphql + headers: + user-agent: JS GraphQL + introspect: true diff --git a/src/Artemis.WebClient.Updating/schema.extensions.graphql b/src/Artemis.WebClient.Updating/schema.extensions.graphql deleted file mode 100644 index 0b5fbd98b..000000000 --- a/src/Artemis.WebClient.Updating/schema.extensions.graphql +++ /dev/null @@ -1,13 +0,0 @@ -scalar _KeyFieldSet - -directive @key(fields: _KeyFieldSet!) on SCHEMA | OBJECT - -directive @serializationType(name: String!) on SCALAR - -directive @runtimeType(name: String!) on SCALAR - -directive @enumValue(value: String!) on ENUM_VALUE - -directive @rename(name: String!) on INPUT_FIELD_DEFINITION | INPUT_OBJECT | ENUM | ENUM_VALUE - -extend schema @key(fields: "id") \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/schema.graphql b/src/Artemis.WebClient.Updating/schema.graphql index fe855cb6c..c0973062b 100644 --- a/src/Artemis.WebClient.Updating/schema.graphql +++ b/src/Artemis.WebClient.Updating/schema.graphql @@ -1,4 +1,4 @@ -# This file was generated based on ".graphqlconfig". Do not edit manually. +# This file was generated. Do not edit manually. schema { query: Query @@ -107,10 +107,11 @@ type Release { type ReleaseStatistic { count: Int! + date: Date! lastReportedUsage: DateTime! linuxCount: Int! osxCount: Int! - releaseId: UUID! + release: Release windowsCount: Int! } @@ -144,6 +145,9 @@ enum SortEnumType { DESC } +"The `Date` scalar represents an ISO-8601 compliant date type." +scalar Date + "The `DateTime` scalar represents an ISO-8601 compliant date time type." scalar DateTime @@ -176,6 +180,21 @@ input BooleanOperationFilterInput { neq: Boolean } +input DateOperationFilterInput { + eq: Date + gt: Date + gte: Date + in: [Date] + lt: Date + lte: Date + neq: Date + ngt: Date + ngte: Date + nin: [Date] + nlt: Date + nlte: Date +} + input DateTimeOperationFilterInput { eq: DateTime gt: DateTime @@ -265,20 +284,22 @@ input ReleaseSortInput { input ReleaseStatisticFilterInput { and: [ReleaseStatisticFilterInput!] count: IntOperationFilterInput + date: DateOperationFilterInput lastReportedUsage: DateTimeOperationFilterInput linuxCount: IntOperationFilterInput or: [ReleaseStatisticFilterInput!] osxCount: IntOperationFilterInput - releaseId: UuidOperationFilterInput + release: ReleaseFilterInput windowsCount: IntOperationFilterInput } input ReleaseStatisticSortInput { count: SortEnumType + date: SortEnumType lastReportedUsage: SortEnumType linuxCount: SortEnumType osxCount: SortEnumType - releaseId: SortEnumType + release: ReleaseSortInput windowsCount: SortEnumType } diff --git a/src/Artemis.WebClient.Workshop/.config/dotnet-tools.json b/src/Artemis.WebClient.Workshop/.config/dotnet-tools.json new file mode 100644 index 000000000..7d8626c03 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "strawberryshake.tools": { + "version": "13.0.0-rc.4", + "commands": [ + "dotnet-graphql" + ] + } + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/.graphqlrc.json b/src/Artemis.WebClient.Workshop/.graphqlrc.json new file mode 100644 index 000000000..24fb39de3 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/.graphqlrc.json @@ -0,0 +1,22 @@ +{ + "schema": "schema.graphql", + "documents": "**/*.graphql", + "extensions": { + "strawberryShake": { + "name": "WorkshopClient", + "namespace": "Artemis.WebClient.Workshop", + "url": "https://localhost:7281/graphql/", + "emitGeneratedCode": false, + "records": { + "inputs": false, + "entities": false + }, + "transportProfiles": [ + { + "default": "Http", + "subscription": "WebSocket" + } + ] + } + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj new file mode 100644 index 000000000..0b6943ddc --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj @@ -0,0 +1,20 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs b/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs new file mode 100644 index 000000000..aed1df5a7 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/DryIoc/ContainerExtensions.cs @@ -0,0 +1,26 @@ +using DryIoc; +using DryIoc.Microsoft.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace Artemis.WebClient.Workshop.DryIoc; + +/// +/// Provides an extension method to register services onto a DryIoc . +/// +public static class ContainerExtensions +{ + /// + /// Registers the updating client into the container. + /// + /// The builder building the current container + public static void RegisterWorkshopClient(this IContainer container) + { + ServiceCollection serviceCollection = new(); + serviceCollection + .AddHttpClient() + .AddWorkshopClient() + .ConfigureHttpClient(client => client.BaseAddress = new Uri("https://localhost:7281/graphql")); + + container.WithDependencyInjectionAdapter(serviceCollection); + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql new file mode 100644 index 000000000..9d9457e62 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql @@ -0,0 +1,9 @@ +query GetEntries { + entries { + nodes { + author + name + entryType + } + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Queries/GetEntryById.graphql b/src/Artemis.WebClient.Workshop/Queries/GetEntryById.graphql new file mode 100644 index 000000000..e5ba4a96d --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Queries/GetEntryById.graphql @@ -0,0 +1,7 @@ +query GetEntryById($id: UUID!) { + entry(id: $id) { + author + name + entryType + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/graphql.config.yml b/src/Artemis.WebClient.Workshop/graphql.config.yml new file mode 100644 index 000000000..a8ba99703 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/graphql.config.yml @@ -0,0 +1,8 @@ +schema: schema.graphql +extensions: + endpoints: + Default GraphQL Endpoint: + url: https://localhost:7281/graphql + headers: + user-agent: JS GraphQL + introspect: true diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql new file mode 100644 index 000000000..daa53bde8 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/schema.graphql @@ -0,0 +1,244 @@ +# This file was generated. Do not edit manually. + +schema { + query: Query + mutation: Mutation +} + +"A connection to a list of items." +type EntriesConnection { + "A list of edges." + edges: [EntriesEdge!] + "A flattened list of the nodes." + nodes: [Entry!] + "Information to aid in pagination." + pageInfo: PageInfo! + "Identifies the total count of items in the connection." + totalCount: Int! +} + +"An edge in a connection." +type EntriesEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Entry! +} + +type Entry { + author: UUID! + description: String! + entryType: EntryType! + icon: Image + id: UUID! + images: [Image!]! + name: String! + releases: [Release!]! + tags: [String!]! +} + +type Image { + id: UUID! + mimeType: String! +} + +type Mutation { + addEntry(input: EntryInput!): Entry +} + +"Information about pagination in a connection." +type PageInfo { + "When paginating forwards, the cursor to continue." + endCursor: String + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! + "When paginating backwards, the cursor to continue." + startCursor: String +} + +type Query { + entries( + "Returns the elements in the list that come after the specified cursor." + after: String, + "Returns the elements in the list that come before the specified cursor." + before: String, + "Returns the first _n_ elements from the list." + first: Int, + "Returns the last _n_ elements from the list." + last: Int, + order: [EntrySortInput!], + where: EntryFilterInput + ): EntriesConnection + entry(id: UUID!): Entry +} + +type Release { + createdAt: DateTime! + downloadSize: Long! + downloads: Long! + id: UUID! + md5Hash: String + version: String! +} + +enum EntryType { + LAYOUT + PLUGIN + PROFILE +} + +enum SortEnumType { + ASC + DESC +} + +"The `DateTime` scalar represents an ISO-8601 compliant date time type." +scalar DateTime + +"The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." +scalar Long + +scalar UUID + +input DateTimeOperationFilterInput { + eq: DateTime + gt: DateTime + gte: DateTime + in: [DateTime] + lt: DateTime + lte: DateTime + neq: DateTime + ngt: DateTime + ngte: DateTime + nin: [DateTime] + nlt: DateTime + nlte: DateTime +} + +input EntryFilterInput { + and: [EntryFilterInput!] + author: UuidOperationFilterInput + description: StringOperationFilterInput + entryType: EntryTypeOperationFilterInput + icon: ImageFilterInput + id: UuidOperationFilterInput + images: ListFilterInputTypeOfImageFilterInput + name: StringOperationFilterInput + or: [EntryFilterInput!] + releases: ListFilterInputTypeOfReleaseFilterInput + tags: ListStringOperationFilterInput +} + +input EntryInput { + description: String! + entryType: EntryType! + name: String! + tags: [String!]! +} + +input EntrySortInput { + author: SortEnumType + description: SortEnumType + entryType: SortEnumType + icon: ImageSortInput + id: SortEnumType + name: SortEnumType +} + +input EntryTypeOperationFilterInput { + eq: EntryType + in: [EntryType!] + neq: EntryType + nin: [EntryType!] +} + +input ImageFilterInput { + and: [ImageFilterInput!] + id: UuidOperationFilterInput + mimeType: StringOperationFilterInput + or: [ImageFilterInput!] +} + +input ImageSortInput { + id: SortEnumType + mimeType: SortEnumType +} + +input ListFilterInputTypeOfImageFilterInput { + all: ImageFilterInput + any: Boolean + none: ImageFilterInput + some: ImageFilterInput +} + +input ListFilterInputTypeOfReleaseFilterInput { + all: ReleaseFilterInput + any: Boolean + none: ReleaseFilterInput + some: ReleaseFilterInput +} + +input ListStringOperationFilterInput { + all: StringOperationFilterInput + any: Boolean + none: StringOperationFilterInput + some: StringOperationFilterInput +} + +input LongOperationFilterInput { + eq: Long + gt: Long + gte: Long + in: [Long] + lt: Long + lte: Long + neq: Long + ngt: Long + ngte: Long + nin: [Long] + nlt: Long + nlte: Long +} + +input ReleaseFilterInput { + and: [ReleaseFilterInput!] + createdAt: DateTimeOperationFilterInput + downloadSize: LongOperationFilterInput + downloads: LongOperationFilterInput + id: UuidOperationFilterInput + md5Hash: StringOperationFilterInput + or: [ReleaseFilterInput!] + version: StringOperationFilterInput +} + +input StringOperationFilterInput { + and: [StringOperationFilterInput!] + contains: String + endsWith: String + eq: String + in: [String] + ncontains: String + nendsWith: String + neq: String + nin: [String] + nstartsWith: String + or: [StringOperationFilterInput!] + startsWith: String +} + +input UuidOperationFilterInput { + eq: UUID + gt: UUID + gte: UUID + in: [UUID] + lt: UUID + lte: UUID + neq: UUID + ngt: UUID + ngte: UUID + nin: [UUID] + nlt: UUID + nlte: UUID +} diff --git a/src/Artemis.sln b/src/Artemis.sln index 2e53b5d9a..8c5b6ee51 100644 --- a/src/Artemis.sln +++ b/src/Artemis.sln @@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.VisualScripting", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.WebClient.Updating", "Artemis.WebClient.Updating\Artemis.WebClient.Updating.csproj", "{7C8C6F50-0CC8-45B3-B608-A7218C005E4B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.WebClient.Workshop", "Artemis.WebClient.Workshop\Artemis.WebClient.Workshop.csproj", "{2B982C2E-3CBC-4DAB-9167-CCCA4C78E92B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -63,6 +65,10 @@ Global {7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Debug|x64.Build.0 = Debug|Any CPU {7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Release|x64.ActiveCfg = Release|Any CPU {7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Release|x64.Build.0 = Release|Any CPU + {2B982C2E-3CBC-4DAB-9167-CCCA4C78E92B}.Debug|x64.ActiveCfg = Debug|Any CPU + {2B982C2E-3CBC-4DAB-9167-CCCA4C78E92B}.Debug|x64.Build.0 = Debug|Any CPU + {2B982C2E-3CBC-4DAB-9167-CCCA4C78E92B}.Release|x64.ActiveCfg = Release|Any CPU + {2B982C2E-3CBC-4DAB-9167-CCCA4C78E92B}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE