rename from Pixiview to Gallery

This commit is contained in:
2021-08-03 19:16:54 +08:00
parent 98676ce8b2
commit c41282a4b7
206 changed files with 7900 additions and 7891 deletions

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<i:FavoriteIllustCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i="clr-namespace:Gallery.Illust"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:r="clr-namespace:Gallery.Resources"
x:Class="Gallery.Illust.FavoritesPage"
BackgroundColor="{DynamicResource WindowColor}"
Title="{r:Text Favorites}">
<Shell.TitleView>
<Grid VerticalOptions="Fill" ColumnSpacing="6"
HorizontalOptions="{x:OnPlatform Android=Start, iOS=Fill}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform Android=Auto}"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackLayout Grid.Column="1" Orientation="Horizontal" Spacing="6">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
</StackLayout.GestureRecognizers>
<Label Text="{Binding Title}"
TextColor="{DynamicResource TextColor}"
FontSize="{OnPlatform Android=18}"
LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center" FontAttributes="Bold"/>
<Label x:Name="labelCaret"
Text="{DynamicResource IconCaretDown}"
TextColor="{DynamicResource TextColor}"
FontFamily="{DynamicResource IconSolidFontFamily}"
FontSize="Small"
VerticalTextAlignment="Center"/>
</StackLayout>
</Grid>
</Shell.TitleView>
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconCloudDownload}"/>
<ToolbarItem Order="Primary" Clicked="ShareFavorites_Clicked"
IconImageSource="{DynamicResource FontIconShare}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"
HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<StackLayout>
<ActivityIndicator x:Name="activityLoading" Margin="0, -40, 0, 0"
HeightRequest="40"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
<u:FlowLayout ItemsSource="{Binding Illusts}" MaxHeightChanged="FlowLayout_MaxHeightChanged"
HorizontalOptions="Fill" Column="{Binding Columns}"
Margin="16" RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}"/>
<ActivityIndicator x:Name="activityBottomLoading" Margin="0, -10, 0, 16"
IsRunning="{Binding IsBottomLoading}"
IsVisible="{Binding IsBottomLoading}"/>
</StackLayout>
</ScrollView>
<u:BlurryPanel x:Name="panelFilter" VerticalOptions="Start" Opacity="0"
Margin="{Binding PanelTopMargin}"
BackgroundColor="{DynamicResource WindowColor}"
HeightRequest="{Binding Height, Source={x:Reference gridFilter}}"/>
<Grid x:Name="gridFilter" VerticalOptions="Start" Opacity="0"
Margin="{Binding PageTopMargin}" Padding="10">
<Grid RowSpacing="0" HorizontalOptions="Center">
<u:SegmentedControl Margin="6, 6, 6, 3" VerticalOptions="Center"
SelectedSegmentIndex="{Binding SegmentType, Mode=TwoWay}"
SelectedTextColor="{DynamicResource TextColor}"
TintColor="{DynamicResource CardBackgroundColor}">
<u:SegmentedControl.Children>
<u:SegmentedControlOption Text="{r:Text All}"/>
<u:SegmentedControlOption Text="{r:Text General}"/>
<u:SegmentedControlOption Text="{r:Text Animation}"/>
<u:SegmentedControlOption Text="{r:Text Online}"/>
</u:SegmentedControl.Children>
</u:SegmentedControl>
</Grid>
</Grid>
</Grid>
</i:FavoriteIllustCollectionPage>

View File

