1.0-dev17 (Subscription page created)

This commit is contained in:
2022-05-05 15:14:26 +02:00
Unverified
parent e23258a638
commit 7a57fb65f3
14 changed files with 1043 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Core.Enums
{
public enum SubscriptionStatus
{
Added,
Loaded,
Ready
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VDownload.Core.Exceptions
{
public class SubscriptionExistsException : Exception
{
public SubscriptionExistsException() { }
public SubscriptionExistsException(string message) : base(message) { }
public SubscriptionExistsException(string message, Exception inner) : base(message, inner) { }
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
namespace VDownload.Core.Interfaces
{
public interface IPlaylist
{
#region PROPERTIES
// PLAYLIST PROPERTIES
string ID { get; }
PlaylistSource Source { get; }
Uri Url { get; }
string Name { get; }
IVideo[] Videos { get; }
#endregion
#region METHODS
// GET PLAYLIST METADATA
Task GetMetadataAsync(CancellationToken cancellationToken = default);
// GET VIDEOS FROM PLAYLIST
Task GetVideosAsync(CancellationToken cancellationToken = default);
Task GetVideosAsync(int numberOfVideos, CancellationToken cancellationToken = default);
#endregion
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using VDownload.Core.Enums;
using VDownload.Core.Structs;
using Windows.Storage;
namespace VDownload.Core.Interfaces
{
public interface IVideo
{
#region PROPERTIES
// VIDEO PROPERTIES
VideoSource Source { get; }
string ID { get; }
Uri Url { get; }
Metadata Metadata { get; }
BaseStream[] BaseStreams { get; }
#endregion
#region METHODS
// GET VIDEO METADATA
Task GetMetadataAsync(CancellationToken cancellationToken = default);
// GET VIDEO STREAMS
Task GetStreamsAsync(CancellationToken cancellationToken = default);
// DOWNLOAD VIDEO
Task<StorageFile> DownloadAndTranscodeAsync(StorageFolder downloadingFolder, BaseStream baseStream, MediaFileExtension extension, MediaType mediaType, TimeSpan trimStart, TimeSpan trimEnd, CancellationToken cancellationToken = default);
#endregion
#region EVENT HANDLERS
event EventHandler<EventArgs.ProgressChangedEventArgs> DownloadingProgressChanged;
event EventHandler<EventArgs.ProgressChangedEventArgs> ProcessingProgressChanged;
#endregion
}
}

View File

@@ -0,0 +1,86 @@
using System.Text.RegularExpressions;
using VDownload.Core.Enums;
using VDownload.Core.Interfaces;
namespace VDownload.Core.Services.Sources
{
public static class Source
{
#region CONSTANTS
// VIDEO SOURCES REGULAR EXPRESSIONS
private static readonly (Regex Regex, VideoSource Type)[] VideoSources = new (Regex Regex, VideoSource Type)[]
{
(new Regex(@"^https://www.twitch.tv/videos/(?<id>\d+)"), VideoSource.TwitchVod),
(new Regex(@"^https://www.twitch.tv/\S+/clip/(?<id>[^?]+)"), VideoSource.TwitchClip),
(new Regex(@"^https://clips.twitch.tv/(?<id>[^?]+)"), VideoSource.TwitchClip),
};
// PLAYLIST SOURCES REGULAR EXPRESSIONS
private static readonly (Regex Regex, PlaylistSource Type)[] PlaylistSources = new (Regex Regex, PlaylistSource Type)[]
{
(new Regex(@"^https://www.twitch.tv/(?<id>[^?/]+)"), PlaylistSource.TwitchChannel),
};
#endregion
#region METHODS
// GET VIDEO SOURCE
public static IVideo GetVideo(string url)
{
VideoSource source = VideoSource.Null;
string id = string.Empty;
foreach ((Regex Regex, VideoSource Type) Source in VideoSources)
{
Match sourceMatch = Source.Regex.Match(url);
if (sourceMatch.Success)
{
source = Source.Type;
id = sourceMatch.Groups["id"].Value;
}
}
return GetVideo(source, id);
}
public static IVideo GetVideo(VideoSource source, string id)
{
IVideo videoService = null;
switch (source)
{
case VideoSource.TwitchVod: videoService = new Twitch.Vod(id); break;
case VideoSource.TwitchClip: videoService = new Twitch.Clip(id); break;
}
return videoService;
}
// GET PLAYLIST SOURCE
public static IPlaylist GetPlaylist(string url)
{
PlaylistSource source = PlaylistSource.Null;
string id = string.Empty;
foreach ((Regex Regex, PlaylistSource Type) Source in PlaylistSources)
{
Match sourceMatch = Source.Regex.Match(url);
if (sourceMatch.Success)
{
source = Source.Type;
id = sourceMatch.Groups["id"].Value;
}
}
return GetPlaylist(source, id);
}
public static IPlaylist GetPlaylist(PlaylistSource source, string id)
{
IPlaylist playlistService = null;
switch (source)
{
case PlaylistSource.TwitchChannel: playlistService = new Twitch.Channel(id); break;
}
return playlistService;
}
#endregion
}
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using VDownload.Core.Interfaces;
namespace VDownload.Core.Services
{
[Serializable]
public class Subscription
{
#region CONSTRUCTORS
public Subscription(IPlaylist playlist)
{
Playlist = playlist;
SavedVideos = Playlist.Videos;
}
#endregion
#region PROPERTIES
public IPlaylist Playlist { get; private set; }
public IVideo[] SavedVideos { get; private set; }
#endregion
#region PUBLIC METHODS
public async Task<IVideo[]> GetNewVideosAsync()
{
await Playlist.GetVideosAsync();
return GetUnsavedVideos();
}
public async Task<IVideo[]> GetNewVideosAndUpdateAsync()
{
await Playlist.GetVideosAsync();
IVideo[] newVideos = GetUnsavedVideos();
SavedVideos = Playlist.Videos;
return newVideos;
}
#endregion
#region PRIVATE METHODS
private IVideo[] GetUnsavedVideos()
{
List<IVideo> newVideos = Playlist.Videos.ToList();
foreach (IVideo savedVideo in SavedVideos)
{
newVideos.RemoveAll((v) => v.Source == savedVideo.Source && v.ID == savedVideo.ID);
}
return newVideos.ToArray();
}
#endregion
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading.Tasks;
using VDownload.Core.Exceptions;
using Windows.Storage;
namespace VDownload.Core.Services
{
public static class SubscriptionsCollectionManagement
{
#region CONSTANTS
private static readonly StorageFolder SubscriptionFolderLocation = ApplicationData.Current.LocalFolder;
private static readonly string SubscriptionsFolderName = "Subscriptions";
private static readonly string SubscriptionFileExtension = "vsub";
#endregion
#region PUBLIC METHODS
public static async Task<(Subscription Subscription, StorageFile SubscriptionFile)[]> GetSubscriptionsAsync()
{
List<(Subscription Subscription, StorageFile SubscriptionFile)> subscriptions = new List<(Subscription Subscription,StorageFile SubscriptionFile)> ();
StorageFolder subscriptionsFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(SubscriptionsFolderName, CreationCollisionOption.OpenIfExists);
BinaryFormatter formatter = new BinaryFormatter();
foreach (StorageFile file in await subscriptionsFolder.GetFilesAsync())
{
if (file.Name.EndsWith(SubscriptionFileExtension))
{
Stream fileStream = await file.OpenStreamForReadAsync();
Subscription subscription = (Subscription)formatter.Deserialize(fileStream);
subscriptions.Add((subscription, file));
}
}
return subscriptions.ToArray();
}
public static async Task<StorageFile> CreateSubscriptionFileAsync(Subscription subscription)
{
StorageFolder subscriptionsFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(SubscriptionsFolderName, CreationCollisionOption.OpenIfExists);
try
{
StorageFile subscriptionFile = await subscriptionsFolder.CreateFileAsync($"{(int)subscription.Playlist.Source}-{subscription.Playlist.ID}.{SubscriptionFileExtension}", CreationCollisionOption.FailIfExists);
BinaryFormatter formatter = new BinaryFormatter();
Stream subscriptionFileStream = await subscriptionFile.OpenStreamForWriteAsync();
formatter.Serialize(subscriptionFileStream, subscription);
return subscriptionFile;
}
catch (Exception ex)
{
if ((uint)ex.HResult == 0x800700B7)
{
throw new SubscriptionExistsException($"Subscription with id \"{(int)subscription.Playlist.Source}-{subscription.Playlist.ID}\" already exists");
}
else
{
throw;
}
}
}
public static async Task UpdateSubscriptionFileAsync(Subscription subscription, StorageFile subscriptionFile)
{
BinaryFormatter formatter = new BinaryFormatter();
Stream subscriptionFileStream = await subscriptionFile.OpenStreamForWriteAsync();
formatter.Serialize(subscriptionFileStream, subscription);
}
public static async Task DeleteSubscriptionFileAsync(StorageFile subscriptionFile) => await subscriptionFile.DeleteAsync(StorageDeleteOption.PermanentDelete);
#endregion
}
}

View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Base_CloseButtonText" xml:space="preserve">
<value>OK</value>
</data>
<data name="Subscription_Adding_Base_Title" xml:space="preserve">
<value>Playlist adding error</value>
</data>
<data name="Subscription_Adding_InternetNotAvailable_Content" xml:space="preserve">
<value>Unable to connect to servers. Check your internet connection.</value>
</data>
<data name="Subscription_Adding_PlaylistNotFound_Content" xml:space="preserve">
<value>Playlist not found. Check the URL.</value>
</data>
<data name="Subscription_Adding_SubscriptionExists_Content" xml:space="preserve">
<value>This playlist has been already subscribed</value>
</data>
<data name="Subscription_Adding_TwitchAccessTokenNotFound_Content" xml:space="preserve">
<value>To be able to subscribe Twitch playlists (Channels), you have to link your Twitch account with VDownload. Go to Sources page to sign in.</value>
</data>
<data name="Subscription_Adding_TwitchAccessTokenNotValid_Content" xml:space="preserve">
<value>There is a problem with linked Twitch account. Check Twitch login status in Sources page.</value>
</data>
</root>

View File

@@ -0,0 +1,34 @@
<Page
x:Class="VDownload.Views.Sources.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VDownload.Views.Sources"
xmlns:cc="using:VDownload.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.Resources>
<ResourceDictionary Source="ms-appx:///Resources/Icons.xaml"/>
</Page.Resources>
<Grid Padding="20" RowSpacing="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock x:Uid="Sources_HeaderTextBlock" Grid.Row="0" FontSize="28" FontWeight="SemiBold"/>
<StackPanel Grid.Row="1" Spacing="10">
<cc:SettingControl x:Name="TwitchSettingControl" x:Uid="Sources_TwitchSettingControl" Grid.Row="0" Icon="{StaticResource TwitchIcon}" Title="Twitch">
<cc:SettingControl.SettingContent>
<Button x:Name="TwitchSettingControlLoginButton" x:Uid="Sources_TwitchSettingControl_LoginButton" IsEnabled="False" Click="TwitchSettingControlLoginButton_Click"/>
</cc:SettingControl.SettingContent>
</cc:SettingControl>
</StackPanel>
</Grid>
</Page>

View File

@@ -0,0 +1,212 @@
using Microsoft.Toolkit.Uwp.Connectivity;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Windows.ApplicationModel.Resources;
using Windows.UI.WindowManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Hosting;
using Windows.UI.Xaml.Navigation;
namespace VDownload.Views.Sources
{
public sealed partial class MainPage : Page
{
#region CONSTRUCTORS
public MainPage()
{
InitializeComponent();
}
#endregion
#region EVENT HANDLERS
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
Task[] checkingTasks = new Task[1];
checkingTasks[0] = CheckTwitch();
await Task.WhenAll(checkingTasks);
}
#endregion
#region PRIVATE METHODS
#endregion
private async Task CheckTwitch()
{
try
{
string twitchAccessToken = await Core.Services.Sources.Twitch.Helpers.Auth.ReadAccessTokenAsync();
#pragma warning disable IDE0042 // Deconstruct variable declaration
(bool IsValid, string Login, DateTime? ExpirationDate) twitchAccessTokenValidation = await Core.Services.Sources.Twitch.Helpers.Auth.ValidateAccessTokenAsync(twitchAccessToken);
#pragma warning restore IDE0042 // Deconstruct variable declaration
if (twitchAccessTokenValidation.IsValid)
{
TwitchSettingControl.Description = $"{ResourceLoader.GetForCurrentView().GetString("Sources_TwitchSettingControl_Description_LoggedIn")} {twitchAccessTokenValidation.Login}";
TwitchSettingControlLoginButton.Content = ResourceLoader.GetForCurrentView().GetString("Sources_TwitchSettingControl_LoginButton_Content_LoggedIn");
}
else
{
if (twitchAccessToken != null)
{
TwitchSettingControl.Description = ResourceLoader.GetForCurrentView().GetString("Sources_TwitchSettingControl_Description_AccessTokenExpired");
}
else if (twitchAccessTokenValidation.ExpirationDate < DateTime.Now)
{
TwitchSettingControl.Description = ResourceLoader.GetForCurrentView().GetString("Sources_TwitchSettingControl_Description_NotLoggedIn");
}
Debug.WriteLine(twitchAccessTokenValidation.ExpirationDate.Value.ToString("dd.MM.yyyy"));
TwitchSettingControlLoginButton.Content = ResourceLoader.GetForCurrentView().GetString("Sources_TwitchSettingControl_LoginButton_Content_NotLoggedIn");
}
TwitchSettingControlLoginButton.IsEnabled = true;
}
catch (WebException ex)
{
if (!NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable)
{
TwitchSettingControl.Description = ResourceLoader.GetForCurrentView().GetString("Sources_TwitchSettingControl_Description_InternetNotAvailable");
TwitchSettingControlLoginButton.Content = ResourceLoader.GetForCurrentView().GetString("Sources_TwitchSettingControl_LoginButton_Content_NotLoggedIn");
TwitchSettingControlLoginButton.IsEnabled = false;
}
else throw;
}
}
#region TWITCH
// TWITCH LOGIN BUTTON CLICKED
private async void TwitchSettingControlLoginButton_Click(object sender, RoutedEventArgs e)
{
try
{
string accessToken = await Core.Services.Sources.Twitch.Helpers.Auth.ReadAccessTokenAsync();
var accessTokenValidation = await Core.Services.Sources.Twitch.Helpers.Auth.ValidateAccessTokenAsync(accessToken);
if (accessTokenValidation.IsValid)
{
// Revoke access token
await Core.Services.Sources.Twitch.Helpers.Auth.RevokeAccessTokenAsync(accessToken);
// Delete access token
await Core.Services.Sources.Twitch.Helpers.Auth.DeleteAccessTokenAsync();
// Update Twitch SettingControl
TwitchSettingControl.Description = ResourceLoader.GetForCurrentView().GetString("Sources_TwitchSettingControl_Description_NotLoggedIn");
TwitchSettingControlLoginButton.Content = ResourceLoader.GetForCurrentView().GetString("Sources_TwitchSettingControl_LoginButton_Content_NotLoggedIn");
}
else
{
// Open new window
AppWindow TwitchAuthWindow = await AppWindow.TryCreateAsync();
TwitchAuthWindow.Title = "Twitch Authentication";
#pragma warning disable CS8305 // Type is for evaluation purposes only and is subject to change or removal in future updates.
WebView2 TwitchAuthWebView = new WebView2();
await TwitchAuthWebView.EnsureCoreWebView2Async();
TwitchAuthWebView.Source = Core.Services.Sources.Twitch.Helpers.Auth.AuthorizationUrl;
ElementCompositionPreview.SetAppWindowContent(TwitchAuthWindow, TwitchAuthWebView);
// NavigationStarting event (only when redirected)
TwitchAuthWebView.NavigationStarting += async (s, a) =>
{
if (new Uri(a.Uri).Host == Core.Services.Sources.Twitch.Helpers.Auth.RedirectUrl.Host)
{
// Close window
await TwitchAuthWindow.CloseAsync();
// Get response
string response = a.Uri.Replace(Core.Services.Sources.Twitch.Helpers.Auth.RedirectUrl.OriginalString, "");
if (response[1] == '#')
{
// Get access token
accessToken = response.Split('&')[0].Replace("/#access_token=", "");
// Check token
accessTokenValidation = await Core.Services.Sources.Twitch.Helpers.Auth.ValidateAccessTokenAsync(accessToken);
// Save token
await Core.Services.Sources.Twitch.Helpers.Auth.SaveAccessTokenAsync(accessToken);
// Update Twitch SettingControl
TwitchSettingControl.Description = $"{ResourceLoader.GetForCurrentView().GetString("Sources_TwitchSettingControl_Description_LoggedIn")} {accessTokenValidation.Login}";
TwitchSettingControlLoginButton.Content = ResourceLoader.GetForCurrentView().GetString("Sources_TwitchSettingControl_LoginButton_Content_LoggedIn");
}
else
{
// Ignored errors
string[] ignoredErrors = new[]
{
"The user denied you access",
};
// Errors translation
Dictionary<string, string> errorsTranslation = new Dictionary<string, string>
{
};
// Get error info
string errorInfo = (response.Split('&')[1].Replace("error_description=", "")).Replace('+', ' ');
if (!ignoredErrors.Contains(errorInfo))
{
// Error
ContentDialog loginErrorDialog = new ContentDialog
{
Title = ResourceLoader.GetForCurrentView().GetString("SourcesTwitchLoginErrorDialogTitle"),
Content = errorsTranslation.Keys.Contains(errorInfo) ? errorsTranslation[errorInfo] : $"{ResourceLoader.GetForCurrentView().GetString("SourcesTwitchLoginErrorDialogDescriptionUnknown")} ({errorInfo})",
CloseButtonText = ResourceLoader.GetForCurrentView().GetString("CloseErrorDialogButtonText"),
};
await loginErrorDialog.ShowAsync();
}
}
}
};
#pragma warning restore CS8305 // Type is for evaluation purposes only and is subject to change or removal in future updates.
await TwitchAuthWindow.TryShowAsync();
// Clear cache
TwitchAuthWebView.CoreWebView2.CookieManager.DeleteAllCookies();
}
}
catch (WebException wex)
{
if (!NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable)
{
TwitchSettingControl.Description = ResourceLoader.GetForCurrentView().GetString("SourcesTwitchSettingControlDescriptionInternetConnectionError");
TwitchSettingControlLoginButton.Content = ResourceLoader.GetForCurrentView().GetString("SourcesTwitchLoginButtonTextNotLoggedIn");
TwitchSettingControlLoginButton.IsEnabled = false;
ContentDialog internetAccessErrorDialog = new ContentDialog
{
Title = ResourceLoader.GetForCurrentView().GetString("SourcesTwitchLoginErrorDialogTitle"),
Content = ResourceLoader.GetForCurrentView().GetString("SourcesTwitchLoginErrorDialogDescriptionInternetConnectionError"),
CloseButtonText = ResourceLoader.GetForCurrentView().GetString("CloseErrorDialogButtonText"),
};
await internetAccessErrorDialog.ShowAsync();
}
else throw;
}
}
#endregion
}
}

View File

@@ -0,0 +1,36 @@
<UserControl
x:Class="VDownload.Views.Subscriptions.Controls.SubscriptionPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VDownload.Views.Subscriptions.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400"
CornerRadius="{ThemeResource ControlCornerRadius}">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///Resources/Colors.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Background="{ThemeResource SubscriptionsSubscriptionPanelBackgroundColor}" Padding="5" ColumnSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="TitleTextBlock" Grid.Column="1" FontSize="16" FontWeight="SemiBold" VerticalAlignment="Center"/>
<TextBlock x:Name="CountTextBlock" x:Uid="Subscriptions_SubscriptionPanel_CountTextBlock" Grid.Column="2" FontSize="12" VerticalAlignment="Center"/>
<AppBarButton x:Name="UpdateButton" Grid.Column="3" Width="40" Height="48" Margin="0,-4,0,-4" Icon="Sync" Click="UpdateButton_Click"/>
<AppBarButton x:Name="RemoveButton" Grid.Column="4" Width="40" Height="48" Margin="0,-4,0,-4" Icon="Delete" Click="RemoveButton_Click"/>
<AppBarButton x:Name="SourceButton" Grid.Column="5" Width="40" Height="48" Margin="0,-4,0,-4" Icon="Link" Click="SourceButton_Click"/>
</Grid>
</UserControl>

View File

@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using VDownload.Core.Services;
using Windows.ApplicationModel.Resources;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.Storage;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
namespace VDownload.Views.Subscriptions.Controls
{
public sealed partial class SubscriptionPanel : UserControl
{
#region CONSTRUCTORS
public SubscriptionPanel(Subscription subscription, StorageFile subscriptionFile)
{
InitializeComponent();
Subscription = subscription;
SubscriptionFile = subscriptionFile;
TitleTextBlock.Text = Subscription.Playlist.Name;
SourceButton.Icon = new BitmapIcon { UriSource = new Uri($"ms-appx:///Assets/Sources/{Subscription.Playlist.GetType().Namespace.Split(".").Last()}.png"), ShowAsMonochrome = false };
}
#endregion
#region PROPERTIES
private Subscription Subscription { get; set; }
private StorageFile SubscriptionFile { get; set; }
#endregion
#region PUBLIC METHODS
public async Task UpdateNewVideosCounterAsync()
{
CountTextBlock.Text = (await Subscription.GetNewVideosAsync()).Length.ToString();
}
#endregion
#region EVENT HANDLERS
private async void RemoveButton_Click(object sender, RoutedEventArgs e)
{
await SubscriptionsCollectionManagement.DeleteSubscriptionFileAsync(SubscriptionFile);
((StackPanel)Parent).Children.Remove(this);
}
private async void UpdateButton_Click(object sender, RoutedEventArgs e)
{
CountTextBlock.Text = ResourceLoader.GetForCurrentView().GetString("Subscriptions_SubscriptionPanel_CountTextBlock_SyncText");
await Subscription.GetNewVideosAndUpdateAsync();
await SubscriptionsCollectionManagement.UpdateSubscriptionFileAsync(Subscription, SubscriptionFile);
CountTextBlock.Text = "0";
}
private async void SourceButton_Click(object sender, RoutedEventArgs e)
{
await Windows.System.Launcher.LaunchUriAsync(Subscription.Playlist.Url);
}
#endregion
}
}

View File

@@ -0,0 +1,44 @@
<Page
x:Class="VDownload.Views.Subscriptions.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:VDownload.Views.Subscriptions"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///Resources/Colors.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Page.Resources>
<Grid Padding="20" RowSpacing="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock x:Uid="Subscriptions_HeaderTextBlock" Grid.Row="0" FontSize="28" FontWeight="SemiBold"/>
<ScrollViewer Grid.Row="1">
<StackPanel x:Name="SubscriptionsListStackPanel" VerticalAlignment="Stretch" Spacing="10"/>
</ScrollViewer>
<Grid Grid.Row="2" VerticalAlignment="Center" Padding="8" CornerRadius="{ThemeResource ControlCornerRadius}" ColumnSpacing="10" Background="{ThemeResource SubscriptionsAddingPanelBackgroundColor}">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="AddingTextBox" x:Uid="Subscriptions_AddingTextBox" Grid.Column="0" VerticalAlignment="Center"/>
<Button x:Name="AddingButton" x:Uid="Subscriptions_AddingButton" Grid.Column="1" Click="AddingButton_Click"/>
<muxc:ProgressRing x:Name="AddingProgressRing" Grid.Column="2" Visibility="Collapsed" Width="15" Height="15" VerticalAlignment="Center"/>
</Grid>
</Grid>
</Page>

View File

@@ -0,0 +1,148 @@
using Microsoft.Toolkit.Uwp.Connectivity;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading.Tasks;
using VDownload.Core.Exceptions;
using VDownload.Core.Interfaces;
using VDownload.Core.Services;
using VDownload.Core.Services.Sources;
using VDownload.Views.Subscriptions.Controls;
using Windows.ApplicationModel.Resources;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.Storage;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
namespace VDownload.Views.Subscriptions
{
public sealed partial class MainPage : Page
{
#region CONSTRUCTORS
public MainPage()
{
InitializeComponent();
}
#endregion
#region EVENT HANDLERS
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
(Subscription Subscription, StorageFile SubscriptionFile)[] subscriptions = await SubscriptionsCollectionManagement.GetSubscriptionsAsync();
foreach((Subscription Subscription, StorageFile SubscriptionFile) subscription in subscriptions)
{
AddSubscriptionToList(subscription.Subscription, subscription.SubscriptionFile);
}
}
private async void AddingButton_Click(object sender, RoutedEventArgs e)
{
async Task ShowDialog(string localErrorKey)
{
ContentDialog errorDialog = new ContentDialog
{
Title = ResourceLoader.GetForCurrentView("DialogResources").GetString("Subscription_Adding_Base_Title"),
Content = ResourceLoader.GetForCurrentView("DialogResources").GetString($"Subscription_Adding_{localErrorKey}_Content"),
CloseButtonText = ResourceLoader.GetForCurrentView("DialogResources").GetString("Base_CloseButtonText"),
};
await errorDialog.ShowAsync();
AddingProgressRing.Visibility = Visibility.Collapsed;
AddingButton.IsEnabled = true;
}
AddingProgressRing.Visibility = Visibility.Visible;
AddingButton.IsEnabled = false;
IPlaylist playlist = Source.GetPlaylist(AddingTextBox.Text); // TODO: Change name of class (method returns Playlist object, not source of playlist)
if (playlist is null)
{
await ShowDialog("PlaylistNotFound");
return;
}
else
{
try
{
await playlist.GetMetadataAsync();
await playlist.GetVideosAsync();
}
catch (MediaNotFoundException)
{
await ShowDialog("PlaylistNotFound");
return;
}
catch (TwitchAccessTokenNotFoundException)
{
await ShowDialog("TwitchAccessTokenNotFound");
return;
}
catch (TwitchAccessTokenNotValidException)
{
await ShowDialog("TwitchAccessTokenNotValid");
return;
}
catch (WebException)
{
if (!NetworkHelper.Instance.ConnectionInformation.IsInternetAvailable)
{
await ShowDialog("InternetNotAvailable");
return;
}
else throw;
}
}
Subscription subscription = new Subscription(playlist);
StorageFile subscriptionFile = null;
try
{
subscriptionFile = await SubscriptionsCollectionManagement.CreateSubscriptionFileAsync(subscription);
}
catch (SubscriptionExistsException)
{
await ShowDialog("SubscriptionExists");
return;
}
AddSubscriptionToList(subscription, subscriptionFile);
AddingProgressRing.Visibility = Visibility.Collapsed;
AddingButton.IsEnabled = true;
AddingTextBox.Text = string.Empty;
}
#endregion
#region PRIVATE METHODS
private void AddSubscriptionToList(Subscription subscription, StorageFile subscriptionFile)
{
SubscriptionPanel subscriptionPanel = new SubscriptionPanel(subscription, subscriptionFile);
SubscriptionsListStackPanel.Children.Add(subscriptionPanel);
subscriptionPanel.UpdateNewVideosCounterAsync();
}
#endregion
}
}