Skip to content

Commit

Permalink
Plugin store full search and uninstall functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
KimihikoAkayasaki committed Mar 12, 2023
1 parent ce5f7c0 commit aee21e1
Show file tree
Hide file tree
Showing 8 changed files with 836 additions and 250 deletions.
3 changes: 2 additions & 1 deletion Amethyst/Amethyst.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@

<ItemGroup>
<PackageReference Include="CommunityToolkit.WinUI" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls.Markdown" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Media" Version="7.1.2" />
<PackageReference Include="Microsoft.AppCenter.Analytics" Version="5.0.1" />
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="5.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.230217.4" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.25231-preview" />
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.0.1" />
Expand Down
5 changes: 4 additions & 1 deletion Amethyst/Assets/Strings/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -315,12 +315,15 @@
"/SharedStrings/PluginUpdates/Statuses/Downloading": "Downloading {0} v{1}...",
"/SharedStrings/PluginUpdates/Statuses/Error": "Error updating {0}!",

"/PluginStore/Titles/Install": "Install {0} {1}",
"/PluginStore/Titles/Installing/Setup": "Installing {0}...",
"/PluginStore/Titles/Uninstalling/Notice": "Enqueued {0} removal!",
"/PluginStore/Titles/Installing/Error": "Error installing {0}!",
"/PluginStore/Titles/Installing/Success": "Installed {0}!",
"/PluginStore/Captions/Uninstalling/Notice": "Amethyst should delete it during the next startup.",
"/PluginStore/Captions/Installing/Downloading": "Downloading the plugin package...",
"/PluginStore/Captions/Installing/Error": "Couldn't install the plugin due to an error. Please try again later.",
"/PluginStore/Captions/Installing/Success": "Plugin installed! Amethyst should load it the next time it opens.",
"/PluginStore/Captions/Installing/Success": "Plugin installed! Amethyst should load it during the next startup.",

"/CrashHandler/Content/AlreadyRunning": "Looks like the app is already running\nand you've tried to launch a second instance,\nthis action is not currently supported.\n\nPlease check if the app isn't opened.\nIf problem persists, press the 'Force Exit' button.",
"/CrashHandler/Content/Crash/NoPlugins": "There were no appropriate plugins available to load and use in Amethyst.\n\nPlease check if you have all dependencies installed, like proper Service or Framework Runtime and other dependency libraries needed by your plugins.",
Expand Down
35 changes: 29 additions & 6 deletions Amethyst/MVVM/PluginHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,18 @@
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.System;
using Amethyst.Classes;
using Amethyst.Plugins.Contract;
using Amethyst.Schedulers;
using Amethyst.Utils;
using AmethystSupport;
using Microsoft.AppCenter.Crashes;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using RestSharp;
using static Amethyst.Classes.Interfacing;
using Windows.Media.Protection.PlayReady;
using Windows.Storage;
using Amethyst.Schedulers;

namespace Amethyst.MVVM;

Expand Down Expand Up @@ -266,8 +265,8 @@ public void RequestExit(string message, bool fatal = false)

