using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Gallery.Resources; using Gallery.UI; using Gallery.UI.Theme; using Gallery.Utils; using Xamarin.Essentials; using Xamarin.Forms; namespace Gallery.Illust { [QueryProperty("IllustId", "id")] public partial class ViewIllustPage : AdaptedPage { #region - Properties - public static readonly BindableProperty FavoriteIconProperty = BindableProperty.Create( nameof(FavoriteIcon), typeof(ImageSource), typeof(ViewIllustPage)); public ImageSource FavoriteIcon { get => (ImageSource)GetValue(FavoriteIconProperty); set => SetValue(FavoriteIconProperty, value); } public static readonly BindableProperty IllustsProperty = BindableProperty.Create( nameof(Illusts), typeof(IllustDetailItem[]), typeof(ViewIllustPage)); public static readonly BindableProperty IsPageVisibleProperty = BindableProperty.Create( nameof(IsPageVisible), typeof(bool), typeof(ViewIllustPage)); public static readonly BindableProperty PagePositionTextProperty = BindableProperty.Create( nameof(PagePositionText), typeof(string), typeof(ViewIllustPage)); public static readonly BindableProperty CurrentPageProperty = BindableProperty.Create( nameof(CurrentPage), typeof(int), typeof(ViewIllustPage), propertyChanged: OnCurrentPagePropertyChanged); public static readonly BindableProperty IsScrollAnimatedProperty = BindableProperty.Create( nameof(IsScrollAnimated), typeof(bool), typeof(ViewIllustPage), true); public IllustDetailItem[] Illusts { get => (IllustDetailItem[])GetValue(IllustsProperty); set => SetValue(IllustsProperty, value); } public bool IsPageVisible { get => (bool)GetValue(IsPageVisibleProperty); set => SetValue(IsPageVisibleProperty, value); } public string PagePositionText { get => (string)GetValue(PagePositionTextProperty); set => SetValue(PagePositionTextProperty, value); } public int CurrentPage { get => (int)GetValue(CurrentPageProperty); set => SetValue(CurrentPageProperty, value); } public bool IsScrollAnimated { get => (bool)GetValue(IsScrollAnimatedProperty); set => SetValue(IsScrollAnimatedProperty, value); } private static void OnCurrentPagePropertyChanged(BindableObject obj, object old, object @new) { var page = (ViewIllustPage)obj; var index = (int)@new; var items = page.Illusts; var length = items.Length; page.PagePositionText = $"{index + 1}/{length}"; } public static readonly BindableProperty AnimeStatusProperty = BindableProperty.Create( nameof(AnimeStatus), typeof(string), typeof(ViewIllustPage), StyleDefinition.IconPlay); public static readonly BindableProperty IsAnimateSliderVisibleProperty = BindableProperty.Create( nameof(IsAnimateSliderVisible), typeof(bool), typeof(ViewIllustPage)); public static readonly BindableProperty IsAnimateSliderEnabledProperty = BindableProperty.Create( nameof(IsAnimateSliderEnabled), typeof(bool), typeof(ViewIllustPage)); public static readonly BindableProperty CurrentAnimeFrameProperty = BindableProperty.Create( nameof(CurrentAnimeFrame), typeof(double), typeof(ViewIllustPage), propertyChanged: OnCurrentAnimeFramePropertyChanged); public static readonly BindableProperty MaximumFrameProperty = BindableProperty.Create( nameof(MaximumFrame), typeof(double), typeof(ViewIllustPage), 1.0); private static void OnCurrentAnimeFramePropertyChanged(BindableObject obj, object old, object @new) { var page = (ViewIllustPage)obj; if (page.ugoira != null && page.IsAnimateSliderEnabled) { var frame = (double)@new; page.ugoira.ToggleFrame((int)frame); } } public string AnimeStatus { get => (string)GetValue(AnimeStatusProperty); set => SetValue(AnimeStatusProperty, value); } public bool IsAnimateSliderVisible { get => (bool)GetValue(IsAnimateSliderVisibleProperty); set => SetValue(IsAnimateSliderVisibleProperty, value); } public bool IsAnimateSliderEnabled { get => (bool)GetValue(IsAnimateSliderEnabledProperty); set => SetValue(IsAnimateSliderEnabledProperty, value); } public double CurrentAnimeFrame { get => (double)GetValue(CurrentAnimeFrameProperty); set => SetValue(CurrentAnimeFrameProperty, value); } public double MaximumFrame { get => (double)GetValue(MaximumFrameProperty); set => SetValue(MaximumFrameProperty, value); } public static readonly BindableProperty ProgressVisibleProperty = BindableProperty.Create( nameof(ProgressVisible), typeof(bool), typeof(ViewIllustPage)); public bool ProgressVisible { get => (bool)GetValue(ProgressVisibleProperty); set => SetValue(ProgressVisibleProperty, value); } #endregion public IllustItem IllustItem { get; private set; } private readonly ImageSource fontIconLove; private readonly ImageSource fontIconNotLove; private readonly ImageSource fontIconCircleLove; private bool favoriteChanged; private IllustUgoiraData ugoiraData; private Ugoira ugoira; private ParallelTask task; private readonly object sync = new object(); private int downloaded = 0; private int pageCount; private bool isPreloading; public ViewIllustPage(IllustItem illust) { IllustItem = illust; fontIconLove = (ImageSource)Application.Current.Resources[ThemeBase.FontIconLove]; fontIconNotLove = (ImageSource)Application.Current.Resources[ThemeBase.FontIconNotLove]; fontIconCircleLove = (ImageSource)Application.Current.Resources[ThemeBase.FontIconCircleLove]; if (illust != null) { pageCount = illust.PageCount; RefreshInformation(illust, pageCount); } Resources.Add("carouselView", GetCarouseTemplate()); BindingContext = this; InitializeComponent(); if (illust != null) { LoadIllust(illust); } } protected override void OnAppearing() { base.OnAppearing(); Screen.SetHomeIndicatorAutoHidden(Shell.Current, true); } protected override void OnDisappearing() { Screen.SetHomeIndicatorAutoHidden(Shell.Current, false); base.OnDisappearing(); if (ugoira != null) { IllustItem.IsPlaying = false; ugoira.FrameChanged -= OnUgoiraFrameChanged; ugoira.TogglePlay(false); ugoira.Dispose(); ugoira = null; } if (favoriteChanged) { Stores.SaveFavoritesIllusts(); } } public override void OnUnload() { if (task != null) { task.Dispose(); task = null; } } #if __IOS__ protected override void OnPageTopMarginChanged(Thickness old, Thickness @new) { var illusts = Illusts; if (illusts != null) { for (var i = 0; i < illusts.Length; i++) { illusts[i].TopPadding = @new; } } } #endif private DataTemplate GetCarouseTemplate() { return new DataTemplate(() => { // image var image = new Image { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, Aspect = Aspect.AspectFit } .Binding(Image.SourceProperty, nameof(IllustDetailItem.Image)); // downloading var downloading = new Frame { HasShadow = false, Margin = default, Padding = new Thickness(20), CornerRadius = 8, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, Content = new ActivityIndicator() .Binding(IsVisibleProperty, nameof(IllustDetailItem.Loading)) .Binding(ActivityIndicator.IsRunningProperty, nameof(IllustDetailItem.Loading)) .DynamicResource(ActivityIndicator.ColorProperty, ThemeBase.WindowColor) } .Binding(IsVisibleProperty, nameof(IllustDetailItem.Loading)) .DynamicResource(BackgroundColorProperty, ThemeBase.MaskColor); // loading original var original = new ActivityIndicator { Margin = new Thickness(10), HorizontalOptions = LayoutOptions.Start, VerticalOptions = LayoutOptions.Start } .Binding(IsVisibleProperty, nameof(IllustDetailItem.Downloading)) .Binding(ActivityIndicator.IsRunningProperty, nameof(IllustDetailItem.Downloading)) .DynamicResource(ActivityIndicator.ColorProperty, ThemeBase.TextColor); var tap = new TapGestureRecognizer(); tap.Tapped += Image_Tapped; var tapPrevious = new TapGestureRecognizer(); tapPrevious.Tapped += TapPrevious_Tapped; var tapNext = new TapGestureRecognizer(); tapNext.Tapped += TapNext_Tapped; var grid = new Grid { Children = { // image image, // tap holder new Grid { RowDefinitions = { new RowDefinition { Height = new GridLength(.3, GridUnitType.Star) }, new RowDefinition { Height = new GridLength(.4, GridUnitType.Star) }, new RowDefinition { Height = new GridLength(.3, GridUnitType.Star) } }, Children = { new Label { GestureRecognizers = { tapPrevious } }, new Label { GestureRecognizers = { tap } }.GridRow(1), new Label { GestureRecognizers = { tapNext } }.GridRow(2) } }, // downloading downloading, // loading original original } }; #if __IOS__ grid.SetBinding(Xamarin.Forms.Layout.PaddingProperty, nameof(IllustDetailItem.TopPadding)); #endif return grid; }); } private void LoadIllust(IllustItem illust) { if (illust == null) { return; } var items = new IllustDetailItem[illust.PageCount]; #if __IOS__ var topMargin = PageTopMargin; #endif for (var i = 0; i < items.Length; i++) { items[i] = new IllustDetailItem { #if __IOS__ TopPadding = topMargin, #endif Id = illust.Id }; if (i == 0) { items[i].Loading = true; items[i].Image = illust.Image; } } Illusts = items; Task.Run(() => DoLoadImages()); } private void DoLoadImages(bool force = false) { var illustItem = IllustItem; var pages = Stores.LoadIllustPageData(illustItem.Id, out string error, force); if (pages == null) { App.DebugError("illustPage.load", $"failed to load illust page data, id: {illustItem.Id}"); if (error != null) { MainThread.BeginInvokeOnMainThread(() => { DisplayAlert(ResourceHelper.Title, error, ResourceHelper.Ok); }); } return; } var items = Illusts; var reload = false; if (pages.body.Length > items.Length) { #if DEBUG App.DebugPrint($"local page count ({items.Length}) is not equals the remote one ({pages.body.Length})"); #endif var tmp = new IllustDetailItem[pages.body.Length]; items.CopyTo(items, 0); #if __IOS__ var topMargin = PageTopMargin; #endif for (var i = items.Length; i < tmp.Length; i++) { tmp[i] = new IllustDetailItem { Id = illustItem.Id, #if __IOS__ TopPadding = topMargin, #endif Loading = i == 0 }; } Illusts = items = tmp; reload = true; } for (var i = 0; i < items.Length; i++) { var item = items[i]; var p = pages.body[i]; item.PreviewUrl = p.urls.regular; item.OriginalUrl = p.urls.original; if (i == 0 && illustItem.ImageUrl == null) { // maybe open from a link reload = true; } } if (reload) { DoForcePreload(false); } if (task != null) { task.Dispose(); task = null; } task = ParallelTask.Start("view.illusts", 0, items.Length, 2, i => { DoLoadImage(i); return true; }); if (illustItem.IsAnimeVisible) { // anime ugoiraData = Stores.LoadIllustUgoiraData(illustItem.Id); if (ugoiraData != null && ugoiraData.body != null) { var length = ugoiraData.body.frames.Length; MaximumFrame = length > 0 ? length : 1; } } } private void DoLoadImage(int index, bool force = false) { var items = Illusts; if (index < 0 || (!force && index >= items.Length)) { App.DebugPrint($"invalid index: {index}"); return; } var item = items[index]; if (index > 0 && !force) { if (item.Loading || item.Image != null) { #if DEBUG App.DebugPrint($"skipped, loading or already loaded, index: {index}, loading: {item.Loading}"); #endif return; } } item.Loading = true; var image = Stores.LoadPreviewImage(item.PreviewUrl, true, IllustItem.Id, force: force); if (image != null) { item.Image = image; if(index == 0) { IllustItem.Image = image; } } item.Loading = false; RefreshProgress(); } private void RefreshProgress() { if (pageCount <= 1) { return; } lock (sync) { downloaded++; } if (downloaded >= pageCount) { MainThread.BeginInvokeOnMainThread(async () => { ViewExtensions.CancelAnimations(progress); await progress.ProgressTo(1, 250, Easing.CubicIn); await progress.FadeTo(0, easing: Easing.CubicIn); ProgressVisible = false; }); } else { var val = downloaded / (float)pageCount; MainThread.BeginInvokeOnMainThread(() => { ViewExtensions.CancelAnimations(progress); progress.ProgressTo(val, 250, Easing.CubicIn); }); } } private void TapPrevious_Tapped(object sender, EventArgs e) { var index = CurrentPage; if (index > 0) { IsScrollAnimated = false; CurrentPage = index - 1; IsScrollAnimated = true; } } private void TapNext_Tapped(object sender, EventArgs e) { var index = CurrentPage; var illusts = Illusts; if (illusts != null && index < illusts.Length - 1) { IsScrollAnimated = false; CurrentPage = index + 1; IsScrollAnimated = true; } } private async void Favorite_Clicked(object sender, EventArgs e) { var favorites = Stores.Favorites; var illust = IllustItem; var index = favorites.FindIndex(i => i.Id == illust.Id); var bookmarkId = illust.BookmarkId; bool add = index < 0 && bookmarkId == null; if (add) { illust.IsFavorite = true; illust.FavoriteDateUtc = DateTime.UtcNow; favorites.Insert(0, illust); FavoriteIcon = fontIconLove; } else { if (index >= 0) { var item = favorites[index]; if (bookmarkId == null && item.BookmarkId != null) { bookmarkId = item.BookmarkId; illust.BookmarkId = bookmarkId; } favorites.RemoveAt(index); } illust.IsFavorite = false; FavoriteIcon = bookmarkId == null ? fontIconNotLove : fontIconCircleLove; } favoriteChanged = true; if (Configs.SyncFavType == SyncType.None) { return; } if (Configs.Cookie == null) { return; } if (!add && string.IsNullOrEmpty(bookmarkId)) { return; } if (Configs.SyncFavType == SyncType.Prompt) { var ok = await DisplayAlert( ResourceHelper.Title, ResourceHelper.ConfirmSyncFavorite, ResourceHelper.Yes, ResourceHelper.No); if (!ok) { return; } } if (add) { var id = await Task.Run(() => Stores.AddBookmark(illust.Id)); if (id != null) { bookmarkId = id; illust.BookmarkId = bookmarkId; FavoriteIcon = fontIconCircleLove; } } else { var result = await Task.Run(() => Stores.DeleteBookmark(bookmarkId)); if (result) { FavoriteIcon = fontIconNotLove; } } // immediately save after changing remote Stores.SaveFavoritesIllusts(); favoriteChanged = false; } private void Image_Tapped(object sender, EventArgs e) { if (ugoiraData == null) { return; } var illustItem = IllustItem; if (ugoira != null) { var playing = !ugoira.IsPlaying; AnimeStatus = playing ? StyleDefinition.IconPause : StyleDefinition.IconPlay; IsAnimateSliderEnabled = !playing; ugoira.TogglePlay(playing); illustItem.IsPlaying = playing; } else if (((VisualElement)sender).BindingContext is IllustDetailItem item) { if (illustItem.IsPlaying || !illustItem.IsAnimeVisible) { return; } ugoira = new Ugoira(ugoiraData, item); ugoira.FrameChanged += OnUgoiraFrameChanged; AnimeStatus = StyleDefinition.IconPause; illustItem.IsPlaying = true; ugoira.TogglePlay(true); } } private void OnUgoiraFrameChanged(object sender, UgoiraEventArgs e) { e.DetailItem.Image = e.Image; CurrentAnimeFrame = e.FrameIndex; } private void RefreshInformation(IllustItem item, int count) { Title = item.Title; FavoriteIcon = item.BookmarkId != null ? fontIconCircleLove : Stores.Favorites.Any(i => i.Id == item.Id) ? fontIconLove : fontIconNotLove; IsAnimateSliderVisible = item.IsAnimeVisible; if (count > 1) { IsPageVisible = true; ProgressVisible = true; PagePositionText = $"{CurrentPage + 1}/{count}"; } } private void DoForcePreload(bool force) { isPreloading = true; var illustItem = IllustItem; if (force) { var illusts = Illusts; var currentPage = CurrentPage; if (currentPage >= 0 && illusts != null && currentPage < illusts.Length) { illusts[currentPage].Loading = true; } } // force to reload var preload = Stores.LoadIllustPreloadData(illustItem.Id, true, force: force); if (preload != null && preload.illust.TryGetValue(illustItem.Id, out var illust)) { illust.CopyToItem(illustItem); pageCount = illustItem.PageCount; MainThread.BeginInvokeOnMainThread(() => { RefreshInformation(illustItem, pageCount); }); if (preload.user.TryGetValue(illust.userId, out var user)) { illustItem.ProfileUrl = user.image; } } isPreloading = false; } private void Refresh_Clicked(object sender, EventArgs e) { if (isPreloading) { return; } Task.Run(() => { DoForcePreload(true); DoLoadImage(CurrentPage, true); }); } private async void More_Clicked(object sender, EventArgs e) { int p = CurrentPage; var illusts = Illusts; if (illusts == null || p < 0 || p >= illusts.Length) { return; } var item = illusts[p]; List extras = new List(); var share = ResourceHelper.Share; var preview = Stores.GetPreviewImagePath(item.PreviewUrl); if (preview != null) { extras.Add(share); } var userDetail = ResourceHelper.UserDetail; extras.Add(userDetail); var related = ResourceHelper.RelatedIllusts; extras.Add(related); #if __IOS__ var exportVideo = ResourceHelper.ExportVideo; if (IsAnimateSliderVisible) { extras.Add(exportVideo); } #endif var saveOriginal = ResourceHelper.SaveOriginal; var illustItem = IllustItem; var result = await DisplayActionSheet( $"{illustItem.Title} (id: {illustItem.Id})", ResourceHelper.Cancel, saveOriginal, extras.ToArray()); if (result == saveOriginal) { SaveOriginalImage(item); } else if (result == share) { await Share.RequestAsync(new ShareFileRequest { Title = illustItem.Title, File = new ShareFile(preview) }); } else if (result == userDetail) { var page = new UserIllustPage(illustItem); await Navigation.PushAsync(page); } else if (result == related) { var page = new RelatedIllustsPage(illustItem); await Navigation.PushAsync(page); } #if __IOS__ else if (result == exportVideo) { ExportVideo(); } #endif } #if __IOS__ private async void ExportVideo() { string msg = ResourceHelper.CantExportVideo; if (ugoira != null && ugoiraData != null && ugoiraData.body != null) { if (Stores.CheckUgoiraVideo(ugoiraData.body.originalSrc)) { var flag = await DisplayAlert(ResourceHelper.Operation, ResourceHelper.AlreadySavedVideo, ResourceHelper.Yes, ResourceHelper.No); if (!flag) { return; } } var status = await Permissions.CheckStatusAsync(); if (status != PermissionStatus.Granted) { status = await Permissions.RequestAsync(); if (status != PermissionStatus.Granted) { App.DebugPrint("access denied to gallery."); return; } } var success = await Task.Run(ugoira.ExportVideo); // ugoira.ExportGif if (success != null) { #if DEBUG msg = ResourceHelper.ExportSuccess; #else var result = await FileStore.SaveVideoToGalleryAsync(success); msg = result ?? ResourceHelper.ExportSuccess; #endif } } await DisplayAlert(ResourceHelper.Title, msg, ResourceHelper.Ok); } #endif private async void SaveOriginalImage(IllustDetailItem item) { if (Stores.CheckIllustImage(item.OriginalUrl)) { var flag = await DisplayAlert(ResourceHelper.Operation, ResourceHelper.AlreadySavedQuestion, ResourceHelper.Yes, ResourceHelper.No); if (!flag) { return; } } var status = await Permissions.CheckStatusAsync(); if (status != PermissionStatus.Granted) { status = await Permissions.RequestAsync(); if (status != PermissionStatus.Granted) { App.DebugPrint("access denied to gallery."); return; } } if (item == null || item.Downloading) { return; } item.Downloading = true; _ = Task.Run(() => DoLoadOriginalImage(item)); } private void DoLoadOriginalImage(IllustDetailItem item) { var image = Stores.LoadIllustImage(item.OriginalUrl); if (image != null) { MainThread.BeginInvokeOnMainThread(async () => { var result = await FileStore.SaveImageToGalleryAsync(image); string message = result ?? ResourceHelper.SaveSuccess; await DisplayAlert(ResourceHelper.Title, message, ResourceHelper.Ok); }); } item.Downloading = false; } } public class IllustDetailItem : BindableObject { public static readonly BindableProperty ImageProperty = BindableProperty.Create( nameof(Image), typeof(ImageSource), typeof(IllustDetailItem)); public static readonly BindableProperty LoadingProperty = BindableProperty.Create( nameof(Loading), typeof(bool), typeof(IllustDetailItem)); public static readonly BindableProperty DownloadingProperty = BindableProperty.Create( nameof(Downloading), typeof(bool), typeof(IllustDetailItem)); #if __IOS__ public static readonly BindableProperty TopPaddingProperty = BindableProperty.Create( nameof(TopPadding), typeof(Thickness), typeof(IllustDetailItem)); public Thickness TopPadding { get => (Thickness)GetValue(TopPaddingProperty); set => SetValue(TopPaddingProperty, value); } #endif public ImageSource Image { get => (ImageSource)GetValue(ImageProperty); set => SetValue(ImageProperty, value); } public bool Loading { get => (bool)GetValue(LoadingProperty); set => SetValue(LoadingProperty, value); } public bool Downloading { get => (bool)GetValue(DownloadingProperty); set => SetValue(DownloadingProperty, value); } public string Id { get; set; } public string PreviewUrl { get; set; } public string OriginalUrl { get; set; } } }