From 59cc3a77c992f4b247754f2b6967f3cea8277f6e Mon Sep 17 00:00:00 2001
From: Tsanie Lily <tsorgy@gmail.com>
Date: Fri, 8 May 2020 14:27:15 +0800
Subject: [PATCH] feature: long press to save original illust

---
 .../Effects/LongPressEffectImplement.cs       |  58 ++++++
 Pixiview.iOS/Pixiview.iOS.csproj              |   2 +
 Pixiview.iOS/Services/EnvironmentService.cs   |  17 +-
 Pixiview/App.cs                               |   6 +-
 Pixiview/Illust/IllustCollectionPage.cs       |   4 +-
 Pixiview/Illust/ViewIllustPage.xaml           |  11 +-
 Pixiview/Illust/ViewIllustPage.xaml.cs        | 175 +++++++++++-------
 Pixiview/Resources/Languages/zh-CN.xml        |   6 +
 Pixiview/Resources/ResourceHelper.cs          |   6 +
 Pixiview/UI/AdaptedPage.cs                    |   8 +-
 Pixiview/Utils/Converters.cs                  |   1 -
 Pixiview/Utils/IEnvironmentService.cs         |   8 +-
 Pixiview/Utils/LongPressEffect.cs             |  26 +++
 Pixiview/Utils/Stores.cs                      |  11 +-
 14 files changed, 237 insertions(+), 102 deletions(-)
 create mode 100644 Pixiview.iOS/Effects/LongPressEffectImplement.cs
 create mode 100644 Pixiview/Utils/LongPressEffect.cs

diff --git a/Pixiview.iOS/Effects/LongPressEffectImplement.cs b/Pixiview.iOS/Effects/LongPressEffectImplement.cs
new file mode 100644
index 0000000..00f6ead
--- /dev/null
+++ b/Pixiview.iOS/Effects/LongPressEffectImplement.cs
@@ -0,0 +1,58 @@
+using System.Threading.Tasks;
+using Pixiview.iOS.Effects;
+using Pixiview.Utils;
+using UIKit;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.iOS;
+
+[assembly: ResolutionGroupName("Pixiview")]
+[assembly: ExportEffect(typeof(LongPressEffectImplement), "LongPressEffect")]
+namespace Pixiview.iOS.Effects
+{
+    public class LongPressEffectImplement : PlatformEffect
+    {
+        private bool attached;
+        private readonly UILongPressGestureRecognizer longPressGesture;
+
+        public LongPressEffectImplement()
+        {
+            longPressGesture = new UILongPressGestureRecognizer(OnLongPressed);
+        }
+
+        protected override void OnAttached()
+        {
+            if (!attached)
+            {
+                attached = true;
+                Container.AddGestureRecognizer(longPressGesture);
+            }
+        }
+
+        protected override void OnDetached()
+        {
+            if (attached)
+            {
+                attached = false;
+                Container.RemoveGestureRecognizer(longPressGesture);
+            }
+        }
+
+        private void OnLongPressed(UILongPressGestureRecognizer e)
+        {
+            if (e.State != UIGestureRecognizerState.Began)
+            {
+                return;
+            }
+            var element = Element;
+            if (element != null)
+            {
+                var command = LongPressEffect.GetCommand(element);
+                if (command != null)
+                {
+                    var o = LongPressEffect.GetCommandParameter(element);
+                    command.Execute(o);
+                }
+            }
+        }
+    }
+}
diff --git a/Pixiview.iOS/Pixiview.iOS.csproj b/Pixiview.iOS/Pixiview.iOS.csproj
index 8b5ff94..e93e687 100644
--- a/Pixiview.iOS/Pixiview.iOS.csproj
+++ b/Pixiview.iOS/Pixiview.iOS.csproj
@@ -79,6 +79,7 @@
     <Compile Include="Renderers\AppShellRenderer.cs" />
     <Compile Include="Renderers\AppShellSection\AppShellSectionRootHeader.cs" />
     <Compile Include="Renderers\SegmentedControlRenderer.cs" />
+    <Compile Include="Effects\LongPressEffectImplement.cs" />
   </ItemGroup>
   <ItemGroup>
     <InterfaceDefinition Include="Resources\LaunchScreen.storyboard" />
