* Pixiview/Utils/Stores.cs:

* Pixiview/Pixiview.projitems:
* Pixiview/Utils/IllustData.cs:
* Pixiview/Utils/HttpUtility.cs:
* Pixiview/Utils/IllustLegacy.cs:
* Pixiview/Illust/ViewIllustPage.xaml:
* Pixiview/Illust/ViewIllustPage.xaml.cs: feature: view animate

* Pixiview/Illust/FavoritesPage.xaml.cs: lazy load favorites page

* Pixiview/UI/CardView.cs:
* Pixiview/UI/AdaptedPage.cs:
* Pixiview/Utils/Converters.cs:
* Pixiview/UI/StyleDefinition.cs:
* Pixiview/UI/Theme/ThemeBase.cs:
* Pixiview/Illust/RankingPage.xaml:
* Pixiview/Illust/RankingPage.xaml.cs:
* Pixiview/Resources/Languages/zh-CN.xml:
* Pixiview/Illust/IllustCollectionPage.cs:
* Pixiview.iOS/Renderers/SegmentedControlRenderer.cs: feature: filter
  ranking

* Pixiview.iOS/Info.plist:
* Pixiview.iOS/Pixiview.iOS.csproj:
* Pixiview.iOS.OpenExtension/Info.plist:
* Pixiview.Android/Pixiview.Android.csproj:
* Pixiview.Android/Properties/AndroidManifest.xml: version update
This commit is contained in:
Tsanie Lily 2020-05-13 01:18:45 +08:00
parent 4561bc2674
commit ba46ba02c4
23 changed files with 1301 additions and 244 deletions

View File

@ -55,7 +55,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Forms" Version="4.6.0.726" />
<PackageReference Include="Xamarin.Essentials" Version="1.5.3.1" />
<PackageReference Include="Xamarin.Essentials" Version="1.5.3.2" />
<PackageReference Include="Newtonsoft.Json">
<Version>12.0.3</Version>
</PackageReference>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="1.0.511" package="org.tsanie.pixiview" android:versionCode="2">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="1.0.513" package="org.tsanie.pixiview" android:versionCode="3">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
<application android:label="Pixiview" android:icon="@mipmap/icon" android:roundIcon="@mipmap/icon_round"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

View File

@ -14,8 +14,6 @@
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleVersion</key>
<string>2</string>
<key>MinimumOSVersion</key>
<string>13.4</string>
<key>NSExtension</key>
@ -31,6 +29,8 @@
<string>com.apple.share-services</string>
</dict>
<key>CFBundleShortVersionString</key>
<string>1.0.511</string>
<string>1.0.513</string>
<key>CFBundleVersion</key>
<string>3</string>
</dict>
</plist>

View File

@ -79,8 +79,8 @@
</dict>
</array>
<key>CFBundleShortVersionString</key>
<string>1.0.511</string>
<string>1.0.513</string>
<key>CFBundleVersion</key>
<string>2</string>
<string>3</string>
</dict>
</plist>

View File

@ -143,7 +143,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Forms" Version="4.6.0.726" />
<PackageReference Include="Xamarin.Essentials" Version="1.5.3.1" />
<PackageReference Include="Xamarin.Essentials" Version="1.5.3.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
<Import Project="..\Pixiview\Pixiview.projitems" Label="Shared" Condition="Exists('..\Pixiview\Pixiview.projitems')" />

View File

@ -114,7 +114,7 @@ namespace Pixiview.iOS.Renderers
var tintColor = element.TintColor;
if (tintColor == default)
{
return UIColor.QuaternaryLabelColor;
return UIColor.SystemGray6Color;
}
else
{

View File

@ -5,6 +5,7 @@ using System.Windows.Input;
using Pixiview.Resources;
using Pixiview.Utils;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Pixiview.Illust
{
@ -25,7 +26,11 @@ namespace Pixiview.Illust
protected override void OnAppearing()
{
//base.OnAppearing();
Reload();
Device.StartTimer(TimeSpan.FromMilliseconds(200), () =>
{
Reload();
return false;
});
}
protected override IEnumerable<IllustItem> DoGetIllustList(IllustItem[] data, ICommand command)

View File

@ -15,6 +15,7 @@ namespace Pixiview.Illust
{
public abstract class FavoriteIllustCollectionPage : IllustCollectionPage<IllustItem[]> { }
public abstract class IllustDataCollectionPage : IllustCollectionPage<IllustData> { }
public abstract class IllustRankingDataCollectionPage : IllustCollectionPage<IllustRankingData> { }
public abstract class IllustUserDataCollectionPage : IllustCollectionPage<IllustUserData> { }
public interface IIllustCollectionPage
@ -132,20 +133,38 @@ namespace Pixiview.Illust
{
base.OnSizeAllocated(width, height);
int columns;
var oldMargin = PanelTopMargin;
Thickness newMargin;
if (StyleDefinition.IsFullscreenDevice)
{
newMargin = width > height ?
AppShell.HalfNavigationBarOffset :
AppShell.NavigationBarOffset;
}
else if (isPhone)
{
newMargin = width > height ?
StyleDefinition.TopOffset16 :
StyleDefinition.TopOffset32;
}
else
{
// TODO ipad
newMargin = StyleDefinition.TopOffset32;
}
if (width > height)
{
PanelTopMargin = StyleDefinition.IsFullscreenDevice ?
AppShell.HalfNavigationBarOffset :
StyleDefinition.TopOffset16;
columns = isPhone ? 4 : 6;
}
else
{
PanelTopMargin = StyleDefinition.IsFullscreenDevice ?
AppShell.NavigationBarOffset :
StyleDefinition.TopOffset32;
columns = isPhone ? 2 : 4;
}
if (oldMargin != newMargin)
{
PanelTopMargin = newMargin;
OnPanelTopMarginChanged(oldMargin, newMargin);
}
if (Columns != columns)
{
Columns = columns;
@ -162,6 +181,11 @@ namespace Pixiview.Illust
var page = new ViewIllustPage(illust, IsFavoriteVisible);
Navigation.PushAsync(page);
}
protected virtual void DoIllustsLoaded(IllustCollection collection)
{
IllustCollection = collection;
Illusts = collection;
}
protected void StartLoad(bool force = false)
{
@ -227,16 +251,33 @@ namespace Pixiview.Illust
.Binding(IsVisibleProperty, nameof(IllustItem.IsPageVisible))
.DynamicResource(Label.FontFamilyProperty, ThemeBase.IconSolidFontFamily);
// label: is anime
var anime = new RoundLabel
{
Text = StyleDefinition.IconPlay,
BackgroundColor = StyleDefinition.ColorDeepShadow,
Margin = new Thickness(0, 0, 6, 6),
Padding = new Thickness(12, 9, 0, 0),
WidthRequest = 36,
HeightRequest = 36,
CornerRadius = 18,
HorizontalOptions = LayoutOptions.End,
VerticalOptions = LayoutOptions.End,
FontSize = StyleDefinition.FontSizeTitle,
TextColor = Color.White
}
.Binding(IsVisibleProperty, nameof(IllustItem.IsAnimeVisible))
.DynamicResource(Label.FontFamilyProperty, ThemeBase.IconSolidFontFamily);
// label: title
var title = new Label
{
Padding = new Thickness(8, 2),
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.Center,
LineBreakMode = LineBreakMode.TailTruncation,
FontSize = StyleDefinition.FontSizeSmall
}
.Binding(Label.TextProperty, nameof(IllustItem.Title))
.DynamicResource(Label.TextColorProperty, ThemeBase.TextColor);
}.DynamicResource(Label.TextColorProperty, ThemeBase.TextColor);
// label: favorite
var favorite = new Label
@ -266,24 +307,34 @@ namespace Pixiview.Illust
Content = new Grid
{
HorizontalOptions = LayoutOptions.Fill,
RowSpacing = 0,
RowDefinitions =
{
new RowDefinition().Binding(RowDefinition.HeightProperty, nameof(IllustItem.ImageHeight)),
new RowDefinition { Height = GridLength.Auto }
new RowDefinition { Height = 30 }
},
Children =
{
image,
r18,
pages,
title,
anime,
// stacklayout: user
new StackLayout
// stacklayout: title
new Grid
{
Orientation = StackOrientation.Horizontal,
ColumnDefinitions =
{
new ColumnDefinition(),
new ColumnDefinition { Width = 20 }
},
VerticalOptions = LayoutOptions.Center,
Padding = new Thickness(0, 0, 8, 0),
Children = { title, favorite }
Children =
{
title.Binding(Label.TextProperty, nameof(IllustItem.Title)),
favorite.GridColumn(1)
}
}
.GridRow(1)
}
@ -302,23 +353,31 @@ namespace Pixiview.Illust
Content = new Grid
{
HorizontalOptions = LayoutOptions.Fill,
RowSpacing = 0,
RowDefinitions =
{
new RowDefinition().Binding(RowDefinition.HeightProperty, nameof(IllustItem.ImageHeight)),
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Auto }
new RowDefinition { Height = 30 },
new RowDefinition { Height = 40 }
},
Children =
{
image,
r18,
pages,
title.GridRow(1),
anime,
title.Binding(Label.TextProperty, nameof(IllustItem.RankTitle)).GridRow(1),
// stacklayout: user
new StackLayout
new Grid
{
Orientation = StackOrientation.Horizontal,
ColumnDefinitions =
{
new ColumnDefinition { Width = 30 },
new ColumnDefinition(),
new ColumnDefinition { Width = 20 }
},
Padding = new Thickness(8, 0, 8, 8),
Children =
{
@ -340,10 +399,11 @@ namespace Pixiview.Illust
FontSize = StyleDefinition.FontSizeMicro
}
.Binding(Label.TextProperty, nameof(IllustItem.UserName))
.DynamicResource(Label.TextColorProperty, ThemeBase.SubTextColor),
.DynamicResource(Label.TextColorProperty, ThemeBase.SubTextColor)
.GridColumn(1),
// label: favorite
favorite
favorite.GridColumn(2)
}
}
.GridRow(2)
@ -367,7 +427,9 @@ namespace Pixiview.Illust
}
if (force && IsFavoriteVisible)
{
LastUpdated = DateTime.Now;
var now = DateTime.Now;
LastUpdated = now;
lastUpdated = now;
}
var data = DoGetIllustList(illustData, commandIllustImageTapped).Where(i => i != null);
@ -385,8 +447,7 @@ namespace Pixiview.Illust
item.IsFavorite = favorites.Any(i => i.Id == item.Id);
}
}
IllustCollection = collection;
Illusts = collection;
DoIllustsLoaded(collection);
IsLoading = false;
DoLoadImages(collection);
@ -474,11 +535,20 @@ namespace Pixiview.Illust
public List<IllustItem> Illusts { get; set; }
}
public enum IllustType
{
Illust = 0,
Manga = 1,
Anime = 2
}
[JsonObject(MemberSerialization.OptIn)]
public class IllustItem : BindableObject
{
public static readonly BindableProperty TitleProperty = BindableProperty.Create(
nameof(Title), typeof(string), typeof(IllustItem));
public static readonly BindableProperty RankTitleProperty = BindableProperty.Create(
nameof(RankTitle), typeof(string), typeof(IllustItem));
public static readonly BindableProperty ImageProperty = BindableProperty.Create(
nameof(Image), typeof(ImageSource), typeof(IllustItem));
public static readonly BindableProperty ProfileImageProperty = BindableProperty.Create(
@ -487,6 +557,8 @@ namespace Pixiview.Illust
nameof(ImageHeight), typeof(GridLength), typeof(IllustItem), GridLength.Auto);
public static readonly BindableProperty IsFavoriteProperty = BindableProperty.Create(
nameof(IsFavorite), typeof(bool), typeof(IllustItem));
public static readonly BindableProperty IsPlayingProperty = BindableProperty.Create(
nameof(IsPlaying), typeof(bool), typeof(IllustItem));
[JsonProperty]
public string Title
@ -494,6 +566,11 @@ namespace Pixiview.Illust
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public string RankTitle
{
get => (string)GetValue(RankTitleProperty);
set => SetValue(RankTitleProperty, value);
}
public ImageSource Image
{
get => (ImageSource)GetValue(ImageProperty);
@ -514,6 +591,11 @@ namespace Pixiview.Illust
get => (bool)GetValue(IsFavoriteProperty);
set => SetValue(IsFavoriteProperty, value);
}
public bool IsPlaying
{
get => (bool)GetValue(IsPlayingProperty);
set => SetValue(IsPlayingProperty, value);
}
public ICommand IllustTapped { get; set; }
[JsonProperty]
@ -540,8 +622,19 @@ namespace Pixiview.Illust
get => ImageHeight.IsAuto ? -1 : ImageHeight.Value;
set => ImageHeight = value > 0 ? value : GridLength.Auto;
}
[JsonProperty]
public int YesRank { get; set; }
[JsonProperty]
public int RatingCount { get; set; }
[JsonProperty]
public int ViewCount { get; set; }
[JsonProperty]
public long UploadTimestamp { get; set; }
[JsonProperty]
public IllustType IllustType { get; set; }
public string PageCountText => $"{StyleDefinition.IconLayer} {PageCount}";
public bool IsPageVisible => PageCount > 1;
public bool IsAnimeVisible => IllustType == IllustType.Anime;
}
}

View File

