favorite & view page & etc.

This commit is contained in:
Tsanie 2021-08-12 16:27:42 +08:00
parent 3152f47db5
commit fa0b033f2a
33 changed files with 728 additions and 94 deletions

View File

@ -107,9 +107,11 @@ namespace Gallery
GallerySources = new List<IGallerySource>()
{
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();

View File

@ -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 =
{

View File

@ -45,6 +45,8 @@
<Compile Include="$(MSBuildThisFileDirectory)Sources\Yandere\YandereItem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Sources\Danbooru\GallerySource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Sources\Gelbooru\GallerySource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Util\Consts.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Sources\FavoriteGallerySource.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="$(MSBuildThisFileDirectory)Services\" />

View File

@ -11,4 +11,5 @@
<Detail>详细</Detail>
<ProxyHost>代理主机</ProxyHost>
<ProxyPort>代理端口</ProxyPort>
<Favorite>收藏夹</Favorite>
</root>

View File

@ -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);

View File

@ -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));

View File

@ -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);
}

View File

@ -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()
{

View File

@ -10,7 +10,7 @@ using Xamarin.Forms;
namespace Gallery.Resources.UI
{
public abstract class GalleryCollectionPage : GalleryScrollableCollectionPage<GalleryItem[]>
public abstract class GalleryCollectionPage : GalleryScrollableCollectionPage<IEnumerable<GalleryItem>>
{
public GalleryCollectionPage(IGallerySource source) : base(source) { }
}
@ -93,11 +93,11 @@ namespace Gallery.Resources.UI
}
}
protected readonly Command<GalleryItem> commandGalleryItemTapped;
protected double topOffset;
protected string lastError;
private readonly object sync = new();
private readonly Command<GalleryItem> commandGalleryItemTapped;
private readonly Stack<ParallelTask> tasks = new();
private T galleryData;
@ -107,17 +107,8 @@ namespace Gallery.Resources.UI
commandGalleryItemTapped = new Command<GalleryItem>(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);
}
}
}

View File

@ -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<GalleryItem[]> GetRecentItemsAsync(int page)
public override async Task<IEnumerable<GalleryItem>> 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
{

View File

@ -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<IEnumerable<GalleryItem>> GetRecentItemsAsync(int page)
{
return Task.FromResult((IEnumerable<GalleryItem>)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);
}
}
}

View File

@ -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<GalleryItem[]> GetRecentItemsAsync(int page)
public override async Task<IEnumerable<GalleryItem>> 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("&amp;", "&"),
PreviewUrl = g[5].Value,
Tags = g[6].Value.Split(' '),
Source = Route,
IsRawPage = true
};
}
return items;
}
public void SetCookie()
public override async Task<string> ResolveImageUrl(GalleryItem item)
{
var (result, error) = await NetHelper.RequestObject(item.RawUrl, @return: content =>
{
var regex = new Regex(
@"<section class=""image-container.+?" + // data-id=""(\d+?)"".+?data-tags=""([^""]+?)"".+?
@"data-width=""(\d+?)"" data-height=""(\d+?)"".+?data-uploader-id=""([^""]*?)"" " +
@"data-normalized-source=""([^""]+?)""(.|\n)*?<img .*?src=""([^""]+?)""",
RegexOptions.Multiline);
var m = regex.Match(content);
if (m.Success)
{
var g = m.Groups;
item.Width = int.Parse(g[1].Value);
item.Height = int.Parse(g[2].Value);
item.UserId = g[3].Value;
item.SourceUrl = g[4].Value;
item.RawUrl = g[6].Value;
return item.RawUrl;
}
return null;
});
if (result == null || !string.IsNullOrEmpty(error))
{
Log.Error("gelbooru.imageUrl.resolve", $"failed to load data, error: {error}");
return null;
}
return result;
}
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
{

View File

@ -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.Yandere
{
public class GallerySource : IGallerySource
public class GallerySource : GallerySourceBase
{
public string Name => "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<GalleryItem[]> GetRecentItemsAsync(int page)
public override async Task<IEnumerable<GalleryItem>> GetRecentItemsAsync(int page)
{
var url = $"https://yande.re/post?page={page}";
var (result, error) = await NetHelper.RequestObject<YandereItem[]>(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
{

View File

@ -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";
}
}

View File

@ -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;
}
}
}

View File

@ -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<IEnumerable<GalleryItem>> GetRecentItemsAsync(int page);
Task<string> ResolveImageUrl(GalleryItem item);
}
Task<GalleryItem[]> 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<IEnumerable<GalleryItem>> GetRecentItemsAsync(int page);
public virtual Task<string> ResolveImageUrl(GalleryItem item) => Task.FromResult(item.RawUrl);
}
}

View File

@ -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}";
}
}

View File

@ -125,7 +125,7 @@ namespace Gallery.Util
}
}
public static async Task<string> DownloadImageAsync(string url, string id, string working, string folder)
public static async Task<string> 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;
},

View File

@ -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<ImageSource> 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<ImageSource> 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<FavoriteList>(file);
}
public static void SaveFavoriteList()
{
var file = FavoriteListPath;
WriteObject(file, FavoriteList);
}
private static T LoadObject<T>(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<T>(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<ImageSource> 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<ImageSource> 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<ImageSource> 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<ImageSource> LoadImageAsync(string url, string id, string working, string folder, bool force = false)
private static async Task<ImageSource> 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<GalleryItem>
{
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<GalleryItem> 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<GalleryItem> 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;
}
}
}

View File

