diff --git a/Pixiview/Illust/ViewIllustPage.xaml.cs b/Pixiview/Illust/ViewIllustPage.xaml.cs
index ae86fc1..8ec7f00 100644
--- a/Pixiview/Illust/ViewIllustPage.xaml.cs
+++ b/Pixiview/Illust/ViewIllustPage.xaml.cs
@@ -19,6 +19,8 @@ namespace Pixiview.Illust
public static readonly BindableProperty IllustsProperty = BindableProperty.Create(
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(
nameof(PagePositionText), typeof(string), typeof(ViewIllustPage));
public static readonly BindableProperty CurrentPageProperty = BindableProperty.Create(
@@ -64,6 +66,11 @@ namespace Pixiview.Illust
get => (IllustDetailItem[])GetValue(IllustsProperty);
set => SetValue(IllustsProperty, value);
}
+ public bool IsPageVisible
+ {
+ get => (bool)GetValue(IsPageVisibleProperty);
+ set => SetValue(IsPageVisibleProperty, value);
+ }
public string PagePositionText
{
get => (string)GetValue(PagePositionTextProperty);
@@ -101,7 +108,6 @@ namespace Pixiview.Illust
}
public IllustItem IllustItem { get; private set; }
- public bool IsPageVisible { get; private set; }
private readonly ParallelOptions parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Configs.MaxThreads };
private readonly bool saveFavorites;
@@ -124,9 +130,8 @@ namespace Pixiview.Illust
? fontIconLove
: fontIconNotLove;
- var pageVisible = illust != null && illust.PageCount > 1;
- IsPageVisible = pageVisible;
- Resources.Add("carouselView", GetCarouseTemplate(pageVisible));
+ IsPageVisible = illust != null && illust.PageCount > 1;
+ Resources.Add("carouselView", GetCarouseTemplate());
InitializeComponent();
@@ -156,6 +161,7 @@ namespace Pixiview.Illust
if (ugoira != null)
{
IllustItem.IsPlaying = false;
+ ugoira.FrameChanged -= OnUgoiraFrameChanged;
ugoira.TogglePlay(false);
ugoira.Dispose();
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(() =>
{
// image
@@ -179,8 +182,7 @@ namespace Pixiview.Illust
{
HorizontalOptions = LayoutOptions.Fill,
VerticalOptions = LayoutOptions.Fill,
- Aspect = Aspect.AspectFit,
- GestureRecognizers = { tap }
+ Aspect = Aspect.AspectFit
}
.Binding(Image.SourceProperty, nameof(IllustDetailItem.Image));
@@ -214,51 +216,12 @@ namespace Pixiview.Illust
.Binding(IsVisibleProperty, nameof(IllustDetailItem.Downloading))
.DynamicResource(ActivityIndicator.ColorProperty, ThemeBase.TextColor);
- if (multiPages)
- {
- var tapPrevious = new TapGestureRecognizer();
- tapPrevious.Tapped += TapPrevious_Tapped;
- var tapNext = new TapGestureRecognizer();
- 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
- }
- };
- }
+ var tap = new TapGestureRecognizer();
+ tap.Tapped += Image_Tapped;
+ var tapPrevious = new TapGestureRecognizer();
+ tapPrevious.Tapped += TapPrevious_Tapped;
+ var tapNext = new TapGestureRecognizer();
+ tapNext.Tapped += TapNext_Tapped;
return new Grid
{
@@ -267,6 +230,23 @@ namespace Pixiview.Illust
// 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,
@@ -346,7 +326,12 @@ namespace Pixiview.Illust
if (preload != null && preload.illust.TryGetValue(illustItem.Id, out var illust))
{
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))
{
illustItem.ProfileUrl = user.image;
@@ -463,10 +448,9 @@ namespace Pixiview.Illust
ugoira.TogglePlay(playing);
illustItem.IsPlaying = playing;
}
- else if (((Image)sender).BindingContext is IllustDetailItem item)
+ else if (((VisualElement)sender).BindingContext is IllustDetailItem item)
{
- if (illustItem.IsPlaying ||
- illustItem.IllustType != IllustType.Anime)
+ if (illustItem.IsPlaying || !illustItem.IsAnimeVisible)
{
return;
}
diff --git a/Pixiview/Pixiview.projitems b/Pixiview/Pixiview.projitems
index 2d237e4..8925fe3 100644
--- a/Pixiview/Pixiview.projitems
+++ b/Pixiview/Pixiview.projitems
@@ -100,6 +100,7 @@
RelatedIllustsPage.xaml
+
diff --git a/Pixiview/Utils/Extensions.cs b/Pixiview/Utils/Extensions.cs
index dd65ad6..9d9b017 100644
--- a/Pixiview/Utils/Extensions.cs
+++ b/Pixiview/Utils/Extensions.cs
@@ -1,4 +1,7 @@
-using Xamarin.Forms;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Xamarin.Forms;
namespace Pixiview.Utils
{
@@ -40,6 +43,123 @@ namespace Pixiview.Utils
Grid.SetColumnSpan(view, columnSpan);
return view;
}
+
+ public static int IndexOf(this T[] array, Predicate predicate)
+ {
+ for (var i = 0; i < array.Length; i++)
+ {
+ if (predicate(array[i]))
+ {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public static int LastIndexOf(this T[] array, Predicate predicate)
+ {
+ for (var i = array.Length - 1; i >= 0; i--)
+ {
+ if (predicate(array[i]))
+ {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public static bool All(this T[] array, Predicate predicate)
+ {
+ for (var i = 0; i < array.Length; i++)
+ {
+ if (!predicate(array[i]))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static bool AnyFor(this T[] array, int from, int to, Predicate 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 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 action;
+
+ private ParallelTask(int from, int to, int maxCount, Predicate 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
diff --git a/Pixiview/Utils/HttpUtility.cs b/Pixiview/Utils/HttpUtility.cs
index 96c3c72..7d3e0d4 100644
--- a/Pixiview/Utils/HttpUtility.cs
+++ b/Pixiview/Utils/HttpUtility.cs
@@ -5,10 +5,7 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
-using System.Threading.Tasks;
using Newtonsoft.Json;
-using Pixiview.Illust;
-using Xamarin.Forms;
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 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; }
- }
}
diff --git a/Pixiview/Utils/IllustData.cs b/Pixiview/Utils/IllustData.cs
index c3eb02c..b579e56 100644
--- a/Pixiview/Utils/IllustData.cs
+++ b/Pixiview/Utils/IllustData.cs
@@ -265,6 +265,12 @@ namespace Pixiview.Utils
{
public string file;
public int delay;
+
+ public bool Incompleted;
+ public int First;
+ public int Last;
+ public int Offset;
+ public int Length;
}
}
}
diff --git a/Pixiview/Utils/Ugoira.cs b/Pixiview/Utils/Ugoira.cs
new file mode 100644
index 0000000..a4d1900
--- /dev/null
+++ b/Pixiview/Utils/Ugoira.cs
@@ -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 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();
+ 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; }
+ }
+}
+