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; } + } +} +