@ -0,0 +1,463 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Gallery.Resources;
using Gallery.UI;
using Gallery.Utils;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.Illust
{
public partial class FavoritesPage : FavoriteIllustCollectionPage
{
private const int STEP = 20;
public static readonly BindableProperty SegmentTypeProperty = BindableProperty.Create(
nameof(SegmentType), typeof(int), typeof(FavoritesPage), propertyChanged: OnSegmentTypePropertyChanged);
private static void OnSegmentTypePropertyChanged(BindableObject obj, object old, object @new)
{
var page = (FavoritesPage)obj;
MainThread.BeginInvokeOnMainThread(page.ChangeFilter);
}
public int SegmentType
{
get => (int)GetValue(SegmentTypeProperty);
set => SetValue(SegmentTypeProperty, value);
}
private bool isFilterVisible;
private int startIndex;
private int nextIndex;
private bool flag = false;
private ParallelTask task;
public FavoritesPage()
{
Resources.Add("cardView", GetCardViewTemplate());
InitializeComponent();
gridFilter.TranslationY = -60;
panelFilter.TranslationY = -60;
SegmentType = Preferences.Get(Configs.FavoriteTypeKey, 0);
startIndex = -1;
nextIndex = 0;
}
protected override bool IsFavoriteVisible => false;
protected override ActivityIndicator LoadingIndicator => activityLoading;
protected override void OnAppearing()
{
if (lastUpdated != LastUpdated)
{
startIndex = -1;
StartLoad();
}
else
{
var favorites = Stores.Favorites;
if (favorites.Changed)
{
lastUpdated = default;
startIndex = -1;
StartLoad();
}
}
}
protected override void OnDisappearing()
{
base.OnDisappearing();
// saving state
Preferences.Set(Configs.FavoriteTypeKey, SegmentType);
}
public override void OnUnload()
{
if (task != null)
{
task.Dispose();
task = null;
}
base.OnUnload();
}
protected override IEnumerable<IllustItem> DoGetIllustList(IllustItem[] data, out int tag)
{
tag = startIndex;
return data;
}
protected override IllustItem[] DoLoadIllustData(bool force)
{
IEnumerable<IllustItem> favs;
if (startIndex < 0)
{
var favorites = Stores.GetFavoriteObject(flag);
flag = false;
if (favorites == null)
{
return null;
}
favs = favorites.Illusts.Reload();
startIndex = 0;
}
else
{
favs = Stores.Favorites;
}
switch (SegmentType)
{
case 1: // general (non r-18)
favs = favs.Where(f => !f.IsRestrict);
break;
case 2: // animation
favs = favs.Where(f => f.IllustType == IllustType.Anime);
break;
case 3: // online
favs = favs.Where(f => f.BookmarkId != null);
break;
}
var illusts = favs.Skip(startIndex).Take(STEP).ToArray();
nextIndex = startIndex + STEP;
if (illusts.Length == 0 || nextIndex >= Stores.Favorites.Count)
{
// reach the bottom
startIndex = nextIndex;
}
return illusts;
}
private async void ToggleFilterPanel(bool flag)
{
ViewExtensions.CancelAnimations(gridFilter);
ViewExtensions.CancelAnimations(panelFilter);
if (flag)
{
isFilterVisible = true;
if (scrollDirection == ScrollDirection.Down)
{
// stop the scrolling
await scrollView.ScrollToAsync(scrollView.ScrollX, scrollView.ScrollY, false);
}
await Task.WhenAll(
labelCaret.RotateTo(180, easing: Easing.CubicOut),
gridFilter.TranslateTo(0, 0, easing: Easing.CubicOut),
gridFilter.FadeTo(1, easing: Easing.CubicOut),
panelFilter.TranslateTo(0, 0, easing: Easing.CubicOut),
panelFilter.FadeTo(1, easing: Easing.CubicOut)
);
}
else
{
isFilterVisible = false;
await Task.WhenAll(
labelCaret.RotateTo(0, easing: Easing.CubicIn),
gridFilter.TranslateTo(0, -60, easing: Easing.CubicIn),
gridFilter.FadeTo(0, easing: Easing.CubicIn),
panelFilter.TranslateTo(0, -60, easing: Easing.CubicIn),
panelFilter.FadeTo(0, easing: Easing.CubicIn)
);
}
}
private async void ChangeFilter()
{
ToggleFilterPanel(false);
await ScrollToTopAsync(scrollView);
lastUpdated = default;
startIndex = 0;
StartLoad();
}
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
ToggleFilterPanel(!isFilterVisible);
}
private void FlowLayout_MaxHeightChanged(object sender, HeightEventArgs e)
{
SetOffset(e.ContentHeight - scrollView.Bounds.Height - SCROLL_OFFSET);
}
protected override bool CheckRefresh()
{
if (nextIndex > startIndex)
{
startIndex = nextIndex;
return true;
}
return false;
}
private void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
{
var y = e.ScrollY;
if (IsScrollingDown(y))
{
// down
if (isFilterVisible)
{
ToggleFilterPanel(false);
}
}
OnScrolled(y);
}
public void Reload(bool force = false)
{
if (force)
{
flag = true;
}
lastUpdated = default;
startIndex = -1;
StartLoad(force);
}
private async void Refresh_Clicked(object sender, EventArgs e)
{
if (IsLoading)
{
return;
}
await ScrollToTopAsync(scrollView);
IsLoading = true;
var offset = 16 - IndicatorMarginTop;
activityLoading.Margin = new Thickness(0, loadingOffset - offset, 0, offset);
activityLoading.Animate("margin", top =>
{
activityLoading.Margin = new Thickness(0, top, 0, offset);
},
loadingOffset - offset, 16 - offset, easing: Easing.CubicOut, finished: (v, r) =>
{
Task.Run(() =>
{
var list = Stores.LoadOnlineFavorites();
if (list != null && list.Length > 0)
{
MainThread.BeginInvokeOnMainThread(() => ConfirmNext(list));
}
else
{
flag = false;
lastUpdated = default;
startIndex = -1;
MainThread.BeginInvokeOnMainThread(() => StartLoad(true));
}
});
});
}
private void CloseLoading(Action next = null)
{
var offset = 16 - IndicatorMarginTop;
activityLoading.Animate("margin", top =>
{
activityLoading.Margin = new Thickness(0, top, 0, offset);
},
16 - offset, loadingOffset - offset, easing: Easing.CubicIn, finished: (v, r) =>
{
IsLoading = false;
next?.Invoke();
});
}
private async void ConfirmNext(IllustItem[] list)
{
var cancel = ResourceHelper.Cancel;
var combine = ResourceHelper.FavoritesCombine;
var replace = ResourceHelper.FavoritesReplace;
var result = await DisplayActionSheet(
ResourceHelper.FavoritesOperation,
cancel,
combine,
replace);
if (result == cancel)
{
return;
}
if (result == replace)
{
Stores.GetFavoriteObject().Illusts = new FavoriteList(list);
}
else if (result == combine)
{
// combine
var nows = Stores.GetFavoriteObject().Illusts;
// sync bookmarks from remote
for (var i = nows.Count - 1; i >= 0; i--)
{
var b = nows[i];
var bookmarkId = b.BookmarkId;
var bookmark = list.FirstOrDefault(f => f.Id == b.Id);
if (bookmark == null)
{
if (bookmarkId != null)
{
// not exists in remote any more
#if LOG
App.DebugPrint($"remove bookmark ({bookmarkId}) - {b.Id}: {b.Title}");
#endif
nows.RemoveAt(i);
}
}
else if (bookmarkId != bookmark.BookmarkId)
{
// update bookmark id
#if LOG
App.DebugPrint($"change bookmark ({bookmarkId}) to ({bookmark.BookmarkId}) - {b.Id}: {b.Title}");
#endif
b.BookmarkId = bookmark.BookmarkId;
}
}
// add bookmarks that exists in remote only
list = list.Where(f => !nows.Any(i => i.Id == f.Id)).ToArray();
if (list.Length > 0)
{
#if LOG
for (var i = 0; i < list.Length; i++)
{
var item = list[i];
App.DebugPrint($"add bookmark ({item.BookmarkId}) - {item.Id}: {item.Title}");
}
#endif
nows.InsertRange(0, list);
}
}
else
{
return;
}
SyncRemoteFavorites(list);
}
private void SyncRemoteFavorites(IllustItem[] list)
{
for (var i = 0; i < list.Length; i++)
{
var item = list[i];
var data = Stores.LoadIllustPreloadData(item.Id, false);
if (data != null && data.illust.TryGetValue(item.Id, out var illust))
{
illust.CopyToItem(item);
if (data.user.TryGetValue(illust.userId, out var user))
{
item.ProfileUrl = user.image;
}
var url = Configs.GetThumbnailUrl(item.ImageUrl);
if (url != null)
{
var image = Stores.LoadPreviewImage(url, false);
if (image == null)
{
image = Stores.LoadThumbnailImage(url, false);
}
if (image != null)
{
item.Image = image;
}
}
url = item.ProfileUrl;
if (url == null)
{
item.ProfileImage = StyleDefinition.ProfileNone;
}
else
{
var image = Stores.LoadUserProfileImage(url, false);
if (image != null)
{
item.ProfileImage = image;
}
}
}
}
list = list.Where(i => i.Image == null || i.ProfileImage == null).ToArray();
if (task != null)
{
task.Dispose();
task = null;
}
task = ParallelTask.Start("favorite.loadimages", 0, list.Length, Configs.MaxPageThreads, i =>
{
var item = list[i];
if (item.ImageUrl == null || item.ProfileUrl == null)
{
var data = Stores.LoadIllustPreloadData(item.Id, true, force: true);
if (data != null && data.illust.TryGetValue(item.Id, out var illust))
{
illust.CopyToItem(item);
if (data.user.TryGetValue(illust.userId, out var user))
{
item.ProfileUrl = user.image;
}
}
else
{
App.DebugError("load.favorite", $"cannot fetch preload data, {item.Id}, disposed.");
return false;
}
}
if (item.Image == null && item.ImageUrl != null)
{
var url = Configs.GetThumbnailUrl(item.ImageUrl);
item.Image = StyleDefinition.DownloadBackground;
var image = Stores.LoadThumbnailImage(url, true, force: true);
if (image != null)
{
item.Image = image;
}
}
if (item.ProfileImage == null && item.ProfileUrl != null)
{
item.ProfileImage = StyleDefinition.ProfileNone;
var userImage = Stores.LoadUserProfileImage(item.ProfileUrl, true, force: true);
if (userImage != null)
{
item.ProfileImage = userImage;
}
}
return true;
},
complete: () =>
{
Stores.SaveFavoritesIllusts();
MainThread.BeginInvokeOnMainThread(() =>
{
CloseLoading(() =>
{
flag = false;
lastUpdated = default;
startIndex = -1;
StartLoad();
});
});
});
}
private async void ShareFavorites_Clicked(object sender, EventArgs e)
{
var file = Stores.FavoritesPath;
await Share.RequestAsync(new ShareFileRequest
{
Title = ResourceHelper.Favorites,
File = new ShareFile(file)
});
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<i:IllustDataCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i="clr-namespace:Gallery.Illust"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:r="clr-namespace:Gallery.Resources"
x:Class="Gallery.Illust.MainPage"
BackgroundColor="{DynamicResource WindowColor}"
Title="{r:Text Follow}">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconRefresh}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"
HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<StackLayout>
<ActivityIndicator x:Name="activityLoading" Margin="0, 10, 0, 0"
HeightRequest="40"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
<u:FlowLayout ItemsSource="{Binding Illusts}"
HorizontalOptions="Fill" Column="{Binding Columns}"
RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}">
<u:FlowLayout.Margin>
<OnPlatform x:TypeArguments="Thickness"
iOS="16, 66, 16, 16"
Android="16, 56, 16, 16"/>
</u:FlowLayout.Margin>
</u:FlowLayout>
</StackLayout>
</ScrollView>
<u:BlurryPanel x:Name="panelBar" VerticalOptions="Start"
HeightRequest="60"/>
<SearchBar x:Name="searchBar" Placeholder="{r:Text Search}"
HeightRequest="40"
VerticalOptions="Start"
CancelButtonColor="{DynamicResource TintColor}"
Text="{Binding Keywords, Mode=TwoWay}"
SearchButtonPressed="SearchBar_SearchButtonPressed"/>
</Grid>
</i:IllustDataCollectionPage>

