feature: parellel downloading animation
This commit is contained in:
parent
2da73d5f51
commit
00e8fe0f04
@ -19,6 +19,8 @@ namespace Pixiview.Illust
|
|||||||
|
|
||||||
public static readonly BindableProperty IllustsProperty = BindableProperty.Create(
|
public static readonly BindableProperty IllustsProperty = BindableProperty.Create(
|
||||||
nameof(Illusts), typeof(IllustDetailItem[]), typeof(ViewIllustPage));
|
nameof(Illusts), typeof(IllustDetailItem[]), typeof(ViewIllustPage));
|
||||||
|
public static readonly BindableProperty IsPageVisibleProperty = BindableProperty.Create(
|
||||||
|
nameof(IsPageVisible), typeof(bool), typeof(ViewIllustPage));
|
||||||
public static readonly BindableProperty PagePositionTextProperty = BindableProperty.Create(
|
public static readonly BindableProperty PagePositionTextProperty = BindableProperty.Create(
|
||||||
nameof(PagePositionText), typeof(string), typeof(ViewIllustPage));
|
nameof(PagePositionText), typeof(string), typeof(ViewIllustPage));
|
||||||
public static readonly BindableProperty CurrentPageProperty = BindableProperty.Create(
|
public static readonly BindableProperty CurrentPageProperty = BindableProperty.Create(
|
||||||
@ -64,6 +66,11 @@ namespace Pixiview.Illust
|
|||||||
get => (IllustDetailItem[])GetValue(IllustsProperty);
|
get => (IllustDetailItem[])GetValue(IllustsProperty);
|
||||||
set => SetValue(IllustsProperty, value);
|
set => SetValue(IllustsProperty, value);
|
||||||
}
|
}
|
||||||
|
public bool IsPageVisible
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(IsPageVisibleProperty);
|
||||||
|
set => SetValue(IsPageVisibleProperty, value);
|
||||||
|
}
|
||||||
public string PagePositionText
|
public string PagePositionText
|
||||||
{
|
{
|
||||||
get => (string)GetValue(PagePositionTextProperty);
|
get => (string)GetValue(PagePositionTextProperty);
|
||||||
@ -101,7 +108,6 @@ namespace Pixiview.Illust
|
|||||||
}
|
}
|
||||||
|
|
||||||
public IllustItem IllustItem { get; private set; }
|
public IllustItem IllustItem { get; private set; }
|
||||||
public bool IsPageVisible { get; private set; }
|
|
||||||
|
|
||||||
private readonly ParallelOptions parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Configs.MaxThreads };
|
private readonly ParallelOptions parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Configs.MaxThreads };
|
||||||
private readonly bool saveFavorites;
|
private readonly bool saveFavorites;
|
||||||
@ -124,9 +130,8 @@ namespace Pixiview.Illust
|
|||||||
? fontIconLove
|
? fontIconLove
|
||||||
: fontIconNotLove;
|
: fontIconNotLove;
|
||||||
|
|
||||||
var pageVisible = illust != null && illust.PageCount > 1;
|
IsPageVisible = illust != null && illust.PageCount > 1;
|
||||||
IsPageVisible = pageVisible;
|
Resources.Add("carouselView", GetCarouseTemplate());
|
||||||
Resources.Add("carouselView", GetCarouseTemplate(pageVisible));
|
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
@ -156,6 +161,7 @@ namespace Pixiview.Illust
|
|||||||
if (ugoira != null)
|
if (ugoira != null)
|
||||||
{
|
{
|
||||||
IllustItem.IsPlaying = false;
|
IllustItem.IsPlaying = false;
|
||||||
|
ugoira.FrameChanged -= OnUgoiraFrameChanged;
|
||||||
ugoira.TogglePlay(false);
|
ugoira.TogglePlay(false);
|
||||||
ugoira.Dispose();
|
ugoira.Dispose();
|
||||||
ugoira = null;
|
ugoira = null;
|
||||||
@ -167,11 +173,8 @@ namespace Pixiview.Illust
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DataTemplate GetCarouseTemplate(bool multiPages)
|
private DataTemplate GetCarouseTemplate()
|
||||||
{
|
{
|
||||||
var tap = new TapGestureRecognizer();
|
|
||||||
tap.Tapped += Image_Tapped;
|
|
||||||
|
|
||||||
return new DataTemplate(() =>
|
return new DataTemplate(() =>
|
||||||
{
|
{
|
||||||
// image
|
// image
|
||||||
@ -179,8 +182,7 @@ namespace Pixiview.Illust
|
|||||||
{
|
{
|
||||||
HorizontalOptions = LayoutOptions.Fill,
|
HorizontalOptions = LayoutOptions.Fill,
|
||||||
VerticalOptions = LayoutOptions.Fill,
|
VerticalOptions = LayoutOptions.Fill,
|
||||||
Aspect = Aspect.AspectFit,
|
Aspect = Aspect.AspectFit
|
||||||
GestureRecognizers = { tap }
|
|
||||||
}
|
}
|
||||||
.Binding(Image.SourceProperty, nameof(IllustDetailItem.Image));
|
.Binding(Image.SourceProperty, nameof(IllustDetailItem.Image));
|
||||||
|
|
||||||
@ -214,51 +216,12 @@ namespace Pixiview.Illust
|
|||||||
.Binding(IsVisibleProperty, nameof(IllustDetailItem.Downloading))
|
.Binding(IsVisibleProperty, nameof(IllustDetailItem.Downloading))
|
||||||
.DynamicResource(ActivityIndicator.ColorProperty, ThemeBase.TextColor);
|
.DynamicResource(ActivityIndicator.ColorProperty, ThemeBase.TextColor);
|
||||||
|
|
||||||
if (multiPages)
|
var tap = new TapGestureRecognizer();
|
||||||
{
|
tap.Tapped += Image_Tapped;
|
||||||
var tapPrevious = new TapGestureRecognizer();
|
var tapPrevious = new TapGestureRecognizer();
|
||||||
tapPrevious.Tapped += TapPrevious_Tapped;
|
tapPrevious.Tapped += TapPrevious_Tapped;
|
||||||
var tapNext = new TapGestureRecognizer();
|
var tapNext = new TapGestureRecognizer();
|
||||||
tapNext.Tapped += TapNext_Tapped;
|
tapNext.Tapped += TapNext_Tapped;
|
||||||
|
|
||||||
return new Grid
|
|
||||||
{
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
// image
|
|
||||||
image,
|
|
||||||
|
|
||||||
// tap holder
|
|
||||||
new Grid
|
|
||||||
{
|
|
||||||
RowDefinitions =
|
|
||||||
{
|
|
||||||
new RowDefinition(),
|
|
||||||
new RowDefinition(),
|
|
||||||
new RowDefinition()
|
|
||||||
},
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new Label
|
|
||||||
{
|
|
||||||
GestureRecognizers = { tapPrevious }
|
|
||||||
},
|
|
||||||
new Label
|
|
||||||
{
|
|
||||||
GestureRecognizers = { tapNext }
|
|
||||||
}
|
|
||||||
.GridRow(2)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// downloading
|
|
||||||
downloading,
|
|
||||||
|
|
||||||
// loading original
|
|
||||||
original
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Grid
|
return new Grid
|
||||||
{
|
{
|
||||||
@ -267,6 +230,23 @@ namespace Pixiview.Illust
|
|||||||
// image
|
// image
|
||||||
image,
|
image,
|
||||||
|
|
||||||
|
// tap holder
|
||||||
|
new Grid
|
||||||
|
{
|
||||||
|
RowDefinitions =
|
||||||
|
{
|
||||||
|
new RowDefinition { Height = new GridLength(.3, GridUnitType.Star) },
|
||||||
|
new RowDefinition { Height = new GridLength(.4, GridUnitType.Star) },
|
||||||
|
new RowDefinition { Height = new GridLength(.3, GridUnitType.Star) }
|
||||||
|
},
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new Label { GestureRecognizers = { tapPrevious } },
|
||||||
|
new Label { GestureRecognizers = { tap } }.GridRow(1),
|
||||||
|
new Label { GestureRecognizers = { tapNext } }.GridRow(2)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// downloading
|
// downloading
|
||||||
downloading,
|
downloading,
|
||||||
|
|
||||||
@ -346,7 +326,12 @@ namespace Pixiview.Illust
|
|||||||
if (preload != null && preload.illust.TryGetValue(illustItem.Id, out var illust))
|
if (preload != null && preload.illust.TryGetValue(illustItem.Id, out var illust))
|
||||||
{
|
{
|
||||||
illust.CopyToItem(illustItem);
|
illust.CopyToItem(illustItem);
|
||||||
MainThread.BeginInvokeOnMainThread(() => Title = illustItem.Title);
|
MainThread.BeginInvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
Title = illustItem.Title;
|
||||||
|
IsPageVisible = illustItem.PageCount > 1;
|
||||||
|
IsAnimateSliderVisible = illustItem.IsAnimeVisible;
|
||||||
|
});
|
||||||
if (preload.user.TryGetValue(illust.userId, out var user))
|
if (preload.user.TryGetValue(illust.userId, out var user))
|
||||||
{
|
{
|
||||||
illustItem.ProfileUrl = user.image;
|
illustItem.ProfileUrl = user.image;
|
||||||
@ -463,10 +448,9 @@ namespace Pixiview.Illust
|
|||||||
ugoira.TogglePlay(playing);
|
ugoira.TogglePlay(playing);
|
||||||
illustItem.IsPlaying = playing;
|
illustItem.IsPlaying = playing;
|
||||||
}
|
}
|
||||||
else if (((Image)sender).BindingContext is IllustDetailItem item)
|
else if (((VisualElement)sender).BindingContext is IllustDetailItem item)
|
||||||
{
|
{
|
||||||
if (illustItem.IsPlaying ||
|
if (illustItem.IsPlaying || !illustItem.IsAnimeVisible)
|
||||||
illustItem.IllustType != IllustType.Anime)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -100,6 +100,7 @@
|
|||||||
<Compile Include="$(MSBuildThisFileDirectory)Illust\RelatedIllustsPage.xaml.cs">
|
<Compile Include="$(MSBuildThisFileDirectory)Illust\RelatedIllustsPage.xaml.cs">
|
||||||
<DependentUpon>RelatedIllustsPage.xaml</DependentUpon>
|
<DependentUpon>RelatedIllustsPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="$(MSBuildThisFileDirectory)Utils\Ugoira.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="$(MSBuildThisFileDirectory)Illust\" />
|
<Folder Include="$(MSBuildThisFileDirectory)Illust\" />
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
using Xamarin.Forms;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Pixiview.Utils
|
namespace Pixiview.Utils
|
||||||
{
|
{
|
||||||
@ -40,6 +43,123 @@ namespace Pixiview.Utils
|
|||||||
Grid.SetColumnSpan(view, columnSpan);
|
Grid.SetColumnSpan(view, columnSpan);
|
||||||
return view;
|
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
|
||||||
|
{
|
||||||
|
public static void Start(int from, int toExclusive, int maxCount, Predicate<int> action)
|
||||||
|
{
|
||||||
|
var task = new ParallelTask(from, toExclusive, maxCount, action);
|
||||||
|
Task.Run(task.Start);
|
||||||
|
}
|
||||||
|
|
||||||
|
private volatile int count;
|
||||||
|
private volatile bool disposed;
|
||||||
|
|
||||||
|
private readonly int max;
|
||||||
|
private readonly int from;
|
||||||
|
private readonly int to;
|
||||||
|
private readonly Predicate<int> action;
|
||||||
|
|
||||||
|
private ParallelTask(int from, int to, int maxCount, Predicate<int> action)
|
||||||
|
{
|
||||||
|
if (maxCount <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(maxCount));
|
||||||
|
}
|
||||||
|
max = maxCount;
|
||||||
|
if (from >= to)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(from));
|
||||||
|
}
|
||||||
|
this.from = from;
|
||||||
|
this.to = to;
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
for (int i = from; i < to; i++)
|
||||||
|
{
|
||||||
|
var index = i;
|
||||||
|
while (count >= max)
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
{
|
||||||
|
App.DebugPrint($"parallel task determinate, disposed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Thread.Sleep(100);
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!action(index))
|
||||||
|
{
|
||||||
|
disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.DebugError("parallel.start", $"failed to run action, index: {i}, error: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
count--;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Screen
|
public static class Screen
|
||||||
|
@ -5,10 +5,7 @@ using System.Net.Http;
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Pixiview.Illust;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Pixiview.Utils
|
namespace Pixiview.Utils
|
||||||
{
|
{
|
||||||
@ -292,268 +289,4 @@ namespace Pixiview.Utils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Ugoira : IDisposable
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
timer = new Timer(OnTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
|
|
||||||
|
|
||||||
Task.Run(LoadFrames);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
lock (sync)
|
|
||||||
{
|
|
||||||
if (IsPlaying)
|
|
||||||
{
|
|
||||||
TogglePlay(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (timer != null)
|
|
||||||
{
|
|
||||||
timer.Dispose();
|
|
||||||
timer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void TogglePlay(bool flag)
|
|
||||||
{
|
|
||||||
lock (sync)
|
|
||||||
{
|
|
||||||
if (timer == null || IsPlaying == flag)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (flag)
|
|
||||||
{
|
|
||||||
IsPlaying = true;
|
|
||||||
timer.Change(0, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
IsPlaying = false;
|
|
||||||
timer.Change(Timeout.Infinite, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
// not downloaded yet, waiting...
|
|
||||||
Thread.Sleep(100);
|
|
||||||
}
|
|
||||||
FrameChanged?.Invoke(this, new UgoiraEventArgs
|
|
||||||
{
|
|
||||||
DetailItem = detailItem,
|
|
||||||
Image = frame,
|
|
||||||
FrameIndex = i
|
|
||||||
});
|
|
||||||
i++;
|
|
||||||
if (i >= frames.Length)
|
|
||||||
{
|
|
||||||
i = 0;
|
|
||||||
}
|
|
||||||
index = i;
|
|
||||||
lock (sync)
|
|
||||||
{
|
|
||||||
if (timer != null && IsPlaying)
|
|
||||||
{
|
|
||||||
timer.Change(info.delay, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const int BUFFER_SIZE = 300000;
|
|
||||||
|
|
||||||
private void LoadFrames()
|
|
||||||
{
|
|
||||||
var zip = Path.GetFileName(ugoira.src);
|
|
||||||
bool download = false;
|
|
||||||
for (var i = 0; i < ugoira.frames.Length; i++)
|
|
||||||
{
|
|
||||||
var frame = ugoira.frames[i];
|
|
||||||
var image = Stores.LoadUgoiraImage(zip, frame.file);
|
|
||||||
if (image != null)
|
|
||||||
{
|
|
||||||
frames[i] = image;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
download = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (download)
|
|
||||||
{
|
|
||||||
// need download
|
|
||||||
var url = ugoira.src;
|
|
||||||
var id = detailItem.Id;
|
|
||||||
var (size, lastModified, client) = HttpUtility.GetUgoiraHeader(url, id);
|
|
||||||
App.DebugPrint($"starting download ugoira: {size} bytes, last modified: {lastModified}");
|
|
||||||
|
|
||||||
var data = new byte[size];
|
|
||||||
using (var ms = new MemoryStream(data))
|
|
||||||
{
|
|
||||||
var index = 0;
|
|
||||||
for (var i = 0; ; i += BUFFER_SIZE)
|
|
||||||
{
|
|
||||||
long to;
|
|
||||||
if (i + BUFFER_SIZE > size)
|
|
||||||
{
|
|
||||||
to = size - 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
to = i + BUFFER_SIZE - 1;
|
|
||||||
}
|
|
||||||
HttpUtility.DownloadUgoiraImage(client, url, id, lastModified, i, to, ms);
|
|
||||||
var last = ms.Position;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var pos = ExtractImage(zip, data, index, last);
|
|
||||||
if (pos > index)
|
|
||||||
{
|
|
||||||
index = pos;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i + BUFFER_SIZE > size)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int ExtractImage(string zip, byte[] data, int index, long last)
|
|
||||||
{
|
|
||||||
var i = index;
|
|
||||||
if (i + 30 > last)
|
|
||||||
{
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
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 index;
|
|
||||||
}
|
|
||||||
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 index;
|
|
||||||
}
|
|
||||||
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} ({rawSize}) bytes");
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
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 > last)
|
|
||||||
{
|
|
||||||
App.DebugPrint($"download is not completed, index: {index}, size: {size}, last: {last}");
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
if (file != null)
|
|
||||||
{
|
|
||||||
for (var n = 0; n < ugoira.frames.Length; n++)
|
|
||||||
{
|
|
||||||
if (ugoira.frames[n].file == filename)
|
|
||||||
{
|
|
||||||
App.DebugPrint($"load frame: {filename}");
|
|
||||||
frames[n] = ImageSource.FromFile(file);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UgoiraEventArgs : EventArgs
|
|
||||||
{
|
|
||||||
public IllustDetailItem DetailItem { get; set; }
|
|
||||||
public ImageSource Image { get; set; }
|
|
||||||
public int FrameIndex { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -265,6 +265,12 @@ namespace Pixiview.Utils
|
|||||||
{
|
{
|
||||||
public string file;
|
public string file;
|
||||||
public int delay;
|
public int delay;
|
||||||
|
|
||||||
|
public bool Incompleted;
|
||||||
|
public int First;
|
||||||
|
public int Last;
|
||||||
|
public int Offset;
|
||||||
|
public int Length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
395
Pixiview/Utils/Ugoira.cs
Normal file
395
Pixiview/Utils/Ugoira.cs
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Pixiview.Illust;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Pixiview.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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
timer = new Timer(OnTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
|
||||||
|
|
||||||
|
Task.Run(LoadFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (sync)
|
||||||
|
{
|
||||||
|
if (IsPlaying)
|
||||||
|
{
|
||||||
|
TogglePlay(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (timer != null)
|
||||||
|
{
|
||||||
|
timer.Dispose();
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TogglePlay(bool flag)
|
||||||
|
{
|
||||||
|
lock (sync)
|
||||||
|
{
|
||||||
|
if (timer == null || IsPlaying == flag)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (flag)
|
||||||
|
{
|
||||||
|
IsPlaying = true;
|
||||||
|
timer.Change(0, Timeout.Infinite);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
IsPlaying = false;
|
||||||
|
timer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
// 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;
|
||||||
|
lock (sync)
|
||||||
|
{
|
||||||
|
if (timer != null && IsPlaying)
|
||||||
|
{
|
||||||
|
timer.Change(info.delay, Timeout.Infinite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadFrames()
|
||||||
|
{
|
||||||
|
var zip = Path.GetFileName(ugoira.src);
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
frames[i] = image;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
frame.Incompleted = true;
|
||||||
|
download = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (download)
|
||||||
|
{
|
||||||
|
// need download
|
||||||
|
var url = ugoira.src;
|
||||||
|
var id = detailItem.Id;
|
||||||
|
var (size, lastModified, client) = HttpUtility.GetUgoiraHeader(url, id);
|
||||||
|
App.DebugPrint($"starting download ugoira: {size} bytes, last modified: {lastModified}");
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelTask.Start(0, inSegs.Count, 2, i =>
|
||||||
|
{
|
||||||
|
var seg = inSegs[i];
|
||||||
|
App.DebugPrint($"start to download segment #{i}, from {seg.From} to {seg.To} / {size}");
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
if (timer == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Thread.Sleep(DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
var file = ExtractImage(zip, data, frame.Offset);
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
frames[i] = ImageSource.FromFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UgoiraEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
public IllustDetailItem DetailItem { get; set; }
|
||||||
|
public ImageSource Image { get; set; }
|
||||||
|
public int FrameIndex { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user