diff --git a/source/Generic/SteamViewer/Application/DiscoverSupportedFeaturesService.cs b/source/Generic/SteamViewer/Application/DiscoverSupportedFeaturesService.cs new file mode 100644 index 000000000..81da8f903 --- /dev/null +++ b/source/Generic/SteamViewer/Application/DiscoverSupportedFeaturesService.cs @@ -0,0 +1,119 @@ +using SteamShortcuts.Domain.ValueObjects; +using SteamCommon; +using System.Threading; +using System.Threading.Tasks; +using SteamShortcuts.Infrastructure; +using System.Diagnostics; +using Playnite.SDK; + +namespace SteamShortcuts.Application +{ + public class DiscoverSupportedFeaturesService + { + private class ResponseCache + { + public string steamId; + public SupportedFeatures feature; + + public ResponseCache() { } + public ResponseCache(string steamId, SupportedFeatures feature) + { + this.steamId = steamId; + this.feature = feature; + } + } + + private static readonly object _lock = new object(); + private static CancellationTokenSource _cts; + private static string _runningSteamId = null; + private static Task _runningRequestTask = Task.FromResult((ResponseCache)null); + private static ResponseCache _cache = new ResponseCache(); + + public SupportedFeatures GetCachedFeature(string steamId) + { + if (_cache is null) + { + return null; + } + + if (steamId == _cache.steamId) + { + return _cache.feature; + } + + return null; + } + + public async Task GetFeatures(string steamId) + { + Task task; + lock (_lock) + { + if (_cache != null && steamId == _cache.steamId) + { + return _cache.feature; + } + + CancellationToken cancelToken; + if (_runningRequestTask.IsCompleted || steamId != _runningSteamId) + { + _cts?.Cancel(); + _cts?.Dispose(); + _cts = new CancellationTokenSource(); + cancelToken = _cts.Token; + _runningSteamId = steamId; + _runningRequestTask = SendRequest(steamId, cancelToken); + } + + task = _runningRequestTask; + } + + return (await task)?.feature; + } + + private async Task SendRequest(string steamId, CancellationToken cancelToken) + { + var t1 = Task.Run(() => { + return SteamWeb.GetSteamAppDetails(steamId, cancelToken); + }); + + var t2 = QueryRewardItems.GetPointsShopCount(steamId, cancelToken); + + var appDetails = await t1; + var pointsShopCount = await t2; + + if (appDetails == null || pointsShopCount == null) + { + return null; + } + + var achievements = false; + var workshop = false; + + if (appDetails.success) + { + foreach (var category in appDetails.data.categories) + { + switch (category.id) + { + case 22: + achievements = true; + break; + case 30: + workshop = true; + break; + } + } + } + + _cache = new ResponseCache(steamId, new SupportedFeatures + { + Achievements = achievements, + Workshop = workshop, + PointsShop = pointsShopCount.response.total_count > 0, + }); + + return _cache; + } + } +} diff --git a/source/Generic/SteamViewer/Application/SteamUriLauncherService.cs b/source/Generic/SteamViewer/Application/SteamUriLauncherService.cs index 07f335fb6..d5e19edaa 100644 --- a/source/Generic/SteamViewer/Application/SteamUriLauncherService.cs +++ b/source/Generic/SteamViewer/Application/SteamUriLauncherService.cs @@ -27,7 +27,8 @@ public class SteamUriLauncherService { SteamUrlType.Guides, "https://steamcommunity.com/app/{0}/guides/" }, { SteamUrlType.Achievements, "https://steamcommunity.com/stats/{0}/achievements/" }, { SteamUrlType.News, "https://store.steampowered.com/news/?appids={0}" }, - { SteamUrlType.PointsShop, "https://store.steampowered.com/points/shop/app/{0}" } + { SteamUrlType.PointsShop, "https://store.steampowered.com/points/shop/app/{0}" }, + { SteamUrlType.Workshop, "https://steamcommunity.com/app/{0}/workshop" } }; private static readonly Dictionary _steamComponentTemplates = new Dictionary() diff --git a/source/Generic/SteamViewer/Domain/Enums/SteamUrlType.cs b/source/Generic/SteamViewer/Domain/Enums/SteamUrlType.cs index 9f35fc251..1b09447b6 100644 --- a/source/Generic/SteamViewer/Domain/Enums/SteamUrlType.cs +++ b/source/Generic/SteamViewer/Domain/Enums/SteamUrlType.cs @@ -14,6 +14,7 @@ public enum SteamUrlType Guides, Achievements, News, - PointsShop + PointsShop, + Workshop } } \ No newline at end of file diff --git a/source/Generic/SteamViewer/Domain/ValueObjects/PointsShopCount.cs b/source/Generic/SteamViewer/Domain/ValueObjects/PointsShopCount.cs new file mode 100644 index 000000000..e8b78d2c8 --- /dev/null +++ b/source/Generic/SteamViewer/Domain/ValueObjects/PointsShopCount.cs @@ -0,0 +1,13 @@ +namespace SteamShortcuts.Domain.ValueObjects +{ + public class PointsShopCount + { + public class Response + { + public int total_count { get; set; } + public int count { get; set; } + } + + public Response response; + } +} diff --git a/source/Generic/SteamViewer/Domain/ValueObjects/SupportedFeatures.cs b/source/Generic/SteamViewer/Domain/ValueObjects/SupportedFeatures.cs new file mode 100644 index 000000000..d9282f071 --- /dev/null +++ b/source/Generic/SteamViewer/Domain/ValueObjects/SupportedFeatures.cs @@ -0,0 +1,18 @@ +using SteamCommon.Models; + +namespace SteamShortcuts.Domain.ValueObjects +{ + public class SupportedFeatures + { + public bool Achievements { get; set; } = false; + public bool PointsShop { get; set; } = false; + public bool Workshop { get; set; } = false; + + public static SupportedFeatures All = new SupportedFeatures + { + Achievements = true, + PointsShop = true, + Workshop = true, + }; + } +} \ No newline at end of file diff --git a/source/Generic/SteamViewer/Infrastructure/QueryRewardItems.cs b/source/Generic/SteamViewer/Infrastructure/QueryRewardItems.cs new file mode 100644 index 000000000..b77e08b1a --- /dev/null +++ b/source/Generic/SteamViewer/Infrastructure/QueryRewardItems.cs @@ -0,0 +1,27 @@ +using FlowHttp; +using Playnite.SDK.Data; +using SteamShortcuts.Domain.ValueObjects; +using System.Threading; +using System.Threading.Tasks; + +namespace SteamShortcuts.Infrastructure +{ + class QueryRewardItems + { + private const string queryUrl = @"https://api.steampowered.com/ILoyaltyRewardsService/QueryRewardItems/v1/?appids[0]={0}&count=1"; + + public static async Task GetPointsShopCount(string appId, CancellationToken cancelToken = default) + { + var url = string.Format(queryUrl, appId); + var request = HttpRequestFactory.GetHttpRequest().WithUrl(url); + var downloadedString = await request.DownloadStringAsync(cancelToken); + + if (downloadedString.IsSuccess) + { + return Serialization.FromJson(downloadedString.Content); + } + + return null; + } + } +} diff --git a/source/Generic/SteamViewer/Localization/en_US.xaml b/source/Generic/SteamViewer/Localization/en_US.xaml index df711fca4..8ed55bb91 100644 --- a/source/Generic/SteamViewer/Localization/en_US.xaml +++ b/source/Generic/SteamViewer/Localization/en_US.xaml @@ -13,6 +13,7 @@ News Achievements PointsShop + Workshop View in Steam Library Details Steam Input diff --git a/source/Generic/SteamViewer/Presentation/SteamGameActionsLauncher.xaml b/source/Generic/SteamViewer/Presentation/SteamGameActionsLauncher.xaml index 4e600b0c7..7313ca63b 100644 --- a/source/Generic/SteamViewer/Presentation/SteamGameActionsLauncher.xaml +++ b/source/Generic/SteamViewer/Presentation/SteamGameActionsLauncher.xaml @@ -28,9 +28,10 @@ - - + + + diff --git a/source/Generic/SteamViewer/Presentation/SteamGameActionsLauncher.xaml.cs b/source/Generic/SteamViewer/Presentation/SteamGameActionsLauncher.xaml.cs index 2b6dfcadb..a9cbdf60a 100644 --- a/source/Generic/SteamViewer/Presentation/SteamGameActionsLauncher.xaml.cs +++ b/source/Generic/SteamViewer/Presentation/SteamGameActionsLauncher.xaml.cs @@ -4,6 +4,7 @@ using SteamCommon; using SteamShortcuts.Application; using SteamShortcuts.Domain.Enums; +using SteamShortcuts.Domain.ValueObjects; using System; using System.Collections.Generic; using System.Linq; @@ -33,17 +34,20 @@ public partial class SteamGameActionsLauncher : PluginUserControlBase public SteamShortcutsSettingsViewModel SettingsModel { get; } private readonly SteamUriLauncherService _launcher; + private readonly DiscoverSupportedFeaturesService _featuresService; private string _steamId = string.Empty; private bool _areBindingValuesDefault = true; public SteamGameActionsLauncher( SteamUriLauncherService steamUriLauncherService, + DiscoverSupportedFeaturesService discoverSupportedFeaturesSerivce, SteamShortcutsSettingsViewModel settingsViewModel, IPlayniteAPI playniteApi, ILogger logger) : base(playniteApi) { InitializeComponent(); _launcher = steamUriLauncherService ?? throw new ArgumentNullException(nameof(steamUriLauncherService)); + _featuresService = discoverSupportedFeaturesSerivce ?? throw new ArgumentNullException(nameof(discoverSupportedFeaturesSerivce)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); SettingsModel = settingsViewModel ?? throw new ArgumentNullException(nameof(settingsViewModel)); DataContext = this; @@ -83,6 +87,23 @@ public override void GameContextChanged(Game oldContext, Game newContext) } _steamId = foundSteamId; + _ = _featuresService.GetFeatures(foundSteamId).ContinueWith(task => + { + var features = SupportedFeatures.All; + + if (task.Status == TaskStatus.RanToCompletion) + { + features = task.Result ?? SupportedFeatures.All; + } + + PlayniteApi.MainView.UIDispatcher.Invoke(() => + { + MenuItemGameAchievements.Visibility = features.Achievements ? Visibility.Visible : Visibility.Collapsed; + MenuItemGamePointsShop.Visibility = features.PointsShop ? Visibility.Visible : Visibility.Collapsed; + MenuItemGameWorkshop.Visibility = features.Workshop ? Visibility.Visible : Visibility.Collapsed; + }); + }); + Visibility = Visibility.Visible; SettingsModel.Settings.IsControlVisible = true; _areBindingValuesDefault = false; @@ -138,6 +159,9 @@ private void OnNews(object sender, RoutedEventArgs e) private void OnPointsShop(object sender, RoutedEventArgs e) => _launcher.LaunchSteamWebUrl(SteamUrlType.PointsShop, _steamId); + private void OnWorkshop(object sender, RoutedEventArgs e) + => _launcher.LaunchSteamWebUrl(SteamUrlType.Workshop, _steamId); + #endregion #region Global Steam diff --git a/source/Generic/SteamViewer/SteamShortcuts.cs b/source/Generic/SteamViewer/SteamShortcuts.cs index daaad818c..116c71935 100644 --- a/source/Generic/SteamViewer/SteamShortcuts.cs +++ b/source/Generic/SteamViewer/SteamShortcuts.cs @@ -7,6 +7,7 @@ using SteamCommon; using SteamShortcuts.Application; using SteamShortcuts.Domain.Enums; +using SteamShortcuts.Domain.ValueObjects; using SteamShortcuts.Presentation; using System; using System.Collections.Generic; @@ -25,6 +26,7 @@ public class SteamShortcuts : GenericPlugin public SteamShortcutsSettingsViewModel Settings { get; set; } private readonly List _steamComponentMenuItems; private static readonly string _menuSection = ResourceProvider.GetString("LOCSteam_Viewer_SteamShortcutsLabel"); + private readonly DiscoverSupportedFeaturesService _featuresService; public override Guid Id { get; } = Guid.Parse("0a3edabb-065f-4056-a294-d6bc0656e2ac"); @@ -35,6 +37,7 @@ public SteamShortcuts(IPlayniteAPI api) : base(api) Properties = new GenericPluginProperties { HasSettings = true }; _steamComponentMenuItems = GetSteamComponentMenuItems(); _steamUriLauncherService.LaunchUrlsInSteamClient = Settings.Settings.LaunchUrlsInSteamClient; + _featuresService = new DiscoverSupportedFeaturesService(); AddSettingsSupport(new AddSettingsSupportArgs { @@ -53,7 +56,7 @@ public override Control GetGameViewControl(GetGameViewControlArgs args) { if (args.Name == "SteamGameActionsLauncher") { - return new SteamGameActionsLauncher(_steamUriLauncherService, Settings, PlayniteApi, _logger); + return new SteamGameActionsLauncher(_steamUriLauncherService, _featuresService, Settings, PlayniteApi, _logger); } return null; @@ -113,10 +116,25 @@ private void AddSteamWebLinkMenuItems(List menuItems, string steam CreateSteamWebLinkMenuItem("LOCSteam_Viewer_MenuItemGameCommunityHubDescription", SteamUrlType.CommunityHub, steamId), CreateSteamWebLinkMenuItem("LOCSteam_Viewer_MenuItemGameDiscussionsDescription", SteamUrlType.Discussions, steamId, '\uED44'), CreateSteamWebLinkMenuItem("LOCSteam_Viewer_MenuItemGameGuidesDescription", SteamUrlType.Guides, steamId, '\uEF8B'), - CreateSteamWebLinkMenuItem("LOCSteam_Viewer_MenuItemGameAchievementsDescription", SteamUrlType.Achievements, steamId, '\uEDD7'), CreateSteamWebLinkMenuItem("LOCSteam_Viewer_MenuItemGameNewsDescription", SteamUrlType.News, steamId, '\uEFA7'), - CreateSteamWebLinkMenuItem("LOCSteam_Viewer_MenuItemGamePointsShopDescription", SteamUrlType.PointsShop, steamId, '\uEFE7') }); + + var feature = _featuresService.GetCachedFeature(steamId) ?? SupportedFeatures.All; + + if (feature.Achievements) + { + menuItems.Add(CreateSteamWebLinkMenuItem("LOCSteam_Viewer_MenuItemGameAchievementsDescription", SteamUrlType.Achievements, steamId, '\uEDD7')); + } + + if (feature.PointsShop) + { + menuItems.Add(CreateSteamWebLinkMenuItem("LOCSteam_Viewer_MenuItemGamePointsShopDescription", SteamUrlType.PointsShop, steamId, '\uEFE7')); + } + + if (feature.Workshop) + { + menuItems.Add(CreateSteamWebLinkMenuItem("LOCSteam_Viewer_MenuItemGameWorkshopDescription", SteamUrlType.Workshop, steamId, '\uE9C4')); + } } private List GetSteamComponentMenuItems() @@ -174,6 +192,29 @@ public override UserControl GetSettingsView(bool firstRunSettings) { return new SteamShortcutsSettingsView(); } - } + public override void OnGameSelected(OnGameSelectedEventArgs args) + { + // Happens when transitioning to another layout (e.g., details to grid) + if (args.NewValue is null) + { + return; + } + + if (args.NewValue.Count > 1) + { + return; + } + + var game = args.NewValue[0]; + string steamId = Steam.GetGameSteamId(game, Settings.Settings.AddWebLinksForNonSteam); + + if (string.IsNullOrEmpty(steamId)) + { + return; + } + + _ = _featuresService.GetFeatures(steamId); + } + } } \ No newline at end of file diff --git a/source/Generic/SteamViewer/SteamShortcuts.csproj b/source/Generic/SteamViewer/SteamShortcuts.csproj index 2aee232aa..b12fbc551 100644 --- a/source/Generic/SteamViewer/SteamShortcuts.csproj +++ b/source/Generic/SteamViewer/SteamShortcuts.csproj @@ -12,6 +12,8 @@ v4.6.2 512 true + + true @@ -31,13 +33,66 @@ 4 + + ..\..\packages\AngleSharp.0.9.9\lib\net45\AngleSharp.dll + + + ..\..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + + + ..\..\packages\Microsoft.Extensions.DependencyInjection.6.0.0\lib\net461\Microsoft.Extensions.DependencyInjection.dll + + + ..\..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.6.0.0\lib\net461\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + ..\..\packages\Microsoft.Extensions.Http.6.0.0\lib\net461\Microsoft.Extensions.Http.dll + + + ..\..\packages\Microsoft.Extensions.Logging.6.0.0\lib\net461\Microsoft.Extensions.Logging.dll + + + ..\..\packages\Microsoft.Extensions.Logging.Abstractions.6.0.0\lib\net461\Microsoft.Extensions.Logging.Abstractions.dll + + + ..\..\packages\Microsoft.Extensions.Options.6.0.0\lib\net461\Microsoft.Extensions.Options.dll + + + ..\..\packages\Microsoft.Extensions.Primitives.6.0.0\lib\net461\Microsoft.Extensions.Primitives.dll + ..\..\packages\PlayniteSDK.6.11.0\lib\net462\Playnite.SDK.dll + + ..\..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + + ..\..\packages\System.Diagnostics.DiagnosticSource.6.0.0\lib\net461\System.Diagnostics.DiagnosticSource.dll + + + ..\..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + + + + ..\..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + ..\..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\..\packages\System.Text.Encoding.CodePages.8.0.0\lib\net462\System.Text.Encoding.CodePages.dll + + + ..\..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + ..\..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll + @@ -163,10 +218,17 @@ Shared\SteamCommon\Models\SteamAppDetailsResponse.cs + + Shared\SteamCommon\Web.cs + + + + + SteamGameActionsLauncher.xaml @@ -178,6 +240,7 @@ + PreserveNewest @@ -210,8 +273,17 @@ + + xcopy "$(ProjectDir)Localization\*.xaml" "$(TargetDir)\Localization" /Y /I /E + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + \ No newline at end of file diff --git a/source/Generic/SteamViewer/packages.config b/source/Generic/SteamViewer/packages.config index 4ddbc2809..2ffcfafe9 100644 --- a/source/Generic/SteamViewer/packages.config +++ b/source/Generic/SteamViewer/packages.config @@ -1,4 +1,20 @@  + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/PlayniteExtensions.sln b/source/PlayniteExtensions.sln index a475eaa22..0ce4e3673 100644 --- a/source/PlayniteExtensions.sln +++ b/source/PlayniteExtensions.sln @@ -406,6 +406,8 @@ Global Common\FlowHttp\FlowHttp.projitems*{a54423d1-8e7a-4339-b0d5-24e7cefe469b}*SharedItemsImports = 4 Common\EventsCommon\EventsCommon.projitems*{a802a57c-64a5-40c2-8528-8c4c8fcbf77b}*SharedItemsImports = 4 Common\FlowHttp\FlowHttp.projitems*{abaf89bf-6b65-4091-b4bf-73fa058fea7a}*SharedItemsImports = 4 + Common\FlowHttp\FlowHttp.projitems*{b362182c-0d36-4e85-94d6-2504ca50ed5a}*SharedItemsImports = 4 + Common\SteamCommon\SteamCommon.projitems*{b362182c-0d36-4e85-94d6-2504ca50ed5a}*SharedItemsImports = 4 Common\ThrottlerSharp\ThrottlerSharp.projitems*{b3a1fefc-8c7f-4aa7-9993-b1d4f51222a9}*SharedItemsImports = 13 Common\FlowHttp\FlowHttp.projitems*{b572d4f6-ebdb-475e-a0d6-ce4ce050ab47}*SharedItemsImports = 4 Common\FlowHttp\FlowHttp.projitems*{bcb07ce2-d6e4-4c73-b133-cc5a6ad1bfc4}*SharedItemsImports = 4