View File

@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Gallery.Utils;
using Xamarin.Forms;
namespace Gallery.Illust
{
public partial class MainPage : IllustDataCollectionPage
{
public static readonly BindableProperty KeywordsProperty = BindableProperty.Create(
nameof(Keywords), typeof(string), typeof(MainPage));
public string Keywords
{
get => (string)GetValue(KeywordsProperty);
set => SetValue(KeywordsProperty, value);
}
private double lastScrollY = double.MinValue;
private ScrollDirection scrollDirection = ScrollDirection.Stop;
public MainPage()
{
Resources.Add("cardView", GetCardViewTemplate());
InitializeComponent();
#if __IOS__
searchBar.BackgroundColor = Color.Transparent;
#else
searchBar.SetDynamicResource(SearchBar.TextColorProperty, UI.Theme.ThemeBase.TextColor);
searchBar.SetDynamicResource(SearchBar.PlaceholderColorProperty, UI.Theme.ThemeBase.SubTextColor);
searchBar.SetDynamicResource(BackgroundColorProperty, UI.Theme.ThemeBase.WindowColor);
#endif
}
protected override bool IsRedirectLogin => true;
protected override bool NeedCookie => true;
protected override ActivityIndicator LoadingIndicator => activityLoading;
protected override double IndicatorMarginTop => 66;
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
searchBar.Margin = new Thickness(0, PageTopMargin.Top + 8, 0, 0);
panelBar.Margin = PanelTopMargin;
}
#if __IOS__
public override void OnOrientationChanged(bool landscape)
{
base.OnOrientationChanged(landscape);
AnimateToMargin(searchBar, new Thickness(0, PageTopMargin.Top + 8, 0, 0));
AnimateToMargin(panelBar, PanelTopMargin);
}
#endif
protected override IEnumerable<IllustItem> DoGetIllustList(IllustData data, out int tag)
{
tag = 0;
if (data.body == null)
{
return null;
}
var illusts = data.body.page.follow.Select(i =>
data.body.thumbnails.illust.FirstOrDefault(l => (l.illustId ?? l.id) == i.ToString())?.ConvertToItem());
return illusts;
}
protected override IllustData DoLoadIllustData(bool force)
{
return Stores.LoadIllustData(force);
}
private async void Refresh_Clicked(object sender, EventArgs e)
{
if (IsLoading)
{
return;
}
await ScrollToTopAsync(scrollView);
StartLoad(true);
}
private void SearchBar_SearchButtonPressed(object sender, EventArgs e)
{
var key = Keywords;
if (key != null)
{
key = key.Trim().ToLower();
}
if (!string.IsNullOrEmpty(key))
{
Task.Run(() => App.OpenUrl(new Uri("gallery://example.com/artworks/" + key)));
}
}
private const int searchBarHeight = 60;
private void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
{
var y = e.ScrollY;
if (y > lastScrollY)
{
// down
if (scrollDirection != ScrollDirection.Down && y > searchBarHeight - topOffset)
{
scrollDirection = ScrollDirection.Down;
if (searchBar.IsFocused)
{
searchBar.Unfocus();
}
ViewExtensions.CancelAnimations(searchBar);
ViewExtensions.CancelAnimations(panelBar);
searchBar.TranslateTo(0, -searchBarHeight, easing: Easing.CubicIn);
panelBar.TranslateTo(0, -searchBarHeight, easing: Easing.CubicIn);
}
}
else
{
// up
if (scrollDirection != ScrollDirection.Up)
{
scrollDirection = ScrollDirection.Up;
ViewExtensions.CancelAnimations(searchBar);
ViewExtensions.CancelAnimations(panelBar);
searchBar.TranslateTo(0, 0, easing: Easing.CubicOut);
panelBar.TranslateTo(0, 0, easing: Easing.CubicOut);
}
}
lastScrollY = y;
}
}
}

