From b3212738e8aba946407f50c8b6c8800402839640 Mon Sep 17 00:00:00 2001 From: Michael Gordeev Date: Thu, 14 May 2020 23:45:11 +0300 Subject: [PATCH] Added Subscriptions page and updated subscriptions management --- FoxTube.Core/Extensions.cs | 13 ++- FoxTube.Core/FoxTube.Core.csproj | 1 + FoxTube.Core/Models/Subscription.cs | 16 +++ FoxTube.Core/Models/User.cs | 85 ++++++++++---- FoxTube.Core/UserManagement.cs | 1 - FoxTube/Classes/MenuItemsList.cs | 10 +- FoxTube/Classes/Navigation.cs | 4 +- .../Converters/SubscriptionDataConverter.cs | 37 ++++++ FoxTube/FoxTube.csproj | 1 + FoxTube/MainPage.xaml.cs | 28 ++++- FoxTube/Views/Subscriptions.xaml | 107 ++++++++++++------ FoxTube/Views/Subscriptions.xaml.cs | 48 +++++++- 12 files changed, 284 insertions(+), 67 deletions(-) create mode 100644 FoxTube.Core/Models/Subscription.cs create mode 100644 FoxTube/Converters/SubscriptionDataConverter.cs diff --git a/FoxTube.Core/Extensions.cs b/FoxTube.Core/Extensions.cs index d923bc8..f6d6005 100644 --- a/FoxTube.Core/Extensions.cs +++ b/FoxTube.Core/Extensions.cs @@ -1,10 +1,12 @@ using FoxTube.Utils; +using Google.Apis.YouTube.v3.Data; using SQLitePCL; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; using System.Xml; using Windows.Devices.PointOfService; using Windows.UI; @@ -47,7 +49,7 @@ namespace FoxTube public static string ToHex(this Color color) => $"#{color.R:X}{color.G:X}{color.B:X}"; - public static Color FromHex(this Color parent, string hex) + public static Color FromHex(string hex) { hex = hex.Replace("#", ""); List values = new List(); @@ -126,5 +128,14 @@ namespace FoxTube else return Math.Round(span.TotalDays / 365) + " " + "years ago"; } + + public static async Task GetChannel(string channelId, string part) + { + var request = UserManagement.Service.Channels.List(part); + request.Id = channelId; + request.MaxResults = 1; + + return (await request.ExecuteAsync()).Items.FirstOrDefault(); + } } } \ No newline at end of file diff --git a/FoxTube.Core/FoxTube.Core.csproj b/FoxTube.Core/FoxTube.Core.csproj index c2b857b..9968c80 100644 --- a/FoxTube.Core/FoxTube.Core.csproj +++ b/FoxTube.Core/FoxTube.Core.csproj @@ -130,6 +130,7 @@ PackageReference + diff --git a/FoxTube.Core/Models/Subscription.cs b/FoxTube.Core/Models/Subscription.cs new file mode 100644 index 0000000..aef37a3 --- /dev/null +++ b/FoxTube.Core/Models/Subscription.cs @@ -0,0 +1,16 @@ +using Google.Apis.YouTube.v3.Data; + +namespace FoxTube.Models +{ + public class Subscription + { + public string Title { get; set; } + public string Description { get; set; } + public string ChannelId { get; set; } + public string SubscriptionId { get; set; } + public ThumbnailDetails Avatar { get; set; } + public ImageSettings Banner { get; set; } + public ulong? SubscribersCount { get; set; } + public ulong VideosCount { get; set; } + } +} \ No newline at end of file diff --git a/FoxTube.Core/Models/User.cs b/FoxTube.Core/Models/User.cs index 2e8d436..0c386b2 100644 --- a/FoxTube.Core/Models/User.cs +++ b/FoxTube.Core/Models/User.cs @@ -30,26 +30,24 @@ namespace FoxTube.Models public async Task UpdateSubscriptionState(string channelId) { - if (Subscriptions.Exists(x => x.Snippet.ResourceId.ChannelId == channelId)) + if (Subscriptions.Find(i => i.ChannelId == channelId) is Subscription subscription) { - Subscription s = Subscriptions.Find(x => x.Snippet.ResourceId.ChannelId == channelId); - - try { await Service.Subscriptions.Delete(s.Id).ExecuteAsync(); } + try { await Service.Subscriptions.Delete(subscription.SubscriptionId).ExecuteAsync(); } catch (Exception e) { Metrics.SendReport(new Exception("Failed to unsubscribe", e)); return true; } - UserManagement.SubscriptionsChangedInvoker(this, s); - Subscriptions.Remove(s); + UserManagement.SubscriptionsChangedInvoker(this, subscription); + Subscriptions.Remove(subscription); SaveSubscriptions(); return false; } else { - var request = Service.Subscriptions.Insert(new Subscription() + var request = Service.Subscriptions.Insert(new Google.Apis.YouTube.v3.Data.Subscription() { Snippet = new SubscriptionSnippet() { @@ -59,14 +57,27 @@ namespace FoxTube.Models Kind = "youtube#channel" } } - }, "snippet"); + }, "id"); - Subscription s = await request.ExecuteAsync(); - if (s == null) + var subscriptionData = await request.ExecuteAsync(); + if (subscriptionData == null) return false; - Subscriptions.Add(s); - UserManagement.SubscriptionsChangedInvoker(this, s); + Channel channel = await Extensions.GetChannel(subscriptionData.Snippet.ChannelId, "snippet,statistics,brandingSettins"); + + subscription = new Subscription + { + Title = channel.Snippet.Title, + ChannelId = channel.Id, + SubscriptionId = subscriptionData.Id, + VideosCount = channel.Statistics.VideoCount.Value, + SubscribersCount = channel.Statistics.SubscriberCount, + Avatar = channel.Snippet.Thumbnails, + Banner = channel.BrandingSettings.Image + }; + Subscriptions.Add(subscription); + + UserManagement.SubscriptionsChangedInvoker(this, subscription); SaveSubscriptions(); return true; @@ -76,7 +87,7 @@ namespace FoxTube.Models private void SaveSubscriptions() { Dictionary subs = Subscriptions.Select(i => - new KeyValuePair(i.Snippet.ResourceId.ChannelId, i.Snippet.Thumbnails.Default__?.Url)) + new KeyValuePair(i.ChannelId, i.Avatar.Default__?.Url)) as Dictionary; ApplicationData.Current.RoamingSettings.Values[$"Subscriptions.{UserInfo.Id}"] = JsonConvert.SerializeObject(subs); @@ -99,9 +110,22 @@ namespace FoxTube.Models user.UserInfo = await new Oauth2Service(initializer).Userinfo.Get().ExecuteAsync(); + var request = user.Service.Channels.List("snippet,contentDetails,brandingSettings"); + request.Mine = true; + user.Channel = request.Execute().Items[0]; + // TODO: Retrieve history and WL - SubscriptionsResource.ListRequest subRequest = user.Service.Subscriptions.List("snippet"); + await user.LoadSubscriptions(); + + return user; + } + + public async Task LoadSubscriptions() + { + Subscriptions.Clear(); + + SubscriptionsResource.ListRequest subRequest = Service.Subscriptions.List("snippet"); subRequest.Mine = true; subRequest.MaxResults = 50; subRequest.Order = SubscriptionsResource.ListRequest.OrderEnum.Relevance; @@ -112,16 +136,37 @@ namespace FoxTube.Models subResponse = await subRequest.ExecuteAsync(); subRequest.PageToken = subResponse.NextPageToken; - user.Subscriptions.AddRange(subResponse.Items); + Subscriptions.AddRange(subResponse.Items.Select(i => new Subscription + { + Title = i.Snippet.Title, + ChannelId = i.Snippet.ResourceId.ChannelId, + SubscriptionId = i.Id, + Avatar = i.Snippet.Thumbnails, + Description = i.Snippet.Description + })); } while (!string.IsNullOrWhiteSpace(subRequest.PageToken)); - - var request = user.Service.Channels.List("snippet,contentDetails,brandingSettings"); - request.Mine = true; - user.Channel = request.Execute().Items[0]; + var request = Service.Channels.List("statistics,brandingSettings"); + request.Id = string.Join(',', Subscriptions.Select(i => i.ChannelId)); + request.MaxResults = 50; + ChannelListResponse response; - return user; + if (Subscriptions.Count > 0) + do + { + response = await request.ExecuteAsync(); + request.PageToken = response.NextPageToken; + + Subscriptions.ForEach(i => + { + Channel channel = response.Items.FirstOrDefault(c => c.Id == i.ChannelId); + i.Banner = channel.BrandingSettings.Image; + i.SubscribersCount = channel.Statistics.HiddenSubscriberCount.Value ? null : channel.Statistics.SubscriberCount; + i.VideosCount = channel.Statistics.VideoCount.Value; + }); + } + while (!string.IsNullOrWhiteSpace(request.PageToken)); } } } diff --git a/FoxTube.Core/UserManagement.cs b/FoxTube.Core/UserManagement.cs index 0c6c1c3..0dd61d1 100644 --- a/FoxTube.Core/UserManagement.cs +++ b/FoxTube.Core/UserManagement.cs @@ -9,7 +9,6 @@ using FoxTube.Models; using YouTube; using System.Threading; using Google.Apis.YouTube.v3; -using Google.Apis.YouTube.v3.Data; using Windows.UI.Xaml.Controls; using FoxTube.Utils; using YoutubeExplode; diff --git a/FoxTube/Classes/MenuItemsList.cs b/FoxTube/Classes/MenuItemsList.cs index 1470158..4bf0d3a 100644 --- a/FoxTube/Classes/MenuItemsList.cs +++ b/FoxTube/Classes/MenuItemsList.cs @@ -1,8 +1,8 @@ -using Google.Apis.YouTube.v3.Data; -using System.Collections.Generic; +using System.Collections.Generic; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml; using Windows.UI.Xaml.Media.Imaging; +using FoxTube.Models; namespace FoxTube.Utils { @@ -65,19 +65,19 @@ namespace FoxTube.Utils { Height = 20, Margin = new Thickness(-5, 0, 15, 0), - ProfilePicture = new BitmapImage().LoadImage(subscription.Snippet.Thumbnails.Default__.Url, 20, 20) + ProfilePicture = new BitmapImage().LoadImage(subscription.Avatar.Default__.Url, 20, 20) }); stack.Children.Add(new TextBlock { FontSize = 14, - Text = subscription.Snippet.Title + Text = subscription.Title }); return new Microsoft.UI.Xaml.Controls.NavigationViewItem { Content = stack, - Tag = subscription.Snippet.ChannelId + Tag = subscription.ChannelId }; } } diff --git a/FoxTube/Classes/Navigation.cs b/FoxTube/Classes/Navigation.cs index d1b3318..e092b08 100644 --- a/FoxTube/Classes/Navigation.cs +++ b/FoxTube/Classes/Navigation.cs @@ -4,7 +4,8 @@ { Home, Settings, - Downloads + Downloads, + Subscriptions } public static class Navigation @@ -21,6 +22,7 @@ { NavigationTarget.Settings => typeof(Views.Settings), NavigationTarget.Downloads => typeof(Views.Downloads), + NavigationTarget.Subscriptions => typeof(Views.Subscriptions), _ => UserManagement.Authorized ? typeof(Views.Home) : typeof(Views.HomeSections.Trending), }, parameters); diff --git a/FoxTube/Converters/SubscriptionDataConverter.cs b/FoxTube/Converters/SubscriptionDataConverter.cs new file mode 100644 index 0000000..9241a6a --- /dev/null +++ b/FoxTube/Converters/SubscriptionDataConverter.cs @@ -0,0 +1,37 @@ +using FoxTube.Models; +using System; +using System.Collections.Generic; +using Windows.UI.Xaml.Data; + +namespace FoxTube.Converters +{ + public class SubscriptionDataConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + Subscription subscription = (Subscription)value; + List data = new List(); + if (subscription.SubscribersCount.HasValue) + data.Add($"{GetTruncatedSubscribersCount(subscription.SubscribersCount)} subscribers"); + + data.Add($"{subscription.VideosCount} videos"); + return string.Join(" • ", data); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) => + throw new NotImplementedException(); + + private string GetTruncatedSubscribersCount(ulong? count) + { + if (!count.HasValue) + return "-"; + + if (count > 1000000) + return Math.Round(count.Value / (double)1000000, 1) + "M"; + else if (count > 1000) + return Math.Round(count.Value / (double)1000, 1) + "k"; + else + return count.Value.ToString(); + } + } +} \ No newline at end of file diff --git a/FoxTube/FoxTube.csproj b/FoxTube/FoxTube.csproj index 74af58f..7bf020c 100644 --- a/FoxTube/FoxTube.csproj +++ b/FoxTube/FoxTube.csproj @@ -138,6 +138,7 @@ ItemsGrid.xaml + MainPage.xaml diff --git a/FoxTube/MainPage.xaml.cs b/FoxTube/MainPage.xaml.cs index 6d66424..0a3471a 100644 --- a/FoxTube/MainPage.xaml.cs +++ b/FoxTube/MainPage.xaml.cs @@ -2,12 +2,12 @@ using FoxTube.Controls.Dialogs; using FoxTube.Utils; using FoxTube.Services; -using Google.Apis.YouTube.v3.Data; using System; using System.Linq; using Windows.ApplicationModel.Core; using Windows.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using FoxTube.Models; // TODO: Fix header (UI) namespace FoxTube @@ -69,6 +69,9 @@ namespace FoxTube case "Downloads": NavigationViewControl.SelectedItem = NavigationViewControl.MenuItems.FirstOrDefault(i => ((FrameworkElement)i).Tag as string == "downloads"); break; + case "Subscriptions": + NavigationViewControl.SelectedItem = NavigationViewControl.MenuItems.FirstOrDefault(i => ((FrameworkElement)i).Tag as string == "subscriptions"); + break; case "Home": case "Trending": NavigationViewControl.SelectedItem = NavigationViewControl.MenuItems.FirstOrDefault(i => ((FrameworkElement)i).Tag as string == "home"); @@ -86,7 +89,7 @@ namespace FoxTube private void NavigationViewControl_ItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args) { - (NavigationTarget target, object parameters) suggestedTransition = (NavigationTarget.Home, null); + (NavigationTarget target, object parameters) suggestedTransition; if (args.IsSettingsInvoked) suggestedTransition = (NavigationTarget.Settings, null); @@ -95,6 +98,7 @@ namespace FoxTube { "home" => (NavigationTarget.Home, null), "downloads" => (NavigationTarget.Downloads, null), + "subscriptions" => (NavigationTarget.Subscriptions, null), _ => (NavigationTarget.Home, null) // TODO: Navigate to channel }; @@ -135,7 +139,7 @@ namespace FoxTube if (authorized && UserManagement.CurrentUser.Subscriptions.Count > 0) { NavigationViewControl.MenuItems.Add(new NavigationViewItemHeader { Content = "Subscriptions" }); - UserManagement.CurrentUser.Subscriptions.GetRange(0, Math.Min(UserManagement.CurrentUser.Subscriptions.Count, 10)).ForEach(i => + UserManagement.CurrentUser.Subscriptions.ToList().GetRange(0, Math.Min(UserManagement.CurrentUser.Subscriptions.Count, 10)).ForEach(i => NavigationViewControl.MenuItems.Add(MenuItemsList.GenerateItemFromSubscription(i))); } @@ -150,7 +154,7 @@ namespace FoxTube private void UsersControl_SubscriptionsChanged(object sender, Subscription subscription) { - if (NavigationViewControl.MenuItems.FirstOrDefault(i => ((NavigationViewItemBase)i).Tag as string == subscription.Snippet.ChannelId) is NavigationViewItem container) + if (NavigationViewControl.MenuItems.FirstOrDefault(i => ((NavigationViewItemBase)i).Tag as string == subscription.ChannelId) is NavigationViewItem container) { NavigationViewControl.MenuItems.Remove(container); @@ -166,6 +170,22 @@ namespace FoxTube NavigationViewControl.MenuItems.Add(MenuItemsList.GenerateItemFromSubscription(subscription)); } } + + public void UpdateSubscriptions() + { + // Add general menu items + NavigationViewControl.MenuItems.Clear(); + MenuItemsList.GetMenuItems(UserManagement.Authorized).ForEach(i => + NavigationViewControl.MenuItems.Add(i)); + + // Add subscriptions list to the menu + if (UserManagement.Authorized && UserManagement.CurrentUser.Subscriptions.Count > 0) + { + NavigationViewControl.MenuItems.Add(new NavigationViewItemHeader { Content = "Subscriptions" }); + UserManagement.CurrentUser.Subscriptions.ToList().GetRange(0, Math.Min(UserManagement.CurrentUser.Subscriptions.Count, 10)).ForEach(i => + NavigationViewControl.MenuItems.Add(MenuItemsList.GenerateItemFromSubscription(i))); + } + } #endregion #region Navigation actions diff --git a/FoxTube/Views/Subscriptions.xaml b/FoxTube/Views/Subscriptions.xaml index 5be7160..0300e98 100644 --- a/FoxTube/Views/Subscriptions.xaml +++ b/FoxTube/Views/Subscriptions.xaml @@ -1,40 +1,81 @@  + NavigationCacheMode="Enabled" + x:Class="FoxTube.Views.Subscriptions" + 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:data="using:FoxTube.Models" + xmlns:converters="using:FoxTube.Converters" xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls" + mc:Ignorable="d"> - - - - - - - - + + + - - - + + + + + + + + + + + + + + + + + + + + - - - - - + + + - + + + + + - - - - - - - + + + + + + + + + + + + +