diff --git a/Pixiview/AppShell.xaml b/Pixiview/AppShell.xaml
index 63cbb30..a59005e 100644
--- a/Pixiview/AppShell.xaml
+++ b/Pixiview/AppShell.xaml
@@ -85,6 +85,11 @@
              Route="{x:Static util:Routes.Ranking}">
             <ShellContent ContentTemplate="{DataTemplate i:RankingPage}"/>
         </Tab>
+        <Tab Icon="{DynamicResource FontIconFavorite}"
+             Title="{r:Text Favorites}"
+             Route="{x:Static util:Routes.Favorites}">
+            <ShellContent ContentTemplate="{DataTemplate i:FavoritesPage}"/>
+        </Tab>
     </FlyoutItem>
     <FlyoutItem Icon="{DynamicResource FontIconOption}"
                 Title="{r:Text Option}"
diff --git a/Pixiview/Illust/FavoritesPage.xaml b/Pixiview/Illust/FavoritesPage.xaml
new file mode 100644
index 0000000..f33ced8
--- /dev/null
+++ b/Pixiview/Illust/FavoritesPage.xaml
@@ -0,0 +1,25 @@
+<?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:Pixiview.Illust"
+                                xmlns:u="clr-namespace:Pixiview.UI"
+                                xmlns:r="clr-namespace:Pixiview.Resources"
+                                x:Class="Pixiview.Illust.FavoritesPage"
+                                BackgroundColor="{DynamicResource WindowColor}"
+                                Title="{r:Text Favorites}">
+    <Grid>
+        <ScrollView HorizontalOptions="Fill">
+            <u:FlowLayout ItemsSource="{Binding Illusts}"
+                          HorizontalOptions="Fill" Column="{Binding Columns}"
+                          Margin="16" RowSpacing="16" ColumnSpacing="16"
+                          ItemTemplate="{StaticResource cardView}"/>
+        </ScrollView>
+        <Frame HasShadow="False" Margin="0" Padding="20" CornerRadius="8"
+               IsVisible="{Binding Loading}"
+               HorizontalOptions="Center" VerticalOptions="Center"
+               BackgroundColor="{DynamicResource MaskColor}">
+            <ActivityIndicator IsRunning="True" IsVisible="True"
+                               Color="{DynamicResource WindowColor}"/>
+        </Frame>
+    </Grid>
+</i:FavoriteIllustCollectionPage>
diff --git a/Pixiview/Illust/FavoritesPage.xaml.cs b/Pixiview/Illust/FavoritesPage.xaml.cs
new file mode 100644
index 0000000..0b0954d
--- /dev/null
+++ b/Pixiview/Illust/FavoritesPage.xaml.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows.Input;
+using Pixiview.Utils;
+
+namespace Pixiview.Illust
+{
+    public partial class FavoritesPage : FavoriteIllustCollectionPage
+    {
+        public FavoritesPage()
+        {
+            Resources.Add("cardView", GetCardViewTemplate());
+            InitializeComponent();
+        }
+
+        protected override void OnAppearing()
+        {
+            base.OnAppearing();
+
+            StartLoad(true);
+        }
+
+        protected override void OnDisappearing()
+        {
+            base.OnDisappearing();
+
+            var illusts = Illusts;
+            if (illusts != null)
+            {
+                illusts.Clear();
+            }
+        }
+
+        protected override IEnumerable<IllustItem> DoGetIllustList(IllustItem[] data, ICommand command)
+        {
+            return data.Select(i =>
+            {
+                i.IllustTapped = command;
+                return i;
+            });
+        }
+
+        protected override IllustItem[] DoLoadIllustData(bool force)
+        {
+            var favorites = Stores.LoadFavoritesIllusts();
+            if (favorites == null)
+            {
+                return null;
+            }
+            return favorites.Illusts;
+        }
+    }
+}
diff --git a/Pixiview/Illust/IllustCollectionPage.cs b/Pixiview/Illust/IllustCollectionPage.cs
index b47f04a..69ff7d2 100644
--- a/Pixiview/Illust/IllustCollectionPage.cs
+++ b/Pixiview/Illust/IllustCollectionPage.cs
@@ -1,8 +1,10 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Linq;
 using System.Threading.Tasks;
 using System.Windows.Input;
+using Newtonsoft.Json;
 using Pixiview.Resources;
 using Pixiview.UI;
 using Pixiview.UI.Theme;
