rename from Pixiview to Gallery

This commit is contained in:
2021-08-03 19:16:54 +08:00
parent 98676ce8b2
commit c41282a4b7
206 changed files with 7900 additions and 7891 deletions

29
Gallery/Utils/Converters.cs Executable file
View File

@ -0,0 +1,29 @@
using System;
using System.Globalization;
using Gallery.UI;
using Xamarin.Forms;
namespace Gallery.Utils
{
public class FavoriteIconConverter : IValueConverter
{
private readonly bool isFavorite;
public FavoriteIconConverter(bool favorite)
{
isFavorite = favorite;
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value == null ?
isFavorite ? StyleDefinition.IconLove : string.Empty :
StyleDefinition.IconCircleLove;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,248 @@
using System.Globalization;
using System.Threading;
using Gallery.Resources;
#if __IOS__
using System.Diagnostics.CodeAnalysis;
using Foundation;
using UIKit;
#elif __ANDROID__
using Android.OS;
using Xamarin.Forms.Platform.Android;
#endif
using Xamarin.Forms;
namespace Gallery.Utils
{
public class EnvironmentService
{
#region - Theme -
/*
[SuppressMessage("Code Notifications", "XI0002:Notifies you from using newer Apple APIs when targeting an older OS version", Justification = "<Pending>")]
public AppTheme GetApplicationTheme()
{
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{
var currentController = Platform.GetCurrentUIViewController();
if (currentController == null)
{
return AppTheme.Unspecified;
}
var style = currentController.TraitCollection.UserInterfaceStyle;
if (style == UIUserInterfaceStyle.Dark)
{
return AppTheme.Dark;
}
else if (style == UIUserInterfaceStyle.Light)
{
return AppTheme.Light;
}
}
return AppTheme.Unspecified;
}
//*/
public static void SetStatusBarColor(Color color)
{
#if __ANDROID_21__
if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop)
{
Droid.MainActivity.Main.SetStatusBarColor(color.ToAndroid());
Droid.MainActivity.Main.Window.DecorView.SystemUiVisibility =
App.CurrentTheme == Xamarin.Essentials.AppTheme.Dark ?
Android.Views.StatusBarVisibility.Visible :
(Android.Views.StatusBarVisibility)Android.Views.SystemUiFlags.LightStatusBar;
}
#endif
}
public static void SetStatusBarStyle(StatusBarStyles style)
{
#if __IOS__
SetStatusBarStyle(ConvertStyle(style));
}
public static void SetStatusBarStyle(UIStatusBarStyle style)
{
if (UIApplication.SharedApplication.StatusBarStyle == style)
{
return;
}
if (style == UIStatusBarStyle.BlackOpaque)
{
UIApplication.SharedApplication.SetStatusBarHidden(true, true);
}
else
{
UIApplication.SharedApplication.SetStatusBarStyle(style, true);
UIApplication.SharedApplication.SetStatusBarHidden(false, true);
}
}
[SuppressMessage("Code Notifications", "XI0002:Notifies you from using newer Apple APIs when targeting an older OS version", Justification = "<Pending>")]
public static UIStatusBarStyle ConvertStyle(StatusBarStyles style)
{
switch (style)
{
case StatusBarStyles.DarkText:
return UIStatusBarStyle.DarkContent;
case StatusBarStyles.WhiteText:
return UIStatusBarStyle.LightContent;
case StatusBarStyles.Hidden:
return UIStatusBarStyle.BlackOpaque;
case StatusBarStyles.Default:
default:
return UIStatusBarStyle.Default;
}
}
#else
}
#endif
#endregion
#region - Culture Info -
public static void SetCultureInfo(CultureInfo ci)
{
Thread.CurrentThread.CurrentCulture = ci;
Thread.CurrentThread.CurrentUICulture = ci;
#if LOG
App.DebugPrint($"CurrentCulture set: {ci.Name}");
#endif
}
public static CultureInfo GetCurrentCultureInfo()
{
string lang;
#if __IOS__
if (NSLocale.PreferredLanguages.Length > 0)
{
var pref = NSLocale.PreferredLanguages[0];
lang = iOSToDotnetLanguage(pref);
}
else
{
lang = "zh-CN";
}
#elif __ANDROID__
var locale = Java.Util.Locale.Default;
lang = AndroidToDotnetLanguage(locale.ToString().Replace('_', '-'));
#endif
CultureInfo ci;
var platform = new PlatformCulture(lang);
try
{
ci = new CultureInfo(platform.Language);
}
catch (CultureNotFoundException e)
{
try
{
var fallback = ToDotnetFallbackLanguage(platform);
App.DebugPrint($"{lang} failed, trying {fallback} ({e.Message})");
ci = new CultureInfo(fallback);
}
catch (CultureNotFoundException e1)
{
App.DebugError("culture.get", $"{lang} couldn't be set, using 'zh-CN' ({e1.Message})");
ci = new CultureInfo("zh-CN");
}
}
return ci;
}
#if __IOS__
[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
private static string iOSToDotnetLanguage(string iOSLanguage)
{
string netLanguage;
//certain languages need to be converted to CultureInfo equivalent
switch (iOSLanguage)
{
case "ms-MY": // "Malaysian (Malaysia)" not supported .NET culture
case "ms-SG": // "Malaysian (Singapore)" not supported .NET culture
netLanguage = "ms"; // closest supported
break;
case "gsw-CH": // "Schwiizertüütsch (Swiss German)" not supported .NET culture
netLanguage = "de-CH"; // closest supported
break;
// add more application-specific cases here (if required)
// ONLY use cultures that have been tested and known to work
default:
netLanguage = iOSLanguage;
break;
}
#if DEBUG
App.DebugPrint($"iOS Language: {iOSLanguage}, .NET Language/Locale: {netLanguage}");
#endif
return netLanguage;
}
#elif __ANDROID__
private static string AndroidToDotnetLanguage(string androidLanguage)
{
string netLanguage;
//certain languages need to be converted to CultureInfo equivalent
switch (androidLanguage)
{
case "ms-BN": // "Malaysian (Brunei)" not supported .NET culture
case "ms-MY": // "Malaysian (Malaysia)" not supported .NET culture
case "ms-SG": // "Malaysian (Singapore)" not supported .NET culture
netLanguage = "ms"; // closest supported
break;
case "in-ID": // "Indonesian (Indonesia)" has different code in .NET
netLanguage = "id-ID"; // correct code for .NET
break;
case "gsw-CH": // "Schwiizertüütsch (Swiss German)" not supported .NET culture
netLanguage = "de-CH"; // closest supported
break;
// add more application-specific cases here (if required)
// ONLY use cultures that have been tested and known to work
default:
netLanguage = androidLanguage;
break;
}
#if DEBUG
App.DebugPrint($"Android Language: {androidLanguage}, .NET Language/Locale: {netLanguage}");
#endif
return netLanguage;
}
#endif
private static string ToDotnetFallbackLanguage(PlatformCulture platCulture)
{
string netLanguage;
switch (platCulture.LanguageCode)
{
//
case "pt":
netLanguage = "pt-PT"; // fallback to Portuguese (Portugal)
break;
case "gsw":
netLanguage = "de-CH"; // equivalent to German (Switzerland) for this app
break;
// add more application-specific cases here (if required)
// ONLY use cultures that have been tested and known to work
default:
netLanguage = platCulture.LanguageCode; // use the first part of the identifier (two chars, usually);
break;
}
#if DEBUG
App.DebugPrint($".NET Fallback Language/Locale: {platCulture.LanguageCode} to {netLanguage} (application-specific)");
#endif
return netLanguage;
}
#endregion
}
}

279
Gallery/Utils/Extensions.cs Executable file
View File

@ -0,0 +1,279 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace Gallery.Utils
{
public static class Extensions
{
public static T Binding<T>(this T view, BindableProperty property, string name,
BindingMode mode = BindingMode.Default, IValueConverter converter = null) where T : BindableObject
{
if (name == null)
{
view.SetValue(property, property.DefaultValue);
}
else
{
view.SetBinding(property, name, mode, converter);
}
return view;
}
public static T DynamicResource<T>(this T view, BindableProperty property, string key) where T : Element
{
view.SetDynamicResource(property, key);
return view;
}
public static T GridRow<T>(this T view, int row) where T : BindableObject
{
Grid.SetRow(view, row);
return view;
}
public static T GridColumn<T>(this T view, int column) where T : BindableObject
{
Grid.SetColumn(view, column);
return view;
}
public static T GridColumnSpan<T>(this T view, int columnSpan) where T : BindableObject
{
Grid.SetColumnSpan(view, columnSpan);
return view;
}
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 class ParallelTask : IDisposable
{
public static ParallelTask Start(string tag, int from, int toExclusive, int maxCount, Predicate<int> action, int tagIndex = -1, Action complete = null)
{
if (toExclusive <= from)
{
if (complete != null)
{
Task.Run(complete);
}
return null;
}
var task = new ParallelTask(tag, from, toExclusive, maxCount, action, tagIndex, complete);
task.Start();
return task;
}
private readonly object sync = new object();
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 Action complete;
private ParallelTask(string tag, int from, int to, int maxCount, Predicate<int> action, int tagIndex, Action 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);
}
private void DoStart(object state)
{
#if LOG
var sw = new System.Diagnostics.Stopwatch();
long lastElapsed = 0;
sw.Start();
#endif
for (int i = from; i < to; i++)
{
var index = i;
while (true)
{
if (count < max)
{
break;
}
#if LOG
var elapsed = sw.ElapsedMilliseconds;
if (elapsed - lastElapsed > 60000)
{
lastElapsed = elapsed;
App.DebugPrint($"WARNING: parallel task ({tag}), {count} tasks in queue, cost too much time ({elapsed:n0}ms)");
}
#endif
if (disposed)
{
#if LOG
sw.Stop();
App.DebugPrint($"parallel task determinate, disposed ({tag}), cost time ({elapsed:n0}ms)");
#endif
return;
}
Thread.Sleep(16);
}
lock (sync)
{
count++;
}
ThreadPool.QueueUserWorkItem(o =>
//Task.Run(() =>
{
try
{
if (!action(index))
{
disposed = true;
}
}
catch (Exception ex)
{
App.DebugError($"parallel.start ({tag})", $"failed to run action, index: {index}, error: {ex}");
}
finally
{
lock (sync)
{
count--;
}
}
});
}
while (count > 0)
{
#if LOG
var elapsed = sw.ElapsedMilliseconds;
if (elapsed - lastElapsed > 60000)
{
lastElapsed = elapsed;
App.DebugPrint($"WARNING: parallel task ({tag}), {count} tasks are waiting for end, cost too much time ({elapsed:n0}ms)");
}
#endif
if (disposed)
{
#if LOG
sw.Stop();
App.DebugPrint($"parallel task determinate, disposed ({tag}), cost time ({elapsed:n0}ms)");
#endif
return;
}
Thread.Sleep(16);
}
#if LOG
sw.Stop();
App.DebugPrint($"parallel task done ({tag}), cost time ({sw.ElapsedMilliseconds:n0}ms)");
#endif
complete?.Invoke();
}
public void Dispose()
{
disposed = true;
}
}
public static class Screen
{
private const string StatusBarStyle = nameof(StatusBarStyle);
private const string HomeIndicatorAutoHidden = nameof(HomeIndicatorAutoHidden);
public static readonly BindableProperty StatusBarStyleProperty = BindableProperty.CreateAttached(
StatusBarStyle,
typeof(StatusBarStyles),
typeof(Page),
StatusBarStyles.WhiteText);
public static StatusBarStyles GetStatusBarStyle(VisualElement page) => (StatusBarStyles)page.GetValue(StatusBarStyleProperty);
public static void SetStatusBarStyle(VisualElement page, StatusBarStyles value) => page.SetValue(StatusBarStyleProperty, value);
public static readonly BindableProperty HomeIndicatorAutoHiddenProperty = BindableProperty.CreateAttached(
HomeIndicatorAutoHidden,
typeof(bool),
typeof(Shell),
false);
public static bool GetHomeIndicatorAutoHidden(VisualElement page) => (bool)page.GetValue(HomeIndicatorAutoHiddenProperty);
public static void SetHomeIndicatorAutoHidden(VisualElement page, bool value) => page.SetValue(HomeIndicatorAutoHiddenProperty, value);
}
public enum StatusBarStyles
{
Default,
// Will behave as normal.
// White text on black NavigationBar/in iOS Dark mode and
// Black text on white NavigationBar/in iOS Light mode
DarkText,
// Will switch the color of content of StatusBar to black.
WhiteText,
// Will switch the color of content of StatusBar to white.
Hidden
// Will hide the StatusBar
}
}

92
Gallery/Utils/FileStore.cs Executable file
View File

@ -0,0 +1,92 @@
using System.Threading.Tasks;
using Xamarin.Forms;
#if __IOS__
using UIKit;
using Xamarin.Forms.Platform.iOS;
#elif __ANDROID__
using Android.Content;
using Android.Net;
using System.IO;
using System.Linq;
#endif
namespace Gallery.Utils
{
public class FileStore
{
#if __IOS__
public static Task<string> SaveVideoToGalleryAsync(string file)
{
var task = new TaskCompletionSource<string>();
if (UIVideo.IsCompatibleWithSavedPhotosAlbum(file))
{
UIVideo.SaveToPhotosAlbum(file, (path, err) =>
{
task.SetResult(err?.ToString());
});
}
return task.Task;
}
#endif
public static Task<string> SaveImageToGalleryAsync(ImageSource image)
{
#if __IOS__
IImageSourceHandler renderer;
if (image is UriImageSource)
{
renderer = new ImageLoaderSourceHandler();
}
else if (image is FileImageSource)
{
renderer = new FileImageSourceHandler();
}
else
{
renderer = new StreamImagesourceHandler();
}
var photo = renderer.LoadImageAsync(image).Result;
var task = new TaskCompletionSource<string>();
if (photo == null)
{
task.SetResult(null);
}
else
{
photo.SaveToPhotosAlbum((img, error) =>
{
task.SetResult(error?.ToString());
});
}
return task.Task;
#elif __ANDROID__
Java.IO.File camera;
var dirs = Droid.MainActivity.Main.GetExternalMediaDirs();
camera = dirs.FirstOrDefault();
if (camera == null)
{
camera = Droid.MainActivity.Main.GetExternalFilesDir(Android.OS.Environment.DirectoryPictures);
}
if (!camera.Exists())
{
camera.Mkdirs();
}
var original = ((FileImageSource)image).File;
var filename = Path.GetFileName(original);
var imgFile = new Java.IO.File(camera, filename).AbsolutePath;
File.Copy(original, imgFile);
var uri = Uri.FromFile(new Java.IO.File(imgFile));
var intent = new Intent(Intent.ActionMediaScannerScanFile);
intent.SetData(uri);
Droid.MainActivity.Main.SendBroadcast(intent);
var task = new TaskCompletionSource<string>();
task.SetResult(null);
return task.Task;
#endif
}
}
}

View File

@ -0,0 +1,464 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Gallery.Utils
{
public class HttpUtility
{
public static T LoadObject<T>(string file, string url, string referer, out string error,
bool force = false,
bool nojson = false,
HttpContent post = null,
Action<HttpRequestHeaders> header = null,
Func<T, string> namehandler = null,
Func<string, string> action = null,
Func<string, T> @return = null)
{
string content = null;
if (post == null && !force && file != null && File.Exists(file))
{
try
{
content = File.ReadAllText(file);
}
catch (Exception ex)
{
App.DebugError("load", $"failed to read file: {file}, error: {ex.Message}");
}
}
if (content == null)
{
bool noToken = string.IsNullOrEmpty(Configs.CsrfToken);
if (noToken)
{
post = null;
}
var response = Download(url, headers =>
{
if (referer != null)
{
headers.Referrer = new Uri(referer);
}
headers.Add("User-Agent", Configs.UserAgent);
headers.Add("Accept", Configs.AcceptJson);
var cookie = Configs.Cookie;
if (cookie != null)
{
headers.Add("Cookie", cookie);
}
if (post != null && !noToken)
{
headers.Add("Origin", Configs.Referer);
headers.Add("X-Csrf-Token", Configs.CsrfToken);
}
if (header == null)
{
var userId = Configs.UserId;
if (userId != null)
{
headers.Add("X-User-Id", userId);
}
}
else
{
header(headers);
}
}, post);
if (response == null)
{
error = "response is null";
return default;
}
if (!response.IsSuccessStatusCode)
{
App.DebugPrint($"http failed with code: {(int)response.StatusCode} - {response.StatusCode}");
error = response.StatusCode.ToString();
return default;
}
using (response)
{
try
{
content = response.Content.ReadAsStringAsync().Result;
if (action != null)
{
content = action(content);
}
if (@return != null)
{
error = null;
return @return(content);
}
}
catch (Exception ex)
{
App.DebugError("load.stream", $"failed to read stream, error: {ex.Message}");
error = ex.Message;
return default;
}
if (content == null)
{
content = string.Empty;
}
bool rtn = false;
T result = default;
if (namehandler != null)
{
try
{
result = JsonConvert.DeserializeObject<T>(content);
file = namehandler(result);
rtn = true;
}
catch (Exception ex)
{
var memo = content.Length < 20 ? content : content.Substring(0, 20) + "...";
App.DebugError("load", $"failed to parse illust JSON object, content: {memo}, error: {ex.Message}");
error = content;
return default;
}
}
if (file != null)
{
try
{
var folder = Path.GetDirectoryName(file);
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
File.WriteAllText(file, content, Encoding.UTF8);
}
catch (Exception ex)
{
App.DebugError("save", $"failed to save illust JSON object, error: {ex.Message}");
}
}
if (rtn)
{
error = null;
return result;
}
}
}
try
{
error = null;
if (nojson)
{
return JsonConvert.DeserializeObject<T>("{}");
}
else
{
return JsonConvert.DeserializeObject<T>(content);
}
}
catch (Exception ex)
{
var memo = content.Length < 20 ? content : content.Substring(0, 20) + "...";
App.DebugError("load", $"failed to parse illust JSON object, content: {memo}, error: {ex.Message}");
error = content;
return default;
}
}
public static 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 = Download(url, headers =>
{
headers.Referrer = new Uri(Configs.Referer);
headers.Add("User-Agent", Configs.UserAgent);
headers.Add("Accept", Configs.AcceptPureImage);
});
if (response == null)
{
return null;
}
using (response)
using (var fs = File.OpenWrite(file))
{
response.Content.CopyToAsync(fs).Wait();
//if (response.Headers.Date != null)
//{
// File.SetLastWriteTimeUtc(file, response.Headers.Date.Value.UtcDateTime);
//}
}
return file;
}
catch (Exception ex)
{
App.DebugError("image.download", ex.Message);
return null;
}
}
public static 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 = Configs.Proxy;
var referer = new Uri(string.Format(Configs.RefererIllust, id));
var handler = new HttpClientHandler
{
UseCookies = false
};
if (proxy != null)
{
handler.Proxy = proxy;
handler.UseProxy = true;
}
var client = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
long size;
DateTimeOffset lastModified;
using (var request = new HttpRequestMessage(HttpMethod.Head, url))
{
var headers = request.Headers;
headers.Add("Accept", Configs.AcceptPureImage);
headers.Add("Accept-Language", Configs.AcceptLanguage);
headers.Referrer = referer;
headers.Add("User-Agent", Configs.UserAgent);
using (var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result)
{
size = response.Content.Headers.ContentLength.Value;
lastModified = response.Content.Headers.LastModified.Value;
#if DEBUG
App.DebugPrint($"content length: {size:n0} bytes, last modified: {lastModified}");
#endif
}
}
// segments
const int SIZE = 150000;
var list = new List<(long from, long to)>();
for (var i = 0L; 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, Configs.DownloadIllustThreads, i =>
{
var (from, to) = list[i];
using (var request = new HttpRequestMessage(HttpMethod.Get, url))
{
var headers = request.Headers;
headers.Add("Accept", Configs.AcceptPureImage);
headers.Add("Accept-Language", Configs.AcceptLanguage);
headers.Add("Accept-Encoding", "identity");
headers.Referrer = referer;
headers.IfRange = new RangeConditionHeaderValue(lastModified);
headers.Range = new RangeHeaderValue(from, to);
headers.Add("User-Agent", Configs.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
App.DebugPrint($"downloaded range: from ({from:n0}) to ({to:n0})");
#endif
}
}
return true;
},
complete: () =>
{
using (var fs = File.OpenWrite(file))
{
fs.Write(data, 0, data.Length);
}
task.SetResult(file);
});
return task.Task;
}
catch (Exception ex)
{
App.DebugError("image.download.async", ex.Message);
return Task.FromResult<string>(null);
}
}
private static HttpResponseMessage Download(string url, Action<HttpRequestHeaders> headerAction, HttpContent post = null)
{
#if DEBUG
var method = post == null ? "GET" : "POST";
App.DebugPrint($"{method}: {url}");
#endif
var uri = new Uri(url);
var proxy = Configs.Proxy;
var handler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = false
};
if (proxy != null)
{
handler.Proxy = proxy;
handler.UseProxy = true;
}
var client = new HttpClient(handler)
{
BaseAddress = new Uri($"{uri.Scheme}://{uri.Host}"),
Timeout = TimeSpan.FromSeconds(30)
};
return TryCount(() =>
{
using (var request = new HttpRequestMessage(post == null ? HttpMethod.Get : HttpMethod.Post, uri.PathAndQuery)
{
Version = new Version(1, 1)
})
{
var headers = request.Headers;
headerAction(headers);
//if (proxy == null)
//{
// var time = BitConverter.GetBytes(DateTime.UtcNow.Ticks);
// headers.Add("X-Reverse-Ticks", Convert.ToBase64String(time));
// time = time.Concat(Encoding.UTF8.GetBytes("_reverse_for_pixiv_by_tsanie")).ToArray();
// var reverse = System.Security.Cryptography.SHA256.Create().ComputeHash(time);
// headers.Add("X-Reverse", Convert.ToBase64String(reverse));
//}
headers.Add("Accept-Language", Configs.AcceptLanguage);
//headers.Add("Accept-Encoding", Configs.AcceptEncoding);
if (post != null)
{
request.Content = post;
}
return client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result;
}
});
}
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);
App.DebugError("try.do", $"tries: {tries}, error: {ex.Message}");
}
}
return default;
}
public static (long Size, DateTimeOffset LastModified, HttpClient Client) GetUgoiraHeader(string url, string id)
{
var uri = new Uri(url);
var proxy = Configs.Proxy;
var handler = new HttpClientHandler
{
UseCookies = false
};
if (proxy != null)
{
handler.Proxy = proxy;
handler.UseProxy = true;
}
var client = new HttpClient(handler)
{
BaseAddress = new Uri($"{uri.Scheme}://{uri.Host}"),
Timeout = TimeSpan.FromSeconds(30)
};
var response = TryCount(() =>
{
using (var request = new HttpRequestMessage(HttpMethod.Head, uri.PathAndQuery)
{
Version = new Version(1, 1)
})
{
var headers = request.Headers;
UgoiraHeaderAction(headers, id);
headers.Add("Accept-Encoding", "gzip, deflate");
headers.Add("Accept-Language", Configs.AcceptLanguage);
return client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result;
}
});
var size = response.Content.Headers.ContentLength.Value;
var lastModified = response.Content.Headers.LastModified.Value;
return (size, lastModified, client);
}
public static long DownloadUgoiraImage(HttpClient client, string url, string id, DateTimeOffset lastModified, long from, long to, Stream stream)
{
var uri = new Uri(url);
var response = TryCount(() =>
{
using (var request = new HttpRequestMessage(HttpMethod.Get, uri.PathAndQuery)
{
Version = new Version(1, 1)
})
{
var headers = request.Headers;
UgoiraHeaderAction(headers, id);
headers.Add("Accept-Encoding", "identity");
headers.IfRange = new RangeConditionHeaderValue(lastModified);
headers.Range = new RangeHeaderValue(from, to);
headers.Add("Accept-Language", Configs.AcceptLanguage);
return client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result;
}
});
var length = response.Content.Headers.ContentLength.Value;
response.Content.CopyToAsync(stream).Wait();
return length;
}
private static void UgoiraHeaderAction(HttpRequestHeaders headers, string id)
{
headers.Add("Accept", "*/*");
headers.Add("Origin", Configs.Referer);
headers.Referrer = new Uri(string.Format(Configs.RefererIllust, id));
headers.Add("User-Agent", Configs.UserAgent);
}
}
}