@ -2,11 +2,44 @@
<ui:AdaptedPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
xmlns:ui="clr-namespace:Gallery.Resources.UI"
x:Class="Gallery.Views.GalleryItemPage"
x:Name="galleryItemPage"
ios:Page.UseSafeArea="False"
Shell.TabBarIsVisible="False"
BindingContext="{x:Reference galleryItemPage}">
<ContentPage.ToolbarItems>
<ToolbarItem Command="{Binding ToolbarCommand}" CommandParameter="favorite"
Order="Primary" IconImageSource="{Binding FavoriteIcon}"/>
<ToolbarItem Command="{Binding ToolbarCommand}" CommandParameter="refresh"
Order="Primary" IconImageSource="{DynamicResource FontIconRefresh}"/>
<ToolbarItem Command="{Binding ToolbarCommand}" CommandParameter="share"
Order="Primary" IconImageSource="{DynamicResource FontIconShare}"/>
</ContentPage.ToolbarItems>
<ContentPage.Content>
<Grid Padding="{Binding TopMargin}">
<Image HorizontalOptions="Fill"
VerticalOptions="Fill"
Aspect="AspectFit"
Source="{Binding GalleryItem.PreviewImage}"/>
<Frame HasShadow="False" Margin="0" Padding="20" CornerRadius="8"
HorizontalOptions="Center" VerticalOptions="Center"
IsVisible="{Binding IsBusy}"
BackgroundColor="{DynamicResource MaskColor}">
<ActivityIndicator IsVisible="{Binding IsBusy}"
IsRunning="{Binding IsBusy}"
Color="{DynamicResource WindowColor}"/>
</Frame>
<ProgressBar x:Name="progress" IsVisible="{Binding ProgressVisible}"
VerticalOptions="Start">
<ProgressBar.Margin>
<OnPlatform x:TypeArguments="Thickness" Android="0, -6, 0, 0"/>
</ProgressBar.Margin>
</ProgressBar>
</Grid>
</ContentPage.Content>
</ui:AdaptedPage>

View File

@ -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<string>), 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<string> ToolbarCommand
{
get => (Command<string>)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<string>(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)
});
});
}
}
}

View File

@ -8,6 +8,11 @@
BackgroundColor="{DynamicResource WindowColor}"
BindingContext="{x:Reference yanderePage}"
Title="{Binding Source.Name}">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Command="{Binding ToolbarCommand}"
IconImageSource="{DynamicResource FontIconRefresh}"/>
</ContentPage.ToolbarItems>
<ContentPage.Content>
<Grid>
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"

View File

@ -4,17 +4,25 @@ using Gallery.Resources.UI;
using Gallery.Util;
using Gallery.Util.Interface;
using Gallery.Util.Model;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.Views
{
public partial class GalleryPage : GalleryCollectionPage
{
public static readonly BindableProperty ToolbarCommandProperty = BindableProperty.Create(nameof(ToolbarCommand), typeof(Command), typeof(GalleryPage));
public Command ToolbarCommand
{
get => (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<GalleryItem[]> DoloadGalleryData(bool force)
protected override void BeforeLoading()
{
MainThread.BeginInvokeOnMainThread(ToolbarCommand.ChangeCanExecute);
}
protected override void AfterLoaded()
{
MainThread.BeginInvokeOnMainThread(ToolbarCommand.ChangeCanExecute);
}
protected override async Task<IEnumerable<GalleryItem>> DoloadGalleryData(bool force)
{
var result = await Source.GetRecentItemsAsync(currentPage);
return result;
}
protected override IEnumerable<GalleryItem> DoGetGalleryList(GalleryItem[] data, out int tag)
protected override IEnumerable<GalleryItem> DoGetGalleryList(IEnumerable<GalleryItem> 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}");

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

View File

@ -135,6 +135,14 @@
<BundleResource Include="Resources\logo_light.png" />
<BundleResource Include="Resources\logo_light%402x.png" />
<BundleResource Include="Resources\logo_light%403x.png" />
<ImageAsset Include="Assets.xcassets\IconBookmark.imageset\Contents.json" />
<ImageAsset Include="Assets.xcassets\IconBookmarkRegular.imageset\Contents.json" />
<ImageAsset Include="Assets.xcassets\IconBookmark.imageset\bookmark-solid.png" />
<ImageAsset Include="Assets.xcassets\IconBookmark.imageset\bookmark-solid%402x.png" />
<ImageAsset Include="Assets.xcassets\IconBookmark.imageset\bookmark-solid%403x.png" />
<ImageAsset Include="Assets.xcassets\IconBookmarkRegular.imageset\bookmark-regular.png" />
<ImageAsset Include="Assets.xcassets\IconBookmarkRegular.imageset\bookmark-regular%402x.png" />
<ImageAsset Include="Assets.xcassets\IconBookmarkRegular.imageset\bookmark-regular%403x.png" />
</ItemGroup>
<ItemGroup>
<Reference Include="System" />
@ -158,6 +166,8 @@
<Folder Include="Assets.xcassets\IconSourceRegular.imageset\" />
<Folder Include="Assets.xcassets\LaunchLogo.imageset\" />
<Folder Include="Assets.xcassets\LauncherLogo.imageset\" />
<Folder Include="Assets.xcassets\IconBookmark.imageset\" />
<Folder Include="Assets.xcassets\IconBookmarkRegular.imageset\" />
</ItemGroup>
<Import Project="..\Gallery.UI\Gallery.UI.projitems" Label="Shared" Condition="Exists('..\Gallery.UI\Gallery.UI.projitems')" />
<Import Project="..\Gallery.Share\Gallery.Share.projitems" Label="Shared" Condition="Exists('..\Gallery.Share\Gallery.Share.projitems')" />

View File

@ -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");
}