@@ -148,6 +149,7 @@
     <Folder Include="Renderers\" />
     <Folder Include="Services\" />
     <Folder Include="Renderers\AppShellSection\" />
+    <Folder Include="Effects\" />
   </ItemGroup>
   <ItemGroup>
     <BundleResource Include="Resources\fa-light-300.ttf" />
diff --git a/Pixiview.iOS/Services/EnvironmentService.cs b/Pixiview.iOS/Services/EnvironmentService.cs
index 9588281..307546e 100644
--- a/Pixiview.iOS/Services/EnvironmentService.cs
+++ b/Pixiview.iOS/Services/EnvironmentService.cs
@@ -29,29 +29,28 @@ namespace Pixiview.iOS.Services
         #region - Theme -
 
         [SuppressMessage("Code Notifications", "XI0002:Notifies you from using newer Apple APIs when targeting an older OS version", Justification = "<Pending>")]
-        public Theme GetApplicationTheme()
+        public OSAppTheme GetApplicationTheme()
         {
             if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
             {
                 var currentController = Platform.GetCurrentUIViewController();
                 if (currentController == null)
                 {
-                    return Theme.Light;
+                    return OSAppTheme.Unspecified;
                 }
+
                 var style = currentController.TraitCollection.UserInterfaceStyle;
                 if (style == UIUserInterfaceStyle.Dark)
                 {
-                    return Theme.Dark;
+                    return OSAppTheme.Dark;
                 }
-                else
+                else if (style == UIUserInterfaceStyle.Light)
                 {
-                    return Theme.Light;
+                    return OSAppTheme.Light;
                 }
             }
-            else
-            {
-                return Theme.Light;
-            }
+
+            return OSAppTheme.Unspecified;
         }
 
         public void SetStatusBarStyle(StatusBarStyles style)
diff --git a/Pixiview/App.cs b/Pixiview/App.cs
index f924c2f..31ab7ed 100644
--- a/Pixiview/App.cs
+++ b/Pixiview/App.cs
@@ -11,7 +11,7 @@ namespace Pixiview
     public class App : Application
     {
         // public properties
-        public static Theme CurrentTheme { get; private set; }
+        public static OSAppTheme CurrentTheme { get; private set; }
         public static PlatformCulture CurrentCulture { get; private set; }
         public static Dictionary<string, object> ExtraResources { get; private set; }
 
@@ -39,7 +39,7 @@ namespace Pixiview
             CurrentCulture = new PlatformCulture(ci.Name.ToLower());
         }
 