116
Gallery/Illust/RankingPage.xaml Executable file
View File

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<i:IllustRankingDataCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i="clr-namespace:Gallery.Illust"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:r="clr-namespace:Gallery.Resources"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
x:Class="Gallery.Illust.RankingPage"
BackgroundColor="{DynamicResource WindowColor}"
Title="{r:Text Ranking}">
<Shell.TitleView>
<Grid VerticalOptions="Fill" ColumnSpacing="6"
HorizontalOptions="{x:OnPlatform Android=Start, iOS=Fill}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform Android=Auto}"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackLayout Grid.Column="1" Orientation="Horizontal" Spacing="6">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
</StackLayout.GestureRecognizers>
<Label Text="{Binding InternalTitle}"
TextColor="{DynamicResource TextColor}"
FontSize="{OnPlatform Android=18}"
LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center" FontAttributes="Bold"/>
<Label x:Name="labelCaret"
Text="{DynamicResource IconCaretDown}"
TextColor="{DynamicResource TextColor}"
FontFamily="{DynamicResource IconSolidFontFamily}"
FontSize="Small"
VerticalTextAlignment="Center"/>
</StackLayout>
</Grid>
</Shell.TitleView>
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary"
Command="{Binding ToolbarCommand}" CommandParameter="prev"
IconImageSource="{DynamicResource FontIconCaretCircleLeft}"/>
<ToolbarItem Order="Primary"
Command="{Binding ToolbarCommand}" CommandParameter="select"
IconImageSource="{DynamicResource FontIconCalendarDay}"/>
<ToolbarItem Order="Primary"
Command="{Binding ToolbarCommand}" CommandParameter="next"
IconImageSource="{DynamicResource FontIconCaretCircleRight}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"
HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<StackLayout>
<ActivityIndicator x:Name="activityLoading" Margin="0, -40, 0, 0"
HeightRequest="40"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
<u:FlowLayout ItemsSource="{Binding Illusts}" MaxHeightChanged="FlowLayout_MaxHeightChanged"
HorizontalOptions="Fill" Column="{Binding Columns}"
Margin="16" RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}"/>
<ActivityIndicator x:Name="activityBottomLoading" Margin="0, -10, 0, 16"
IsRunning="{Binding IsBottomLoading}"
IsVisible="{Binding IsBottomLoading}"/>
</StackLayout>
</ScrollView>
<u:BlurryPanel x:Name="panelFilter" VerticalOptions="Start" Opacity="0"
Margin="{Binding PanelTopMargin}"
BackgroundColor="{DynamicResource WindowColor}"
HeightRequest="{Binding Height, Source={x:Reference gridFilter}}"/>
<Grid x:Name="gridFilter" VerticalOptions="Start" Opacity="0"
Margin="{Binding PageTopMargin}" Padding="10">
<Grid RowSpacing="0" HorizontalOptions="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<u:SegmentedControl Margin="6, 6, 6, 3" VerticalOptions="Center"
SelectedSegmentIndex="{Binding SegmentDate, Mode=TwoWay}"
SelectedTextColor="{DynamicResource TextColor}"
TintColor="{DynamicResource CardBackgroundColor}">
<u:SegmentedControl.Children>
<u:SegmentedControlOption Text="{r:Text Daily}"/>
<u:SegmentedControlOption Text="{r:Text Weekly}"/>
<u:SegmentedControlOption Text="{r:Text Monthly}"/>
<u:SegmentedControlOption Text="{r:Text Male}"/>
</u:SegmentedControl.Children>
</u:SegmentedControl>
<u:SegmentedControl Grid.Row="1" HorizontalOptions="Start"
IsVisible="{Binding SegmentTypeVisible}"
Margin="6, 3, 6, 6" VerticalOptions="Center"
SelectedSegmentIndex="{Binding SegmentType, Mode=TwoWay}"
SelectedTextColor="{DynamicResource TextColor}"
TintColor="{DynamicResource CardBackgroundColor}">
<u:SegmentedControl.Children>
<u:SegmentedControlOption Text="{r:Text General}"/>
<u:SegmentedControlOption Text="{r:Text R18}"/>
</u:SegmentedControl.Children>
</u:SegmentedControl>
<Button Grid.Row="1" HorizontalOptions="End" VerticalOptions="Center"
Text="{DynamicResource IconCircleCheck}"
FontFamily="{DynamicResource IconSolidFontFamily}"
BackgroundColor="Transparent"
TextColor="{DynamicResource TintColor}"
FontSize="20" Margin="0, 0, 6, 0"
Clicked="Filter_Clicked"/>
</Grid>
</Grid>
<DatePicker x:Name="datePicker" IsVisible="False"
ios:DatePicker.UpdateMode="WhenFinished"
Date="{Binding SelectedDate}"
MaximumDate="{Binding MaximumDate}"
Focused="DatePicker_Focused"
Unfocused="DatePicker_Focused"
DateSelected="DatePicker_DateSelected"/>
</Grid>
</i:IllustRankingDataCollectionPage>

View File