@@ -12,20 +14,28 @@ using Xamarin.Forms;
 
 namespace Pixiview.Illust
 {
-    public abstract class IllustCollectionPage : AdaptedPage
+    public interface IIllustCollectionPage
+    {
+        List<IllustItem> Favorites { get; }
+    }
+
+    public abstract class IllustDataCollectionPage : IllustCollectionPage<IllustData> { }
+    public abstract class FavoriteIllustCollectionPage : IllustCollectionPage<IllustItem[]> { }
+
+    public abstract class IllustCollectionPage<T> : AdaptedPage, IIllustCollectionPage
     {
         #region - Properties -
 
         public static readonly BindableProperty IllustsProperty = BindableProperty.Create(
-            nameof(Illusts), typeof(IllustCollection), typeof(IllustCollectionPage));
+            nameof(Illusts), typeof(IllustCollection), typeof(IllustCollectionPage<T>));
         public static readonly BindableProperty ColumnsProperty = BindableProperty.Create(
-            nameof(Columns), typeof(int), typeof(IllustCollectionPage), 2);
+            nameof(Columns), typeof(int), typeof(IllustCollectionPage<T>), 2);
         public static readonly BindableProperty LoadingProperty = BindableProperty.Create(
-            nameof(Loading), typeof(bool), typeof(IllustCollectionPage), propertyChanged: OnLoadingPropertyChanged);
+            nameof(Loading), typeof(bool), typeof(IllustCollectionPage<T>), propertyChanged: OnLoadingPropertyChanged);
 
         private static void OnLoadingPropertyChanged(BindableObject obj, object oldValue, object newValue)
         {
-            var page = (IllustCollectionPage)obj;
+            var page = (IllustCollectionPage<T>)obj;
             var now = (bool)newValue;
             if (!page.loaded && now && Stores.NetworkAvailable)
             {
@@ -50,11 +60,13 @@ namespace Pixiview.Illust
             set => SetValue(LoadingProperty, value);
         }
 
+        public List<IllustItem> Favorites { get; } = new List<IllustItem>();
+
         #endregion
 
         protected bool loaded;
 
-        private IllustData illustData;
+        private T illustData;
         private readonly ParallelOptions parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Configs.MaxThreads };
         private readonly Command<IllustItem> commandIllustImageTapped;
 
@@ -62,6 +74,12 @@ namespace Pixiview.Illust
         {
             BindingContext = this;
             commandIllustImageTapped = new Command<IllustItem>(IllustImageTapped);
+
+            var favorites = Stores.LoadFavoritesIllusts();
+            if (favorites != null)
+            {
+                Favorites.AddRange(favorites.Illusts);
+            }
         }
 
         private void IllustImageTapped(IllustItem illust)
@@ -128,11 +146,11 @@ namespace Pixiview.Illust
 
         #endregion
 
-        protected abstract IllustData DoLoadIllustData(bool force);
-        protected abstract IEnumerable<string> DoGetIllustList(IllustData data);
+        protected abstract T DoLoadIllustData(bool force);
+        protected abstract IEnumerable<IllustItem> DoGetIllustList(T data, ICommand command);
         protected virtual void OnIllustImageTapped(IllustItem illust)
         {
-            var page = new ViewIllustPage(illust);
+            var page = new ViewIllustPage(illust, this);
             Navigation.PushAsync(page);
         }
 
@@ -270,33 +288,12 @@ namespace Pixiview.Illust
             illustData = DoLoadIllustData(force);
             if (illustData == null)
             {
-                App.DebugError("illusts.load", "failed to load illusts data.");
+                //App.DebugError("illusts.load", "failed to load illusts data.");
+                Loading = false;
                 return;
             }
 
-            var data = DoGetIllustList(illustData).Select(id =>
-            {
-                var illust = illustData.body.thumbnails.illust.FirstOrDefault(l => l.illustId == id);
-                if (illust == null)
-                {
-                    return null;
-                }
-                return new IllustItem
-                {
-                    Id = illust.illustId,
-                    ImageUrl = illust.urls.x360 ?? illust.url,
-                    Title = illust.illustTitle,
-                    IsRestrict = illust.xRestrict == 1,
-                    ProfileUrl = illust.profileImageUrl,
-                    UserId = illust.userId,
-                    UserName = illust.userName,
-                    Width = illust.width,
-                    Height = illust.height,
-                    PageCount = illust.pageCount,
-
-                    IllustTapped = commandIllustImageTapped
-                };
-            }).Where(i => i != null);
+            var data = DoGetIllustList(illustData, commandIllustImageTapped).Where(i => i != null);
 
             var collection = new IllustCollection(data);
             Illusts = collection;
@@ -340,7 +337,24 @@ namespace Pixiview.Illust
     public class IllustCollection : ObservableCollection<IllustItem>
     {
         private static readonly object sync = new object();
+        private static IllustCollection empty;
 
+        public static IllustCollection Empty
+        {
+            get
+            {
+                if (empty == null)
+                {
+                    empty = new IllustCollection();
+                }
+                return empty;
+            }
+        }
+
+        public IllustCollection() : base()
+        {
+            running = true;
+        }
         public IllustCollection(IEnumerable<IllustItem> illusts) : base(illusts)
         {
             running = true;
@@ -360,6 +374,13 @@ namespace Pixiview.Illust
         }
     }
 
