Implement plugin browser and downloader

This commit is contained in:
Brant Martin
2019-03-02 11:42:08 -05:00
parent 295bbd62b8
commit 651865f28a
9 changed files with 405 additions and 2 deletions

View File

@@ -39,6 +39,10 @@
<HintPath>..\GameBinaries\HavokWrapper.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
@@ -189,6 +193,7 @@
<Compile Include="Session\ITorchSessionManager.cs" />
<Compile Include="Session\TorchSessionState.cs" />
<Compile Include="TorchGameState.cs" />
<Compile Include="WebAPI\PluginQuery.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NLog;
namespace Torch.API.WebAPI
{
public class PluginQuery
{
private const string ALL_QUERY = "https://torchapi.net/api/plugins";
private const string PLUGIN_QUERY = "https://torchapi.net/api/plugins/{0}";
private readonly HttpClient _client;
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private static PluginQuery _instance;
public static PluginQuery Instance => _instance ?? (_instance = new PluginQuery());
private PluginQuery()
{
_client = new HttpClient();
}
public async Task<PluginResponse> QueryAll()
{
var h = await _client.GetAsync(ALL_QUERY);
if (!h.IsSuccessStatusCode)
{
Log.Error($"Plugin query returned response {h.StatusCode}");
return null;
}
var r = await h.Content.ReadAsStringAsync();
PluginResponse response;
try
{
response = JsonConvert.DeserializeObject<PluginResponse>(r);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to deserialize plugin query response!");
return null;
}
return response;
}
public async Task<PluginFullItem> QueryOne(Guid guid)
{
return await QueryOne(guid.ToString());
}
public async Task<PluginFullItem> QueryOne(string guid)
{
var h = await _client.GetAsync(string.Format(PLUGIN_QUERY, guid));
if (!h.IsSuccessStatusCode)
{
Log.Error($"Plugin query returned response {h.StatusCode}");
return null;
}
var r = await h.Content.ReadAsStringAsync();
PluginFullItem response;
try
{
response = JsonConvert.DeserializeObject<PluginFullItem>(r);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to deserialize plugin query response!");
return null;
}
return response;
}
public async Task<bool> DownloadPlugin(Guid guid)
{
return await DownloadPlugin(guid.ToString());
}
public async Task<bool> DownloadPlugin(string guid)
{
var item = await QueryOne(guid);
return await DownloadPlugin(item);
}
public async Task<bool> DownloadPlugin(PluginFullItem item)
{
try
{
var h = await _client.GetAsync(string.Format(PLUGIN_QUERY, item.ID));
string res = await h.Content.ReadAsStringAsync();
var response = JsonConvert.DeserializeObject<PluginFullItem>(res);
if (response.Versions.Length == 0)
{
Log.Error($"Selected plugin {item.Name} does not have any versions to download!");
return false;
}
var version = response.Versions.FirstOrDefault(v => v.Version == response.LatestVersion);
if (version == null)
{
Log.Error($"Could not find latest version for selected plugin {item.Name}");
return false;
}
var s = await _client.GetStreamAsync(version.URL);
using (var f = new FileStream($"Plugins\\{item.Name}.zip", FileMode.Create))
{
await s.CopyToAsync(f);
await f.FlushAsync();
}
}
catch (Exception ex)
{
Log.Error(ex, "Failed to download plugin!");
}
return true;
}
}
public class PluginResponse
{
public PluginItem[] Plugins;
public int Count;
}
public class PluginItem
{
public string ID;
public string Name;
public string Author;
public string Description;
public string LatestVersion;
public override string ToString()
{
return Name;
}
}
public class PluginFullItem : PluginItem
{
public VersionItem[] Versions;
}
public class VersionItem
{
public string Version;
public string Note;
public bool IsBeta;
public string URL;
}
}

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net461" />
<package id="NLog" version="4.4.12" targetFramework="net461" />
</packages>

View File