public class LoadAttemptedPlugin : INotifyPropertyChanged
{
private bool _updateEnqueued;
private RestClient _githubClient;
private bool _updateEnqueued;

private RestClient GithubClient => _githubClient ??= new RestClient(
UpdateEndpoint ?? $"{Website?.TrimEnd('/')}/releases/download/latest/");
Expand Down Expand Up @@ -332,8 +331,8 @@ public bool IsLoaded
loadedDeviceSet.Add("K2VRTEAM-AME2-APII-DVCE-DVCEKINECTV2");
if (AppPlugins.TrackingDevicesList.ContainsKey("K2VRTEAM-AME2-APII-DVCE-DVCEPSMOVEEX"))
loadedDeviceSet.Add("K2VRTEAM-AME2-APII-DVCE-DVCEPSMOVEEX");
if (AppPlugins.TrackingDevicesList.ContainsKey("K2VRTEAM-VEND-API1-DVCE-DVCEOWOTRACK"))
loadedDeviceSet.Add("K2VRTEAM-VEND-API1-DVCE-DVCEOWOTRACK");
if (AppPlugins.TrackingDevicesList.ContainsKey("K2VRTEAM-AME2-APII-DVCE-DVCEOWOTRACK"))
loadedDeviceSet.Add("K2VRTEAM-AME2-APII-DVCE-DVCEOWOTRACK");

// If we've just disabled the last loaded device, re-enable the first
if (AppPlugins.TrackingDevicesList.Keys.All(
Expand Down Expand Up @@ -397,6 +396,16 @@ public bool IsLoaded
public bool LocationValid => !string.IsNullOrEmpty(Folder);
public bool GuidValid => !string.IsNullOrEmpty(Guid) && Guid is not "[INVALID]" or "INVALID";
public bool ErrorValid => !string.IsNullOrEmpty(Error);
public bool Uninstalling { get; set; }

public bool CanUninstall =>
!Uninstalling && LocationValid && GuidValid && Guid
is not "K2VRTEAM-AME2-APII-DVCE-DVCEKINECTV1"
and not "K2VRTEAM-AME2-APII-DVCE-DVCEKINECTV2"
and not "K2VRTEAM-AME2-APII-DVCE-DVCEPSMOVEEX"
and not "K2VRTEAM-AME2-APII-DVCE-DVCEOWOTRACK"
and not "K2VRTEAM-AME2-APII-SNDP-SENDPTOPENVR"
and not "K2VRTEAM-AME2-APII-SNDP-SENDPTVRCOSC";

public event PropertyChangedEventHandler PropertyChanged;

Expand Down Expand Up @@ -604,6 +613,20 @@ public async Task<bool> ExecuteUpdates()
}
}

public void EnqueuePluginUninstall()
{
// Enqueue a delete startup action to uninstall this plugin
StartupController.Controller.StartupTasks.Add(new StartupDeleteTask
{
Name = $"Delete plugin {Name} v{Version}",
PluginFolder = Folder
});

// Show a badge that this plugin will be uninstalled
Uninstalling = true;
OnPropertyChanged();
}

public string TrimString(string s, int l)
{
return s?[..Math.Min(s.Length, l)] +
Expand Down
184 changes: 177 additions & 7 deletions Amethyst/MVVM/StorePlugin.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,52 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using Windows.System;
using Amethyst.Classes;
using Microsoft.UI.Xaml.Controls;
using Amethyst.Utils;
using RestSharp;
using Newtonsoft.Json.Linq;
using Microsoft.UI.Xaml;
using System.Net.Http;
using System.Numerics;
using Amethyst.Schedulers;
using System.Threading.Tasks;

namespace Amethyst.MVVM;

public class StorePlugin : INotifyPropertyChanged
{
private RestClient ApiClient { get; } = new("https://api.github.com");
private RestClient GithubClient { get; } = new("https://github.com");

public string Name { get; set; }
public bool Official { get; set; } = false;

public bool Installing { get; set; } = false;
public bool LoadingData { get; set; } = true;
public bool FinishedLoadingData { get; set; } = false;
public bool Uninstalling => InstalledPlugin?.Uninstalling ?? false;
public bool InstallSuccess { get; set; } = false;
public bool InstallError { get; set; } = false;
public bool PluginExpanderExpanded { get; set; } = false;

public IEnumerable<Contributor> Contributors { get; set; }
public IEnumerable<StorePluginContributor> Contributors { get; set; }
public PluginRepository Repository { get; set; }
public PluginRelease LatestRelease { get; set; }

public bool WebsiteValid => !string.IsNullOrEmpty(Repository.Url);
public bool DescriptionValid => !string.IsNullOrEmpty(Repository.Description);
public bool HasRelease => LatestRelease is not null && !string.IsNullOrEmpty(LatestRelease.Download);
public bool NoReleases => !HasRelease;

public bool CanUninstall => InstalledPlugin?.CanUninstall ?? false;
public bool CanInstall => HasRelease && InstalledPlugin is null && !Uninstalling;

private LoadAttemptedPlugin InstalledPlugin => AppPlugins.LoadAttemptedPluginsList.FirstOrDefault(x =>
x.Website == Repository?.Url || (x.GuidValid && x.Guid == LatestRelease?.Guid), null);

public event PropertyChangedEventHandler PropertyChanged;

// MVVM stuff
Expand All @@ -43,28 +71,170 @@ public string FormatResourceString(string resourceName)
return string.Format(Interfacing.LocalizedJsonString(resourceName), Name);
}

public string FormatReleaseString(string resourceName)
{
return string.Format(Interfacing.LocalizedJsonString(resourceName),
LatestRelease?.DisplayName, LatestRelease?.Version);
}

public async void OpenPluginWebsite()
{
try
{
await Launcher.LaunchUriAsync(new Uri(Repository.Url));
}
catch (Exception)
{
// ignored
}
}

public void SchedulePluginUninstall()
{
// Enqueue a delete startup action to uninstall this plugin
InstalledPlugin?.EnqueuePluginUninstall();
OnPropertyChanged(); // Refresh everything
}

public async void InstallPlugin()
{
// TODO download action etc
OnPropertyChanged(); // Refresh everything
}

public async void FetchPluginData()
{
try
{
// Mark as loading and trigger a partial refresh
LoadingData = true;
FinishedLoadingData = false;
OnPropertyChanged("LoadingData");
OnPropertyChanged("FinishedLoadingData");

// Fetch contributor details
var contributorsResponse = await ApiClient.GetAsyncAuthorized(
new RestRequest($"repos/{Repository.FullName}/contributors"));
if (!contributorsResponse.IsSuccessStatusCode || contributorsResponse.Content is null)
throw new Exception("Contributors request failed!");

var contributorsResult = JObject.Parse($"{{\"items\":{contributorsResponse.Content}}}");
var contributors = contributorsResult["items"]?.Children().ToList()
?? throw new Exception("Contributors were invalid!");

// Add contributors to the plugin data
Contributors = contributors.Select(x => new StorePluginContributor
{
Name = x["login"]?.ToString() ?? string.Empty,
Avatar = new Uri(x["avatar_url"]?.ToString() ?? string.Empty),
Url = new Uri(x["html_url"]?.ToString() ?? string.Empty)
});

// Fetch release details
var releasesResponse = await ApiClient.GetAsyncAuthorized(
new RestRequest($"repos/{Repository.FullName}/releases"));
if (!releasesResponse.IsSuccessStatusCode || releasesResponse.Content is null)
throw new Exception("Releases request failed!");

var releasesResult = JObject.Parse($"{{\"items\":{releasesResponse.Content}}}");
var release = releasesResult["items"]?.Children().FirstOrDefault(defaultValue: null)
?? throw new Exception("Releases were invalid!");

// Fetch manifest details and content
var manifestUrl = release["assets"]?.Children()
.FirstOrDefault(x => x["name"]?.ToString() == "manifest.json", null)?["browser_download_url"]
?.ToString() ?? throw new Exception("No manifest found!");

var manifestResponse = await GithubClient.GetAsync(
new RestRequest(manifestUrl.Replace("https://github.com/", "")));
if (!manifestResponse.IsSuccessStatusCode || manifestResponse.Content is null)
throw new Exception("Manifest request failed!");

var manifestResult = manifestResponse.Content.TryParseJson(out var manifest);
if (!manifestResult || manifest is null) throw new Exception("The manifest was invalid!");

// Add everything to the plugin data
LatestRelease = new PluginRelease
{
Title = release["name"]?.ToString() ?? Repository.Name,
Date = DateTime.Parse(release["published_at"]?.ToString() ?? string.Empty).ToLocalTime(),
Description = release["body"]?.ToString() ?? Repository.Description,

Guid = manifest["guid"]?.ToString(),
Version = manifest["version"]?.ToString(),
Changelog = manifest["changelog"]?.ToString(),
DisplayName = manifest["display_name"]?.ToString(),

Download = release["assets"]?.Children().FirstOrDefault(
x => x["name"]?.ToString().EndsWith(".zip") ?? false, null)?
["browser_download_url"]?.ToString()
};
}
catch (HttpRequestException e)
{
// API rate exceeded, show the authorization toast
if (e.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.Unauthorized)
Pages.Plugins.RequestShowRateExceededEvent(this, EventArgs.Empty);
}
catch (Exception e)
{
// Show a 'fetch failed' error view
LatestRelease = null;
Logger.Error(e);
}

// Refresh everything else
OnPropertyChanged();

// Wait a bit and show the rest
await Task.Delay(500);

// Unblock the resource controls
LoadingData = false;
FinishedLoadingData = true;

OnPropertyChanged("LoadingData");
OnPropertyChanged("FinishedLoadingData");
}

public class PluginRepository
{
public string Name { get; set; }
public string FullName { get; set; }
public string Owner { get; set; }
public string Description { get; set; }
public string Url { get; set; }
}

public class PluginRelease
{
public string Name { get; set; }
public string Title { get; set; }
public string DisplayName { get; set; }
public string Version { get; set; }
public DateTime Date { get; set; }
public string Description { get; set; }
public string Changelog { get; set; }
public string Download { get; set; }
public string Guid { get; set; }
}
}

public class Contributor
public class StorePluginContributor
{
public string Name { get; set; }
public Uri Avatar { get; set; }
public Uri Url { get; set; }

// MVVM stuff
public async void OpenContributorWebsite()
{
public string Name { get; set; }
public Uri Avatar { get; set; }
public Uri Url { get; set; }
try
{
await Launcher.LaunchUriAsync(Url);
}
catch (Exception e)
{
Logger.Warn(e);
}
}
}
25 changes: 25 additions & 0 deletions Amethyst/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,31 @@ public MainWindow()
Logger.Error($"Reading the startup scheduler configuration failed. Message: {e.Message}");
}

// Execute plugin uninstalls: delete plugin files
foreach (var action in StartupController.Controller.DeleteTasks.ToList())
try
{
Logger.Info($"Parsing a startup {action.GetType()} task with name \"{action.Name}\"...");
if (!Directory.Exists(action.PluginFolder) ||
action.PluginFolder == Interfacing.ProgramLocation.DirectoryName || Directory
.EnumerateFiles(action.PluginFolder)
.Any(x => x == Interfacing.ProgramLocation.FullName)) continue;

Logger.Info("Cleaning the plugin folder now...");
Directory.Delete(action.PluginFolder, true);

Logger.Info("Deleting attempted scheduled startup " +
$"{action.GetType()} task with name \"{action.Name}\"...");

StartupController.Controller.StartupTasks.Remove(action);
Logger.Info($"Looks like a startup {action.GetType()} task with " +
$"name \"{action.Name}\" has been executed successfully!");
}
catch (Exception e)
{
Logger.Warn(e);
}

// Execute plugin updates: replace plugin files
foreach (var action in StartupController.Controller.UpdateTasks.ToList())
try
Expand Down
Loading

0 comments on commit aee21e1

Please sign in to comment.