diff --git a/FoxTube.Core/Extensions.cs b/FoxTube.Core/Extensions.cs new file mode 100644 index 0000000..bf43401 --- /dev/null +++ b/FoxTube.Core/Extensions.cs @@ -0,0 +1,114 @@ +using FoxTube.Utils; +using SQLitePCL; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Xml; +using Windows.UI; + +namespace FoxTube +{ + public static class Extensions + { + public static Uri ToUri(this string url) + { + Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out Uri result); + return result; + } + + public static string ReplaceInvalidChars(this string str, char newValue) + { + foreach (char i in Path.GetInvalidFileNameChars()) + str = str.Replace(i, newValue); + return str; + } + + public static Windows.Data.Xml.Dom.XmlDocument ToXml(this string text) + { + Windows.Data.Xml.Dom.XmlDocument doc = new Windows.Data.Xml.Dom.XmlDocument(); + try + { + doc.LoadXml(text); + return doc; + } + catch { return null; } + } + + public static bool Belongs(this T obj, params T[] args) => + args.Contains(obj); + + 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) + { + hex = hex.Replace("#", ""); + List values = new List(); + for (int k = 0; k < hex.Length; k++) + values.Add(byte.Parse(string.Join("", hex[k], hex[++k]), System.Globalization.NumberStyles.HexNumber)); + + return hex.Length switch + { + 6 => Color.FromArgb(255, values[0], values[1], values[2]), + 8 => Color.FromArgb(values[0], values[1], values[2], values[3]), + _ => Colors.Black + }; + } + + public static TimeSpan GetDuration(this string rawDuration) + { + try + { + return XmlConvert.ToTimeSpan(rawDuration); + } + catch (FormatException) + { + TimeSpan time = XmlConvert.ToTimeSpan("PT" + rawDuration.Split('T')[1]); + TimeSpan date = TimeSpan.FromDays(int.Parse(rawDuration.Split('W')[0].Remove('P')) * 7); + date.Add(time); + + return date; + } + catch (Exception e) + { + Metrics.SendReport(new Exception("Failed to parse duration", e), null, ("RawDuration", rawDuration)); + + return TimeSpan.FromMilliseconds(0); + } + } + + public static string GetFriendlyDate(this DateTime date) + { + TimeSpan span = DateTime.Now - date; + + if (span.TotalMinutes < 1) + return "Just now"; + else if (Math.Round(span.TotalMinutes) == 1) + return "Minute ago"; + else if (span.TotalMinutes < 60) + return Math.Round(span.TotalMinutes) + " " + "minutes ago"; + else if (Math.Round(span.TotalHours) == 1) + return "Hour ago"; + else if (span.TotalHours < 24) + return Math.Round(span.TotalHours) + " " + "hours ago"; + else if (Math.Round(span.TotalDays) == 1) + return "Day ago"; + else if (span.TotalDays < 7) + return Math.Round(span.TotalDays) + " " + "days ago"; + else if (Math.Round(span.TotalDays) == 7) + return "Week ago"; + else if (span.TotalDays < 30) + return Math.Round(span.TotalDays / 7) + " " + "weeks ago"; + else if (Math.Round(span.TotalDays) == 30) + return "Month ago"; + else if (Math.Round(span.TotalDays) < 365) + return Math.Round(span.TotalDays / 30) + " " + "months ago"; + else if (Math.Round(span.TotalDays / 365) == 365) + return "Year ago"; + else + return Math.Round(span.TotalDays / 365) + " " + "years ago"; + } + } +} \ No newline at end of file diff --git a/FoxTube.Core/FoxTube.Core.csproj b/FoxTube.Core/FoxTube.Core.csproj index 6b56580..afc0e11 100644 --- a/FoxTube.Core/FoxTube.Core.csproj +++ b/FoxTube.Core/FoxTube.Core.csproj @@ -7,7 +7,7 @@ {29C01E10-76E7-4527-984F-B0EEF7E1AC64} Library Properties - FoxTube.Core + FoxTube FoxTube.Core en-US UAP @@ -130,59 +130,73 @@ PackageReference - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - 0.13.0 + 0.14.0 1.0.2 - 1.42.0 + 1.45.0 - 1.42.0.1602 + 1.44.1.1869 - 1.42.0.1758 + 1.45.0.1929 + + + 10.1811.22001 - 2.6.2 + 3.2.1 - 2.6.2 + 3.2.1 - 6.2.9 + 6.2.10 10.1901.28001 + + 2.3.200213001 + + + 5.0.2 + + + Microsoft Advertising SDK for XAML + Microsoft Engagement Framework @@ -190,6 +204,9 @@ Visual C++ 2015 Runtime for Universal Windows Platform Apps + + + 14.0 diff --git a/FoxTube.Core/Helpers/Constants.cs b/FoxTube.Core/Helpers/Constants.cs deleted file mode 100644 index ac65322..0000000 --- a/FoxTube.Core/Helpers/Constants.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace FoxTube.Core.Helpers -{ - public static class Constants - { - public static string ApplicationId => "9ncqqxjtdlfh"; - public static string AdsId => "1100044398"; - } -} diff --git a/FoxTube.Core/Helpers/Extensions.cs b/FoxTube.Core/Helpers/Extensions.cs deleted file mode 100644 index 9fdb790..0000000 --- a/FoxTube.Core/Helpers/Extensions.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Windows.Data.Xml.Dom; -using Windows.UI; - -namespace FoxTube -{ - public static class Extensions - { - public static Uri ToUri(this string url) - { - try { return new Uri(url); } - catch { return null; } - } - - public static XmlDocument ToXml(this string text) - { - XmlDocument doc = new XmlDocument(); - try - { - doc.LoadXml(text); - return doc; - } - catch { return null; } - } - - public static bool Belongs(this T obj, params T[] args) => - args.Contains(obj); - - 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) - { - hex = hex.Replace("#", ""); - List values = new List(); - for(int k = 0; k < hex.Length; k++) - values.Add(byte.Parse(string.Join("", hex[k], hex[++k]), System.Globalization.NumberStyles.HexNumber)); - - return hex.Length switch - { - 6 => Color.FromArgb(255, values[0], values[1], values[2]), - 8 => Color.FromArgb(values[0], values[1], values[2], values[3]), - _ => Colors.Black - }; - } - } -} diff --git a/FoxTube.Core/Helpers/Feedback.cs b/FoxTube.Core/Helpers/Feedback.cs deleted file mode 100644 index 4814833..0000000 --- a/FoxTube.Core/Helpers/Feedback.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using Microsoft.Services.Store.Engagement; -using Windows.UI.Popups; - -namespace FoxTube.Core.Helpers -{ - public static class Feedback - { - public static bool HasFeedbackHub => StoreServicesFeedbackLauncher.IsSupported(); - - public static async void OpenFeedbackHub() - { - if (HasFeedbackHub) - await StoreServicesFeedbackLauncher.GetDefault().LaunchAsync(); - } - - public static async void PromptFeedback() - { - if (!HasFeedbackHub) - { - Settings.PromptFeedback = false; - return; - } - - MessageDialog dialog = new MessageDialog("Have some thoughts to share about the app or any suggestions? Leave feedback!"); - dialog.Commands.Add(new UICommand("Don't ask me anymore", (command) => Settings.PromptFeedback = false)); - dialog.Commands.Add(new UICommand("Maybe later")); - dialog.Commands.Add(new UICommand("Sure!", (command) => - { - Settings.PromptFeedback = false; - OpenFeedbackHub(); - })); - dialog.DefaultCommandIndex = 2; - dialog.CancelCommandIndex = 1; - await dialog.ShowAsync(); - } - public static async void PromptReview() - { - MessageDialog dialog = new MessageDialog("Like our app? Review it on Microsoft Store!"); - dialog.Commands.Add(new UICommand("Don't ask me anymore", (command) => Settings.PromptReview = false)); - dialog.Commands.Add(new UICommand("Maybe later")); - dialog.Commands.Add(new UICommand("Sure!", (command) => - { - StoreInterop.RequestReview(); - Settings.PromptReview = false; - })); - dialog.DefaultCommandIndex = 2; - dialog.CancelCommandIndex = 1; - await dialog.ShowAsync(); - } - } -} diff --git a/FoxTube.Core/Helpers/Inbox.cs b/FoxTube.Core/Helpers/Inbox.cs deleted file mode 100644 index edbbdf7..0000000 --- a/FoxTube.Core/Helpers/Inbox.cs +++ /dev/null @@ -1,137 +0,0 @@ -using FoxTube.Core.Models; -using FoxTube.Core.Models.Inbox; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Windows.Data.Xml.Dom; -using Windows.Storage; -using Windows.UI.Notifications; - -namespace FoxTube.Core.Helpers -{ - public static class Inbox - { - static HttpClient client = new HttpClient(); - static ApplicationDataContainer storage = ApplicationData.Current.RoamingSettings; - - public static async Task>GetInbox() - { - List list = new List(); - list.AddRange(await GetMessages()); - list.AddRange(await GetChangelogs()); - list.OrderByDescending(i => i.TimeStamp); - - return list; - } - - public static async void PushNew() - { - try - { - // TODO: Add backend - HttpResponseMessage response = await client.GetAsync("https://xfox111.net/FoxTube/Messages?toast=true&publishedAfter=" + storage.Values["Inbox.lastCheck"] + "&lang=" + Settings.Language); - - if (response.StatusCode == System.Net.HttpStatusCode.NoContent) - return; - - XmlDocument doc = new XmlDocument(); - doc.LoadXml(await response.Content.ReadAsStringAsync()); - foreach (IXmlNode toast in doc.LastChild.ChildNodes) - ToastNotificationManager.CreateToastNotifier().Show(new ToastNotification(toast.GetXml().ToXml())); - - storage.Values["Inbox.lastCheck"] = DateTime.Now; - } - catch (Exception e) - { - Metrics.AddEvent("Unable to retrieve developers' messages", - ("Exception", e.GetType().ToString()), - ("Message", e.Message), - ("App version", Metrics.CurrentVersion), - ("StackTrace", e.StackTrace)); - } - } - - static async Task>GetChangelogs() - { - List list = new List(); - try - { - // TODO: Add backend - HttpResponseMessage response = await client.GetAsync($"https://xfox111.net/FoxTube/Changelogs?lang={Settings.Language}¤tVersion={Metrics.CurrentVersion}"); - - if (response.StatusCode == System.Net.HttpStatusCode.NoContent) - return list; - - dynamic responseObj = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - foreach (var item in responseObj) - list.Add(new Changelog(item.Version, item.Content, item.Description, item.TimeStamp)); - } - catch (Exception e) - { - Metrics.AddEvent("Unable to retrieve changelogs", - ("Exception", e.GetType().ToString()), - ("Message", e.Message), - ("App version", Metrics.CurrentVersion), - ("StackTrace", e.StackTrace)); - } - - return list; - } - static async Task>GetMessages() - { - List list = new List(); - try - { - // TODO: Add backend - HttpResponseMessage response = await client.GetAsync("https://xfox111.net/FoxTube/Messages?lang=" + Settings.Language); - - if (response.StatusCode == System.Net.HttpStatusCode.NoContent) - return list; - - dynamic responseObj = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - foreach (var item in responseObj) - list.Add(new DeveloperMessage(item.Id, item.Title, item.Content, item.TimeStamp, item.Avatar)); - } - catch (Exception e) - { - Metrics.AddEvent("Unable to retrieve developers' messages", - ("Exception", e.GetType().ToString()), - ("Message", e.Message), - ("App version", Metrics.CurrentVersion), - ("StackTrace", e.StackTrace)); - } - - return list; - } - - /// - /// Fires toast notification with the last changelog content - /// - public static async void PushChangelog() - { - try - { - // TODO: Add backend - Settings.LastReviewedVersion = Metrics.CurrentVersion; - - HttpResponseMessage response = await client.GetAsync("https://xfox111.net/FoxTube/Changelogs?toast=true&lang=" + Settings.Language + "&version=" + Metrics.CurrentVersion); - - if (response.StatusCode == System.Net.HttpStatusCode.NoContent) - return; - - ToastNotificationManager.CreateToastNotifier().Show(new ToastNotification((await response.Content.ReadAsStringAsync()).ToXml())); - } - catch (Exception e) - { - Metrics.AddEvent("Unable to retrieve changelog", - ("Exception", e.GetType().ToString()), - ("Message", e.Message), - ("App version", Metrics.CurrentVersion), - ("StackTrace", e.StackTrace)); - } - } - } -} diff --git a/FoxTube.Core/Helpers/Metrics.cs b/FoxTube.Core/Helpers/Metrics.cs deleted file mode 100644 index 18069c1..0000000 --- a/FoxTube.Core/Helpers/Metrics.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.AppCenter; -using Microsoft.AppCenter.Analytics; -using Microsoft.AppCenter.Crashes; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net.Http; -using System.Threading.Tasks; -using Windows.ApplicationModel; -using Windows.Storage; - -namespace FoxTube -{ - public static class Metrics - { - static readonly ApplicationDataContainer storage = ApplicationData.Current.RoamingSettings; - - static readonly Stopwatch sw = new Stopwatch(); - public static TimeSpan Uptime - { - get => (TimeSpan?)storage.Values["Metrics.SpentTime"] ?? TimeSpan.FromSeconds(0); - set => storage.Values["Metrics.SpentTime"] = value; - } - public static string CurrentVersion - { - get - { - PackageVersion v = Package.Current.Id.Version; - return $"{v.Major}.{v.Minor}.{v.Revision}.{v.Build}"; - } - } - - public static void StartSession() - { - sw.Start(); - AppCenter.Start("45774462-9ea7-438a-96fc-03982666f39e", typeof(Analytics), typeof(Crashes)); - AppCenter.SetCountryCode(Settings.Region); - } - - public static void EndSession() - { - sw.Stop(); - Uptime += sw.Elapsed; - - AddEvent("Session closed", - ("Duration", sw.Elapsed.ToString()), - ("Spend time total", Uptime.ToString())); - } - - public static void AddEvent(string eventName, params (string key, string value)[] details) - { - Dictionary parameters = new Dictionary(); - foreach (var (key, value) in details) - parameters.Add(key, value); - Analytics.TrackEvent(eventName, parameters.Count > 0 ? parameters : null); - } - - public static async Task SendExtendedData(string packageTitle, string content) - { - // TODO: Add backend - using(HttpClient client = new HttpClient()) - { - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "https://xfox111.net/FoxTube/AddMetrics"); - Dictionary body = new Dictionary - { - { "Title", packageTitle }, - { "Content", content }, - { "Version", CurrentVersion } - }; - request.Content = new FormUrlEncodedContent(body); - HttpResponseMessage response = await client.SendAsync(request); - return await response.Content.ReadAsStringAsync(); - } - } - } -} diff --git a/FoxTube.Core/Helpers/Settings.cs b/FoxTube.Core/Helpers/Settings.cs deleted file mode 100644 index c9d38c4..0000000 --- a/FoxTube.Core/Helpers/Settings.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.Globalization; -using Windows.Storage; - -namespace FoxTube -{ - public static class Settings - { - static readonly ApplicationDataContainer settings = ApplicationData.Current.RoamingSettings; - - public static string DesiredVideoQuality - { - get => (string)settings.Values["DesiredVideoQuality"] ?? "auto"; - set => settings.Values["DesiredVideoQuality"] = value; - } - public static string RememberedQuality - { - get => (string)settings.Values["RememberedVideoQuality"] ?? "1080p"; - set => settings.Values["RememberedVideoQuality"] = value; - } - public static bool VideoNotifications - { - get => (bool?)settings.Values["NewVideosNotificationsAll"] ?? true; - set => settings.Values["NewVideosNotificationsAll"] = value; - } - public static bool DevNotifications - { - get => (bool?)settings.Values["DevelopersNewsNotifications"] ?? true; - set => settings.Values["DevelopersNewsNotifications"] = value; - } - public static bool CheckConnection - { - get => (bool?)settings.Values["WarnIfOnMeteredConnection"] ?? false; - set => settings.Values["WarnIfOnMeteredConnection"] = value; - } - public static bool Autoplay - { - get => (bool?)settings.Values["VideoAutoplay"] ?? true; - set => settings.Values["VideoAutoplay"] = value; - } - public static double Volume - { - get => (double?)settings.Values["Volume"] ?? 1; - set => settings.Values["Volume"] = value; - } - public static string Language - { - get => (string)settings.Values["InterfaceLanguage"] ?? GetDefaultLanguage(); - set => settings.Values["InterfaceLanguage"] = value; - } - public static string RelevanceLanguage - { - get => (string)settings.Values["DesiredContentLanguage"] ?? CultureInfo.InstalledUICulture.TwoLetterISOLanguageName; - set => settings.Values["DesiredContentLanguage"] = value; - } - public static string Region - { - get => (string)settings.Values["Region"] ?? CultureInfo.InstalledUICulture.Name.Split('-')[1]; - set => settings.Values["Region"] = value; - } - public static int SafeSearch - { - get => (int?)settings.Values["SafeSearch"] ?? 0; //Moderate - set => settings.Values["SafeSearch"] = value; - } - public static bool BlockExplicitContent - { - get => (bool?)settings.Values["BlockExplicitContent"] ?? true; - set => settings.Values["BlockExplicitContent"] = value; - } - - public static bool HasAccount - { - get => (bool?)settings.Values["HasAccount"] ?? false; - set => settings.Values["HasAccount"] = value; - } - public static int Theme - { - get => (int?)settings.Values["PreferedUITheme"] ?? 2; //System - set => settings.Values["PreferedUITheme"] = value; - } - public static bool PromptReview - { - get => (bool?)settings.Values["PromptReview"] ?? Metrics.Uptime.TotalHours > 24; - set => settings.Values["PromptReview"] = value; - } - public static bool PromptFeedback - { - get => (bool?)settings.Values["PromptFeedback"] ?? Metrics.Uptime.TotalHours > 12; - set => settings.Values["PromptFeedback"] = value; - } - public static bool ProcessClipboard - { - get => (bool?)settings.Values["ProcessClipboardEntry"] ?? true; - set => settings.Values["ProcessClipboardEntry"] = value; - } - public static string LastReviewedVersion - { - get - { - if (settings.Values["LastReviewedVersion"] == null) - settings.Values["LastReviewedVersion"] = Metrics.CurrentVersion; - return (string)settings.Values["LastReviewedVersion"]; - } - set => settings.Values["LastReviewedVersion"] = value; - } - - static string GetDefaultLanguage() - { - if (CultureInfo.InstalledUICulture.TwoLetterISOLanguageName.Belongs("ua", "ru", "by", "kz", "kg", "md", "lv", "ee")) //Languages for Russian-speaking countries - return "ru-RU"; - else - return "en-US"; - } - - public static void ResetSettings() => - settings.Values.Clear(); - } -} diff --git a/FoxTube.Core/Helpers/StoreInterop.cs b/FoxTube.Core/Helpers/StoreInterop.cs deleted file mode 100644 index 9ef6c22..0000000 --- a/FoxTube.Core/Helpers/StoreInterop.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Threading.Tasks; -using Windows.Services.Store; - -namespace FoxTube.Core.Helpers -{ - public static class StoreInterop - { - public static bool AdsDisabled { get; private set; } = true; - public static string Price { get; private set; } - - public static async Task UpdateStoreState() - { - StoreProductQueryResult requset = await StoreContext.GetDefault().GetAssociatedStoreProductsAsync(new[] { "Durable" }); - - if (requset.Products["9NP1QK556625"].IsInUserCollection) - return; - - Price = requset.Products["9NP1QK556625"].Price.FormattedPrice; - AdsDisabled = false; - } - - public static async Task PurchaseApp() - { - StorePurchaseResult request = await StoreContext.GetDefault().RequestPurchaseAsync("9NP1QK556625"); - - switch (request.Status) - { - case StorePurchaseStatus.AlreadyPurchased: - case StorePurchaseStatus.Succeeded: - AdsDisabled = true; - return true; - default: - return false; - } - } - - public static async void RequestReview() - { - StoreRateAndReviewResult result = await StoreContext.GetDefault().RequestRateAndReviewAppAsync(); - - string attachedPackageId = result.Status == StoreRateAndReviewStatus.Error ? await Metrics.SendExtendedData("StoreReviewRequestError", result.ExtendedJsonData) : "Success"; - Metrics.AddEvent("Store review request has been recieved", - ("Result", result.Status.ToString()), - ("ErrorData", attachedPackageId)); - } - } -} diff --git a/FoxTube.Core/Helpers/UsersControl.cs b/FoxTube.Core/Helpers/UsersControl.cs deleted file mode 100644 index e5fde2b..0000000 --- a/FoxTube.Core/Helpers/UsersControl.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Google.Apis.Auth.OAuth2; -using Google.Apis.Oauth2.v2; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Windows.Security.Authentication.Web; -using YouTube.Authorization; -using System.Text.RegularExpressions; -using Windows.Security.Credentials; -using FoxTube.Core.Models; -using YouTube; -using System.Threading; -using Google.Apis.YouTube.v3; -using System.Net.Http; -using Google.Apis.Auth.OAuth2.Responses; -using Newtonsoft.Json; -using Google.Apis.Auth.OAuth2.Flows; - -namespace FoxTube -{ - public static class UsersControl - { - static ExtendedYouTubeService _defaultService = new ExtendedYouTubeService(new Google.Apis.Services.BaseClientService.Initializer - { - ApplicationName = "FoxTube", - ApiKey = "AIzaSyD7tpbuvmYDv9h4udo9L_g3r0sLPFAnN00" - }); - - static string[] Scopes { get; } = new string[] - { - Oauth2Service.Scope.UserinfoProfile, - Oauth2Service.Scope.UserinfoEmail, - YouTubeService.Scope.YoutubeForceSsl - }; - - static ClientSecrets ClientSecrets { get; } = new ClientSecrets - { - ClientId = "1096685398208-u95rcpkqb4e1ijfmb8jdq3jsg37l8igv.apps.googleusercontent.com", - ClientSecret = "IU5bbdjwvmx8ttJoXQ7e6JWd" - }; - - public static User CurrentUser { get; set; } - public static bool Authorized => CurrentUser != null; - public static ExtendedYouTubeService Service => CurrentUser?.Service ?? _defaultService; - - public static async Task AddUser() - { - Uri requestString = AuthorizationHelpers.FormQueryString(ClientSecrets, Scopes); - - WebAuthenticationResult result = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.UseTitle, requestString, AuthorizationHelpers.Endpoint); - switch(result.ResponseStatus) - { - case WebAuthenticationStatus.Success: - string successCode = new Regex(@"(?<=code=)(.*?)(?=&)").Match(result.ResponseData).Value; - - UserCredential credential = await AuthorizationHelpers.ExchangeToken(ClientSecrets, successCode); - CurrentUser = new User(credential); - - PasswordVault passwordVault = new PasswordVault(); - passwordVault.Add(new PasswordCredential("foxtube", CurrentUser.UserInfo.Id, credential.Token.RefreshToken)); - return true; - case WebAuthenticationStatus.UserCancel: - break; - case WebAuthenticationStatus.ErrorHttp: - Metrics.AddEvent("Authorization failed (HTTP Error)", ("Response data", result.ResponseData), ("Error details", result.ResponseErrorDetail.ToString())); - break; - } - return false; - } - - public static async Task Initialize() - { - PasswordVault passwordVault = new PasswordVault(); - IReadOnlyList credentials; - credentials = passwordVault.RetrieveAll(); - - if (credentials.Count == 0) - return; - - credentials[0].RetrievePassword(); - UserCredential credential = await AuthorizationHelpers.RestoreUser(ClientSecrets, credentials[0].Password); - CurrentUser = new User(credential); - } - - public static async Task Logout() - { - PasswordVault passwordVault = new PasswordVault(); - PasswordCredential credential = passwordVault.Retrieve("foxtube", CurrentUser.UserInfo.Id); - passwordVault.Remove(credential); - await CurrentUser.Credential.RevokeTokenAsync(CancellationToken.None); - } - } -} diff --git a/FoxTube.Core/Helpers/Utils.cs b/FoxTube.Core/Helpers/Utils.cs deleted file mode 100644 index 8c7a586..0000000 --- a/FoxTube.Core/Helpers/Utils.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using Windows.ApplicationModel.Core; -using Windows.Security.Credentials; - -namespace FoxTube.Core.Helpers -{ - public static class Utils - { - /// - /// Terminates current application session - /// - public static void CloseApp() => - CoreApplication.Exit(); - - /// - /// Restarts application - /// - public static void RestartApp() => - RestartApp(null); - - /// - /// Restarts application with specified parameters - /// - /// Parameters which will be provided to new application instance - public static async void RestartApp(string args) => - await CoreApplication.RequestRestartAsync(args ?? ""); - - public static void InitializeFailsafeProtocol() - { - Metrics.AddEvent("Failsafe protocol initiated"); - Settings.ResetSettings(); - PasswordVault passwordVault = new PasswordVault(); - IReadOnlyList credentialEntries = passwordVault.RetrieveAll(); - foreach (PasswordCredential credential in credentialEntries) - passwordVault.Remove(credential); - RestartApp(); - } - } -} diff --git a/FoxTube.Core/Models/Attributes.cs b/FoxTube.Core/Models/Attributes.cs new file mode 100644 index 0000000..550c86b --- /dev/null +++ b/FoxTube.Core/Models/Attributes.cs @@ -0,0 +1,7 @@ +using System; + +namespace FoxTube.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class RefreshableAttribute : Attribute { } +} \ No newline at end of file diff --git a/FoxTube.Core/Models/DownloadItem.cs b/FoxTube.Core/Models/DownloadItem.cs new file mode 100644 index 0000000..1a5bdda --- /dev/null +++ b/FoxTube.Core/Models/DownloadItem.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Windows.Storage; +using YoutubeExplode; +using YoutubeExplode.Videos.Streams; + +namespace FoxTube.Models +{ + public enum DownloadState { Initializing, Downloading, Cancelling } + + public class DownloadItem : SavedVideo + { + public DownloadState State { get; private set; } = DownloadState.Initializing; + public IProgress DownloadPercentage { get; set; } + + + private CancellationTokenSource CTS { get; set; } = new CancellationTokenSource(); + + public async Task CommenceDownload(IStreamInfo stream, IStorageFile destination) + { + Path = destination.Path; + YoutubeClient client = new YoutubeClient(UserManagement.Service.HttpClient); + + State = DownloadState.Downloading; + Task task = client.Videos.Streams.DownloadAsync(stream, Path, DownloadPercentage, CTS.Token); + await task.ConfigureAwait(false); + + if (!task.IsCanceled) + return; + + await destination.DeleteAsync(StorageDeleteOption.PermanentDelete); + throw new OperationCanceledException(); + } + + public void Cancel() + { + State = DownloadState.Cancelling; + CTS.Cancel(); + } + } +} \ No newline at end of file diff --git a/FoxTube.Core/Models/IRefreshable.cs b/FoxTube.Core/Models/IRefreshable.cs deleted file mode 100644 index 2137791..0000000 --- a/FoxTube.Core/Models/IRefreshable.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FoxTube.Core.Models -{ - public interface IRefreshable - { - void RefreshPage(); - } -} diff --git a/FoxTube.Core/Models/Inbox/Changelog.cs b/FoxTube.Core/Models/Inbox/Changelog.cs deleted file mode 100644 index ec3660b..0000000 --- a/FoxTube.Core/Models/Inbox/Changelog.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace FoxTube.Core.Models.Inbox -{ - public class Changelog : InboxItem - { - public override string DefaultIcon => "\xE728"; - public override string Title => "What's new in version " + Id; - public override string Type => "Changelog"; - - public Changelog(string version, string content, string description, DateTime timeStamp) - { - Id = version; - Content = content; - Description = description; - TimeStamp = timeStamp; - } - } -} diff --git a/FoxTube.Core/Models/Inbox/DeveloperMessage.cs b/FoxTube.Core/Models/Inbox/DeveloperMessage.cs deleted file mode 100644 index 7202e21..0000000 --- a/FoxTube.Core/Models/Inbox/DeveloperMessage.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace FoxTube.Core.Models.Inbox -{ - public class DeveloperMessage : InboxItem - { - public override string DefaultIcon => "\xE119"; - public override string Title => _title; - string _title; - public override string Type => "Message from developers"; - - public DeveloperMessage(string id, string title, string content, DateTime timeStamp, string avatar) - { - Id = id; - _title = title; - Content = content; - Description = content; - TimeStamp = timeStamp; - Avatar = avatar; - } - } -} diff --git a/FoxTube.Core/Models/Inbox/InboxItem.cs b/FoxTube.Core/Models/Inbox/InboxItem.cs deleted file mode 100644 index 85da766..0000000 --- a/FoxTube.Core/Models/Inbox/InboxItem.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace FoxTube.Core.Models -{ - public abstract class InboxItem - { - public string Id { get; set; } - public abstract string DefaultIcon { get; } - public string Avatar { get; set; } - public abstract string Title { get; } - public string Description { get; set; } - public string Content { get; set; } - public DateTime TimeStamp { get; set; } - public abstract string Type { get; } - - public string ShortTimeStamp => $"{TimeStamp.ToShortDateString()} {TimeStamp.ToShortTimeString()}"; - } -} diff --git a/FoxTube.Core/Models/InboxItem.cs b/FoxTube.Core/Models/InboxItem.cs new file mode 100644 index 0000000..ab281b9 --- /dev/null +++ b/FoxTube.Core/Models/InboxItem.cs @@ -0,0 +1,21 @@ +using System; + +namespace FoxTube.Models +{ + public class InboxItem + { + public string Id { get; set; } + + public string DefaultIcon { get; set; } + public string Avatar { get; set; } + + public string Title { get; set; } + public string Description { get; set; } + public string Content { get; set; } + + public DateTime TimeStamp { get; set; } + public string Type { get; set; } + + public string ShortTimeStamp => $"{TimeStamp.ToShortDateString()} {TimeStamp.ToShortTimeString()}"; + } +} \ No newline at end of file diff --git a/FoxTube.Core/Models/Notifications.cs b/FoxTube.Core/Models/Notifications.cs deleted file mode 100644 index 2e88e3f..0000000 --- a/FoxTube.Core/Models/Notifications.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Windows.Data.Xml.Dom; -using Windows.UI.Notifications; - -namespace FoxTube.Core.Models -{ - public static class Notifications - { - public static ToastNotification GetChangelogToast(string version) - { - XmlDocument template = new XmlDocument(); - - // TODO: Add backend - template.LoadXml($@" - - - - - Changelog - See what's new in {version} - - - "); - - return new ToastNotification(template); - } - } -} diff --git a/FoxTube.Core/Models/PageView.cs b/FoxTube.Core/Models/PageView.cs deleted file mode 100644 index 3d6c96f..0000000 --- a/FoxTube.Core/Models/PageView.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Navigation; - -namespace FoxTube.Core.Models -{ - public class PageView : Page - { - public string Header - { - get => _header; - set - { - _header = value; - UpdateTitle(); - } - } - string _header; - public object Parameter { get; private set; } - - protected override void OnNavigatedTo(NavigationEventArgs e) - { - base.OnNavigatedTo(e); - Parameter = e.Parameter; - } - - public virtual void UpdateTitle() { } - } -} diff --git a/FoxTube.Core/Models/SavedVideo.cs b/FoxTube.Core/Models/SavedVideo.cs new file mode 100644 index 0000000..8ce30e7 --- /dev/null +++ b/FoxTube.Core/Models/SavedVideo.cs @@ -0,0 +1,18 @@ +using System; + +namespace FoxTube.Models +{ + public class SavedVideo + { + public string Title { get; set; } + public string Author { get; set; } + public string Thumbnail { get; set; } + public TimeSpan Duration { get; set; } + + public string Id { get; set; } + public string AccessToken { get; set; } + + public string Path { get; set; } + public bool IsPathValid { get; set; } = true; + } +} \ No newline at end of file diff --git a/FoxTube.Core/Models/SearchSuggestion.cs b/FoxTube.Core/Models/SearchSuggestion.cs index 555dcab..e382947 100644 --- a/FoxTube.Core/Models/SearchSuggestion.cs +++ b/FoxTube.Core/Models/SearchSuggestion.cs @@ -1,15 +1,15 @@ -namespace FoxTube.Core.Models +namespace FoxTube.Models { - public class SearchSuggestion - { - public string Icon { get; set; } - public string Text { get; set; } + public class SearchSuggestion + { + public string Icon { get; set; } + public string Text { get; set; } - public SearchSuggestion(string text) : this(text, false) { } - public SearchSuggestion(string text, bool isHistoryEntry) - { - Text = text; - Icon = isHistoryEntry ? "\xE81C" : ""; - } - } -} + public SearchSuggestion(string text) : this(text, false) { } + public SearchSuggestion(string text, bool isHistoryEntry) + { + Text = text; + Icon = isHistoryEntry ? "\xE81C" : ""; + } + } +} \ No newline at end of file diff --git a/FoxTube.Core/Models/SuspendedUser.cs b/FoxTube.Core/Models/SuspendedUser.cs new file mode 100644 index 0000000..d986cf3 --- /dev/null +++ b/FoxTube.Core/Models/SuspendedUser.cs @@ -0,0 +1,10 @@ +namespace FoxTube.Models +{ + public class SuspendedUser + { + public string Name { get; set; } + public string Email { get; set; } + public string Avatar { get; set; } + public string RefreshToken { get; set; } + } +} \ No newline at end of file diff --git a/FoxTube.Core/Models/User.cs b/FoxTube.Core/Models/User.cs index bd4936f..b8b9f04 100644 --- a/FoxTube.Core/Models/User.cs +++ b/FoxTube.Core/Models/User.cs @@ -7,52 +7,52 @@ using Google.Apis.YouTube.v3.Data; using System.Collections.Generic; using YouTube; -namespace FoxTube.Core.Models +namespace FoxTube.Models { - public class User - { - public Userinfoplus UserInfo { get; } - public UserCredential Credential { get; } - public Channel Channel { get; private set; } - public List Subscriptions { get; } = new List(); - public ExtendedYouTubeService Service { get; } + public class User + { + public Userinfoplus UserInfo { get; } + public UserCredential Credential { get; } + public Channel Channel { get; private set; } + public List Subscriptions { get; } = new List(); + public ExtendedYouTubeService Service { get; } - public User(UserCredential credential) - { - Credential = credential; - BaseClientService.Initializer initializer = new BaseClientService.Initializer - { - ApplicationName = "FoxTube", - HttpClientInitializer = Credential - }; + public User(UserCredential credential) + { + Credential = credential; + BaseClientService.Initializer initializer = new BaseClientService.Initializer + { + ApplicationName = "FoxTube", + HttpClientInitializer = Credential + }; - Service = new ExtendedYouTubeService(initializer); + Service = new ExtendedYouTubeService(initializer); - UserInfo = new Oauth2Service(initializer).Userinfo.Get().Execute(); + UserInfo = new Oauth2Service(initializer).Userinfo.Get().Execute(); - // TODO: Retrieve history and WL + // TODO: Retrieve history and WL - SubscriptionsResource.ListRequest subRequest = Service.Subscriptions.List("snippet"); - subRequest.Mine = true; - subRequest.MaxResults = 50; - subRequest.Order = SubscriptionsResource.ListRequest.OrderEnum.Relevance; - SubscriptionListResponse subResponse; - string nextToken = null; - Subscriptions.Clear(); + SubscriptionsResource.ListRequest subRequest = Service.Subscriptions.List("snippet"); + subRequest.Mine = true; + subRequest.MaxResults = 50; + subRequest.Order = SubscriptionsResource.ListRequest.OrderEnum.Relevance; + SubscriptionListResponse subResponse; + string nextToken = null; + Subscriptions.Clear(); - do - { - subRequest.PageToken = nextToken; - subResponse = subRequest.Execute(); - foreach (Subscription s in subResponse.Items) - Subscriptions.Add(s); - nextToken = subResponse.NextPageToken; + do + { + subRequest.PageToken = nextToken; + subResponse = subRequest.Execute(); + foreach (Subscription s in subResponse.Items) + Subscriptions.Add(s); + nextToken = subResponse.NextPageToken; - } while (!string.IsNullOrWhiteSpace(nextToken)); + } while (!string.IsNullOrWhiteSpace(nextToken)); - var request = Service.Channels.List("snippet,contentDetails"); - request.Mine = true; - Channel = request.Execute().Items[0]; - } - } + var request = Service.Channels.List("snippet,contentDetails,brandingSettings"); + request.Mine = true; + Channel = request.Execute().Items[0]; + } + } } diff --git a/FoxTube.Core/Models/VideoItem.cs b/FoxTube.Core/Models/VideoItem.cs new file mode 100644 index 0000000..4849a6e --- /dev/null +++ b/FoxTube.Core/Models/VideoItem.cs @@ -0,0 +1,48 @@ +using Google.Apis.YouTube.v3.Data; +using System; +using YoutubeExplode; + +namespace FoxTube.Models +{ + public class VideoItem + { + public Video Meta { get; set; } + public YoutubeExplode.Videos.Video AdditionalMeta { get; set; } + public YoutubeExplode.Channels.Channel ChannelMeta { get; set; } + + public string TimeLabel { get; set; } + public string ViewsLabel { get; set; } + public int LiveLabelOpacity => Meta?.LiveStreamingDetails == null ? 0 : 1; + public string LiveLabel + { + get + { + if (Meta?.LiveStreamingDetails == null) + return ""; + else if (Meta.LiveStreamingDetails.ActualStartTime.HasValue) + return "LIVE"; + else if (Meta.LiveStreamingDetails.ScheduledStartTime.HasValue) + return $"Live in {Meta.LiveStreamingDetails.ScheduledStartTime - DateTime.Now}"; + else + return "Upcoming"; + } + } + + public VideoItem(Video meta) + { + Meta = meta; + LoadInfo(); + } + + private async void LoadInfo() + { + YoutubeClient client = new YoutubeClient(UserManagement.Service.HttpClient); + + AdditionalMeta = await client.Videos.GetAsync(Meta.Id); + ChannelMeta = await client.Channels.GetByVideoAsync(Meta.Id); + + TimeLabel = $"{AdditionalMeta?.Duration} • {AdditionalMeta.UploadDate.DateTime.GetFriendlyDate()}"; + ViewsLabel = $"{AdditionalMeta?.Engagement.ViewCount} views"; + } + } +} \ No newline at end of file diff --git a/FoxTube.Core/Services/DownloadsCenter.cs b/FoxTube.Core/Services/DownloadsCenter.cs new file mode 100644 index 0000000..aa9d924 --- /dev/null +++ b/FoxTube.Core/Services/DownloadsCenter.cs @@ -0,0 +1,127 @@ +using Newtonsoft.Json; +using System; +using System.IO; +using System.Collections.Generic; +using System.Threading.Tasks; +using Windows.Storage; +using Windows.Storage.AccessCache; +using YoutubeExplode.Videos; +using YoutubeExplode.Videos.Streams; +using FoxTube.Utils; +using FoxTube.Models; + +namespace FoxTube.Services +{ + public static class DownloadsCenter + { + public static List History { get; private set; } + public static List Queue { get; } = new List(); + + static DownloadsCenter() => + Initialize(); + + private static async void Initialize() + { + StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync("DownloadHistory.json", CreationCollisionOption.OpenIfExists); + try + { + History = JsonConvert.DeserializeObject>(File.ReadAllText(file.Path) ?? "") ?? new List(); + + foreach (SavedVideo i in History) + try { i.IsPathValid = await StorageApplicationPermissions.MostRecentlyUsedList.GetFileAsync(i.AccessToken) != null; } + catch { i.IsPathValid = false; } + } + catch (Exception e) + { + History = new List(); + await file.DeleteAsync(StorageDeleteOption.PermanentDelete); + StorageApplicationPermissions.MostRecentlyUsedList.Clear(); + Metrics.SendReport(new Exception("Failed to load downloads history", e)); + } + } + + public static Task DownloadVideo(Video meta, IStreamInfo streamInfo) => + DownloadVideo(meta, streamInfo, null); + + public static async Task DownloadVideo(Video meta, IStreamInfo streamInfo, IStorageFile destination) + { + DownloadItem item = new DownloadItem + { + Title = $"[{(streamInfo as IVideoStreamInfo)?.VideoQualityLabel ?? "Audio"}] {meta.Title}", + Author = meta.Author, + Thumbnail = meta.Thumbnails.LowResUrl, + Duration = meta.Duration, + Id = meta.Id + }; + Queue.Add(item); + + if (destination == null) + destination = await (await GetDefaultDownloadsFolder()).CreateFileAsync($"{meta.Title.ReplaceInvalidChars('_')}.{streamInfo.Container.Name}", CreationCollisionOption.GenerateUniqueName); + + item.Path = destination.Path; + + try + { + await item.CommenceDownload(streamInfo, destination); + + SavedVideo savedItem = item as SavedVideo; + savedItem.AccessToken = StorageApplicationPermissions.MostRecentlyUsedList.Add(destination); + + History.Add(savedItem); + + StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync("DownloadHistory.json", CreationCollisionOption.OpenIfExists); + File.WriteAllText(file.Path, JsonConvert.SerializeObject(History)); + } + catch (OperationCanceledException) { } + catch (Exception e) + { + await destination.DeleteAsync(StorageDeleteOption.PermanentDelete); + Metrics.SendReport(new Exception("Failed to download video", e), null, + ("Video ID", meta.Id), + ("Stream tag", streamInfo.Tag.ToString())); + } + finally + { + Queue.Remove(item); + } + } + + public static async Task RemoveItems(bool removeFiles, params SavedVideo[] items) + { + foreach(SavedVideo i in items) + { + History.Remove(i); + try + { + if (removeFiles) + { + StorageFile file = await StorageApplicationPermissions.MostRecentlyUsedList.GetFileAsync(i.AccessToken); + await file?.DeleteAsync(); + } + } + finally + { + StorageApplicationPermissions.MostRecentlyUsedList.Remove(i.AccessToken); + } + } + + StorageFile historyFile = await ApplicationData.Current.LocalFolder.CreateFileAsync("DownloadHistory.json", CreationCollisionOption.OpenIfExists); + File.WriteAllText(historyFile.Path, JsonConvert.SerializeObject(History)); + } + + public static async Task GetDefaultDownloadsFolder() + { + if (string.IsNullOrWhiteSpace(Settings.DefaultDownloadsFolder)) + return await KnownFolders.VideosLibrary.CreateFolderAsync("FoxTube", CreationCollisionOption.OpenIfExists); + else + return await StorageApplicationPermissions.FutureAccessList.GetFolderAsync(Settings.DefaultDownloadsFolder); + } + + public static async Task CancelAll() + { + Queue.ForEach(i => i.Cancel()); + while (Queue.Count > 0) + await Task.Delay(500); + } + } +} \ No newline at end of file diff --git a/FoxTube.Core/Services/History.cs b/FoxTube.Core/Services/History.cs new file mode 100644 index 0000000..03c95da --- /dev/null +++ b/FoxTube.Core/Services/History.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using Windows.Storage; + +namespace FoxTube.Services +{ + public static class History + { + static readonly ApplicationDataContainer storage = ApplicationData.Current.RoamingSettings; + + public static string[] SearchHistory + { + get => JsonConvert.DeserializeObject(storage.Values["SearchHistory"] as string) ?? new string[0]; + private set => JsonConvert.SerializeObject(value); + } + + public static void AddSearchHistoryEntry(string term) + { + List history = SearchHistory.ToList(); + history.Insert(0, term); + + if (history.Count > 5) + history.RemoveRange(5, history.Count - 5); + + SearchHistory = history.ToArray(); + } + + public static void ClearSearchHistory() => + SearchHistory = new string[0]; + } +} \ No newline at end of file diff --git a/FoxTube.Core/Services/Inbox.cs b/FoxTube.Core/Services/Inbox.cs new file mode 100644 index 0000000..052ba36 --- /dev/null +++ b/FoxTube.Core/Services/Inbox.cs @@ -0,0 +1,83 @@ +using FoxTube.Models; +using FoxTube.Utils; +using Newtonsoft.Json; +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Windows.Storage; +using Windows.UI.Notifications; + +namespace FoxTube.Services +{ + public static class Inbox + { + private static readonly HttpClient client = new HttpClient(); + private static readonly ApplicationDataContainer storage = ApplicationData.Current.RoamingSettings; + + public static async void PushNew() + { + try + { + // TODO: Add backend + HttpResponseMessage response = await client.GetAsync($"https://xfox111.net/FoxTube/Messages?toast=true&publishedAfter={storage.Values["Inbox.lastCheck"]}&lang={Settings.Language}&appVersion={Metrics.CurrentVersion}"); + storage.Values["Inbox.lastCheck"] = DateTime.UtcNow.Ticks; + + if (response.StatusCode == HttpStatusCode.NoContent) + return; + + string[] toasts = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + + foreach (string toast in toasts) + ToastNotificationManager.CreateToastNotifier().Show(new ToastNotification(toast.ToXml())); + } + catch (Exception e) + { + Metrics.SendReport(new Exception("Unable to retrieve toast notifications", e)); + } + } + + public static async Task GetMessages() + { + try + { + // TODO: Add backend + HttpResponseMessage response = await client.GetAsync($"https://xfox111.net/API/FoxTube/Inbox?lang={Settings.Language}¤tVersion={Metrics.CurrentVersion}"); + + if (response.StatusCode == HttpStatusCode.NoContent) + return new InboxItem[0]; + + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + } + catch (Exception e) + { + Metrics.SendReport(new Exception("Unable to retrieve inbox messages", e)); + + return new InboxItem[0]; + } + } + + /// + /// Fires toast notification with the last changelog content + /// + public static async void PushChangelog() + { + try + { + // TODO: Add backend + Settings.LastReviewedVersion = Metrics.CurrentVersion; + + HttpResponseMessage response = await client.GetAsync($"https://xfox111.net/API/FoxTube/Changelogs?toast=true&lang={Settings.Language}&version={Metrics.CurrentVersion}"); + + if (response.StatusCode == HttpStatusCode.NoContent) + return; + + ToastNotificationManager.CreateToastNotifier().Show(new ToastNotification((await response.Content.ReadAsStringAsync()).ToXml())); + } + catch (Exception e) + { + Metrics.SendReport(new Exception("Unable to retrieve changelog", e)); + } + } + } +} diff --git a/FoxTube.Core/Services/Search.cs b/FoxTube.Core/Services/Search.cs new file mode 100644 index 0000000..a80f8fe --- /dev/null +++ b/FoxTube.Core/Services/Search.cs @@ -0,0 +1,36 @@ +using FoxTube.Models; +using System.Linq; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using System.Xml; + +namespace FoxTube.Services +{ + public static class Search + { + public static async Task> GetSuggestions(string term) + { + List suggestions = new List(); + + try + { + using HttpClient client = new HttpClient(); + string results = await client.GetStringAsync($"http://suggestqueries.google.com/complete/search?ds=yt&client=toolbar&q={term}&hl={Settings.RelevanceLanguage}"); + XmlDocument doc = new XmlDocument(); + doc.LoadXml(results); + + for (int i = 0; i < doc["toplevel"].ChildNodes.Count && i < 5; i++) + suggestions.Add(new SearchSuggestion(doc["toplevel"].ChildNodes[i]["suggestion"].GetAttribute("data"))); + + // Appending search history + suggestions.AddRange(History.SearchHistory.Select(i => new SearchSuggestion(i, true))); + + return suggestions; + } + catch { } + + return suggestions; + } + } +} diff --git a/FoxTube.Core/Settings.cs b/FoxTube.Core/Settings.cs new file mode 100644 index 0000000..a2c2501 --- /dev/null +++ b/FoxTube.Core/Settings.cs @@ -0,0 +1,139 @@ +using FoxTube.Utils; +using System.Globalization; +using Windows.Storage; + +namespace FoxTube +{ + public static class Settings + { + static readonly ApplicationDataContainer settings = ApplicationData.Current.RoamingSettings; + + public static string DefaultDownloadsFolder + { + get => (string)settings.Values["DefaultDownloadsFolder"] ?? ""; + set => settings.Values["DefaultDownloadsFolder"] = value; + } + public static bool AskBeforeDownloading + { + get => (bool?)settings.Values["AskBeforeDownloading"] ?? true; + set => settings.Values["AskBeforeDownloading"] = value; + } + public static bool AllowAnalytics + { + get => (bool?)settings.Values["AllowAnalytics"] ?? true; + set => settings.Values["AllowAnalytics"] = value; + } + public static string DesiredVideoQuality + { + get => (string)settings.Values["DesiredVideoQuality"] ?? "auto"; + set => settings.Values["DesiredVideoQuality"] = value; + } + public static string RememberedQuality + { + get => (string)settings.Values["RememberedVideoQuality"] ?? "1080p"; + set => settings.Values["RememberedVideoQuality"] = value; + } + public static bool VideoNotifications + { + get => (bool?)settings.Values["NewVideosNotificationsAll"] ?? true; + set => settings.Values["NewVideosNotificationsAll"] = value; + } + public static bool DevNotifications + { + get => (bool?)settings.Values["DevelopersNewsNotifications"] ?? true; + set => settings.Values["DevelopersNewsNotifications"] = value; + } + public static bool CheckConnection + { + get => (bool?)settings.Values["WarnIfOnMeteredConnection"] ?? false; + set => settings.Values["WarnIfOnMeteredConnection"] = value; + } + public static bool Autoplay + { + get => (bool?)settings.Values["VideoAutoplay"] ?? true; + set => settings.Values["VideoAutoplay"] = value; + } + public static double Volume + { + get => (double?)settings.Values["Volume"] ?? 1; + set => settings.Values["Volume"] = value; + } + public static string Language + { + get => (string)settings.Values["InterfaceLanguage"] ?? GetDefaultLanguage(); + set => settings.Values["InterfaceLanguage"] = value; + } + public static string RelevanceLanguage + { + get => (string)settings.Values["DesiredContentLanguage"] ?? CultureInfo.InstalledUICulture.TwoLetterISOLanguageName; + set => settings.Values["DesiredContentLanguage"] = value; + } + public static string Region + { + get => (string)settings.Values["Region"] ?? CultureInfo.InstalledUICulture.Name.Split('-')[1]; + set => settings.Values["Region"] = value; + } + public static int SafeSearch + { + get => (int?)settings.Values["SafeSearch"] ?? 0; // Moderate + set => settings.Values["SafeSearch"] = value; + } + public static int DefaultHomeTab + { + get => (int?)settings.Values["DefaultHomeTab"] ?? 0; // Recommendations + set => settings.Values["DefaultHomeTab"] = value; + } + public static bool BlockExplicitContent + { + get => (bool?)settings.Values["BlockExplicitContent"] ?? true; + set => settings.Values["BlockExplicitContent"] = value; + } + + public static bool HasAccount + { + get => (bool?)settings.Values["HasAccount"] ?? false; + set => settings.Values["HasAccount"] = value; + } + public static int Theme + { + get => (int?)settings.Values["PreferedUITheme"] ?? 2; // System + set => settings.Values["PreferedUITheme"] = value; + } + public static bool PromptReview + { + get => (bool?)settings.Values["PromptReview"] ?? Metrics.Uptime.TotalHours > 24; + set => settings.Values["PromptReview"] = value; + } + public static bool PromptFeedback + { + get => (bool?)settings.Values["PromptFeedback"] ?? Metrics.Uptime.TotalHours > 12; + set => settings.Values["PromptFeedback"] = value; + } + public static bool ProcessClipboard + { + get => (bool?)settings.Values["ProcessClipboardEntry"] ?? true; + set => settings.Values["ProcessClipboardEntry"] = value; + } + public static string LastReviewedVersion + { + get + { + if (settings.Values["LastReviewedVersion"] == null) + settings.Values["LastReviewedVersion"] = Metrics.CurrentVersion; + return (string)settings.Values["LastReviewedVersion"]; + } + set => settings.Values["LastReviewedVersion"] = value; + } + + static string GetDefaultLanguage() + { + if (CultureInfo.InstalledUICulture.TwoLetterISOLanguageName.Belongs("ua", "ru", "by", "kz", "kg", "md", "lv", "ee")) //Languages for Russian-speaking countries + return "ru-RU"; + else + return "en-US"; + } + + public static void ResetSettings() => + settings.Values.Clear(); + } +} diff --git a/FoxTube.Core/UserManagement.cs b/FoxTube.Core/UserManagement.cs new file mode 100644 index 0000000..87d3f2d --- /dev/null +++ b/FoxTube.Core/UserManagement.cs @@ -0,0 +1,124 @@ +using Google.Apis.Auth.OAuth2; +using Google.Apis.Oauth2.v2; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Windows.Security.Authentication.Web; +using YouTube.Authorization; +using System.Text.RegularExpressions; +using Windows.Security.Credentials; +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 Windows.UI.Popups; +using FoxTube.Utils; + +namespace FoxTube +{ + public static class UserManagement + { + static ExtendedYouTubeService _defaultService = new ExtendedYouTubeService(new Google.Apis.Services.BaseClientService.Initializer + { + ApplicationName = "FoxTube", + ApiKey = "AIzaSyBgHrCnrlzlVmk0cJKL8RqP9Y8x6XSuk_0" + //ApiKey = "AIzaSyD7tpbuvmYDv9h4udo9L_g3r0sLPFAnN00" + }); + + static string[] Scopes { get; } = new string[] + { + Oauth2Service.Scope.UserinfoProfile, + Oauth2Service.Scope.UserinfoEmail, + YouTubeService.Scope.YoutubeForceSsl + }; + + static ClientSecrets ClientSecrets { get; } = new ClientSecrets + { + ClientId = "349735264870-2ekqlm0a4mkg3mmrfcv90s3qp3o15dq0.apps.googleusercontent.com", + ClientSecret = "BkVZOAaCU2Zclf0Zlicg6y2_" + //ClientId = "1096685398208-u95rcpkqb4e1ijfmb8jdq3jsg37l8igv.apps.googleusercontent.com", + //ClientSecret = "IU5bbdjwvmx8ttJoXQ7e6JWd" + }; + + public static User CurrentUser { get; set; } + public static bool Authorized => CurrentUser != null; + public static ExtendedYouTubeService Service => CurrentUser?.Service ?? _defaultService; + + public static event EventHandler UserStateUpdated; + public static event EventHandler SubscriptionsChanged; + + public static async Task AddUser() + { + Uri requestString = AuthorizationHelpers.FormQueryString(ClientSecrets, Scopes); + + WebAuthenticationResult result = await WebAuthenticationBroker.AuthenticateAsync(WebAuthenticationOptions.UseTitle, requestString, AuthorizationHelpers.Endpoint); + switch (result.ResponseStatus) + { + case WebAuthenticationStatus.Success: + string successCode = new Regex(@"(?<=code=)(.*?)(?=&)").Match(result.ResponseData).Value; + + UserCredential credential = await AuthorizationHelpers.ExchangeToken(ClientSecrets, successCode); + CurrentUser = new User(credential); + + PasswordVault passwordVault = new PasswordVault(); + passwordVault.Add(new PasswordCredential("foxtube", CurrentUser.UserInfo.Id, credential.Token.RefreshToken)); + UserStateUpdated?.Invoke(null, true); + return true; + case WebAuthenticationStatus.UserCancel: + break; + case WebAuthenticationStatus.ErrorHttp: + await new ContentDialog + { + Title = "Something went wrong...", + Content = "It may be a bug or temporary server issues. Please, try again later" + }.ShowAsync(); + + Metrics.SendReport(new Exception("Authorization failed (HTTP Error)"), null, + ("Response status", result.ResponseStatus.ToString()), + ("Response data", result.ResponseData), + ("Error details", result.ResponseErrorDetail.ToString())); + break; + } + return false; + } + + public static async Task Initialize() + { + PasswordVault passwordVault = new PasswordVault(); + IReadOnlyList credentials; + credentials = passwordVault.RetrieveAll(); + + if (credentials.Count == 0) + return; + + try + { + credentials[0].RetrievePassword(); + UserCredential credential = await AuthorizationHelpers.RestoreUser(ClientSecrets, credentials[0].Password); + credentials[0].Password = credential.Token.RefreshToken; + CurrentUser = new User(credential); + } + catch (Exception e) + { + await new MessageDialog("It may be a bug or temporary server issues. Please, try again later", "Something went wrong...").ShowAsync(); + + Metrics.SendReport(new Exception("Refresh token exchange failed", e)); + + foreach (PasswordCredential i in credentials) + passwordVault.Remove(i); + } + } + + public static async Task Logout() + { + PasswordVault passwordVault = new PasswordVault(); + PasswordCredential credential = passwordVault.Retrieve("foxtube", CurrentUser.UserInfo.Id); + passwordVault.Remove(credential); + await CurrentUser.Credential.RevokeTokenAsync(CancellationToken.None); + + UserStateUpdated?.Invoke(null, false); + } + } +} diff --git a/FoxTube.Core/Utils/Feedback.cs b/FoxTube.Core/Utils/Feedback.cs new file mode 100644 index 0000000..44e0f52 --- /dev/null +++ b/FoxTube.Core/Utils/Feedback.cs @@ -0,0 +1,77 @@ +using System; +using Microsoft.Services.Store.Engagement; +using Windows.System; +using Windows.UI.Xaml.Controls; + +namespace FoxTube.Utils +{ + public static class Feedback + { + public static bool HasFeedbackHub => StoreServicesFeedbackLauncher.IsSupported(); + + public static async void OpenFeedbackHub() + { + if (HasFeedbackHub) + await StoreServicesFeedbackLauncher.GetDefault().LaunchAsync(); + else + await Launcher.LaunchUriAsync("mailto:feedback@xfox111.net".ToUri()); + } + + public static async void PromptFeedback() + { + if (!HasFeedbackHub) + { + Settings.PromptFeedback = false; + return; + } + + ContentDialog dialog = new ContentDialog + { + Title = "Have some thoughts?", + + PrimaryButtonText = "Sure!", + SecondaryButtonText = "Don't ask me anymore", + CloseButtonText = "Maybe later", + + DefaultButton = ContentDialogButton.Primary, + + Content = "Would you like to share something you like or dislike in the app? Or perhaps you have some ideas? Leave feedback!" + }; + + ContentDialogResult result = await dialog.ShowAsync(); + + if (result != ContentDialogResult.None) + Settings.PromptFeedback = false; + + if (result == ContentDialogResult.Primary) + OpenFeedbackHub(); + } + + public static async void PromptReview() + { + ContentDialog dialog = new ContentDialog + { + Title = "Like our app?", + + PrimaryButtonText = "Sure!", + SecondaryButtonText = "Don't ask me anymore", + CloseButtonText = "Maybe later", + + DefaultButton = ContentDialogButton.Primary, + + Content = new TextBlock + { + Text = "Could you leave a feedback on Microsfot Store page? It's very important for me :)" + } + }; + + ContentDialogResult result = await dialog.ShowAsync(); + + if (result != ContentDialogResult.None) + Settings.PromptReview = false; + + if (result == ContentDialogResult.Primary) + StoreInterop.RequestReview(); + } + } +} diff --git a/FoxTube.Core/Utils/Metrics.cs b/FoxTube.Core/Utils/Metrics.cs new file mode 100644 index 0000000..5e45219 --- /dev/null +++ b/FoxTube.Core/Utils/Metrics.cs @@ -0,0 +1,66 @@ +using Microsoft.AppCenter; +using Microsoft.AppCenter.Analytics; +using Microsoft.AppCenter.Crashes; +using System; +using System.Linq; +using System.Collections.Generic; +using System.Diagnostics; +using Windows.ApplicationModel; +using Windows.Storage; + +namespace FoxTube.Utils +{ + public static class Metrics + { + static readonly ApplicationDataContainer storage = ApplicationData.Current.RoamingSettings; + + static readonly Stopwatch sw = new Stopwatch(); + public static TimeSpan Uptime + { + get => (TimeSpan?)storage.Values["Metrics.SpentTime"] ?? TimeSpan.FromSeconds(0); + set => storage.Values["Metrics.SpentTime"] = value; + } + public static string CurrentVersion + { + get + { + PackageVersion v = Package.Current.Id.Version; + return $"{v.Major}.{v.Minor}.{v.Revision}.{v.Build}"; + } + } + + static Metrics() + { + sw.Start(); + if (!Settings.AllowAnalytics) + return; + + AppCenter.Start("45774462-9ea7-438a-96fc-03982666f39e", typeof(Analytics), typeof(Crashes)); + AppCenter.SetCountryCode(Settings.Region); + AppCenter.LogLevel = LogLevel.Verbose; + } + + public static void EndSession() + { + sw.Stop(); + Uptime += sw.Elapsed; + + AddEvent("Session closed", + ("Duration", sw.Elapsed.ToString()), + ("Spend time total", Uptime.ToString())); + } + + public static void AddEvent(string eventName, params (string key, string value)[] details) => + Analytics.TrackEvent(eventName, + details.Length < 1 ? null : + details.Select(i => new KeyValuePair(i.key, i.value)) as Dictionary); + + public static void SendReport(Exception exception, ErrorAttachmentLog[] logs = null, params (string key, string value)[] details) + { + Crashes.TrackError(exception, + details.Length < 1 ? null : + details.Select(i => new KeyValuePair(i.key, i.value)) as Dictionary, + logs); + } + } +} \ No newline at end of file diff --git a/FoxTube.Core/Utils/StoreInterop.cs b/FoxTube.Core/Utils/StoreInterop.cs new file mode 100644 index 0000000..4e01151 --- /dev/null +++ b/FoxTube.Core/Utils/StoreInterop.cs @@ -0,0 +1,61 @@ +using Microsoft.Advertising.WinRT.UI; +using Microsoft.AppCenter.Crashes; +using System; +using System.Threading.Tasks; +using Windows.Services.Store; + +namespace FoxTube.Utils +{ + public static class StoreInterop + { + public static bool AdsDisabled { get; private set; } = true; + public static string Price { get; private set; } + + + private static bool UseTestAds => true; + + private static string ApplicationId => UseTestAds ? "d25517cb-12d4-4699-8bdc-52040c712cab" : "9ncqqxjtdlfh"; + private static string AdsId => UseTestAds ? "test" : "1100044398"; + private static string ProProductId => "9NP1QK556625"; + + public static NativeAdsManagerV2 AdsManager => new NativeAdsManagerV2(ApplicationId, AdsId); + + public static async Task UpdateStoreState() + { + StoreProductQueryResult requset = await StoreContext.GetDefault().GetAssociatedStoreProductsAsync(new[] { "Durable" }); + + if (requset.Products[ProProductId].IsInUserCollection) + return; + + Price = requset.Products[ProProductId].Price.FormattedPrice; + AdsDisabled = false; + } + + public static async Task PurchaseApp() + { + StorePurchaseResult request = await StoreContext.GetDefault().RequestPurchaseAsync(ProProductId); + + switch (request.Status) + { + case StorePurchaseStatus.AlreadyPurchased: + case StorePurchaseStatus.Succeeded: + AdsDisabled = true; + return true; + default: + return false; + } + } + + public static async void RequestReview() + { + StoreRateAndReviewResult result = await StoreContext.GetDefault().RequestRateAndReviewAppAsync(); + + if (result.Status == StoreRateAndReviewStatus.Error) + Metrics.SendReport(result.ExtendedError, new[] { ErrorAttachmentLog.AttachmentWithText(result.ExtendedJsonData, "extendedJsonData.json") }, + ("Status", result.Status.ToString()), + ("WasReviewUpdated", result.WasUpdated.ToString())); + + Metrics.AddEvent("Store review request has been recieved"); + } + } +} diff --git a/FoxTube.Core/Utils/Utils.cs b/FoxTube.Core/Utils/Utils.cs new file mode 100644 index 0000000..ca9d1ef --- /dev/null +++ b/FoxTube.Core/Utils/Utils.cs @@ -0,0 +1,38 @@ +using System; +using Windows.ApplicationModel.Core; +using Windows.Security.Credentials; + +namespace FoxTube.Utils +{ + public static class Utils + { + /// + /// Terminates current application session + /// + public static void CloseApp() => + CoreApplication.Exit(); + + /// + /// Restarts application + /// + public static void RestartApp() => + RestartApp(null); + + /// + /// Restarts application with specified parameters + /// + /// Parameters which will be provided to new application instance + public static async void RestartApp(string args) => + await CoreApplication.RequestRestartAsync(args ?? ""); + + public static void InitializeFailsafeProtocol() + { + Metrics.AddEvent("Failsafe protocol initiated"); + Settings.ResetSettings(); + PasswordVault passwordVault = new PasswordVault(); + foreach (PasswordCredential credential in passwordVault.RetrieveAll()) + passwordVault.Remove(credential); + RestartApp(); + } + } +}