combine projects into one

This commit is contained in:
2021-08-11 14:09:03 +08:00
parent 24f39a2e27
commit 521d82829d
34 changed files with 431 additions and 192 deletions

View File

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Gallery.Util.Model;
namespace Gallery.Util
{
public class GalleryItemConverter : JsonConverter<GalleryItem>
{
public override GalleryItem Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
return null;
}
var item = new GalleryItem();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return item;
}
if (reader.TokenType == JsonTokenType.PropertyName)
{
var name = reader.GetString();
reader.Read();
switch (name)
{
case nameof(GalleryItem.Id): item.Id = reader.GetInt64(); break;
case nameof(GalleryItem.Tags):
if (reader.TokenType == JsonTokenType.StartArray)
{
var tags = new List<string>();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
break;
}
if (reader.TokenType == JsonTokenType.String)
{
tags.Add(reader.GetString());
}
}
item.Tags = tags.ToArray();
}
break;
case nameof(GalleryItem.CreatedTime): item.CreatedTime = reader.GetDateTime(); break;
case nameof(GalleryItem.UpdatedTime): item.UpdatedTime = reader.GetDateTime(); break;
case nameof(GalleryItem.UserId): item.UserId = reader.GetString(); break;
case nameof(GalleryItem.UserName): item.UserName = reader.GetString(); break;
case nameof(GalleryItem.Source): item.Source = reader.GetString(); break;
case nameof(GalleryItem.PreviewUrl): item.PreviewUrl = reader.GetString(); break;
case nameof(GalleryItem.RawUrl): item.RawUrl = reader.GetString(); break;
case nameof(GalleryItem.Width): item.Width = reader.GetInt32(); break;
case nameof(GalleryItem.Height): item.Height = reader.GetInt32(); break;
case nameof(GalleryItem.BookmarkId): item.BookmarkId = reader.GetString(); break;
case nameof(GalleryItem.IsRawPage): item.IsRawPage = reader.GetBoolean(); break;
}
}
}
throw new ArgumentOutOfRangeException();
}
public override void Write(Utf8JsonWriter writer, GalleryItem value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
writer.WriteStartObject();
writer.WriteNumber(nameof(GalleryItem.Id), value.Id);
if (value.Tags != null)
{
writer.WritePropertyName(nameof(GalleryItem.Tags));
writer.WriteStartArray();
for (var i = 0; i < value.Tags.Length; i++)
{
writer.WriteStringValue(value.Tags[i]);
}
writer.WriteEndArray();
}
writer.WriteString(nameof(GalleryItem.CreatedTime), value.CreatedTime);
writer.WriteString(nameof(GalleryItem.UpdatedTime), value.UpdatedTime);
writer.WriteString(nameof(GalleryItem.UserId), value.UserId);
writer.WriteString(nameof(GalleryItem.UserName), value.UserName);
writer.WriteString(nameof(GalleryItem.Source), value.Source);
writer.WriteString(nameof(GalleryItem.PreviewUrl), value.PreviewUrl);
writer.WriteString(nameof(GalleryItem.RawUrl), value.RawUrl);
writer.WriteNumber(nameof(GalleryItem.Width), value.Width);
writer.WriteNumber(nameof(GalleryItem.Height), value.Height);
writer.WriteString(nameof(GalleryItem.BookmarkId), value.BookmarkId);
writer.WriteBoolean(nameof(GalleryItem.IsRawPage), value.IsRawPage);
writer.WriteEndObject();
}
}
}

View File

@ -0,0 +1,67 @@
using System;
namespace Gallery.Util
{
public static class Extensions
{
public static int IndexOf<T>(this T[] array, Predicate<T> predicate)
{
for (var i = 0; i < array.Length; i++)
{
if (predicate(array[i]))
{
return i;
}
}
return -1;
}
public static int LastIndexOf<T>(this T[] array, Predicate<T> predicate)
{
for (var i = array.Length - 1; i >= 0; i--)
{
if (predicate(array[i]))
{
return i;
}
}
return -1;
}
public static bool All<T>(this T[] array, Predicate<T> predicate)
{
for (var i = 0; i < array.Length; i++)
{
if (!predicate(array[i]))
{
return false;
}
}
return true;
}
public static bool AnyFor<T>(this T[] array, int from, int to, Predicate<T> predicate)
{
for (var i = from; i <= to; i++)
{
if (predicate(array[i]))
{
return true;
}
}
return false;
}
public static DateTime ToLocalTime(this long time)
{
//return new DateTime(1970, 1, 1, 0, 0, 0).AddMilliseconds(time).ToLocalTime();
return new DateTime(621355968000000000L + time * 10000000).ToLocalTime();
}
public static long ToTimestamp(this DateTime datetime)
{
var ticks = datetime.Ticks;
return (ticks - 621355968000000000L) / 10000000;
}
}
}