+    public class IllustFavorite
+    {
+        public DateTime LastFavoriteUtc { get; set; }
+        public IllustItem[] Illusts { get; set; }
+    }
+
+    [JsonObject(MemberSerialization.OptIn)]
     public class IllustItem : BindableObject
     {
         public static readonly BindableProperty ImageProperty = BindableProperty.Create(
@@ -379,6 +400,7 @@ namespace Pixiview.Illust
             get => (ImageSource)GetValue(ProfileImageProperty);
             set => SetValue(ProfileImageProperty, value);
         }
+        [JsonProperty]
         public GridLength ImageHeight
         {
             get => (GridLength)GetValue(ImageHeightProperty);
@@ -386,16 +408,27 @@ namespace Pixiview.Illust
         }
         public ICommand IllustTapped { get; set; }
 
+        [JsonProperty]
         public string Id { get; set; }
+        [JsonProperty]
         public string ImageUrl { get; set; }
+        [JsonProperty]
         public string Title { get; set; }
+        [JsonProperty]
         public bool IsRestrict { get; set; }
+        [JsonProperty]
         public string ProfileUrl { get; set; }
+        [JsonProperty]
         public string UserId { get; set; }
+        [JsonProperty]
         public string UserName { get; set; }
+        [JsonProperty]
         public int Width { get; set; }
+        [JsonProperty]
         public int Height { get; set; }
+        [JsonProperty]
         public int PageCount { get; set; }
+
         public string PageCountText => $"{StyleDefinition.IconLayer} {PageCount}";
         public bool IsPageVisible => PageCount > 1;
     }
diff --git a/Pixiview/Illust/MainPage.xaml b/Pixiview/Illust/MainPage.xaml
index 5b28a13..bfbe213 100644
--- a/Pixiview/Illust/MainPage.xaml
+++ b/Pixiview/Illust/MainPage.xaml
@@ -1,14 +1,12 @@
 <?xml version="1.0" encoding="utf-8"?>
-<i:IllustCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
-                        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
-                        xmlns:i="clr-namespace:Pixiview.Illust"
-                        xmlns:u="clr-namespace:Pixiview.UI"
-                        xmlns:util="clr-namespace:Pixiview.Utils"
-                        xmlns:r="clr-namespace:Pixiview.Resources"
-                        x:Class="Pixiview.Illust.MainPage"
-                        util:Screen.StatusBarStyle="{DynamicResource StatusBarStyle}"
-                        BackgroundColor="{DynamicResource WindowColor}"
-                        Title="{r:Text Follow}">
+<i:IllustDataCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
+                            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+                            xmlns:i="clr-namespace:Pixiview.Illust"
+                            xmlns:u="clr-namespace:Pixiview.UI"
+                            xmlns:r="clr-namespace:Pixiview.Resources"
+                            x:Class="Pixiview.Illust.MainPage"
+                            BackgroundColor="{DynamicResource WindowColor}"
+                            Title="{r:Text Follow}">
     <ContentPage.ToolbarItems>
         <ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
                      IconImageSource="{DynamicResource FontIconRefresh}"/>
@@ -28,4 +26,4 @@
                                Color="{DynamicResource WindowColor}"/>
         </Frame>
     </Grid>