@ -0,0 +1,406 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Gallery.Resources;
using Gallery.UI;
using Gallery.Utils;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.Illust
{
public partial class RankingPage : IllustRankingDataCollectionPage
{
private static readonly string[] segmentDates = { "daily", "weekly", "monthly", "male" };
public static readonly BindableProperty InternalTitleProperty = BindableProperty.Create(
nameof(InternalTitle), typeof(string), typeof(RankingPage));
public string InternalTitle
{
get => (string)GetValue(InternalTitleProperty);
set => SetValue(InternalTitleProperty, value);
}
public static readonly BindableProperty SegmentDateProperty = BindableProperty.Create(
nameof(SegmentDate), typeof(int), typeof(RankingPage), propertyChanged: OnSegmentDatePropertyChanged);
public static readonly BindableProperty SegmentTypeVisibleProperty = BindableProperty.Create(
nameof(SegmentTypeVisible), typeof(bool), typeof(RankingPage));
public static readonly BindableProperty SegmentTypeProperty = BindableProperty.Create(
nameof(SegmentType), typeof(int), typeof(RankingPage), propertyChanged: OnSegmentTypePropertyChanged);
public static readonly BindableProperty MaximumDateProperty = BindableProperty.Create(
nameof(MaximumDate), typeof(DateTime), typeof(RankingPage), DateTime.Today);
public static readonly BindableProperty SelectedDateProperty = BindableProperty.Create(
nameof(SelectedDate), typeof(DateTime), typeof(RankingPage), DateTime.Today);
private static void OnSegmentDatePropertyChanged(BindableObject obj, object old, object @new)
{
var page = (RankingPage)obj;
var index = (int)@new;
if (index == 2)
{
// monthly
var typeIndex = page.SegmentType;
if (typeIndex == 1)
{
// r-18
page.SegmentType = 0;
}
}
}
private static void OnSegmentTypePropertyChanged(BindableObject obj, object old, object @new)
{
var page = (RankingPage)obj;
var index = (int)@new;
if (index == 1)
{
// r-18
var dateIndex = page.SegmentDate;
if (dateIndex == 2)
{
// monthly
page.SegmentDate = 0;
}
}
}
public int SegmentDate
{
get => (int)GetValue(SegmentDateProperty);
set => SetValue(SegmentDateProperty, value);
}
public bool SegmentTypeVisible
{
get => (bool)GetValue(SegmentTypeVisibleProperty);
set => SetValue(SegmentTypeVisibleProperty, value);
}
public int SegmentType
{
get => (int)GetValue(SegmentTypeProperty);
set => SetValue(SegmentTypeProperty, value);
}
public DateTime MaximumDate
{
get => (DateTime)GetValue(MaximumDateProperty);
set => SetValue(MaximumDateProperty, value);
}
public DateTime SelectedDate
{
get => (DateTime)GetValue(SelectedDateProperty);
set => SetValue(SelectedDateProperty, value);
}
public Command<string> ToolbarCommand { get; private set; }
private bool previousEnabled;
private bool dateEnabled;
private bool nextEnabled;
private bool isFilterVisible;
private bool isDatePickerVisible;
private string lastQueryKey;
private string queryDate;
private string previousDate;
private string nextDate;
private int currentPage;
private int nextPage;
private string QueryKey => segmentDates[SegmentDate] + (SegmentType == 1 ? "_r18" : string.Empty);
public RankingPage()
{
Resources.Add("cardView", GetCardViewTemplate(titleBinding: nameof(IllustItem.RankTitle)));
ToolbarCommand = new Command<string>(OnDateTrigger, OnCanDateTrigger);
InitializeComponent();
gridFilter.TranslationY = -100;
panelFilter.TranslationY = -100;
// preferences
SegmentDate = Preferences.Get(Configs.QueryModeKey, 0);
SegmentType = Preferences.Get(Configs.QueryTypeKey, 0);
lastQueryKey = QueryKey;
queryDate = Preferences.Get(Configs.QueryDateKey, null);
currentPage = 1;
datePicker.MinimumDate = new DateTime(2007, 9, 13);
MaximumDate = DateTime.Today.AddDays(-1);
}
protected override ActivityIndicator LoadingIndicator => activityLoading;
protected override void OnAppearing()
{
var r18 = Configs.IsOnR18;
if (!r18)
{
SegmentType = 0;
var query = QueryKey;
if (lastQueryKey != query)
{
ReleaseCollection();
lastQueryKey = query;
}
}
SegmentTypeVisible = r18;
if (currentPage != 1 && Illusts == null)
{
currentPage = 1;
}
base.OnAppearing();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
// saving state
Preferences.Set(Configs.QueryModeKey, SegmentDate);
Preferences.Set(Configs.QueryTypeKey, SegmentType);
if (queryDate != null)
{
Preferences.Set(Configs.QueryDateKey, queryDate);
}
}
protected override IEnumerable<IllustItem> DoGetIllustList(IllustRankingData data, out int tag)
{
tag = currentPage;
if (lastQueryKey != null && lastQueryKey.StartsWith(segmentDates[3]))
{
return data.contents.Where(i => i.illust_type == "0").Select(i => i.ConvertToItem());
}
return data.contents.Select(i => i.ConvertToItem());
}
protected override IllustRankingData DoLoadIllustData(bool force)
{
var data = Stores.LoadIllustRankingData(lastQueryKey, queryDate, currentPage, out lastError, force);
if (data != null)
{
if (int.TryParse(data.next, out int next))
{
nextPage = next;
}
var date = data.date;
DateTime now;
if (date.Length == 8 && int.TryParse(date, out _))
{
queryDate = date;
now = new DateTime(
int.Parse(date.Substring(0, 4)),
int.Parse(date.Substring(4, 2)),
int.Parse(date.Substring(6, 2)));
SelectedDate = now;
//date = now.ToShortDateString();
date = now.ToString("yyyy-MM-dd");
}
else
{
now = default;
}
date = ResourceHelper.GetResource(data.mode, date);
MainThread.BeginInvokeOnMainThread(() => InternalTitle = date);
var prev_date = data.prev_date;
if (int.TryParse(prev_date, out _))
{
previousDate = prev_date;
previousEnabled = true;
}
else
{
previousDate = null;
previousEnabled = false;
}
var next_date = data.next_date;
if (int.TryParse(next_date, out _))
{
nextDate = next_date;
nextEnabled = true;
}
else
{
nextDate = null;
nextEnabled = false;
if (now != default && force)
{
MaximumDate = now;
}
}
}
dateEnabled = true;
MainThread.BeginInvokeOnMainThread(ToolbarCommand.ChangeCanExecute);
return data;
}
private async void ToggleFilterPanel(bool flag)
{
ViewExtensions.CancelAnimations(gridFilter);
ViewExtensions.CancelAnimations(panelFilter);
if (flag)
{
isFilterVisible = true;
if (scrollDirection == ScrollDirection.Down)
{
// stop the scrolling
await scrollView.ScrollToAsync(scrollView.ScrollX, scrollView.ScrollY, false);
}
await Task.WhenAll(
labelCaret.RotateTo(180, easing: Easing.CubicOut),
gridFilter.TranslateTo(0, 0, easing: Easing.CubicOut),
gridFilter.FadeTo(1, easing: Easing.CubicOut),
panelFilter.TranslateTo(0, 0, easing: Easing.CubicOut),
panelFilter.FadeTo(1, easing: Easing.CubicOut)
);
}
else
{
isFilterVisible = false;
await Task.WhenAll(
labelCaret.RotateTo(0, easing: Easing.CubicIn),
gridFilter.TranslateTo(0, -100, easing: Easing.CubicIn),
gridFilter.FadeTo(0, easing: Easing.CubicIn),
panelFilter.TranslateTo(0, -100, easing: Easing.CubicIn),
panelFilter.FadeTo(0, easing: Easing.CubicIn)
);
}
}
private async void OnDateTrigger(string action)
{
if (IsLoading)
{
return;
}
if (action == "select")
{
// stop the scrolling
await scrollView.ScrollToAsync(scrollView.ScrollX, scrollView.ScrollY, false);
datePicker.Focus();
}
else
{
var date = action == "prev" ? previousDate : nextDate;
if (date == null)
{
return;
}
if (isFilterVisible)
{
ToggleFilterPanel(false);
}
queryDate = date;
PrepareLoad();
}
}
private bool OnCanDateTrigger(string action)
{
switch (action)
{
case "prev":
return previousEnabled;
case "next":
return nextEnabled;
default:
return dateEnabled;
}
}
private void DatePicker_Focused(object sender, FocusEventArgs e)
{
isDatePickerVisible = e.IsFocused;
}
private void DatePicker_DateSelected(object sender, DateChangedEventArgs e)
{
if (e.OldDate == DateTime.Today || IsLoading)
{
// first load or loading
return;
}
queryDate = e.NewDate.ToString("yyyyMMdd");
PrepareLoad();
}
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
ToggleFilterPanel(!isFilterVisible);
}
private void FlowLayout_MaxHeightChanged(object sender, HeightEventArgs e)
{
SetOffset(e.ContentHeight - scrollView.Bounds.Height - SCROLL_OFFSET);
}
protected override bool CheckRefresh()
{
if (nextPage == currentPage + 1)
{
currentPage = nextPage;
#if DEBUG
App.DebugPrint($"loading page {nextPage}");
#endif
return true;
}
return false;
}
private void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
{
var y = e.ScrollY;
if (IsScrollingDown(y))
{
// down
if (isFilterVisible)
{
ToggleFilterPanel(false);
}
}
if (isDatePickerVisible)
{
isDatePickerVisible = false;
datePicker.Unfocus();
}
OnScrolled(y);
}
private void Filter_Clicked(object sender, EventArgs e)
{
var query = QueryKey;
ToggleFilterPanel(false);
if (IsLoading)
{
return;
}
//if (lastQueryKey != query)
{
// query changed.
lastQueryKey = query;
#if DEBUG
App.DebugPrint($"query changed: {query}");
#endif
PrepareLoad();
}
}
private async void PrepareLoad()
{
await ScrollToTopAsync(scrollView);
// release
currentPage = 1;
previousEnabled = false;
dateEnabled = false;
nextEnabled = false;
ToolbarCommand.ChangeCanExecute();
StartLoad(true);
}
private void ReleaseCollection()
{
currentPage = 1;
InvalidateCollection();
}
}
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<i:IllustDataCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i="clr-namespace:Gallery.Illust"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:r="clr-namespace:Gallery.Resources"
x:Class="Gallery.Illust.RecommendsPage"
BackgroundColor="{DynamicResource WindowColor}"
Title="{r:Text Recommends}">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconRefresh}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView x:Name="scrollView" HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<StackLayout>
<ActivityIndicator x:Name="activityLoading" Margin="0, -40, 0, 0"
HeightRequest="40"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
<u:FlowLayout ItemsSource="{Binding Users}" IsVisible="{Binding UserRecommendsVisible}"
HorizontalOptions="Fill" Column="{Binding UserColumns}"
Margin="16" RowSpacing="16"
ItemTemplate="{StaticResource userCardView}"/>
<u:FlowLayout ItemsSource="{Binding Illusts}"
HorizontalOptions="Fill" Column="{Binding Columns}"
Margin="16" RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}"/>
</StackLayout>
</ScrollView>
</Grid>
</i:IllustDataCollectionPage>

