Archived
1
0

Added Subscriptions page and updated subscriptions management

This commit is contained in:
Michael Gordeev
2020-05-14 23:45:11 +03:00
parent 8159d33a43
commit b3212738e8
12 changed files with 284 additions and 67 deletions
+12 -1
View File
@@ -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<byte> values = new List<byte>();
@@ -126,5 +128,14 @@ namespace FoxTube
else
return Math.Round(span.TotalDays / 365) + " " + "years ago";
}
public static async Task<Channel> GetChannel(string channelId, string part)
{
var request = UserManagement.Service.Channels.List(part);
request.Id = channelId;
request.MaxResults = 1;
return (await request.ExecuteAsync()).Items.FirstOrDefault();
}
}
}
+1
View File
@@ -130,6 +130,7 @@
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>
<ItemGroup>
<Compile Include="Models\Subscription.cs" />
<Compile Include="Services\DownloadsCenter.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="Utils\Feedback.cs" />
+16
View File
@@ -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; }
}
}
+64 -19
View File
@@ -30,26 +30,24 @@ namespace FoxTube.Models
public async Task<bool> 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<string, string> subs = Subscriptions.Select(i =>
new KeyValuePair<string, string>(i.Snippet.ResourceId.ChannelId, i.Snippet.Thumbnails.Default__?.Url))
new KeyValuePair<string, string>(i.ChannelId, i.Avatar.Default__?.Url))
as Dictionary<string, string>;
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 = Service.Channels.List("statistics,brandingSettings");
request.Id = string.Join(',', Subscriptions.Select(i => i.ChannelId));
request.MaxResults = 50;
ChannelListResponse response;
var request = user.Service.Channels.List("snippet,contentDetails,brandingSettings");
request.Mine = true;
user.Channel = request.Execute().Items[0];
if (Subscriptions.Count > 0)
do
{
response = await request.ExecuteAsync();
request.PageToken = response.NextPageToken;
return user;
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));
}
}
}
-1
View File
@@ -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;
+5 -5
View File
@@ -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
};
}
}
+3 -1
View File
@@ -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);
@@ -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<string> data = new List<string>();
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();
}
}
}
+1
View File
@@ -138,6 +138,7 @@
<Compile Include="Controls\ItemsGrid.xaml.cs">
<DependentUpon>ItemsGrid.xaml</DependentUpon>
</Compile>
<Compile Include="Converters\SubscriptionDataConverter.cs" />
<Compile Include="MainPage.xaml.cs">
<DependentUpon>MainPage.xaml</DependentUpon>
</Compile>
+24 -4
View File
@@ -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
+53 -12
View File
@@ -1,17 +1,37 @@
<Page
xmlns:models="using:FoxTube.Core.Models"
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:local="using:FoxTube.Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
xmlns:data="using:FoxTube.Models"
xmlns:converters="using:FoxTube.Converters" xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
mc:Ignorable="d">
<ListView Padding="10" SelectionMode="None">
<ListViewItem Padding="0" HorizontalAlignment="Left">
<Grid Height="150" Width="700" Padding="20" ColumnSpacing="20" CornerRadius="20">
<Page.Resources>
<converters:SubscriptionDataConverter x:Name="converter"/>
</Page.Resources>
<Grid>
<RefreshContainer RefreshRequested="RefreshContainer_RefreshRequested">
<ListView Padding="10" SelectionMode="None" IsItemClickEnabled="True" Name="list" ItemClick="List_ItemClick">
<ListView.ItemContainerTransitions>
<TransitionCollection>
<EntranceThemeTransition IsStaggeringEnabled="True"/>
<AddDeleteThemeTransition/>
</TransitionCollection>
</ListView.ItemContainerTransitions>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="0,0,0,10"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate x:DataType="data:Subscription">
<Grid Height="140" Padding="20" ColumnSpacing="20" CornerRadius="20" RequestedTheme="Dark">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
@@ -19,22 +39,43 @@
</Grid.ColumnDefinitions>
<Grid.Background>
<ImageBrush ImageSource="https://yt3.ggpht.com/TckH7MhPH7UcxPnHeEj6R78xe1uOmrlHdUD1Usy7sr36xLDzl86NxAyfmDOIUrxl6kpiBgtKgA=w1060-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj"/>
<ImageBrush ImageSource="{Binding Path=Banner.BannerImageUrl}" Stretch="UniformToFill"/>
</Grid.Background>
<Grid Grid.ColumnSpan="3" Margin="-20">
<Grid.Background>
<AcrylicBrush TintColor="Black" TintOpacity=".5" Opacity=".97"/>
<AcrylicBrush TintColor="Black" TintOpacity=".5"/>
</Grid.Background>
</Grid>
<controls:ImageEx CornerRadius="999" Source="https://yt3.ggpht.com/a/AGF-l7_hHK5yjEPhlM72Tmk26zoOVkNx88pWoZnFVQ=s88-c-k-c0xffffffff-no-rj-mo"/>
<PersonPicture ProfilePicture="{Binding Path=Avatar.Medium.Url}"/>
<StackPanel Grid.Column="1">
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="[BadComedian]"/>
<TextBlock Text="Обзоры кино. Юмор, ирония, ненависть, плохие российские / зарубежные фильмы. &#xD;ПРИЯТНОГО ПРОСМОТРА." TextTrimming="CharacterEllipsis" TextWrapping="WrapWholeWords" MaxLines="4"/>
<TextBlock Style="{StaticResource TitleTextBlockStyle}" Text="{Binding Path=Title}" MaxLines="1" TextTrimming="CharacterEllipsis"/>
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="{Binding Converter={StaticResource converter}}" MaxLines="1" TextTrimming="WordEllipsis"/>
<TextBlock Text="{Binding Path=Description}" MaxLines="2"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Column="2">
<StackPanel.Resources>
<Style TargetType="Button" BasedOn="{StaticResource ButtonRevealStyle}">
<Setter Property="Height" Value="50"/>
<Setter Property="MinWidth" Value="50"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="Background" Value="Transparent"/>
</Style>
</StackPanel.Resources>
<Button Content="Subscribed" Width="200" Click="Unsubscribe" Tag="{Binding Path=ChannelId}"/>
<Button Content="&#xEA8F;" FontFamily="Segoe MDL2 Assets" Visibility="Collapsed"/>
</StackPanel>
</Grid>
</ListViewItem>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</RefreshContainer>
<controls:Loading IsLoading="False" x:Name="loadingScreen" Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<ProgressRing Width="50" Height="50" IsActive="True"/>
</controls:Loading>
</Grid>
</Page>
+46 -2
View File
@@ -1,13 +1,57 @@
using Windows.UI.Xaml.Controls;
using FoxTube.Attributes;
using FoxTube.Models;
using System.Linq;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
namespace FoxTube.Views
{
/// <summary>
/// User's subscriptions list
/// </summary>
[Refreshable]
public sealed partial class Subscriptions : Page
{
public Subscriptions() =>
public Subscriptions()
{
InitializeComponent();
list.ItemsSource = UserManagement.CurrentUser.Subscriptions;
}
private void List_ItemClick(object sender, ItemClickEventArgs e)
{
// TODO: Navigate to channel
}
private async void Unsubscribe(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
loadingScreen.IsLoading = true;
await UserManagement.CurrentUser.UpdateSubscriptionState((sender as Button).Tag as string);
list.ItemsSource = UserManagement.CurrentUser.Subscriptions;
loadingScreen.IsLoading = false;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.NavigationMode == NavigationMode.Refresh)
RefreshSubscriptions();
}
private void RefreshContainer_RefreshRequested(RefreshContainer sender, RefreshRequestedEventArgs args) =>
RefreshSubscriptions();
private async void RefreshSubscriptions()
{
loadingScreen.IsLoading = true;
await UserManagement.CurrentUser.LoadSubscriptions();
list.ItemsSource = UserManagement.CurrentUser.Subscriptions;
MainPage.Current.UpdateSubscriptions();
loadingScreen.IsLoading = false;
}
}
}