-</i:IllustCollectionPage>
\ No newline at end of file
+</i:IllustDataCollectionPage>
diff --git a/Pixiview/Illust/MainPage.xaml.cs b/Pixiview/Illust/MainPage.xaml.cs
index 3376229..8951355 100644
--- a/Pixiview/Illust/MainPage.xaml.cs
+++ b/Pixiview/Illust/MainPage.xaml.cs
@@ -1,26 +1,41 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Windows.Input;
 using Pixiview.Utils;
 
 namespace Pixiview.Illust
 {
-    // Learn more about making custom code visible in the Xamarin.Forms previewer
-    // by visiting https://aka.ms/xamarinforms-previewer
-    //[DesignTimeVisible(false)]
-    public partial class MainPage : IllustCollectionPage
+    public partial class MainPage : IllustDataCollectionPage
     {
         public MainPage()
         {
             Resources.Add("cardView", GetCardViewTemplate());
             InitializeComponent();
+        }
 
+        public override void OnLoad()
+        {
             StartLoad();
         }
 
-        protected override IEnumerable<string> DoGetIllustList(IllustData data)
+        public override void OnUnload()
         {
-            return data.body.page.follow.Select(i => i.ToString());
+            Illusts = IllustCollection.Empty;
+            loaded = false;
+        }
+
+        protected override IEnumerable<IllustItem> DoGetIllustList(IllustData data, ICommand command)
+        {
+            return data.body.page.follow.Select(i =>
+            {
+                var item = data.body.thumbnails.illust.FirstOrDefault(l => l.illustId == i.ToString())?.ConvertToItem();
+                if (item != null)
+                {
+                    item.IllustTapped = command;
+                }
+                return item;
+            });
         }
 
         protected override IllustData DoLoadIllustData(bool force)
diff --git a/Pixiview/Illust/RankingPage.xaml b/Pixiview/Illust/RankingPage.xaml
index 5f0766a..491deab 100644
--- a/Pixiview/Illust/RankingPage.xaml
+++ b/Pixiview/Illust/RankingPage.xaml
@@ -1,12 +1,14 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<i:IllustCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
-                        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
-                        xmlns:i="clr-namespace:Pixiview.Illust"
-                        xmlns:u="clr-namespace:Pixiview.UI"
-                        xmlns:util="clr-namespace:Pixiview.Utils"
-                        x:Class="Pixiview.Illust.RankingPage"
-                        util:Screen.StatusBarStyle="{DynamicResource StatusBarStyle}"
-                        BackgroundColor="{DynamicResource WindowColor}">
+<i:IllustDataCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
+                            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+                            xmlns:i="clr-namespace:Pixiview.Illust"
+                            xmlns:u="clr-namespace:Pixiview.UI"
+                            x:Class="Pixiview.Illust.RankingPage"
+                            BackgroundColor="{DynamicResource WindowColor}">
+    <ContentPage.ToolbarItems>
+        <ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
+                     IconImageSource="{DynamicResource FontIconRefresh}"/>
+    </ContentPage.ToolbarItems>
     <Grid>
         <ScrollView HorizontalOptions="Fill">
             <u:FlowLayout ItemsSource="{Binding Illusts}"
@@ -22,4 +24,4 @@
                                Color="{DynamicResource WindowColor}"/>
         </Frame>
     </Grid>
-</i:IllustCollectionPage>
+</i:IllustDataCollectionPage>
diff --git a/Pixiview/Illust/RankingPage.xaml.cs b/Pixiview/Illust/RankingPage.xaml.cs
index bb4cfcd..770d383 100644
--- a/Pixiview/Illust/RankingPage.xaml.cs
+++ b/Pixiview/Illust/RankingPage.xaml.cs
@@ -1,23 +1,42 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Linq;
+using System.Windows.Input;
 using Pixiview.Utils;
 using Xamarin.Forms;
 
 namespace Pixiview.Illust
 {
-    public partial class RankingPage : IllustCollectionPage
+    public partial class RankingPage : IllustDataCollectionPage
     {
         public RankingPage()
         {
             Resources.Add("cardView", GetCardViewTemplate());
             InitializeComponent();
+        }
 
+        public override void OnLoad()
+        {
             StartLoad();
         }
 
-        protected override IEnumerable<string> DoGetIllustList(IllustData data)
+        public override void OnUnload()
         {
-            return data.body.page.ranking.items.Select(i => i.id);
+            Illusts = IllustCollection.Empty;
+            loaded = false;
+        }
+
+        protected override IEnumerable<IllustItem> DoGetIllustList(IllustData data, ICommand command)
+        {
+            return data.body.page.ranking.items.Select(i =>
+            {
+                var item = data.body.thumbnails.illust.FirstOrDefault(l => l.illustId == i.id)?.ConvertToItem();
+                if (item != null)
+                {
+                    item.IllustTapped = command;
+                }
+                return item;
+            });
         }
 
         protected override IllustData DoLoadIllustData(bool force)
@@ -34,5 +53,14 @@ namespace Pixiview.Illust
             }
             return data;
         }
+
+        private void Refresh_Clicked(object sender, EventArgs e)
+        {
+            if (Loading)
+            {
+                return;
+            }
+            StartLoad(true);
+        }
     }
 }
diff --git a/Pixiview/Illust/RecommendsPage.xaml b/Pixiview/Illust/RecommendsPage.xaml
index 13793c1..8993efb 100644
--- a/Pixiview/Illust/RecommendsPage.xaml
+++ b/Pixiview/Illust/RecommendsPage.xaml
@@ -1,14 +1,16 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<i:IllustCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
-                        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
-                        xmlns:i="clr-namespace:Pixiview.Illust"
-                        xmlns:u="clr-namespace:Pixiview.UI"
-                        xmlns:util="clr-namespace:Pixiview.Utils"
-                        xmlns:r="clr-namespace:Pixiview.Resources"
-                        x:Class="Pixiview.Illust.RecommendsPage"
-                        util:Screen.StatusBarStyle="{DynamicResource StatusBarStyle}"
-                        BackgroundColor="{DynamicResource WindowColor}"
-                        Title="{r:Text Recommends}">
+<i:IllustDataCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
+                            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+                            xmlns:i="clr-namespace:Pixiview.Illust"
+                            xmlns:u="clr-namespace:Pixiview.UI"
+                            xmlns:r="clr-namespace:Pixiview.Resources"
+                            x:Class="Pixiview.Illust.RecommendsPage"
+                            BackgroundColor="{DynamicResource WindowColor}"
+                            Title="{r:Text Recommends}">
+    <ContentPage.ToolbarItems>
+        <ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
+                     IconImageSource="{DynamicResource FontIconRefresh}"/>
+    </ContentPage.ToolbarItems>
     <Grid>
         <ScrollView HorizontalOptions="Fill">
             <u:FlowLayout ItemsSource="{Binding Illusts}"