View File

@ -0,0 +1,369 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Gallery.UI;
using Gallery.UI.Theme;
using Gallery.Utils;
using Xamarin.Forms;
namespace Gallery.Illust
{
public partial class RecommendsPage : IllustDataCollectionPage
{
public static readonly BindableProperty UsersProperty = BindableProperty.Create(
nameof(Users), typeof(List<IllustUserItem>), typeof(RecommendsPage));
public static readonly BindableProperty UserColumnsProperty = BindableProperty.Create(
nameof(UserColumns), typeof(int), typeof(RecommendsPage), 1);
public static readonly BindableProperty UserRecommendsVisibleProperty = BindableProperty.Create(
nameof(UserRecommendsVisible), typeof(bool), typeof(RecommendsPage));
public List<IllustUserItem> Users
{
get => (List<IllustUserItem>)GetValue(UsersProperty);
set => SetValue(UsersProperty, value);
}
public int UserColumns
{
get => (int)GetValue(UserColumnsProperty);
set => SetValue(UserColumnsProperty, value);
}
public bool UserRecommendsVisible
{
get => (bool)GetValue(UserRecommendsVisibleProperty);
set => SetValue(UserRecommendsVisibleProperty, value);
}
private IllustData illustData;
public RecommendsPage()
{
Resources.Add("cardView", GetCardViewTemplate());
Resources.Add("userCardView", GetUserCardViewTemplate());
InitializeComponent();
}
protected override bool NeedCookie => true;
protected override ActivityIndicator LoadingIndicator => activityLoading;
public override void OnUnload()
{
base.OnUnload();
Users = null;
}
private Image GetUserCardViewImage(int column, CornerMask masks, string source, string parameter)
{
Image image;
if (masks == CornerMask.None)
{
image = new Image();
}
else
{
image = new RoundImage
{
CornerRadius = 10,
CornerMasks = masks
};
}
image.HorizontalOptions = LayoutOptions.Fill;
image.Aspect = Aspect.AspectFill;
image.GestureRecognizers.Add(new TapGestureRecognizer
{
Command = commandIllustImageTapped
}
.Binding(TapGestureRecognizer.CommandParameterProperty, parameter));
image.SetBinding(Image.SourceProperty, source);
if (column > 0)
{
Grid.SetColumn(image, column);
}
Grid.SetRow(image, 1);
return image;
}
private DataTemplate GetUserCardViewTemplate()
{
return new DataTemplate(() =>
{
return new Grid
{
RowSpacing = 0,
ColumnSpacing = 0,
RowDefinitions =
{
new RowDefinition { Height = 40 },
new RowDefinition { Height = 120 }
},
ColumnDefinitions =
{
new ColumnDefinition(),
new ColumnDefinition(),
new ColumnDefinition()
},
Children =
{
// stacklayout: user
new Grid
{
ColumnDefinitions =
{
new ColumnDefinition { Width = 30 },
new ColumnDefinition()
},
Padding = new Thickness(8, 0, 8, 8),
Children =
{
// user icon
new CircleImage
{
WidthRequest = 30,
HeightRequest = 30,
Aspect = Aspect.AspectFill
}
.Binding(Image.SourceProperty, nameof(IllustUserItem.ProfileImage)),
// user name
new Label
{
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.Center,
LineBreakMode = LineBreakMode.TailTruncation,
FontSize = StyleDefinition.FontSizeSmall
}
.Binding(Label.TextProperty, nameof(IllustUserItem.UserName))
.DynamicResource(Label.TextColorProperty, ThemeBase.SubTextColor)
.GridColumn(1),
},
GestureRecognizers =
{
new TapGestureRecognizer
{
Command = commandUserTapped
}
.Binding(TapGestureRecognizer.CommandParameterProperty, ".")
}
}
.GridColumnSpan(3),
GetUserCardViewImage(0, CornerMask.Left,
$"{nameof(IllustUserItem.Image1Item)}.{nameof(IllustItem.Image)}",
nameof(IllustUserItem.Image1Item)),
GetUserCardViewImage(1, CornerMask.None,
$"{nameof(IllustUserItem.Image2Item)}.{nameof(IllustItem.Image)}",
nameof(IllustUserItem.Image2Item)),
GetUserCardViewImage(2, CornerMask.Right,
$"{nameof(IllustUserItem.Image3Item)}.{nameof(IllustItem.Image)}",
nameof(IllustUserItem.Image3Item))
}
};
});
}
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
int columns;
if (width > height)
{
columns = isPhone ? 2 : 3;
}
else
{
columns = isPhone ? 1 : 2;
}
if (UserColumns != columns)
{
UserColumns = columns;
#if DEBUG
App.DebugPrint($"change user columns to {columns}");
#endif
}
}
protected override void DoIllustsLoaded(IllustCollection collection, bool bottom)
{
//IsLoading = false;
if (illustData != null)
{
IllustCollection = collection;
Task.Run(() => DoLoadUserRecommendsData(illustData));
}
else
{
base.DoIllustsLoaded(collection, bottom);
}
}
protected override IEnumerable<IllustItem> DoGetIllustList(IllustData data, out int tag)
{
tag = 0;
if (data.body == null)
{
return null;
}
return data.body.page.recommend.ids.Select(id =>
data.body.thumbnails.illust.FirstOrDefault(l => (l.illustId ?? l.id) == id)?.ConvertToItem());
}
protected override IllustData DoLoadIllustData(bool force)
{
illustData = Stores.LoadIllustData(force);
return illustData;
}
private void DoLoadUserRecommendsData(IllustData data)
{
var r18 = Configs.IsOnR18;
var defaultImage = StyleDefinition.DownloadBackground;
var users = data.body.page.recommendUser.Select(u =>
{
var usrId = u.id.ToString();
var usr = data.body.users.FirstOrDefault(r => r.userId == usrId);
if (usr == null)
{
return null;
}
IllustItem item1 = null, item2 = null, item3 = null;
if (u.illustIds != null)
{
var length = u.illustIds.Length;
if (length > 0)
{
var id = u.illustIds[0];
item1 = data.body.thumbnails.illust.FirstOrDefault(l => (l.illustId ?? l.id) == id)?.ConvertToItem(defaultImage);
}
if (length > 1)
{
var id = u.illustIds[1];
item2 = data.body.thumbnails.illust.FirstOrDefault(l => (l.illustId ?? l.id) == id)?.ConvertToItem(defaultImage);
}
if (length > 2)
{
var id = u.illustIds[2];
item3 = data.body.thumbnails.illust.FirstOrDefault(l => (l.illustId ?? l.id) == id)?.ConvertToItem(defaultImage);
}
}
if (!r18)
{
if (item1?.IsRestrict == true ||
item2?.IsRestrict == true ||
item3?.IsRestrict == true)
{
return null;
}
}
return new IllustUserItem
{
UserId = usrId,
UserName = usr.name,
ProfileUrl = usr.image,
Image1Item = item1,
Image2Item = item2,
Image3Item = item3
};
}).Where(u => u != null);
var list = new List<IllustUserItem>(users);
activityLoading.Animate("margin", top =>
{
activityLoading.Margin = new Thickness(0, top, 0, 0);
},
16, -40, easing: Easing.CubicIn, finished: (v, r) =>
{
IsLoading = false;
UserRecommendsVisible = list.Count > 0;
#if __IOS__
Device.StartTimer(TimeSpan.FromMilliseconds(48), () =>
#else
Device.StartTimer(TimeSpan.FromMilliseconds(150), () =>
#endif
{
Users = list;
Illusts = IllustCollection;
return false;
});
});
DoLoadUserRecommendsImages(list);
}
private void DoLoadUserRecommendsImages(List<IllustUserItem> users)
{
foreach (var user in users)
{
if (user.ProfileUrl != null)
{
var userImage = Stores.LoadUserProfileImage(user.ProfileUrl, true);
if (userImage != null)
{
user.ProfileImage = userImage;
}
}
Task.WaitAll(
Task.Run(() => DoLoadUserRecommendsImage(user.Image1Item)),
Task.Run(() => DoLoadUserRecommendsImage(user.Image2Item)),
Task.Run(() => DoLoadUserRecommendsImage(user.Image3Item)));
}
}
private void DoLoadUserRecommendsImage(IllustItem illust)
{
if (illust.ImageUrl != null)
{
var url = Configs.GetThumbnailUrl(illust.ImageUrl);
var image = Stores.LoadPreviewImage(url, false);
if (image == null)
{
image = Stores.LoadThumbnailImage(url, true);
}
if (image != null)
{
illust.Image = image;
}
}
}
private async void Refresh_Clicked(object sender, EventArgs e)
{
if (IsLoading)
{
return;
}
await ScrollToTopAsync(scrollView);
StartLoad(true);
}
}
public class IllustUserItem : BindableObject, IIllustItem
{
//public static readonly BindableProperty Image1Property = BindableProperty.Create(
// nameof(Image1), typeof(ImageSource), typeof(IllustUserItem));
public static readonly BindableProperty ProfileImageProperty = BindableProperty.Create(
nameof(ProfileImage), typeof(ImageSource), typeof(IllustUserItem));
//public ImageSource Image1
//{
// get => (ImageSource)GetValue(Image1Property);
// set => SetValue(Image1Property, value);
//}
public ImageSource ProfileImage
{
get => (ImageSource)GetValue(ProfileImageProperty);
set => SetValue(ProfileImageProperty, value);
}
public string UserId { get; set; }
public string UserName { get; set; }
public IllustItem Image1Item { get; set; }
public IllustItem Image2Item { get; set; }
public IllustItem Image3Item { get; set; }
public string ProfileUrl { get; set; }
public bool IsFavorite { get; }
public string BookmarkId { get; }
}
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<i:IllustRecommendsCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i="clr-namespace:Gallery.Illust"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:r="clr-namespace:Gallery.Resources"
x:Class="Gallery.Illust.RelatedIllustsPage"
Title="{r:Text RelatedIllusts}"
BackgroundColor="{DynamicResource WindowColor}">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconRefresh}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"
HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<StackLayout>
<ActivityIndicator x:Name="activityLoading" Margin="0, -40, 0, 0"
HeightRequest="40"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
<u:FlowLayout ItemsSource="{Binding Illusts}" MaxHeightChanged="FlowLayout_MaxHeightChanged"
HorizontalOptions="Fill" Column="{Binding Columns}"
Margin="16" RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}"/>
<ActivityIndicator x:Name="activityBottomLoading" Margin="0, -10, 0, 16"
IsRunning="{Binding IsBottomLoading}"
IsVisible="{Binding IsBottomLoading}"/>
</StackLayout>
</ScrollView>
</Grid>
</i:IllustRecommendsCollectionPage>