@@ -51,7 +51,6 @@
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit>
<DocumentationFile>$(SolutionDir)\bin\x64\Release\Torch.Server.xml</DocumentationFile>
<NoWarn>1591</NoWarn>
</PropertyGroup>
<PropertyGroup>
<StartupObject>Torch.Server.Program</StartupObject>
@@ -71,6 +70,9 @@
<Reference Include="MahApps.Metro, Version=1.6.1.4, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\MahApps.Metro.1.6.1\lib\net45\MahApps.Metro.dll</HintPath>
</Reference>
<Reference Include="Markdown.Xaml, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Markdown.Xaml.1.0.0\lib\net45\Markdown.Xaml.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CodeAnalysis, Version=1.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\GameBinaries\Microsoft.CodeAnalysis.dll</HintPath>
@@ -273,6 +275,9 @@
<Compile Include="Views\ModListControl.xaml.cs">
<DependentUpon>ModListControl.xaml</DependentUpon>
</Compile>
<Compile Include="Views\PluginBrowser.xaml.cs">
<DependentUpon>PluginBrowser.xaml</DependentUpon>
</Compile>
<Compile Include="Views\ThemeControl.xaml.cs">
<DependentUpon>ThemeControl.xaml</DependentUpon>
</Compile>
@@ -437,6 +442,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\PluginBrowser.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\PluginsControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
@@ -492,6 +501,7 @@
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\TransformOnBuild.targets" />
<PropertyGroup>

View File