321
Gallery/Utils/IllustData.cs Normal file
View File

@ -0,0 +1,321 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Gallery.Illust;
using Xamarin.Forms;
namespace Gallery.Utils
{
public class IllustResponse<T>
{
public bool error;
public string message;
public T body;
}
public class BookmarkResultData
{
public string last_bookmark_id;
public string stacc_status_id;
}
public class Illust
{
public string illustId;
public string illustTitle;
public string id;
public string title;
public int illustType;
public int xRestrict;
public string url;
public string description;
public string[] tags;
public string userId;
public string userName;
public int width;
public int height;
public int pageCount;
public IllustBookmark bookmarkData;
public string alt;
public IllustUrls urls;
public string seriesId;
public string seriesTitle;
public string profileImageUrl;
public class IllustUrls
{
[JsonProperty("250x250")]
public string x250;
[JsonProperty("360x360")]
public string x360;
[JsonProperty("540x540")]
public string x540;
}
public class IllustBookmark
{
public string id;
[JsonProperty("private")]
public bool isPrivate;
}
public IllustItem ConvertToItem(ImageSource image = null)
{
return new IllustItem
{
Id = illustId ?? id,
BookmarkId = bookmarkData?.id,
Title = illustTitle ?? title,
IllustType = (IllustType)illustType,
Image = image,
ImageUrl = urls?.x360 ?? url,
IsRestrict = xRestrict == 1,
Tags = tags ?? new string[0],
ProfileUrl = profileImageUrl,
UserId = userId,
UserName = userName,
Width = width,
Height = height,
PageCount = pageCount
};
}
}
public class User
{
public string userId;
public string name;
public string image;
public string imageBig;
public bool premium;
public bool isFollowed;
//public string background;
public int partial;
}
public class IllustFavoriteData : IllustResponse<IllustFavoriteBody> { }
public class IllustFavoriteBody
{
public int total;
public Illust[] works;
}
public class IllustData : IllustResponse<IllustBody> { }
public class IllustBody
{
public Page page;
public Thumbnail thumbnails;
public User[] users;
public class Page
{
public int[] follow;
public Recommends recommend;
public RecommendByTag[] recommendByTags;
public Ranking ranking;
public RecommendUser[] recommendUser;
public EditorRecommend[] editorRecommend;
public string[] newPost;
public class Recommends
{
public string[] ids;
}
public class RecommendByTag
{
public string tag;
public string[] ids;
}
public class Ranking
{
public RankingItem[] items;
public string date;
public class RankingItem
{
public string rank;
public string id;
}
}
public class RecommendUser
{
public int id;
public string[] illustIds;
}
public class EditorRecommend
{
public string illustId;
public string comment;
}
}
public class Thumbnail
{
public Illust[] illust;
}
}
public class IllustPreloadBody
{
public Dictionary<string, Illust> illust;
public Dictionary<string, User> user;
public class Illust
{
public string illustId;
public string illustTitle;
public string illustComment;
public string id;
public string title;
public string description;
public int illustType;
public DateTime createDate;
public DateTime uploadDate;
public int xRestrict;
public IllustUrls urls;
public IllustTag tags;
public string alt;
public string userId;
public string userName;
public string userAccount;
//public Dictionary<string, Illust> userIllusts;
public int width;
public int height;
public int pageCount;
public int bookmarkCount;
public int likeCount;
public int commentCount;
public int responseCount;
public int viewCount;
public bool isOriginal;
public IllustBookmark bookmarkData;
public IllustItem CopyToItem(IllustItem item)
{
item.BookmarkId = bookmarkData?.id;
item.Title = illustTitle ?? title;
item.IllustType = (IllustType)illustType;
item.ImageUrl = urls?.regular;
item.IsRestrict = xRestrict == 1;
if (tags != null && tags.tags != null)
{
item.Tags = tags.tags.Where(t => t.locked).Select(t => t.tag).ToArray();
}
else
{
item.Tags = new string[0];
}
item.UserId = userId;
item.UserName = userName;
item.Width = width;
item.Height = height;
item.PageCount = pageCount;
return item;
}
[JsonIgnore]
public string Url => urls.regular;
public class IllustBookmark
{
public string id;
[JsonProperty("private")]
public bool isPrivate;
}
public class IllustUrls
{
public string mini;
public string thumb;
public string small;
public string regular;
public string original;
}
public class IllustTag
{
public string authorId;
public bool isLocked;
public IllustTagItem[] tags;
public bool writable;
public class IllustTagItem
{
public string tag;
public bool locked;
public bool deletable;
public string userId;
public IllustTranslate translation;
public string userName;
public class IllustTranslate
{
public string en;
}
}
}
}
}
public class IllustPageData : IllustResponse<IllustPageBody[]> { }
public class IllustPageBody
{
public Urls urls;
public int width;
public int height;
public class Urls
{
public string thumb_mini;
public string small;
public string regular;
public string original;
}
}
public class IllustRecommendsData : IllustResponse<IllustRecommendsBody> { }
public class IllustRecommendsBody
{
public Illust[] illusts;
public string[] nextIds;
}
public class IllustUserListData : IllustResponse<IllustUserListBody> { }
public class IllustUserListBody
{
public Dictionary<string, object> illusts;
}
public class IllustUserData : IllustResponse<IllustUserBody> { }
public class IllustUserBody
{
public Dictionary<string, Illust> works;
}
public class IllustUgoiraData : IllustResponse<IllustUgoiraBody> { }
public class IllustUgoiraBody
{
public string src;
public string originalSrc;
public string mime_type;
public Frame[] frames;
public class Frame
{
public string file;
public int delay;
public string FilePath;
public bool Incompleted;
public int First;
public int Last;
public int Offset;
public int Length;
}
}
}