View File

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gallery.UI;
using Gallery.Utils;
using Xamarin.Forms;
namespace Gallery.Illust
{
public partial class RelatedIllustsPage : IllustRecommendsCollectionPage
{
private const int STEP = 18;
private readonly IllustItem illustItem;
private int startIndex;
private int nextIndex;
private string[] illustIds;
public RelatedIllustsPage(IllustItem item)
{
illustItem = item;
Resources.Add("cardView", GetCardViewTemplate());
InitializeComponent();
startIndex = -1;
nextIndex = 0;
}
protected override void OnAppearing()
{
if (startIndex != -1 && Illusts == null)
{
startIndex = -1;
}
base.OnAppearing();
}
protected override bool IsAutoReload => false;
protected override ActivityIndicator LoadingIndicator => activityLoading;
protected override IEnumerable<IllustItem> DoGetIllustList(IllustRecommendsData data, out int tag)
{
tag = startIndex;
if (data.body == null)
{
return null;
}
return data.body.illusts.Where(i => i.url != null).Select(i => i.ConvertToItem());
}
protected override IllustRecommendsData DoLoadIllustData(bool force)
{
IllustRecommendsData data;
if (startIndex < 0)
{
// init
data = Stores.LoadIllustRecommendsInitData(illustItem.Id);
if (data == null || data.body == null)
{
return null;
}
illustIds = data.body.nextIds;
}
else
{
if (illustIds == null || startIndex >= illustIds.Length)
{
return null;
}
var ids = illustIds.Skip(startIndex).Take(STEP).ToArray();
data = Stores.LoadIllustRecommendsListData(illustItem.Id, ids);
nextIndex = startIndex + STEP;
if (ids.Length == 0 || nextIndex >= illustIds.Length)
{
// done
#if DEBUG
App.DebugPrint($"download completed: {startIndex}");
#endif
startIndex = nextIndex;
}
}
return data;
}
private void FlowLayout_MaxHeightChanged(object sender, HeightEventArgs e)
{
SetOffset(e.ContentHeight - scrollView.Bounds.Height - SCROLL_OFFSET);
}
protected override bool CheckRefresh()
{
if (nextIndex > startIndex)
{
startIndex = nextIndex;
return true;
}
return false;
}
private void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
{
var y = e.ScrollY;
OnScrolled(y);
}
private async void Refresh_Clicked(object sender, EventArgs e)
{
if (IsLoading)
{
return;
}
await ScrollToTopAsync(scrollView);
startIndex = -1;
nextIndex = 0;
illustIds = null;
StartLoad(true);
}
}
}

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<i:IllustUserDataCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i="clr-namespace:Gallery.Illust"
xmlns:u="clr-namespace:Gallery.UI"
x:Class="Gallery.Illust.UserIllustPage"
BackgroundColor="{DynamicResource WindowColor}">
<Shell.TitleView>
<StackLayout Orientation="Horizontal">
<u:CircleImage Aspect="AspectFill" Source="{Binding UserIcon}"
VerticalOptions="Center"
WidthRequest="34" HeightRequest="34" >
<u:CircleImage.Margin>
<OnPlatform x:TypeArguments="Thickness" Android="0, 5, 0, 5"/>
</u:CircleImage.Margin>
</u:CircleImage>
<Label Text="{Binding Title}" Margin="10, 0, 0, 0"
VerticalOptions="Center" LineBreakMode="TailTruncation"
TextColor="{DynamicResource TextColor}"/>
</StackLayout>
</Shell.TitleView>
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconRefresh}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"
HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<StackLayout>
<ActivityIndicator x:Name="activityLoading" Margin="0, -40, 0, 0"
HeightRequest="40"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
<u:FlowLayout ItemsSource="{Binding Illusts}" MaxHeightChanged="FlowLayout_MaxHeightChanged"
HorizontalOptions="Fill" Column="{Binding Columns}"
Margin="16" RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}"/>
<ActivityIndicator x:Name="activityBottomLoading" Margin="0, -10, 0, 16"
IsRunning="{Binding IsBottomLoading}"
IsVisible="{Binding IsBottomLoading}"/>
</StackLayout>
</ScrollView>
</Grid>
</i:IllustUserDataCollectionPage>

View File

@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gallery.UI;
using Gallery.Utils;
using Xamarin.Forms;
namespace Gallery.Illust
{
public partial class UserIllustPage : IllustUserDataCollectionPage
{
private const int STEP = 48;
public static readonly BindableProperty UserIconProperty = BindableProperty.Create(
nameof(UserIcon), typeof(ImageSource), typeof(UserIllustPage));
public ImageSource UserIcon
{
get => (ImageSource)GetValue(UserIconProperty);
set => SetValue(UserIconProperty, value);
}
public IIllustItem UserItem { get; }
private int startIndex;
private int nextIndex;
private string[] illustIds;
public UserIllustPage(IIllustItem item)
{
UserItem = item;
UserIcon = item.ProfileImage;
Title = item.UserName;
Resources.Add("cardView", GetCardViewTemplate(true));
InitializeComponent();
startIndex = -1;
nextIndex = 0;
}
protected override void OnAppearing()
{
if (startIndex != -1 && Illusts == null)
{
startIndex = -1;
}
base.OnAppearing();
}
protected override bool IsAutoReload => false;
protected override ActivityIndicator LoadingIndicator => activityLoading;
protected override IEnumerable<IllustItem> DoGetIllustList(IllustUserData data, out int tag)
{
tag = startIndex;
if (data.body == null)
{
return null;
}
return data.body.works.Select(i => i.Value?.ConvertToItem());
}
protected override IllustUserData DoLoadIllustData(bool force)
{
var userId = UserItem.UserId;
if (startIndex < 0)
{
var init = Stores.LoadIllustUserInitData(userId);
if (init == null || init.body == null)
{
return null;
}
illustIds = init.body.illusts.Keys.OrderByDescending(i => int.TryParse(i, out int id) ? id : -1).ToArray();
#if DEBUG
App.DebugPrint($"user has ({illustIds.Length}) illusts.");
#endif
startIndex = 0;
}
if (illustIds == null || startIndex >= illustIds.Length)
{
return null;
}
var ids = illustIds.Skip(startIndex).Take(STEP).ToArray();
var data = Stores.LoadIllustUserData(userId, ids, startIndex == 0);
nextIndex = startIndex + STEP;
if (ids.Length == 0 || nextIndex >= illustIds.Length)
{
// done
#if DEBUG
App.DebugPrint($"download completed: {startIndex}");
#endif
startIndex = nextIndex;
}
return data;
}
private void FlowLayout_MaxHeightChanged(object sender, HeightEventArgs e)
{
SetOffset(e.ContentHeight - scrollView.Bounds.Height - SCROLL_OFFSET);
}
protected override bool CheckRefresh()
{
if (nextIndex > startIndex)
{
startIndex = nextIndex;
return true;
}
return false;
}
private void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
{
var y = e.ScrollY;
OnScrolled(y);
}
private async void Refresh_Clicked(object sender, EventArgs e)
{
if (IsLoading)
{
return;
}
await ScrollToTopAsync(scrollView);
startIndex = -1;
nextIndex = 0;
illustIds = null;
StartLoad(true);
}
}
}

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<u:AdaptedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
x:Class="Gallery.Illust.ViewIllustPage"
ios:Page.UseSafeArea="False"
Shell.TabBarIsVisible="False"
BackgroundColor="{DynamicResource WindowColor}">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="Favorite_Clicked"
IconImageSource="{Binding FavoriteIcon}"/>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconRefresh}"/>
<ToolbarItem Order="Primary" Clicked="More_Clicked"
IconImageSource="{DynamicResource FontIconMore}"/>
</ContentPage.ToolbarItems>
<Grid RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<CarouselView ItemsSource="{Binding Illusts}" HorizontalScrollBarVisibility="Never"
Position="{Binding CurrentPage}"
ItemTemplate="{StaticResource carouselView}"
IsScrollAnimated="{Binding IsScrollAnimated}">
<CarouselView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="20"/>
</CarouselView.ItemsLayout>
</CarouselView>
<Grid Margin="{Binding PageTopMargin}" VerticalOptions="Start">
<ProgressBar x:Name="progress" IsVisible="{Binding ProgressVisible}"
Progress="0.05">
<ProgressBar.Margin>
<OnPlatform x:TypeArguments="Thickness" Android="0, -6, 0, 0"/>
</ProgressBar.Margin>
</ProgressBar>
</Grid>
<Grid Margin="{Binding PageTopMargin}" IsVisible="{Binding IsPageVisible}"
HorizontalOptions="End" VerticalOptions="Start">
<u:RoundLabel Text="{Binding PagePositionText}"
BackgroundColor="{DynamicResource MaskColor}" Margin="0, 6, 6, 0"
Padding="6, 4" CornerRadius="6"
FontSize="Micro" TextColor="White"/>
</Grid>
<u:RoundLabel Text="{Binding AnimeStatus}"
BackgroundColor="{DynamicResource MaskColor}" Margin="0, 0, 6, 6"
Padding="13, 12, 0, 0" CornerRadius="22"
WidthRequest="44" HeightRequest="44"
HorizontalOptions="End" VerticalOptions="End"
FontFamily="{DynamicResource IconSolidFontFamily}"
FontSize="20" TextColor="White"
IsVisible="{Binding IsAnimateSliderVisible}"/>
<Slider Grid.Row="1" VerticalOptions="End"
Margin="{DynamicResource ScreenBottomPadding}"
MinimumTrackColor="{DynamicResource TintColor}"
IsEnabled="{Binding IsAnimateSliderEnabled}"
IsVisible="{Binding IsAnimateSliderVisible}"
Value="{Binding CurrentAnimeFrame, Mode=TwoWay}"
Maximum="{Binding MaximumFrame}"/>
</Grid>
</u:AdaptedPage>

View File

@ -0,0 +1,897 @@
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<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)
{
#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<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; }
}
}