@@ -24,4 +26,4 @@
                                Color="{DynamicResource WindowColor}"/>
         </Frame>
     </Grid>
-</i:IllustCollectionPage>
+</i:IllustDataCollectionPage>
diff --git a/Pixiview/Illust/RecommendsPage.xaml.cs b/Pixiview/Illust/RecommendsPage.xaml.cs
index b71bbda..3c2d2b2 100644
--- a/Pixiview/Illust/RecommendsPage.xaml.cs
+++ b/Pixiview/Illust/RecommendsPage.xaml.cs
@@ -1,10 +1,12 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Linq;
+using System.Windows.Input;
 using Pixiview.Utils;
 
 namespace Pixiview.Illust
 {
-    public partial class RecommendsPage : IllustCollectionPage
+    public partial class RecommendsPage : IllustDataCollectionPage
     {
         public bool ByUser { get; set; }
 
@@ -12,19 +14,45 @@ namespace Pixiview.Illust
         {
             Resources.Add("cardView", GetCardViewTemplate());
             InitializeComponent();
+        }
 
+        public override void OnLoad()
+        {
             StartLoad();
         }
 
-        protected override IEnumerable<string> DoGetIllustList(IllustData data)
+        public override void OnUnload()
+        {
+            Illusts = IllustCollection.Empty;
+            loaded = false;
+        }
+
+        protected override IEnumerable<IllustItem> DoGetIllustList(IllustData data, ICommand command)
         {
             if (ByUser)
             {
-                return data.body.page.recommendUser.SelectMany(i => i.illustIds);
+                return data.body.page.recommendUser.SelectMany(i => i.illustIds)
+                    .Select(id =>
+                    {
+                        var item = data.body.thumbnails.illust.FirstOrDefault(l => l.illustId == id)?.ConvertToItem();
+                        if (item != null)
+                        {
+                            item.IllustTapped = command;
+                        }
+                        return item;
+                    });
             }
             else
             {
-                return data.body.page.recommend;
+                return data.body.page.recommend.Select(id =>
+                {
+                    var item = data.body.thumbnails.illust.FirstOrDefault(l => l.illustId == id)?.ConvertToItem();
+                    if (item != null)
+                    {
+                        item.IllustTapped = command;
+                    }
+                    return item;
+                });
             }
         }
 
@@ -32,5 +60,14 @@ namespace Pixiview.Illust
         {
             return Stores.LoadIllustData(force);
         }
+
+        private void Refresh_Clicked(object sender, EventArgs e)
+        {
+            if (Loading)
+            {
+                return;
+            }
+            StartLoad(true);
+        }
     }
 }
diff --git a/Pixiview/Illust/ViewIllustPage.xaml b/Pixiview/Illust/ViewIllustPage.xaml
index 0bcae70..7420cbf 100644
--- a/Pixiview/Illust/ViewIllustPage.xaml
+++ b/Pixiview/Illust/ViewIllustPage.xaml
@@ -3,17 +3,15 @@
                xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                xmlns:mdl="clr-namespace:Pixiview.Illust"
                xmlns:u="clr-namespace:Pixiview.UI"
-               xmlns:util="clr-namespace:Pixiview.Utils"
                xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
                x:Class="Pixiview.Illust.ViewIllustPage"
-               util:Screen.StatusBarStyle="{DynamicResource StatusBarStyle}"
                ios:Page.UseSafeArea="False"
                Shell.TabBarIsVisible="False"
                BackgroundColor="{DynamicResource WindowColor}"
                Title="{Binding IllustItem.Title}">
     <ContentPage.ToolbarItems>
-        <ToolbarItem Order="Primary" Clicked="Download_Clicked"
-                     IconImageSource="{DynamicResource FontIconDownload}"/>
+        <ToolbarItem Order="Primary" Clicked="Favorite_Clicked"
+                     IconImageSource="{Binding IsFavorite}"/>
     </ContentPage.ToolbarItems>
     <Grid Padding="{Binding PageTopMargin}">
         <CarouselView ItemsSource="{Binding Illusts}" HorizontalScrollBarVisibility="Never"