@ -1,31 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<i:IllustDataCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i="clr-namespace:Pixiview.Illust"
xmlns:u="clr-namespace:Pixiview.UI"
xmlns:r="clr-namespace:Pixiview.Resources"
x:Class="Pixiview.Illust.RankingPage"
BackgroundColor="{DynamicResource WindowColor}"
Shell.NavBarHasShadow="False">
<i:IllustRankingDataCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i="clr-namespace:Pixiview.Illust"
xmlns:u="clr-namespace:Pixiview.UI"
xmlns:r="clr-namespace:Pixiview.Resources"
x:Class="Pixiview.Illust.RankingPage"
BackgroundColor="{DynamicResource WindowColor}">
<Shell.TitleView>
<Grid VerticalOptions="Fill" HorizontalOptions="Fill" ColumnSpacing="6">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Column="1" Text="{Binding Title}"
VerticalTextAlignment="Center" FontAttributes="Bold">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
</Label.GestureRecognizers>
</Label>
<Label Grid.Column="2" Text="{Binding PanelState}"
FontFamily="{DynamicResource IconSolidFontFamily}"
FontSize="Small"
VerticalTextAlignment="Center"/>
</Grid>
</Shell.TitleView>
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconRefresh}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"
HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<u:FlowLayout ItemsSource="{Binding Illusts}"
HorizontalOptions="Fill" Column="{Binding Columns}"
Margin="16, 62, 16, 16" RowSpacing="16" ColumnSpacing="16"
Margin="16, 16, 16, 16" RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}"/>
</ScrollView>
<u:BlurryPanel VerticalOptions="Start" HeightRequest="50" Margin="{Binding PanelTopMargin}"/>
<!--<u:BlurryPanel VerticalOptions="Start" HeightRequest="50" Margin="{Binding PanelTopMargin}"/>
<SearchBar x:Name="searchBar" Placeholder="{r:Text Search}" HeightRequest="50"
VerticalOptions="Start"
Margin="{Binding PageTopMargin}"
CancelButtonColor="{DynamicResource TintColor}"
Text="{Binding Keywords, Mode=TwoWay}"
SearchButtonPressed="SearchBar_SearchButtonPressed"
Unfocused="SearchBar_Unfocused"/>
Unfocused="SearchBar_Unfocused"/>-->
<u:BlurryPanel x:Name="panelFilter" VerticalOptions="Start" Opacity="0"
Margin="{Binding PanelTopMargin}"
HeightRequest="{Binding Height, Source={x:Reference gridFilter}}"/>
<Grid x:Name="gridFilter" VerticalOptions="Start" Opacity="0"
Margin="{Binding PageTopMargin}" Padding="10">
<Grid RowSpacing="0" HorizontalOptions="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<u:SegmentedControl Margin="6, 6, 6, 3" VerticalOptions="Center"
SelectedSegmentIndex="{Binding SegmentDate, Mode=TwoWay}">
<u:SegmentedControl.Children>
<u:SegmentedControlOption Text="{r:Text Daily}"/>
<u:SegmentedControlOption Text="{r:Text Weekly}"/>
<u:SegmentedControlOption Text="{r:Text Monthly}"/>
<u:SegmentedControlOption Text="{r:Text Male}"/>
</u:SegmentedControl.Children>
</u:SegmentedControl>
<u:SegmentedControl Grid.Row="1" HorizontalOptions="Start"
Margin="6, 3, 6, 6" VerticalOptions="Center"
SelectedSegmentIndex="{Binding SegmentType, Mode=TwoWay}">
<u:SegmentedControl.Children>
<u:SegmentedControlOption Text="{r:Text General}"/>
<u:SegmentedControlOption Text="{r:Text R18}"/>
</u:SegmentedControl.Children>
</u:SegmentedControl>
<Button Grid.Row="1" HorizontalOptions="End" VerticalOptions="Center"
Text="{DynamicResource IconCircleCheck}"
FontFamily="{DynamicResource IconSolidFontFamily}"
TextColor="{DynamicResource TintColor}"
FontSize="20" Margin="0, 0, 6, 0"
Clicked="Filter_Clicked"/>
</Grid>
</Grid>
<Frame HasShadow="False" Margin="0" Padding="20" CornerRadius="8"
IsVisible="{Binding IsLoading}"
HorizontalOptions="Center" VerticalOptions="Center"
@ -34,4 +89,4 @@
Color="{DynamicResource WindowColor}"/>
</Frame>
</Grid>
</i:IllustDataCollectionPage>
</i:IllustRankingDataCollectionPage>

View File

@ -1,45 +1,156 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Pixiview.Resources;
using Pixiview.UI;
using Pixiview.Utils;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Pixiview.Illust
{
public partial class RankingPage : IllustDataCollectionPage
public partial class RankingPage : IllustRankingDataCollectionPage
{
private static readonly string[] segmentDates = { "daily", "weekly", "monthly", "male" };
private static readonly object sync = new object();
public static readonly BindableProperty KeywordsProperty = BindableProperty.Create(
nameof(Keywords), typeof(string), typeof(RankingPage));
public static readonly BindableProperty PanelStateProperty = BindableProperty.Create(
nameof(PanelState), typeof(string), typeof(RankingPage), StyleDefinition.IconCaretDown);
public static readonly BindableProperty SegmentDateProperty = BindableProperty.Create(
nameof(SegmentDate), typeof(int), typeof(RankingPage), propertyChanged: OnSegmentDatePropertyChanged);
public static readonly BindableProperty SegmentTypeProperty = BindableProperty.Create(
nameof(SegmentType), typeof(int), typeof(RankingPage), propertyChanged: OnSegmentTypePropertyChanged);
private static void OnSegmentDatePropertyChanged(BindableObject obj, object old, object @new)
{
var page = (RankingPage)obj;
var index = (int)@new;
if (index == 2)
{
// monthly
var typeIndex = page.SegmentType;
if (typeIndex == 1)
{
// r-18
page.SegmentType = 0;
}
}
}
private static void OnSegmentTypePropertyChanged(BindableObject obj, object old, object @new)
{
var page = (RankingPage)obj;
var index = (int)@new;
if (index == 1)
{
// r-18
var dateIndex = page.SegmentDate;
if (dateIndex == 2)
{
// monthly
page.SegmentDate = 0;
}
}
}
public string Keywords
{
get => (string)GetValue(KeywordsProperty);
set => SetValue(KeywordsProperty, value);
}
public string PanelState
{
get => (string)GetValue(PanelStateProperty);
set => SetValue(PanelStateProperty, value);
}
public int SegmentDate
{
get => (int)GetValue(SegmentDateProperty);
set => SetValue(SegmentDateProperty, value);
}
public int SegmentType
{
get => (int)GetValue(SegmentTypeProperty);
set => SetValue(SegmentTypeProperty, value);
}
private bool isFilterVisible;
private double topOffset;
private string lastQueryKey;
private string queryDate;
private int currentPage;
private int nextPage;
private string QueryKey => segmentDates[SegmentDate] + (SegmentType == 1 ? "_r18" : string.Empty);
public RankingPage()
{
Resources.Add("cardView", GetCardViewTemplate());
InitializeComponent();
gridFilter.TranslationY = -100;
panelFilter.TranslationY = -100;
#if __IOS__
searchBar.BackgroundColor = Color.Transparent;
#elif __ANDROID__
searchBar.SetDynamicResource(SearchBar.TextColorProperty, UI.Theme.ThemeBase.TextColor);
searchBar.SetDynamicResource(SearchBar.PlaceholderColorProperty, UI.Theme.ThemeBase.SubTextColor);
searchBar.SetDynamicResource(BackgroundColorProperty, UI.Theme.ThemeBase.WindowColor);
#endif
//#if __IOS__
// searchBar.BackgroundColor = Color.Transparent;
//#elif __ANDROID__
// searchBar.SetDynamicResource(SearchBar.TextColorProperty, UI.Theme.ThemeBase.TextColor);
// searchBar.SetDynamicResource(SearchBar.PlaceholderColorProperty, UI.Theme.ThemeBase.SubTextColor);
// searchBar.SetDynamicResource(BackgroundColorProperty, UI.Theme.ThemeBase.WindowColor);
//#endif
lastQueryKey = QueryKey;
queryDate = null; // $"{now.Year}{now.Month:00}{now.Day:00}";
currentPage = 1;
}
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
if (StyleDefinition.IsFullscreenDevice)
{
topOffset = width > height ?
AppShell.NavigationBarOffset.Top :
AppShell.TotalBarOffset.Top;
}
else if (isPhone)
{
topOffset = width > height ?
StyleDefinition.TopOffset32.Top :
AppShell.TotalBarOffset.Top;
}
else
{
// TODO: ipad
topOffset = AppShell.TotalBarOffset.Top;
}
}
protected override bool IsLazyload => true;
protected override IEnumerable<IllustItem> DoGetIllustList(IllustData data, ICommand command)
protected override void DoIllustsLoaded(IllustCollection collection)
{
return data.body.page.ranking.items.Select(i =>
var now = IllustCollection;
if (now == null)
{
var item = data.body.thumbnails.illust.FirstOrDefault(l => l.illustId == i.id)?.ConvertToItem();
IllustCollection = collection;
Illusts = collection;
}
else
{
now = new IllustCollection(now.Concat(collection));
IllustCollection = now;
Illusts = now;
}
}
protected override IEnumerable<IllustItem> DoGetIllustList(IllustRankingData data, ICommand command)
{
return data.contents.Select(i =>
{
var item = i.ConvertToItem();
if (item != null)
{
item.IllustTapped = command;
@ -48,27 +159,67 @@ namespace Pixiview.Illust
});
}
protected override IllustData DoLoadIllustData(bool force)
protected override IllustRankingData DoLoadIllustData(bool force)
{
var data = Stores.LoadIllustData(force);
var data = Stores.LoadIllustRankingData(lastQueryKey, queryDate, currentPage, force);
if (data != null)
{
var date = data.body.page.ranking.date;
//if (data.contents.Length * data.page < data.rank_total)
//{
// nextPage = currentPage + 1;
//}
if (int.TryParse(data.next, out int next))
{
nextPage = next;
}
var date = data.date;
if (date.Length == 8)
{
queryDate = date;
date = date.Substring(0, 4) + "-" + date.Substring(4, 2) + "-" + date.Substring(6, 2);
}
date = ResourceHelper.GetResource(data.mode, date);
MainThread.BeginInvokeOnMainThread(() => Title = date);
}
return data;
}
private async void ToggleFilterPanel(bool flag)
{
ViewExtensions.CancelAnimations(gridFilter);
ViewExtensions.CancelAnimations(panelFilter);
if (flag)
{
isFilterVisible = true;
PanelState = StyleDefinition.IconCaretUp;
await Task.WhenAll(
gridFilter.TranslateTo(0, 0, easing: Easing.CubicOut),
gridFilter.FadeTo(1, easing: Easing.CubicOut),
panelFilter.TranslateTo(0, 0, easing: Easing.CubicOut),
panelFilter.FadeTo(1, easing: Easing.CubicOut)
);
}
else
{
isFilterVisible = false;
PanelState = StyleDefinition.IconCaretDown;
await Task.WhenAll(
gridFilter.TranslateTo(0, -100, easing: Easing.CubicOut),
gridFilter.FadeTo(0, easing: Easing.CubicOut),
panelFilter.TranslateTo(0, -100, easing: Easing.CubicOut),
panelFilter.FadeTo(0, easing: Easing.CubicOut)
);
}
}
private void Refresh_Clicked(object sender, EventArgs e)
{
if (IsLoading)
{
return;
}
currentPage = 1;
IllustCollection = null;
StartLoad(true);
}
@ -100,5 +251,65 @@ namespace Pixiview.Illust
SearchBar_SearchButtonPressed(sender, e);
}
}
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
ToggleFilterPanel(!isFilterVisible);
}
private void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
{
var bottomOffset = scrollView.ContentSize.Height - (scrollView.LayoutAreaOverride.Height * 2);
if (e.ScrollY > 0 && e.ScrollY + topOffset > bottomOffset)
{
bool refresh = false;
lock (sync)
{
if (IsLoading)
{
return;
}
App.DebugPrint("bottom arrived.");
if (nextPage == currentPage + 1)
{
currentPage = nextPage;
refresh = true;
App.DebugPrint($"loading page {nextPage}");
}
}
if (refresh)
{
StartLoad(true);
}
}
}
private void Filter_Clicked(object sender, EventArgs e)
{
var query = QueryKey;
ToggleFilterPanel(false);
bool refresh = false;
lock (sync)
{
if (IsLoading)
{
return;
}
if (lastQueryKey != query)
{
// query changed.
currentPage = 1;
lastQueryKey = query;
IllustCollection = null;
refresh = true;
App.DebugPrint($"query changed: {query}");
}
}
if (refresh)
{
StartLoad(true);
}
}
}
}