View File

@ -0,0 +1,23 @@
using System.Threading.Tasks;
using Gallery.Util.Model;
using Xamarin.Forms;
namespace Gallery.Util.Interface
{
public interface IGallerySource
{
string Name { get; }
string Route { get; }
string FlyoutIconKey { get; }
string HomePage { get; }
void SetCookie();
void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark);
Task<GalleryItem[]> GetRecentItemsAsync(int page);
}
}

41
Gallery.Share/Util/Log.cs Normal file
View File

@ -0,0 +1,41 @@
using System;
using System.Diagnostics;
namespace Gallery.Util
{
public static class Log
{
public static ILog Logger { get; set; } = new DefaultLogger();
public static void Print(string message)
{
Logger?.Print(message);
}
public static void Error(string category, string message)
{
Logger?.Error(category, message);
}
}
public class DefaultLogger : ILog
{
public void Print(string message)
{
#if DEBUG
Debug.WriteLine("[{0:HH:mm:ss.fff}] - {1}", DateTime.Now, message);
#endif
}
public void Error(string category, string message)
{
Debug.Fail(string.Format("[{0:HH:mm:ss.fff}] - {1} - {2}", DateTime.Now, category, message));
}
}
public interface ILog
{
void Print(string message);
void Error(string category, string message);
}
}

View File

@ -0,0 +1,131 @@
using System;
//using System.Text.Json.Serialization;
using Newtonsoft.Json;
using Xamarin.Forms;
namespace Gallery.Util.Model
{
//[JsonConverter(typeof(GalleryItemConverter))]
[JsonObject(MemberSerialization.OptIn)]
public class GalleryItem : BindableObject
{
const double PREVIEW_WIDTH = 200.0;
public static readonly BindableProperty TagDescriptionProperty = BindableProperty.Create(nameof(TagDescription), typeof(string), typeof(GalleryItem));
public static readonly BindableProperty PreviewImageProperty = BindableProperty.Create(nameof(PreviewImage), typeof(ImageSource), typeof(GalleryItem));
public static readonly BindableProperty UserNameProperty = BindableProperty.Create(nameof(UserName), typeof(string), typeof(GalleryItem));
public static readonly BindableProperty CreatedTimeProperty = BindableProperty.Create(nameof(CreatedTime), typeof(DateTime), typeof(GalleryItem));
public static readonly BindableProperty UpdatedTimeProperty = BindableProperty.Create(nameof(UpdatedTime), typeof(DateTime), typeof(GalleryItem));
public static readonly BindableProperty ImageHeightProperty = BindableProperty.Create(nameof(ImageHeight), typeof(GridLength), typeof(GalleryItem),
defaultValue: GridLength.Auto);
public static readonly BindableProperty IsFavoriteProperty = BindableProperty.Create(nameof(IsFavorite), typeof(bool), typeof(GalleryItem));
public static readonly BindableProperty BookmarkIdProperty = BindableProperty.Create(nameof(BookmarkId), typeof(string), typeof(GalleryItem));
public string TagDescription
{
get => (string)GetValue(TagDescriptionProperty);
set => SetValue(TagDescriptionProperty, value);
}
public ImageSource PreviewImage
{
get => (ImageSource)GetValue(PreviewImageProperty);
set => SetValue(PreviewImageProperty, value);
}
public GridLength ImageHeight
{
get => (GridLength)GetValue(ImageHeightProperty);
set => SetValue(ImageHeightProperty, value);
}
public bool IsFavorite
{
get => (bool)GetValue(IsFavoriteProperty);
set => SetValue(IsFavoriteProperty, value);
}
[JsonProperty]
public string BookmarkId
{
get => (string)GetValue(BookmarkIdProperty);
set => SetValue(BookmarkIdProperty, value);
}
[JsonProperty]
public long Id { get; internal set; }
private string[] tags;
[JsonProperty]
public string[] Tags
{
get => tags;
set
{
tags = value;
if (value != null)
{
TagDescription = string.Join(' ', tags);
}
else
{
TagDescription = null;
}
}
}
[JsonProperty]
public DateTime CreatedTime { get; set; }
[JsonProperty]
public DateTime UpdatedTime { get; set; }
[JsonProperty]
public string UserId { get; set; }
[JsonProperty]
public string UserName { get; set; }
[JsonProperty]
public string Source { get; set; }
[JsonProperty]
public string PreviewUrl { get; set; }
[JsonProperty]
public string RawUrl { get; set; }
[JsonProperty]
public bool IsRawPage { get; set; }
private int width;
private int height;
[JsonProperty]
public int Width
{
get => width;
set
{
width = value;
if (width > 0 && height > 0)
{
ImageHeight = new GridLength(PREVIEW_WIDTH * height / width);
}
}
}
[JsonProperty]
public int Height
{
get => height;
set
{
height = value;
if (width > 0 && height > 0)
{
ImageHeight = new GridLength(PREVIEW_WIDTH * height / width);
}
}
}
internal GalleryItem() { }
public GalleryItem(long id)
{
Id = id;
}
public override string ToString()
{
var source = string.IsNullOrEmpty(Source) ? RawUrl : Source;
return $"{Id}, {source}";
}
}
}

