diff --git a/FoxTube/App.xaml.cs b/FoxTube/App.xaml.cs index dc925ae..ab6a176 100644 --- a/FoxTube/App.xaml.cs +++ b/FoxTube/App.xaml.cs @@ -66,6 +66,9 @@ namespace FoxTube Window.Current.Content = new MainPage(e.SplashScreen, e); Window.Current.Activate(); + if (e.Kind == ActivationKind.Protocol) + Authenticator.SetResponse((e as ProtocolActivatedEventArgs).Uri); + /*if(e.Kind == ActivationKind.ToastNotification) { if (UserManagement.IsAuthorized) diff --git a/FoxTube/Classes/Authenticator.cs b/FoxTube/Classes/Authenticator.cs new file mode 100644 index 0000000..c103947 --- /dev/null +++ b/FoxTube/Classes/Authenticator.cs @@ -0,0 +1,226 @@ +using Google.Apis.Oauth2.v2; +using Google.Apis.YouTube.v3; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Windows.Data.Json; +using Windows.Security.Cryptography; +using Windows.Security.Cryptography.Core; +using Windows.Storage.Streams; +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Auth.OAuth2; + +namespace FoxTube.Classes +{ + public static class Authenticator + { + const string redirectURI = "xfox111.foxtube:/oauth2redirect"; + const string authorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth"; + const string tokenEndpoint = "https://www.googleapis.com/oauth2/v4/token"; + + readonly static string scopes = Oauth2Service.Scope.UserinfoProfile + " " + + Oauth2Service.Scope.UserinfoEmail + " " + + YouTubeService.Scope.YoutubeForceSsl; + + + static Uri responseUri = null; + static string state = null; + static string codeVerifier = null; + + + public static async Task Authorize(string clientId) + { + await SendRequest(clientId); + await WaitForResponse(); + string code = ProcessResponse(responseUri); + JsonObject response = await ExchangeForTokens(clientId, code); + + ClearFields(); + + return new TokenResponse + { + AccessToken = response.GetNamedString("access_token"), + ExpiresInSeconds = 3600, + IssuedUtc = DateTime.UtcNow, + TokenType = response.GetNamedString("token_type"), + IdToken = response.GetNamedString("id_token"), + RefreshToken = response.GetNamedString("refresh_token"), + Scope = response.GetNamedString("scope") + }; + } + + public static async Task RefreshToken(string clientId, string refreshToken) => + await RefreshToken(clientId, refreshToken, null); + + public static async Task RefreshToken(string clientId, string refreshToken, string idToken) + { + HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Host = "www.googleapis.com"; + client.DefaultRequestHeaders.Add("Content-Type", "application/x-www-form-urlencoded"); + + Dictionary body = new Dictionary + { + { "client_id", clientId }, + { "refresh_token", refreshToken }, + { "grant_type", "refresh_token" } + }; + + using(HttpResponseMessage raw = await client.PostAsync("https://www.googleapis.com/oauth2/v4/token", new FormUrlEncodedContent(body))) + { + if (!raw.IsSuccessStatusCode) + return null; + + JsonObject response = JsonObject.Parse(await raw.Content.ReadAsStringAsync()); + + return new TokenResponse + { + AccessToken = response.GetNamedString("access_token"), + IssuedUtc = DateTime.UtcNow, + RefreshToken = refreshToken, + Scope = scopes, + TokenType = "Bearer", + ExpiresInSeconds = int.Parse(response.GetNamedString("expires_in")), + IdToken = idToken + }; + } + } + + static async Task WaitForResponse() + { + while (responseUri == null) + await Task.Delay(1000); + } + + public static void SetResponse(Uri response) => + responseUri = response; + + static async Task SendRequest(string clientId) + { + // Generates state and PKCE values. + state = randomDataBase64url(32); + codeVerifier = randomDataBase64url(32); + string code_challenge = base64urlencodeNoPadding(sha256(codeVerifier)); + + // Creates the OAuth 2.0 authorization request. + string authorizationRequest = string.Format("{0}?response_type=code&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}&scope={6}", + authorizationEndpoint, + Uri.EscapeDataString(redirectURI), + clientId, + state, + code_challenge, + "S256", + scopes); + + // Opens the Authorization URI in the browser. + await Windows.System.Launcher.LaunchUriAsync(new Uri(authorizationRequest)); + } + + public static string ProcessResponse(Uri response) + { + // Gets URI from navigation parameters. + string queryString = response.Query; + + // Parses URI params into a dictionary + // ref: http://stackoverflow.com/a/11957114/72176 + Dictionary queryStringParams = + queryString.Substring(1).Split('&') + .ToDictionary(c => c.Split('=')[0], + c => Uri.UnescapeDataString(c.Split('=')[1])); + + if (!queryStringParams.ContainsKey("code") + || !queryStringParams.ContainsKey("state") + || queryStringParams.ContainsKey("error")) + return null; + + // Gets the Authorization state + string incoming_state = queryStringParams["state"]; + + // Compares the receieved state to the expected value, to ensure that + // this app made the request which resulted in authorization + if (incoming_state != state) + return null; + + // Returns the Authorization code + return queryStringParams["code"]; + } + + + static async Task ExchangeForTokens(string clientId, string code) + { + // Builds the Token request + string tokenRequestBody = string.Format("code={0}&redirect_uri={1}&client_id={2}&code_verifier={3}&scope=&grant_type=authorization_code", + code, + Uri.EscapeDataString(redirectURI), + clientId, + codeVerifier + ); + StringContent content = new StringContent(tokenRequestBody, Encoding.UTF8, "application/x-www-form-urlencoded"); + + // Performs the authorization code exchange. + HttpClientHandler handler = new HttpClientHandler(); + handler.AllowAutoRedirect = true; + HttpClient client = new HttpClient(handler); + + HttpResponseMessage response = await client.PostAsync(tokenEndpoint, content); + string responseString = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + return null; + + // Retrieves access token + return JsonObject.Parse(responseString); + } + + static void ClearFields() + { + codeVerifier = null; + state = null; + responseUri = null; + } + + + /// + /// Returns URI-safe data with a given input length. + /// + /// Input length (nb. output will be longer) + /// + static string randomDataBase64url(uint length) + { + IBuffer buffer = CryptographicBuffer.GenerateRandom(length); + return base64urlencodeNoPadding(buffer); + } + + /// + /// Returns the SHA256 hash of the input string. + /// + /// + /// + static IBuffer sha256(string inputStirng) + { + HashAlgorithmProvider sha = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Sha256); + IBuffer buff = CryptographicBuffer.ConvertStringToBinary(inputStirng, BinaryStringEncoding.Utf8); + return sha.HashData(buff); + } + + /// + /// Base64url no-padding encodes the given input buffer. + /// + /// + /// + static string base64urlencodeNoPadding(IBuffer buffer) + { + string base64 = CryptographicBuffer.EncodeToBase64String(buffer); + + // Converts base64 to base64url. + base64 = base64.Replace("+", "-"); + base64 = base64.Replace("/", "_"); + // Strips padding. + base64 = base64.Replace("=", ""); + + return base64; + } + } +} diff --git a/FoxTube/Classes/Extensions.cs b/FoxTube/Classes/Extensions.cs index 25da4d6..969207a 100644 --- a/FoxTube/Classes/Extensions.cs +++ b/FoxTube/Classes/Extensions.cs @@ -32,5 +32,8 @@ namespace FoxTube public static bool Belongs(this T obj, params T[] values) => values.Contains(obj); + + public static string GuardFromNull(this object obj, string str) => + string.IsNullOrWhiteSpace(str) ? string.Empty : str; } } diff --git a/FoxTube/Classes/Methods.cs b/FoxTube/Classes/Methods.cs index 393ce1a..263ff33 100644 --- a/FoxTube/Classes/Methods.cs +++ b/FoxTube/Classes/Methods.cs @@ -4,7 +4,9 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Windows.ApplicationModel.Core; using Windows.UI; +using Windows.UI.Core; using Windows.UI.ViewManagement; using Windows.UI.Xaml; @@ -80,6 +82,8 @@ namespace FoxTube titleBar.ButtonInactiveBackgroundColor = Colors.Transparent; titleBar.ButtonInactiveForegroundColor = Colors.Gray; titleBar.ButtonForegroundColor = Colors.White; + + CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = true; } } } diff --git a/FoxTube/Classes/Navigation.cs b/FoxTube/Classes/Navigation.cs index 972e4dc..3e356a5 100644 --- a/FoxTube/Classes/Navigation.cs +++ b/FoxTube/Classes/Navigation.cs @@ -12,6 +12,14 @@ namespace FoxTube public static void GoToSubscriptions() { + } + public static void GoToSettings() + { + + } + public static void GoToUploadPage() + { + } public static void GoToSearch(SearchParameters args) { diff --git a/FoxTube/Classes/Service.cs b/FoxTube/Classes/Service.cs index 03e2401..b9178ba 100644 --- a/FoxTube/Classes/Service.cs +++ b/FoxTube/Classes/Service.cs @@ -6,7 +6,6 @@ namespace FoxTube { public static class Service { - public static bool AdsCheckPerformed { get; private set; } = false; public static bool AdsDisabled { get; private set; } = true; public static event ProVersionStateChangedEventHandler Purchased; diff --git a/FoxTube/Classes/Settings.cs b/FoxTube/Classes/Settings.cs index c2cad10..f817af8 100644 --- a/FoxTube/Classes/Settings.cs +++ b/FoxTube/Classes/Settings.cs @@ -40,6 +40,8 @@ namespace FoxTube.Classes public bool processClipboard = true; public bool minimizeCommandbar = false; + public int selectedUser = -1; + static string GetLanguage() { if ((new[] { "ua", "ru", "by", "kz", "kg", "md", "lv", "ee" }).Contains(CultureInfo.InstalledUICulture.TwoLetterISOLanguageName)) @@ -157,6 +159,15 @@ namespace FoxTube.Classes SaveData(); } } + public static int SelectedUser + { + get => Container.selectedUser; + set + { + Container.selectedUser = value; + SaveData(); + } + } public static bool[] HasAccount => Container.hasAccount; public static void SetAccount(int index, bool isAuthorized) { diff --git a/FoxTube/Classes/User.cs b/FoxTube/Classes/User.cs index 6b5398e..883ef43 100644 --- a/FoxTube/Classes/User.cs +++ b/FoxTube/Classes/User.cs @@ -3,58 +3,239 @@ using YoutubeExplode.Models; using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using Google.Apis.Oauth2.v2.Data; -using Google.Apis.Auth.OAuth2; -using Google.Apis.YouTube.v3; -using Google.Apis.Services; -using System.Threading; using YoutubeExplode; using System.Net.Http; +using Google.Apis.Auth.OAuth2.Responses; +using Newtonsoft.Json; +using Windows.UI.Xaml; +using Windows.UI.Popups; namespace FoxTube.Classes { public class User { - public List Subscriptions { get; } + public event SubscriptionChangedEventHandler SubscriptionsChanged; - public List WatchLater { get; } - public List WebHistory { get; } + public List Subscriptions { get; } = new List(); + + public List WatchLater { get; private set; } + public List WebHistory { get; private set; } public List AppHistory { get; } - public Userinfoplus UserInfo { get; } - public UserCredential Credential { get; } - - public YouTubeService Service { get; } + public Userinfoplus UserInfo { get; private set; } + public TokenResponse Tokens { get; private set; } public YoutubeClient YoutubeClient { get; } - public string AccountId { get; } - public Google.Apis.YouTube.v3.Data.Channel Channel { get; } + public string AccountId => Channel.Id; + public Google.Apis.YouTube.v3.Data.Channel Channel { get; private set; } - public async void Deauthenticate() + HttpClient HttpClient { get; } + + DispatcherTimer timer = new DispatcherTimer(); + + public async Task Deauthenticate() => + await HttpClient.GetAsync($"https://accounts.google.com/o/oauth2/revoke?token={Tokens.AccessToken}"); + + public User(TokenResponse tokens) { - await Credential.RevokeTokenAsync(CancellationToken.None); + Tokens = tokens; + + HttpClient = new HttpClient(); + HttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Tokens.AccessToken); + YoutubeClient = new YoutubeClient(HttpClient); + + timer.Interval = TimeSpan.FromSeconds((int)tokens.ExpiresInSeconds); + timer.Tick += Timer_Tick; + timer.Start(); } - public User(UserCredential credential) + async void Timer_Tick(object sender, object e) { - Credential = credential; - Service = new YouTubeService(new BaseClientService.Initializer + Tokens = await UserManagement.RefreshToken(); + timer.Interval = TimeSpan.FromSeconds((int)Tokens.ExpiresInSeconds); + timer.Start(); + } + + public async Task Initialize() + { + string responseRaw = await HttpClient.GetStringAsync("https://www.googleapis.com/oauth2/v2/userinfo"); + dynamic response = JsonConvert.DeserializeObject(responseRaw); + + UserInfo = new Userinfoplus { - HttpClientInitializer = Credential, - ApplicationName = "FoxTube" - }); + Email = this.GuardFromNull((string)response.email), + FamilyName = this.GuardFromNull((string)response.family_name), + Gender = this.GuardFromNull((string)response.gender), + GivenName = this.GuardFromNull((string)response.given_name), + Hd = this.GuardFromNull((string)response.hd), + Id = this.GuardFromNull((string)response.id), + Link = this.GuardFromNull((string)response.link), + Locale = this.GuardFromNull((string)response.locale), + Name = this.GuardFromNull((string)response.name), + Picture = this.GuardFromNull((string)response.picture), + VerifiedEmail = true //As it said in documentation it's always true because it applies to primary e-mail + }; - HttpClient httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", credential.Token.AccessToken); - YoutubeClient = new YoutubeClient(httpClient); + string nextToken = null; + do + { + string query = $"https://www.googleapis.com/youtube/v3/subscriptions?part=snippet&maxResults=50&mine=true&order=relevance&access_token={Tokens.AccessToken}{(string.IsNullOrWhiteSpace(nextToken) ? "" : "&pageToken=" + nextToken)}"; + HttpResponseMessage r = await HttpClient.GetAsync(query); + MessageDialog dialog = new MessageDialog(await r.Content.ReadAsStringAsync(), r.StatusCode.ToString()); + await dialog.ShowAsync(); + + responseRaw = await HttpClient.GetStringAsync(query); + response = JsonConvert.DeserializeObject(responseRaw); + nextToken = response.nextPageToken; + foreach (dynamic item in response.items) + Subscriptions.Add(new Subscription + { + Snippet = new SubscriptionSnippet + { + ChannelId = item.channelId, + ChannelTitle = item.channelTitle, + Description = item.description, + PublishedAt = item.publishedAt, + Title = item.title, + Thumbnails = new ThumbnailDetails + { + Default__ = new Thumbnail + { + Height = item.thumbnails["default"].height, + Width = item.thumbnails["default"].width, + Url = item.thumbnails["default"].url, + }, + Medium = new Thumbnail + { + Height = item.thumbnails.medium.height, + Width = item.thumbnails.medium.width, + Url = item.thumbnails.medium.url, + }, + High = new Thumbnail + { + Height = item.thumbnails.high.height, + Width = item.thumbnails.high.width, + Url = item.thumbnails.high.url, + } + }, + ResourceId = new ResourceId + { + ChannelId = item.resourceId.channelId, + Kind = item.resourceId.kind + } + }, + ETag = item.etag, + Id = item.id, + Kind = "youtube#subscription" + }); + } + while (!string.IsNullOrWhiteSpace(nextToken)); + + responseRaw = await HttpClient.GetStringAsync("https://www.googleapis.com/youtube/v3/channels?mine=true&part=contentDetails,snippet,image"); + response = JsonConvert.DeserializeObject(responseRaw); + + Channel = new Google.Apis.YouTube.v3.Data.Channel + { + Kind = "youtube#channel", + ETag = response.items[0].etag, + Id = response.items[0].id, + ContentDetails = new ChannelContentDetails + { + RelatedPlaylists = new ChannelContentDetails.RelatedPlaylistsData + { + Likes = response.items[0].contentDetails.relatedPlaylists.likes, + Favorites = response.items[0].contentDetails.relatedPlaylists.favorites, + Uploads = response.items[0].contentDetails.relatedPlaylists.uploads, + WatchHistory = "HL", + WatchLater = "WL" + } + }, + Snippet = new ChannelSnippet + { + Title = response.items[0].snippet.title, + Description = response.items[0].snippet.description, + CustomUrl = response.items[0].snippet.customUrl, + PublishedAt = response.items[0].snippet.publishedAt, + DefaultLanguage = response.items[0].snippet.defaultLanguage, + Country = response.items[0].snippet.country, + Localized = new ChannelLocalization + { + Description = response.items[0].snippet.localized.description, + Title = response.items[0].snippet.localized.title, + }, + Thumbnails = new ThumbnailDetails + { + Default__ = new Thumbnail + { + Height = response.items[0].snippet.thumbnails["default"].height, + Width = response.items[0].snippet.thumbnails["default"].width, + Url = response.items[0].snippet.thumbnails["default"].url, + }, + Medium = new Thumbnail + { + Height = response.items[0].snippet.thumbnails.medium.height, + Width = response.items[0].snippet.thumbnails.medium.width, + Url = response.items[0].snippet.thumbnails.medium.url, + }, + High = new Thumbnail + { + Height = response.items[0].snippet.thumbnails.high.height, + Width = response.items[0].snippet.thumbnails.high.width, + Url = response.items[0].snippet.thumbnails.high.url, + } + } + }, + BrandingSettings = new ChannelBrandingSettings + { + Channel = new ChannelSettings + { + Country = response.items[0].brandingSettings.channel.country, + DefaultLanguage = response.items[0].brandingSettings.channel.defaultLanguage, + DefaultTab = response.items[0].brandingSettings.channel.defaultTab, + Description = response.items[0].brandingSettings.channel.description, + FeaturedChannelsTitle = response.items[0].brandingSettings.channel.featuredChannelsTitle, + FeaturedChannelsUrls = response.items[0].brandingSettings.channel.featuredChannelsUrls, + Keywords = response.items[0].brandingSettings.channel.keywords, + ModerateComments = response.items[0].brandingSettings.channel.moderateComments, + ProfileColor = response.items[0].brandingSettings.channel.profileColor, + ShowBrowseView = response.items[0].brandingSettings.channel.showBrowseView, + ShowRelatedChannels = response.items[0].brandingSettings.channel.showRelatedChannels, + Title = response.items[0].brandingSettings.channel.title, + TrackingAnalyticsAccountId = response.items[0].brandingSettings.channel.trackingAnalyticsAccountId, + UnsubscribedTrailer = response.items[0].brandingSettings.channel.unsubscribedTrailer + }, + Image = new ImageSettings + { + BannerImageUrl = response.items[0].brandingSettings.image.bannerImageUrl, + BannerMobileImageUrl = response.items[0].brandingSettings.image.bannerMobileImage.Url, + WatchIconImageUrl = response.items[0].brandingSettings.image.watchIconImageUrl, + TrackingImageUrl = response.items[0].brandingSettings.image.trackingImageUrl, + BannerTabletLowImageUrl = response.items[0].brandingSettings.image.bannerTabletLowImageUrl, + BannerTabletImageUrl = response.items[0].brandingSettings.image.bannerTabletImageUrl, + BannerTabletHdImageUrl = response.items[0].brandingSettings.image.bannerTabletHdImageUrl, + BannerTabletExtraHdImageUrl = response.items[0].brandingSettings.image.bannerTabletExtraHdImageUrl, + BannerMobileLowImageUrl = response.items[0].brandingSettings.image.bannerMobileLowImageUrl, + BannerMobileMediumHdImageUrl = response.items[0].brandingSettings.image.bannerMobileMediumHdImageUrl, + BannerMobileHdImageUrl = response.items[0].brandingSettings.image.bannerMobileHdImageUrl, + BannerMobileExtraHdImageUrl = response.items[0].brandingSettings.image.bannerMobileExtraHdImageUrl, + BannerTvLowImageUrl = response.items[0].brandingSettings.image.bannerTvLowImageUrl, + BannerTvImageUrl = response.items[0].brandingSettings.image.bannerTvImageUrl, + BannerTvMediumImageUrl = response.items[0].brandingSettings.image.bannerTvMediumImageUrl, + BannerTvHighImageUrl = response.items[0].brandingSettings.image.bannerTvHighImageUrl, + BannerExternalUrl = response.items[0].brandingSettings.image.bannerExternalUrl + }, + Hints = response.items[0].brandingSettings.hints + } + }; + + + WatchLater = (await YoutubeClient.GetPlaylistAsync("WL")).Videos.ToList(); + WebHistory = (await YoutubeClient.GetPlaylistAsync("HL")).Videos.ToList(); } - public async void RefreshToken() => - await Credential.RefreshTokenAsync(CancellationToken.None); - /// /// Subscribes or unsibscribes authorized user from the channel /// @@ -64,36 +245,84 @@ namespace FoxTube.Classes { if (Subscriptions.Find(i => i.Snippet.ResourceId.ChannelId == id) is Subscription subscription) { - try { await Service.Subscriptions.Delete(subscription.Id).ExecuteAsync(); } + try + { + HttpResponseMessage response = await HttpClient.DeleteAsync("https://www.googleapis.com/youtube/v3/subscriptions?id=" + subscription.Id); + if (response.StatusCode != System.Net.HttpStatusCode.NoContent) + return true; + } catch { return true; } - //SubscriptionsChanged?.Invoke("remove", subscription); + SubscriptionsChanged?.Invoke("remove", subscription); Subscriptions.Remove(subscription); return false; } else { - var request = Service.Subscriptions.Insert(new Subscription + Dictionary body = new Dictionary + { + { "snippet", " { \"resourceId\" : { \"channelId\" : " + id + " } }" } + }; + HttpResponseMessage response = await HttpClient.PostAsync("https://www.googleapis.com/youtube/v3/subscriptions?part=snippet", new FormUrlEncodedContent(body)); + + if (!response.IsSuccessStatusCode) + return false; + + dynamic raw = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + + Subscription sub = new Subscription { Snippet = new SubscriptionSnippet { + ChannelId = raw.channelId, + ChannelTitle = raw.channelTitle, + Description = raw.description, + PublishedAt = raw.publishedAt, + Title = raw.title, + Thumbnails = new ThumbnailDetails + { + Default__ = new Thumbnail + { + Height = raw.thumbnails["default"].height, + Width = raw.thumbnails["default"].width, + Url = raw.thumbnails["default"].url, + }, + Medium = new Thumbnail + { + Height = raw.thumbnails.medium.height, + Width = raw.thumbnails.medium.width, + Url = raw.thumbnails.medium.url, + }, + High = new Thumbnail + { + Height = raw.thumbnails.high.height, + Width = raw.thumbnails.high.width, + Url = raw.thumbnails.high.url, + } + }, ResourceId = new ResourceId { - ChannelId = id, - Kind = "youtube#channel" + ChannelId = raw.resourceId.channelId, + Kind = raw.resourceId.channelId } - } - }, "snippet"); - - if (!(await request.ExecuteAsync() is Subscription sub)) - return false; + }, + ETag = raw.etag, + Id = raw.id, + Kind = "youtube#subscription" + }; Subscriptions.Add(sub); - //SubscriptionsChanged?.Invoke("add", sub); + SubscriptionsChanged?.Invoke("add", sub); return true; } } + + public void Dispose() + { + timer.Stop(); + timer.Tick -= Timer_Tick; + } } } diff --git a/FoxTube/Classes/UserManagement.cs b/FoxTube/Classes/UserManagement.cs index 3ef8c5e..58fd2f7 100644 --- a/FoxTube/Classes/UserManagement.cs +++ b/FoxTube/Classes/UserManagement.cs @@ -1,49 +1,38 @@ using FoxTube.Classes; using Google.Apis.Auth.OAuth2; +using Google.Apis.Auth.OAuth2.Flows; +using Google.Apis.Auth.OAuth2.Responses; using Google.Apis.Oauth2.v2.Data; using Google.Apis.Services; using Google.Apis.YouTube.v3; +using Microsoft.AppCenter.Analytics; +using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; +using Windows.Security.Credentials; +using Windows.Storage; using YoutubeExplode; namespace FoxTube { public static class UserManagement { - public static event AuthorizationChangedEventHandler AuthorizationStateChanged; - public static event SubscriptionChangedEventHandler SubscriptionChanged; + static ApplicationDataContainer storage = ApplicationData.Current.LocalSettings; - static ClientSecrets[] Secrets => new ClientSecrets[5] + public static event AuthorizationChangedEventHandler AuthorizationStateChanged; + public static event SubscriptionChangedEventHandler SubscriptionsChanged; + + static string[] Secrets => new string[5] { - new ClientSecrets - { - ClientId = "349735264870-2ekqlm0a4mkg3mmrfcv90s3qp3o15dq0.apps.googleusercontent.com", - ClientSecret = "BkVZOAaCU2Zclf0Zlicg6y2_" - }, - new ClientSecrets - { - ClientId = "1096685398208-ha281qmg52ga8g8nn4n6943oct1tq6nj.apps.googleusercontent.com", - ClientSecret = "Cew9WbJcsoisYnR-Wuyhukov" - }, - new ClientSecrets - { - ClientId = "1096685398208-59ktbv2jsvalrmli1pevur9aujnjhjeb.apps.googleusercontent.com", - ClientSecret = "eHvXHlIJ9r64yReOmMnuben7" - }, - new ClientSecrets - { - ClientId = "1096685398208-ebckt9ncp69qutsmdbbufov60nq4rvpa.apps.googleusercontent.com", - ClientSecret = "XCaBFmjoT2ZAKsGf-y80lBvc" - }, - new ClientSecrets - { - ClientId = "1096685398208-ligbo8gqkp4qar1uo0e5o5di434qklma.apps.googleusercontent.com", - ClientSecret = "oXauUkFY9I7Q1CmNkKGmszRQ" - }, + "1096685398208-bktbnoe59bt69nhnrrb5j0tpld58orsv.apps.googleusercontent.com", + "1096685398208-u4a2pgpcn27c2kb3ud2eck1oh2ot68vs.apps.googleusercontent.com", + "1096685398208-a65ebfpqnhl7u3iipfmfe5cif6j07db3.apps.googleusercontent.com", + "1096685398208-in0gco58ckrumgjuo68st55fvb0ntllj.apps.googleusercontent.com", + "1096685398208-7gd029f6tku4sc756v2338g1f0fu4k2k.apps.googleusercontent.com" }; static YouTubeService NoAuthService { get; } = new YouTubeService(new BaseClientService.Initializer @@ -53,20 +42,40 @@ namespace FoxTube }); static YoutubeClient NoAuthClient { get; } = new YoutubeClient(); - public static YouTubeService Service => Current == null ? NoAuthService : Current.Service; - public static YoutubeClient YoutubeClient => Current == null ? NoAuthClient : Current.YoutubeClient; + public static YoutubeClient YoutubeClient => Current?.YoutubeClient ?? NoAuthClient; public static bool IsAuthorized => Current != null; public static User Current { get; set; } - static (Userinfoplus info, UserCredential credential)[] UserInfos = new (Userinfoplus, UserCredential)[5]; + public static Userinfoplus[] UserInfos { get; private set; } = new Userinfoplus[5]; + static string[] tokens = new string[5]; - public static bool CheckPerformed { get; } - - public static void Initialize() + public static async void Initialize() { - + if(storage.Values["users"] != null) + UserInfos = JsonConvert.DeserializeObject(storage.Values["users"] as string); + PasswordVault vault = new PasswordVault(); + foreach (Userinfoplus info in UserInfos) + { + if (info == null) + continue; + PasswordCredential credential = vault.Retrieve("FoxTube", info.Id); + credential.RetrievePassword(); + tokens[UserInfos.ToList().IndexOf(info)] = credential.Password; + } + + if(Settings.SelectedUser >= 0) + { + try { Current = new User(await Authenticator.RefreshToken(Secrets[Settings.SelectedUser], tokens[Settings.SelectedUser])); } + catch { Current = new User(await Authenticator.Authorize(Secrets[Settings.SelectedUser])); } + + await Current.Initialize(); + Current.SubscriptionsChanged += SubscriptionsChanged; + UserInfos[Settings.SelectedUser] = Current.UserInfo; + } + + AuthorizationStateChanged?.Invoke(Settings.SelectedUser > -1); } public static async Task AddItemToWL(string id) @@ -74,14 +83,91 @@ namespace FoxTube } + public static async void ChangeAccount(Userinfoplus newUser) + { + Current.Dispose(); + Current.SubscriptionsChanged -= SubscriptionsChanged; + + Current = null; + + Settings.SelectedUser = UserInfos.ToList().IndexOf(newUser); + + try { Current = new User(await Authenticator.RefreshToken(Secrets[Settings.SelectedUser], tokens[Settings.SelectedUser])); } + catch { Current = new User(await Authenticator.Authorize(Secrets[Settings.SelectedUser])); } + + await Current.Initialize(); + Current.SubscriptionsChanged += SubscriptionsChanged; + UserInfos[Settings.SelectedUser] = Current.UserInfo; + + AuthorizationStateChanged?.Invoke(true); + } + public static async void CreateNew() { + Analytics.TrackEvent("Initialized authorization sequence"); + try + { + for (int k = 0; k < 5; k++) + if (tokens[k] == null) + { + TokenResponse response = await Authenticator.Authorize(Secrets[k]); + tokens[k] = response.RefreshToken; + Current?.Dispose(); + if (Current != null) + Current.SubscriptionsChanged -= SubscriptionsChanged; + Current = new User(response); + await Current.Initialize(); + Current.SubscriptionsChanged += SubscriptionsChanged; + UserInfos[k] = Current.UserInfo; + storage.Values["users"] = JsonConvert.SerializeObject(UserInfos); + PasswordVault vault = new PasswordVault(); + vault.Add(new PasswordCredential("FoxTube", Current.UserInfo.Id, response.RefreshToken)); + + Settings.SelectedUser = k; + AuthorizationStateChanged?.Invoke(true); + break; + } + } + catch { } } public static async void Logout() { - Current?.Deauthenticate(); + Current.Dispose(); + Current.SubscriptionsChanged -= SubscriptionsChanged; + await Current.Deauthenticate(); + + PasswordVault vault = new PasswordVault(); + vault.Remove(vault.Retrieve("FoxTube", Current.UserInfo.Id)); + UserInfos[Settings.SelectedUser] = null; + tokens[Settings.SelectedUser] = null; + storage.Values["users"] = JsonConvert.SerializeObject(UserInfos); + + Current = null; + + Settings.SelectedUser = -1; + for (int i = 0; i < 5; i++) + if (UserInfos[i] != null) + { + Settings.SelectedUser = i; + break; + } + + if (Settings.SelectedUser >= 0) + { + try { Current = new User(await Authenticator.RefreshToken(Secrets[Settings.SelectedUser], tokens[Settings.SelectedUser])); } + catch { Current = new User(await Authenticator.Authorize(Secrets[Settings.SelectedUser])); } + + await Current.Initialize(); + Current.SubscriptionsChanged += SubscriptionsChanged; + UserInfos[Settings.SelectedUser] = Current.UserInfo; + } + + AuthorizationStateChanged?.Invoke(Settings.SelectedUser > -1); } + + public static async Task RefreshToken() => + await Authenticator.RefreshToken(Secrets[Settings.SelectedUser], tokens[Settings.SelectedUser]); } } diff --git a/FoxTube/Controls/AccountManager.xaml b/FoxTube/Controls/AccountManager.xaml index f0cc915..d5b6908 100644 --- a/FoxTube/Controls/AccountManager.xaml +++ b/FoxTube/Controls/AccountManager.xaml @@ -6,6 +6,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" xmlns:ui="using:Microsoft.UI.Xaml.Controls" + xmlns:data="using:Google.Apis.Oauth2.v2.Data" Orientation="Horizontal"> @@ -17,7 +18,7 @@ - + @@ -28,32 +29,38 @@ - + - - + + - + - - + + - - - - - - - - - - + + + + + + + + + + + + + + - - - + + + + + @@ -62,7 +69,7 @@ - + diff --git a/FoxTube/Controls/AccountManager.xaml.cs b/FoxTube/Controls/AccountManager.xaml.cs index 1bfc668..2c0081f 100644 --- a/FoxTube/Controls/AccountManager.xaml.cs +++ b/FoxTube/Controls/AccountManager.xaml.cs @@ -1,9 +1,11 @@ -using Microsoft.AppCenter.Analytics; +using Google.Apis.Oauth2.v2.Data; +using Microsoft.AppCenter.Analytics; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; +using Windows.ApplicationModel.Resources; using Windows.Foundation; using Windows.Foundation.Collections; using Windows.UI.Xaml; @@ -21,25 +23,21 @@ namespace FoxTube.Controls { public sealed partial class AccountManager : StackPanel { + readonly ResourceLoader resources = ResourceLoader.GetForCurrentView("Main"); + public AccountManager() { InitializeComponent(); Service.Purchased += (purchased, price) => { - /*removeAds.Visibility = purchased ? Visibility.Collapsed : Visibility.Visible; - removeAds.Content = $"{resources.GetString("/Main/adsFree/Content")} ({price})";*/ + removeAds.Visibility = purchased ? Visibility.Collapsed : Visibility.Visible; + removeAds.Content = $"{resources.GetString("/Main/adsFree/Content")} ({price})"; }; - - if (Service.AdsCheckPerformed) - Service.CheckAddons(); } - void SignIn_Click(object sender, RoutedEventArgs e) - { + void SignIn_Click(object sender, RoutedEventArgs e) => UserManagement.CreateNew(); - Analytics.TrackEvent("Initialized authorization sequence"); - } void Logout_Click(object sender, RoutedEventArgs e) { @@ -47,18 +45,23 @@ namespace FoxTube.Controls UserManagement.Logout(); } - public async void Logged() //=> await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => + public async void Logged() => await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { account.Visibility = Visibility.Collapsed; - /*ToolTipService.SetToolTip(avatar, $"{UserManagement.Current.UserInfo.Name} ({UserManagement.Current.UserInfo.Email})"); + ToolTipService.SetToolTip(manager, $"{UserManagement.Current.UserInfo.Name} ({UserManagement.Current.UserInfo.Email})"); name.Text = UserManagement.Current.UserInfo.Name; email.Text = UserManagement.Current.UserInfo.Email; + banner.Source = new BitmapImage(UserManagement.Current.Channel.BrandingSettings.Image.BannerMobileLowImageUrl.ToUri()); avatar.ProfilePicture = new BitmapImage(UserManagement.Current.UserInfo.Picture.ToUri()) { DecodePixelHeight = 65, DecodePixelWidth = 65 }; - (icon.Fill as ImageBrush).ImageSource = new BitmapImage(UserManagement.Current.UserInfo.Picture.ToUri()) { DecodePixelHeight = 25, DecodePixelWidth = 25 };*/ + icon.ProfilePicture = new BitmapImage(UserManagement.Current.UserInfo.Picture.ToUri()) { DecodePixelHeight = 25, DecodePixelWidth = 25 }; + + foreach (Userinfoplus user in UserManagement.UserInfos) + if (user != UserManagement.Current.UserInfo) + accountsList.Items.Add(user); manager.Visibility = Visibility.Visible; - }//); + }); public void Quit() { @@ -66,27 +69,31 @@ namespace FoxTube.Controls account.Visibility = Visibility.Visible; } - private void NavigationViewItem_Tapped(object sender, TappedRoutedEventArgs e) - { - - } - void Manager_Tapped(object sender, TappedRoutedEventArgs e) => (sender as Microsoft.UI.Xaml.Controls.NavigationViewItem).ContextFlyout.ShowAt(sender as FrameworkElement); - private void Settings_Click(object sender, RoutedEventArgs e) - { + void Settings_Click(object sender, RoutedEventArgs e) => + Navigation.GoToSettings(); - } + void Settings_Click(object sender, TappedRoutedEventArgs e) => + Navigation.GoToSettings(); - private void Settings_Click(object sender, TappedRoutedEventArgs e) - { + void RemoveAds_Tapped(object sender, TappedRoutedEventArgs e) => + Service.GetPro(); - } + void Upload_Tapped(object sender, TappedRoutedEventArgs e) => + Navigation.GoToUploadPage(); - private void RemoveAds_Tapped(object sender, TappedRoutedEventArgs e) - { + void MyChannel_Tapped(object sender, TappedRoutedEventArgs e) => + Navigation.GoToChannel(UserManagement.Current.AccountId); - } + void AccountsList_ItemClick(object sender, ItemClickEventArgs e) => + UserManagement.ChangeAccount(e.ClickedItem as Userinfoplus); + + void AddAccount_Tapped(object sender, TappedRoutedEventArgs e) => + UserManagement.CreateNew(); + + void Logout_Tapped(object sender, TappedRoutedEventArgs e) => + UserManagement.Logout(); } } diff --git a/FoxTube/Controls/ContentFrame.xaml.cs b/FoxTube/Controls/ContentFrame.xaml.cs index a0c23ea..f9d1512 100644 --- a/FoxTube/Controls/ContentFrame.xaml.cs +++ b/FoxTube/Controls/ContentFrame.xaml.cs @@ -102,7 +102,7 @@ namespace FoxTube.Controls /// /// Hides loading UI /// - public void Complete(IEnumerable pivotCollection = null, int? selectedPivot = null) + public void Complete(IList pivotCollection = null, int? selectedPivot = null) { Navigated?.Invoke(pivotCollection, selectedPivot); loaded.Storyboard.Begin(); diff --git a/FoxTube/Controls/MainFrame.xaml.cs b/FoxTube/Controls/MainFrame.xaml.cs index 14921d2..81feb15 100644 --- a/FoxTube/Controls/MainFrame.xaml.cs +++ b/FoxTube/Controls/MainFrame.xaml.cs @@ -151,7 +151,7 @@ namespace FoxTube.Controls content.GoBack(); } - void Content_Navigated(System.Collections.IEnumerable pivotCollection, int? selectedPivot) => + void Content_Navigated(System.Collections.IList pivotCollection, int? selectedPivot) => Navigated?.Invoke(pivotCollection, selectedPivot); } } diff --git a/FoxTube/FoxTube.csproj b/FoxTube/FoxTube.csproj index 26d2c9d..781c48a 100644 --- a/FoxTube/FoxTube.csproj +++ b/FoxTube/FoxTube.csproj @@ -105,6 +105,7 @@ App.xaml + @@ -248,19 +249,19 @@ 0.12.1 - 1.30.0-beta02 + 1.40.3 - 1.30.0-beta02 + 1.40.3 - 1.30.0-beta02 + 1.40.3 - 1.29.2.994 + 1.40.3.1602 - 1.29.2.1006 + 1.40.3.1663 10.1811.22001 @@ -290,7 +291,7 @@ 4.3.0 - 4.7.6 + 4.7.8 diff --git a/FoxTube/Package.appxmanifest b/FoxTube/Package.appxmanifest index 27125f3..3482166 100644 --- a/FoxTube/Package.appxmanifest +++ b/FoxTube/Package.appxmanifest @@ -28,7 +28,9 @@ - + + FoxTube + diff --git a/FoxTube/Pages/MainPage.xaml.cs b/FoxTube/Pages/MainPage.xaml.cs index 5693b7c..2abe82c 100644 --- a/FoxTube/Pages/MainPage.xaml.cs +++ b/FoxTube/Pages/MainPage.xaml.cs @@ -23,20 +23,14 @@ namespace FoxTube.Pages Methods.SetTitleBar(); UserManagement.AuthorizationStateChanged += AuthorizationStateChanged; - UserManagement.SubscriptionChanged += SubscriptionChanged; + UserManagement.SubscriptionsChanged += SubscriptionChanged; if (StoreServicesFeedbackLauncher.IsSupported()) feedback.Visibility = Visibility.Visible; + UserManagement.Initialize(); + Service.CheckAddons(); //TODO: Initialize stuff - //TODO: Remove this method - Begin(); - } - - async void Begin() - { - await Task.Delay(5000); - FadeOut.Storyboard.Begin(); } void SetSplashScreen(SplashScreen splash) @@ -82,6 +76,7 @@ namespace FoxTube.Pages void AuthorizationStateChanged(bool isAuthorized) { + subscriptionsList.Items.Clear(); if (isAuthorized) { accountManager.Logged(); @@ -109,11 +104,12 @@ namespace FoxTube.Pages likedVideos.Visibility = watchLater.Visibility = subscriptionsHeader.Visibility = Visibility.Collapsed; - - subscriptionsList.Items.Clear(); } content.AuthorizationChanged(isAuthorized); + + if (splashScreen.Visibility == Visibility.Visible) + FadeOut.Storyboard.Begin(); } public void UpdateView() diff --git a/Src/FoxTube backup upload page/MainPage.xaml b/Src/FoxTube backup upload page/MainPage.xaml new file mode 100644 index 0000000..771585e --- /dev/null +++ b/Src/FoxTube backup upload page/MainPage.xaml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Src/FoxTube backup upload page/MainPage.xaml.cs b/Src/FoxTube backup upload page/MainPage.xaml.cs new file mode 100644 index 0000000..f616c9d --- /dev/null +++ b/Src/FoxTube backup upload page/MainPage.xaml.cs @@ -0,0 +1,569 @@ +using System; +using System.Collections.Generic; +using Windows.UI; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Navigation; +using Windows.UI.Xaml.Media.Imaging; +using System.Xml; +using Google.Apis.YouTube.v3.Data; +using Windows.ApplicationModel.Core; +using Windows.System; +using FoxTube.Pages; +using Windows.UI.Popups; +using Windows.ApplicationModel.Resources; +using Microsoft.Services.Store.Engagement; +using Windows.UI.Xaml.Shapes; +using Windows.UI.Xaml.Media; +using FoxTube.Controls; + +namespace FoxTube +{ + /// + /// Main app's layout + /// + public sealed partial class MainPage : Page + { + bool wasInvoked = false; + readonly ResourceLoader resources = ResourceLoader.GetForCurrentView("Main"); + Dictionary headers; + + public ContentFrame PageContent => content; + public ContentFrame VideoContent => videoPlaceholder; + + public MainPage() + { + headers = new Dictionary() + { + { typeof(Settings), resources.GetString("/Main/settings/Content") }, + { typeof(ChannelPage), resources.GetString("/Main/channel") }, + { typeof(PlaylistPage), resources.GetString("/Main/playlist") }, + { typeof(Search), resources.GetString("/Main/searchPlaceholder/PlaceholderText") }, + { typeof(Subscriptions), resources.GetString("/Main/subscriptions/Content") }, + { typeof(History), resources.GetString("/Main/history/Content") }, + { typeof(Home), resources.GetString("/Main/home/Content") }, + { typeof(UploadPage), "Uploading a video" }, + { typeof(Downloads), resources.GetString("/Main/downloads/Content") } + }; + + InitializeComponent(); + + Window.Current.SetTitleBar(AppTitleBar); + CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = true; + CoreApplication.GetCurrentView().TitleBar.LayoutMetricsChanged += (s, e) => SetTitleBar(s); + + SecretsVault.AuthorizationStateChanged += AuthorizationStateChanged; + SecretsVault.SubscriptionsChanged += SecretsVault_SubscriptionsChanged; + SecretsVault.Purchased += async (sender, e) => + { + removeAds.Visibility = (e[0] as bool?).Value ? Visibility.Collapsed : Visibility.Visible; + + if (!(bool)e[0]) + { + removeAds.Content = $"{resources.GetString("/Main/adsFree/Content")} ({e[1]})"; + return; + } + + MessageDialog dialog = new MessageDialog(resources.GetString("/Main/purchaseSuccess")); + dialog.Commands.Add(new UICommand(resources.GetString("/Main/close"), (command) => Methods.CloseApp())); + dialog.Commands.Add(new UICommand(resources.GetString("/Main/delay"))); + dialog.CancelCommandIndex = 1; + dialog.DefaultCommandIndex = 0; + await dialog.ShowAsync(); + }; + SecretsVault.Initialize(); + + if(StoreServicesFeedbackLauncher.IsSupported()) + feedback.Visibility = Visibility.Visible; + + PromptFeedback(); + } + + async void PromptFeedback() + { + if (SettingsStorage.Uptime.TotalHours >= 12 && SettingsStorage.PromptFeedback) + { + MessageDialog dialog = new MessageDialog(resources.GetString("/Main/feedbackMessage")); + dialog.Commands.Add(new UICommand(resources.GetString("/Main/dontAsk"), (command) => SettingsStorage.PromptFeedback = false)); + dialog.Commands.Add(new UICommand(resources.GetString("/Main/promptLater"))); + dialog.Commands.Add(new UICommand(resources.GetString("/Main/sure"), async (command) => + { + SettingsStorage.PromptFeedback = false; + if (StoreServicesFeedbackLauncher.IsSupported()) + await StoreServicesFeedbackLauncher.GetDefault().LaunchAsync(); + else + { + MessageDialog message = new MessageDialog(resources.GetString("/Main/feedbackFail")); + message.Commands.Add(new UICommand(resources.GetString("/Main/sendEmail"), async (c) => await Launcher.LaunchUriAsync("mailto:michael.xfox@outlook.com".ToUri()))); + message.Commands.Add(new UICommand(resources.GetString("/Main/goBack"))); + message.CancelCommandIndex = 1; + message.DefaultCommandIndex = 0; + await message.ShowAsync(); + } + })); + dialog.DefaultCommandIndex = 2; + dialog.CancelCommandIndex = 1; + await dialog.ShowAsync(); + } + + if (SettingsStorage.Uptime.TotalHours >= 24 && SettingsStorage.PromptReview) + { + MessageDialog dialog = new MessageDialog(resources.GetString("/Main/rate")); + dialog.Commands.Add(new UICommand(resources.GetString("/Main/dontAsk"), (command) => SettingsStorage.PromptReview = false)); + dialog.Commands.Add(new UICommand(resources.GetString("/Main/promptLater"))); + dialog.Commands.Add(new UICommand(resources.GetString("/Main/sure"), async (command) => + { + SettingsStorage.PromptReview = false; + await Launcher.LaunchUriAsync("ms-windows-store://review/?ProductId=9NCQQXJTDLFH".ToUri()); + })); + dialog.DefaultCommandIndex = 2; + dialog.CancelCommandIndex = 1; + await dialog.ShowAsync(); + } + } + + public void SetTitleBar(CoreApplicationViewTitleBar coreTitleBar = null) + { + if (coreTitleBar != null) + { + bool full = ApplicationView.GetForCurrentView().IsFullScreenMode; + double left = 12 + (full ? 0 : coreTitleBar.SystemOverlayLeftInset); + AppTitle.Margin = new Thickness(left, 8, 0, 0); + AppTitleBar.Height = coreTitleBar.Height; + } + + var titleBar = ApplicationView.GetForCurrentView().TitleBar; + + titleBar.ButtonBackgroundColor = Colors.Transparent; + titleBar.ButtonHoverBackgroundColor = Colors.IndianRed; + titleBar.ButtonPressedBackgroundColor = Colors.DarkRed; + titleBar.ButtonInactiveBackgroundColor = Colors.Transparent; + titleBar.ButtonInactiveForegroundColor = Colors.Gray; + + if(RequestedTheme == ElementTheme.Dark || (RequestedTheme == ElementTheme.Default && Application.Current.RequestedTheme == ApplicationTheme.Dark)) + titleBar.ButtonForegroundColor = Colors.White; + else + titleBar.ButtonForegroundColor = Colors.Black; + } + + private void SecretsVault_SubscriptionsChanged(object sender, params object[] args) + { + switch((string)args[0]) + { + case "add": + if (nav.MenuItems.Count < 19) + nav.MenuItems.Add(args[1] as Subscription); + break; + + case "remove": + if (nav.MenuItems.Find(i => ((i as Microsoft.UI.Xaml.Controls.NavigationViewItem).Content as StackPanel).Tag.ToString() == (string)args[1]) is Microsoft.UI.Xaml.Controls.NavigationViewItem item) + { + nav.MenuItems.Remove(item); + if (SecretsVault.Subscriptions.Count >= 10) + nav.MenuItems.Add(SecretsVault.Subscriptions[9]); + } + break; + } + } + + private async void AuthorizationStateChanged(object sender, params object[] e) + { + wasInvoked = false; + + switch (e[0] as bool?) + { + case true: + account.Visibility = Visibility.Collapsed; + + ToolTipService.SetToolTip(avatar, $"{SecretsVault.UserInfo.Name} ({SecretsVault.UserInfo.Email})"); + myNameFlyout.Text = SecretsVault.UserInfo.Name; + myEmail.Text = SecretsVault.UserInfo.Email; + avatarFlyout.ProfilePicture = new BitmapImage(SecretsVault.UserInfo.Picture.ToUri()) { DecodePixelHeight = 65, DecodePixelWidth = 65 }; + ((avatar.Content as Ellipse).Fill as ImageBrush).ImageSource = new BitmapImage(SecretsVault.UserInfo.Picture.ToUri()) { DecodePixelHeight = 25, DecodePixelWidth = 25 }; + + avatar.Visibility = Visibility.Visible; + + toChannel.Visibility = Visibility.Visible; + toSubscriptions.Visibility = Visibility.Visible; + libHeader.Visibility = Visibility.Visible; + toHistory.Visibility = Visibility.Visible; + toLiked.Visibility = Visibility.Visible; + toLater.Visibility = Visibility.Visible; + + if (SecretsVault.Subscriptions.Count > 0) + { + subsHeader.Visibility = Visibility.Visible; + for (int k = 0; k < SecretsVault.Subscriptions.Count && k < 10; k++) + nav.MenuItems.Add(SecretsVault.Subscriptions[k]); + } + HistorySet.Load(); + + if (content.Frame.Content != null) + content.Refresh(); + else + content.Frame.Navigate(typeof(Home)); + break; + + case false: + content.Frame.Navigate(typeof(Home)); + + for (int k = nav.MenuItems.Count - 1; k > 8; k--) + nav.MenuItems.RemoveAt(k); + + account.Visibility = Visibility.Visible; + avatar.Visibility = Visibility.Collapsed; + + toChannel.Visibility = Visibility.Collapsed; + toSubscriptions.Visibility = Visibility.Collapsed; + libHeader.Visibility = Visibility.Collapsed; + toHistory.Visibility = Visibility.Collapsed; + toLiked.Visibility = Visibility.Collapsed; + toLater.Visibility = Visibility.Collapsed; + subsHeader.Visibility = Visibility.Collapsed; + + subsHeader.Visibility = Visibility.Collapsed; + + content.Frame.BackStack.Clear(); + content.Frame.ForwardStack.Clear(); + break; + + default: + MessageDialog dialog = new MessageDialog(resources.GetString("/Main/connectErrContent"), resources.GetString("/Main/connectErrHeader")); + + dialog.Commands.Add(new UICommand(resources.GetString("/Main/tryAgain"), (command) => + { + SecretsVault.Authorize(); + })); + dialog.Commands.Add(new UICommand(resources.GetString("/Main/quit"), (command) => + { + Methods.CloseApp(); + })); + + dialog.CancelCommandIndex = 1; + dialog.DefaultCommandIndex = 0; + + await dialog.ShowAsync(); + break; + } + + if (videoPlaceholder.Frame.Content != null) + { + MaximizeVideo(); + videoPlaceholder.Refresh(); + } + + DownloadAgent.Initialize(); + } + + private async void Feedback_Click(object sender, RoutedEventArgs e) + { + await StoreServicesFeedbackLauncher.GetDefault().LaunchAsync(); + } + + private void SignIn_Click(object sender, RoutedEventArgs e) + { + SecretsVault.Authorize(); + } + + private void Logout_Click(object sender, RoutedEventArgs e) + { + avatar.Flyout.Hide(); + SecretsVault.Deauthenticate(); + } + + public void GoToSearch(SearchParameters args) + { + content.Frame.Navigate(typeof(Search), new object[] { args, content }); + } + + public void GoToChannel(string id) + { + content.Frame.Navigate(typeof(ChannelPage), id); + } + + public void GoToHome() + { + content.Frame.Navigate(typeof(Home)); + } + + public void GoToVideo(string id, string playlistId = null, bool incognito = false) + { + MaximizeVideo(); + + nav.IsBackEnabled = true; + nav.ExpandedModeThresholdWidth = short.MaxValue; + nav.IsPaneOpen = false; + + videoPlaceholder.Frame.Navigate(typeof(VideoPage), new object[3] { id, playlistId, incognito }); + + Title.Text = resources.GetString("/Main/video"); + } + + public void GoToDeveloper(string id) + { + content.Frame.Navigate(typeof(Settings), id); + } + + public void GoToPlaylist(string id) + { + content.Frame.Navigate(typeof(PlaylistPage), id); + } + + public void GoToHistory() + { + content.Frame.Navigate(typeof(History)); + } + + public void GoToDownloads() + { + content.Frame.Navigate(typeof(Downloads)); + } + + public void MinimizeAsInitializer() + { + if (videoPlaceholder.Frame.Content == null) + return; + + if (videoPlaceholder.LoadingPage.State != LoadingState.Loaded) + CloseVideo(); + else + (videoPlaceholder.Frame.Content as VideoPage).Player.Minimize(); + + Title.Text = headers[content.Frame.SourcePageType]; + } + + public void MinimizeVideo() + { + videoPlaceholder.Width = 432; + videoPlaceholder.Height = 243; + videoPlaceholder.VerticalAlignment = VerticalAlignment.Bottom; + videoPlaceholder.HorizontalAlignment = HorizontalAlignment.Right; + videoPlaceholder.Margin = new Thickness(0, 0, 25, 50); + + if (content.Frame.CanGoBack) + nav.IsBackEnabled = true; + else + nav.IsBackEnabled = false; + + SetNavigationMenu(); + + Title.Text = headers[content.Frame.SourcePageType]; + } + + void SetNavigationMenu() + { + if (content.Frame.SourcePageType == typeof(Home) || content.Frame.SourcePageType == typeof(Settings) || content.Frame.SourcePageType == typeof(Subscriptions)) + { + nav.ExpandedModeThresholdWidth = 1008; + nav.IsPaneOpen = nav.DisplayMode == Microsoft.UI.Xaml.Controls.NavigationViewDisplayMode.Expanded ? true : false; + } + else + { + nav.ExpandedModeThresholdWidth = short.MaxValue; + nav.IsPaneOpen = false; + } + } + + public void MaximizeVideo() + { + videoPlaceholder.Width = double.NaN; + videoPlaceholder.Height = double.NaN; + videoPlaceholder.VerticalAlignment = VerticalAlignment.Stretch; + videoPlaceholder.HorizontalAlignment = HorizontalAlignment.Stretch; + videoPlaceholder.Margin = new Thickness(0); + + if (videoPlaceholder.Frame.Content == null) + return; + + nav.IsBackEnabled = true; + Title.Text = resources.GetString("/Main/video"); + nav.ExpandedModeThresholdWidth = short.MaxValue; + nav.IsPaneOpen = false; + } + + public void CloseVideo() + { + if (ApplicationView.GetForCurrentView().IsFullScreenMode) + ApplicationView.GetForCurrentView().ExitFullScreenMode(); + + videoPlaceholder.Frame.Content = null; + GC.Collect(); + MaximizeVideo(); + + nav.IsBackEnabled = content.Frame.CanGoBack; + + SetNavigationMenu(); + + Title.Text = headers[content.Frame.SourcePageType]; + } + + private void Search_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) + { + if(!string.IsNullOrWhiteSpace(search.Text)) + GoToSearch(new SearchParameters(search.Text)); + } + + private async void Search_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) + { + await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => + { + if (search.Text.Length > 2 && args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) + { + try + { + XmlDocument doc = new XmlDocument(); + doc.Load($"http://suggestqueries.google.com/complete/search?ds=yt&client=toolbar&q={search.Text}&hl={SettingsStorage.RelevanceLanguage}"); + + List suggestions = new List(); + + for (int i = 0; i < doc["toplevel"].ChildNodes.Count && i < 5; i++) + suggestions.Add(doc["toplevel"].ChildNodes[i]["suggestion"].GetAttribute("data")); + + search.ItemsSource = suggestions; + } + catch { search.ItemsSource = new List(); } + } + }); + } + + void SetNavigationItem(object item) + { + try + { + if (nav.SelectedItem != item) + nav.SelectedItem = item; + else + wasInvoked = false; + } + catch { } + } + + public void Content_Navigated(object sender, NavigationEventArgs e) + { + Title.Text = headers[content.Frame.SourcePageType]; + + if (!wasInvoked) + { + wasInvoked = true; + + if (e.SourcePageType == typeof(Settings)) + SetNavigationItem(nav.SettingsItem); + else if (e.SourcePageType == typeof(Subscriptions)) + SetNavigationItem(toSubscriptions); + else if (e.SourcePageType == typeof(Downloads)) + SetNavigationItem(toDownloads); + else if (e.SourcePageType == typeof(Home)) + SetNavigationItem(toHome); + else if (e.SourcePageType == typeof(Search)) + SetNavigationItem(null); + else if(e.SourcePageType == typeof(ChannelPage)) + { + if (SecretsVault.IsAuthorized) + { + if (e.Parameter.ToString() == SecretsVault.AccountId) + SetNavigationItem(toChannel); + else if (nav.MenuItems.Contains(SecretsVault.Subscriptions.Find(i => i.Snippet.ResourceId.ChannelId == e.Parameter.ToString()))) + SetNavigationItem(SecretsVault.Subscriptions.Find(i => i.Snippet.ResourceId.ChannelId == e.Parameter.ToString())); + else + SetNavigationItem(null); + } + else + SetNavigationItem(null); + } + else if(e.SourcePageType == typeof(History)) + SetNavigationItem(toHistory); + else if(e.SourcePageType == typeof(PlaylistPage)) + { + if (SecretsVault.IsAuthorized) + { + if (e.Parameter.ToString() == SecretsVault.UserChannel.ContentDetails.RelatedPlaylists.Likes) + SetNavigationItem(toLiked); + else if (e.Parameter.Equals("WL")) + SetNavigationItem(toLater); + } + else + SetNavigationItem(null); + } + } + else + wasInvoked = false; + + nav.IsBackEnabled = content.Frame.CanGoBack; + + SetNavigationMenu(); + + if (videoPlaceholder.Frame.Content != null && videoPlaceholder.HorizontalAlignment == HorizontalAlignment.Stretch) + MinimizeAsInitializer(); + } + + private void RemoveAds_Tapped(object sender, TappedRoutedEventArgs e) + { + SecretsVault.GetAdblock(); + } + + private void Nav_SelectionChanged(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs args) + { + if (!wasInvoked) + { + if (content == null) + return; + wasInvoked = true; + if (args.IsSettingsSelected) + content.Frame.Navigate(typeof(Settings)); + else + { + if (args.SelectedItem == toHome) + content.Frame.Navigate(typeof(Home)); + else if (args.SelectedItem == toHistory) + content.Frame.Navigate(typeof(History)); + else if (args.SelectedItem == toLiked) + content.Frame.Navigate(typeof(PlaylistPage), SecretsVault.UserChannel.ContentDetails.RelatedPlaylists.Likes); + else if (args.SelectedItem == toLater) + content.Frame.Navigate(typeof(PlaylistPage), "WL"); + else if (args.SelectedItem == toSubscriptions) + content.Frame.Navigate(typeof(Subscriptions)); + else if (args.SelectedItem == toDownloads) + content.Frame.Navigate(typeof(Downloads)); + else if (args.SelectedItem == toChannel) + content.Frame.Navigate(typeof(ChannelPage), SecretsVault.UserChannel.Id); + else + content.Frame.Navigate(typeof(ChannelPage), (args.SelectedItem as Subscription).Snippet.ResourceId.ChannelId); + } + } + else + wasInvoked = false; + } + + private void Nav_BackRequested(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewBackRequestedEventArgs args) + { + if (videoPlaceholder.Frame.Content != null && double.IsNaN(videoPlaceholder.Width)) + { + if (videoPlaceholder.Frame.CanGoBack) + videoPlaceholder.Frame.GoBack(); + else if (videoPlaceholder.LoadingPage.State != LoadingState.Loaded) + CloseVideo(); + else + MinimizeAsInitializer(); + } + else + content.Frame.GoBack(); + } + + private void Nav_PaneClosing(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewPaneClosingEventArgs args) + { + AppTitle.Visibility = Visibility.Collapsed; + } + + private void Nav_PaneOpened(Microsoft.UI.Xaml.Controls.NavigationView sender, object args) + { + AppTitle.Visibility = Visibility.Visible; + } + + private void NavigationViewItem_Tapped(object sender, TappedRoutedEventArgs e) + { + content.Frame.Navigate(typeof(UploadPage)); + } + } +} diff --git a/Src/FoxTube backup upload page/UploadPage.xaml b/Src/FoxTube backup upload page/UploadPage.xaml new file mode 100644 index 0000000..dd3ed26 --- /dev/null +++ b/Src/FoxTube backup upload page/UploadPage.xaml @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + Your video will be live at: https://youtu.be/hulumulu + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +