From 66f0c1ba1b3a31a325d18e7f7a4362b7a9f93d40 Mon Sep 17 00:00:00 2001
From: Tsanie Lily <tsorgy@gmail.com>
Date: Tue, 5 May 2020 01:53:33 +0800
Subject: [PATCH] * Pixiview/UI/CircleUIs.cs: *
 Pixiview.iOS/Pixiview.iOS.csproj: *
 Pixiview.iOS/Renderers/RoundLabelRenderer.cs: *
 Pixiview.iOS/Renderers/CircleImageRenderer.cs: custom round corner   controls

* Pixiview/App.xaml:
* Pixiview/Utils/Converters.cs:
* Pixiview/GlobalSuppressions.cs:
* Pixiview/UI/StyleDefinition.cs:

* Pixiview/UI/AdaptedPage.cs:
* Pixiview.iOS/Renderers/AdaptedPageRenderer.cs: observe orientation

* Pixiview/MainPage.xaml:
* Pixiview/Utils/Stores.cs:
* Pixiview/MainPage.xaml.cs:
* Pixiview/Utils/IllustData.cs: data and UI adjust
---
 Pixiview.iOS/Pixiview.iOS.csproj              |   2 +
 Pixiview.iOS/Renderers/AdaptedPageRenderer.cs |  36 ++++
 Pixiview.iOS/Renderers/CircleImageRenderer.cs |  31 ++++
 Pixiview.iOS/Renderers/RoundLabelRenderer.cs  |  47 ++++++
 Pixiview/App.xaml                             |   1 +
 Pixiview/GlobalSuppressions.cs                |   1 +
 Pixiview/MainPage.xaml                        |  59 ++++++-
 Pixiview/MainPage.xaml.cs                     | 154 +++++++++++++++++-
 Pixiview/UI/AdaptedPage.cs                    |  46 +++++-
 Pixiview/UI/CircleUIs.cs                      |  25 +++
 Pixiview/UI/StyleDefinition.cs                |  79 ++++++++-
 Pixiview/Utils/Converters.cs                  |  20 +++
 Pixiview/Utils/IllustData.cs                  |   2 +
 Pixiview/Utils/Stores.cs                      |  63 +++++--
 14 files changed, 536 insertions(+), 30 deletions(-)
 create mode 100644 Pixiview.iOS/Renderers/CircleImageRenderer.cs
 create mode 100644 Pixiview.iOS/Renderers/RoundLabelRenderer.cs
 create mode 100644 Pixiview/UI/CircleUIs.cs
 create mode 100644 Pixiview/Utils/Converters.cs

diff --git a/Pixiview.iOS/Pixiview.iOS.csproj b/Pixiview.iOS/Pixiview.iOS.csproj
index a297a95..a188d5a 100644
--- a/Pixiview.iOS/Pixiview.iOS.csproj
+++ b/Pixiview.iOS/Pixiview.iOS.csproj
@@ -70,6 +70,8 @@
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Renderers\AdaptedPageRenderer.cs" />
     <Compile Include="Services\EnvironmentService.cs" />
+    <Compile Include="Renderers\CircleImageRenderer.cs" />
+    <Compile Include="Renderers\RoundLabelRenderer.cs" />
   </ItemGroup>
   <ItemGroup>
     <InterfaceDefinition Include="Resources\LaunchScreen.storyboard" />
diff --git a/Pixiview.iOS/Renderers/AdaptedPageRenderer.cs b/Pixiview.iOS/Renderers/AdaptedPageRenderer.cs
index 248e127..c06fbd2 100644
--- a/Pixiview.iOS/Renderers/AdaptedPageRenderer.cs
+++ b/Pixiview.iOS/Renderers/AdaptedPageRenderer.cs
@@ -1,4 +1,5 @@
 using System.Diagnostics.CodeAnalysis;
+using Foundation;
 using Pixiview.iOS.Renderers;
 using Pixiview.UI;
 using Pixiview.Utils;
@@ -11,6 +12,9 @@ namespace Pixiview.iOS.Renderers
 {
     public class AdaptedPageRenderer : PageRenderer
     {
+        UIDeviceOrientation lastOrientation;
+        NSObject observer;
+
         public override void ViewDidLoad()
         {
             base.ViewDidLoad();
@@ -30,6 +34,20 @@ namespace Pixiview.iOS.Renderers
             {
                 SetStatusBarStyle(style);
             }
+
+            observer = UIDevice.Notifications.ObserveOrientationDidChange(ChangeOrientation);
+            ChangeOrientation(null, null);
+        }
+
+        public override void ViewWillDisappear(bool animated)
+        {
+            if (observer != null)
+            {
+                observer.Dispose();
+                observer = null;
+            }
+
+            base.ViewWillDisappear(animated);
         }
 
         private void SetStatusBarStyle(UIStatusBarStyle style)
@@ -64,5 +82,23 @@ namespace Pixiview.iOS.Renderers
                     return UIStatusBarStyle.Default;
             }
         }
+
+        void ChangeOrientation(object sender, NSNotificationEventArgs e)
+        {
+            var current = UIDevice.CurrentDevice.Orientation;
+            if (current == UIDeviceOrientation.FaceUp || current == UIDeviceOrientation.FaceDown)
+            {
+                //current = UIDeviceOrientation.Portrait;
+                return;
+            }
+            if (lastOrientation != current)
+            {
+                lastOrientation = current;
+                if (Element is AdaptedPage page)
+                {
+                    page.OnOrientationChanged((Orientation)lastOrientation);
+                }
+            }
+        }
     }
 }