View File

@ -0,0 +1,283 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Xamarin.Essentials;
namespace Gallery.Util
{
public static class NetHelper
{
public static bool NetworkAvailable
{
get
{
try
{
return Connectivity.NetworkAccess == NetworkAccess.Internet
|| Connectivity.NetworkAccess == NetworkAccess.ConstrainedInternet;
}
catch
{
return false;
}
}
}
public static async Task<(T result, string error)> RequestObject<T>(string url,
string referer = null,
HttpContent post = null,
Action<HttpRequestHeaders> headerHandler = null,
Func<string, string> contentHandler = null,
Func<string, T> @return = null)
{
var response = await Request(url, headers =>
{
if (referer != null)
{
headers.Referrer = new Uri(referer);
}
headers.Add("User-Agent", Config.UserAgent);
headerHandler?.Invoke(headers);
}, post);
if (response == null)
{
return (default, "response is null");
}
if (!response.IsSuccessStatusCode)
{
Log.Print($"http failed with code: {(int)response.StatusCode} - {response.StatusCode}");
return (default, response.StatusCode.ToString());
}
string content;
using (response)
{
try
{
content = await response.Content.ReadAsStringAsync();
if (contentHandler != null)
{
content = contentHandler(content);
}
if (@return != null)
{
return (@return(content), null);
}
}
catch (Exception ex)
{
Log.Error("stream.load", $"failed to read stream, error: {ex.Message}");
return (default, ex.Message);
}
}
if (content == null)
{
content = string.Empty;
}
try
{
var result = JsonConvert.DeserializeObject<T>(content);
return (result, null);
}
catch (Exception ex)
{
var memo = content.Length < 20 ? content : content[0..20] + "...";
Log.Error("content.deserialize", $"failed to parse JSON object, content: {memo}, error: {ex.Message}");
return (default, content);
}
}
public static async Task<string> DownloadImage(string url, string working, string folder)
{
try
{
var directory = Path.Combine(working, folder);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var file = Path.Combine(directory, Path.GetFileName(url));
var response = await Request(url, headers =>
{
headers.Add("User-Agent", Config.UserAgent);
headers.Add("Accept", Config.AcceptImage);
});
if (response == null)
{
return null;
}
using (response)
using (var fs = File.OpenWrite(file))
{
await response.Content.CopyToAsync(fs);
}
return file;
}
catch (Exception ex)
{
Log.Error("image.download", ex.Message);
return null;
}
}
public static async Task<string> DownloadImageAsync(string url, string id, string working, string folder)
{
try
{
var directory = Path.Combine(working, folder);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var file = Path.Combine(directory, Path.GetFileName(url));
var proxy = Config.Proxy;
var handler = new HttpClientHandler
{
UseCookies = false
};
if (proxy != null)
{
handler.Proxy = proxy;
handler.UseProxy = true;
}
var client = new HttpClient(handler, true)
{
Timeout = Config.Timeout
};
long size;
DateTimeOffset lastModified;
using (var request = new HttpRequestMessage(HttpMethod.Head, url))
{
var headers = request.Headers;
headers.Add("Accept", Config.AcceptImage);
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;
lastModified = response.Content.Headers.LastModified.Value;
#if DEBUG
Log.Print($"content length: {size:n0} bytes, last modified: {lastModified}");
#endif
}
// segments
const int SIZE = 150000;
var list = new List<(long from, long to)>();
for (long i = 0; i < size; i += SIZE)
{
long to;
if (i + SIZE >= size)
{
to = size - 1;
}
else
{
to = i + SIZE - 1;
}
list.Add((i, to));
}
var data = new byte[size];
var task = new TaskCompletionSource<string>();
ParallelTask.Start($"download.async.{id}", 0, list.Count, 2, i =>
{
var (from, to) = list[i];
using (var request = new HttpRequestMessage(HttpMethod.Get, url))
{
var headers = request.Headers;
headers.Add("Accept", Config.AcceptImage);
headers.Add("Accept-Language", Config.AcceptLanguage);
headers.Add("Accept-Encoding", "identity");
headers.IfRange = new RangeConditionHeaderValue(lastModified);
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));
response.Content.CopyToAsync(ms).Wait();
#if DEBUG
Log.Print($"downloaded range: from({from:n0}) to ({to:n0})");
#endif
}
return true;
},
complete: o =>
{
using (var fs = File.OpenWrite(file))
{
fs.Write(data, 0, data.Length);
}
task.SetResult(file);
});
return await task.Task;
}
catch (Exception ex)
{
Log.Error("image.download.async", $"failed to download image, error: {ex.Message}");
return null;
}
}
private static async Task<HttpResponseMessage> Request(string url, Action<HttpRequestHeaders> headerHandler, HttpContent post = null)
{
#if DEBUG
var method = post == null ? "GET" : "POST";
Log.Print($"{method}: {url}");
#endif
var uri = new Uri(url);
var proxy = Config.Proxy;
var handler = new HttpClientHandler
{
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate,
UseCookies = false
};
if (proxy != null)
{
handler.Proxy = proxy;
handler.UseProxy = true;
}
var client = new HttpClient(handler, true)
{
BaseAddress = new Uri($"{uri.Scheme}://{uri.Host}:{uri.Port}"),
Timeout = Config.Timeout
};
return await TryCount(() =>
{
using var request = new HttpRequestMessage(post == null ? HttpMethod.Get : HttpMethod.Post, uri.PathAndQuery);
var headers = request.Headers;
headerHandler?.Invoke(headers);
headers.Add("Accept-Language", Config.AcceptLanguage);
if (post != null)
{
request.Content = post;
}
return client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
});
}
private static T TryCount<T>(Func<T> func, int tryCount = 2)
{
int tries = 0;
while (tries < tryCount)
{
try
{
return func();
}
catch (Exception ex)
{
tries++;
Thread.Sleep(1000);
Log.Error("try.do", $"tries: {tries}, error: {ex.Message}");
}
}
return default;
}
}
}

