621 lines
20 KiB
C#
621 lines
20 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Gallery.Services;
|
|
using Gallery.Util;
|
|
using Gallery.Util.Interface;
|
|
using Gallery.Util.Model;
|
|
using Xamarin.Forms;
|
|
|
|
namespace Gallery.Resources.UI
|
|
{
|
|
public abstract class GalleryCollectionPage : GalleryScrollableCollectionPage<IEnumerable<GalleryItem>>
|
|
{
|
|
public GalleryCollectionPage(IGallerySource source) : base(source) { }
|
|
}
|
|
|
|
public interface IGalleryCollectionPage
|
|
{
|
|
GalleryCollection GalleryCollection { get; set; }
|
|
}
|
|
|
|
public abstract class GalleryCollectionPage<T> : AdaptedPage, IGalleryCollectionPage
|
|
{
|
|
const int EXPIRED_MINUTES = 5;
|
|
|
|
protected const double loadingOffset = -40;
|
|
|
|
public static readonly BindableProperty GalleryProperty = BindableProperty.Create(nameof(Gallery), typeof(GalleryCollection), typeof(GalleryCollectionPage<T>));
|
|
public static readonly BindableProperty ColumnsProperty = BindableProperty.Create(nameof(Columns), typeof(int), typeof(GalleryCollectionPage<T>),
|
|
defaultValue: 2);
|
|
public static readonly BindableProperty IsLoadingProperty = BindableProperty.Create(nameof(IsLoading), typeof(bool), typeof(GalleryCollectionPage<T>),
|
|
defaultValue: true);
|
|
public static readonly BindableProperty IsBottomLoadingProperty = BindableProperty.Create(nameof(IsBottomLoading), typeof(bool), typeof(GalleryCollectionPage<T>));
|
|
|
|
public GalleryCollection Gallery
|
|
{
|
|
get => (GalleryCollection)GetValue(GalleryProperty);
|
|
set => SetValue(GalleryProperty, 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);
|
|
}
|
|
|
|
public IGallerySource Source { get; }
|
|
public GalleryCollection GalleryCollection { get; set; }
|
|
public DateTime LastUpdated
|
|
{
|
|
get
|
|
{
|
|
if (App.RefreshTimes.TryGetValue(Source.Route, out var time))
|
|
{
|
|
#if DEBUG
|
|
Log.Print($"get last updated time for: {Source.Route}, {time}");
|
|
#endif
|
|
return time;
|
|
}
|
|
#if DEBUG
|
|
Log.Print($"cannot get last updated time for: {Source.Route}");
|
|
#endif
|
|
return default;
|
|
}
|
|
set
|
|
{
|
|
#if DEBUG
|
|
Log.Print($"set last updated time for: {Source.Route} to {value}");
|
|
#endif
|
|
App.RefreshTimes[Source.Route] = value;
|
|
}
|
|
}
|
|
|
|
protected virtual ActivityIndicator LoadingIndicator => null;
|
|
protected virtual double IndicatorMarginTop => 16;
|
|
|
|
protected bool Expired
|
|
{
|
|
get
|
|
{
|
|
var lastUpdated = LastUpdated;
|
|
return lastUpdated == default || (DateTime.Now - lastUpdated).TotalMinutes > EXPIRED_MINUTES;
|
|
}
|
|
}
|
|
|
|
protected double topOffset;
|
|
protected string lastError;
|
|
|
|
private readonly object sync = new();
|
|
private readonly Command<GalleryItem> commandGalleryItemTapped;
|
|
private readonly Stack<ParallelTask> tasks = new();
|
|
private T galleryData;
|
|
|
|
public GalleryCollectionPage(IGallerySource source)
|
|
{
|
|
Source = source;
|
|
commandGalleryItemTapped = new Command<GalleryItem>(OnGalleryItemTapped);
|
|
}
|
|
|
|
protected virtual void OnGalleryItemTapped(GalleryItem item)
|
|
{
|
|
}
|
|
|
|
public override void OnUnload()
|
|
{
|
|
lock (sync)
|
|
{
|
|
while (tasks.TryPop(out var task))
|
|
{
|
|
if (task != null)
|
|
{
|
|
task.Dispose();
|
|
}
|
|
}
|
|
}
|
|
InvalidateCollection();
|
|
Gallery = null;
|
|
LastUpdated = default;
|
|
}
|
|
|
|
protected override void OnAppearing()
|
|
{
|
|
base.OnAppearing();
|
|
|
|
if (Expired)
|
|
{
|
|
StartLoading();
|
|
}
|
|
else if (GalleryCollection != null)
|
|
{
|
|
var favorites = Store.FavoriteList;
|
|
foreach (var item in GalleryCollection)
|
|
{
|
|
item.IsFavorite = favorites.Any(i => i.SourceEquals(item));
|
|
}
|
|
}
|
|
}
|
|
|
|
#if __IOS__
|
|
public override void OnOrientationChanged(bool landscape)
|
|
{
|
|
base.OnOrientationChanged(landscape);
|
|
|
|
if (Definition.IsFullscreenDevice)
|
|
{
|
|
topOffset = landscape ?
|
|
AppShell.NavigationBarOffset.Top :
|
|
AppShell.TotalBarOffset.Top;
|
|
}
|
|
else if (isPhone)
|
|
{
|
|
topOffset = landscape ?
|
|
Definition.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
|
|
Log.Print($"changing columns to {columns}");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
protected abstract Task<T> DoloadGalleryData(bool force);
|
|
protected abstract IEnumerable<GalleryItem> DoGetGalleryList(T data, out int tag);
|
|
|
|
protected virtual GalleryCollection FilterGalleryCollection(GalleryCollection collection, bool bottom)
|
|
{
|
|
GalleryCollection = collection;
|
|
return collection;
|
|
}
|
|
|
|
protected void InvalidateCollection()
|
|
{
|
|
var collection = GalleryCollection;
|
|
if (collection != null)
|
|
{
|
|
collection.Running = false;
|
|
GalleryCollection = null;
|
|
}
|
|
}
|
|
|
|
protected virtual void StartLoading(bool force = false, bool isBottom = false)
|
|
{
|
|
if (force || Expired)
|
|
{
|
|
if (!isBottom)
|
|
{
|
|
lock (sync)
|
|
{
|
|
// destory all the tasks
|
|
while (tasks.TryPop(out var t))
|
|
{
|
|
t?.Dispose();
|
|
}
|
|
}
|
|
}
|
|
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
|
|
{
|
|
_ = DoloadGallerySource(force, isBottom);
|
|
return false;
|
|
});
|
|
}
|
|
else
|
|
{
|
|
InvalidateCollection();
|
|
IsLoading = true;
|
|
|
|
var offset = 16 - IndicatorMarginTop;
|
|
indicator.CancelAnimations();
|
|
indicator.Animate("margin", top =>
|
|
{
|
|
indicator.Margin = new Thickness(0, top, 0, offset);
|
|
},
|
|
loadingOffset - offset, 16 - offset,
|
|
easing: Easing.CubicOut,
|
|
finished: (v, r) =>
|
|
{
|
|
_ = DoloadGallerySource(force, isBottom);
|
|
});
|
|
}
|
|
BeforeLoading();
|
|
}
|
|
}
|
|
|
|
protected virtual void DoGalleryLoaded(GalleryCollection collection, bool bottom)
|
|
{
|
|
collection = FilterGalleryCollection(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
|
|
{
|
|
Gallery = collection;
|
|
AfterLoaded();
|
|
return false;
|
|
});
|
|
}
|
|
else
|
|
{
|
|
var offset = 16 - IndicatorMarginTop;
|
|
indicator.CancelAnimations();
|
|
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
|
|
{
|
|
Gallery = collection;
|
|
AfterLoaded();
|
|
return false;
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
protected virtual void BeforeLoading() { }
|
|
protected virtual void AfterLoaded() { }
|
|
|
|
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(string titleBinding = null)
|
|
{
|
|
return new DataTemplate(() =>
|
|
{
|
|
var image = new RoundImage
|
|
{
|
|
BackgroundColor = Definition.ColorDownloadBackground,
|
|
CornerRadius = 10,
|
|
CornerMasks = CornerMask.Top,
|
|
HorizontalOptions = LayoutOptions.Fill,
|
|
Aspect = Aspect.AspectFill,
|
|
GestureRecognizers =
|
|
{
|
|
new TapGestureRecognizer
|
|
{
|
|
Command = commandGalleryItemTapped
|
|
}
|
|
.Binding(TapGestureRecognizer.CommandParameterProperty, ".")
|
|
}
|
|
}
|
|
.Binding(Image.SourceProperty, nameof(GalleryItem.PreviewImage));
|
|
|
|
var title = new Label
|
|
{
|
|
Padding = new Thickness(8, 2),
|
|
HorizontalOptions = LayoutOptions.FillAndExpand,
|
|
VerticalOptions = LayoutOptions.Center,
|
|
LineBreakMode = LineBreakMode.TailTruncation,
|
|
FontSize = Definition.FontSizeSmall
|
|
}
|
|
.DynamicResource(Label.TextColorProperty, Theme.Theme.TextColor);
|
|
|
|
var favorite = new Label
|
|
{
|
|
WidthRequest = 26,
|
|
HorizontalOptions = LayoutOptions.End,
|
|
HorizontalTextAlignment = TextAlignment.End,
|
|
VerticalOptions = LayoutOptions.Center,
|
|
FontSize = Definition.FontSizeSmall,
|
|
TextColor = Definition.ColorRedBackground,
|
|
IsVisible = false
|
|
}
|
|
.Binding(Label.TextProperty, nameof(GalleryItem.BookmarkId), converter: new FavoriteIconConverter())
|
|
.Binding(IsVisibleProperty, nameof(GalleryItem.IsFavorite))
|
|
.DynamicResource(Label.FontFamilyProperty, Theme.Theme.IconSolidFamily);
|
|
|
|
return new CardView
|
|
{
|
|
Padding = 0,
|
|
Margin = 0,
|
|
CornerRadius = 10,
|
|
ShadowColor = Definition.ColorLightShadow,
|
|
ShadowOffset = new Size(1, 1),
|
|
Content = new Grid
|
|
{
|
|
HorizontalOptions = LayoutOptions.Fill,
|
|
RowSpacing = 0,
|
|
RowDefinitions =
|
|
{
|
|
new RowDefinition().Binding(RowDefinition.HeightProperty, nameof(GalleryItem.ImageHeight)),
|
|
new RowDefinition { Height = 30 }
|
|
},
|
|
Children =
|
|
{
|
|
image,
|
|
new Grid
|
|
{
|
|
ColumnDefinitions =
|
|
{
|
|
new ColumnDefinition(),
|
|
new ColumnDefinition { Width = 20 }
|
|
},
|
|
VerticalOptions = LayoutOptions.Center,
|
|
Padding = new Thickness(0, 0, 8, 0),
|
|
Children =
|
|
{
|
|
title.Binding(Label.TextProperty, titleBinding ?? nameof(GalleryItem.TagDescription)),
|
|
favorite.GridColumn(1)
|
|
}
|
|
}
|
|
.GridRow(1)
|
|
}
|
|
}
|
|
}
|
|
.DynamicResource(BackgroundColorProperty, Theme.Theme.CardBackgroundColor);
|
|
});
|
|
}
|
|
|
|
protected async Task DoloadGallerySource(bool force = false, bool bottom = false)
|
|
{
|
|
#if DEBUG
|
|
Log.Print($"start loading data, force: {force}");
|
|
#endif
|
|
galleryData = await DoloadGalleryData(force);
|
|
if (galleryData == null)
|
|
{
|
|
Log.Error("gallery.load", "failed to load gallery data.");
|
|
return;
|
|
}
|
|
if (force || Expired)
|
|
{
|
|
LastUpdated = DateTime.Now;
|
|
}
|
|
|
|
var data = DoGetGalleryList(galleryData, out int tag).Where(i => i != null);
|
|
var collection = new GalleryCollection(data);
|
|
var favorites = Store.FavoriteList;
|
|
foreach (var item in collection)
|
|
{
|
|
if (item.PreviewImage == null)
|
|
{
|
|
var image = await Store.LoadRawImage(item, false);
|
|
if (image != null)
|
|
{
|
|
item.PreviewImage = image;
|
|
}
|
|
else
|
|
{
|
|
image = await Store.LoadPreviewImage(item, false);
|
|
if (image != null)
|
|
{
|
|
item.PreviewImage = image;
|
|
}
|
|
}
|
|
}
|
|
item.IsFavorite = favorites.Any(i => i.SourceEquals(item));
|
|
}
|
|
|
|
DoGalleryLoaded(collection, bottom);
|
|
DoloadImages(collection, tag);
|
|
}
|
|
|
|
private void DoloadImages(GalleryCollection collection, int tag)
|
|
{
|
|
lock (sync)
|
|
{
|
|
if (tasks.TryPeek(out var peek))
|
|
{
|
|
if (peek != null && peek.TagIndex >= tag)
|
|
{
|
|
Log.Print($"tasks expired ({tasks.Count}, peek: {peek.TagIndex}, now: {tag}, will be disposed.");
|
|
while (tasks.TryPop(out var t))
|
|
{
|
|
t?.Dispose();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var list = collection.Where(i => i.PreviewImage == null).ToArray();
|
|
var task = ParallelTask.Start("collection.load", 0, list.Length, Config.DownloadThreads, i =>
|
|
{
|
|
if (!collection.Running)
|
|
{
|
|
return false;
|
|
}
|
|
var item = list[i];
|
|
if (item.PreviewImage == null && item.PreviewUrl != null)
|
|
{
|
|
item.PreviewImage = Definition.DownloadBackground;
|
|
#if DEBUG
|
|
var model = Xamarin.Essentials.DeviceInfo.Model;
|
|
if (model.StartsWith("iPhone") || model.StartsWith("iPad"))
|
|
{
|
|
#endif
|
|
var image = Store.LoadPreviewImage(item, true, force: true).Result;
|
|
if (image != null)
|
|
{
|
|
item.PreviewImage = image;
|
|
}
|
|
#if DEBUG
|
|
}
|
|
#endif
|
|
}
|
|
return true;
|
|
}, tagIndex: tag);
|
|
|
|
if (task != null)
|
|
{
|
|
lock (sync)
|
|
{
|
|
tasks.Push(task);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public abstract class GalleryScrollableCollectionPage<T> : GalleryCollectionPage<T>
|
|
{
|
|
protected const int SCROLL_OFFSET = 33;
|
|
protected ScrollDirection scrollDirection = ScrollDirection.Stop;
|
|
protected double lastScrollY = double.MinValue;
|
|
|
|
private double lastRefreshY = double.MinValue;
|
|
private double offset;
|
|
|
|
public GalleryScrollableCollectionPage(IGallerySource source) : base(source) { }
|
|
|
|
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 StartLoading(bool force = false, bool isBottom = false)
|
|
{
|
|
if (!isBottom)
|
|
{
|
|
lastRefreshY = double.MinValue;
|
|
}
|
|
base.StartLoading(force: force, isBottom: isBottom);
|
|
}
|
|
|
|
protected override GalleryCollection FilterGalleryCollection(GalleryCollection collection, bool bottom)
|
|
{
|
|
var now = GalleryCollection;
|
|
if (now == null)
|
|
{
|
|
now = collection;
|
|
GalleryCollection = now;
|
|
}
|
|
else
|
|
{
|
|
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
|
|
Log.Print("start to load next page");
|
|
#endif
|
|
StartLoading(force: true, isBottom: true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum ScrollDirection
|
|
{
|
|
Stop,
|
|
Up,
|
|
Down
|
|
}
|
|
}
|