-        private void SetTheme(Theme theme, bool force = false)
+        private void SetTheme(OSAppTheme theme, bool force = false)
         {
             if (force || theme != CurrentTheme)
             {
@@ -51,7 +51,7 @@ namespace Pixiview
             }
             DebugPrint($"application theme: {theme}");
             ThemeBase themeInstance;
-            if (theme == Theme.Dark)
+            if (theme == OSAppTheme.Dark)
             {
                 themeInstance = DarkTheme.Instance;
             }
diff --git a/Pixiview/Illust/IllustCollectionPage.cs b/Pixiview/Illust/IllustCollectionPage.cs
index a0212ec..af674df 100644
--- a/Pixiview/Illust/IllustCollectionPage.cs
+++ b/Pixiview/Illust/IllustCollectionPage.cs
@@ -363,7 +363,6 @@ namespace Pixiview.Illust
 
     public class IllustCollection : List<IllustItem>
     {
-        private static readonly object sync = new object();
         private static IllustCollection empty;
 
         public static IllustCollection Empty
@@ -387,7 +386,8 @@ namespace Pixiview.Illust
             running = true;
         }
 
-        private bool running;
+        private readonly object sync = new object();
+        private volatile bool running;
         public bool Running
         {
             get => running;
diff --git a/Pixiview/Illust/ViewIllustPage.xaml b/Pixiview/Illust/ViewIllustPage.xaml
index 7420cbf..0dd2967 100644
--- a/Pixiview/Illust/ViewIllustPage.xaml
+++ b/Pixiview/Illust/ViewIllustPage.xaml
@@ -3,6 +3,7 @@
                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"
                ios:Page.UseSafeArea="False"
@@ -11,7 +12,7 @@
                Title="{Binding IllustItem.Title}">
     <ContentPage.ToolbarItems>
         <ToolbarItem Order="Primary" Clicked="Favorite_Clicked"
-                     IconImageSource="{Binding IsFavorite}"/>
+                     IconImageSource="{Binding FavoriteIcon}"/>
     </ContentPage.ToolbarItems>
     <Grid Padding="{Binding PageTopMargin}">
         <CarouselView ItemsSource="{Binding Illusts}" HorizontalScrollBarVisibility="Never"
@@ -24,7 +25,13 @@
                     <Grid>
                         <Image Source="{Binding Image}"
                                HorizontalOptions="Fill" VerticalOptions="Fill"
-                               Aspect="AspectFit"/>
+                               Aspect="AspectFit"
+                               util:LongPressEffect.Command="{Binding LongPressed}"
+                               util:LongPressEffect.CommandParameter="{Binding .}">
+                            <Image.Effects>
+                                <util:LongPressEffect/>
+                            </Image.Effects>
+                        </Image>
                         <Frame HasShadow="False" Margin="0" Padding="20" CornerRadius="8"
                                IsVisible="{Binding Loading}"
                                HorizontalOptions="Center" VerticalOptions="Center"
diff --git a/Pixiview/Illust/ViewIllustPage.xaml.cs b/Pixiview/Illust/ViewIllustPage.xaml.cs
index 5a743c5..11b46ef 100644
--- a/Pixiview/Illust/ViewIllustPage.xaml.cs
+++ b/Pixiview/Illust/ViewIllustPage.xaml.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Linq;
 using System.Threading.Tasks;
+using System.Windows.Input;
 using Pixiview.Resources;
 using Pixiview.UI;
 using Pixiview.UI.Theme;
@@ -13,8 +14,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 FavoriteIconProperty = BindableProperty.Create(
+            nameof(FavoriteIcon), typeof(ImageSource), typeof(ViewIllustPage));
         public static readonly BindableProperty IllustsProperty = BindableProperty.Create(
             nameof(Illusts), typeof(IllustDetailItem[]), typeof(ViewIllustPage));
         public static readonly BindableProperty PagePositionTextProperty = BindableProperty.Create(
@@ -24,7 +25,11 @@ namespace Pixiview.Illust
         public static readonly BindableProperty IllustItemProperty = BindableProperty.Create(
             nameof(IllustItem), typeof(IllustItem), typeof(ViewIllustPage));
 
-        public ImageSource IsFavorite => (ImageSource)GetValue(IsFavoriteProperty);
+        public ImageSource FavoriteIcon
+        {
+            get => (ImageSource)GetValue(FavoriteIconProperty);
+            set => SetValue(FavoriteIconProperty, value);
+        }
 
         public IllustDetailItem[] Illusts
         {
@@ -50,22 +55,24 @@ namespace Pixiview.Illust
         public int CurrentPage { get; private set; }
 
         private readonly IIllustCollectionPage collectionPage;
-        private readonly object fontIconLove;
-        private readonly object fontIconNotLove;
+        private readonly ICommand longPressed;
+        private readonly ImageSource fontIconLove;
+        private readonly ImageSource fontIconNotLove;
 
         public ViewIllustPage(IllustItem illust, IIllustCollectionPage page)
         {
             IllustItem = illust;
             collectionPage = page;
+            longPressed = new Command<IllustDetailItem>(Illust_LongPressed);
             BindingContext = this;
 
-            fontIconLove = Application.Current.Resources[ThemeBase.FontIconLove];
-            fontIconNotLove = Application.Current.Resources[ThemeBase.FontIconNotLove];
+            fontIconLove = (ImageSource)Application.Current.Resources[ThemeBase.FontIconLove];
+            fontIconNotLove = (ImageSource)Application.Current.Resources[ThemeBase.FontIconNotLove];
             if (page.Favorites != null)
             {
-                SetValue(IsFavoriteProperty, page.Favorites.Any(i => i.Id == illust.Id)
+                FavoriteIcon = page.Favorites.Any(i => i.Id == illust.Id)
                     ? fontIconLove
-                    : fontIconNotLove);
+                    : fontIconNotLove;
             }
 
             InitializeComponent();
@@ -81,38 +88,6 @@ namespace Pixiview.Illust
             OnOrientationChanged(CurrentOrientation);
         }
 
-        private void LoadIllust(IllustItem illust)
-        {
-            if (illust == null)
-            {
-                return;
-            }
-
-            var items = new IllustDetailItem[illust.PageCount];
-            if (items.Length > 1)
-            {
-                IsPageVisible = true;
-                PagePositionText = $"1/{items.Length}";
-            }
-            else
-            {
-                IsPageVisible = false;
-            }
-
-            for (var i = 0; i < items.Length; i++)
-            {
-                items[i] = new IllustDetailItem();
-                if (i == 0)
-                {
-                    items[i].Loading = true;
-                    items[i].Image = illust.Image;
-                }
-            }
-
-            Illusts = items;
-            Task.Run(DoLoadImages);
-        }
-
         protected override void OnAppearing()
         {
             base.OnAppearing();
@@ -135,27 +110,39 @@ namespace Pixiview.Illust
             Screen.SetHomeIndicatorAutoHidden(Shell.Current, false);
         }
 
-        private void CarouselView_PositionChanged(object sender, PositionChangedEventArgs e)
+        private void LoadIllust(IllustItem illust)
         {
-            var index = e.CurrentPosition;
-            CurrentPage = index;
-            var items = Illusts;
-            var length = items.Length;
-            PagePositionText = $"{index + 1}/{length}";
-
-            var item = items[index];
-            if (!item.Loading && item.Image == null)
+            if (illust == null)
             {
-                Task.Run(() => DoLoadImage(index));
+                return;
             }
-            if (index < length - 1)
+
+            var items = new IllustDetailItem[illust.PageCount];
+            if (items.Length > 1)
             {
-                item = items[index + 1];
-                if (!item.Loading && item.Image == null)
+                IsPageVisible = true;
+                PagePositionText = $"1/{items.Length}";
+            }
+            else
+            {
+                IsPageVisible = false;
+            }
+
+            for (var i = 0; i < items.Length; i++)
+            {
+                items[i] = new IllustDetailItem
                 {
-                    Task.Run(() => DoLoadImage(index + 1));
+                    LongPressed = longPressed
+                };
+                if (i == 0)
+                {
+                    items[i].Loading = true;
+                    items[i].Image = illust.Image;
                 }
             }
+
+            Illusts = items;
+            Task.Run(DoLoadImages);
         }
 
         private void DoLoadImages()
@@ -174,7 +161,10 @@ namespace Pixiview.Illust
                 items.CopyTo(items, 0);
                 for (var i = items.Length; i < tmp.Length; i++)
                 {
-                    tmp[i] = new IllustDetailItem();
+                    tmp[i] = new IllustDetailItem
+                    {
+                        LongPressed = longPressed
+                    };
                 }
                 items = tmp;
             }
@@ -221,6 +211,29 @@ namespace Pixiview.Illust
             item.Loading = false;
         }
 
+        private void CarouselView_PositionChanged(object sender, PositionChangedEventArgs e)
+        {
+            var index = e.CurrentPosition;
+            CurrentPage = index;
+            var items = Illusts;
+            var length = items.Length;
+            PagePositionText = $"{index + 1}/{length}";
+
+            var item = items[index];
+            if (!item.Loading && item.Image == null)
+            {
+                Task.Run(() => DoLoadImage(index));
+            }
+            if (index < length - 1)
+            {
+                item = items[index + 1];
+                if (!item.Loading && item.Image == null)
+                {
+                    Task.Run(() => DoLoadImage(index + 1));
+                }
+            }
+        }
+
         private void Favorite_Clicked(object sender, EventArgs e)
         {
             if (collectionPage.Favorites == null)
@@ -232,36 +245,55 @@ namespace Pixiview.Illust
             {
                 collectionPage.Favorites.Insert(0, IllustItem);
                 IllustItem.IsFavorite = true;
-                SetValue(IsFavoriteProperty, fontIconLove);
+                FavoriteIcon = fontIconLove;
             }
             else
             {
                 collectionPage.Favorites.RemoveAt(index);
                 IllustItem.IsFavorite = false;
-                SetValue(IsFavoriteProperty, fontIconNotLove);
+                FavoriteIcon = fontIconNotLove;
             }
         }
 
-        private async void Download_Clicked(object sender, EventArgs e)
+        private async void Illust_LongPressed(IllustDetailItem item)
         {
-            var status = await Permissions.CheckStatusAsync<Permissions.Photos>();
-            if (status != PermissionStatus.Granted)
+            var saveOriginal = ResourceHelper.SaveOriginal;
+            var result = await DisplayActionSheet(
+                IllustItem.Title,
+                ResourceHelper.Cancel,
+                saveOriginal);
+            if (result == saveOriginal)
             {
-                status = await Permissions.RequestAsync<Permissions.Photos>();
+                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)
                 {
-                    App.DebugPrint("access denied to gallery.");
+                    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));
             }
-
-            var item = Illusts[CurrentPage];
-            if (item.Downloading)
-            {
-                return;
-            }
-            item.Downloading = true;
-            _ = Task.Run(() => DoLoadOriginalImage(item));
         }
 
         private void DoLoadOriginalImage(IllustDetailItem item)