141
Gallery/Utils/IllustLegacy.cs Executable file
View File

@ -0,0 +1,141 @@
using System.Linq;
using Gallery.Illust;
namespace Gallery.Utils
{
public class IllustRankingData
{
public Content[] contents;
public string mode;
public string content;
public int page;
public string prev;
public string next;
public string date;
public string prev_date;
public string next_date;
public int rank_total;
public class Content
{
public string title;
public string date;
public string[] tags;
public string url;
public string illust_type;
public string illust_book_style;
public string illust_page_count;
public string user_name;
public string profile_img;
public ContentType illust_content_type;
public object illust_series; // bool, Series
public long illust_id;
public int width;
public int height;
public long user_id;
public int rank;
public int yes_rank;
public int rating_count;
public int view_count;
public long illust_upload_timestamp;
public string attr;
public bool is_bookmarked;
public bool bookmarkable;
public string bookmark_id;
public string bookmark_illust_restrict;
public class ContentType
{
public int sexual;
public bool lo;
public bool grotesque;
public bool violent;
public bool homosexual;
public bool drug;
public bool thoughts;
public bool antisocial;
public bool religion;
public bool original;
public bool furry;
public bool bl;
public bool yuri;
}
public class Series
{
public string illust_series_caption;
public string illust_series_content_count;
public string illust_series_content_illust_id;
public string illust_series_content_order;
public string illust_series_create_datetime;
public string illust_series_id;
public string illust_series_title;
public string illust_series_user_id;
public string page_url;
}
public IllustItem ConvertToItem()
{
if (!int.TryParse(illust_page_count, out int count))
{
count = 1;
}
if (!int.TryParse(illust_type, out int type))
{
type = 0;
}
bool restrict;
if (tags != null && tags.Contains("R-18"))
{
restrict = true;
}
else
{
restrict = false;
}
return new IllustItem
{
Id = illust_id.ToString(),
BookmarkId = bookmark_id,
Title = title,
Rank = rank,
IllustType = (IllustType)type,
ImageUrl = url,
IsRestrict = restrict,
Tags = tags ?? new string[0],
ProfileUrl = profile_img,
UserId = user_id.ToString(),
UserName = user_name,
Width = width,
Height = height,
PageCount = count,
YesRank = yes_rank,
RatingCount = rating_count,
ViewCount = view_count,
UploadTimestamp = illust_upload_timestamp
};
}
}
}
public class IllustGlobalData
{
public string token;
public string oneSignalAppId;
public UserData userData;
public class UserData
{
public string id;
public string pixivId;
public string name;
public string profileImg;
public string profileImgBig;
public bool premium;
public int xRestrict;
public bool adult;
public bool safeMode;
}
}
}

