feature: parellel downloading animation
This commit is contained in:
		| @@ -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; } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
		Reference in New Issue
	
	Block a user