View File

@ -31,6 +31,9 @@
<Image.Effects>
<util:LongPressEffect/>
</Image.Effects>
<Image.GestureRecognizers>
<TapGestureRecognizer Tapped="Image_Tapped"/>
</Image.GestureRecognizers>
</Image>
<Frame HasShadow="False" Margin="0" Padding="20" CornerRadius="8"
IsVisible="{Binding Loading}"

View File

@ -60,6 +60,8 @@ namespace Pixiview.Illust
private readonly ICommand longPressed;
private readonly ImageSource fontIconLove;
private readonly ImageSource fontIconNotLove;
private IllustUgoiraData ugoiraData;
private Ugoira ugoira;
public ViewIllustPage(IllustItem illust, bool save)
{
@ -98,6 +100,12 @@ namespace Pixiview.Illust
{
base.OnDisappearing();
if (ugoira != null)
{
ugoira.TogglePlay(false);
IllustItem.IsPlaying = false;
}
if (saveFavorites)
{
Stores.SaveFavoritesIllusts();
@ -127,6 +135,7 @@ namespace Pixiview.Illust
{
items[i] = new IllustDetailItem
{
Id = illust.Id,
LongPressed = longPressed
};
if (i == 0)
@ -159,6 +168,7 @@ namespace Pixiview.Illust
{
tmp[i] = new IllustDetailItem
{
Id = illustItem.Id,
LongPressed = longPressed
};
}
@ -197,6 +207,11 @@ namespace Pixiview.Illust
{
DoLoadImage(1);
}
else if (illustItem.IllustType == IllustType.Anime)
{
// anime
ugoiraData = Stores.LoadIllustUgoiraData(illustItem.Id);
}
}
private void DoLoadImage(int index, bool force = false)
@ -268,6 +283,40 @@ namespace Pixiview.Illust
}
}
private void Image_Tapped(object sender, EventArgs e)
{
if (ugoiraData == null)
{
return;
}
var illustItem = IllustItem;
if (ugoira != null)
{
var playing = !ugoira.IsPlaying;
ugoira.TogglePlay(playing);
illustItem.IsPlaying = playing;
}
else if (((Image)sender).BindingContext is IllustDetailItem item)
{
if (illustItem.IsPlaying ||
illustItem.IllustType != IllustType.Anime)
{
return;
}
ugoira = new Ugoira(ugoiraData, item);
ugoira.FrameChanged += OnUgoiraFrameChanged;
illustItem.IsPlaying = true;
ugoira.TogglePlay(true);
}
}
private void OnUgoiraFrameChanged(object sender, UgoiraEventArgs e)
{
e.DetailItem.Image = e.Image;
}
private async void Illust_LongPressed(IllustDetailItem item)
{
List<string> extras = new List<string>();
@ -380,6 +429,7 @@ namespace Pixiview.Illust
get => (bool)GetValue(DownloadingProperty);
set => SetValue(DownloadingProperty, value);
}
public string Id { get; set; }
public ICommand LongPressed { get; set; }
public string PreviewUrl { get; set; }
public string OriginalUrl { get; set; }

View File

@ -91,6 +91,8 @@
<Compile Include="$(MSBuildThisFileDirectory)UI\Theme\LightTheme.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\Theme\ThemeBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\BlurryPanel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utils\IllustLegacy.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utils\HttpUtility.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="$(MSBuildThisFileDirectory)Illust\" />

View File

@ -10,6 +10,19 @@
<Enabled>启用</Enabled>
<Host>主机</Host>
<Port>端口</Port>
<Daily>今日</Daily>
<Weekly>本周</Weekly>
<Monthly>本月</Monthly>
<Male>受男性欢迎</Male>
<monthly>当月截至 {0}</monthly>
<weekly>当周截至 {0}</weekly>
<daily>当日 {0}</daily>
<male>受欢迎 {0}</male>
<monthly_r18>当月截至 {0}</monthly_r18>
<weekly_r18>当周截至 {0}</weekly_r18>
<daily_r18>当日 {0}</daily_r18>
<male_r18>受欢迎 {0}</male_r18>
<General>一般</General>
<R18>R-18</R18>
<Follow>已关注</Follow>
<Recommends>推荐</Recommends>

View File

@ -27,6 +27,7 @@ namespace Pixiview.UI
public event EventHandler Load;
public event EventHandler Unload;
public event EventHandler<ThicknessEventArgs> PageTopMarginChanged;
public event EventHandler<OrientationEventArgs> OrientationChanged;
protected static readonly bool isPhone = DeviceInfo.Idiom == DeviceIdiom.Phone;
@ -49,10 +50,12 @@ namespace Pixiview.UI
public virtual void OnOrientationChanged(Orientation orientation)
{
var oldMargin = PageTopMargin;
Thickness newMargin;
switch (orientation)
{
case Orientation.Portrait:
PageTopMargin = AppShell.TotalBarOffset;
newMargin = AppShell.TotalBarOffset;
break;
case Orientation.PortraitUpsideDown:
case Orientation.Unknown:
@ -61,17 +64,35 @@ namespace Pixiview.UI
default:
if (StyleDefinition.IsFullscreenDevice)
{
PageTopMargin = AppShell.NavigationBarOffset;
newMargin = AppShell.NavigationBarOffset;
}
else
{
PageTopMargin = isPhone ? StyleDefinition.TopOffset32 : AppShell.TotalBarOffset;
newMargin = isPhone ? StyleDefinition.TopOffset32 : AppShell.TotalBarOffset;
}
break;
}
if (oldMargin != newMargin)
{
PageTopMargin = newMargin;
OnPageTopMarginChanged(oldMargin, newMargin);
}
OrientationChanged?.Invoke(this, new OrientationEventArgs { CurrentOrientation = orientation });
}
public virtual void OnPageTopMarginChanged(Thickness old, Thickness @new)
{
PageTopMarginChanged?.Invoke(this, new ThicknessEventArgs
{
OldMargin = old,
NewMargin = @new
});
}
public virtual void OnPanelTopMarginChanged(Thickness old, Thickness @new)
{
}
public void InitOrientation(Orientation orientation)
{
CurrentOrientation = orientation;
@ -120,6 +141,12 @@ namespace Pixiview.UI
}
}
public class ThicknessEventArgs : EventArgs
{
public Thickness OldMargin { get; set; }
public Thickness NewMargin { get; set; }
}
public class OrientationEventArgs : EventArgs
{
public Orientation CurrentOrientation { get; set; }

View File

@ -13,6 +13,8 @@ namespace Pixiview.UI
nameof(ShadowRadius), typeof(float), typeof(CardView), 5f);
public static readonly BindableProperty ShadowOffsetProperty = BindableProperty.Create(
nameof(ShadowOffset), typeof(Size), typeof(CardView));
public static readonly BindableProperty RankProperty = BindableProperty.Create(
nameof(Rank), typeof(int), typeof(CardView));
public float CornerRadius
{
@ -34,6 +36,11 @@ namespace Pixiview.UI
get => (Size)GetValue(ShadowOffsetProperty);
set => SetValue(ShadowOffsetProperty, value);
}
public int Rank
{
get => (int)GetValue(RankProperty);
set => SetValue(RankProperty, value);
}
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{

View File

@ -41,6 +41,10 @@ namespace Pixiview.UI
public const string IconOption = "\uf013";
public const string IconFavorite = "\uf02e";
public const string IconShare = "\uf35d";
public const string IconCaretDown = "\uf0d7";
public const string IconCaretUp = "\uf0d8";
public const string IconCircleCheck = "\uf058";
public const string IconPlay = "\uf04b";
static StyleDefinition()
{

View File

@ -13,6 +13,7 @@ namespace Pixiview.UI.Theme
public const string FontIconOption = nameof(FontIconOption);
public const string FontIconFavorite = nameof(FontIconFavorite);
public const string FontIconShare = nameof(FontIconShare);
public const string IconCircleCheck = nameof(IconCircleCheck);
public const string StatusBarStyle = nameof(StatusBarStyle);
public const string WindowColor = nameof(WindowColor);
@ -52,6 +53,8 @@ namespace Pixiview.UI.Theme
Add(FontIconOption, GetSolidIcon(StyleDefinition.IconOption, solidFontFamily));
Add(FontIconFavorite, GetSolidIcon(StyleDefinition.IconFavorite, solidFontFamily));
Add(FontIconShare, GetSolidIcon(StyleDefinition.IconShare, solidFontFamily));
Add(IconCircleCheck, StyleDefinition.IconCircleCheck);
}
private FontImageSource GetSolidIcon(string icon, string family, Color color = default)

View File

@ -17,4 +17,24 @@ namespace Pixiview.Utils
throw new NotImplementedException();
}
}
public class OffsetConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Thickness thickness && parameter != null)
{
if (double.TryParse(parameter.ToString(), out var offset))
{
return new Thickness(thickness.Left, thickness.Top + offset, thickness.Right, thickness.Bottom);
}
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,502 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Pixiview.Illust;
using Xamarin.Forms;
namespace Pixiview.Utils
{
public class HttpUtility
{
public static T LoadObject<T>(string file, string url, string referer,
bool force = false,
Action<HttpRequestHeaders> header = null,
Func<T, string> namehandler = null,
Func<string, string> action = null)
{
string content = null;
if (!force && file != null && File.Exists(file))
{
try
{
content = File.ReadAllText(file);
}
catch (Exception ex)
{
App.DebugError("load", $"failed to read file: {file}, error: {ex.Message}");
}
}
if (content == null)
{
var response = Download(url, headers =>
{
if (referer != null)
{
headers.Referrer = new Uri(referer);
}
headers.Add("User-Agent", Configs.UserAgent);
headers.Add("Accept", Configs.AcceptJson);
headers.Add("Cookie", Configs.Cookie);
if (header == null)
{
headers.Add("X-User-Id", Configs.UserId);
}
else
{
header(headers);
}
});
if (response == null)
{
return default;
}
using (response)
{
try
{
content = response.Content.ReadAsStringAsync().Result;
if (action != null)
{
content = action(content);
}
}
catch (Exception ex)
{
App.DebugError("load.strea", $"failed to read stream, error: {ex.Message}");
return default;
}
bool rtn = false;
T result = default;
if (namehandler != null)
{
try
{
result = JsonConvert.DeserializeObject<T>(content);
file = namehandler(result);
rtn = true;
}
catch (Exception ex)
{
App.DebugError("load", $"failed to parse illust JSON object, error: {ex.Message}");
}
}
if (file != null)
{
try
{
var folder = Path.GetDirectoryName(file);
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
File.WriteAllText(file, content, Encoding.UTF8);
}
catch (Exception ex)
{
App.DebugError("save", $"failed to save illust JSON object, error: {ex.Message}");
}
}
if (rtn)
{
return result;
}
}
}
try
{
return JsonConvert.DeserializeObject<T>(content);
}
catch (Exception ex)
{
App.DebugError("load", $"failed to parse illust JSON object, error: {ex.Message}");
return default;
}
}
public static string DownloadImage(string url, string working, string folder)
{
try
{
var directory = Path.Combine(working, folder);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var file = Path.Combine(directory, Path.GetFileName(url));
App.DebugPrint($"download, url: {url}");
var response = Download(url, headers =>
{
headers.Referrer = new Uri(Configs.Referer);
headers.Add("User-Agent", Configs.UserAgent);
headers.Add("Accept", Configs.AcceptImage);
});
if (response == null)
{
return null;
}
using (response)
using (var fs = File.OpenWrite(file))
{
response.Content.CopyToAsync(fs).Wait();
//if (response.Headers.Date != null)
//{
// File.SetLastWriteTimeUtc(file, response.Headers.Date.Value.UtcDateTime);
//}
}
return file;
}
catch (Exception ex)
{
App.DebugError("image.download", ex.Message);
return null;
}
}
private static HttpResponseMessage Download(string url, Action<HttpRequestHeaders> headerAction)
{
App.DebugPrint($"GET: {url}");
var uri = new Uri(url);
var proxy = Configs.Proxy;
var handler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = false
};
if (proxy != null)
{
handler.Proxy = proxy;
handler.UseProxy = true;
}
var client = new HttpClient(handler)
{
BaseAddress = new Uri($"{uri.Scheme}://{uri.Host}"),
Timeout = TimeSpan.FromSeconds(30)
};
return TryCount(() =>
{
using (var request = new HttpRequestMessage(HttpMethod.Get, uri.PathAndQuery)
{
Version = new Version(2, 0)
})
{
var headers = request.Headers;
headerAction(headers);
if (proxy == null)
{
headers.Add("x-reverse", "yes");
}
headers.Add("Accept-Language", Configs.AcceptLanguage);
//headers.Add("Accept-Encoding", Configs.AcceptEncoding);
return client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result;
}
});
}
private static T TryCount<T>(Func<T> func, int tryCount = 2)
{
int tries = 0;
while (tries < tryCount)
{
try
{
return func();
}
catch (Exception ex)
{
tries++;
Thread.Sleep(1000);
App.DebugError("try.do", $"tries: {tries}, error: {ex.Message}");
}
}
return default;
}
public static (long Size, DateTimeOffset LastModified, HttpClient Client) GetUgoiraHeader(string url, string id)
{
var uri = new Uri(url);
var proxy = Configs.Proxy;
var handler = new HttpClientHandler
{
UseCookies = false
};
if (proxy != null)
{
handler.Proxy = proxy;
handler.UseProxy = true;
}
var client = new HttpClient(handler)
{
BaseAddress = new Uri($"{uri.Scheme}://{uri.Host}"),
Timeout = TimeSpan.FromSeconds(30)
};
var response = TryCount(() =>
{
using (var request = new HttpRequestMessage(HttpMethod.Head, uri.PathAndQuery)
{
Version = new Version(2, 0)
})
{
var headers = request.Headers;
UgoiraHeaderAction(headers, id);
headers.Add("Accept-Encoding", "gzip, deflate");
headers.Add("Accept-Language", Configs.AcceptLanguage);
return client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result;
}
});
var size = response.Content.Headers.ContentLength.Value;
var lastModified = response.Content.Headers.LastModified.Value;
return (size, lastModified, client);
}
public static long DownloadUgoiraImage(HttpClient client, string url, string id, DateTimeOffset lastModified, long from, long to, Stream stream)
{
var uri = new Uri(url);
var response = TryCount(() =>
{
using (var request = new HttpRequestMessage(HttpMethod.Get, uri.PathAndQuery)
{
Version = new Version(2, 0)
})
{
var headers = request.Headers;
UgoiraHeaderAction(headers, id);
headers.Add("Accept-Encoding", "identity");
headers.IfRange = new RangeConditionHeaderValue(lastModified);
headers.Range = new RangeHeaderValue(from, to);
headers.Add("Accept-Language", Configs.AcceptLanguage);
return client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result;
}
});
var length = response.Content.Headers.ContentLength.Value;
response.Content.CopyToAsync(stream).Wait();
return length;
}
private static void UgoiraHeaderAction(HttpRequestHeaders headers, string id)
{
headers.Add("Accept", "*/*");
headers.Add("Origin", Configs.Referer);
headers.Referrer = new Uri(string.Format(Configs.RefererIllust, id));
headers.Add("User-Agent", Configs.UserAgent);
}
}
public class Ugoira
{
private readonly IllustUgoiraBody ugoira;
private readonly IllustDetailItem detailItem;
private readonly ImageSource[] frames;
private readonly Timer timer;
private int index = 0;
public bool IsPlaying { get; private set; }
public event EventHandler<UgoiraEventArgs> FrameChanged;
public Ugoira(IllustUgoiraData illust, IllustDetailItem item)
{
ugoira = illust.body;
detailItem = item;
frames = new ImageSource[ugoira.frames.Length];
timer = new Timer(OnTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
Task.Run(LoadFrames);
}
public void TogglePlay(bool flag)
{
if (IsPlaying == flag)
{
return;
}
if (flag)
{
IsPlaying = true;
timer.Change(0, Timeout.Infinite);
}
else
{
IsPlaying = false;
timer.Change(Timeout.Infinite, Timeout.Infinite);
}
}
private void OnTimerCallback(object state)
{
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
});
i++;
if (i >= frames.Length)
{
i = 0;
}
index = i;
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; }
}
}