diff --git a/Pixiview/Illust/ViewIllustPage.xaml.cs b/Pixiview/Illust/ViewIllustPage.xaml.cs
index 080d803..a42c0c7 100644
--- a/Pixiview/Illust/ViewIllustPage.xaml.cs
+++ b/Pixiview/Illust/ViewIllustPage.xaml.cs
@@ -1,7 +1,9 @@
 using System;
+using System.Linq;
 using System.Threading.Tasks;
 using Pixiview.Resources;
 using Pixiview.UI;
+using Pixiview.UI.Theme;
 using Pixiview.Utils;
 using Xamarin.Essentials;
 using Xamarin.Forms;
@@ -11,6 +13,8 @@ namespace Pixiview.Illust
     [QueryProperty("IllustId", "id")]
     public partial class ViewIllustPage : AdaptedPage
     {
+        public static readonly BindableProperty IsFavoriteProperty = BindableProperty.Create(
+            nameof(IsFavorite), typeof(ImageSource), typeof(ViewIllustPage));
         public static readonly BindableProperty IllustsProperty = BindableProperty.Create(
             nameof(Illusts), typeof(IllustDetailItem[]), typeof(ViewIllustPage));
         public static readonly BindableProperty PagePositionTextProperty = BindableProperty.Create(
@@ -22,6 +26,8 @@ namespace Pixiview.Illust
         public static readonly BindableProperty PageTopMarginProperty = BindableProperty.Create(
             nameof(PageTopMargin), typeof(Thickness), typeof(ViewIllustPage));
 
+        public ImageSource IsFavorite => (ImageSource)GetValue(IsFavoriteProperty);
+
         public IllustDetailItem[] Illusts
         {
             get => (IllustDetailItem[])GetValue(IllustsProperty);
@@ -50,23 +56,38 @@ namespace Pixiview.Illust
 
         public int CurrentPage { get; private set; }
 
-        public ViewIllustPage(IllustItem illust)
+        private readonly IIllustCollectionPage collectionPage;
+        private readonly object fontIconLove;
+        private readonly object fontIconNotLove;
+
+        public ViewIllustPage(IllustItem illust, IIllustCollectionPage page)
         {
             IllustItem = illust;
+            collectionPage = page;
             BindingContext = this;
 
-            InitializeComponent();
-        }
+            fontIconLove = Application.Current.Resources[ThemeBase.FontIconLove];
+            fontIconNotLove = Application.Current.Resources[ThemeBase.FontIconNotLove];
+            if (page.Favorites != null)
+            {
+                SetValue(IsFavoriteProperty, page.Favorites.Any(i => i.Id == illust.Id)
+                    ? fontIconLove
+                    : fontIconNotLove);
+            }
+
+            InitializeComponent();
 
-        public override void OnLoad()
-        {
-            var illust = IllustItem;
             if (illust != null)
             {
                 LoadIllust(illust);
             }
         }
 
+        public override void OnLoad()
+        {
+            OnOrientationChanged(CurrentOrientation);
+        }
+
         private void LoadIllust(IllustItem illust)
         {
             if (illust == null)
@@ -95,7 +116,6 @@ namespace Pixiview.Illust
             }
 
             Illusts = items;
-            OnOrientationChanged(CurrentOrientation);
             Task.Run(DoLoadImages);
         }
 
@@ -134,6 +154,13 @@ namespace Pixiview.Illust
         protected override void OnDisappearing()
         {
             base.OnDisappearing();
+
+            var favorite = new IllustFavorite
+            {
+                LastFavoriteUtc = DateTime.UtcNow,
+                Illusts = collectionPage.Favorites.ToArray()
+            };
+            Stores.SaveFavoritesIllusts(favorite);
             Screen.SetHomeIndicatorAutoHidden(Shell.Current, false);
         }
 
@@ -220,6 +247,25 @@ namespace Pixiview.Illust
             item.Loading = false;
         }
 
+        private void Favorite_Clicked(object sender, EventArgs e)
+        {
+            if (collectionPage.Favorites == null)
+            {
+                return;
+            }
+            var index = collectionPage.Favorites.FindIndex(i => i.Id == IllustItem.Id);
+            if (index < 0)
+            {
+                collectionPage.Favorites.Insert(0, IllustItem);
+                SetValue(IsFavoriteProperty, fontIconLove);
+            }
+            else
+            {
+                collectionPage.Favorites.RemoveAt(index);
+                SetValue(IsFavoriteProperty, fontIconNotLove);
+            }
+        }
+
         private async void Download_Clicked(object sender, EventArgs e)
         {
             var status = await Permissions.CheckStatusAsync<Permissions.Photos>();
diff --git a/Pixiview/OptionPage.xaml b/Pixiview/OptionPage.xaml
index 03bdddf..2bf334a 100644
--- a/Pixiview/OptionPage.xaml
+++ b/Pixiview/OptionPage.xaml
@@ -2,10 +2,10 @@
 <u:AdaptedPage xmlns="http://xamarin.com/schemas/2014/forms"
                xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                xmlns:u="clr-namespace:Pixiview.UI"
-               xmlns:util="clr-namespace:Pixiview.Utils"
+               xmlns:r="clr-namespace:Pixiview.Resources"
                x:Class="Pixiview.OptionPage"
-               util:Screen.StatusBarStyle="{DynamicResource StatusBarStyle}"
-               BackgroundColor="{DynamicResource WindowColor}">
+               BackgroundColor="{DynamicResource WindowColor}"
+               Title="{r:Text Option}">
     <ContentPage.Content>
         <Label Text="Option" Margin="20"/>
     </ContentPage.Content>
diff --git a/Pixiview/Resources/Languages/zh-CN.xml b/Pixiview/Resources/Languages/zh-CN.xml
index 359d63e..1ae7584 100644
--- a/Pixiview/Resources/Languages/zh-CN.xml
+++ b/Pixiview/Resources/Languages/zh-CN.xml
@@ -7,6 +7,7 @@
     <Recommends>推荐</Recommends>
     <ByUser>按用户</ByUser>
     <Ranking>排行榜</Ranking>
+    <Favorites>收藏夹</Favorites>
     <Option>选项</Option>
     <Preview>预览</Preview>
     <SaveSuccess>成功保存图片到照片库。</SaveSuccess>
diff --git a/Pixiview/UI/AdaptedPage.cs b/Pixiview/UI/AdaptedPage.cs
index f3f5411..74f3c0a 100644
--- a/Pixiview/UI/AdaptedPage.cs
+++ b/Pixiview/UI/AdaptedPage.cs
@@ -1,4 +1,6 @@
 using System;
+using Pixiview.UI.Theme;
+using Pixiview.Utils;
 using Xamarin.Forms;
 
 namespace Pixiview.UI
@@ -13,6 +15,7 @@ namespace Pixiview.UI
 
         public AdaptedPage()
         {
+            SetDynamicResource(Screen.StatusBarStyleProperty, ThemeBase.StatusBarStyle);
             Shell.SetNavBarHasShadow(this, true);
         }
 
diff --git a/Pixiview/UI/StyleDefinition.cs b/Pixiview/UI/StyleDefinition.cs
index 76d939f..393cded 100644
--- a/Pixiview/UI/StyleDefinition.cs
+++ b/Pixiview/UI/StyleDefinition.cs
@@ -25,8 +25,10 @@ namespace Pixiview.UI
         public const string IconOrder = "\uf88f";
         public const string IconLayer = "\uf302";
         public const string IconRefresh = "\uf2f1";
+        public const string IconLove = "\uf004";
         public const string IconOption = "\uf013";
         public const string IconDownload = "\uf019";
+        public const string IconFavorite = "\uf02e";
 
         static StyleDefinition()
         {
diff --git a/Pixiview/UI/Theme/ThemeBase.cs b/Pixiview/UI/Theme/ThemeBase.cs
index 6ad95ec..b675f9e 100644
--- a/Pixiview/UI/Theme/ThemeBase.cs
+++ b/Pixiview/UI/Theme/ThemeBase.cs
@@ -10,8 +10,11 @@ namespace Pixiview.UI.Theme
         public const string FontIconSparkles = nameof(FontIconSparkles);
         public const string FontIconOrder = nameof(FontIconOrder);
         public const string FontIconRefresh = nameof(FontIconRefresh);
+        public const string FontIconLove = nameof(FontIconLove);
+        public const string FontIconNotLove = nameof(FontIconNotLove);
         public const string FontIconOption = nameof(FontIconOption);
         public const string FontIconDownload = nameof(FontIconDownload);
+        public const string FontIconFavorite = nameof(FontIconFavorite);
 
         public const string StatusBarStyle = nameof(StatusBarStyle);
         public const string WindowColor = nameof(WindowColor);
@@ -87,8 +90,11 @@ namespace Pixiview.UI.Theme
             Add(FontIconSparkles, GetSolidIcon(StyleDefinition.IconSparkles, solidFontFamily, mainColor));
             Add(FontIconOrder, GetSolidIcon(StyleDefinition.IconOrder, solidFontFamily, mainColor));
             Add(FontIconRefresh, GetSolidIcon(StyleDefinition.IconRefresh, solidFontFamily, mainColor));
+            Add(FontIconLove, GetSolidIcon(StyleDefinition.IconLove, solidFontFamily, mainColor));
+            Add(FontIconNotLove, GetSolidIcon(StyleDefinition.IconLove, (string)this[IconRegularFontFamily], mainColor));
             Add(FontIconOption, GetSolidIcon(StyleDefinition.IconOption, solidFontFamily, mainColor));
             Add(FontIconDownload, GetSolidIcon(StyleDefinition.IconDownload, solidFontFamily, mainColor));
+            Add(FontIconFavorite, GetSolidIcon(StyleDefinition.IconFavorite, solidFontFamily, mainColor));
         }
 
         private FontImageSource GetSolidIcon(string icon, string family, Color color)
diff --git a/Pixiview/Utils/IllustData.cs b/Pixiview/Utils/IllustData.cs
index 2442de9..8195602 100644
--- a/Pixiview/Utils/IllustData.cs
+++ b/Pixiview/Utils/IllustData.cs
@@ -1,4 +1,5 @@
 using Newtonsoft.Json;
+using Pixiview.Illust;
 
 namespace Pixiview.Utils
 {
@@ -89,6 +90,23 @@ namespace Pixiview.Utils
                         [JsonProperty("540x540")]
                         public string x540;
                     }
+
+                    public IllustItem ConvertToItem()
+                    {
+                        return new IllustItem
+                        {
+                            Id = illustId,
+                            ImageUrl = urls.x360 ?? url,
+                            Title = illustTitle,
+                            IsRestrict = xRestrict == 1,
+                            ProfileUrl = profileImageUrl,
+                            UserId = userId,
+                            UserName = userName,
+                            Width = width,
+                            Height = height,
+                            PageCount = pageCount
+                        };
+                    }
                 }
             }
 