diff --git a/Pixiview.iOS/Renderers/CircleImageRenderer.cs b/Pixiview.iOS/Renderers/CircleImageRenderer.cs
new file mode 100644
index 0000000..85f77a2
--- /dev/null
+++ b/Pixiview.iOS/Renderers/CircleImageRenderer.cs
@@ -0,0 +1,31 @@
+using Pixiview.iOS.Renderers;
+using Pixiview.UI;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.iOS;
+
+[assembly: ExportRenderer(typeof(CircleImage), typeof(CircleImageRenderer))]
+namespace Pixiview.iOS.Renderers
+{
+    public class CircleImageRenderer : ImageRenderer
+    {
+        protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
+        {
+            base.OnElementChanged(e);
+
+            if (Control != null)
+            {
+                Control.Layer.MasksToBounds = true;
+            }
+        }
+
+        public override void LayoutSubviews()
+        {
+            base.LayoutSubviews();
+
+            if (Control != null)
+            {
+                Control.Layer.CornerRadius = Control.Frame.Size.Width / 2;
+            }
+        }
+    }
+}
diff --git a/Pixiview.iOS/Renderers/RoundLabelRenderer.cs b/Pixiview.iOS/Renderers/RoundLabelRenderer.cs
new file mode 100644
index 0000000..27f5fae
--- /dev/null
+++ b/Pixiview.iOS/Renderers/RoundLabelRenderer.cs
@@ -0,0 +1,47 @@
+using System.ComponentModel;
+using Pixiview.iOS.Renderers;
+using Pixiview.UI;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.iOS;
+
+[assembly: ExportRenderer(typeof(RoundLabel), typeof(RoundLabelRenderer))]
+namespace Pixiview.iOS.Renderers
+{
+    public class RoundLabelRenderer : LabelRenderer
+    {
+        protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
+        {
+            base.OnElementChanged(e);
+
+            if (Control != null && Element is RoundLabel label)
+            {
+                int radius = label.CornerRadius;
+                if (radius > 0)
+                {
+                    Control.Layer.CornerRadius = radius;
+                    Control.BackgroundColor = label.BackgroundColor.ToUIColor();
+                    Control.Layer.MasksToBounds = true;
+                }
+            }
+        }
+
+        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
+        {
+            base.OnElementPropertyChanged(sender, e);
+
+            if (e.PropertyName == RoundLabel.CornerRadiusProperty.PropertyName)
+            {
+                if (Control != null && Element is RoundLabel label)
+                {
+                    int radius = label.CornerRadius;
+                    if (radius > 0)
+                    {
+                        Control.Layer.CornerRadius = radius;
+                        Control.BackgroundColor = label.BackgroundColor.ToUIColor();
+                        Control.Layer.MasksToBounds = true;
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/Pixiview/App.xaml b/Pixiview/App.xaml
index cd3f8d4..0a8bdae 100644
--- a/Pixiview/App.xaml
+++ b/Pixiview/App.xaml
@@ -8,6 +8,7 @@
     <Application.Resources>
         <Color x:Key="WindowColor">Black</Color>
         <Color x:Key="TextColor">White</Color>
+        <Color x:Key="SubTextColor">LightGray</Color>
         <Color x:Key="MainColor">#333333</Color>
         <Color x:Key="MainTextColor">White</Color>
         <Style x:Key="TitleLabel" TargetType="Label">
diff --git a/Pixiview/GlobalSuppressions.cs b/Pixiview/GlobalSuppressions.cs
index 6b30c4a..f5c46f7 100644
--- a/Pixiview/GlobalSuppressions.cs
+++ b/Pixiview/GlobalSuppressions.cs
@@ -6,3 +6,4 @@
 using System.Diagnostics.CodeAnalysis;
 
 [assembly: SuppressMessage("Style", "IDE0063:Use simple 'using' statement", Justification = "<Pending>")]
+[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "<Pending>")]
diff --git a/Pixiview/MainPage.xaml b/Pixiview/MainPage.xaml
index 7802739..16edc0a 100644
--- a/Pixiview/MainPage.xaml
+++ b/Pixiview/MainPage.xaml
@@ -5,14 +5,63 @@
                xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                xmlns:u="clr-namespace:Pixiview.UI"
                mc:Ignorable="d"
-               x:Class="Pixiview.MainPage">
+               x:Class="Pixiview.MainPage"
+               BackgroundColor="{DynamicResource WindowColor}"
+               OrientationChanged="Page_OrientationChanged">
     <NavigationPage.TitleView>
-        <u:NavigationTitle Title="Startup Name Generator"
+        <u:NavigationTitle Title="Follow"
                            IsLeftButtonVisible="True"
                            LeftButtonClicked="NavigationTitle_LeftButtonClicked"
                            RightButtonClicked="NavigationTitle_RightButtonClicked"/>
     </NavigationPage.TitleView>
-    <Grid>
-
-    </Grid>
+    <ScrollView HorizontalOptions="Fill" Padding="{Binding StatusBarPadding}">
+        <u:FlowLayout ItemsSource="{Binding Illusts}"
+                      HorizontalOptions="Fill" Column="{Binding Columns}"
+                      Margin="16" RowSpacing="16" ColumnSpacing="16">
+            <u:FlowLayout.ItemTemplate>
+                <DataTemplate>
+                    <Frame HasShadow="False" Padding="0" Margin="0" CornerRadius="10"
+                           BackgroundColor="{DynamicResource MainColor}">
+                        <Grid HorizontalOptions="Fill" Margin="0, -5, 0, 0">
+                            <Grid.RowDefinitions>
+                                <RowDefinition Height="Auto"/>
+                                <RowDefinition Height="Auto"/>
+                                <RowDefinition Height="Auto"/>
+                            </Grid.RowDefinitions>
+                            <Image BackgroundColor="LightGray"
+                                   Source="{Binding Image}"
+                                   HorizontalOptions="Fill"
+                                   Aspect="AspectFit"/>
+                            <u:RoundLabel Text="R-18" BackgroundColor="#fd4363" Margin="6, 11, 0, 0"
+                                          Padding="6, 2" CornerRadius="4"
+                                          HorizontalOptions="Start" VerticalOptions="Start"
+                                          FontSize="Micro" TextColor="White"
+                                          IsVisible="{Binding IsRestrict}"/>
+                            <u:RoundLabel Text="{Binding PageCountText}"
+                                          FontFamily="{DynamicResource IconSolidFontFamily}"
+                                          BackgroundColor="#50000000" Margin="0, 11, 6, 0"
+                                          Padding="6, 4" CornerRadius="6"
+                                          HorizontalOptions="End" VerticalOptions="Start"
+                                          FontSize="Micro" TextColor="White"
+                                          IsVisible="{Binding IsPageVisible}"/>
+                            <Label Grid.Row="1" Text="{Binding Title}"
+                                   Padding="8, 2"
+                                   TextColor="{DynamicResource TextColor}"
+                                   LineBreakMode="TailTruncation"
+                                   FontSize="Small"/>
+                            <StackLayout Grid.Row="2" Orientation="Horizontal" Padding="8, 0, 8, 8">
+                                <u:CircleImage WidthRequest="30" HeightRequest="30" Aspect="AspectFill"
+                                               Source="{Binding ProfileImage}"/>
+                                <Label Text="{Binding UserName}"
+                                       VerticalOptions="Center"
+                                       TextColor="{DynamicResource SubTextColor}"
+                                       LineBreakMode="TailTruncation"
+                                       FontSize="Micro"/>
+                            </StackLayout>
+                        </Grid>
+                    </Frame>
+                </DataTemplate>
+            </u:FlowLayout.ItemTemplate>
+        </u:FlowLayout>
+    </ScrollView>
 </u:AdaptedPage>
\ No newline at end of file
diff --git a/Pixiview/MainPage.xaml.cs b/Pixiview/MainPage.xaml.cs
index 54c18ed..45f6f59 100644
--- a/Pixiview/MainPage.xaml.cs
+++ b/Pixiview/MainPage.xaml.cs
@@ -1,8 +1,12 @@
 using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.ComponentModel;
+using System.Linq;
 using System.Threading.Tasks;
 using Pixiview.UI;
 using Pixiview.Utils;
+using Xamarin.Forms;
 
 namespace Pixiview
 {
@@ -11,24 +15,115 @@ namespace Pixiview
     [DesignTimeVisible(false)]
     public partial class MainPage : AdaptedPage
     {
+        public static readonly BindableProperty IllustsProperty = BindableProperty.Create(
+            nameof(Illusts), typeof(IllustCollection), typeof(MainPage));
+        public static readonly BindableProperty ColumnsProperty = BindableProperty.Create(
+            nameof(Columns), typeof(int), typeof(MainPage), 2);
+
+        public IllustCollection Illusts
+        {
+            get => (IllustCollection)GetValue(IllustsProperty);
+            set => SetValue(IllustsProperty, value);
+        }
+        public int Columns
+        {
+            get => (int)GetValue(ColumnsProperty);
+            set => SetValue(ColumnsProperty, value);
+        }
+
+        private readonly ParallelOptions parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Configs.MaxThreads };
+
         private IllustData illustData;
 
         public MainPage()
         {
             InitializeComponent();
+
             BindingContext = this;
         }
 
         public override void OnLoad()
         {
             App.DebugPrint($"folder: {Stores.PersonalFolder}");
-            Task.Run(async () =>
+            Task.Run(DoLoadIllusts);
+        }
+
+        async void DoLoadIllusts()
+        {
+            illustData = await Stores.LoadIllustData();
+
+            var data = illustData.body.page.follow.Select(i =>
             {
-                illustData = await Stores.LoadIllustData();
-                App.DebugPrint(illustData.message);
+                var illust = illustData.body.thumbnails.illust.FirstOrDefault(l => l.illustId == i.ToString());
+                if (illust == null)
+                {
+                    return null;
+                }
+                return new IllustItem
+                {
+                    ImageUrl = illust.urls.x360 ?? illust.url,
+                    Title = illust.illustTitle,
+                    IsRestrict = illust.xRestrict == 1,
+                    ProfileUrl = illust.profileImageUrl,
+                    UserName = illust.userName,
+                    Width = illust.width,
+                    Height = illust.height,
+                    PageCount = illust.pageCount
+                };
+            }).Where(i => i != null);
+
+            var collection = new IllustCollection(data);
+            Illusts = collection;
+
+            DoLoadImages(collection);
+        }
+
+        void DoLoadImages(IllustCollection collection)
+        {
+            Parallel.ForEach(collection, parallelOptions, illust =>
+            {
+                if (!collection.Running)
+                {
+                    return;
+                }
+                if (illust.ImageUrl != null)
+                {
+                    var url = Configs.GetThumbnailUrl(illust.ImageUrl);
+                    var image = Stores.LoadThumbnailImage(url);
+                    if (image != null)
+                    {
+                        illust.Image = image;
+                    }
+                }
+
+                if (illust.ProfileUrl != null)
+                {
+                    var userImage = Stores.LoadUserProfileImage(illust.ProfileUrl);
+                    if (userImage != null)
+                    {
+                        illust.ProfileImage = userImage;
+                    }
+                }
             });
         }
 
+        void Page_OrientationChanged(object sender, OrientationEventArgs e)
+        {
+            switch (e.CurrentOrientation)
+            {
+                case Orientation.Portrait:
+                    Columns = 2;
+                    break;
+                case Orientation.Unknown:
+                case Orientation.PortraitUpsideDown:
+                case Orientation.LandscapeLeft:
+                case Orientation.LandscapeRight:
+                default:
+                    Columns = 4;
+                    break;
+            }
+        }
+
         void NavigationTitle_RightButtonClicked(object sender, EventArgs e)
         {
         }
@@ -38,4 +133,57 @@ namespace Pixiview
             DisplayAlert("title", "message", "Ok");
         }
     }
+
+    public class IllustCollection : ObservableCollection<IllustItem>
+    {
+        private static readonly object sync = new object();
+
+        public IllustCollection(IEnumerable<IllustItem> illusts) : base(illusts)
+        {
+            running = true;
+        }
+
+        private bool running;
+        public bool Running
+        {
+            get => running;
+            set
+            {
+                lock (sync)
+                {
+                    running = value;
+                }
+            }
+        }
+    }
+
+    public class IllustItem : BindableObject
+    {
+        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 ImageSource Image
+        {
+            get => (ImageSource)GetValue(ImageProperty);
+            set => SetValue(ImageProperty, value);
+        }
+        public ImageSource ProfileImage
+        {
+            get => (ImageSource)GetValue(ProfileImageProperty);
+            set => SetValue(ProfileImageProperty, value);
+        }
+
+        public string ImageUrl { get; set; }
+        public string Title { get; set; }
+        public bool IsRestrict { get; set; }
+        public string ProfileUrl { get; set; }
+        public string UserName { get; set; }
+        public int Width { get; set; }
+        public int Height { get; set; }
+        public int PageCount { get; set; }
+        public string PageCountText => $"{StyleDefinition.IconLayer} {PageCount}";
+        public bool IsPageVisible => PageCount > 1;
+    }
 }
diff --git a/Pixiview/UI/AdaptedPage.cs b/Pixiview/UI/AdaptedPage.cs
index 1fc6131..f2b6223 100644
--- a/Pixiview/UI/AdaptedPage.cs
+++ b/Pixiview/UI/AdaptedPage.cs
@@ -5,16 +5,56 @@ namespace Pixiview.UI
 {
     public class AdaptedPage : ContentPage
     {
-        public event EventHandler Load;
+        static readonly Thickness LandscapeLeftPadding = new Thickness(34, 0, 0, 0);
+        static readonly Thickness LandscapeRightPadding = new Thickness(0, 0, 34, 0);
 
-        public AdaptedPage()
+        public static readonly BindableProperty StatusBarPaddingProperty = BindableProperty.Create(
+            nameof(StatusBarPadding), typeof(Thickness), typeof(AdaptedPage), default(Thickness));
+
+        public Thickness StatusBarPadding
         {
-            SetDynamicResource(BackgroundColorProperty, App.WindowColor);
+            get => (Thickness)GetValue(StatusBarPaddingProperty);
+            set => SetValue(StatusBarPaddingProperty, value);
         }
 
+        public event EventHandler Load;
+        public event EventHandler<OrientationEventArgs> OrientationChanged;
+
         public virtual void OnLoad()
         {
             Load?.Invoke(this, EventArgs.Empty);
         }
+
+        public virtual void OnOrientationChanged(Orientation orientation)
+        {
+            if (orientation == Orientation.LandscapeLeft || orientation == Orientation.PortraitUpsideDown)
+            {
+                StatusBarPadding = StyleDefinition.IsFullscreenDevice ? LandscapeLeftPadding : default;
+            }
+            else if (orientation == Orientation.LandscapeRight)
+            {
+                StatusBarPadding = StyleDefinition.IsFullscreenDevice ? LandscapeRightPadding : default;
+            }
+            else
+            {
+                StatusBarPadding = default;
+            }
+
+            OrientationChanged?.Invoke(this, new OrientationEventArgs { CurrentOrientation = orientation });
+        }
+    }
+
+    public class OrientationEventArgs : EventArgs
+    {
+        public Orientation CurrentOrientation { get; set; }
+    }
+
+    public enum Orientation
+    {
+        Unknown,
+        Portrait,
+        PortraitUpsideDown,
+        LandscapeLeft,
+        LandscapeRight
     }
 }
diff --git a/Pixiview/UI/CircleUIs.cs b/Pixiview/UI/CircleUIs.cs
new file mode 100644
index 0000000..1906dfc
--- /dev/null
+++ b/Pixiview/UI/CircleUIs.cs
@@ -0,0 +1,25 @@
+using Xamarin.Forms;
+
+namespace Pixiview.UI
+{
+    public class CircleImage : Image { }
+
+    public class RoundLabel : Label
+    {
+        public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(
+            nameof(CornerRadius), typeof(int), typeof(RoundLabel));
+        public static new readonly BindableProperty BackgroundColorProperty = BindableProperty.Create(
+            nameof(BackgroundColor), typeof(Color), typeof(RoundLabel), Color.Transparent);
+
+        public int CornerRadius
+        {
+            get => (int)GetValue(CornerRadiusProperty);
+            set => SetValue(CornerRadiusProperty, value);
+        }
+        public new Color BackgroundColor
+        {
+            get => (Color)GetValue(BackgroundColorProperty);
+            set => SetValue(BackgroundColorProperty, value);
+        }
+    }
+}
diff --git a/Pixiview/UI/StyleDefinition.cs b/Pixiview/UI/StyleDefinition.cs
index 744af93..bbd2b29 100644
--- a/Pixiview/UI/StyleDefinition.cs
+++ b/Pixiview/UI/StyleDefinition.cs
@@ -1,13 +1,86 @@
-using Xamarin.Forms;
+using System;
+using Xamarin.Essentials;
+using Xamarin.Forms;
 
 namespace Pixiview.UI
 {
     public static class StyleDefinition
     {
-        public static readonly double FontSizeTitle = 18.0;
-        public static readonly double FontSizeTitleIcon = 24.0;
+        public const double FontSizeTitle = 18.0;
+        public const double FontSizeTitleIcon = 24.0;
 
         public static readonly Thickness HorizonLeft10 = new Thickness(10, 0, 0, 0);
         public static readonly Thickness HorizonRight10 = new Thickness(0, 0, 10, 0);
+        public static readonly Thickness LeftBottom10 = new Thickness(10, 0, 0, 10);
+
+        public const string IconLayer = "\uf302";
+
+        private static bool? _isFullscreenDevice;
+        public static bool IsFullscreenDevice
+        {
+            get
+            {
+                if (_isFullscreenDevice != null)
+                {
+                    return _isFullscreenDevice.Value;
+                }
+                if (Device.RuntimePlatform == Device.iOS)
+                {
+                    try
+                    {
+                        var model = DeviceInfo.Model;
+                        if (model == "iPhone10,3")
+                        {
+                            // iPhone X
+                            _isFullscreenDevice = true;
+                        }
+                        else if (model.StartsWith("iPhone"))
+                        {
+                            var vs = model.Substring(6).Split(',');
+                            if (vs.Length == 2 && int.TryParse(vs[0], out int main) && int.TryParse(vs[1], out int sub))
+                            {
+                                // iPhone X/XS/XR or iPhone 11
+                                _isFullscreenDevice = (main == 10 && sub >= 6) || (main > 10);
+                            }
+                            else
+                            {
+                                _isFullscreenDevice = false;
+                            }
+                        }
+                        else if (model.StartsWith("iPad8,"))
+                        {
+                            // iPad 11-inch or 12.9-inch (3rd+)
+                            _isFullscreenDevice = true;
+                        }
+#if DEBUG
+                        else
+                        {
+                            // iPad or Simulator
+                            var name = DeviceInfo.Name;
+                            _isFullscreenDevice = name.StartsWith("iPhone X")
+                                || name.StartsWith("iPhone 11")
+                                || name.StartsWith("iPad Pro (11-inch)")
+                                || name.StartsWith("iPad Pro (12.9-inch) (3rd generation)")
+                                || name.StartsWith("iPad Pro (12.9-inch) (4th generation)");
+                        }
+#endif
+                    }
+                    catch (Exception ex)
+                    {
+                        App.DebugError("device.get", $"failed to get the device model. {ex.Message}");
+                    }
+                }
+                else
+                {
+                    // TODO:
+                    _isFullscreenDevice = false;
+                }
+                if (_isFullscreenDevice == null)
+                {
+                    _isFullscreenDevice = false;
+                }
+                return _isFullscreenDevice.Value;
+            }
+        }
     }
 }
diff --git a/Pixiview/Utils/Converters.cs b/Pixiview/Utils/Converters.cs
new file mode 100644
index 0000000..1dbaeda
--- /dev/null
+++ b/Pixiview/Utils/Converters.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Globalization;
+using Pixiview.UI;
+using Xamarin.Forms;
+
+namespace Pixiview.Utils
+{
+    public class PageConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            return StyleDefinition.IconLayer + value;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}
diff --git a/Pixiview/Utils/IllustData.cs b/Pixiview/Utils/IllustData.cs
index 60ddaef..dbfab17 100644
--- a/Pixiview/Utils/IllustData.cs
+++ b/Pixiview/Utils/IllustData.cs
@@ -72,11 +72,13 @@ namespace Pixiview.Utils
                     public string userName;
                     public int width;
                     public int height;
+                    public int pageCount;
                     public string alt;
                     public IllustUrls urls;
                     public string seriesId;
                     public string seriesTitle;
                     public string profileImageUrl;
+                    public int xRestrict;
 
                     public class IllustUrls
                     {
diff --git a/Pixiview/Utils/Stores.cs b/Pixiview/Utils/Stores.cs
index 74d04d4..ca127f3 100644
--- a/Pixiview/Utils/Stores.cs
+++ b/Pixiview/Utils/Stores.cs
@@ -6,6 +6,7 @@ using System.Net.Http;
 using System.Text;
 using System.Threading.Tasks;
 using Newtonsoft.Json;
+using Xamarin.Forms;
 
 namespace Pixiview.Utils
 {
@@ -14,6 +15,7 @@ namespace Pixiview.Utils
         public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
         //public static readonly string CacheFolder = Environment.GetFolderPath(Environment.SpecialFolder.InternetCache);
         private const string imageFolder = "img-master";
+        private const string thumbFolder = "img-thumb";
         private const string userFolder = "user-profile";
         private const string illustFile = "illust.json";
 
@@ -67,35 +69,46 @@ namespace Pixiview.Utils
             }
         }
 
-        public static string LoadIllustImage(string url)
+        public static ImageSource LoadIllustImage(string url)
         {
             return LoadImage(url, imageFolder);
         }
 
-        public static string LoadUserProfileImage(string url)
+        public static ImageSource LoadThumbnailImage(string url)
+        {
+            return LoadImage(url, thumbFolder);
+        }
+
+        public static ImageSource LoadUserProfileImage(string url)
         {
             return LoadImage(url, userFolder);
         }
 
-        public static string LoadImage(string url, string folder)
+        public static ImageSource LoadImage(string url, string folder)
         {
-            var file = Path.Combine(PersonalFolder, imageFolder, Path.GetFileName(url));
-            if (File.Exists(file))
+            var file = Path.Combine(PersonalFolder, folder, Path.GetFileName(url));
+            if (!File.Exists(file))
             {
-                return file;
+                file = DownloadImage(url, folder);
             }
-            else
+            if (file != null)
             {
-                file = DownloadImage(url, folder).Result;
-                return file;
+                return ImageSource.FromFile(file);
             }
+            return null;
         }
 
-        public static async Task<string> DownloadImage(string url, string folder)
+        public static string DownloadImage(string url, string folder)
         {
             try
             {
-                var file = Path.Combine(PersonalFolder, folder, Path.GetFileName(url));
+                var directory = Path.Combine(PersonalFolder, folder);
+                if (!Directory.Exists(directory))
+                {
+                    Directory.CreateDirectory(directory);
+                }
+                var file = Path.Combine(directory, Path.GetFileName(url));
+                App.DebugPrint($"download, url: {url}");
                 var response = Download(url);
                 if (response == null)
                 {
@@ -104,11 +117,11 @@ namespace Pixiview.Utils
                 using (response)
                 using (var fs = File.OpenWrite(file))
                 {
-                    await response.Content.CopyToAsync(fs);
-                    if (response.Headers.Date != null)
-                    {
-                        File.SetLastWriteTimeUtc(file, response.Headers.Date.Value.UtcDateTime);
-                    }
+                    response.Content.CopyToAsync(fs).Wait();
+                    //if (response.Headers.Date != null)
+                    //{
+                    //    File.SetLastWriteTimeUtc(file, response.Headers.Date.Value.UtcDateTime);
+                    //}
                 }
                 return file;
             }
@@ -156,6 +169,7 @@ namespace Pixiview.Utils
             })
             {
                 request.Headers.Referrer = referer == null ? Configs.Referer : new Uri(referer);
+                request.Headers.Add("user-agent", Configs.UserAgent);
                 if (headers != null)
                 {
                     foreach (var (header, value) in headers)
@@ -173,6 +187,8 @@ namespace Pixiview.Utils
         public static readonly WebProxy Proxy = new WebProxy("10.0.10.100", 8088);
         public static readonly Uri Referer = new Uri("https://www.pixiv.net/");
 
+        public const int MaxThreads = 3;
+        public const string UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36";
         public const string UrlIllust = "https://www.pixiv.net/ajax/top/illust?mode=all&lang=zh";
         public const string Cookie = "first_visit_datetime_pc=2019-10-29+22%3A05%3A30; p_ab_id=2; p_ab_id_2=6;" +
             " p_ab_d_id=1155161977; a_type=0; b_type=1; d_type=2; module_orders_mypage=%5B%7B%22name%22%3A%22s" +
@@ -188,5 +204,20 @@ namespace Pixiview.Utils
             "SID=2603358_VHyGPeRaz7LpeoFkRsHvjXIpApCMb56a; __cfduid=d9fa2d4d1ddd30db85ebb519f9855d256158780674" +
             "7; privacy_policy_agreement=2";
         public const string UserId = "2603358";
+
+        public static string GetThumbnailUrl(string url)
+        {
+            url = url.Replace("/custom-thumb/", "/img-master/");
+            var index = url.LastIndexOf("_square1200.jpg");
+            if (index < 0)
+            {
+                index = url.LastIndexOf("_custom1200.jpg");
+            }
+            if (index >= 0)
+            {
+                return url.Substring(0, index) + "_master1200.jpg";
+            }
+            return url;
+        }
     }
 }