From 9bd13b6792a58102600ece41a50c82eca8f78057 Mon Sep 17 00:00:00 2001 From: Michael Gordeev Date: Sun, 28 Apr 2019 11:58:32 +0300 Subject: [PATCH] - History page re-design - Added app history management (doesn't affect web site's history) - Extended history information for videos (watching progress) - Continue where you left off feature - Watch later playlist now acts like regular playlist - If video is longer than 1 hour ads will be shown every 30 minutes - Added incognito mode (available in video card context menu) - Search suggestions now run smoother Related Work Items: #140, #252 --- FoxTube/Assets/Data/Patchnotes.xml | 18 ++- FoxTube/Classes/Methods.cs | 44 +++++++ FoxTube/Classes/SecretsVault.cs | 2 +- FoxTube/Classes/VideoProcessor.cs | 35 ++++++ FoxTube/Controls/Player/VideoPlayer.xaml | 3 +- FoxTube/Controls/Player/VideoPlayer.xaml.cs | 116 ++++++----------- FoxTube/Controls/VideoCard.xaml | 2 +- FoxTube/Controls/VideoCard.xaml.cs | 58 ++++++++- FoxTube/FoxTube.csproj | 1 + FoxTube/Pages/History.xaml | 51 +++++++- FoxTube/Pages/History.xaml.cs | 128 ++++++++++++++----- FoxTube/Pages/MainPage.xaml.cs | 97 +++++++------- FoxTube/Pages/PlaylistPage.xaml | 13 ++ FoxTube/Pages/PlaylistPage.xaml.cs | 64 ++++++++++ FoxTube/Pages/VideoPage.xaml | 1 + FoxTube/Pages/VideoPage.xaml.cs | 133 +++++++++++++++----- FoxTube/Strings/en-US/CommentsPage.resw | 2 +- FoxTube/Strings/en-US/Playlist.resw | 21 ++++ FoxTube/Strings/en-US/VideoPage.resw | 3 + FoxTube/Strings/ru-RU/CommentsPage.resw | 2 +- FoxTube/Strings/ru-RU/Playlist.resw | 21 ++++ FoxTube/Strings/ru-RU/VideoPage.resw | 3 + 22 files changed, 610 insertions(+), 208 deletions(-) create mode 100644 FoxTube/Classes/VideoProcessor.cs diff --git a/FoxTube/Assets/Data/Patchnotes.xml b/FoxTube/Assets/Data/Patchnotes.xml index 9b1ed01..49b14e6 100644 --- a/FoxTube/Assets/Data/Patchnotes.xml +++ b/FoxTube/Assets/Data/Patchnotes.xml @@ -1,6 +1,6 @@  - + ### What's new: - Fixed fails when trying to retrieve history, WL or recommended @@ -9,6 +9,14 @@ - Fixed videos loading - Fixed special characters appearing in toast notifications - Cursor now hides on playback +- History page re-design +- Added app history management (doesn't affect web site's history) +- Extended history information for videos (watching progress) +- Continue where you left off feature +- Watch later playlist now acts like regular playlist +- If video is longer than 1 hour ads will be shown every 30 minutes +- Added incognito mode (available in video card context menu) +- Search suggestions now run smoother ### Что нового: - Исправлена проблема получения истории, "Посмотреть позже" и рекомендаций @@ -17,6 +25,14 @@ - Исправлена загрузка видео - Исправлено появление особых символов в уведомлениях - Теперь курсор скрывается при просмотре +- Редизайн страницы истории +- Добавлено управление историей просмотра приложения (не влияет на историю просмотров на сайте) +- Расширенная информация о просмотренном видео (прогресс просмотра) +- Функция продолжения просмотра +- Плейлист "Посмотреть позже" теперь ведет себя как обычный плейлист +- Если видео длится более 1 часа, рекламный баннер будет появляться каждые 30 минут +- Добавлен режим инкогнито (доступен в контекстном меню видео карточки) +- Подсказки при поиске работают плавнее diff --git a/FoxTube/Classes/Methods.cs b/FoxTube/Classes/Methods.cs index f6ebd82..559aa29 100644 --- a/FoxTube/Classes/Methods.cs +++ b/FoxTube/Classes/Methods.cs @@ -25,6 +25,50 @@ namespace FoxTube { object Parameter { get; set; } } + + public class HistoryItem + { + public string Id { get; set; } + public TimeSpan LeftOn { get; set; } = TimeSpan.FromSeconds(0); + } + + public static class HistorySet + { + public static List Items { get; set; } = new List(); + + public static void Update(HistoryItem item) + { + if(Items.Exists(i => i.Id == item.Id)) + Items.RemoveAll(i => i.Id == item.Id); + + Items.Insert(0, item); + Save(); + } + + public static void Delete(HistoryItem item) + { + Items.Remove(item); + Save(); + } + + public static void Clear() + { + Items.Clear(); + Save(); + } + + private static void Save() + { + ApplicationData.Current.RoamingSettings.Values[$"history-{SecretsVault.AccountId}"] = JsonConvert.SerializeObject(Items); + } + + public static void Load() + { + if (ApplicationData.Current.RoamingSettings.Values[$"history-{SecretsVault.AccountId}"] != null) + Items = JsonConvert.DeserializeObject>(ApplicationData.Current.RoamingSettings.Values[$"history-{SecretsVault.AccountId}"] as string); + } + } + public static class Methods { private static ResourceLoader resources = ResourceLoader.GetForCurrentView("Methods"); diff --git a/FoxTube/Classes/SecretsVault.cs b/FoxTube/Classes/SecretsVault.cs index e79e879..ab7d4ea 100644 --- a/FoxTube/Classes/SecretsVault.cs +++ b/FoxTube/Classes/SecretsVault.cs @@ -42,7 +42,7 @@ namespace FoxTube }; public static YouTubeService Service => IsAuthorized ? new YouTubeService(Initializer) : NoAuthService; public static HttpClient HttpClient { get; } = new HttpClient(); - private static bool TestAds => false; //Change this bool + private static bool TestAds => true; //Change this bool public static string AppId => TestAds ? "d25517cb-12d4-4699-8bdc-52040c712cab" : "9ncqqxjtdlfh"; public static string AdUnitId => TestAds ? "test" : "1100044398"; public static bool AdsDisabled { get; private set; } = true; diff --git a/FoxTube/Classes/VideoProcessor.cs b/FoxTube/Classes/VideoProcessor.cs new file mode 100644 index 0000000..36dc814 --- /dev/null +++ b/FoxTube/Classes/VideoProcessor.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using YoutubeExplode.Models.MediaStreams; +using Windows.Media.Editing; +using Windows.Media.Core; +using Windows.Storage; + +namespace FoxTube.Classes +{ + public class VideoProcessor + { + public VideoPlayer Player { get; set; } + MediaComposition composition = new MediaComposition(); + + StorageFolder roaming = ApplicationData.Current.RoamingFolder; + StorageFile audioCache; + StorageFile videoCache; + + public async Task GetStream(MediaStreamInfo video, MediaStreamInfo audio) + { + audioCache = await roaming.CreateFileAsync("audioCache.mp4", CreationCollisionOption.ReplaceExisting); + videoCache = await roaming.CreateFileAsync("videoCache.mp4", CreationCollisionOption.ReplaceExisting); + + + + composition.BackgroundAudioTracks.Add(await BackgroundAudioTrack.CreateFromFileAsync(audioCache)); + composition.Clips.Add(await MediaClip.CreateFromFileAsync(videoCache)); + + return composition.GenerateMediaStreamSource(); + } + } +} diff --git a/FoxTube/Controls/Player/VideoPlayer.xaml b/FoxTube/Controls/Player/VideoPlayer.xaml index 385ae45..70b1de7 100644 --- a/FoxTube/Controls/Player/VideoPlayer.xaml +++ b/FoxTube/Controls/Player/VideoPlayer.xaml @@ -11,7 +11,7 @@ RequestedTheme="Dark" PointerMoved="UserControl_PointerMoved"> - + - diff --git a/FoxTube/Controls/Player/VideoPlayer.xaml.cs b/FoxTube/Controls/Player/VideoPlayer.xaml.cs index b869bff..47bb6b0 100644 --- a/FoxTube/Controls/Player/VideoPlayer.xaml.cs +++ b/FoxTube/Controls/Player/VideoPlayer.xaml.cs @@ -11,6 +11,7 @@ using YoutubeExplode.Models.MediaStreams; using YoutubeExplode; using System.Diagnostics; using Windows.Foundation; +using FoxTube.Classes; namespace FoxTube { @@ -18,16 +19,19 @@ namespace FoxTube { public Video item; public string avatar; + public HistoryItem history; + bool incognito = false; public event Event NextClicked; public event ObjectEventHandler MiniMode; public PlayerControls Controls => videoSource.TransportControls as PlayerControls; - public TimeSpan Position => videoSource.Position; + public TimeSpan Position + { + get { return videoSource.Position; } + set { videoSource.Position = value; } + } - bool audioLoaded = false; - bool videoLoaded = false; - bool isMuxed = false; - DispatcherTimer muxedTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) }; + VideoProcessor processor = new VideoProcessor(); DispatcherTimer cursorTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) }; @@ -43,8 +47,13 @@ namespace FoxTube public async void Initialize(Video meta, string channelAvatar, bool privateMode = false) { + incognito = privateMode; item = meta; avatar = channelAvatar; + history = new HistoryItem + { + Id = item.Id + }; videoSource.PosterSource = new BitmapImage((meta.Snippet.Thumbnails.Maxres ?? meta.Snippet.Thumbnails.Medium).Url.ToUri()); Controls.SetMeta(meta.Snippet.Localized.Title, meta.Snippet.ChannelTitle); @@ -54,6 +63,10 @@ namespace FoxTube InitializeContols(); if (Methods.GetDuration(item.ContentDetails.Duration).TotalMinutes > 5) videoSource.Markers.Add(new Windows.UI.Xaml.Media.TimelineMarker { Time = Methods.GetDuration(item.ContentDetails.Duration) - TimeSpan.FromMinutes(1) }); + if(Methods.GetDuration(item.ContentDetails.Duration).TotalMinutes >= 60) + for (int k = 1; k < Methods.GetDuration(item.ContentDetails.Duration).TotalMinutes / 30; k++) + videoSource.Markers.Add(new Windows.UI.Xaml.Media.TimelineMarker { Time = TimeSpan.FromMinutes(k * 30) }); + Controls.SetQualities(await new YoutubeClient().GetVideoMediaStreamInfosAsync(item.Id)); Controls.SetCaptions(await new YoutubeClient().GetVideoClosedCaptionTrackInfosAsync(item.Id)); } @@ -70,8 +83,7 @@ namespace FoxTube videoSource.AreTransportControlsEnabled = false; if (!privateMode) - Debug.WriteLine("TODO: history entry creation"); - // TODO: Create history entry + HistorySet.Update(history); Visibility = Visibility.Visible; } @@ -85,12 +97,6 @@ namespace FoxTube { videoSource.Volume = SettingsStorage.Volume; - muxedTimer.Tick += (s, e) => - { - if (!Enumerable.Range(-100, 100).Contains((int)(videoSource.Position - audioSource.Position).TotalMilliseconds)) - audioSource.Position = videoSource.Position; - }; - cursorTimer.Tick += (s, e) => { Point cursorPoint = CoreWindow.GetForCurrentThread().PointerPosition; @@ -98,8 +104,8 @@ namespace FoxTube cursorPoint.Y -= Window.Current.Bounds.Y; Rect playerBounds = TransformToVisual(Methods.MainPage).TransformBounds(new Rect(0, 0, ActualWidth, ActualHeight)); - if(cursorPoint.Y > playerBounds.Top && cursorPoint.Y < playerBounds.Bottom && - cursorPoint.X > playerBounds.Left && cursorPoint.X < playerBounds.Right) + if((cursorPoint.Y > playerBounds.Top && cursorPoint.Y < playerBounds.Bottom && + cursorPoint.X > playerBounds.Left && cursorPoint.X < playerBounds.Right) || videoSource.IsFullWindow) CoreWindow.GetForCurrentThread().PointerCursor = null; cursorTimer.Stop(); }; @@ -108,7 +114,6 @@ namespace FoxTube Controls.NextRequested += (s, e) => NextClicked?.Invoke(); Controls.QualityChanged += Controls_QualityChanged; Controls.MiniModeChanged += Controls_MiniModeChanged; - Controls.MuteClicked += Controls_MuteClicked; Controls.Player = videoSource; #region System Media Transport Controls @@ -128,12 +133,6 @@ namespace FoxTube #endregion } - private void Controls_MuteClicked() - { - if (audioSource != null) - audioSource.IsMuted = videoSource.IsMuted; - } - public void Controls_MiniModeChanged(object sender, bool e) { videoSource.IsFullWindow = false; @@ -146,33 +145,17 @@ namespace FoxTube Controls.Minimize(); } - private void Controls_QualityChanged(object sender, MediaStreamInfo requestedQuality, MediaStreamInfoSet list) + private async void Controls_QualityChanged(object sender, MediaStreamInfo requestedQuality, MediaStreamInfoSet list) { - /*new MediaStreamSourceSampleRequest() - new MediaStreamSource() - - MediaSource. - new MediaPlaybackItem(null).VideoTracks[0] - IMediaPlaybackSource - videoSource.Sou*/ - videoSource.Pause(); timecodeBackup = videoSource.Position; needUpdateTimecode = true; - audioLoaded = false; - videoLoaded = false; - muxedTimer.Stop(); - - videoSource.Source = requestedQuality.Url.ToUri(); - if(requestedQuality is MuxedStreamInfo) - { - isMuxed = true; - audioSource.Source = null; - } + if (requestedQuality is MuxedStreamInfo) + videoSource.Source = requestedQuality.Url.ToUri(); else - audioSource.Source = list.Audio.First().Url.ToUri(); + videoSource.SetMediaStreamSource(await processor.GetStream(requestedQuality, list.Audio.First())); } public void Controls_CloseRequested(object sender, RoutedEventArgs e) @@ -180,8 +163,13 @@ namespace FoxTube if(systemControls != null) systemControls.IsEnabled = false; + if (!incognito) + { + history.LeftOn = videoSource.Position; + HistorySet.Update(history); + } + videoSource.Stop(); - audioSource.Stop(); Methods.MainPage.CloseVideo(); } @@ -190,9 +178,6 @@ namespace FoxTube { await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { - if (args.Button == SystemMediaTransportControlsButton.Next) - NextClicked?.Invoke(); - switch (args.Button) { case SystemMediaTransportControlsButton.Play: @@ -202,7 +187,7 @@ namespace FoxTube videoSource.Pause(); break; case SystemMediaTransportControlsButton.Next: - NextClicked.Invoke(); + NextClicked?.Invoke(); break; } }); @@ -220,48 +205,29 @@ namespace FoxTube case Windows.UI.Xaml.Media.MediaElementState.Buffering: case Windows.UI.Xaml.Media.MediaElementState.Paused: systemControls.PlaybackStatus = MediaPlaybackStatus.Paused; - // TODO: Create history entry - audioSource?.Pause(); + if(!incognito) + { + history.LeftOn = videoSource.Position; + HistorySet.Update(history); + } break; case Windows.UI.Xaml.Media.MediaElementState.Playing: systemControls.PlaybackStatus = MediaPlaybackStatus.Playing; - audioSource?.Play(); break; } } - private void AudioSource_CurrentStateChanged(object sender, RoutedEventArgs e) - { - if(audioSource.CurrentState == Windows.UI.Xaml.Media.MediaElementState.Playing) - muxedTimer.Start(); - else - muxedTimer.Stop(); - } - private void VideoSource_MediaOpened(object sender, RoutedEventArgs e) { - videoLoaded = true; - if (needUpdateTimecode) - { - videoSource.Position = timecodeBackup; - needUpdateTimecode = false; - } - if (audioLoaded || isMuxed) - videoSource.Play(); - } + if (!needUpdateTimecode) + return; - private void AudioSource_MediaOpened(object sender, RoutedEventArgs e) - { - audioLoaded = true; - if (needUpdateTimecode) - audioSource.Position = timecodeBackup; - if (videoLoaded) - videoSource.Play(); + videoSource.Position = timecodeBackup; + needUpdateTimecode = false; } private void VideoSource_VolumeChanged(object sender, RoutedEventArgs e) { - audioSource.Volume = videoSource.Volume; SettingsStorage.Volume = videoSource.Volume; } diff --git a/FoxTube/Controls/VideoCard.xaml b/FoxTube/Controls/VideoCard.xaml index 1794833..0266720 100644 --- a/FoxTube/Controls/VideoCard.xaml +++ b/FoxTube/Controls/VideoCard.xaml @@ -72,7 +72,7 @@ - + diff --git a/FoxTube/Controls/VideoCard.xaml.cs b/FoxTube/Controls/VideoCard.xaml.cs index 41b23c8..586aee2 100644 --- a/FoxTube/Controls/VideoCard.xaml.cs +++ b/FoxTube/Controls/VideoCard.xaml.cs @@ -27,6 +27,7 @@ namespace FoxTube.Controls public string playlistId; public string videoId; Video item; + HistoryItem history; public VideoCard(string id, string playlist = null) { @@ -66,7 +67,7 @@ namespace FoxTube.Controls return; } - if (item.Snippet.LiveBroadcastContent == "live") + if (item.Snippet.LiveBroadcastContent == "live") { views.Text = $"{item.LiveStreamingDetails.ConcurrentViewers:0,0} {resources.GetString("/Cards/viewers")}"; if (item.LiveStreamingDetails.ScheduledStartTime.HasValue && item.LiveStreamingDetails.ScheduledEndTime.HasValue) @@ -103,8 +104,13 @@ namespace FoxTube.Controls try { avatar.ProfilePicture = new BitmapImage((await new YoutubeClient().GetChannelAsync(item.Snippet.ChannelId)).LogoUrl.ToUri()) { DecodePixelWidth = 46, DecodePixelHeight = 46 }; } catch { } - if(SecretsVault.History.Contains(videoId)) + if (SecretsVault.History.Contains(videoId)) watched.Visibility = Visibility.Visible; + if (HistorySet.Items.Exists(i => i.Id == item.Id)) + { + watched.Visibility = Visibility.Visible; + leftOn.Value = 100 * HistorySet.Items.Find(i => i.Id == item.Id).LeftOn.TotalSeconds / Methods.GetDuration(item.ContentDetails.Duration).TotalSeconds; + } show.Begin(); } @@ -196,6 +202,8 @@ namespace FoxTube.Controls if (SecretsVault.History.Contains(videoId)) watched.Visibility = Visibility.Visible; + if (HistorySet.Items.Exists(i => i.Id == item.Id)) + leftOn.Value = 100 * HistorySet.Items.Find(i => i.Id == item.Id).LeftOn.TotalSeconds / Methods.GetDuration(item.ContentDetails.Duration).TotalSeconds; show.Begin(); } @@ -234,7 +242,7 @@ namespace FoxTube.Controls } } - Methods.MainPage.GoToVideo(videoId, playlistId); + Methods.MainPage.GoToVideo(videoId, playlistId == "HL" ? null : playlistId, ((FrameworkElement)sender).Name == "incognito" ? true : false); } private void Share(DataTransferManager sender, DataRequestedEventArgs args) @@ -358,9 +366,35 @@ namespace FoxTube.Controls Item_Click(menuItem, null); } - private void Wl_Click(object sender, RoutedEventArgs e) + private async void Wl_Click(object sender, RoutedEventArgs e) { + if (wl.IsChecked) + { + try + { + PlaylistItem playlist = new PlaylistItem + { + Snippet = new PlaylistItemSnippet + { + PlaylistId = "WL", + ResourceId = new ResourceId + { + VideoId = item.Id, + Kind = "youtube#video" + } + } + }; + PlaylistItemsResource.InsertRequest request = SecretsVault.Service.PlaylistItems.Insert(playlist, "snippet"); + await request.ExecuteAsync(); + } + catch + { + wl.IsChecked = false; + } + } + else + wl.IsChecked = true; } async void LoadAddTo() @@ -449,6 +483,22 @@ namespace FoxTube.Controls private async void Delete_Click(object sender, RoutedEventArgs e) { + if (playlistId == "WL") + { + await Launcher.LaunchUriAsync("https://youtube.com/playlist?list=WL".ToUri()); + return; + } + else if(playlistId == "HL") + { + if (history == null) + await Launcher.LaunchUriAsync("https://youtube.com/feed/history".ToUri()); + else + { + HistorySet.Delete(history); + (Methods.MainPage.PageContent as History).Delete(this); + } + return; + } try { PlaylistItemsResource.ListRequest request = SecretsVault.Service.PlaylistItems.List("snippet"); diff --git a/FoxTube/FoxTube.csproj b/FoxTube/FoxTube.csproj index fb86d33..71a18b3 100644 --- a/FoxTube/FoxTube.csproj +++ b/FoxTube/FoxTube.csproj @@ -107,6 +107,7 @@ + Advert.xaml diff --git a/FoxTube/Pages/History.xaml b/FoxTube/Pages/History.xaml index 9da4a4a..da7b620 100644 --- a/FoxTube/Pages/History.xaml +++ b/FoxTube/Pages/History.xaml @@ -15,16 +15,57 @@ - + + + + + + + + + + + + + + + + + + +