diff --git a/Pixiview/Utils/Stores.cs b/Pixiview/Utils/Stores.cs
index 97e1d0b..cf3f1ba 100644
--- a/Pixiview/Utils/Stores.cs
+++ b/Pixiview/Utils/Stores.cs
@@ -5,6 +5,7 @@ using System.Net;
 using System.Net.Http;
 using System.Text;
 using Newtonsoft.Json;
+using Pixiview.Illust;
 using Xamarin.Essentials;
 using Xamarin.Forms;
 
@@ -14,12 +15,17 @@ 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 pagesFolder = "pages";
         private const string imageFolder = "img-original";
         private const string previewFolder = "img-master";
         private const string thumbFolder = "img-thumb";
         private const string userFolder = "user-profile";
+
         private const string illustFile = "illust.json";
+        private const string favoriteFile = "favorites.json";
+
+        private static readonly object sync = new object();
 
         public static bool NetworkAvailable
         {
@@ -87,6 +93,64 @@ namespace Pixiview.Utils
             }
         }
 
+        private static T ReadObject<T>(string file)
+        {
+            string content = null;
+            if (File.Exists(file))
+            {
+                try
+                {
+                    content = File.ReadAllText(file);
+                }
+                catch (Exception ex)
+                {
+                    App.DebugError("read", $"failed to read file: {file}, error: {ex.Message}");
+                }
+            }
+            else
+            {
+                App.DebugError("read", $"file not found: {file}");
+                return default;
+            }
+            try
+            {
+                return JsonConvert.DeserializeObject<T>(content);
+            }
+            catch (Exception ex)
+            {
+                App.DebugError("read", $"failed to parse illust JSON object, error: {ex.Message}");
+                return default;
+            }
+        }
+
+        private static void WriteObject(string file, object obj)
+        {
+            var dir = Path.GetDirectoryName(file);
+            if (!Directory.Exists(dir))
+            {
+                Directory.CreateDirectory(dir);
+            }
+            string content;
+            try
+            {
+                content = JsonConvert.SerializeObject(obj, Formatting.None);
+            }
+            catch (Exception ex)
+            {
+                App.DebugError("write", $"failed to serialize object, error: {ex.Message}");
+                return;
+            }
+
+            try
+            {
+                File.WriteAllText(file, content, Encoding.UTF8);
+            }
+            catch (Exception ex)
+            {
+                App.DebugError("write", $"failed to write file: {file}, error: {ex.Message}");
+            }
+        }
+
         public static IllustData LoadIllustData(bool force = false)
         {
             var file = Path.Combine(PersonalFolder, illustFile);
@@ -118,6 +182,21 @@ namespace Pixiview.Utils
             return result;
         }
 
+        public static IllustFavorite LoadFavoritesIllusts()
+        {
+            var file = Path.Combine(PersonalFolder, favoriteFile);
+            return ReadObject<IllustFavorite>(file);
+        }
+
+        public static void SaveFavoritesIllusts(IllustFavorite data)
+        {
+            var file = Path.Combine(PersonalFolder, favoriteFile);
+            lock (sync)
+            {
+                WriteObject(file, data);
+            }
+        }
+
         public static ImageSource LoadIllustImage(string url)
         {
             return LoadImage(url, PersonalFolder, imageFolder);
@@ -306,6 +385,7 @@ namespace Pixiview.Utils
         public const string Recommends = "recommends";
         public const string ByUser = "byuser";
         public const string Ranking = "ranking";
+        public const string Favorites = "favorites";
         public const string Option = "option";
     }
 }