@@ -306,6 +338,7 @@ namespace Pixiview.Illust
             get => (bool)GetValue(DownloadingProperty);
             set => SetValue(DownloadingProperty, value);
         }
+        public ICommand LongPressed { get; set; }
         public string PreviewUrl { get; set; }
         public string OriginalUrl { get; set; }
     }
diff --git a/Pixiview/Resources/Languages/zh-CN.xml b/Pixiview/Resources/Languages/zh-CN.xml
index 7f595a9..5cad518 100644
--- a/Pixiview/Resources/Languages/zh-CN.xml
+++ b/Pixiview/Resources/Languages/zh-CN.xml
@@ -2,6 +2,9 @@
 <root>
     <Title>Pixiview</Title>
     <Ok>OK</Ok>
+    <Cancel>取消</Cancel>
+    <Yes>是</Yes>
+    <No>否</No>
     <R18>R-18</R18>
     <Follow>已关注</Follow>
     <Recommends>推荐</Recommends>
@@ -11,5 +14,8 @@
     <Favorites>收藏夹</Favorites>
     <Option>选项</Option>
     <Preview>预览</Preview>
+    <Operation>操作</Operation>
+    <SaveOriginal>保存原图</SaveOriginal>
     <SaveSuccess>成功保存图片到照片库。</SaveSuccess>