View File

@ -0,0 +1,26 @@
using System.Windows.Input;
using Xamarin.Forms;
namespace Gallery.Utils
{
public class LongPressEffect : RoutingEffect
{
private const string Command = nameof(Command);
private const string CommandParameter = nameof(CommandParameter);
public static readonly BindableProperty CommandProperty = BindableProperty.CreateAttached(
Command, typeof(ICommand), typeof(LongPressEffect), null);
public static readonly BindableProperty CommandParameterProperty = BindableProperty.CreateAttached(
CommandParameter, typeof(object), typeof(LongPressEffect), null);
public static ICommand GetCommand(BindableObject view) => (ICommand)view.GetValue(CommandProperty);
public static void SetCommand(BindableObject view, ICommand command) => view.SetValue(CommandProperty, command);
public static object GetCommandParameter(BindableObject view) => view.GetValue(CommandParameterProperty);
public static void SetCommandParameter(BindableObject view, object value) => view.SetValue(CommandParameterProperty, value);
public LongPressEffect() : base("Gallery.LongPressEffect")
{
}
}
}

863
Gallery/Utils/Stores.cs Normal file
View File

@ -0,0 +1,863 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Gallery.Illust;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.Utils
{
public static class Stores
{
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 globalFile = "global.json";
private const string imageFolder = "img-original";
private const string previewFolder = "img-master";
private const string ugoiraFolder = "img-zip-ugoira";
private const string illustFile = "illust.json";
private const string pagesFolder = "pages";
private const string preloadsFolder = "preloads";
private const string thumbFolder = "img-thumb";
private const string userFolder = "user-profile";
//private const string recommendsFolder = "recommends";
public static bool NetworkAvailable
{
get
{
try
{
return Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.Internet;
}
catch
{
return false;
}
}
}
public static FavoriteList Favorites => GetFavoriteObject().Illusts;
public static string FavoritesPath => Path.Combine(PersonalFolder, favoriteFile);
public static DateTime FavoritesLastUpdated { get; set; } = DateTime.Now;
private static IllustFavorite favoriteObject;
public static IllustFavorite GetFavoriteObject(bool force = false)
{
if (force || favoriteObject == null)
{
var favorites = LoadFavoritesIllusts();
if (favorites != null)
{
favoriteObject = favorites;
}
else
{
favoriteObject = new IllustFavorite
{
Illusts = new FavoriteList()
};
}
}
return favoriteObject;
}
public static IllustFavorite LoadFavoritesIllusts(string file = null)
{
if (file == null)
{
file = FavoritesPath;
}
return ReadObject<IllustFavorite>(file);
}
public static void SaveFavoritesIllusts()
{
var file = FavoritesPath;
var data = GetFavoriteObject();
data.LastFavoriteUtc = DateTime.UtcNow;
WriteObject(file, data);
}
public static string LoadUgoiraImage(string zip, string frame)
{
var file = Path.Combine(PersonalFolder, ugoiraFolder, zip, frame);
if (File.Exists(file))
{
return file;
}
return null;
}
public static string SaveUgoiraImage(string zip, string frame, byte[] data)
{
try
{
var directory = Path.Combine(PersonalFolder, ugoiraFolder, zip);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var file = Path.Combine(directory, frame);
File.WriteAllBytes(file, data);
return file;
}
catch (Exception ex)
{
App.DebugError("save.ugoira", $"failed to save ugoira frame: {zip}/{frame}, error: {ex.Message}");
return null;
}
}
public static string GetUgoiraPath(string url, string ext)
{
return Path.Combine(PersonalFolder, ugoiraFolder, Path.GetFileNameWithoutExtension(url) + ext);
}
private static T ReadObject<T>(string file)
{
string content = null;
if (File.Exists(file))
{
try
{
content = File.ReadAllText(file);
}
catch (Exception ex)
{
App.DebugError("read", $"failed to read file: {file}, error: {ex.Message}");
}
}
else
{
//App.DebugError("read", $"file not found: {file}");
return default;
}
try
{
return JsonConvert.DeserializeObject<T>(content);
}
catch (Exception ex)
{
App.DebugError("read", $"failed to parse illust JSON object, error: {ex.Message}");
return default;
}
}
private static void WriteObject(string file, object obj)
{
var dir = Path.GetDirectoryName(file);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
string content;
try
{
content = JsonConvert.SerializeObject(obj, Formatting.None);
}
catch (Exception ex)
{
App.DebugError("write", $"failed to serialize object, error: {ex.Message}");
return;
}
try
{
File.WriteAllText(file, content, Encoding.UTF8);
}
catch (Exception ex)
{
App.DebugError("write", $"failed to write file: {file}, error: {ex.Message}");
}
}
public static IllustData LoadIllustData(bool force = false)
{
var file = Path.Combine(PersonalFolder, illustFile);
var result = HttpUtility.LoadObject<IllustData>(
file,
Configs.UrlIllustList,
Configs.Referer,
out _,
force: force);
if (result == null || result.error)
{
App.DebugPrint($"error when load illust data: {result?.message}, force({force})");
}
return result;
}
public static IllustRankingData LoadIllustRankingData(string mode, string date, int page, out string error, bool force = false)
{
var file = Path.Combine(CacheFolder, mode, $"{date}_{page}.json");
string query = $"mode={mode}";
if (mode != "male" && mode != "male_r18")
{
query += "&content=illust";
}
if (date != null)
{
query += $"&date={date}";
}
var referer = string.Format(Configs.RefererIllustRanking, query);
if (page > 1)
{
query += $"&p={page}";
}
query += "&format=json";
var result = HttpUtility.LoadObject<IllustRankingData>(
file,
string.Format(Configs.UrlIllustRanking, query),
referer,
out error,
namehandler: rst =>
{
return Path.Combine(CacheFolder, mode, $"{rst.date}_{page}.json");
},
header: headers =>
{
headers.Add("X-Requested-With", "XMLHttpRequest");
},
force: force);
if (result == null)
{
App.DebugPrint($"error when load ranking data: mode({mode}), date({date}), page({page}), force({force})");
}
return result;
}
public static IllustRecommendsData LoadIllustRecommendsInitData(string id)
{
//var file = Path.Combine(CacheFolder, recommendsFolder, $"{id}.json");
var result = HttpUtility.LoadObject<IllustRecommendsData>(
null,
string.Format(Configs.UrlIllustRecommendsInit, id),
string.Format(Configs.RefererIllust, id),
out _);
if (result == null || result.error)
{
App.DebugPrint($"error when load recommends init data: {result?.message}");
}
return result;
}
public static IllustRecommendsData LoadIllustRecommendsListData(string id, string[] ids)
{
if (ids == null || ids.Length == 0)
{
return null;
}
var ps = string.Concat(ids.Select(i => $"illust_ids%5B%5D={i}&"));
var result = HttpUtility.LoadObject<IllustRecommendsData>(
null,
string.Format(Configs.UrlIllustRecommendsList, ps),
string.Format(Configs.RefererIllust, id),
out _);
if (result == null || result.error)
{
App.DebugPrint($"error when load recommends list data: {result?.message}");
}
return result;
}
public static IllustGlobalData LoadGlobalData(bool force = false)
{
var file = Path.Combine(PersonalFolder, globalFile);
var result = HttpUtility.LoadObject<IllustGlobalData>(
file,
Configs.Prefix,
null,
out _,
force: force,
header: h => { },
action: content =>
{
var index = content.IndexOf(Configs.SuffixGlobal);
if (index > 0)
{
index += Configs.SuffixGlobalLength;
var end = content.IndexOf('\'', index);
if (end > index)
{
content = content.Substring(index, end - index);
}
}
return content;
});
if (result == null)
{
App.DebugPrint($"error when load global data, is null");
}
else
{
#if LOG
App.DebugPrint($"current csrf token: {result.token}");
#endif
Configs.CsrfToken = result.token;
}
return result;
}
public static string AddBookmark(string id)
{
if (string.IsNullOrEmpty(Configs.CsrfToken))
{
return null;
}
var content = new StringContent(
"{\"illust_id\":\"" + id + "\",\"restrict\":0,\"comment\":\"\",\"tags\":[]}",
Encoding.UTF8,
Configs.AcceptJson);
var result = HttpUtility.LoadObject<IllustResponse<BookmarkResultData>>(
null,
Configs.BookmarkAdd,
Configs.Referer,
out string error,
force: true,
post: content);
if (error != null)
{
App.DebugPrint($"failed to add bookmark, error: {error}");
}
else if (result == null || result.error || result.body == null)
{
App.DebugPrint($"failed to add bookmark, message: {result?.message}");
}
else
{
#if LOG
App.DebugPrint($"successs, bookmark id: {result.body.last_bookmark_id}, status: {result.body.stacc_status_id}");
#endif
return result.body.last_bookmark_id;
}
return null;
}
public static bool DeleteBookmark(string id)
{
if (string.IsNullOrEmpty(Configs.CsrfToken))
{
return false;
}
var content = new StringContent(
"mode=delete_illust_bookmark&bookmark_id=" + id,
Encoding.UTF8,
Configs.AcceptUrlEncoded);
var result = HttpUtility.LoadObject<object>(
null,
Configs.BookmarkRpc,
Configs.Referer,
out string error,
force: true,
nojson: true,
post: content);
if (error != null)
{
App.DebugPrint($"failed to delete bookmark, error: {error}");
return false;
}
else if (result == null)
{
App.DebugPrint("failed to delete bookmark, result is null");
return false;
}
#if LOG
App.DebugPrint($"successs, delete bookmark");
#endif
return true;
}
public static IllustPreloadBody LoadIllustPreloadData(string id, bool downloading, bool force = false)
{
var file = Path.Combine(CacheFolder, preloadsFolder, $"{id}.json");
IllustPreloadBody result;
if (!force)
{
result = ReadObject<IllustPreloadBody>(file);
if (result != null)
{
return result;
}
else if (!downloading)
{
return null;
}
}
if (downloading)
{
result = HttpUtility.LoadObject<IllustPreloadBody>(
file,
string.Format(Configs.UrlIllust, id),
null,
out _,
force: force,
action: content =>
{
var index = content.IndexOf(Configs.SuffixPreload);
if (index > 0)
{
index += Configs.SuffixPreloadLength;
var end = content.IndexOf('\'', index);
if (end > index)
{
content = content.Substring(index, end - index);
}
}
return content;
});
}
else
{
result = null;
}
if (result == null)
{
App.DebugPrint($"error when load preload data: force({force})");
}
return result;
}
public static IllustPageData LoadIllustPageData(string id, out string error, bool force = false)
{
var file = Path.Combine(CacheFolder, pagesFolder, $"{id}.json");
var result = HttpUtility.LoadObject<IllustPageData>(
file,
string.Format(Configs.UrlIllustPage, id),
string.Format(Configs.RefererIllust, id),
out _,
force: force);
if (result == null || result.error)
{
error = result?.message ?? "result is null";
App.DebugPrint($"error when load page data: {error}, force({force})");
return null;
}
error = null;
return result;
}
public static IllustUgoiraData LoadIllustUgoiraData(string id, bool force = false)
{
var file = Path.Combine(PersonalFolder, ugoiraFolder, $"{id}.json");
var result = HttpUtility.LoadObject<IllustUgoiraData>(
file,
string.Format(Configs.UrlIllustUgoira, id),
string.Format(Configs.RefererIllust, id),
out _,
force: force);
if (result == null || result.error)
{
App.DebugPrint($"error when load ugoira data: {result?.message}, force({force})");
}
return result;
}
public static IllustUserListData LoadIllustUserInitData(string userId)
{
var list = HttpUtility.LoadObject<IllustUserListData>(
null,
string.Format(Configs.UrlIllustUserAll, userId),
string.Format(Configs.RefererIllustUser, userId),
out _);
if (list == null || list.error)
{
App.DebugPrint($"error when load user data: {list?.message}");
}
return list;
}
public static IllustUserData LoadIllustUserData(string userId, string[] ids, bool firstPage)
{
if (ids == null || ids.Length == 0)
{
return null;
}
var ps = string.Concat(ids.Select(i => $"ids%5B%5D={i}&"));
var result = HttpUtility.LoadObject<IllustUserData>(
null,
string.Format(Configs.UrlIllustUserArtworks, userId, ps, firstPage ? 1 : 0),
string.Format(Configs.RefererIllustUser, userId),
out _);
if (result == null || result.error)
{
App.DebugPrint($"error when load user illust data: {result?.message}");
}
return result;
}
//private static readonly Regex regexIllust = new Regex(
// @"book_id\[\]"" value=""([0-9]+)"".*data-src=""([^""]+)"".*data-id=""([0-9]+)"".*" +
// @"data-tags=""([^""]+)"".*data-user-id=""([0-9]+)"".*" +
// @"class=""title"" title=""([^""]+)"".*data-user_name=""([^""]+)"".*" +
// @"_bookmark-icon-inline""></i>([0-9]+)</a>",
// RegexOptions.Compiled);
public static IllustItem[] LoadOnlineFavorites()
{
var userId = Configs.UserId;
var list = new List<IllustItem>();
int offset = 0;
while (offset >= 0)
{
var result = HttpUtility.LoadObject<IllustFavoriteData>(
null,
string.Format(Configs.UrlFavoriteList, userId, offset, 48),
string.Format(Configs.RefererFavorites, userId),
out _);
if (result == null || result.error)
{
App.DebugPrint($"error when load favorites data: {result?.message}");
}
else
{
if (offset + 48 < result.body.total)
{
offset += 48;
}
else
{
offset = -1;
}
list.AddRange(result.body.works.Select(i => i.ConvertToItem()));
}
}
return list.Where(l => l != null).ToArray();
}
public static ImageSource LoadIllustImage(string url)
{
return LoadImage(url, PersonalFolder, imageFolder, true);
}
public static ImageSource LoadPreviewImage(string url, bool downloading, string id = null, bool force = false)
{
if (downloading && Configs.DownloadIllustThreads > 1)
{
return LoadImageAsync(url, id, PersonalFolder, previewFolder, force).Result;
}
return LoadImage(url, PersonalFolder, previewFolder, downloading, force);
}
public static ImageSource LoadThumbnailImage(string url, bool downloading, bool force = false)
{
return LoadImage(url, CacheFolder, thumbFolder, downloading, force);
}
public static ImageSource LoadUserProfileImage(string url, bool downloading, bool force = false)
{
return LoadImage(url, CacheFolder, userFolder, downloading, force);
}
public static bool CheckIllustImage(string url)
{
var file = Path.Combine(PersonalFolder, imageFolder, Path.GetFileName(url));
return File.Exists(file);
}
public static bool CheckUgoiraVideo(string url)
{
var file = Path.Combine(PersonalFolder, ugoiraFolder, Path.GetFileNameWithoutExtension(url) + ".mp4");
return File.Exists(file);
}
public static string GetPreviewImagePath(string url)
{
var file = Path.Combine(PersonalFolder, previewFolder, Path.GetFileName(url));
if (File.Exists(file))
{
return file;
}
return null;
}
private static 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))
{
try
{
image = ImageSource.FromFile(file);
}
catch (Exception ex)
{
App.DebugError("image.load", $"failed to load image from file: {file}, error: {ex.Message}");
image = null;
}
}
else
{
image = null;
}
if (downloading && image == null)
{
file = HttpUtility.DownloadImage(url, working, folder);
if (file != null)
{
return ImageSource.FromFile(file);
}
}
return image;
}
private static 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))
{
try
{
image = ImageSource.FromFile(file);
}
catch (Exception ex)
{
App.DebugError("image.load", $"failed to load image from file: {file}, error: {ex.Message}");
image = null;
}
}
else
{
image = null;
}
if (image == null)
{
file = HttpUtility.DownloadImageAsync(url, id, working, folder).Result;
if (file != null)
{
image = ImageSource.FromFile(file);
}
}
return Task.FromResult(image);
}
}
public class IllustFavorite
{
public DateTime LastFavoriteUtc { get; set; }
public FavoriteList Illusts { get; set; }
}
public class FavoriteList : List<IllustItem>
{
public bool Changed { get; private set; }
public FavoriteList() : base() { }
public FavoriteList(IEnumerable<IllustItem> collection) : base(collection) { }
public new void Insert(int index, IllustItem item)
{
base.Insert(index, item);
Changed = true;
}
public new void InsertRange(int index, IEnumerable<IllustItem> collection)
{
base.InsertRange(index, collection);
Changed = true;
}
public new void RemoveAt(int index)
{
base.RemoveAt(index);
Changed = true;
}
public FavoriteList Reload()
{
Changed = false;
return this;
}
}
public enum SyncType
{
None = 0,
Prompt,
AutoSync
}
public static class Configs
{
public const string ProfileNameKey = "name";
public const string ProfileIdKey = "pixiv_id";
public const string ProfileImageKey = "profile_img";
public const string CookieKey = "cookies";
public const string UserIdKey = "user_id";
public const string DownloadIllustThreadsKey = "download_illust_threads";
public const string IsOnR18Key = "is_on_r18";
public const string SyncFavTypeKey = "sync_fav_type";
public const string IsProxiedKey = "is_proxied";
public const string HostKey = "host";
public const string PortKey = "port";
public const string QueryModeKey = "query_mode";
public const string QueryTypeKey = "query_type";
public const string QueryDateKey = "query_date";
public const string FavoriteTypeKey = "favorite_type";
public const int MaxPageThreads = 3;
public const int MaxThreads = 8;
public const string Referer = "https://www.pixiv.net/";
public const string RefererIllust = "https://www.pixiv.net/artworks/{0}";
public const string RefererIllustRanking = "https://www.pixiv.net/ranking.php?{0}";
public const string RefererIllustUser = "https://www.pixiv.net/users/{0}/illustrations";
public const string RefererFavorites = "https://www.pixiv.net/users/{0}/bookmarks/artworks";
public static int DownloadIllustThreads;
public static bool IsOnR18;
public static SyncType SyncFavType;
public static WebProxy Proxy;
public static string Prefix => Proxy == null ?
"https://www.pixiv.net/" : // https://hk.tsanie.org/reverse/
"https://www.pixiv.net/";
public static string UserId { get; private set; }
public static string Cookie { get; private set; }
public static string CsrfToken;
public static void SetUserId(string userId, bool save = false)
{
UserId = userId;
if (!save)
{
return;
}
if (userId == null)
{
Preferences.Remove(UserIdKey);
}
else
{
Preferences.Set(UserIdKey, userId);
}
}
public static void SetCookie(string cookie, bool save = false)
{
Cookie = cookie;
if (!save)
{
return;
}
if (cookie == null)
{
Preferences.Remove(CookieKey);
}
else
{
Preferences.Set(CookieKey, cookie);
}
}
#if __IOS__
public static Task<bool> RequestCookieContainer(WebKit.WKHttpCookieStore cookieStore)
{
var task = new TaskCompletionSource<bool>();
cookieStore.GetAllCookies(cookies =>
{
var list = new List<string>();
foreach (var c in cookies)
{
#if DEBUG
App.DebugPrint($"domain: {c.Domain}, path: {c.Path}, {c.Name}={c.Value}, http only: {c.IsHttpOnly}, session only: {c.IsSessionOnly}");
#endif
var domain = c.Domain;
if (domain == null)
{
continue;
}
if (domain != "www.pixiv.net" && domain != ".pixiv.net")
{
continue;
}
list.Add($"{c.Name}={c.Value}");
}
var cookie = string.Join("; ", list);
Cookie = cookie;
Preferences.Set(CookieKey, cookie);
task.SetResult(true);
});
return task.Task;
}
#endif
public const string SuffixGlobal = " id=\"meta-global-data\" content='";
public const int SuffixGlobalLength = 32;
public const string SuffixPreload = " id=\"meta-preload-data\" content='";
public const int SuffixPreloadLength = 33; // SuffixPreload.Length
public static string UrlIllustList => Prefix + "ajax/top/illust?mode=all&lang=zh";
public static string UrlIllust => Prefix + "artworks/{0}";
public static string UrlIllustRanking => Prefix + "ranking.php?{0}";
public static string UrlIllustUserAll => Prefix + "ajax/user/{0}/profile/all?lang=zh";
public static string UrlIllustUserArtworks => Prefix + "ajax/user/{0}/profile/illusts?{1}work_category=illust&is_first_page={2}&lang=zh";
public static string UrlIllustPage => Prefix + "ajax/illust/{0}/pages?lang=zh";
public static string UrlIllustUgoira => Prefix + "ajax/illust/{0}/ugoira_meta?lang=zh";
public static string UrlIllustRecommendsInit => Prefix + "ajax/illust/{0}/recommend/init?limit=18&lang=zh";
public static string UrlIllustRecommendsList => Prefix + "ajax/illust/recommend/illusts?{0}lang=zh";
public static string UrlFavoriteList => Prefix + "ajax/user/{0}/illusts/bookmarks?tag=&offset={1}&limit={2}&rest=show&lang=zh";
public static string BookmarkAdd => Prefix + "ajax/illusts/bookmarks/add";
public static string BookmarkRpc => Prefix + "rpc/index.php";
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 AcceptImage = "image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5";
public const string AcceptPureImage = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8";
public const string AcceptJson = "application/json";
public const string AcceptUrlEncoded = "application/x-www-form-urlencoded";
//public const string AcceptEncoding = "gzip, deflate";
public const string AcceptLanguage = "zh-cn";
private const string URL_PREVIEW = "https://i.pximg.net/c/360x360_70";
public static string GetThumbnailUrl(string url)
{
if (url == null)
{
return null;
}
url = url.ToLower().Replace("/custom-thumb/", "/img-master/");
var index = url.LastIndexOf("_square1200.jpg");
if (index < 0)
{
index = url.LastIndexOf("_custom1200.jpg");
}
if (index > 0)
{
url = url.Substring(0, index) + "_master1200.jpg";
}
var start = url.IndexOf("/img-master/");
if (start > 0)
{
url = URL_PREVIEW + url.Substring(start);
}
return url;
}
}
public static class Routes
{
public const string Illust = "illust";
public const string Detail = "detail";
public const string Follow = "follow";
public const string Recommends = "recommends";
public const string ByUser = "byuser";
public const string Ranking = "ranking";
public const string Favorites = "favorites";
public const string Option = "option";
}
}

