From fa0b033f2aa60efc7ffbcca2c149835a5ab550e0 Mon Sep 17 00:00:00 2001 From: Tsanie Date: Thu, 12 Aug 2021 16:27:42 +0800 Subject: [PATCH] favorite & view page & etc. --- Gallery.Share/App.cs | 4 +- Gallery.Share/AppShell.xaml.cs | 5 +- Gallery.Share/Gallery.Share.projitems | 2 + Gallery.Share/Resources/Languages/zh-CN.xml | 1 + Gallery.Share/Resources/Theme/DarkTheme.cs | 1 + Gallery.Share/Resources/Theme/LightTheme.cs | 1 + Gallery.Share/Resources/Theme/Theme.cs | 7 + Gallery.Share/Resources/UI/Definition.cs | 1 + .../Resources/UI/GalleryCollectionPage.cs | 50 +++-- .../Sources/Danbooru/GallerySource.cs | 20 +- .../Sources/FavoriteGallerySource.cs | 41 ++++ .../Sources/Gelbooru/GallerySource.cs | 52 ++++- .../Sources/Yandere/GallerySource.cs | 20 +- Gallery.Share/Util/Consts.cs | 30 +++ Gallery.Share/Util/Extensions.cs | 10 + .../Util/Interface/IGallerySource.cs | 24 ++- Gallery.Share/Util/Model/GalleryItem.cs | 4 +- Gallery.Share/Util/NetHelper.cs | 15 +- Gallery.Share/Util/Store.cs | 162 +++++++++++--- Gallery.Share/Views/GalleryItemPage.xaml | 35 ++- Gallery.Share/Views/GalleryItemPage.xaml.cs | 204 +++++++++++++++++- Gallery.Share/Views/GalleryPage.xaml | 5 + Gallery.Share/Views/GalleryPage.xaml.cs | 57 ++++- .../IconBookmark.imageset/Contents.json | 26 +++ .../IconBookmark.imageset/bookmark-solid.png | Bin 0 -> 244 bytes .../bookmark-solid@2x.png | Bin 0 -> 339 bytes .../bookmark-solid@3x.png | Bin 0 -> 458 bytes .../Contents.json | 26 +++ .../bookmark-regular.png | Bin 0 -> 287 bytes .../bookmark-regular@2x.png | Bin 0 -> 432 bytes .../bookmark-regular@3x.png | Bin 0 -> 595 bytes Gallery.iOS/Gallery.iOS.csproj | 10 + .../AppShellSection/AppAppearanceTracker.cs | 9 +- 33 files changed, 728 insertions(+), 94 deletions(-) create mode 100644 Gallery.Share/Sources/FavoriteGallerySource.cs create mode 100644 Gallery.Share/Util/Consts.cs create mode 100644 Gallery.iOS/Assets.xcassets/IconBookmark.imageset/Contents.json create mode 100644 Gallery.iOS/Assets.xcassets/IconBookmark.imageset/bookmark-solid.png create mode 100644 Gallery.iOS/Assets.xcassets/IconBookmark.imageset/bookmark-solid@2x.png create mode 100644 Gallery.iOS/Assets.xcassets/IconBookmark.imageset/bookmark-solid@3x.png create mode 100644 Gallery.iOS/Assets.xcassets/IconBookmarkRegular.imageset/Contents.json create mode 100644 Gallery.iOS/Assets.xcassets/IconBookmarkRegular.imageset/bookmark-regular.png create mode 100644 Gallery.iOS/Assets.xcassets/IconBookmarkRegular.imageset/bookmark-regular@2x.png create mode 100644 Gallery.iOS/Assets.xcassets/IconBookmarkRegular.imageset/bookmark-regular@3x.png diff --git a/Gallery.Share/App.cs b/Gallery.Share/App.cs index 4eada77..d2a92e9 100644 --- a/Gallery.Share/App.cs +++ b/Gallery.Share/App.cs @@ -107,9 +107,11 @@ namespace Gallery GallerySources = new List() { + new Sources.FavoriteGallerySource(), + new Sources.Yandere.GallerySource(), // https://yande.re new Sources.Danbooru.GallerySource(), // https://danbooru.donmai.us - new Sources.Gelbooru.GallerySource() // https://gelbooru.com + new Sources.Gelbooru.GallerySource(), // https://gelbooru.com }; MainPage = new AppShell(); diff --git a/Gallery.Share/AppShell.xaml.cs b/Gallery.Share/AppShell.xaml.cs index 9587d62..32bdffe 100644 --- a/Gallery.Share/AppShell.xaml.cs +++ b/Gallery.Share/AppShell.xaml.cs @@ -1,4 +1,5 @@ -using Gallery.Resources.UI; +using Gallery.Resources; +using Gallery.Resources.UI; using Gallery.Util; using Gallery.Util.Interface; using Gallery.Views; @@ -36,7 +37,7 @@ namespace Gallery } var tab = new Tab { - Title = source.Name, + Title = Helper.GetResource(source.Name), Route = source.Route, Items = { diff --git a/Gallery.Share/Gallery.Share.projitems b/Gallery.Share/Gallery.Share.projitems index d0dabed..da561fe 100644 --- a/Gallery.Share/Gallery.Share.projitems +++ b/Gallery.Share/Gallery.Share.projitems @@ -45,6 +45,8 @@ + + diff --git a/Gallery.Share/Resources/Languages/zh-CN.xml b/Gallery.Share/Resources/Languages/zh-CN.xml index 352d9b2..ad967b7 100644 --- a/Gallery.Share/Resources/Languages/zh-CN.xml +++ b/Gallery.Share/Resources/Languages/zh-CN.xml @@ -11,4 +11,5 @@ 详细 代理主机 代理端口 + 收藏夹 \ No newline at end of file diff --git a/Gallery.Share/Resources/Theme/DarkTheme.cs b/Gallery.Share/Resources/Theme/DarkTheme.cs index d1d4e95..0938a4f 100644 --- a/Gallery.Share/Resources/Theme/DarkTheme.cs +++ b/Gallery.Share/Resources/Theme/DarkTheme.cs @@ -32,6 +32,7 @@ namespace Gallery.Resources.Theme Add(TextColor, Color.White); Add(SubTextColor, Color.LightGray); Add(CardBackgroundColor, Color.FromRgb(0x33, 0x33, 0x33)); + Add(MaskColor, Color.FromRgba(0, 0, 0, 0x64)); Add(NavigationColor, Color.FromRgb(0x11, 0x11, 0x11)); Add(NavigationSelectedColor, Color.FromRgb(0x22, 0x22, 0x22)); Add(OptionBackColor, Color.Black); diff --git a/Gallery.Share/Resources/Theme/LightTheme.cs b/Gallery.Share/Resources/Theme/LightTheme.cs index 2e23760..af44c67 100644 --- a/Gallery.Share/Resources/Theme/LightTheme.cs +++ b/Gallery.Share/Resources/Theme/LightTheme.cs @@ -32,6 +32,7 @@ namespace Gallery.Resources.Theme Add(TextColor, Color.Black); Add(SubTextColor, Color.DimGray); Add(CardBackgroundColor, Color.FromRgb(0xf3, 0xf3, 0xf3)); + Add(MaskColor, Color.FromRgba(0, 0, 0, 0x64)); Add(NavigationColor, Color.FromRgb(0xf0, 0xf0, 0xf0)); Add(NavigationSelectedColor, Color.LightGray); Add(OptionBackColor, Color.FromRgb(0xf0, 0xf0, 0xf0)); diff --git a/Gallery.Share/Resources/Theme/Theme.cs b/Gallery.Share/Resources/Theme/Theme.cs index 044e5d0..721b366 100644 --- a/Gallery.Share/Resources/Theme/Theme.cs +++ b/Gallery.Share/Resources/Theme/Theme.cs @@ -11,6 +11,7 @@ namespace Gallery.Resources.Theme public const string TextColor = nameof(TextColor); public const string SubTextColor = nameof(SubTextColor); public const string CardBackgroundColor = nameof(CardBackgroundColor); + public const string MaskColor = nameof(MaskColor); public const string NavigationColor = nameof(NavigationColor); public const string NavigationSelectedColor = nameof(NavigationSelectedColor); public const string OptionBackColor = nameof(OptionBackColor); @@ -24,6 +25,9 @@ namespace Gallery.Resources.Theme public const string IconClose = nameof(IconClose); public const string FontIconOption = nameof(FontIconOption); public const string FontIconRefresh = nameof(FontIconRefresh); + public const string FontIconLove = nameof(FontIconLove); + public const string FontIconNotLove = nameof(FontIconNotLove); + public const string FontIconShare = nameof(FontIconShare); protected void InitResources() { @@ -34,6 +38,9 @@ namespace Gallery.Resources.Theme Add(FontIconOption, GetFontIcon(Definition.IconOption, Definition.IconSolidFamily)); Add(FontIconRefresh, GetFontIcon(Definition.IconRefresh, Definition.IconSolidFamily)); + Add(FontIconLove, GetFontIcon(Definition.IconLove, Definition.IconSolidFamily, color: Definition.ColorRedBackground)); + Add(FontIconNotLove, GetFontIcon(Definition.IconLove, Definition.IconRegularFamily, color: Definition.ColorRedBackground)); + Add(FontIconShare, GetFontIcon(Definition.IconShare, Definition.IconSolidFamily)); Add(IconClose, Definition.IconClose); } diff --git a/Gallery.Share/Resources/UI/Definition.cs b/Gallery.Share/Resources/UI/Definition.cs index 1c5d5a7..18f23bd 100644 --- a/Gallery.Share/Resources/UI/Definition.cs +++ b/Gallery.Share/Resources/UI/Definition.cs @@ -36,6 +36,7 @@ namespace Gallery.Resources.UI public const string IconLove = "\uf004"; public const string IconCircleLove = "\uf4c7"; public const string IconClose = "\uf057"; + public const string IconShare = "\uf1e0"; static Definition() { diff --git a/Gallery.Share/Resources/UI/GalleryCollectionPage.cs b/Gallery.Share/Resources/UI/GalleryCollectionPage.cs index 21d5be5..7337c55 100644 --- a/Gallery.Share/Resources/UI/GalleryCollectionPage.cs +++ b/Gallery.Share/Resources/UI/GalleryCollectionPage.cs @@ -10,7 +10,7 @@ using Xamarin.Forms; namespace Gallery.Resources.UI { - public abstract class GalleryCollectionPage : GalleryScrollableCollectionPage + public abstract class GalleryCollectionPage : GalleryScrollableCollectionPage> { public GalleryCollectionPage(IGallerySource source) : base(source) { } } @@ -93,11 +93,11 @@ namespace Gallery.Resources.UI } } - protected readonly Command commandGalleryItemTapped; protected double topOffset; protected string lastError; private readonly object sync = new(); + private readonly Command commandGalleryItemTapped; private readonly Stack tasks = new(); private T galleryData; @@ -107,17 +107,8 @@ namespace Gallery.Resources.UI commandGalleryItemTapped = new Command(OnGalleryItemTapped); } - private void OnGalleryItemTapped(GalleryItem item) + protected virtual void OnGalleryItemTapped(GalleryItem item) { - if (item == null) - { - return; - } - //Start(async () => - //{ - // var page = new GalleryItemPage(item); - // await Navigation.PushAsync(page); - //}); } public override void OnUnload() @@ -145,6 +136,14 @@ namespace Gallery.Resources.UI { StartLoading(); } + else if (GalleryCollection != null) + { + var favorites = Store.FavoriteList; + foreach (var item in GalleryCollection) + { + item.IsFavorite = favorites.Any(i => i.SourceEquals(item)); + } + } } #if __IOS__ @@ -217,6 +216,17 @@ namespace Gallery.Resources.UI { if (force || Expired) { + if (!isBottom) + { + lock (sync) + { + // destory all the tasks + while (tasks.TryPop(out var t)) + { + t?.Dispose(); + } + } + } var indicator = LoadingIndicator; if (indicator == null || isBottom) { @@ -257,6 +267,7 @@ namespace Gallery.Resources.UI _ = DoloadGallerySource(force, isBottom); }); } + BeforeLoading(); } } @@ -276,6 +287,7 @@ namespace Gallery.Resources.UI #endif { Gallery = collection; + AfterLoaded(); return false; }); } @@ -301,12 +313,16 @@ namespace Gallery.Resources.UI #endif { Gallery = collection; + AfterLoaded(); return false; }); }); } } + protected virtual void BeforeLoading() { } + protected virtual void AfterLoaded() { } + protected async Task ScrollToTopAsync(ScrollView scrollView) { if (scrollView.ScrollY > -topOffset) @@ -425,16 +441,18 @@ namespace Gallery.Resources.UI var data = DoGetGalleryList(galleryData, out int tag).Where(i => i != null); var collection = new GalleryCollection(data); + var favorites = Store.FavoriteList; foreach (var item in collection) { if (item.PreviewImage == null) { - var image = await Store.LoadPreviewImage(item.PreviewUrl, false); + var image = await Store.LoadPreviewImage(item, false); if (image != null) { item.PreviewImage = image; } } + item.IsFavorite = favorites.Any(i => i.SourceEquals(item)); } DoGalleryLoaded(collection, bottom); @@ -473,7 +491,7 @@ namespace Gallery.Resources.UI if (model.StartsWith("iPhone") || model.StartsWith("iPad")) { #endif - var image = Store.LoadPreviewImage(item.PreviewUrl, true, force: true).Result; + var image = Store.LoadPreviewImage(item, true, force: true).Result; if (image != null) { item.PreviewImage = image; @@ -539,7 +557,7 @@ namespace Gallery.Resources.UI { lastRefreshY = double.MinValue; } - base.StartLoading(force, isBottom); + base.StartLoading(force: force, isBottom: isBottom); } protected override GalleryCollection FilterGalleryCollection(GalleryCollection collection, bool bottom) @@ -578,7 +596,7 @@ namespace Gallery.Resources.UI #if DEBUG Log.Print("start to load next page"); #endif - StartLoading(true, true); + StartLoading(force: true, isBottom: true); } } } diff --git a/Gallery.Share/Sources/Danbooru/GallerySource.cs b/Gallery.Share/Sources/Danbooru/GallerySource.cs index 70c0ca3..3fc33bf 100644 --- a/Gallery.Share/Sources/Danbooru/GallerySource.cs +++ b/Gallery.Share/Sources/Danbooru/GallerySource.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading.Tasks; using Gallery.Util; @@ -8,14 +9,14 @@ using Xamarin.Forms; namespace Gallery.Sources.Danbooru { - public class GallerySource : IGallerySource + public class GallerySource : GallerySourceBase { - public string Name => "Danbooru"; - public string Route => "danbooru"; - public string FlyoutIconKey => "Danbooru"; - public string HomePage => "https://danbooru.donmai.us"; + public override string Name => "Danbooru"; + public override string Route => "danbooru"; + public override string FlyoutIconKey => "Danbooru"; + public override string HomePage => "https://danbooru.donmai.us"; - public async Task GetRecentItemsAsync(int page) + public override async Task> GetRecentItemsAsync(int page) { var url = $"https://danbooru.donmai.us/posts?page={page}"; var (result, error) = await NetHelper.RequestObject(url, @return: content => ResolveGalleryItems(content)); @@ -47,7 +48,8 @@ namespace Gallery.Sources.Danbooru Width = int.Parse(g[3].Value), Height = int.Parse(g[4].Value), UserId = g[6].Value, - Source = g[7].Value, + Source = Route, + SourceUrl = g[7].Value, RawUrl = g[8].Value, PreviewUrl = g[9].Value }; @@ -55,12 +57,12 @@ namespace Gallery.Sources.Danbooru return items; } - public void SetCookie() + public override void SetCookie() { throw new NotImplementedException(); } - public void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark) + public override void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark) { var icon = new FontImageSource { diff --git a/Gallery.Share/Sources/FavoriteGallerySource.cs b/Gallery.Share/Sources/FavoriteGallerySource.cs new file mode 100644 index 0000000..b418c5f --- /dev/null +++ b/Gallery.Share/Sources/FavoriteGallerySource.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Gallery.Util; +using Gallery.Util.Interface; +using Gallery.Util.Model; +using Xamarin.Forms; + +namespace Gallery.Sources +{ + public class FavoriteGallerySource : GallerySourceBase + { + public override string Name => "Favorite"; + public override string Route => Routes.Favorite; + public override string FlyoutIconKey => "Favorite"; + public override string HomePage => null; + public override bool IsScrollable => false; + + public override Task> GetRecentItemsAsync(int page) + { + return Task.FromResult((IEnumerable)Store.FavoriteList); + } + + public override void SetCookie() + { + throw new NotImplementedException(); + } + + public override void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark) + { + var icon = new FontImageSource + { + FontFamily = family, + Glyph = "\uf02e", + Size = 18.0 + }; + light.Add(FlyoutIconKey, icon); + dark.Add(FlyoutIconKey, icon); + } + } +} diff --git a/Gallery.Share/Sources/Gelbooru/GallerySource.cs b/Gallery.Share/Sources/Gelbooru/GallerySource.cs index d9d06f6..54d6495 100644 --- a/Gallery.Share/Sources/Gelbooru/GallerySource.cs +++ b/Gallery.Share/Sources/Gelbooru/GallerySource.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading.Tasks; using Gallery.Util; @@ -8,14 +9,14 @@ using Xamarin.Forms; namespace Gallery.Sources.Gelbooru { - public class GallerySource : IGallerySource + public class GallerySource : GallerySourceBase { - public string Name => "Gelbooru"; - public string Route => "gelbooru"; - public string FlyoutIconKey => "Gelbooru"; - public string HomePage => "https://gelbooru.com"; + public override string Name => "Gelbooru"; + public override string Route => "gelbooru"; + public override string FlyoutIconKey => "Gelbooru"; + public override string HomePage => "https://gelbooru.com"; - public async Task GetRecentItemsAsync(int page) + public override async Task> GetRecentItemsAsync(int page) { var offset = (page - 1) * 42; var url = $"https://gelbooru.com/index.php?page=post&s=list&tags=all&pid={offset}"; @@ -42,21 +43,54 @@ namespace Gallery.Sources.Gelbooru var g = matches[i].Groups; items[i] = new GalleryItem(int.Parse(g[2].Value)) { - RawUrl = g[3].Value, + RawUrl = g[3].Value.Replace("&", "&"), PreviewUrl = g[5].Value, Tags = g[6].Value.Split(' '), + Source = Route, IsRawPage = true }; } return items; } - public void SetCookie() + public override async Task ResolveImageUrl(GalleryItem item) + { + var (result, error) = await NetHelper.RequestObject(item.RawUrl, @return: content => + { + var regex = new Regex( + @"
"Yande.re"; - public string Route => "yandere"; - public string FlyoutIconKey => "Yandere"; - public string HomePage => "https://yande.re"; + public override string Name => "Yande.re"; + public override string Route => "yandere"; + public override string FlyoutIconKey => "Yandere"; + public override string HomePage => "https://yande.re"; - public async Task GetRecentItemsAsync(int page) + public override async Task> GetRecentItemsAsync(int page) { var url = $"https://yande.re/post?page={page}"; var (result, error) = await NetHelper.RequestObject(url, contentHandler: ContentHandler); @@ -37,7 +38,8 @@ namespace Gallery.Sources.Yandere UpdatedTime = y.updated_at.ToLocalTime(), UserId = y.creator_id.ToString(), UserName = y.author, - Source = y.source, + Source = Route, + SourceUrl = y.source, PreviewUrl = y.preview_url, RawUrl = y.file_url, Width = y.width, @@ -60,12 +62,12 @@ namespace Gallery.Sources.Yandere return $"[{string.Join(',', array)}]"; } - public void SetCookie() + public override void SetCookie() { throw new NotImplementedException(); } - public void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark) + public override void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark) { var icon = new FontImageSource { diff --git a/Gallery.Share/Util/Consts.cs b/Gallery.Share/Util/Consts.cs new file mode 100644 index 0000000..14de70f --- /dev/null +++ b/Gallery.Share/Util/Consts.cs @@ -0,0 +1,30 @@ +using System; +using System.Net; + +namespace Gallery.Util +{ + public static class Config + { + public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30); + + public const string DownloadThreadsKey = "download_threads"; + public const string IsProxiedKey = "is_proxied"; + public const string ProxyHostKey = "proxy_host"; + public const string ProxyPortKey = "proxy_port"; + + public const int MaxThreads = 4; + public const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"; + public const string AcceptLanguage = "zh-cn"; + public const string AcceptImage = "image/png,image/*,*/*;q=0.8"; + + public static int DownloadThreads; + public static WebProxy Proxy; + } + + public static class Routes + { + public const string Gallery = "gallery"; + public const string Favorite = "favorite"; + public const string Option = "option"; + } +} diff --git a/Gallery.Share/Util/Extensions.cs b/Gallery.Share/Util/Extensions.cs index e91ee32..e2cd331 100644 --- a/Gallery.Share/Util/Extensions.cs +++ b/Gallery.Share/Util/Extensions.cs @@ -1,4 +1,5 @@ using System; +using Gallery.Util.Model; namespace Gallery.Util { @@ -63,5 +64,14 @@ namespace Gallery.Util var ticks = datetime.Ticks; return (ticks - 621355968000000000L) / 10000000; } + + public static bool SourceEquals(this GalleryItem a, GalleryItem b) + { + if (a == null || b == null) + { + return false; + } + return a.Id == b.Id && a.Source == b.Source; + } } } diff --git a/Gallery.Share/Util/Interface/IGallerySource.cs b/Gallery.Share/Util/Interface/IGallerySource.cs index 6a489c2..9ef67a4 100644 --- a/Gallery.Share/Util/Interface/IGallerySource.cs +++ b/Gallery.Share/Util/Interface/IGallerySource.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using Gallery.Util.Model; using Xamarin.Forms; @@ -7,17 +8,28 @@ namespace Gallery.Util.Interface public interface IGallerySource { string Name { get; } - string Route { get; } - string FlyoutIconKey { get; } - string HomePage { get; } + bool IsScrollable { get; } void SetCookie(); - void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark); + Task> GetRecentItemsAsync(int page); + Task ResolveImageUrl(GalleryItem item); + } - Task GetRecentItemsAsync(int page); + public abstract class GallerySourceBase : IGallerySource + { + public abstract string Name { get; } + public abstract string Route { get; } + public abstract string FlyoutIconKey { get; } + public abstract string HomePage { get; } + public virtual bool IsScrollable => true; + + public abstract void SetCookie(); + public abstract void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark); + public abstract Task> GetRecentItemsAsync(int page); + public virtual Task ResolveImageUrl(GalleryItem item) => Task.FromResult(item.RawUrl); } } diff --git a/Gallery.Share/Util/Model/GalleryItem.cs b/Gallery.Share/Util/Model/GalleryItem.cs index ed5cbb8..54fe389 100644 --- a/Gallery.Share/Util/Model/GalleryItem.cs +++ b/Gallery.Share/Util/Model/GalleryItem.cs @@ -80,6 +80,8 @@ namespace Gallery.Util.Model [JsonProperty] public string Source { get; set; } [JsonProperty] + public string SourceUrl { get; set; } + [JsonProperty] public string PreviewUrl { get; set; } [JsonProperty] public string RawUrl { get; set; } @@ -124,7 +126,7 @@ namespace Gallery.Util.Model public override string ToString() { - var source = string.IsNullOrEmpty(Source) ? RawUrl : Source; + var source = string.IsNullOrEmpty(SourceUrl) ? RawUrl : SourceUrl; return $"{Id}, {source}"; } } diff --git a/Gallery.Share/Util/NetHelper.cs b/Gallery.Share/Util/NetHelper.cs index 75ffced..9060a9c 100644 --- a/Gallery.Share/Util/NetHelper.cs +++ b/Gallery.Share/Util/NetHelper.cs @@ -125,7 +125,7 @@ namespace Gallery.Util } } - public static async Task DownloadImageAsync(string url, string id, string working, string folder) + public static async Task DownloadImageAsync(string url, string id, string working, string folder, Action<(int loc, int size)> action = null) { try { @@ -149,7 +149,7 @@ namespace Gallery.Util { Timeout = Config.Timeout }; - long size; + int size; DateTimeOffset lastModified; using (var request = new HttpRequestMessage(HttpMethod.Head, url)) { @@ -158,7 +158,7 @@ namespace Gallery.Util headers.Add("Accept-Language", Config.AcceptLanguage); headers.Add("User-Agent", Config.UserAgent); using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); - size = response.Content.Headers.ContentLength.Value; + size = (int)response.Content.Headers.ContentLength.Value; lastModified = response.Content.Headers.LastModified.Value; #if DEBUG Log.Print($"content length: {size:n0} bytes, last modified: {lastModified}"); @@ -167,10 +167,10 @@ namespace Gallery.Util // segments const int SIZE = 150000; - var list = new List<(long from, long to)>(); - for (long i = 0; i < size; i += SIZE) + var list = new List<(int from, int to)>(); + for (var i = 0; i < size; i += SIZE) { - long to; + int to; if (i + SIZE >= size) { to = size - 1; @@ -198,11 +198,12 @@ namespace Gallery.Util headers.Range = new RangeHeaderValue(from, to); headers.Add("User-Agent", Config.UserAgent); using var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result; - using var ms = new MemoryStream(data, (int)from, (int)(to - from + 1)); + using var ms = new MemoryStream(data, from, to - from + 1); response.Content.CopyToAsync(ms).Wait(); #if DEBUG Log.Print($"downloaded range: from({from:n0}) to ({to:n0})"); #endif + action?.Invoke((to, size)); } return true; }, diff --git a/Gallery.Share/Util/Store.cs b/Gallery.Share/Util/Store.cs index 24a1f5c..b961620 100644 --- a/Gallery.Share/Util/Store.cs +++ b/Gallery.Share/Util/Store.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; using System.IO; -using System.Net; +using System.Text; using System.Threading.Tasks; +using Gallery.Util.Model; +using Newtonsoft.Json; using Xamarin.Essentials; using Xamarin.Forms; @@ -12,17 +15,120 @@ namespace Gallery.Util public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal); public static readonly string CacheFolder = FileSystem.CacheDirectory; + private const string favoriteFile = "favorites.json"; private const string imageFolder = "img-original"; private const string previewFolder = "img-preview"; - public static async Task LoadRawImage(string url) + public static FavoriteList FavoriteList => GetFavoriteList(); + public static string FavoriteListPath => Path.Combine(PersonalFolder, favoriteFile); + private static FavoriteList favoriteList; + + public static FavoriteList GetFavoriteList(bool force = false) { - return await LoadImageAsync(url, null, PersonalFolder, imageFolder, force: true); + if (force || favoriteList == null) + { + var favorites = LoadFavoriteList(); + if (favorites != null) + { + favoriteList = favorites; + } + else + { + favoriteList = new FavoriteList(); + } + } + return favoriteList; } - public static async Task LoadPreviewImage(string url, bool downloading, bool force = false) + private static FavoriteList LoadFavoriteList(string file = null) { - return await LoadImage(url, CacheFolder, previewFolder, downloading, force: force); + if (file == null) + { + file = FavoriteListPath; + } + return LoadObject(file); + } + + public static void SaveFavoriteList() + { + var file = FavoriteListPath; + WriteObject(file, FavoriteList); + } + + private static T LoadObject(string file) + { + string content = null; + if (File.Exists(file)) + { + try + { + content = File.ReadAllText(file); + } + catch (Exception ex) + { + Log.Error("file.read", $"failed to read file: {file}, error: {ex.Message}"); + } + } + else + { + return default; + } + try + { + return JsonConvert.DeserializeObject(content); + } + catch (Exception ex) + { + Log.Error("file.deserialize", $"failed to parse JSON object, error: {ex.Message}"); + return default; + } + } + + private static void WriteObject(string file, object obj) + { + var folder = Path.GetDirectoryName(file); + if (!Directory.Exists(folder)) + { + Directory.CreateDirectory(folder); + } + string content; + try + { + content = JsonConvert.SerializeObject(obj, Formatting.None); + } + catch (Exception ex) + { + Log.Error("object.serialize", $"failed to serialize object, error: {ex.Message}"); + return; + } + try + { + File.WriteAllText(file, content, Encoding.UTF8); + } + catch (Exception ex) + { + Log.Error("file.write", $"failed to write file: {file}, error: {ex.Message}"); + } + } + + public static async Task LoadRawImage(GalleryItem item, bool force = false, Action<(int loc, int size)> action = null) + { + return await LoadImageAsync(item.RawUrl, null, PersonalFolder, Path.Combine(imageFolder, item.Source), force, action); + } + + public static string GetRawImagePath(GalleryItem item) + { + var file = Path.Combine(PersonalFolder, imageFolder, item.Source, Path.GetFileName(item.RawUrl)); + if (File.Exists(file)) + { + return file; + } + return null; + } + + public static async Task LoadPreviewImage(GalleryItem item, bool downloading, bool force = false) + { + return await LoadImage(item.PreviewUrl, CacheFolder, Path.Combine(previewFolder, item.Source), downloading, force: force); } private static async Task LoadImage(string url, string working, string folder, bool downloading, bool force = false) @@ -48,7 +154,7 @@ namespace Gallery.Util return image; } - private static async Task LoadImageAsync(string url, string id, string working, string folder, bool force = false) + private static async Task LoadImageAsync(string url, string id, string working, string folder, bool force, Action<(int loc, int size)> action) { var file = Path.Combine(working, folder, Path.GetFileName(url)); ImageSource image; @@ -62,7 +168,7 @@ namespace Gallery.Util } if (image == null) { - file = await NetHelper.DownloadImageAsync(url, id, working, folder); + file = await NetHelper.DownloadImageAsync(url, id, working, folder, action); if (file != null) { image = ImageSource.FromFile(file); @@ -72,27 +178,33 @@ namespace Gallery.Util } } - public static class Config + public class FavoriteList : List { - public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30); + public bool Changed { get; private set; } - public const string DownloadThreadsKey = "download_threads"; - public const string IsProxiedKey = "is_proxied"; - public const string ProxyHostKey = "proxy_host"; - public const string ProxyPortKey = "proxy_port"; + public FavoriteList() : base() { } + public FavoriteList(IEnumerable collection) : base(collection) { } - public const int MaxThreads = 2; - public const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"; - public const string AcceptLanguage = "zh-cn"; - public const string AcceptImage = "image/png,image/*,*/*;q=0.8"; + public new void Insert(int index, GalleryItem item) + { + base.Insert(index, item); + Changed = true; + } + public new void InsertRange(int index, IEnumerable collection) + { + base.InsertRange(index, collection); + Changed = true; + } + public new void RemoveAt(int index) + { + base.RemoveAt(index); + Changed = true; + } - public static int DownloadThreads; - public static WebProxy Proxy; - } - - public static class Routes - { - public const string Gallery = "gallery"; - public const string Option = "option"; + public FavoriteList Reload() + { + Changed = false; + return this; + } } } diff --git a/Gallery.Share/Views/GalleryItemPage.xaml b/Gallery.Share/Views/GalleryItemPage.xaml index dffd9af..f4f4ff0 100644 --- a/Gallery.Share/Views/GalleryItemPage.xaml +++ b/Gallery.Share/Views/GalleryItemPage.xaml @@ -2,11 +2,44 @@ + + + + + + - + + + + + + + + + + + + + diff --git a/Gallery.Share/Views/GalleryItemPage.xaml.cs b/Gallery.Share/Views/GalleryItemPage.xaml.cs index cc46dd8..d19a17f 100644 --- a/Gallery.Share/Views/GalleryItemPage.xaml.cs +++ b/Gallery.Share/Views/GalleryItemPage.xaml.cs @@ -1,15 +1,213 @@ -using System; -using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Gallery.Resources.Theme; using Gallery.Resources.UI; +using Gallery.Services; +using Gallery.Util; +using Gallery.Util.Model; +using Xamarin.Essentials; using Xamarin.Forms; namespace Gallery.Views { + [QueryProperty("GalleryId", "id")] public partial class GalleryItemPage : AdaptedPage { - public GalleryItemPage() + public static readonly BindableProperty FavoriteIconProperty = BindableProperty.Create(nameof(FavoriteIcon), typeof(ImageSource), typeof(GalleryItemPage)); + public static readonly BindableProperty GalleryItemProperty = BindableProperty.Create(nameof(GalleryItem), typeof(GalleryItem), typeof(GalleryItemPage)); + public static readonly BindableProperty ProgressVisibleProperty = BindableProperty.Create(nameof(ProgressVisible), typeof(bool), typeof(GalleryItemPage)); + public static readonly BindableProperty ToolbarCommandProperty = BindableProperty.Create(nameof(ToolbarCommand), typeof(Command), typeof(GalleryItemPage)); + + public ImageSource FavoriteIcon { + get => (ImageSource)GetValue(FavoriteIconProperty); + set => SetValue(FavoriteIconProperty, value); + } + public GalleryItem GalleryItem + { + get => (GalleryItem)GetValue(GalleryItemProperty); + } + public bool ProgressVisible + { + get => (bool)GetValue(ProgressVisibleProperty); + set => SetValue(ProgressVisibleProperty, value); + } + public Command ToolbarCommand + { + get => (Command)GetValue(ToolbarCommandProperty); + } + + private readonly ImageSource fontIconLove; + private readonly ImageSource fontIconNotLove; + private bool favoriteChanged; + private bool isLoaded; + + public GalleryItemPage(GalleryItem item) + { + SetValue(GalleryItemProperty, item); + SetValue(ToolbarCommandProperty, new Command(HandleCommand, cmd => !IsBusy)); + + fontIconLove = (ImageSource)Application.Current.Resources[Theme.FontIconLove]; + fontIconNotLove = (ImageSource)Application.Current.Resources[Theme.FontIconNotLove]; + + if (item != null) + { + InitInformation(item); + } + InitializeComponent(); } + + protected override void OnAppearing() + { + base.OnAppearing(); + Screen.SetHomeIndicatorAutoHidden(Shell.Current, true); + + if (!isLoaded) + { + isLoaded = true; + Task.Run(() => LoadImage()); + } + } + + protected override void OnDisappearing() + { + Screen.SetHomeIndicatorAutoHidden(Shell.Current, false); + base.OnDisappearing(); + + if (favoriteChanged) + { + Store.SaveFavoriteList(); + } + } + + private void InitInformation(GalleryItem item) + { + Title = item.TagDescription; + FavoriteIcon = Store.FavoriteList.Any(i => i.SourceEquals(item)) ? + fontIconLove : + fontIconNotLove; + } + + private async void LoadImage(bool force = false) + { + IsBusy = true; + MainThread.BeginInvokeOnMainThread(async () => + { + ToolbarCommand.ChangeCanExecute(); + if (force) + { + ProgressVisible = true; + progress.Progress = 0; + await progress.FadeTo(1, easing: Easing.CubicIn); + } + }); + var item = GalleryItem; + if (item.IsRawPage) + { + var source = App.GallerySources.FirstOrDefault(s => s.Route == item.Source); + if (source != null) + { + var url = await source.ResolveImageUrl(item); + if (!string.IsNullOrEmpty(url)) + { + item.IsRawPage = false; + } + } + } + var image = await Store.LoadRawImage(item, force: force, o => + { + var val = o.loc / (double)o.size; + if (val > progress.Progress) + { + MainThread.BeginInvokeOnMainThread(async () => + { + if (!ProgressVisible) + { + ProgressVisible = true; + } + progress.CancelAnimations(); + await progress.ProgressTo(val, 250, Easing.CubicIn); + }); + } + }); + if (image != null) + { + item.PreviewImage = image; + } + IsBusy = false; + MainThread.BeginInvokeOnMainThread(async () => + { + ToolbarCommand.ChangeCanExecute(); + progress.CancelAnimations(); + await progress.ProgressTo(1, 250, Easing.CubicIn); + await progress.FadeTo(0, easing: Easing.CubicIn); + ProgressVisible = false; + }); + } + + private void HandleCommand(string cmd) + { + switch (cmd) + { + case "favorite": Favorite_Clicked(); break; + case "refresh": Refresh_Clicked(); break; + case "share": Share_Clicked(); break; + } + } + + private void Favorite_Clicked() + { + if (IsBusy) + { + return; + } + Start(() => + { + var favorites = Store.FavoriteList; + var item = GalleryItem; + var index = favorites.FindIndex(i => i.SourceEquals(item)); + if (index < 0) + { + item.IsFavorite = true; + favorites.Insert(0, item); + FavoriteIcon = fontIconLove; + } + else + { + favorites.RemoveAt(index); + item.IsFavorite = false; + FavoriteIcon = fontIconNotLove; + } + favoriteChanged = true; + }); + } + + private void Refresh_Clicked() + { + if (IsBusy) + { + return; + } + Start(async () => await Task.Run(() => LoadImage(true))); + } + + private void Share_Clicked() + { + if (IsBusy) + { + return; + } + Start(async () => + { + var item = GalleryItem; + var path = Store.GetRawImagePath(item); + await Share.RequestAsync(new ShareFileRequest + { + Title = item.TagDescription, + File = new ShareFile(path) + }); + }); + } } } diff --git a/Gallery.Share/Views/GalleryPage.xaml b/Gallery.Share/Views/GalleryPage.xaml index 6bf10b6..7e73ecf 100644 --- a/Gallery.Share/Views/GalleryPage.xaml +++ b/Gallery.Share/Views/GalleryPage.xaml @@ -8,6 +8,11 @@ BackgroundColor="{DynamicResource WindowColor}" BindingContext="{x:Reference yanderePage}" Title="{Binding Source.Name}"> + + + + (Command)GetValue(ToolbarCommandProperty); + } + private int currentPage; public GalleryPage(IGallerySource source) : base(source) { Resources.Add("cardView", GetCardViewTemplate()); + SetValue(ToolbarCommandProperty, new Command(DoRefresh, () => !IsLoading && !IsBottomLoading)); InitializeComponent(); currentPage = 1; @@ -28,21 +36,62 @@ namespace Gallery.Views { currentPage = 1; } + if (Store.FavoriteList.Changed && Source is Sources.FavoriteGallerySource) + { + Store.FavoriteList.Reload(); + LastUpdated = default; + } base.OnAppearing(); } - protected override async Task DoloadGalleryData(bool force) + protected override void BeforeLoading() + { + MainThread.BeginInvokeOnMainThread(ToolbarCommand.ChangeCanExecute); + } + + protected override void AfterLoaded() + { + MainThread.BeginInvokeOnMainThread(ToolbarCommand.ChangeCanExecute); + } + + protected override async Task> DoloadGalleryData(bool force) { var result = await Source.GetRecentItemsAsync(currentPage); return result; } - protected override IEnumerable DoGetGalleryList(GalleryItem[] data, out int tag) + protected override IEnumerable DoGetGalleryList(IEnumerable data, out int tag) { tag = currentPage; return data; } + protected override void OnGalleryItemTapped(GalleryItem item) + { + if (item == null) + { + return; + } + Start(async () => + { + var page = new GalleryItemPage(item); + await Navigation.PushAsync(page); + }); + } + + private void DoRefresh() + { + if (IsLoading) + { + return; + } + Start(async () => + { + await ScrollToTopAsync(scrollView); + StartLoading(force: true); + }); + } + private void FlowLayout_MaxHeightChanged(object sender, HeightEventArgs e) { SetOffset(e.ContentHeight - scrollView.Bounds.Height - SCROLL_OFFSET); @@ -50,6 +99,10 @@ namespace Gallery.Views protected override bool CheckRefresh() { + if (!Source.IsScrollable) + { + return false; + } currentPage++; #if DEBUG Log.Print($"loading page: {currentPage}"); diff --git a/Gallery.iOS/Assets.xcassets/IconBookmark.imageset/Contents.json b/Gallery.iOS/Assets.xcassets/IconBookmark.imageset/Contents.json new file mode 100644 index 0000000..615d7b8 --- /dev/null +++ b/Gallery.iOS/Assets.xcassets/IconBookmark.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images": [ + { + "idiom": "universal" + }, + { + "filename": "bookmark-solid.png", + "scale": "1x", + "idiom": "universal" + }, + { + "filename": "bookmark-solid@2x.png", + "scale": "2x", + "idiom": "universal" + }, + { + "filename": "bookmark-solid@3x.png", + "scale": "3x", + "idiom": "universal" + } + ], + "info": { + "version": 1, + "author": "xcode" + } +} \ No newline at end of file diff --git a/Gallery.iOS/Assets.xcassets/IconBookmark.imageset/bookmark-solid.png b/Gallery.iOS/Assets.xcassets/IconBookmark.imageset/bookmark-solid.png new file mode 100644 index 0000000000000000000000000000000000000000..37bcec8e201d782a042642c07c83d1bd882ac5cc GIT binary patch literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^d_XM3!3HF=W8NDADVAa<&kznEsNqQI0P;BtJR*yM zbSnrmK6>;2Adn$h;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6i`v2r;B5V zMep0o8#$X91XwRDZ(Xus^GQCxCmb6KTh~0`@_T5}w_t`|hZ&nofJwx!hpTp%#}rRk ze7ItR)gGoV9do_q&qVvF|6#xTq3!x%wwp0$#p3pzVb8Y_`^&+4>*^XoW-XiYb>0Dw iem9$5lHK6fRmJqKNJ;Ll;HD^`2h@*RMMKY zP2>n)n)BhjvYR1$>}U4OoSA>`Oo!rm-f1@;tvgcyqV(DCY@~py-923# zLoyoQ-Zb=%P84Z)sBR_dD#Nl+p-Jx_qdEITg;=XsFJ8QgC@d-|6$*C`SNzMkG`iid z&qo=ZV|BE79kdzq489U+D30laC<}GZ!_QF8y|4l|;Cm z?whiP*UB6!+g~RhC@81`$Y|U-{pJSTyAYA1WKi|>!^6DG<+Z`vSM{pdQ zI{Wa+=n2n%d^xwHX!Vbq{GZsYCFG4ZT%M71FRq&P=L!iyW6_7Fe6^pw&(BR^_73f; c+R}2&c%`&E^HkMCpjcz@boFyt=akR{0HDgVPyhe` literal 0 HcmV?d00001 diff --git a/Gallery.iOS/Assets.xcassets/IconBookmarkRegular.imageset/Contents.json b/Gallery.iOS/Assets.xcassets/IconBookmarkRegular.imageset/Contents.json new file mode 100644 index 0000000..39a3480 --- /dev/null +++ b/Gallery.iOS/Assets.xcassets/IconBookmarkRegular.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images": [ + { + "idiom": "universal" + }, + { + "filename": "bookmark-regular.png", + "scale": "1x", + "idiom": "universal" + }, + { + "filename": "bookmark-regular@2x.png", + "scale": "2x", + "idiom": "universal" + }, + { + "filename": "bookmark-regular@3x.png", + "scale": "3x", + "idiom": "universal" + } + ], + "info": { + "version": 1, + "author": "xcode" + } +} \ No newline at end of file diff --git a/Gallery.iOS/Assets.xcassets/IconBookmarkRegular.imageset/bookmark-regular.png b/Gallery.iOS/Assets.xcassets/IconBookmarkRegular.imageset/bookmark-regular.png new file mode 100644 index 0000000000000000000000000000000000000000..0686fab317158d356a5d9759a0d9408a49afce3e GIT binary patch literal 287 zcmeAS@N?(olHy`uVBq!ia0vp^d_XM3!3HF=W8NDADVAa<&kznEsNqQI0P;BtJR*yM zbSnrmK6>;2Adn$h;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6j0F|PZ!4! zi{7`FH*y|I5IFiVevQbRoK4NmJDOU#Uofpp$j*H(vXN`1$^_n~vZSQqBoUsCqVjtl zS>OBrnD1Rev(#IM>k;Y}iw~YO(4Ei`|EY1piEWGZ8^r$}yw4UabLge|j@?DVIeYc~ zvReBnPj-*(X3`G%sJD@4df;lM;F5#KKBsJxD7TW@UwX0RpmbmD=Y%%fb^Hd0_pn!7 bm$@n1;%c7ssMS^i=uQStS3j3^P6HK~zYI?bo|X15psh@n1w^A=udX0FoEb%GOwEYZa7K78Y9B z`wG5-tz96Utsr9KrLwfKH6SP^UWz@jIGoMJtQN8#3}@N1GygL?!!k>zZQP=Z&jPKB z8#DsPPQidYYX}+r1?;*KQk@Q2p5jb#s$(l$_eAt&fgmB?cl@b>gAY^*D`6$9gq5%o zR>J-pYy~G_{WaJEj&UIyYA*wHaU9^V>1qZCc#@t6pK*lQgl{u?b|7pGcR7&Q?y>O` z*b-VfLEBglYihq0{D;E!!Yg^nIr(OE@_J6@Z?gLC4}$d)jvXALDy?eR?<9QVc)2zB zFkZLyyY+oQ%;e20td0nYwT$yoV2{`}`In_4`+Ipv!TJTApAZsjPS$X*eOMc{f+4YL aXnz4_mEetPatylw0000gcyqV(DCY@~pyFL=5* zhGaCpy=j9`jB*i>X$jK^-MIYl=|t(Gv);d|UVf+odaH1 z#Y4Y4)hDgl`*pQVuxz5#)QKxkod`)yS!HAtDmimik58!EGA-p`OyR62tTviUZ(rnsc;T=@^){P!@f)2i z@9HLhkb2H9T*>qO<6^~idvZCyzxT;{VsuTJVfE2{s|A0r6aOtIQ7M_P?DvDUyp=uk zijbYF%x}5tw-wfNIG>n5OE~ADrr)YOHvTL3m8>fw6BbXrZEA0IMEgwN!Kt%#zpbyf zwRpFuH~)|6ot`68cdOOi$ZE~8GU#8O$CS~UV}0~$>7jcox0n{OiVHtJb(WLgH~ESC z9m|xMozXu8n*ZpX(Yh?m);CM&*zJ6+3Hw`r{_t_%QMA1E + + + + + + + + @@ -158,6 +166,8 @@ + + diff --git a/Gallery.iOS/Renderers/AppShellSection/AppAppearanceTracker.cs b/Gallery.iOS/Renderers/AppShellSection/AppAppearanceTracker.cs index 0bcebbd..2c36bc0 100644 --- a/Gallery.iOS/Renderers/AppShellSection/AppAppearanceTracker.cs +++ b/Gallery.iOS/Renderers/AppShellSection/AppAppearanceTracker.cs @@ -151,15 +151,18 @@ namespace Gallery.iOS.Renderers.AppShellSection public void UpdateLayout(UITabBarController controller) { var tabBar = controller.TabBar; - if (tabBar != null && tabBar.Items != null && tabBar.Items.Length == 3) + if (tabBar != null && tabBar.Items != null && tabBar.Items.Length >= 4) { var tabBarItem = tabBar.Items[0]; + tabBarItem.Image = UIImage.FromBundle("IconBookmarkRegular"); + tabBarItem.SelectedImage = UIImage.FromBundle("IconBookmark"); + tabBarItem = tabBar.Items[1]; tabBarItem.Image = UIImage.FromBundle("IconYandereRegular"); tabBarItem.SelectedImage = UIImage.FromBundle("IconYandere"); - tabBarItem = tabBar.Items[1]; + tabBarItem = tabBar.Items[2]; tabBarItem.Image = UIImage.FromBundle("IconSourceRegular"); tabBarItem.SelectedImage = UIImage.FromBundle("IconSource"); - tabBarItem = tabBar.Items[2]; + tabBarItem = tabBar.Items[3]; tabBarItem.Image = UIImage.FromBundle("IconSourceRegular"); tabBarItem.SelectedImage = UIImage.FromBundle("IconSource"); }