+    <AlreadySavedQuestion>原图已保存,是否继续?</AlreadySavedQuestion>
 </root>
\ No newline at end of file
diff --git a/Pixiview/Resources/ResourceHelper.cs b/Pixiview/Resources/ResourceHelper.cs
index fa1dc1a..f0722d7 100644
--- a/Pixiview/Resources/ResourceHelper.cs
+++ b/Pixiview/Resources/ResourceHelper.cs
@@ -12,8 +12,14 @@ namespace Pixiview.Resources
     {
         public static string Title => GetResource(nameof(Title));
         public static string Ok => GetResource(nameof(Ok));
+        public static string Cancel => GetResource(nameof(Cancel));
+        public static string Yes => GetResource(nameof(Yes));
+        public static string No => GetResource(nameof(No));
         public static string R18 => GetResource(nameof(R18));
+        public static string Operation => GetResource(nameof(Operation));
+        public static string SaveOriginal => GetResource(nameof(SaveOriginal));
         public static string SaveSuccess => GetResource(nameof(SaveSuccess));
+        public static string AlreadySavedQuestion => GetResource(nameof(AlreadySavedQuestion));
 
         static readonly Dictionary<string, LanguageResource> dict = new Dictionary<string, LanguageResource>();
 
diff --git a/Pixiview/UI/AdaptedPage.cs b/Pixiview/UI/AdaptedPage.cs
index 453cbe5..abae771 100644
--- a/Pixiview/UI/AdaptedPage.cs
+++ b/Pixiview/UI/AdaptedPage.cs
@@ -83,16 +83,12 @@ namespace Pixiview.UI
             public static bool IsBusy => _instance?.isBusy == true;
 
             private static readonly object sync = new object();
-            private static Tap _instance;
+            private static readonly Tap _instance = new Tap();
 
             private Tap() { }
 
             public static Tap Start()
             {
-                if (_instance == null)
-                {
-                    _instance = new Tap();
-                }
                 lock (sync)
                 {
                     _instance.isBusy = true;
@@ -100,7 +96,7 @@ namespace Pixiview.UI
                 return _instance;
             }
 
-            private bool isBusy = false;
+            private volatile bool isBusy = false;
 
             public void Dispose()
             {
diff --git a/Pixiview/Utils/Converters.cs b/Pixiview/Utils/Converters.cs
index b8fe024..1dbaeda 100644
--- a/Pixiview/Utils/Converters.cs
+++ b/Pixiview/Utils/Converters.cs
@@ -1,7 +1,6 @@
 using System;
 using System.Globalization;
 using Pixiview.UI;
-using Pixiview.UI.Theme;
 using Xamarin.Forms;
 
 namespace Pixiview.Utils
diff --git a/Pixiview/Utils/IEnvironmentService.cs b/Pixiview/Utils/IEnvironmentService.cs
index 015681b..ea7c9da 100644
--- a/Pixiview/Utils/IEnvironmentService.cs
+++ b/Pixiview/Utils/IEnvironmentService.cs
@@ -7,7 +7,7 @@ namespace Pixiview.Utils
     {
         EnvironmentParameter GetEnvironment();
 
-        Theme GetApplicationTheme();
+        OSAppTheme GetApplicationTheme();
         void SetStatusBarStyle(StatusBarStyles style);
         void SetStatusBarColor(Color color);
 
@@ -22,10 +22,4 @@ namespace Pixiview.Utils
         public string IconSolidFontFamily { get; set; }
         public string IconLeft { get; set; }
     }
-
-    public enum Theme
-    {
-        Light,
-        Dark
-    }
 }
diff --git a/Pixiview/Utils/LongPressEffect.cs b/Pixiview/Utils/LongPressEffect.cs
new file mode 100644
index 0000000..e01c879
--- /dev/null
+++ b/Pixiview/Utils/LongPressEffect.cs
@@ -0,0 +1,26 @@
+using System.Windows.Input;
+using Xamarin.Forms;
+
+namespace Pixiview.Utils
+{
+    public class LongPressEffect : RoutingEffect
+    {
+        private const string Command = nameof(Command);
+        private const string CommandParameter = nameof(CommandParameter);
+
+        public static readonly BindableProperty CommandProperty = BindableProperty.CreateAttached(
+            Command, typeof(ICommand), typeof(LongPressEffect), null);
+        public static readonly BindableProperty CommandParameterProperty = BindableProperty.CreateAttached(
+            CommandParameter, typeof(object), typeof(LongPressEffect), null);
+
+        public static ICommand GetCommand(BindableObject view) => (ICommand)view.GetValue(CommandProperty);
+        public static void SetCommand(BindableObject view, ICommand command) => view.SetValue(CommandProperty, command);
+
+        public static object GetCommandParameter(BindableObject view) => view.GetValue(CommandParameterProperty);
+        public static void SetCommandParameter(BindableObject view, object value) => view.SetValue(CommandParameterProperty, value);
+
+        public LongPressEffect() : base("Pixiview.LongPressEffect")
+        {
+        }
+    }
+}
diff --git a/Pixiview/Utils/Stores.cs b/Pixiview/Utils/Stores.cs
index 6080675..ac7f4d3 100644
--- a/Pixiview/Utils/Stores.cs
+++ b/Pixiview/Utils/Stores.cs
@@ -182,7 +182,10 @@ namespace Pixiview.Utils
         public static IllustFavorite LoadFavoritesIllusts()
         {
             var file = Path.Combine(PersonalFolder, favoriteFile);
-            return ReadObject<IllustFavorite>(file);
+            lock (sync)
+            {
+                return ReadObject<IllustFavorite>(file);
+            }
         }
 
         public static void SaveFavoritesIllusts(IllustFavorite data)
@@ -214,6 +217,12 @@ namespace Pixiview.Utils
             return LoadImage(url, CacheFolder, userFolder);
         }
 
+        public static bool CheckIllustImage(string url)
+        {
+            var file = Path.Combine(PersonalFolder, imageFolder, Path.GetFileName(url));
+            return File.Exists(file);
+        }
+
         private static ImageSource LoadImage(string url, string working, string folder)
         {
             var file = Path.Combine(working, folder, Path.GetFileName(url));