rename from Pixiview to Gallery
This commit is contained in:
29
Gallery/Utils/Converters.cs
Executable file
29
Gallery/Utils/Converters.cs
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
248
Gallery/Utils/EnvironmentService.cs
Executable file
248
Gallery/Utils/EnvironmentService.cs
Executable 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
279
Gallery/Utils/Extensions.cs
Executable 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
92
Gallery/Utils/FileStore.cs
Executable 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
|
||||
}
|
||||
}
|
||||
}
|
464
Gallery/Utils/HttpUtility.cs
Normal file
464
Gallery/Utils/HttpUtility.cs
Normal 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
321
Gallery/Utils/IllustData.cs
Normal 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
141
Gallery/Utils/IllustLegacy.cs
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
26
Gallery/Utils/LongPressEffect.cs
Executable file
26
Gallery/Utils/LongPressEffect.cs
Executable 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
863
Gallery/Utils/Stores.cs
Normal 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
605
Gallery/Utils/Ugoira.cs
Executable 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; }
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user