using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; using Gallery.Resources; using Gallery.UI; using Gallery.UI.Theme; using Gallery.Utils; using Xamarin.Essentials; using Xamarin.Forms; namespace Gallery.Illust { public abstract class IllustDataCollectionPage : IllustCollectionPage { } public abstract class IllustRankingDataCollectionPage : IllustScrollableCollectionPage { } public abstract class IllustRecommendsCollectionPage : IllustScrollableCollectionPage { } public abstract class IllustUserDataCollectionPage : IllustScrollableCollectionPage { } public abstract class FavoriteIllustCollectionPage : IllustScrollableCollectionPage { } public interface IIllustCollectionPage { IllustCollection IllustCollection { get; set; } } public abstract class IllustCollectionPage : AdaptedPage, IIllustCollectionPage { protected const double loadingOffset = -40; #region - Properties - public static readonly BindableProperty IllustsProperty = BindableProperty.Create( nameof(Illusts), typeof(IllustCollection), typeof(IllustCollectionPage)); public static readonly BindableProperty ColumnsProperty = BindableProperty.Create( nameof(Columns), typeof(int), typeof(IllustCollectionPage), 2); public static readonly BindableProperty IsLoadingProperty = BindableProperty.Create( nameof(IsLoading), typeof(bool), typeof(IllustCollectionPage), true); public static readonly BindableProperty IsBottomLoadingProperty = BindableProperty.Create( nameof(IsBottomLoading), typeof(bool), typeof(IllustCollectionPage), false); public IllustCollection Illusts { get => (IllustCollection)GetValue(IllustsProperty); set => SetValue(IllustsProperty, value); } public int Columns { get => (int)GetValue(ColumnsProperty); set => SetValue(ColumnsProperty, value); } public bool IsLoading { get => (bool)GetValue(IsLoadingProperty); set => SetValue(IsLoadingProperty, value); } public bool IsBottomLoading { get => (bool)GetValue(IsBottomLoadingProperty); set => SetValue(IsBottomLoadingProperty, value); } #endregion protected static DateTime LastUpdated { get; private set; } = DateTime.Now; public IllustCollection IllustCollection { get; set; } protected virtual bool IsFavoriteVisible => true; protected virtual bool IsAutoReload => true; protected virtual bool IsRedirectLogin => false; protected virtual bool NeedCookie => false; protected virtual ActivityIndicator LoadingIndicator => null; protected virtual double IndicatorMarginTop => 16; protected readonly Command commandIllustImageTapped; protected readonly Command commandUserTapped; protected DateTime lastUpdated; protected double topOffset; protected string lastError; private readonly object sync = new object(); private readonly Stack tasks; private T illustData; public IllustCollectionPage() { commandIllustImageTapped = new Command(OnIllustImageTapped); commandUserTapped = new Command(OnIllustUserItemTapped); tasks = new Stack(); BindingContext = this; } private void OnIllustImageTapped(IllustItem illust) { if (illust == null) { return; } Start(async () => { var page = new ViewIllustPage(illust); await Navigation.PushAsync(page); }); } private void OnIllustUserItemTapped(IIllustItem item) { if (item == null) { return; } Start(async () => { var page = new UserIllustPage(item); await Navigation.PushAsync(page); }); } #region - Overrides - public override void OnUnload() { lock (sync) { while (tasks.TryPop(out var task)) { if (task != null) { task.Dispose(); } } } InvalidateCollection(); Illusts = null; lastUpdated = default; } protected override void OnAppearing() { base.OnAppearing(); if (lastUpdated == default || (IsAutoReload && lastUpdated != LastUpdated)) { StartLoad(); } else if (IllustCollection != null) { var favorites = Stores.Favorites; foreach (var item in IllustCollection) { item.IsFavorite = item.BookmarkId != null || favorites.Any(i => i.Id == item.Id); } } } #if __IOS__ public override void OnOrientationChanged(bool landscape) { base.OnOrientationChanged(landscape); if (StyleDefinition.IsFullscreenDevice) { topOffset = landscape ? AppShell.NavigationBarOffset.Top : AppShell.TotalBarOffset.Top; } else if (isPhone) { topOffset = landscape ? StyleDefinition.TopOffset32.Top : AppShell.TotalBarOffset.Top; } else { // iPad topOffset = AppShell.TotalBarOffset.Top; } } #endif protected override void OnSizeAllocated(double width, double height) { base.OnSizeAllocated(width, height); int columns; if (width > height) { columns = isPhone ? 4 : 6; } else { columns = isPhone ? 2 : 4; } if (Columns != columns) { Columns = columns; #if DEBUG App.DebugPrint($"change columns to {columns}"); #endif } } #endregion protected abstract T DoLoadIllustData(bool force); protected abstract IEnumerable DoGetIllustList(T data, out int tag); protected virtual IllustCollection GetIllustsLoadedCollection(IllustCollection collection, bool bottom) { IllustCollection = collection; return collection; } protected void InvalidateCollection() { var collection = IllustCollection; if (collection != null) { collection.Running = false; IllustCollection = null; } } protected virtual void StartLoad(bool force = false, bool isBottom = false) { if (force || lastUpdated != LastUpdated) { lastUpdated = LastUpdated; var indicator = LoadingIndicator; if (indicator == null || isBottom) { if (isBottom) { IsBottomLoading = true; } else { InvalidateCollection(); IsLoading = true; } #if __IOS__ Device.StartTimer(TimeSpan.FromMilliseconds(48), () => #else Device.StartTimer(TimeSpan.FromMilliseconds(150), () => #endif { Task.Run(() => DoLoadIllusts(force, isBottom)); return false; }); } else { InvalidateCollection(); IsLoading = true; var offset = 16 - IndicatorMarginTop; indicator.Animate("margin", top => { indicator.Margin = new Thickness(0, top, 0, offset); }, loadingOffset - offset, 16 - offset, easing: Easing.CubicOut, finished: (v, r) => { _ = Task.Run(() => DoLoadIllusts(force, isBottom)); }); } } } protected virtual void DoIllustsLoaded(IllustCollection collection, bool bottom) { collection = GetIllustsLoadedCollection(collection, bottom); var indicator = LoadingIndicator; if (indicator == null || bottom) { IsLoading = false; IsBottomLoading = false; #if __IOS__ Device.StartTimer(TimeSpan.FromMilliseconds(48), () => #else Device.StartTimer(TimeSpan.FromMilliseconds(150), () => #endif { Illusts = collection; return false; }); } else { var offset = 16 - IndicatorMarginTop; indicator.Animate("margin", top => { indicator.Margin = new Thickness(0, top, 0, offset); }, 16 - offset, loadingOffset - offset, easing: Easing.CubicIn, finished: (v, r) => { indicator.Margin = new Thickness(0, v, 0, offset); IsLoading = false; IsBottomLoading = false; #if __IOS__ Device.StartTimer(TimeSpan.FromMilliseconds(48), () => #else Device.StartTimer(TimeSpan.FromMilliseconds(150), () => #endif { Illusts = collection; return false; }); }); } } protected async Task ScrollToTopAsync(ScrollView scrollView) { if (scrollView.ScrollY > -topOffset) { #if __IOS__ await scrollView.ScrollToAsync(scrollView.ScrollX, -topOffset, true); #else await scrollView.ScrollToAsync(0, -topOffset, false); #endif } } protected DataTemplate GetCardViewTemplate(bool hideUser = false, string titleBinding = null) { return new DataTemplate(() => { #region - components - // image var image = new RoundImage { BackgroundColor = StyleDefinition.ColorDownloadBackground, CornerRadius = 10, CornerMasks = CornerMask.Top, HorizontalOptions = LayoutOptions.Fill, Aspect = Aspect.AspectFill, GestureRecognizers = { new TapGestureRecognizer { Command = commandIllustImageTapped } .Binding(TapGestureRecognizer.CommandParameterProperty, ".") } } .Binding(Image.SourceProperty, nameof(IllustItem.Image)); // label: r-18 var r18 = new RoundLabel { Text = ResourceHelper.R18, BackgroundColor = StyleDefinition.ColorRedBackground, Margin = new Thickness(6, 6, 0, 0), Padding = new Thickness(6, 2), CornerRadius = 4, HorizontalOptions = LayoutOptions.Start, VerticalOptions = LayoutOptions.Start, FontSize = StyleDefinition.FontSizeMicro, TextColor = Color.White } .Binding(IsVisibleProperty, nameof(IllustItem.IsRestrict)); // label: pages var pages = new RoundLabel { BackgroundColor = StyleDefinition.ColorDeepShadow, Margin = new Thickness(0, 6, 6, 0), Padding = new Thickness(6, 4), CornerRadius = 6, HorizontalOptions = LayoutOptions.End, VerticalOptions = LayoutOptions.Start, FontSize = StyleDefinition.FontSizeMicro, TextColor = Color.White } .Binding(Label.TextProperty, nameof(IllustItem.PageCountText)) .Binding(IsVisibleProperty, nameof(IllustItem.IsPageVisible)) .DynamicResource(Label.FontFamilyProperty, ThemeBase.IconSolidFontFamily); // label: is anime var anime = new RoundLabel { Text = StyleDefinition.IconPlay, BackgroundColor = StyleDefinition.ColorDeepShadow, Margin = new Thickness(0, 0, 6, 6), Padding = new Thickness(12, 9, 0, 0), WidthRequest = 36, HeightRequest = 36, CornerRadius = 18, HorizontalOptions = LayoutOptions.End, VerticalOptions = LayoutOptions.End, FontSize = StyleDefinition.FontSizeTitle, TextColor = Color.White } .Binding(IsVisibleProperty, nameof(IllustItem.IsAnimeVisible)) .DynamicResource(Label.FontFamilyProperty, ThemeBase.IconSolidFontFamily); // label: title var title = new Label { Padding = new Thickness(8, 2), HorizontalOptions = LayoutOptions.FillAndExpand, VerticalOptions = LayoutOptions.Center, LineBreakMode = LineBreakMode.TailTruncation, FontSize = StyleDefinition.FontSizeSmall }.DynamicResource(Label.TextColorProperty, ThemeBase.TextColor); // label: favorite var favorite = new Label { WidthRequest = 26, HorizontalOptions = LayoutOptions.End, HorizontalTextAlignment = TextAlignment.End, VerticalOptions = LayoutOptions.Center, FontSize = StyleDefinition.FontSizeSmall, TextColor = StyleDefinition.ColorRedBackground, IsVisible = false } .Binding(Label.TextProperty, nameof(IllustItem.BookmarkId), converter: new FavoriteIconConverter(IsFavoriteVisible)) .Binding(IsVisibleProperty, nameof(IllustItem.IsFavorite)) .DynamicResource(Label.FontFamilyProperty, ThemeBase.IconSolidFontFamily); #endregion if (hideUser) { return new CardView { Padding = 0, Margin = 0, CornerRadius = 10, ShadowColor = StyleDefinition.ColorLightShadow, ShadowOffset = new Size(1, 1), Content = new Grid { HorizontalOptions = LayoutOptions.Fill, RowSpacing = 0, RowDefinitions = { new RowDefinition().Binding(RowDefinition.HeightProperty, nameof(IllustItem.ImageHeight)), new RowDefinition { Height = 30 } }, Children = { image, r18, pages, anime, // stacklayout: title new Grid { ColumnDefinitions = { new ColumnDefinition(), new ColumnDefinition { Width = 20 } }, VerticalOptions = LayoutOptions.Center, Padding = new Thickness(0, 0, 8, 0), Children = { title.Binding(Label.TextProperty, nameof(IllustItem.Title)), favorite.GridColumn(1) } } .GridRow(1) } } } .DynamicResource(BackgroundColorProperty, ThemeBase.CardBackgroundColor); } return new CardView { Padding = 0, Margin = 0, CornerRadius = 10, ShadowColor = StyleDefinition.ColorLightShadow, ShadowOffset = new Size(1, 1), Content = new Grid { HorizontalOptions = LayoutOptions.Fill, RowSpacing = 0, RowDefinitions = { new RowDefinition().Binding(RowDefinition.HeightProperty, nameof(IllustItem.ImageHeight)), new RowDefinition { Height = 30 }, new RowDefinition { Height = 40 } }, Children = { image, r18, pages, anime, title.Binding(Label.TextProperty, titleBinding ?? nameof(IllustItem.Title)).GridRow(1), // stacklayout: user new Grid { ColumnDefinitions = { new ColumnDefinition { Width = 30 }, new ColumnDefinition(), new ColumnDefinition { Width = 20 } }, Padding = new Thickness(8, 0, 8, 8), Children = { // user icon new CircleImage { WidthRequest = 30, HeightRequest = 30, Aspect = Aspect.AspectFill } .Binding(Image.SourceProperty, nameof(IIllustItem.ProfileImage)), // user name new Label { HorizontalOptions = LayoutOptions.FillAndExpand, VerticalOptions = LayoutOptions.Center, LineBreakMode = LineBreakMode.TailTruncation, FontSize = StyleDefinition.FontSizeMicro } .Binding(Label.TextProperty, nameof(IIllustItem.UserName)) .DynamicResource(Label.TextColorProperty, ThemeBase.SubTextColor) .GridColumn(1), // label: favorite favorite.GridColumn(2) }, GestureRecognizers = { new TapGestureRecognizer { Command = commandUserTapped } .Binding(TapGestureRecognizer.CommandParameterProperty, ".") } } .GridRow(2) } } } .DynamicResource(BackgroundColorProperty, ThemeBase.CardBackgroundColor); }); } #region - Illust Tasks - private async void RedirectFailed() { if (!IsRedirectLogin) { string extra; if (lastError != null) { if (lastError.Length > 40) { extra = $"\n{lastError.Substring(0, 40)}..."; } else { extra = $"\n{lastError}"; } } else { extra = string.Empty; } await DisplayAlert(ResourceHelper.Title, ResourceHelper.FailedResponse + extra, ResourceHelper.Ok); } else { var result = await DisplayAlert( ResourceHelper.Title, ResourceHelper.ConfirmLogin, ResourceHelper.Yes, ResourceHelper.No); if (result) { AppShell.Current.PushToLogin(() => { Task.Run(() => AppShell.Current.DoLoginInformation(true)); StartLoad(true); }); } } IsLoading = false; IsBottomLoading = false; } protected void DoLoadIllusts(bool force = false, bool bottom = false) { #if DEBUG App.DebugPrint($"start loading data, force: {force}"); #endif if (force && NeedCookie && string.IsNullOrEmpty(Configs.Cookie)) { MainThread.BeginInvokeOnMainThread(RedirectFailed); App.DebugPrint($"no cookie found"); return; } illustData = DoLoadIllustData(force); if (illustData == null) { MainThread.BeginInvokeOnMainThread(RedirectFailed); //App.DebugError("illusts.load", "failed to load illusts data."); return; } if (force && IsFavoriteVisible) { var now = DateTime.Now; LastUpdated = now; lastUpdated = now; } var r18 = Configs.IsOnR18; var data = DoGetIllustList(illustData, out int tag).Where(i => i != null && (r18 || !i.IsRestrict)); var collection = new IllustCollection(data); var favorites = Stores.Favorites; foreach (var item in collection) { if (item.Image == null) { string url; try { url = Configs.GetThumbnailUrl(item.ImageUrl); } catch (Exception ex) { App.DebugError("image.getthumbnail", $"{item.ImageUrl}, {ex}"); continue; } var image = Stores.LoadPreviewImage(url, false); if (image == null) { image = Stores.LoadThumbnailImage(url, false); } if (image != null) { item.Image = image; } item.ImageUrl = url; } if (item.ProfileImage == null) { if (item.ProfileUrl == null) { item.ProfileImage = StyleDefinition.ProfileNone; } else { var image = Stores.LoadUserProfileImage(item.ProfileUrl, false); if (image != null) { item.ProfileImage = image; } } } item.IsFavorite = item.BookmarkId != null || favorites.Any(i => i.Id == item.Id); } DoIllustsLoaded(collection, bottom); DoLoadImages(collection, tag); } private void DoLoadImages(IllustCollection collection, int tag) { lock (sync) { if (tasks.TryPeek(out var peek)) { if (peek.TagIndex >= tag) { App.DebugPrint($"tasks expired ({tasks.Count}), peek: {peek.TagIndex}, now: {tag}, will be disposing."); while (tasks.TryPop(out var t)) { if (t != null) { t.Dispose(); } } } } } var list = collection.Where(i => i.Image == null || i.ProfileImage == null).ToArray(); var task = ParallelTask.Start("collection.load", 0, list.Length, Configs.MaxThreads, i => { if (!collection.Running) { return false; } var illust = list[i]; if (illust.Image == null && illust.ImageUrl != null) { illust.Image = StyleDefinition.DownloadBackground; var image = Stores.LoadThumbnailImage(illust.ImageUrl, true, force: true); if (image != null) { illust.Image = image; } } if (illust.ProfileImage == null && illust.ProfileUrl != null) { illust.ProfileImage = StyleDefinition.ProfileNone; var userImage = Stores.LoadUserProfileImage(illust.ProfileUrl, true, force: true); if (userImage != null) { illust.ProfileImage = userImage; } } return true; }, tagIndex: tag); if (task != null) { lock (sync) { tasks.Push(task); } } } #endregion } public abstract class IllustScrollableCollectionPage : IllustCollectionPage { protected const int SCROLL_OFFSET = 33; protected ScrollDirection scrollDirection = ScrollDirection.Stop; protected double lastScrollY = double.MinValue; private double lastRefreshY = double.MinValue; private double offset; protected bool IsScrollingDown(double y) { if (y > lastScrollY) { if (scrollDirection != ScrollDirection.Down) { scrollDirection = ScrollDirection.Down; } return true; } else { if (scrollDirection != ScrollDirection.Up) { scrollDirection = ScrollDirection.Up; } return false; } } protected void SetOffset(double off) { offset = off; } protected abstract bool CheckRefresh(); protected override void StartLoad(bool force = false, bool isBottom = false) { if (!isBottom) { lastRefreshY = double.MinValue; } base.StartLoad(force, isBottom); } protected override IllustCollection GetIllustsLoadedCollection(IllustCollection collection, bool bottom) { var now = IllustCollection; if (now == null) { now = collection; IllustCollection = now; } else { //now = new IllustCollection(now.Concat(collection)); now.AddRange(collection); } return now; } protected void OnScrolled(double y) { lastScrollY = y; if (scrollDirection == ScrollDirection.Up) { return; } if (y > 0 && offset > 0 && y - topOffset > offset) { if (IsLoading || IsBottomLoading) { return; } if (y - lastRefreshY > 200) { if (CheckRefresh()) { lastRefreshY = y; #if DEBUG App.DebugPrint("start to load next page"); #endif StartLoad(true, true); } } } } } public enum ScrollDirection { Stop, Up, Down } public class IllustCollection : List, IIllustCollectionChanged { private static IllustCollection empty; public static IllustCollection Empty { get { if (empty == null) { empty = new IllustCollection(); } return empty; } } public event EventHandler CollectionChanged; public IllustCollection() : base() { Running = true; } public IllustCollection(IEnumerable illusts) : base(illusts) { Running = true; } public void AddRange(List items) { var e = new CollectionChangedEventArgs { NewStartingIndex = Count, NewItems = items }; base.AddRange(items); if (MainThread.IsMainThread) { CollectionChanged?.Invoke(this, e); } else { MainThread.BeginInvokeOnMainThread(() => CollectionChanged?.Invoke(this, e)); } } public bool Running { get; set; } } public enum IllustType { Illust = 0, Manga = 1, Anime = 2 } public interface IIllustItem { string UserId { get; } string UserName { get; } ImageSource ProfileImage { get; } } [JsonObject(MemberSerialization.OptIn)] public class IllustItem : BindableObject, IIllustItem { private static IllustItem empty; public static IllustItem Empty { get { if (empty == null) { empty = new IllustItem(); } return empty; } } public static readonly BindableProperty ImageProperty = BindableProperty.Create( nameof(Image), typeof(ImageSource), typeof(IllustItem)); public static readonly BindableProperty ProfileImageProperty = BindableProperty.Create( nameof(ProfileImage), typeof(ImageSource), typeof(IllustItem)); public static readonly BindableProperty ImageHeightProperty = BindableProperty.Create( nameof(ImageHeight), typeof(GridLength), typeof(IllustItem), GridLength.Auto); public static readonly BindableProperty IsFavoriteProperty = BindableProperty.Create( nameof(IsFavorite), typeof(bool), typeof(IllustItem)); public static readonly BindableProperty BookmarkIdProperty = BindableProperty.Create( nameof(BookmarkId), typeof(string), typeof(IllustItem)); [JsonProperty] public string Title { get; set; } public int Rank { get; set; } public string RankTitle => Rank > 0 ? $"#{Rank} {Title}" : Title; public ImageSource Image { get => (ImageSource)GetValue(ImageProperty); set => SetValue(ImageProperty, value); } public ImageSource ProfileImage { get => (ImageSource)GetValue(ProfileImageProperty); set => SetValue(ProfileImageProperty, value); } public GridLength ImageHeight { get => (GridLength)GetValue(ImageHeightProperty); set => SetValue(ImageHeightProperty, value); } public bool IsFavorite { get => (bool)GetValue(IsFavoriteProperty); set => SetValue(IsFavoriteProperty, value); } public bool IsPlaying { get; set; } [JsonProperty] public string Id { get; set; } [JsonProperty] public string BookmarkId { get => (string)GetValue(BookmarkIdProperty); set => SetValue(BookmarkIdProperty, value); } [JsonProperty] public DateTime FavoriteDateUtc { get; set; } [JsonProperty] public string ImageUrl { get; set; } [JsonProperty] public bool IsRestrict { get; set; } [JsonProperty] public string[] Tags { get; set; } [JsonProperty] public string ProfileUrl { get; set; } [JsonProperty] public string UserId { get; set; } [JsonProperty] public string UserName { get; set; } [JsonProperty] public int Width { get; set; } [JsonProperty] public int Height { get; set; } [JsonProperty] public int PageCount { get; set; } [JsonProperty] public double ImageHeightValue { get => ImageHeight.IsAuto ? -1 : ImageHeight.Value; set => ImageHeight = value > 0 ? value : GridLength.Auto; } [JsonProperty] public int YesRank { get; set; } [JsonProperty] public int RatingCount { get; set; } [JsonProperty] public int ViewCount { get; set; } [JsonProperty] public long UploadTimestamp { get; set; } [JsonProperty] public IllustType IllustType { get; set; } public string PageCountText => $"{StyleDefinition.IconLayer} {PageCount}"; public bool IsPageVisible => PageCount > 1; public bool IsAnimeVisible => IllustType == IllustType.Anime; } }