View File

@ -18,6 +18,8 @@ namespace Pixiview.Utils
public string illustTitle;
public string id;
public string title;
public int illustType;
public int xRestrict;
public string url;
public string description;
public string[] tags;
@ -31,7 +33,6 @@ namespace Pixiview.Utils
public string seriesId;
public string seriesTitle;
public string profileImageUrl;
public int xRestrict;
public class IllustUrls
{
@ -49,6 +50,7 @@ namespace Pixiview.Utils
{
Id = illustId,
Title = illustTitle,
IllustType = (IllustType)illustType,
ImageUrl = urls?.x360 ?? url,
IsRestrict = xRestrict == 1,
ProfileUrl = profileImageUrl,
@ -164,6 +166,7 @@ namespace Pixiview.Utils
public IllustItem CopyToItem(IllustItem item)
{
item.Title = illustTitle;
item.IllustType = (IllustType)illustType;
item.ImageUrl = urls?.regular;
item.IsRestrict = xRestrict == 1;
item.UserId = userId;
@ -238,4 +241,19 @@ namespace Pixiview.Utils
{
public Dictionary<string, Illust> works;
}
public class IllustUgoiraData : IllustResponse<IllustUgoiraBody> { }
public class IllustUgoiraBody
{
public string src;
public string originalSrc;
public string mime_type;
public Frame[] frames;
public class Frame
{
public string file;
public int delay;
}
}
}

View File

@ -0,0 +1,107 @@
using Pixiview.Illust;
namespace Pixiview.Utils
{
public class IllustRankingData
{
public Content[] contents;
public string mode;
public string content;
public int page;
public string prev;
public string next;
public string date;
public string prev_date;
public string next_date;
public int rank_total;
public class Content
{
public string title;
public string date;
public string[] tags;
public string url;
public string illust_type;
public string illust_book_style;
public string illust_page_count;
public string user_name;
public string profile_img;
public ContentType illust_content_type;
public object illust_series; // bool, Series
public long illust_id;
public int width;
public int height;
public long user_id;
public int rank;
public int yes_rank;
public int rating_count;
public int view_count;
public long illust_upload_timestamp;
public string attr;
public bool is_bookmarked;
public bool bookmarkable;
public class ContentType
{
public int sexual;
public bool lo;
public bool grotesque;
public bool violent;
public bool homosexual;
public bool drug;
public bool thoughts;
public bool antisocial;
public bool religion;
public bool original;
public bool furry;
public bool bl;
public bool yuri;
}
public class Series
{
public string illust_series_caption;
public string illust_series_content_count;
public string illust_series_content_illust_id;
public string illust_series_content_order;
public string illust_series_create_datetime;
public string illust_series_id;
public string illust_series_title;
public string illust_series_user_id;
public string page_url;
}
public IllustItem ConvertToItem()
{
if (!int.TryParse(illust_page_count, out int count))
{
count = 1;
}
if (!int.TryParse(illust_type, out int type))
{
type = 0;
}
return new IllustItem
{
Id = illust_id.ToString(),
Title = title,
RankTitle = $"#{rank} {title}",
IllustType = (IllustType)type,
ImageUrl = url,
//IsRestrict = xRestrict == 1,
ProfileUrl = profile_img,
UserId = user_id.ToString(),
UserName = user_name,
Width = width,
Height = height,
PageCount = count,
YesRank = yes_rank,
RatingCount = rating_count,
ViewCount = view_count,
UploadTimestamp = illust_upload_timestamp
};
}
}
}
}

View File

@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json;
using Pixiview.Illust;
@ -25,6 +23,7 @@ namespace Pixiview.Utils
private const string favoriteFile = "favorites.json";
private const string imageFolder = "img-original";
private const string previewFolder = "img-master";
private const string ugoiraFolder = "img-zip-ugoira";
private const string illustFile = "illust.json";
private const string pagesFolder = "pages";
@ -100,79 +99,41 @@ namespace Pixiview.Utils
}
}
private static T LoadObject<T>(string file, string url, string referer, bool force = false, Func<string, string> action = null)
public static ImageSource LoadUgoiraImage(string zip, string frame)
{
string content = null;
if (!force && file != null && File.Exists(file))
var file = Path.Combine(PersonalFolder, ugoiraFolder, zip, frame);
if (File.Exists(file))
{
try
{
content = File.ReadAllText(file);
return ImageSource.FromFile(file);
}
catch (Exception ex)
{
App.DebugError("load", $"failed to read file: {file}, error: {ex.Message}");
App.DebugError("load.ugoira", $"failed to load ugoira frame: {zip}/{frame}, error: {ex.Message}");
}
}
if (content == null)
{
var response = Download(url, headers =>
{
if (referer != null)
{
headers.Referrer = new Uri(referer);
}
headers.Add("User-Agent", Configs.UserAgent);
headers.Add("Accept", Configs.AcceptJson);
headers.Add("Cookie", Configs.Cookie);
headers.Add("X-User-Id", Configs.UserId);
});
if (response == null)
{
return default;
}
using (response)
{
try
{
content = response.Content.ReadAsStringAsync().Result;
if (action != null)
{
content = action(content);
}
}
catch (Exception ex)
{
App.DebugError("load.strea", $"failed to read stream, error: {ex.Message}");
return default;
}
return null;
}
if (file != null)
{
try
{
var folder = Path.GetDirectoryName(file);
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
File.WriteAllText(file, content, Encoding.UTF8);
}
catch (Exception ex)
{
App.DebugError("save", $"failed to save illust JSON object, error: {ex.Message}");
}
}
}
}
public static string SaveUgoiraImage(string zip, string frame, byte[] data)
{
try
{
return JsonConvert.DeserializeObject<T>(content);
var directory = Path.Combine(PersonalFolder, ugoiraFolder, zip);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var file = Path.Combine(directory, frame);
File.WriteAllBytes(file, data);
return file;
}
catch (Exception ex)
{
App.DebugError("load", $"failed to parse illust JSON object, error: {ex.Message}");
return default;
App.DebugError("save.ugoira", $"failed to save ugoira frame: {zip}/{frame}, error: {ex.Message}");
return null;
}
}
@ -237,14 +198,52 @@ namespace Pixiview.Utils
public static IllustData LoadIllustData(bool force = false)
{
var file = Path.Combine(PersonalFolder, illustFile);
var result = LoadObject<IllustData>(
var result = HttpUtility.LoadObject<IllustData>(
file,
Configs.UrlIllustList,
Configs.Referer,
force: force);
if (result == null || result.error)
{
App.DebugPrint($"error when load illust data: {result?.message} ({force})");
App.DebugPrint($"error when load illust data: {result?.message}, force({force})");
}
return result;
}
public static IllustRankingData LoadIllustRankingData(string mode, string date, int page, bool force = false)
{
var file = Path.Combine(CacheFolder, $"{mode}_{date}_{page}.json");
string query = $"mode={mode}";
if (mode != "male" && mode != "male_r18")
{
query += "&content=illust";
}
if (date != null)
{
query += $"&date={date}";
}
var referer = string.Format(Configs.RefererIllustRanking, query);
if (page > 1)
{
query += $"&p={page}";
}
query += "&format=json";
var result = HttpUtility.LoadObject<IllustRankingData>(
file,
string.Format(Configs.UrlIllustRanking, query),
referer,
namehandler: rst =>
{
return Path.Combine(CacheFolder, $"{mode}_{rst.date}_{page}.json");
},
header: headers =>
{
headers.Add("X-Requested-With", "XMLHttpRequest");
},
force: force);
if (result == null)
{
App.DebugPrint($"error when load ranking data: mode({mode}), date({date}), page({page}), force({force})");
}
return result;
}
@ -252,7 +251,7 @@ namespace Pixiview.Utils
public static IllustPreloadBody LoadIllustPreloadData(string id, bool force = false)
{
var file = Path.Combine(CacheFolder, preloadsFolder, $"{id}.json");
var result = LoadObject<IllustPreloadBody>(
var result = HttpUtility.LoadObject<IllustPreloadBody>(
file,
string.Format(Configs.UrlIllust, id),
null,
@ -271,47 +270,66 @@ namespace Pixiview.Utils
}
return content;
});
if (result == null)
{
App.DebugPrint($"error when load preload data: force({force})");
}
return result;
}
public static IllustPageData LoadIllustPageData(string id, bool force = false)
{
var file = Path.Combine(CacheFolder, pagesFolder, $"{id}.json");
var result = LoadObject<IllustPageData>(
var result = HttpUtility.LoadObject<IllustPageData>(
file,
string.Format(Configs.UrlIllustPage, id),
string.Format(Configs.UrlIllust, id),
string.Format(Configs.RefererIllust, id),
force: force);
if (result == null || result.error)
{
App.DebugPrint($"error when load page data: {result?.message} ({force})");
App.DebugPrint($"error when load page data: {result?.message}, force({force})");
}
return result;
}
public static IllustUgoiraData LoadIllustUgoiraData(string id, bool force = false)
{
var file = Path.Combine(PersonalFolder, ugoiraFolder, $"{id}.json");
var result = HttpUtility.LoadObject<IllustUgoiraData>(
file,
string.Format(Configs.UrlIllustUgoira, id),
string.Format(Configs.RefererIllust, id),
force: force);
if (result == null || result.error)
{
App.DebugPrint($"error when load ugoira data: {result?.message}, force({force})");
}
return result;
}
public static IllustUserData LoadIllustUserData(string userId, bool force = false)
{
var list = LoadObject<IllustUserListData>(
var list = HttpUtility.LoadObject<IllustUserListData>(
null,
string.Format(Configs.UrlIllustUserAll, userId),
string.Format(Configs.UrlIllustUser, userId),
string.Format(Configs.RefererIllustUser, userId),
force: force);
if (list == null || list.error)
{
App.DebugPrint($"error when load user data: {list?.message} ({force})");
App.DebugPrint($"error when load user data: {list?.message}, force({force})");
}
// TODO
var ids = string.Join("", list.body.illusts.Keys.Take(20).Select(id => $"ids%5B%5D={id}&"));
var result = LoadObject<IllustUserData>(
var result = HttpUtility.LoadObject<IllustUserData>(
null,
string.Format(Configs.UrlIllustUserArtworks, userId, ids, 1),
string.Format(Configs.UrlIllustUser, userId),
string.Format(Configs.RefererIllustUser, userId),
force: force);
if (result == null || result.error)
{
App.DebugPrint($"error when load user illust data: {result?.message} ({force})");
App.DebugPrint($"error when load user illust data: {result?.message}, force({force})");
}
return result;
}
@ -374,7 +392,7 @@ namespace Pixiview.Utils
}
if (downloading && image == null)
{
file = DownloadImage(url, working, folder);
file = HttpUtility.DownloadImage(url, working, folder);
if (file != null)
{
return ImageSource.FromFile(file);
@ -382,101 +400,6 @@ namespace Pixiview.Utils
}
return image;
}
private static string DownloadImage(string url, string working, string folder)
{
try
{
var directory = Path.Combine(working, folder);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var file = Path.Combine(directory, Path.GetFileName(url));
App.DebugPrint($"download, url: {url}");
var response = Download(url, headers =>
{
headers.Referrer = new Uri(Configs.Referer);
headers.Add("User-Agent", Configs.UserAgent);
headers.Add("Accept", Configs.AcceptImage);
});
if (response == null)
{
return null;
}
using (response)
using (var fs = File.OpenWrite(file))
{
response.Content.CopyToAsync(fs).Wait();
//if (response.Headers.Date != null)
//{
// File.SetLastWriteTimeUtc(file, response.Headers.Date.Value.UtcDateTime);
//}
}
return file;
}
catch (Exception ex)
{
App.DebugError("image.download", ex.Message);
return null;
}
}
private static T TryCount<T>(Func<T> func, int tryCount = 3)
{
int tries = 0;
while (tries < tryCount)
{
try
{
return func();
}
catch (Exception ex)
{
tries++;
System.Threading.Thread.Sleep(400);
App.DebugError("try.do", $"tries: {tries}, error: {ex.Message}");
}
}
return default;
}
private static HttpResponseMessage Download(string url, Action<HttpRequestHeaders> headerAction)
{
App.DebugPrint($"GET: {url}");
var uri = new Uri(url);
var proxy = Configs.Proxy;
var handler = new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = false
};
if (proxy != null)
{
handler.Proxy = proxy;
handler.UseProxy = true;
}
var client = new HttpClient(handler)
{
BaseAddress = new Uri($"{uri.Scheme}://{uri.Host}"),
Timeout = TimeSpan.FromSeconds(30)
};
return TryCount(() =>
{
using (var request = new HttpRequestMessage(HttpMethod.Get, uri.PathAndQuery)
{
Version = new Version(2, 0)
})
{
var headers = request.Headers;
headerAction(headers);
headers.Add("x-reverse", "yes");
headers.Add("Accept-Language", Configs.AcceptLanguage);
//headers.Add("Accept-Encoding", Configs.AcceptEncoding);
return client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result;
}
});
}
}
public static class Configs
@ -487,6 +410,9 @@ namespace Pixiview.Utils
public const int MaxThreads = 3;
public const string Referer = "https://www.pixiv.net/";
public const string RefererIllust = "https://www.pixiv.net/artworks/{0}";
public const string RefererIllustRanking = "https://www.pixiv.net/ranking.php?{0}";
public const string RefererIllustUser = "https://www.pixiv.net/users/{0}/artworks";
public static WebProxy Proxy;
private static string Prefix => Proxy == null ?
@ -497,26 +423,37 @@ namespace Pixiview.Utils
public const int SuffixPreloadLength = 33; // SuffixPreload.Length
public static string UrlIllustList => Prefix + "ajax/top/illust?mode=all&lang=zh";
public static string UrlIllust => Prefix + "artworks/{0}";
public static string UrlIllustRanking => Prefix + "ranking.php?{0}";
public static string UrlIllustUserAll => Prefix + "ajax/user/{0}/profile/all?lang=zh";
public static string UrlIllustUserArtworks => Prefix + "ajax/user/{0}/profile/illusts?{1}work_category=illustManga&is_first_page={2}&lang=zh";
public static string UrlIllustUser => Prefix + "users/{0}/artworks";
public static string UrlIllustPage => Prefix + "ajax/illust/{0}/pages?lang=zh";
public static string UrlIllustUgoira => Prefix + "ajax/illust/{0}/ugoira_meta?lang=zh";
public const string UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36";
public const string AcceptImage = "image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5";
public const string AcceptJson = "application/json";
//public const string AcceptEncoding = "gzip, deflate";
public const string AcceptLanguage = "zh-cn";
public const string UserId = "53887721";
//public const string UserId = "53887721";
//public const string Cookie =
// "PHPSESSID=5sn8n049j5c18l0tlj91qrjhesgddhjv; " +
// "a_type=0; b_type=1; c_type=29; d_type=2; " +
// "p_ab_d_id=1021624041; p_ab_id=2; p_ab_id_2=0; " +
// "privacy_policy_agreement=2; " +
// "login_ever=yes; " +
// "__cfduid=d84153bf70ae67315a8bc297299d39eb61588856027; " +
// "first_visit_datetime_pc=2020-05-07+21%3A53%3A47; " +
// "yuid_b=MYkIJXc";
public const string UserId = "2603358";
public const string Cookie =
"PHPSESSID=5sn8n049j5c18l0tlj91qrjhesgddhjv; " +
"a_type=0; b_type=1; c_type=29; d_type=2; " +
"p_ab_d_id=1021624041; p_ab_id=2; p_ab_id_2=0; " +
"privacy_policy_agreement=2; " +
"login_ever=yes; " +
"__cfduid=d84153bf70ae67315a8bc297299d39eb61588856027; " +
"first_visit_datetime_pc=2020-05-07+21%3A53%3A47; " +
"yuid_b=MYkIJXc";
"PHPSESSID=2603358_VHyGPeRaz7LpeoFkRsHvjXIpApCMb56a; " +
"a_type=0; b_type=1; c_type=31; d_type=2; " +
"p_ab_id=2; p_ab_id_2=6; p_ab_d_id=1155161977; " +
"privacy_policy_agreement=2; " +
"login_ever=yes; " +
"__cfduid=d9fa2d4d1ddd30db85ebb519f9855d2561587806747; " +
"first_visit_datetime_pc=2019-10-29+22%3A05%3A30; " +
"yuid_b=NgcXQWQ";
public static string GetThumbnailUrl(string url)
{