View File

@ -0,0 +1,148 @@
using System;
using System.Threading;
namespace Gallery.Util
{
public class ParallelTask : IDisposable
{
public static ParallelTask Start(string tag, int from, int toExclusive, int maxCount, Predicate<int> action, int tagIndex = -1, WaitCallback complete = null)
{
if (toExclusive <= from)
{
if (complete != null)
{
ThreadPool.QueueUserWorkItem(complete);
}
return null;
}
var task = new ParallelTask(tag, from, toExclusive, maxCount, action, tagIndex, complete);
task.Start();
return task;
}
private readonly object sync = new();
private int count;
private bool disposed;
public int TagIndex { get; private set; }
private readonly string tag;
private readonly int max;
private readonly int from;
private readonly int to;
private readonly Predicate<int> action;
private readonly WaitCallback complete;
private ParallelTask(string tag, int from, int to, int maxCount, Predicate<int> action, int tagIndex, WaitCallback complete)
{
if (maxCount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxCount));
}
max = maxCount;
if (from >= to)
{
throw new ArgumentOutOfRangeException(nameof(from));
}
TagIndex = tagIndex;
this.tag = tag;
this.from = from;
this.to = to;
this.action = action;
this.complete = complete;
}
public void Start()
{
ThreadPool.QueueUserWorkItem(DoStart);
}
public void Dispose()
{
disposed = true;
}
private void DoStart(object state)
{
#if DEBUG
const long TIMEOUT = 60000L;
var sw = new System.Diagnostics.Stopwatch();
long lastElapsed = 0;
sw.Start();
#endif
for (int i = from; i < to; i++)
{
var index = i;
while (count >= max)
{
#if DEBUG
var elapsed = sw.ElapsedMilliseconds;
if (elapsed - lastElapsed > TIMEOUT)
{
lastElapsed = elapsed;
Log.Print($"WARNING: parallel task ({tag}), {count} tasks in queue, cost too much time ({elapsed:n0}ms)");
}
#endif
if (disposed)
{
#if DEBUG
sw.Stop();
Log.Print($"parallel task determinate, disposed ({tag}), cost time ({elapsed:n0}ms)");
#endif
return;
}
Thread.Sleep(16);
}
lock (sync)
{
count++;
}
ThreadPool.QueueUserWorkItem(o =>
{
try
{
if (!action(index))
{
disposed = true;
}
}
catch (Exception ex)
{
Log.Error($"parallel.start ({tag})", $"failed to run action, index: {index}, error: {ex}");
}
finally
{
lock (sync)
{
count--;
}
}
});
}
while (count > 0)
{
#if DEBUG
var elapsed = sw.ElapsedMilliseconds;
if (elapsed - lastElapsed > TIMEOUT)
{
lastElapsed = elapsed;
Log.Print($"WARNING: parallel task ({tag}), {count} ending tasks in queue, cost too much time ({elapsed:n0}ms)");
}
#endif
if (disposed)
{
#if DEBUG
sw.Stop();
Log.Print($"parallel task determinate, disposed ({tag}), ending cost time ({elapsed:n0}ms)");
#endif
return;
}
Thread.Sleep(16);
}
#if DEBUG
sw.Stop();
Log.Print($"parallel task done ({tag}), cost time ({sw.ElapsedMilliseconds:n0}ms)");
#endif
complete?.Invoke(null);
}
}
}