605
Gallery/Utils/Ugoira.cs Executable file
View File

@ -0,0 +1,605 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Gallery.Illust;
using Xamarin.Forms;
using System.Linq;
#if __IOS__
using AVFoundation;
using CoreGraphics;
using CoreMedia;
using CoreVideo;
using Foundation;
using UIKit;
#endif
namespace Gallery.Utils
{
public class Ugoira : IDisposable
{
private const int DELAY = 200;
private const int BUFFER_SIZE = 300000;
private const int BUFFER_TABLE = 30000;
private readonly object sync = new object();
private readonly IllustUgoiraBody ugoira;
private readonly IllustDetailItem detailItem;
private readonly ImageSource[] frames;
private Timer timer;
private int index = 0;
private ParallelTask downloadTask;
public bool IsPlaying { get; private set; }
public readonly int FrameCount;
public event EventHandler<UgoiraEventArgs> FrameChanged;
public Ugoira(IllustUgoiraData illust, IllustDetailItem item)
{
ugoira = illust.body;
detailItem = item;
frames = new ImageSource[ugoira.frames.Length];
FrameCount = frames.Length;
Task.Run(LoadFrames);
}
public void Dispose()
{
if (IsPlaying)
{
TogglePlay(false);
}
if (downloadTask != null)
{
downloadTask.Dispose();
downloadTask = null;
}
ClearTimer();
}
private void ClearTimer()
{
lock (sync)
{
if (timer != null)
{
timer.Dispose();
timer = null;
}
}
}
public void TogglePlay(bool flag)
{
if (IsPlaying == flag)
{
return;
}
ClearTimer();
if (flag)
{
timer = new Timer(OnTimerCallback, null, 0, Timeout.Infinite);
IsPlaying = true;
}
else
{
IsPlaying = false;
}
}
public void ToggleFrame(int frame)
{
if (IsPlaying)
{
// TODO: doesn't support change current frame when playing
return;
}
if (frame < 0 || frame >= frames.Length)
{
return;
}
var image = frames[frame];
if (image != null)
{
index = frame;
FrameChanged?.Invoke(this, new UgoiraEventArgs
{
DetailItem = detailItem,
Image = image,
FrameIndex = frame
});
}
}
private void OnTimerCallback(object state)
{
lock (sync)
{
if (!IsPlaying)
{
return;
}
}
ImageSource frame;
var i = index;
var info = ugoira.frames[i];
while ((frame = frames[i]) == null)
{
lock (sync)
{
if (timer == null)
{
return;
}
}
// not downloaded yet, waiting...
Thread.Sleep(DELAY);
}
FrameChanged?.Invoke(this, new UgoiraEventArgs
{
DetailItem = detailItem,
Image = frame,
FrameIndex = i
});
i++;
if (i >= frames.Length)
{
i = 0;
}
index = i;
if (timer != null && IsPlaying)
{
timer.Change(info.delay, Timeout.Infinite);
}
}
private void LoadFrames()
{
var zip = Path.GetFileName(ugoira.originalSrc);
bool download = false;
var uframes = ugoira.frames;
for (var i = 0; i < uframes.Length; i++)
{
var frame = uframes[i];
var image = Stores.LoadUgoiraImage(zip, frame.file);
if (image != null)
{
frame.FilePath = image;
frames[i] = image;
}
else
{
frame.Incompleted = true;
download = true;
}
}
if (download)
{
// need download
var url = ugoira.originalSrc;
var id = detailItem.Id;
var (size, lastModified, client) = HttpUtility.GetUgoiraHeader(url, id);
#if LOG
App.DebugPrint($"starting download ugoira: {size} bytes, last modified: {lastModified}");
#endif
var data = new byte[size];
Segment[] segs;
var length = (int)Math.Ceiling((double)(size - BUFFER_TABLE) / BUFFER_SIZE) + 1;
segs = new Segment[length];
var tableOffset = size - BUFFER_TABLE;
segs[length - 1] = new Segment(length - 1, tableOffset, size - 1);
segs[length - 2] = new Segment(length - 2, (length - 2) * BUFFER_SIZE, tableOffset - 1);
for (var i = 0; i < length - 2; i++)
{
long from = i * BUFFER_SIZE;
segs[i] = new Segment(i, from, from + BUFFER_SIZE - 1);
}
// table
var segTable = segs[length - 1];
using (var ms = new MemoryStream(data, (int)segTable.From, BUFFER_TABLE))
{
HttpUtility.DownloadUgoiraImage(client, url, id, lastModified, segTable.From, segTable.To, ms);
if (timer == null)
{
return;
}
}
segTable.Done = true;
for (var i = tableOffset; i < size - 4; i++)
{
if (data[i + 0] == 0x50 && data[i + 1] == 0x4b &&
data[i + 2] == 0x01 && data[i + 3] == 0x02)
{
tableOffset = i;
break;
}
}
if (tableOffset > size - 31)
{
App.DebugError("find.table", $"failed to find table offset, id: {id}, url: {url}");
return;
}
for (var n = 0; n < uframes.Length; n++)
{
var frame = uframes[n];
var i = (int)tableOffset;
i += 10; // signature(4) & version(2) & version_need(2) & flags(2)
if (data[i] != 0 || data[i + 1] != 0)
{
App.DebugError("extract.image", $"doesn't support compressed data: {BitConverter.ToInt16(data, i):x4}");
return;
}
i += 10; // compression(2) & mod-time(2) & mod-date(2) & crc-32(4)
int entrySize = BitConverter.ToInt32(data, i);
i += 4; // size(4)
int rawSize = BitConverter.ToInt32(data, i);
if (entrySize != rawSize)
{
App.DebugError("find.table", $"data seems to be compressed: {entrySize:x8} ({rawSize:x8}) bytes");
return;
}
i += 4; // rawSize(4)
int filenameLength = BitConverter.ToInt16(data, i);
i += 2; // filename length(2)
int extraLength = BitConverter.ToInt16(data, i);
i += 2; // extra length(2)
int commentLength = BitConverter.ToInt16(data, i);
i += 10; // comment length(2) & disk start(2) & internal attr(2) & external attr(4)
int entryOffset = BitConverter.ToInt32(data, i);
i += 4; // offset(4)
var filename = Encoding.UTF8.GetString(data, i, filenameLength);
tableOffset = i + filenameLength + extraLength + commentLength; // filename & extra & comment
if (frame.file != filename)
{
App.DebugError("find.table", $"error when fill entry information, read name: {filename}, frame name: {frame.file}");
return;
}
frame.Offset = entryOffset;
frame.Length = entrySize;
}
// only download needed
var inSegs = new List<Segment>();
for (var i = 0; i < uframes.Length; i++)
{
var frame = uframes[i];
if (frame.Incompleted)
{
var (first, last) = QueryRange(segs, frame);
frame.First = first;
frame.Last = last;
for (var n = first; n <= last; n++)
{
var seg = segs[n];
if (!seg.Done && !inSegs.Contains(seg))
{
inSegs.Add(seg);
}
}
}
}
if (downloadTask != null)
{
downloadTask.Dispose();
downloadTask = null;
}
downloadTask = ParallelTask.Start("ugoira.download", 0, inSegs.Count, 3, i =>
{
var seg = inSegs[i];
#if DEBUG
App.DebugPrint($"start to download segment #{seg.Index}, from {seg.From} to {seg.To} / {size}");
#endif
using (var ms = new MemoryStream(data, (int)seg.From, seg.Count))
{
HttpUtility.DownloadUgoiraImage(client, url, id, lastModified, seg.From, seg.To, ms);
}
seg.Done = true;
return timer != null;
});
for (var i = 0; i < uframes.Length; i++)
{
var frame = uframes[i];
if (frame.Incompleted)
{
var first = frame.First;
var last = frame.Last;
while (segs.AnyFor(first, last, s => !s.Done))
{
lock (sync)
{
if (timer == null)
{
return;
}
}
Thread.Sleep(DELAY);
}
var file = ExtractImage(zip, data, frame.Offset);
if (file != null)
{
frame.FilePath = file;
frames[i] = ImageSource.FromFile(file);
frame.Incompleted = false;
}
}
}
#if LOG
App.DebugPrint("load frames over");
#endif
}
}
private (int first, int last) QueryRange(Segment[] segs, IllustUgoiraBody.Frame frame)
{
var start = frame.Offset;
var end = start + frame.Length;
var first = segs.LastIndexOf(s => start >= s.From);
var last = segs.IndexOf(s => end <= s.To);
return (first, last);
}
private string ExtractImage(string zip, byte[] data, int index)
{
var i = index;
if (i + 30 > data.Length - 1) // last
{
return null;
}
if (data[i] != 0x50 || data[i + 1] != 0x4b ||
data[i + 2] != 0x03 || data[i + 3] != 0x04)
{
App.DebugPrint($"extract complete, header: {BitConverter.ToInt32(data, i):x8}");
return null;
}
i += 8; // signature(4) & version(2) & flags(2)
if (data[i] != 0 || data[i + 1] != 0)
{
App.DebugError("extract.image", $"doesn't support compressed data: {BitConverter.ToInt16(data, i):x4}");
return null;
}
i += 10; // compression(2) & mod-time(2) & mod-date(2) & crc-32(4)
int size = BitConverter.ToInt32(data, i);
i += 4; // size(4)
int rawSize = BitConverter.ToInt32(data, i);
if (size != rawSize)
{
App.DebugError("extract.image", $"data seems to be compressed: {size:x8} ({rawSize:x8}) bytes");
return null;
}
i += 4; // rawSize(4)
int filenameLength = BitConverter.ToInt16(data, i);
i += 2; // filename length(2)
int extraLength = BitConverter.ToInt16(data, i);
i += 2; // extra length(2)
if (i + filenameLength + extraLength + size > data.Length - 1) // last
{
App.DebugPrint($"download is not completed, index: {index}, size: {size}, length: {data.Length}"); // , last: {last}
return null;
}
var filename = Encoding.UTF8.GetString(data, i, filenameLength);
i += filenameLength + extraLength; // filename & extra
// content
var content = new byte[size];
Array.Copy(data, i, content, 0, size);
//i += size;
var file = Stores.SaveUgoiraImage(zip, filename, content);
return file;
}
class Segment
{
public int Index;
public long From;
public long To;
public bool Done;
public int Count => (int)(To - From + 1);
public Segment(int index, long from, long to)
{
Index = index;
From = from;
To = to;
Done = false;
}
}
#if __IOS__
public async Task<string> ExportVideo()
{
if (ugoira == null || ugoira.frames.Any(f => f.Incompleted))
{
return null;
}
var file = Stores.GetUgoiraPath(ugoira.originalSrc, ".mp4");
if (File.Exists(file))
{
File.Delete(file);
}
var fileURL = NSUrl.FromFilename(file);
var images = ugoira.frames.Select(f => UIImage.FromFile(f.FilePath)).ToArray();
var videoSettings = new NSMutableDictionary
{
{ AVVideo.CodecKey, AVVideo.CodecH264 },
{ AVVideo.WidthKey, NSNumber.FromNFloat(images[0].Size.Width) },
{ AVVideo.HeightKey, NSNumber.FromNFloat(images[0].Size.Height) }
};
var videoWriter = new AVAssetWriter(fileURL, AVFileType.Mpeg4, out var err);
if (err != null)
{
App.DebugError("export.video", $"failed to create an AVAssetWriter: {err}");
return null;
}
var writerInput = new AVAssetWriterInput(AVMediaType.Video, new AVVideoSettingsCompressed(videoSettings));
var sourcePixelBufferAttributes = new NSMutableDictionary
{
{ CVPixelBuffer.PixelFormatTypeKey, NSNumber.FromInt32((int)CVPixelFormatType.CV32ARGB) }
};
var pixelBufferAdaptor = new AVAssetWriterInputPixelBufferAdaptor(writerInput, sourcePixelBufferAttributes);
videoWriter.AddInput(writerInput);
if (videoWriter.StartWriting())
{
videoWriter.StartSessionAtSourceTime(CMTime.Zero);
var lastTime = CMTime.Zero;
#if DEBUG
bool log = false;
#endif
for (int i = 0; i < images.Length; i++)
{
while (!writerInput.ReadyForMoreMediaData)
{
lock (sync)
{
if (timer == null)
{
return null;
}
}
Thread.Sleep(50);
}
// get pixel buffer and fill it with the image
using (CVPixelBuffer pixelBufferImage = pixelBufferAdaptor.PixelBufferPool.CreatePixelBuffer())
{
pixelBufferImage.Lock(CVPixelBufferLock.None);
try
{
IntPtr pxdata = pixelBufferImage.BaseAddress;
if (pxdata != IntPtr.Zero)
{
using (var rgbColorSpace = CGColorSpace.CreateDeviceRGB())
{
var cgImage = images[i].CGImage;
var width = cgImage.Width;
var height = cgImage.Height;
var bitsPerComponent = cgImage.BitsPerComponent;
var bytesPerRow = cgImage.BytesPerRow;
// padding to 64
var bytes = bytesPerRow >> 6 << 6;
if (bytes < bytesPerRow)
{
bytes += 64;
}
#if DEBUG
if (!log)
{
log = true;
App.DebugPrint($"animation, width: {width}, height: {height}, type: {cgImage.UTType}\n" +
$"bitmapInfo: {cgImage.BitmapInfo}\n" +
$"bpc: {bitsPerComponent}\n" +
$"bpp: {cgImage.BitsPerPixel}\n" +
$"calculated: {bytesPerRow} => {bytes}");
}
#endif
using (CGBitmapContext bitmapContext = new CGBitmapContext(
pxdata, width, height,
bitsPerComponent,
bytes,
rgbColorSpace,
CGImageAlphaInfo.NoneSkipFirst))
{
if (bitmapContext != null)
{
bitmapContext.DrawImage(new CGRect(0, 0, width, height), cgImage);
}
}
}
}
}
finally
{
pixelBufferImage.Unlock(CVPixelBufferLock.None);
}
// and finally append buffer to adapter
if (pixelBufferAdaptor.AssetWriterInput.ReadyForMoreMediaData && pixelBufferImage != null)
{
pixelBufferAdaptor.AppendPixelBufferWithPresentationTime(pixelBufferImage, lastTime);
}
}
var frameTime = new CMTime(ugoira.frames[i].delay, 1000);
lastTime = CMTime.Add(lastTime, frameTime);
}
writerInput.MarkAsFinished();
await videoWriter.FinishWritingAsync();
return file;
}
return null;
}
public Task<string> ExportGif()
{
if (ugoira == null || ugoira.frames.Any(f => f.Incompleted))
{
return null;
}
var file = Stores.GetUgoiraPath(ugoira.originalSrc, ".gif");
if (File.Exists(file))
{
File.Delete(file);
}
var fileURL = NSUrl.FromFilename(file);
var dictFile = new NSMutableDictionary();
var gifDictionaryFile = new NSMutableDictionary
{
{ ImageIO.CGImageProperties.GIFLoopCount, NSNumber.FromFloat(0f) }
};
dictFile.Add(ImageIO.CGImageProperties.GIFDictionary, gifDictionaryFile);
var dictFrame = new NSMutableDictionary();
var gifDictionaryFrame = new NSMutableDictionary
{
{ ImageIO.CGImageProperties.GIFDelayTime, NSNumber.FromFloat(ugoira.frames[0].delay / 1000f) }
};
dictFrame.Add(ImageIO.CGImageProperties.GIFDictionary, gifDictionaryFrame);
var task = new TaskCompletionSource<string>();
Xamarin.Essentials.MainThread.BeginInvokeOnMainThread(() =>
{
var images = ugoira.frames.Select(f => UIImage.FromFile(f.FilePath)).ToArray();
var imageDestination = ImageIO.CGImageDestination.Create(fileURL, MobileCoreServices.UTType.GIF, images.Length);
imageDestination.SetProperties(dictFile);
for (int i = 0; i < images.Length; i++)
{
imageDestination.AddImage(images[i].CGImage, dictFrame);
}
imageDestination.Close();
task.SetResult(file);
});
return task.Task;
}
#endif
}
public class UgoiraEventArgs : EventArgs
{
public IllustDetailItem DetailItem { get; set; }
public ImageSource Image { get; set; }
public int FrameIndex { get; set; }
}
}