@@ -0,0 +1,124 @@
<Window x:Class="Torch.Server.Views.PluginBrowser"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Markdown.Xaml;assembly=Markdown.Xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:views="clr-namespace:Torch.Server.Views"
mc:Ignorable="d"
Title="PluginBrowser" Height="400" Width="600"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
<Style TargetType="FlowDocument" x:Key="DocumentStyle">
<Setter Property="FontFamily"
Value="Calibri" />
<Setter Property="TextAlignment"
Value="Left" />
</Style>
<Style x:Key="H1Style"
TargetType="Paragraph">
<Setter Property="FontSize"
Value="42" />
<Setter Property="Foreground"
Value="#ff000000" />
<Setter Property="FontWeight"
Value="Light" />
</Style>
<Style x:Key="H2Style"
TargetType="Paragraph">
<Setter Property="FontSize"
Value="20" />
<Setter Property="Foreground"
Value="#ff000000" />
<Setter Property="FontWeight"
Value="Light" />
</Style>
<Style x:Key="H3Style"
TargetType="Paragraph">
<Setter Property="FontSize"
Value="20" />
<Setter Property="Foreground"
Value="#99000000" />
<Setter Property="FontWeight"
Value="Light" />
</Style>
<Style x:Key="H4Style"
TargetType="Paragraph">
<Setter Property="FontSize"
Value="14" />
<Setter Property="Foreground"
Value="#99000000" />
<Setter Property="FontWeight"
Value="Light" />
</Style>
<Style x:Key="LinkStyle"
TargetType="Hyperlink">
<Setter Property="TextDecorations"
Value="None" />
</Style>
<Style x:Key="ImageStyle"
TargetType="Image">
<Setter Property="RenderOptions.BitmapScalingMode"
Value="NearestNeighbor" />
<Style.Triggers>
<Trigger Property="Tag"
Value="imageright">
<Setter Property="Margin"
Value="20,0,0,0" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="SeparatorStyle"
TargetType="Line">
<Setter Property="X2"
Value="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=FlowDocumentScrollViewer}}" />
<Setter Property="Stroke"
Value="#99000000" />
<Setter Property="StrokeThickness"
Value="2" />
</Style>
<local:Markdown x:Key="Markdown"
DocumentStyle="{StaticResource DocumentStyle}"
Heading1Style="{StaticResource H1Style}"
Heading2Style="{StaticResource H2Style}"
Heading3Style="{StaticResource H3Style}"
Heading4Style="{StaticResource H4Style}"
LinkStyle="{StaticResource LinkStyle}"
ImageStyle="{StaticResource ImageStyle}"
SeparatorStyle="{StaticResource SeparatorStyle}"
AssetPathRoot="{x:Static system:Environment.CurrentDirectory}"/>
<local:TextToFlowDocumentConverter x:Key="TextToFlowDocumentConverter"
Markdown="{StaticResource Markdown}"/>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView Name="PluginsList" Width="150" Height="Auto" Margin="3" ItemsSource="{Binding Plugins}" SelectionChanged="PluginsList_SelectionChanged">
</ListView>
<Button Name="DownloadButton" Grid.Row ="1" Content="Download" Margin="3" Height="30" Click="DownloadButton_OnClick" IsEnabled="False"/>
</Grid>
<FlowDocumentScrollViewer Grid.Column="1" Name="MarkdownFlow" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="3" Document="{Binding CurrentDescription, Converter={StaticResource TextToFlowDocumentConverter}}"/>
</Grid>
</Window>

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Newtonsoft.Json;
using NLog;
using Torch.API.WebAPI;
using Torch.Collections;
using Torch.Server.Annotations;
namespace Torch.Server.Views
{
/// <summary>
/// Interaction logic for PluginBrowser.xaml
/// </summary>
public partial class PluginBrowser : Window, INotifyPropertyChanged
{
private static Logger Log = LogManager.GetCurrentClassLogger();
public MtObservableList<PluginItem> Plugins { get; set; } = new MtObservableList<PluginItem>();
public PluginItem CurrentItem { get; set; }
private string _description = "Loading data from server, please wait..";
public string CurrentDescription
{
get { return _description; }
set
{
_description = value;
OnPropertyChanged();
}
}
public PluginBrowser()
{
InitializeComponent();
Task.Run(async () =>
{
var res = await PluginQuery.Instance.QueryAll();
if (res == null)
return;
foreach(var item in res.Plugins)
Plugins.Add(item);
});
}
private void PluginsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
CurrentItem = (PluginItem)PluginsList.SelectedItem;
CurrentDescription = CurrentItem.Description;
DownloadButton.IsEnabled = !string.IsNullOrEmpty(CurrentItem.LatestVersion);
}
private void DownloadButton_OnClick(object sender, RoutedEventArgs e)
{
var item = CurrentItem;
Task.Run(async () =>
{
var result = await PluginQuery.Instance.DownloadPlugin(item.ID);
MessageBox.Show(result ? "Plugin downloaded successfully! Please restart the server to load changes."
: "Plugin failed to download! See log for details.",
"Plugin Downloader",
MessageBoxButton.OK);
});
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -26,7 +26,14 @@
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Grid.Row="1" Content="Open Folder" Margin="3" DockPanel.Dock="Bottom" Click="OpenFolder_OnClick"/>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Content="Open Folder" Margin="3" Click="OpenFolder_OnClick"/>
<Button Grid.Row="0" Grid.Column="1" Content="Browse Plugins" Margin="3" Click="BrowsPlugins_OnClick"/>
</Grid>
</Grid>
<ScrollViewer Name="PScroll" Grid.Column="1" Margin="3">
<Frame NavigationUIVisibility="Hidden" Content="{Binding SelectedPlugin.Control}"/>

View File

@@ -71,5 +71,11 @@ namespace Torch.Server.Views
if (_plugins?.PluginDir != null)
Process.Start(_plugins.PluginDir);
}
private void BrowsPlugins_OnClick(object sender, RoutedEventArgs e)
{
var browser = new PluginBrowser();
browser.Show();
}
}
}

View File

@@ -2,6 +2,7 @@
<packages>
<package id="ControlzEx" version="3.0.2.4" targetFramework="net461" />
<package id="MahApps.Metro" version="1.6.1" targetFramework="net461" />
<package id="Markdown.Xaml" version="1.0.0" targetFramework="net461" />
<package id="Microsoft.Win32.Registry" version="4.4.0" targetFramework="net461" />
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net461" />