View File

@ -0,0 +1,98 @@
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.Util
{
public static class Store
{
public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
public static readonly string CacheFolder = FileSystem.CacheDirectory;
private const string imageFolder = "img-original";
private const string previewFolder = "img-preview";
public static async Task<ImageSource> LoadRawImage(string url)
{
return await LoadImageAsync(url, null, PersonalFolder, imageFolder, force: true);
}
public static async Task<ImageSource> LoadPreviewImage(string url, bool downloading, bool force = false)
{
return await LoadImage(url, CacheFolder, previewFolder, downloading, force: force);
}
private static async Task<ImageSource> LoadImage(string url, string working, string folder, bool downloading, bool force = false)
{
var file = Path.Combine(working, folder, Path.GetFileName(url));
ImageSource image;
if (!force && File.Exists(file))
{
image = ImageSource.FromFile(file);
}
else
{
image = null;
}
if (downloading && image == null)
{
file = await NetHelper.DownloadImage(url, working, folder);
if (file != null)
{
return ImageSource.FromFile(file);
}
}
return image;
}
private static async Task<ImageSource> LoadImageAsync(string url, string id, string working, string folder, bool force = false)
{
var file = Path.Combine(working, folder, Path.GetFileName(url));
ImageSource image;
if (!force && File.Exists(file))
{
image = ImageSource.FromFile(file);
}
else
{
image = null;
}
if (image == null)
{
file = await NetHelper.DownloadImageAsync(url, id, working, folder);
if (file != null)
{
image = ImageSource.FromFile(file);
}
}
return image;
}
}
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 = 8;
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 Option = "option";
}
}