Pixiview/Pixiview/Illust/ViewIllustPage.xaml.cs

762 lines
27 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Pixiview.Resources;
using Pixiview.UI;
using Pixiview.UI.Theme;
using Pixiview.Utils;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Pixiview.Illust
{
[QueryProperty("IllustId", "id")]
public partial class ViewIllustPage : AdaptedPage
{
public static readonly BindableProperty FavoriteIconProperty = BindableProperty.Create(
nameof(FavoriteIcon), typeof(ImageSource), typeof(ViewIllustPage));
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 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);
public static readonly BindableProperty ProgressVisibleProperty = BindableProperty.Create(
nameof(ProgressVisible), typeof(bool), typeof(ViewIllustPage));
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}";
}
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 ImageSource FavoriteIcon
{
get => (ImageSource)GetValue(FavoriteIconProperty);
set => SetValue(FavoriteIconProperty, value);
}
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 string AnimeStatus
{
get => (string)GetValue(AnimeStatusProperty);
set => SetValue(AnimeStatusProperty, value);
}
public bool IsScrollAnimated
{
get => (bool)GetValue(IsScrollAnimatedProperty);
set => SetValue(IsScrollAnimatedProperty, 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 bool ProgressVisible
{
get => (bool)GetValue(ProgressVisibleProperty);
set => SetValue(ProgressVisibleProperty, value);
}
public IllustItem IllustItem { get; private set; }
private readonly bool saveFavorites;
private readonly ImageSource fontIconLove;
private readonly ImageSource fontIconNotLove;
private IllustUgoiraData ugoiraData;
private Ugoira ugoira;
private readonly object sync = new object();
private int downloaded = 0;
private int pageCount;
public ViewIllustPage(IllustItem illust, bool save)
{
IllustItem = illust;
Title = illust.Title;
saveFavorites = save;
BindingContext = this;
fontIconLove = (ImageSource)Application.Current.Resources[ThemeBase.FontIconLove];
fontIconNotLove = (ImageSource)Application.Current.Resources[ThemeBase.FontIconNotLove];
FavoriteIcon = Stores.Favorites.Any(i => i.Id == illust.Id)
? fontIconLove
: fontIconNotLove;
if (illust != null)
{
pageCount = illust.PageCount;
ProgressVisible = IsPageVisible = pageCount > 1;
}
Resources.Add("carouselView", GetCarouseTemplate());
InitializeComponent();
if (illust != null)
{
IsAnimateSliderVisible = illust.IsAnimeVisible;
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 (saveFavorites)
{
Stores.SaveFavoritesIllusts();
}
}
#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 (items.Length > 1)
{
PagePositionText = $"1/{items.Length}";
}
#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()
{
var illustItem = IllustItem;
var pages = Stores.LoadIllustPageData(illustItem.Id);
if (pages == null)
{
App.DebugError("illustPage.load", $"failed to load illust page data, id: {illustItem.Id}");
return;
}
var items = Illusts;
if (pages.body.Length > items.Length)
{
App.DebugPrint($"local page count ({items.Length}) is not equals the remote one ({pages.body.Length})");
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;
}
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
var preload = Stores.LoadIllustPreloadData(illustItem.Id);
if (preload != null && preload.illust.TryGetValue(illustItem.Id, out var illust))
{
illust.CopyToItem(illustItem);
MainThread.BeginInvokeOnMainThread(() =>
{
var count = illustItem.PageCount;
pageCount = count;
Title = illustItem.Title;
IsPageVisible = count > 1;
IsAnimateSliderVisible = illustItem.IsAnimeVisible;
ProgressVisible = count > 1;
});
if (preload.user.TryGetValue(illust.userId, out var user))
{
illustItem.ProfileUrl = user.image;
}
}
}
}
ParallelTask.Start(0, items.Length, Configs.MaxPageThreads, 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 || index >= items.Length)
{
App.DebugPrint($"invalid index: {index}");
return;
}
var item = items[index];
if (index > 0 && !force)
{
if (item.Loading || item.Image != null)
{
App.DebugPrint($"skipped, loading or already loaded, index: {index}, loading: {item.Loading}");
return;
}
}
item.Loading = true;
var image = Stores.LoadPreviewImage(item.PreviewUrl, 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;
App.DebugPrint($"download progress: {val}");
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 void Favorite_Clicked(object sender, EventArgs e)
{
var favorites = Stores.Favorites;
var illust = IllustItem;
var index = favorites.FindIndex(i => i.Id == illust.Id);
if (index < 0)
{
illust.IsFavorite = true;
illust.FavoriteDateUtc = DateTime.UtcNow;
favorites.Insert(0, illust);
FavoriteIcon = fontIconLove;
}
else
{
illust.IsFavorite = false;
favorites.RemoveAt(index);
FavoriteIcon = fontIconNotLove;
}
}
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 Refresh_Clicked(object sender, EventArgs e)
{
Task.Run(() => 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<string> extras = new List<string>();
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<Permissions.Photos>();
if (status != PermissionStatus.Granted)
{
status = await Permissions.RequestAsync<Permissions.Photos>();
if (status != PermissionStatus.Granted)
{
App.DebugPrint("access denied to gallery.");
return;
}
}
var success = await Task.Run(ugoira.ExportVideo); // ugoira.ExportGif
if (success != null)
{
var result = await FileStore.SaveVideoToGalleryAsync(success);
msg = result ?? ResourceHelper.ExportSuccess;
}
}
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<Permissions.Photos>();
if (status != PermissionStatus.Granted)
{
status = await Permissions.RequestAsync<Permissions.Photos>();
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; }
}
}