rename from Pixiview to Gallery

This commit is contained in:
2021-08-03 19:16:54 +08:00
parent 98676ce8b2
commit c41282a4b7
206 changed files with 7900 additions and 7891 deletions

257
Gallery/App.cs Executable file
View File

@ -0,0 +1,257 @@
using System;
#if DEBUG
using System.Diagnostics;
#endif
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Gallery.Illust;
using Gallery.Resources;
using Gallery.UI.Theme;
using Gallery.Utils;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery
{
public class App : Application
{
// public properties
public static AppTheme CurrentTheme { get; private set; }
public static PlatformCulture CurrentCulture { get; private set; }
public App()
{
Device.SetFlags(new string[0]);
}
private void InitResources()
{
var theme = AppInfo.RequestedTheme;
SetTheme(theme, true);
}
private void InitPreferences()
{
Configs.SetCookie(Preferences.Get(Configs.CookieKey, null));
Configs.SetUserId(Preferences.Get(Configs.UserIdKey, null));
Configs.DownloadIllustThreads = Preferences.Get(Configs.DownloadIllustThreadsKey, 1);
Configs.IsOnR18 = Preferences.Get(Configs.IsOnR18Key, false);
Configs.SyncFavType = (SyncType)Preferences.Get(Configs.SyncFavTypeKey, 0);
var isProxied = Preferences.Get(Configs.IsProxiedKey, false);
if (isProxied)
{
var host = Preferences.Get(Configs.HostKey, string.Empty);
int port = Preferences.Get(Configs.PortKey, 0);
if (!string.IsNullOrEmpty(host) && port > 0)
{
try
{
if (host.IndexOf(':') >= 0)
{
host = $"[{host}]";
}
var uri = new Uri($"http://{host}:{port}");
Configs.Proxy = new System.Net.WebProxy(uri, true);
#if LOG
DebugPrint($"load proxy: {host}:{port}");
#endif
}
catch (Exception ex)
{
DebugError("init.preferences", $"failed to parse proxy: {host}:{port}, error: {ex.Message}");
}
}
else
{
Configs.Proxy = null;
}
}
else
{
Configs.Proxy = null;
}
}
private void InitLanguage()
{
var ci = EnvironmentService.GetCurrentCultureInfo();
EnvironmentService.SetCultureInfo(ci);
CurrentCulture = new PlatformCulture(ci.Name.ToLower());
}
private void SetTheme(AppTheme theme, bool force = false)
{
if (force || theme != CurrentTheme)
{
CurrentTheme = theme;
}
else
{
return;
}
#if LOG
DebugPrint($"application theme: {theme}");
#endif
ThemeBase themeInstance;
if (theme == AppTheme.Dark)
{
themeInstance = DarkTheme.Instance;
}
else
{
themeInstance = LightTheme.Instance;
}
#if __IOS__
var style = (StatusBarStyles)themeInstance[ThemeBase.StatusBarStyle];
EnvironmentService.SetStatusBarStyle(style);
#elif __ANDROID__
var color = (Color)themeInstance[ThemeBase.NavColor];
EnvironmentService.SetStatusBarColor(color);
#endif
Resources = themeInstance;
}
protected override void OnStart()
{
InitLanguage();
MainPage = new AppShell();
InitResources();
InitPreferences();
}
protected override void OnSleep()
{
base.OnSleep();
}
protected override void OnResume()
{
var theme = AppInfo.RequestedTheme;
SetTheme(theme);
}
#if DEBUG
public static void DebugPrint(string message)
{
Debug.WriteLine("[{0:HH:mm:ss.ffff}] - {1}", DateTime.Now, message);
}
public static void DebugError(string category, string message)
{
Debug.Fail(string.Format("[{0:HH:mm:ss.ffff}] - {1} - {2}", DateTime.Now, category, message));
}
#else
public static void DebugPrint(string message)
{
Console.WriteLine("[Debug.Print] - {0}", message);
}
public static void DebugError(string category, string message)
{
Console.Error.WriteLine(string.Format("[Debug.Error] - {0} - {1}", category, message));
}
#endif
public static bool OpenUrl(Uri uri)
{
var current = Current.MainPage;
if (current != null && uri != null)
{
var url = uri.AbsolutePath;
if ("gallery".Equals(uri.Scheme, StringComparison.OrdinalIgnoreCase))
{
var m = Regex.Match(url, "/artworks/([0-9]+)", RegexOptions.IgnoreCase);
if (m.Success)
{
var illust = new IllustItem { Id = m.Groups[1].Value };
var page = new ViewIllustPage(illust);
MainThread.BeginInvokeOnMainThread(() => current.Navigation.PushAsync(page));
}
else
{
MainThread.BeginInvokeOnMainThread(() => current.DisplayAlert(
url,
ResourceHelper.InvalidUrl,
ResourceHelper.Ok));
}
}
else
{
url = System.Net.WebUtility.UrlDecode(url);
if (File.Exists(url))
{
IllustFavorite favObj;
try
{
favObj = Stores.LoadFavoritesIllusts(url);
foreach (var o in favObj.Illusts)
{
o.BookmarkId = null;
}
}
catch (Exception ex)
{
DebugError("open.file", $"failed to parse file, name: {url}, error: {ex.Message}");
return true;
}
var path = Stores.FavoritesPath;
if (url == path)
{
return false;
}
if (File.Exists(path))
{
MainThread.BeginInvokeOnMainThread(async () =>
{
var opReplace = ResourceHelper.FavoritesReplace;
var opCombine = ResourceHelper.FavoritesCombine;
var result = await current.DisplayActionSheet(
ResourceHelper.FavoritesOperation,
ResourceHelper.Cancel,
opCombine,
opReplace);
if (result == opReplace)
{
// replace favorite file
File.Copy(url, path, true);
}
else if (result == opCombine)
{
// combine favorite file
var favNow = Stores.GetFavoriteObject();
var list = favNow.Illusts;
var distinct = favObj.Illusts.Where(f => !list.Any(i => i.Id == f.Id)).ToList();
list.InsertRange(0, distinct);
//favNow.Illusts = list;
Stores.SaveFavoritesIllusts();
}
if (Shell.Current.CurrentState.Location.OriginalString.EndsWith(Routes.Favorites))
{
var sc = (IShellSectionController)Shell.Current.CurrentItem.CurrentItem;
if (sc.PresentedPage is FavoritesPage fav)
{
fav.Reload(true);
}
}
});
}
else
{
File.Copy(url, path);
if (Shell.Current.CurrentState.Location.OriginalString.EndsWith(Routes.Favorites))
{
var sc = (IShellSectionController)Shell.Current.CurrentItem.CurrentItem;
if (sc.PresentedPage is FavoritesPage fav)
{
fav.Reload(true);
}
}
}
}
}
}
return true;
}
}
}

104
Gallery/AppShell.xaml Normal file
View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:p="clr-namespace:Gallery"
xmlns:i="clr-namespace:Gallery.Illust"
xmlns:r="clr-namespace:Gallery.Resources"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:util="clr-namespace:Gallery.Utils"
x:Class="Gallery.AppShell"
BackgroundColor="{DynamicResource NavColor}"
ForegroundColor="{DynamicResource TintColor}"
TitleColor="{DynamicResource TextColor}"
UnselectedColor="{DynamicResource TintColor}"
FlyoutBackgroundColor="{DynamicResource WindowColor}">
<Shell.FlyoutHeaderTemplate>
<DataTemplate>
<Grid RowSpacing="0" BackgroundColor="{DynamicResource WindowColor}" Padding="20, 0, 0, 20">
<Grid.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
</Grid.GestureRecognizers>
<Grid.RowDefinitions>
<RowDefinition Height="80"/>
<RowDefinition Height="30"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<u:CircleImage Aspect="AspectFill" Source="{Binding UserProfileImage}"
HeightRequest="60" WidthRequest="60"
VerticalOptions="End" HorizontalOptions="Start"/>
<Label Grid.Row="1" VerticalOptions="End" FontAttributes="Bold"
Text="{Binding UserProfileName}" TextColor="{DynamicResource TextColor}"/>
<Label Grid.Row="2" VerticalOptions="Center" FontSize="Small"
Text="{Binding UserProfileId}" TextColor="{DynamicResource SubTextColor}"/>
<ActivityIndicator Grid.RowSpan="3" Margin="0, 20, 20, 0"
Color="{DynamicResource TextColor}"
IsVisible="{Binding IsLoading}" IsRunning="{Binding IsLoading}"/>
</Grid>
</DataTemplate>
</Shell.FlyoutHeaderTemplate>
<Shell.ItemTemplate>
<DataTemplate x:DataType="BaseShellItem">
<Grid HeightRequest="40">
<VisualStateManager.VisualStateGroups>
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor"
Value="{DynamicResource WindowColor}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor"
Value="{DynamicResource NavSelectedColor}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</VisualStateManager.VisualStateGroups>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{x:OnPlatform Android=54, iOS=50}"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image Source="{Binding FlyoutIcon}"
HorizontalOptions="Center" VerticalOptions="Center"
HeightRequest="22"/>
<Label Grid.Column="1" TextColor="{DynamicResource TextColor}"
Text="{Binding Title}"
FontSize="{x:OnPlatform Android=14, iOS=Small}"
VerticalTextAlignment="Center"/>
</Grid>
</DataTemplate>
</Shell.ItemTemplate>
<FlyoutItem FlyoutDisplayOptions="AsMultipleItems"
Route="{x:Static util:Routes.Illust}">
<Tab FlyoutIcon="{DynamicResource FontIconUserFlyout}"
Title="{r:Text Follow}"
Route="{x:Static util:Routes.Follow}">
<ShellContent ContentTemplate="{DataTemplate i:MainPage}"/>
</Tab>
<Tab FlyoutIcon="{DynamicResource FontIconSparklesFlyout}"
Title="{r:Text Recommends}"
Route="{x:Static util:Routes.Recommends}">
<ShellContent ContentTemplate="{DataTemplate i:RecommendsPage}"/>
</Tab>
<Tab FlyoutIcon="{DynamicResource FontIconOrderFlyout}"
Title="{r:Text Ranking}"
Route="{x:Static util:Routes.Ranking}">
<ShellContent ContentTemplate="{DataTemplate i:RankingPage}"/>
</Tab>
<Tab FlyoutIcon="{DynamicResource FontIconFavoriteFlyout}"
Title="{r:Text Favorites}"
Route="{x:Static util:Routes.Favorites}">
<ShellContent ContentTemplate="{DataTemplate i:FavoritesPage}"/>
</Tab>
</FlyoutItem>
<FlyoutItem FlyoutIcon="{DynamicResource FontIconOption}"
Title="{r:Text Option}"
Route="{x:Static util:Routes.Option}">
<Tab>
<ShellContent ContentTemplate="{DataTemplate p:OptionPage}"/>
</Tab>
</FlyoutItem>
</Shell>

187
Gallery/AppShell.xaml.cs Normal file
View File

@ -0,0 +1,187 @@
using System;
using System.Threading.Tasks;
using Gallery.Login;
using Gallery.Resources;
using Gallery.UI;
using Gallery.Utils;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery
{
public partial class AppShell : Shell
{
public static new AppShell Current => Shell.Current as AppShell;
public static Thickness NavigationBarOffset { get; private set; }
public static Thickness HalfNavigationBarOffset { get; private set; }
public static Thickness TotalBarOffset { get; private set; }
public static readonly BindableProperty UserProfileImageProperty = BindableProperty.Create(
nameof(UserProfileImage), typeof(ImageSource), typeof(AppShell), StyleDefinition.ProfileNone);
public static readonly BindableProperty UserProfileNameProperty = BindableProperty.Create(
nameof(UserProfileName), typeof(string), typeof(AppShell), ResourceHelper.Guest);
public static readonly BindableProperty UserProfileIdProperty = BindableProperty.Create(
nameof(UserProfileId), typeof(string), typeof(AppShell));
public static readonly BindableProperty IsLoadingProperty = BindableProperty.Create(
nameof(IsLoading), typeof(bool), typeof(AppShell));
public ImageSource UserProfileImage
{
get => (ImageSource)GetValue(UserProfileImageProperty);
set => SetValue(UserProfileImageProperty, value);
}
public string UserProfileName
{
get => (string)GetValue(UserProfileNameProperty);
set => SetValue(UserProfileNameProperty, value);
}
public string UserProfileId
{
get => (string)GetValue(UserProfileIdProperty);
set => SetValue(UserProfileIdProperty, value);
}
public bool IsLoading
{
get => (bool)GetValue(IsLoadingProperty);
set => SetValue(IsLoadingProperty, value);
}
public event EventHandler<BarHeightEventArgs> NavigationBarHeightChanged;
public event EventHandler<BarHeightEventArgs> StatusBarHeightChanged;
private bool firstLoading = true;
public AppShell()
{
BindingContext = this;
InitializeComponent();
#if LOG
App.DebugPrint($"folder: {Stores.PersonalFolder}");
App.DebugPrint($"cache: {Stores.CacheFolder}");
#endif
}
protected override void OnNavigated(ShellNavigatedEventArgs args)
{
if (firstLoading)
{
firstLoading = false;
// login info
Task.Run(() => DoLoginInformation(true));
}
}
public void SetNavigationBarHeight(double height)
{
NavigationBarOffset = new Thickness(0, height, 0, 0);
HalfNavigationBarOffset = new Thickness(0, height / 2, 0, 0);
NavigationBarHeightChanged?.Invoke(this, new BarHeightEventArgs
{
NavigationBarHeight = height
});
}
public void SetStatusBarHeight(double navigation, double height)
{
TotalBarOffset = new Thickness(0, navigation + height, 0, 0);
StatusBarHeightChanged?.Invoke(this, new BarHeightEventArgs
{
StatusBarHeight = height
});
}
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
if (UserProfileId != null)
{
if (IsLoading)
{
return;
}
IsLoading = true;
Task.Run(() =>
{
DoLoginInformation(true);
IsLoading = false;
});
}
else
{
PushToLogin(() =>
{
Task.Run(() => DoLoginInformation(true));
});
}
}
private bool isLoginOpened;
public void PushToLogin(Action after)
{
if (isLoginOpened)
{
return;
}
isLoginOpened = true;
var loginPage = new LoginPage(()=>
{
isLoginOpened = false;
after?.Invoke();
});
loginPage.Disappearing += (sender, e) =>
{
isLoginOpened = false;
};
MainThread.BeginInvokeOnMainThread(async () =>
{
await Navigation.PushModalAsync(loginPage);
});
}
public void DoLoginInformation(bool force = false)
{
string name = null;
string userId = null;
string img = null;
if (!force)
{
name = Preferences.Get(Configs.ProfileNameKey, null);
userId = Preferences.Get(Configs.ProfileIdKey, null);
img = Preferences.Get(Configs.ProfileImageKey, null);
}
if (name == null || userId == null)
{
var global = Stores.LoadGlobalData(force);
if (global == null || global.userData == null)
{
App.DebugError("login.info", "user data is null");
return;
}
name = global.userData.name;
userId = global.userData.pixivId;
img = global.userData.profileImgBig;
Preferences.Set(Configs.ProfileNameKey, name);
Preferences.Set(Configs.ProfileIdKey, userId);
Preferences.Set(Configs.ProfileImageKey, img);
}
UserProfileName = name ?? ResourceHelper.Guest;
UserProfileId = string.IsNullOrEmpty(userId)
? string.Empty
: $"@{userId}";
UserProfileImage = img == null
? StyleDefinition.ProfileNone
: Stores.LoadUserProfileImage(img, true);
}
}
public class BarHeightEventArgs : EventArgs
{
public double NavigationBarHeight { get; set; }
public double StatusBarHeight { get; set; }
}
}

3
Gallery/AssemblyInfo.cs Executable file
View File

@ -0,0 +1,3 @@
using Xamarin.Forms.Xaml;
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]

122
Gallery/Gallery.projitems Executable file
View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>{57C27E64-049B-4EE8-9308-BCAFE329E8E8}</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>Gallery</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)AppShell.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)OptionPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Illust\FavoritesPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Illust\MainPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Illust\RankingPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Illust\RecommendsPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Illust\UserIllustPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Illust\ViewIllustPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Resources\Languages\zh-CN.xml" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Illust\RelatedIllustsPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Login\LoginPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)App.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AppShell.xaml.cs">
<DependentUpon>AppShell.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)AssemblyInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)OptionPage.xaml.cs">
<DependentUpon>OptionPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Illust\FavoritesPage.xaml.cs">
<DependentUpon>FavoritesPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Illust\IllustCollectionPage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Illust\MainPage.xaml.cs">
<DependentUpon>MainPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Illust\RankingPage.xaml.cs">
<DependentUpon>RankingPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Illust\RecommendsPage.xaml.cs">
<DependentUpon>RecommendsPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Illust\UserIllustPage.xaml.cs">
<DependentUpon>UserIllustPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Illust\ViewIllustPage.xaml.cs">
<DependentUpon>ViewIllustPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Utils\Converters.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utils\Extensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utils\EnvironmentService.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utils\FileStore.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utils\IllustData.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utils\LongPressEffect.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utils\Stores.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Resources\PlatformCulture.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Resources\ResourceHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\AdaptedPage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\CardView.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\CircleUIs.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\FlowLayout.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\OptionCell.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\SegmentedControl.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\StyleDefinition.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\Theme\DarkTheme.cs" />
<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" />
<Compile Include="$(MSBuildThisFileDirectory)Illust\RelatedIllustsPage.xaml.cs">
<DependentUpon>RelatedIllustsPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Utils\Ugoira.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Login\LoginPage.xaml.cs">
<DependentUpon>LoginPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Login\HybridWebView.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="$(MSBuildThisFileDirectory)Illust\" />
<Folder Include="$(MSBuildThisFileDirectory)Utils\" />
<Folder Include="$(MSBuildThisFileDirectory)Resources\" />
<Folder Include="$(MSBuildThisFileDirectory)Resources\Languages\" />
<Folder Include="$(MSBuildThisFileDirectory)UI\" />
<Folder Include="$(MSBuildThisFileDirectory)UI\Theme\" />
<Folder Include="$(MSBuildThisFileDirectory)Login\" />
</ItemGroup>
</Project>

11
Gallery/Gallery.shproj Executable file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{57C27E64-049B-4EE8-9308-BCAFE329E8E8}</ProjectGuid>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<Import Project="Gallery.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<i:FavoriteIllustCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i="clr-namespace:Gallery.Illust"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:r="clr-namespace:Gallery.Resources"
x:Class="Gallery.Illust.FavoritesPage"
BackgroundColor="{DynamicResource WindowColor}"
Title="{r:Text Favorites}">
<Shell.TitleView>
<Grid VerticalOptions="Fill" ColumnSpacing="6"
HorizontalOptions="{x:OnPlatform Android=Start, iOS=Fill}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform Android=Auto}"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackLayout Grid.Column="1" Orientation="Horizontal" Spacing="6">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
</StackLayout.GestureRecognizers>
<Label Text="{Binding Title}"
TextColor="{DynamicResource TextColor}"
FontSize="{OnPlatform Android=18}"
LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center" FontAttributes="Bold"/>
<Label x:Name="labelCaret"
Text="{DynamicResource IconCaretDown}"
TextColor="{DynamicResource TextColor}"
FontFamily="{DynamicResource IconSolidFontFamily}"
FontSize="Small"
VerticalTextAlignment="Center"/>
</StackLayout>
</Grid>
</Shell.TitleView>
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconCloudDownload}"/>
<ToolbarItem Order="Primary" Clicked="ShareFavorites_Clicked"
IconImageSource="{DynamicResource FontIconShare}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"
HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<StackLayout>
<ActivityIndicator x:Name="activityLoading" Margin="0, -40, 0, 0"
HeightRequest="40"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
<u:FlowLayout ItemsSource="{Binding Illusts}" MaxHeightChanged="FlowLayout_MaxHeightChanged"
HorizontalOptions="Fill" Column="{Binding Columns}"
Margin="16" RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}"/>
<ActivityIndicator x:Name="activityBottomLoading" Margin="0, -10, 0, 16"
IsRunning="{Binding IsBottomLoading}"
IsVisible="{Binding IsBottomLoading}"/>
</StackLayout>
</ScrollView>
<u:BlurryPanel x:Name="panelFilter" VerticalOptions="Start" Opacity="0"
Margin="{Binding PanelTopMargin}"
BackgroundColor="{DynamicResource WindowColor}"
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">
<u:SegmentedControl Margin="6, 6, 6, 3" VerticalOptions="Center"
SelectedSegmentIndex="{Binding SegmentType, Mode=TwoWay}"
SelectedTextColor="{DynamicResource TextColor}"
TintColor="{DynamicResource CardBackgroundColor}">
<u:SegmentedControl.Children>
<u:SegmentedControlOption Text="{r:Text All}"/>
<u:SegmentedControlOption Text="{r:Text General}"/>
<u:SegmentedControlOption Text="{r:Text Animation}"/>
<u:SegmentedControlOption Text="{r:Text Online}"/>
</u:SegmentedControl.Children>
</u:SegmentedControl>
</Grid>
</Grid>
</Grid>
</i:FavoriteIllustCollectionPage>

View File

@ -0,0 +1,463 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Gallery.Resources;
using Gallery.UI;
using Gallery.Utils;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.Illust
{
public partial class FavoritesPage : FavoriteIllustCollectionPage
{
private const int STEP = 20;
public static readonly BindableProperty SegmentTypeProperty = BindableProperty.Create(
nameof(SegmentType), typeof(int), typeof(FavoritesPage), propertyChanged: OnSegmentTypePropertyChanged);
private static void OnSegmentTypePropertyChanged(BindableObject obj, object old, object @new)
{
var page = (FavoritesPage)obj;
MainThread.BeginInvokeOnMainThread(page.ChangeFilter);
}
public int SegmentType
{
get => (int)GetValue(SegmentTypeProperty);
set => SetValue(SegmentTypeProperty, value);
}
private bool isFilterVisible;
private int startIndex;
private int nextIndex;
private bool flag = false;
private ParallelTask task;
public FavoritesPage()
{
Resources.Add("cardView", GetCardViewTemplate());
InitializeComponent();
gridFilter.TranslationY = -60;
panelFilter.TranslationY = -60;
SegmentType = Preferences.Get(Configs.FavoriteTypeKey, 0);
startIndex = -1;
nextIndex = 0;
}
protected override bool IsFavoriteVisible => false;
protected override ActivityIndicator LoadingIndicator => activityLoading;
protected override void OnAppearing()
{
if (lastUpdated != LastUpdated)
{
startIndex = -1;
StartLoad();
}
else
{
var favorites = Stores.Favorites;
if (favorites.Changed)
{
lastUpdated = default;
startIndex = -1;
StartLoad();
}
}
}
protected override void OnDisappearing()
{
base.OnDisappearing();
// saving state
Preferences.Set(Configs.FavoriteTypeKey, SegmentType);
}
public override void OnUnload()
{
if (task != null)
{
task.Dispose();
task = null;
}
base.OnUnload();
}
protected override IEnumerable<IllustItem> DoGetIllustList(IllustItem[] data, out int tag)
{
tag = startIndex;
return data;
}
protected override IllustItem[] DoLoadIllustData(bool force)
{
IEnumerable<IllustItem> favs;
if (startIndex < 0)
{
var favorites = Stores.GetFavoriteObject(flag);
flag = false;
if (favorites == null)
{
return null;
}
favs = favorites.Illusts.Reload();
startIndex = 0;
}
else
{
favs = Stores.Favorites;
}
switch (SegmentType)
{
case 1: // general (non r-18)
favs = favs.Where(f => !f.IsRestrict);
break;
case 2: // animation
favs = favs.Where(f => f.IllustType == IllustType.Anime);
break;
case 3: // online
favs = favs.Where(f => f.BookmarkId != null);
break;
}
var illusts = favs.Skip(startIndex).Take(STEP).ToArray();
nextIndex = startIndex + STEP;
if (illusts.Length == 0 || nextIndex >= Stores.Favorites.Count)
{
// reach the bottom
startIndex = nextIndex;
}
return illusts;
}
private async void ToggleFilterPanel(bool flag)
{
ViewExtensions.CancelAnimations(gridFilter);
ViewExtensions.CancelAnimations(panelFilter);
if (flag)
{
isFilterVisible = true;
if (scrollDirection == ScrollDirection.Down)
{
// stop the scrolling
await scrollView.ScrollToAsync(scrollView.ScrollX, scrollView.ScrollY, false);
}
await Task.WhenAll(
labelCaret.RotateTo(180, easing: Easing.CubicOut),
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;
await Task.WhenAll(
labelCaret.RotateTo(0, easing: Easing.CubicIn),
gridFilter.TranslateTo(0, -60, easing: Easing.CubicIn),
gridFilter.FadeTo(0, easing: Easing.CubicIn),
panelFilter.TranslateTo(0, -60, easing: Easing.CubicIn),
panelFilter.FadeTo(0, easing: Easing.CubicIn)
);
}
}
private async void ChangeFilter()
{
ToggleFilterPanel(false);
await ScrollToTopAsync(scrollView);
lastUpdated = default;
startIndex = 0;
StartLoad();
}
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
ToggleFilterPanel(!isFilterVisible);
}
private void FlowLayout_MaxHeightChanged(object sender, HeightEventArgs e)
{
SetOffset(e.ContentHeight - scrollView.Bounds.Height - SCROLL_OFFSET);
}
protected override bool CheckRefresh()
{
if (nextIndex > startIndex)
{
startIndex = nextIndex;
return true;
}
return false;
}
private void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
{
var y = e.ScrollY;
if (IsScrollingDown(y))
{
// down
if (isFilterVisible)
{
ToggleFilterPanel(false);
}
}
OnScrolled(y);
}
public void Reload(bool force = false)
{
if (force)
{
flag = true;
}
lastUpdated = default;
startIndex = -1;
StartLoad(force);
}
private async void Refresh_Clicked(object sender, EventArgs e)
{
if (IsLoading)
{
return;
}
await ScrollToTopAsync(scrollView);
IsLoading = true;
var offset = 16 - IndicatorMarginTop;
activityLoading.Margin = new Thickness(0, loadingOffset - offset, 0, offset);
activityLoading.Animate("margin", top =>
{
activityLoading.Margin = new Thickness(0, top, 0, offset);
},
loadingOffset - offset, 16 - offset, easing: Easing.CubicOut, finished: (v, r) =>
{
Task.Run(() =>
{
var list = Stores.LoadOnlineFavorites();
if (list != null && list.Length > 0)
{
MainThread.BeginInvokeOnMainThread(() => ConfirmNext(list));
}
else
{
flag = false;
lastUpdated = default;
startIndex = -1;
MainThread.BeginInvokeOnMainThread(() => StartLoad(true));
}
});
});
}
private void CloseLoading(Action next = null)
{
var offset = 16 - IndicatorMarginTop;
activityLoading.Animate("margin", top =>
{
activityLoading.Margin = new Thickness(0, top, 0, offset);
},
16 - offset, loadingOffset - offset, easing: Easing.CubicIn, finished: (v, r) =>
{
IsLoading = false;
next?.Invoke();
});
}
private async void ConfirmNext(IllustItem[] list)
{
var cancel = ResourceHelper.Cancel;
var combine = ResourceHelper.FavoritesCombine;
var replace = ResourceHelper.FavoritesReplace;
var result = await DisplayActionSheet(
ResourceHelper.FavoritesOperation,
cancel,
combine,
replace);
if (result == cancel)
{
return;
}
if (result == replace)
{
Stores.GetFavoriteObject().Illusts = new FavoriteList(list);
}
else if (result == combine)
{
// combine
var nows = Stores.GetFavoriteObject().Illusts;
// sync bookmarks from remote
for (var i = nows.Count - 1; i >= 0; i--)
{
var b = nows[i];
var bookmarkId = b.BookmarkId;
var bookmark = list.FirstOrDefault(f => f.Id == b.Id);
if (bookmark == null)
{
if (bookmarkId != null)
{
// not exists in remote any more
#if LOG
App.DebugPrint($"remove bookmark ({bookmarkId}) - {b.Id}: {b.Title}");
#endif
nows.RemoveAt(i);
}
}
else if (bookmarkId != bookmark.BookmarkId)
{
// update bookmark id
#if LOG
App.DebugPrint($"change bookmark ({bookmarkId}) to ({bookmark.BookmarkId}) - {b.Id}: {b.Title}");
#endif
b.BookmarkId = bookmark.BookmarkId;
}
}
// add bookmarks that exists in remote only
list = list.Where(f => !nows.Any(i => i.Id == f.Id)).ToArray();
if (list.Length > 0)
{
#if LOG
for (var i = 0; i < list.Length; i++)
{
var item = list[i];
App.DebugPrint($"add bookmark ({item.BookmarkId}) - {item.Id}: {item.Title}");
}
#endif
nows.InsertRange(0, list);
}
}
else
{
return;
}
SyncRemoteFavorites(list);
}
private void SyncRemoteFavorites(IllustItem[] list)
{
for (var i = 0; i < list.Length; i++)
{
var item = list[i];
var data = Stores.LoadIllustPreloadData(item.Id, false);
if (data != null && data.illust.TryGetValue(item.Id, out var illust))
{
illust.CopyToItem(item);
if (data.user.TryGetValue(illust.userId, out var user))
{
item.ProfileUrl = user.image;
}
var url = Configs.GetThumbnailUrl(item.ImageUrl);
if (url != null)
{
var image = Stores.LoadPreviewImage(url, false);
if (image == null)
{
image = Stores.LoadThumbnailImage(url, false);
}
if (image != null)
{
item.Image = image;
}
}
url = item.ProfileUrl;
if (url == null)
{
item.ProfileImage = StyleDefinition.ProfileNone;
}
else
{
var image = Stores.LoadUserProfileImage(url, false);
if (image != null)
{
item.ProfileImage = image;
}
}
}
}
list = list.Where(i => i.Image == null || i.ProfileImage == null).ToArray();
if (task != null)
{
task.Dispose();
task = null;
}
task = ParallelTask.Start("favorite.loadimages", 0, list.Length, Configs.MaxPageThreads, i =>
{
var item = list[i];
if (item.ImageUrl == null || item.ProfileUrl == null)
{
var data = Stores.LoadIllustPreloadData(item.Id, true, force: true);
if (data != null && data.illust.TryGetValue(item.Id, out var illust))
{
illust.CopyToItem(item);
if (data.user.TryGetValue(illust.userId, out var user))
{
item.ProfileUrl = user.image;
}
}
else
{
App.DebugError("load.favorite", $"cannot fetch preload data, {item.Id}, disposed.");
return false;
}
}
if (item.Image == null && item.ImageUrl != null)
{
var url = Configs.GetThumbnailUrl(item.ImageUrl);
item.Image = StyleDefinition.DownloadBackground;
var image = Stores.LoadThumbnailImage(url, true, force: true);
if (image != null)
{
item.Image = image;
}
}
if (item.ProfileImage == null && item.ProfileUrl != null)
{
item.ProfileImage = StyleDefinition.ProfileNone;
var userImage = Stores.LoadUserProfileImage(item.ProfileUrl, true, force: true);
if (userImage != null)
{
item.ProfileImage = userImage;
}
}
return true;
},
complete: () =>
{
Stores.SaveFavoritesIllusts();
MainThread.BeginInvokeOnMainThread(() =>
{
CloseLoading(() =>
{
flag = false;
lastUpdated = default;
startIndex = -1;
StartLoad();
});
});
});
}
private async void ShareFavorites_Clicked(object sender, EventArgs e)
{
var file = Stores.FavoritesPath;
await Share.RequestAsync(new ShareFileRequest
{
Title = ResourceHelper.Favorites,
File = new ShareFile(file)
});
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
<?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:Gallery.Illust"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:r="clr-namespace:Gallery.Resources"
x:Class="Gallery.Illust.MainPage"
BackgroundColor="{DynamicResource WindowColor}"
Title="{r:Text Follow}">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconRefresh}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"
HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<StackLayout>
<ActivityIndicator x:Name="activityLoading" Margin="0, 10, 0, 0"
HeightRequest="40"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
<u:FlowLayout ItemsSource="{Binding Illusts}"
HorizontalOptions="Fill" Column="{Binding Columns}"
RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}">
<u:FlowLayout.Margin>
<OnPlatform x:TypeArguments="Thickness"
iOS="16, 66, 16, 16"
Android="16, 56, 16, 16"/>
</u:FlowLayout.Margin>
</u:FlowLayout>
</StackLayout>
</ScrollView>
<u:BlurryPanel x:Name="panelBar" VerticalOptions="Start"
HeightRequest="60"/>
<SearchBar x:Name="searchBar" Placeholder="{r:Text Search}"
HeightRequest="40"
VerticalOptions="Start"
CancelButtonColor="{DynamicResource TintColor}"
Text="{Binding Keywords, Mode=TwoWay}"
SearchButtonPressed="SearchBar_SearchButtonPressed"/>
</Grid>
</i:IllustDataCollectionPage>

View File

@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Gallery.Utils;
using Xamarin.Forms;
namespace Gallery.Illust
{
public partial class MainPage : IllustDataCollectionPage
{
public static readonly BindableProperty KeywordsProperty = BindableProperty.Create(
nameof(Keywords), typeof(string), typeof(MainPage));
public string Keywords
{
get => (string)GetValue(KeywordsProperty);
set => SetValue(KeywordsProperty, value);
}
private double lastScrollY = double.MinValue;
private ScrollDirection scrollDirection = ScrollDirection.Stop;
public MainPage()
{
Resources.Add("cardView", GetCardViewTemplate());
InitializeComponent();
#if __IOS__
searchBar.BackgroundColor = Color.Transparent;
#else
searchBar.SetDynamicResource(SearchBar.TextColorProperty, UI.Theme.ThemeBase.TextColor);
searchBar.SetDynamicResource(SearchBar.PlaceholderColorProperty, UI.Theme.ThemeBase.SubTextColor);
searchBar.SetDynamicResource(BackgroundColorProperty, UI.Theme.ThemeBase.WindowColor);
#endif
}
protected override bool IsRedirectLogin => true;
protected override bool NeedCookie => true;
protected override ActivityIndicator LoadingIndicator => activityLoading;
protected override double IndicatorMarginTop => 66;
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
searchBar.Margin = new Thickness(0, PageTopMargin.Top + 8, 0, 0);
panelBar.Margin = PanelTopMargin;
}
#if __IOS__
public override void OnOrientationChanged(bool landscape)
{
base.OnOrientationChanged(landscape);
AnimateToMargin(searchBar, new Thickness(0, PageTopMargin.Top + 8, 0, 0));
AnimateToMargin(panelBar, PanelTopMargin);
}
#endif
protected override IEnumerable<IllustItem> DoGetIllustList(IllustData data, out int tag)
{
tag = 0;
if (data.body == null)
{
return null;
}
var illusts = data.body.page.follow.Select(i =>
data.body.thumbnails.illust.FirstOrDefault(l => (l.illustId ?? l.id) == i.ToString())?.ConvertToItem());
return illusts;
}
protected override IllustData DoLoadIllustData(bool force)
{
return Stores.LoadIllustData(force);
}
private async void Refresh_Clicked(object sender, EventArgs e)
{
if (IsLoading)
{
return;
}
await ScrollToTopAsync(scrollView);
StartLoad(true);
}
private void SearchBar_SearchButtonPressed(object sender, EventArgs e)
{
var key = Keywords;
if (key != null)
{
key = key.Trim().ToLower();
}
if (!string.IsNullOrEmpty(key))
{
Task.Run(() => App.OpenUrl(new Uri("gallery://example.com/artworks/" + key)));
}
}
private const int searchBarHeight = 60;
private void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
{
var y = e.ScrollY;
if (y > lastScrollY)
{
// down
if (scrollDirection != ScrollDirection.Down && y > searchBarHeight - topOffset)
{
scrollDirection = ScrollDirection.Down;
if (searchBar.IsFocused)
{
searchBar.Unfocus();
}
ViewExtensions.CancelAnimations(searchBar);
ViewExtensions.CancelAnimations(panelBar);
searchBar.TranslateTo(0, -searchBarHeight, easing: Easing.CubicIn);
panelBar.TranslateTo(0, -searchBarHeight, easing: Easing.CubicIn);
}
}
else
{
// up
if (scrollDirection != ScrollDirection.Up)
{
scrollDirection = ScrollDirection.Up;
ViewExtensions.CancelAnimations(searchBar);
ViewExtensions.CancelAnimations(panelBar);
searchBar.TranslateTo(0, 0, easing: Easing.CubicOut);
panelBar.TranslateTo(0, 0, easing: Easing.CubicOut);
}
}
lastScrollY = y;
}
}
}

116
Gallery/Illust/RankingPage.xaml Executable file
View File

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<i:IllustRankingDataCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i="clr-namespace:Gallery.Illust"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:r="clr-namespace:Gallery.Resources"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
x:Class="Gallery.Illust.RankingPage"
BackgroundColor="{DynamicResource WindowColor}"
Title="{r:Text Ranking}">
<Shell.TitleView>
<Grid VerticalOptions="Fill" ColumnSpacing="6"
HorizontalOptions="{x:OnPlatform Android=Start, iOS=Fill}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{OnPlatform Android=Auto}"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackLayout Grid.Column="1" Orientation="Horizontal" Spacing="6">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
</StackLayout.GestureRecognizers>
<Label Text="{Binding InternalTitle}"
TextColor="{DynamicResource TextColor}"
FontSize="{OnPlatform Android=18}"
LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center" FontAttributes="Bold"/>
<Label x:Name="labelCaret"
Text="{DynamicResource IconCaretDown}"
TextColor="{DynamicResource TextColor}"
FontFamily="{DynamicResource IconSolidFontFamily}"
FontSize="Small"
VerticalTextAlignment="Center"/>
</StackLayout>
</Grid>
</Shell.TitleView>
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary"
Command="{Binding ToolbarCommand}" CommandParameter="prev"
IconImageSource="{DynamicResource FontIconCaretCircleLeft}"/>
<ToolbarItem Order="Primary"
Command="{Binding ToolbarCommand}" CommandParameter="select"
IconImageSource="{DynamicResource FontIconCalendarDay}"/>
<ToolbarItem Order="Primary"
Command="{Binding ToolbarCommand}" CommandParameter="next"
IconImageSource="{DynamicResource FontIconCaretCircleRight}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"
HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<StackLayout>
<ActivityIndicator x:Name="activityLoading" Margin="0, -40, 0, 0"
HeightRequest="40"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
<u:FlowLayout ItemsSource="{Binding Illusts}" MaxHeightChanged="FlowLayout_MaxHeightChanged"
HorizontalOptions="Fill" Column="{Binding Columns}"
Margin="16" RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}"/>
<ActivityIndicator x:Name="activityBottomLoading" Margin="0, -10, 0, 16"
IsRunning="{Binding IsBottomLoading}"
IsVisible="{Binding IsBottomLoading}"/>
</StackLayout>
</ScrollView>
<u:BlurryPanel x:Name="panelFilter" VerticalOptions="Start" Opacity="0"
Margin="{Binding PanelTopMargin}"
BackgroundColor="{DynamicResource WindowColor}"
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}"
SelectedTextColor="{DynamicResource TextColor}"
TintColor="{DynamicResource CardBackgroundColor}">
<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"
IsVisible="{Binding SegmentTypeVisible}"
Margin="6, 3, 6, 6" VerticalOptions="Center"
SelectedSegmentIndex="{Binding SegmentType, Mode=TwoWay}"
SelectedTextColor="{DynamicResource TextColor}"
TintColor="{DynamicResource CardBackgroundColor}">
<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}"
BackgroundColor="Transparent"
TextColor="{DynamicResource TintColor}"
FontSize="20" Margin="0, 0, 6, 0"
Clicked="Filter_Clicked"/>
</Grid>
</Grid>
<DatePicker x:Name="datePicker" IsVisible="False"
ios:DatePicker.UpdateMode="WhenFinished"
Date="{Binding SelectedDate}"
MaximumDate="{Binding MaximumDate}"
Focused="DatePicker_Focused"
Unfocused="DatePicker_Focused"
DateSelected="DatePicker_DateSelected"/>
</Grid>
</i:IllustRankingDataCollectionPage>

View File

@ -0,0 +1,406 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Gallery.Resources;
using Gallery.UI;
using Gallery.Utils;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.Illust
{
public partial class RankingPage : IllustRankingDataCollectionPage
{
private static readonly string[] segmentDates = { "daily", "weekly", "monthly", "male" };
public static readonly BindableProperty InternalTitleProperty = BindableProperty.Create(
nameof(InternalTitle), typeof(string), typeof(RankingPage));
public string InternalTitle
{
get => (string)GetValue(InternalTitleProperty);
set => SetValue(InternalTitleProperty, value);
}
public static readonly BindableProperty SegmentDateProperty = BindableProperty.Create(
nameof(SegmentDate), typeof(int), typeof(RankingPage), propertyChanged: OnSegmentDatePropertyChanged);
public static readonly BindableProperty SegmentTypeVisibleProperty = BindableProperty.Create(
nameof(SegmentTypeVisible), typeof(bool), typeof(RankingPage));
public static readonly BindableProperty SegmentTypeProperty = BindableProperty.Create(
nameof(SegmentType), typeof(int), typeof(RankingPage), propertyChanged: OnSegmentTypePropertyChanged);
public static readonly BindableProperty MaximumDateProperty = BindableProperty.Create(
nameof(MaximumDate), typeof(DateTime), typeof(RankingPage), DateTime.Today);
public static readonly BindableProperty SelectedDateProperty = BindableProperty.Create(
nameof(SelectedDate), typeof(DateTime), typeof(RankingPage), DateTime.Today);
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 int SegmentDate
{
get => (int)GetValue(SegmentDateProperty);
set => SetValue(SegmentDateProperty, value);
}
public bool SegmentTypeVisible
{
get => (bool)GetValue(SegmentTypeVisibleProperty);
set => SetValue(SegmentTypeVisibleProperty, value);
}
public int SegmentType
{
get => (int)GetValue(SegmentTypeProperty);
set => SetValue(SegmentTypeProperty, value);
}
public DateTime MaximumDate
{
get => (DateTime)GetValue(MaximumDateProperty);
set => SetValue(MaximumDateProperty, value);
}
public DateTime SelectedDate
{
get => (DateTime)GetValue(SelectedDateProperty);
set => SetValue(SelectedDateProperty, value);
}
public Command<string> ToolbarCommand { get; private set; }
private bool previousEnabled;
private bool dateEnabled;
private bool nextEnabled;
private bool isFilterVisible;
private bool isDatePickerVisible;
private string lastQueryKey;
private string queryDate;
private string previousDate;
private string nextDate;
private int currentPage;
private int nextPage;
private string QueryKey => segmentDates[SegmentDate] + (SegmentType == 1 ? "_r18" : string.Empty);
public RankingPage()
{
Resources.Add("cardView", GetCardViewTemplate(titleBinding: nameof(IllustItem.RankTitle)));
ToolbarCommand = new Command<string>(OnDateTrigger, OnCanDateTrigger);
InitializeComponent();
gridFilter.TranslationY = -100;
panelFilter.TranslationY = -100;
// preferences
SegmentDate = Preferences.Get(Configs.QueryModeKey, 0);
SegmentType = Preferences.Get(Configs.QueryTypeKey, 0);
lastQueryKey = QueryKey;
queryDate = Preferences.Get(Configs.QueryDateKey, null);
currentPage = 1;
datePicker.MinimumDate = new DateTime(2007, 9, 13);
MaximumDate = DateTime.Today.AddDays(-1);
}
protected override ActivityIndicator LoadingIndicator => activityLoading;
protected override void OnAppearing()
{
var r18 = Configs.IsOnR18;
if (!r18)
{
SegmentType = 0;
var query = QueryKey;
if (lastQueryKey != query)
{
ReleaseCollection();
lastQueryKey = query;
}
}
SegmentTypeVisible = r18;
if (currentPage != 1 && Illusts == null)
{
currentPage = 1;
}
base.OnAppearing();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
// saving state
Preferences.Set(Configs.QueryModeKey, SegmentDate);
Preferences.Set(Configs.QueryTypeKey, SegmentType);
if (queryDate != null)
{
Preferences.Set(Configs.QueryDateKey, queryDate);
}
}
protected override IEnumerable<IllustItem> DoGetIllustList(IllustRankingData data, out int tag)
{
tag = currentPage;
if (lastQueryKey != null && lastQueryKey.StartsWith(segmentDates[3]))
{
return data.contents.Where(i => i.illust_type == "0").Select(i => i.ConvertToItem());
}
return data.contents.Select(i => i.ConvertToItem());
}
protected override IllustRankingData DoLoadIllustData(bool force)
{
var data = Stores.LoadIllustRankingData(lastQueryKey, queryDate, currentPage, out lastError, force);
if (data != null)
{
if (int.TryParse(data.next, out int next))
{
nextPage = next;
}
var date = data.date;
DateTime now;
if (date.Length == 8 && int.TryParse(date, out _))
{
queryDate = date;
now = new DateTime(
int.Parse(date.Substring(0, 4)),
int.Parse(date.Substring(4, 2)),
int.Parse(date.Substring(6, 2)));
SelectedDate = now;
//date = now.ToShortDateString();
date = now.ToString("yyyy-MM-dd");
}
else
{
now = default;
}
date = ResourceHelper.GetResource(data.mode, date);
MainThread.BeginInvokeOnMainThread(() => InternalTitle = date);
var prev_date = data.prev_date;
if (int.TryParse(prev_date, out _))
{
previousDate = prev_date;
previousEnabled = true;
}
else
{
previousDate = null;
previousEnabled = false;
}
var next_date = data.next_date;
if (int.TryParse(next_date, out _))
{
nextDate = next_date;
nextEnabled = true;
}
else
{
nextDate = null;
nextEnabled = false;
if (now != default && force)
{
MaximumDate = now;
}
}
}
dateEnabled = true;
MainThread.BeginInvokeOnMainThread(ToolbarCommand.ChangeCanExecute);
return data;
}
private async void ToggleFilterPanel(bool flag)
{
ViewExtensions.CancelAnimations(gridFilter);
ViewExtensions.CancelAnimations(panelFilter);
if (flag)
{
isFilterVisible = true;
if (scrollDirection == ScrollDirection.Down)
{
// stop the scrolling
await scrollView.ScrollToAsync(scrollView.ScrollX, scrollView.ScrollY, false);
}
await Task.WhenAll(
labelCaret.RotateTo(180, easing: Easing.CubicOut),
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;
await Task.WhenAll(
labelCaret.RotateTo(0, easing: Easing.CubicIn),
gridFilter.TranslateTo(0, -100, easing: Easing.CubicIn),
gridFilter.FadeTo(0, easing: Easing.CubicIn),
panelFilter.TranslateTo(0, -100, easing: Easing.CubicIn),
panelFilter.FadeTo(0, easing: Easing.CubicIn)
);
}
}
private async void OnDateTrigger(string action)
{
if (IsLoading)
{
return;
}
if (action == "select")
{
// stop the scrolling
await scrollView.ScrollToAsync(scrollView.ScrollX, scrollView.ScrollY, false);
datePicker.Focus();
}
else
{
var date = action == "prev" ? previousDate : nextDate;
if (date == null)
{
return;
}
if (isFilterVisible)
{
ToggleFilterPanel(false);
}
queryDate = date;
PrepareLoad();
}
}
private bool OnCanDateTrigger(string action)
{
switch (action)
{
case "prev":
return previousEnabled;
case "next":
return nextEnabled;
default:
return dateEnabled;
}
}
private void DatePicker_Focused(object sender, FocusEventArgs e)
{
isDatePickerVisible = e.IsFocused;
}
private void DatePicker_DateSelected(object sender, DateChangedEventArgs e)
{
if (e.OldDate == DateTime.Today || IsLoading)
{
// first load or loading
return;
}
queryDate = e.NewDate.ToString("yyyyMMdd");
PrepareLoad();
}
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
ToggleFilterPanel(!isFilterVisible);
}
private void FlowLayout_MaxHeightChanged(object sender, HeightEventArgs e)
{
SetOffset(e.ContentHeight - scrollView.Bounds.Height - SCROLL_OFFSET);
}
protected override bool CheckRefresh()
{
if (nextPage == currentPage + 1)
{
currentPage = nextPage;
#if DEBUG
App.DebugPrint($"loading page {nextPage}");
#endif
return true;
}
return false;
}
private void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
{
var y = e.ScrollY;
if (IsScrollingDown(y))
{
// down
if (isFilterVisible)
{
ToggleFilterPanel(false);
}
}
if (isDatePickerVisible)
{
isDatePickerVisible = false;
datePicker.Unfocus();
}
OnScrolled(y);
}
private void Filter_Clicked(object sender, EventArgs e)
{
var query = QueryKey;
ToggleFilterPanel(false);
if (IsLoading)
{
return;
}
//if (lastQueryKey != query)
{
// query changed.
lastQueryKey = query;
#if DEBUG
App.DebugPrint($"query changed: {query}");
#endif
PrepareLoad();
}
}
private async void PrepareLoad()
{
await ScrollToTopAsync(scrollView);
// release
currentPage = 1;
previousEnabled = false;
dateEnabled = false;
nextEnabled = false;
ToolbarCommand.ChangeCanExecute();
StartLoad(true);
}
private void ReleaseCollection()
{
currentPage = 1;
InvalidateCollection();
}
}
}

View File

@ -0,0 +1,32 @@
<?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:Gallery.Illust"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:r="clr-namespace:Gallery.Resources"
x:Class="Gallery.Illust.RecommendsPage"
BackgroundColor="{DynamicResource WindowColor}"
Title="{r:Text Recommends}">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconRefresh}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView x:Name="scrollView" HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<StackLayout>
<ActivityIndicator x:Name="activityLoading" Margin="0, -40, 0, 0"
HeightRequest="40"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
<u:FlowLayout ItemsSource="{Binding Users}" IsVisible="{Binding UserRecommendsVisible}"
HorizontalOptions="Fill" Column="{Binding UserColumns}"
Margin="16" RowSpacing="16"
ItemTemplate="{StaticResource userCardView}"/>
<u:FlowLayout ItemsSource="{Binding Illusts}"
HorizontalOptions="Fill" Column="{Binding Columns}"
Margin="16" RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}"/>
</StackLayout>
</ScrollView>
</Grid>
</i:IllustDataCollectionPage>

View File

@ -0,0 +1,369 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Gallery.UI;
using Gallery.UI.Theme;
using Gallery.Utils;
using Xamarin.Forms;
namespace Gallery.Illust
{
public partial class RecommendsPage : IllustDataCollectionPage
{
public static readonly BindableProperty UsersProperty = BindableProperty.Create(
nameof(Users), typeof(List<IllustUserItem>), typeof(RecommendsPage));
public static readonly BindableProperty UserColumnsProperty = BindableProperty.Create(
nameof(UserColumns), typeof(int), typeof(RecommendsPage), 1);
public static readonly BindableProperty UserRecommendsVisibleProperty = BindableProperty.Create(
nameof(UserRecommendsVisible), typeof(bool), typeof(RecommendsPage));
public List<IllustUserItem> Users
{
get => (List<IllustUserItem>)GetValue(UsersProperty);
set => SetValue(UsersProperty, value);
}
public int UserColumns
{
get => (int)GetValue(UserColumnsProperty);
set => SetValue(UserColumnsProperty, value);
}
public bool UserRecommendsVisible
{
get => (bool)GetValue(UserRecommendsVisibleProperty);
set => SetValue(UserRecommendsVisibleProperty, value);
}
private IllustData illustData;
public RecommendsPage()
{
Resources.Add("cardView", GetCardViewTemplate());
Resources.Add("userCardView", GetUserCardViewTemplate());
InitializeComponent();
}
protected override bool NeedCookie => true;
protected override ActivityIndicator LoadingIndicator => activityLoading;
public override void OnUnload()
{
base.OnUnload();
Users = null;
}
private Image GetUserCardViewImage(int column, CornerMask masks, string source, string parameter)
{
Image image;
if (masks == CornerMask.None)
{
image = new Image();
}
else
{
image = new RoundImage
{
CornerRadius = 10,
CornerMasks = masks
};
}
image.HorizontalOptions = LayoutOptions.Fill;
image.Aspect = Aspect.AspectFill;
image.GestureRecognizers.Add(new TapGestureRecognizer
{
Command = commandIllustImageTapped
}
.Binding(TapGestureRecognizer.CommandParameterProperty, parameter));
image.SetBinding(Image.SourceProperty, source);
if (column > 0)
{
Grid.SetColumn(image, column);
}
Grid.SetRow(image, 1);
return image;
}
private DataTemplate GetUserCardViewTemplate()
{
return new DataTemplate(() =>
{
return new Grid
{
RowSpacing = 0,
ColumnSpacing = 0,
RowDefinitions =
{
new RowDefinition { Height = 40 },
new RowDefinition { Height = 120 }
},
ColumnDefinitions =
{
new ColumnDefinition(),
new ColumnDefinition(),
new ColumnDefinition()
},
Children =
{
// stacklayout: user
new Grid
{
ColumnDefinitions =
{
new ColumnDefinition { Width = 30 },
new ColumnDefinition()
},
Padding = new Thickness(8, 0, 8, 8),
Children =
{
// user icon
new CircleImage
{
WidthRequest = 30,
HeightRequest = 30,
Aspect = Aspect.AspectFill
}
.Binding(Image.SourceProperty, nameof(IllustUserItem.ProfileImage)),
// user name
new Label
{
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.Center,
LineBreakMode = LineBreakMode.TailTruncation,
FontSize = StyleDefinition.FontSizeSmall
}
.Binding(Label.TextProperty, nameof(IllustUserItem.UserName))
.DynamicResource(Label.TextColorProperty, ThemeBase.SubTextColor)
.GridColumn(1),
},
GestureRecognizers =
{
new TapGestureRecognizer
{
Command = commandUserTapped
}
.Binding(TapGestureRecognizer.CommandParameterProperty, ".")
}
}
.GridColumnSpan(3),
GetUserCardViewImage(0, CornerMask.Left,
$"{nameof(IllustUserItem.Image1Item)}.{nameof(IllustItem.Image)}",
nameof(IllustUserItem.Image1Item)),
GetUserCardViewImage(1, CornerMask.None,
$"{nameof(IllustUserItem.Image2Item)}.{nameof(IllustItem.Image)}",
nameof(IllustUserItem.Image2Item)),
GetUserCardViewImage(2, CornerMask.Right,
$"{nameof(IllustUserItem.Image3Item)}.{nameof(IllustItem.Image)}",
nameof(IllustUserItem.Image3Item))
}
};
});
}
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
int columns;
if (width > height)
{
columns = isPhone ? 2 : 3;
}
else
{
columns = isPhone ? 1 : 2;
}
if (UserColumns != columns)
{
UserColumns = columns;
#if DEBUG
App.DebugPrint($"change user columns to {columns}");
#endif
}
}
protected override void DoIllustsLoaded(IllustCollection collection, bool bottom)
{
//IsLoading = false;
if (illustData != null)
{
IllustCollection = collection;
Task.Run(() => DoLoadUserRecommendsData(illustData));
}
else
{
base.DoIllustsLoaded(collection, bottom);
}
}
protected override IEnumerable<IllustItem> DoGetIllustList(IllustData data, out int tag)
{
tag = 0;
if (data.body == null)
{
return null;
}
return data.body.page.recommend.ids.Select(id =>
data.body.thumbnails.illust.FirstOrDefault(l => (l.illustId ?? l.id) == id)?.ConvertToItem());
}
protected override IllustData DoLoadIllustData(bool force)
{
illustData = Stores.LoadIllustData(force);
return illustData;
}
private void DoLoadUserRecommendsData(IllustData data)
{
var r18 = Configs.IsOnR18;
var defaultImage = StyleDefinition.DownloadBackground;
var users = data.body.page.recommendUser.Select(u =>
{
var usrId = u.id.ToString();
var usr = data.body.users.FirstOrDefault(r => r.userId == usrId);
if (usr == null)
{
return null;
}
IllustItem item1 = null, item2 = null, item3 = null;
if (u.illustIds != null)
{
var length = u.illustIds.Length;
if (length > 0)
{
var id = u.illustIds[0];
item1 = data.body.thumbnails.illust.FirstOrDefault(l => (l.illustId ?? l.id) == id)?.ConvertToItem(defaultImage);
}
if (length > 1)
{
var id = u.illustIds[1];
item2 = data.body.thumbnails.illust.FirstOrDefault(l => (l.illustId ?? l.id) == id)?.ConvertToItem(defaultImage);
}
if (length > 2)
{
var id = u.illustIds[2];
item3 = data.body.thumbnails.illust.FirstOrDefault(l => (l.illustId ?? l.id) == id)?.ConvertToItem(defaultImage);
}
}
if (!r18)
{
if (item1?.IsRestrict == true ||
item2?.IsRestrict == true ||
item3?.IsRestrict == true)
{
return null;
}
}
return new IllustUserItem
{
UserId = usrId,
UserName = usr.name,
ProfileUrl = usr.image,
Image1Item = item1,
Image2Item = item2,
Image3Item = item3
};
}).Where(u => u != null);
var list = new List<IllustUserItem>(users);
activityLoading.Animate("margin", top =>
{
activityLoading.Margin = new Thickness(0, top, 0, 0);
},
16, -40, easing: Easing.CubicIn, finished: (v, r) =>
{
IsLoading = false;
UserRecommendsVisible = list.Count > 0;
#if __IOS__
Device.StartTimer(TimeSpan.FromMilliseconds(48), () =>
#else
Device.StartTimer(TimeSpan.FromMilliseconds(150), () =>
#endif
{
Users = list;
Illusts = IllustCollection;
return false;
});
});
DoLoadUserRecommendsImages(list);
}
private void DoLoadUserRecommendsImages(List<IllustUserItem> users)
{
foreach (var user in users)
{
if (user.ProfileUrl != null)
{
var userImage = Stores.LoadUserProfileImage(user.ProfileUrl, true);
if (userImage != null)
{
user.ProfileImage = userImage;
}
}
Task.WaitAll(
Task.Run(() => DoLoadUserRecommendsImage(user.Image1Item)),
Task.Run(() => DoLoadUserRecommendsImage(user.Image2Item)),
Task.Run(() => DoLoadUserRecommendsImage(user.Image3Item)));
}
}
private void DoLoadUserRecommendsImage(IllustItem illust)
{
if (illust.ImageUrl != null)
{
var url = Configs.GetThumbnailUrl(illust.ImageUrl);
var image = Stores.LoadPreviewImage(url, false);
if (image == null)
{
image = Stores.LoadThumbnailImage(url, true);
}
if (image != null)
{
illust.Image = image;
}
}
}
private async void Refresh_Clicked(object sender, EventArgs e)
{
if (IsLoading)
{
return;
}
await ScrollToTopAsync(scrollView);
StartLoad(true);
}
}
public class IllustUserItem : BindableObject, IIllustItem
{
//public static readonly BindableProperty Image1Property = BindableProperty.Create(
// nameof(Image1), typeof(ImageSource), typeof(IllustUserItem));
public static readonly BindableProperty ProfileImageProperty = BindableProperty.Create(
nameof(ProfileImage), typeof(ImageSource), typeof(IllustUserItem));
//public ImageSource Image1
//{
// get => (ImageSource)GetValue(Image1Property);
// set => SetValue(Image1Property, value);
//}
public ImageSource ProfileImage
{
get => (ImageSource)GetValue(ProfileImageProperty);
set => SetValue(ProfileImageProperty, value);
}
public string UserId { get; set; }
public string UserName { get; set; }
public IllustItem Image1Item { get; set; }
public IllustItem Image2Item { get; set; }
public IllustItem Image3Item { get; set; }
public string ProfileUrl { get; set; }
public bool IsFavorite { get; }
public string BookmarkId { get; }
}
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<i:IllustRecommendsCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i="clr-namespace:Gallery.Illust"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:r="clr-namespace:Gallery.Resources"
x:Class="Gallery.Illust.RelatedIllustsPage"
Title="{r:Text RelatedIllusts}"
BackgroundColor="{DynamicResource WindowColor}">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconRefresh}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"
HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<StackLayout>
<ActivityIndicator x:Name="activityLoading" Margin="0, -40, 0, 0"
HeightRequest="40"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
<u:FlowLayout ItemsSource="{Binding Illusts}" MaxHeightChanged="FlowLayout_MaxHeightChanged"
HorizontalOptions="Fill" Column="{Binding Columns}"
Margin="16" RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}"/>
<ActivityIndicator x:Name="activityBottomLoading" Margin="0, -10, 0, 16"
IsRunning="{Binding IsBottomLoading}"
IsVisible="{Binding IsBottomLoading}"/>
</StackLayout>
</ScrollView>
</Grid>
</i:IllustRecommendsCollectionPage>

View File

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gallery.UI;
using Gallery.Utils;
using Xamarin.Forms;
namespace Gallery.Illust
{
public partial class RelatedIllustsPage : IllustRecommendsCollectionPage
{
private const int STEP = 18;
private readonly IllustItem illustItem;
private int startIndex;
private int nextIndex;
private string[] illustIds;
public RelatedIllustsPage(IllustItem item)
{
illustItem = item;
Resources.Add("cardView", GetCardViewTemplate());
InitializeComponent();
startIndex = -1;
nextIndex = 0;
}
protected override void OnAppearing()
{
if (startIndex != -1 && Illusts == null)
{
startIndex = -1;
}
base.OnAppearing();
}
protected override bool IsAutoReload => false;
protected override ActivityIndicator LoadingIndicator => activityLoading;
protected override IEnumerable<IllustItem> DoGetIllustList(IllustRecommendsData data, out int tag)
{
tag = startIndex;
if (data.body == null)
{
return null;
}
return data.body.illusts.Where(i => i.url != null).Select(i => i.ConvertToItem());
}
protected override IllustRecommendsData DoLoadIllustData(bool force)
{
IllustRecommendsData data;
if (startIndex < 0)
{
// init
data = Stores.LoadIllustRecommendsInitData(illustItem.Id);
if (data == null || data.body == null)
{
return null;
}
illustIds = data.body.nextIds;
}
else
{
if (illustIds == null || startIndex >= illustIds.Length)
{
return null;
}
var ids = illustIds.Skip(startIndex).Take(STEP).ToArray();
data = Stores.LoadIllustRecommendsListData(illustItem.Id, ids);
nextIndex = startIndex + STEP;
if (ids.Length == 0 || nextIndex >= illustIds.Length)
{
// done
#if DEBUG
App.DebugPrint($"download completed: {startIndex}");
#endif
startIndex = nextIndex;
}
}
return data;
}
private void FlowLayout_MaxHeightChanged(object sender, HeightEventArgs e)
{
SetOffset(e.ContentHeight - scrollView.Bounds.Height - SCROLL_OFFSET);
}
protected override bool CheckRefresh()
{
if (nextIndex > startIndex)
{
startIndex = nextIndex;
return true;
}
return false;
}
private void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
{
var y = e.ScrollY;
OnScrolled(y);
}
private async void Refresh_Clicked(object sender, EventArgs e)
{
if (IsLoading)
{
return;
}
await ScrollToTopAsync(scrollView);
startIndex = -1;
nextIndex = 0;
illustIds = null;
StartLoad(true);
}
}
}

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<i:IllustUserDataCollectionPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i="clr-namespace:Gallery.Illust"
xmlns:u="clr-namespace:Gallery.UI"
x:Class="Gallery.Illust.UserIllustPage"
BackgroundColor="{DynamicResource WindowColor}">
<Shell.TitleView>
<StackLayout Orientation="Horizontal">
<u:CircleImage Aspect="AspectFill" Source="{Binding UserIcon}"
VerticalOptions="Center"
WidthRequest="34" HeightRequest="34" >
<u:CircleImage.Margin>
<OnPlatform x:TypeArguments="Thickness" Android="0, 5, 0, 5"/>
</u:CircleImage.Margin>
</u:CircleImage>
<Label Text="{Binding Title}" Margin="10, 0, 0, 0"
VerticalOptions="Center" LineBreakMode="TailTruncation"
TextColor="{DynamicResource TextColor}"/>
</StackLayout>
</Shell.TitleView>
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconRefresh}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"
HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<StackLayout>
<ActivityIndicator x:Name="activityLoading" Margin="0, -40, 0, 0"
HeightRequest="40"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
<u:FlowLayout ItemsSource="{Binding Illusts}" MaxHeightChanged="FlowLayout_MaxHeightChanged"
HorizontalOptions="Fill" Column="{Binding Columns}"
Margin="16" RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}"/>
<ActivityIndicator x:Name="activityBottomLoading" Margin="0, -10, 0, 16"
IsRunning="{Binding IsBottomLoading}"
IsVisible="{Binding IsBottomLoading}"/>
</StackLayout>
</ScrollView>
</Grid>
</i:IllustUserDataCollectionPage>

View File

@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gallery.UI;
using Gallery.Utils;
using Xamarin.Forms;
namespace Gallery.Illust
{
public partial class UserIllustPage : IllustUserDataCollectionPage
{
private const int STEP = 48;
public static readonly BindableProperty UserIconProperty = BindableProperty.Create(
nameof(UserIcon), typeof(ImageSource), typeof(UserIllustPage));
public ImageSource UserIcon
{
get => (ImageSource)GetValue(UserIconProperty);
set => SetValue(UserIconProperty, value);
}
public IIllustItem UserItem { get; }
private int startIndex;
private int nextIndex;
private string[] illustIds;
public UserIllustPage(IIllustItem item)
{
UserItem = item;
UserIcon = item.ProfileImage;
Title = item.UserName;
Resources.Add("cardView", GetCardViewTemplate(true));
InitializeComponent();
startIndex = -1;
nextIndex = 0;
}
protected override void OnAppearing()
{
if (startIndex != -1 && Illusts == null)
{
startIndex = -1;
}
base.OnAppearing();
}
protected override bool IsAutoReload => false;
protected override ActivityIndicator LoadingIndicator => activityLoading;
protected override IEnumerable<IllustItem> DoGetIllustList(IllustUserData data, out int tag)
{
tag = startIndex;
if (data.body == null)
{
return null;
}
return data.body.works.Select(i => i.Value?.ConvertToItem());
}
protected override IllustUserData DoLoadIllustData(bool force)
{
var userId = UserItem.UserId;
if (startIndex < 0)
{
var init = Stores.LoadIllustUserInitData(userId);
if (init == null || init.body == null)
{
return null;
}
illustIds = init.body.illusts.Keys.OrderByDescending(i => int.TryParse(i, out int id) ? id : -1).ToArray();
#if DEBUG
App.DebugPrint($"user has ({illustIds.Length}) illusts.");
#endif
startIndex = 0;
}
if (illustIds == null || startIndex >= illustIds.Length)
{
return null;
}
var ids = illustIds.Skip(startIndex).Take(STEP).ToArray();
var data = Stores.LoadIllustUserData(userId, ids, startIndex == 0);
nextIndex = startIndex + STEP;
if (ids.Length == 0 || nextIndex >= illustIds.Length)
{
// done
#if DEBUG
App.DebugPrint($"download completed: {startIndex}");
#endif
startIndex = nextIndex;
}
return data;
}
private void FlowLayout_MaxHeightChanged(object sender, HeightEventArgs e)
{
SetOffset(e.ContentHeight - scrollView.Bounds.Height - SCROLL_OFFSET);
}
protected override bool CheckRefresh()
{
if (nextIndex > startIndex)
{
startIndex = nextIndex;
return true;
}
return false;
}
private void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
{
var y = e.ScrollY;
OnScrolled(y);
}
private async void Refresh_Clicked(object sender, EventArgs e)
{
if (IsLoading)
{
return;
}
await ScrollToTopAsync(scrollView);
startIndex = -1;
nextIndex = 0;
illustIds = null;
StartLoad(true);
}
}
}

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<u:AdaptedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
x:Class="Gallery.Illust.ViewIllustPage"
ios:Page.UseSafeArea="False"
Shell.TabBarIsVisible="False"
BackgroundColor="{DynamicResource WindowColor}">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="Favorite_Clicked"
IconImageSource="{Binding FavoriteIcon}"/>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconRefresh}"/>
<ToolbarItem Order="Primary" Clicked="More_Clicked"
IconImageSource="{DynamicResource FontIconMore}"/>
</ContentPage.ToolbarItems>
<Grid RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<CarouselView ItemsSource="{Binding Illusts}" HorizontalScrollBarVisibility="Never"
Position="{Binding CurrentPage}"
ItemTemplate="{StaticResource carouselView}"
IsScrollAnimated="{Binding IsScrollAnimated}">
<CarouselView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="20"/>
</CarouselView.ItemsLayout>
</CarouselView>
<Grid Margin="{Binding PageTopMargin}" VerticalOptions="Start">
<ProgressBar x:Name="progress" IsVisible="{Binding ProgressVisible}"
Progress="0.05">
<ProgressBar.Margin>
<OnPlatform x:TypeArguments="Thickness" Android="0, -6, 0, 0"/>
</ProgressBar.Margin>
</ProgressBar>
</Grid>
<Grid Margin="{Binding PageTopMargin}" IsVisible="{Binding IsPageVisible}"
HorizontalOptions="End" VerticalOptions="Start">
<u:RoundLabel Text="{Binding PagePositionText}"
BackgroundColor="{DynamicResource MaskColor}" Margin="0, 6, 6, 0"
Padding="6, 4" CornerRadius="6"
FontSize="Micro" TextColor="White"/>
</Grid>
<u:RoundLabel Text="{Binding AnimeStatus}"
BackgroundColor="{DynamicResource MaskColor}" Margin="0, 0, 6, 6"
Padding="13, 12, 0, 0" CornerRadius="22"
WidthRequest="44" HeightRequest="44"
HorizontalOptions="End" VerticalOptions="End"
FontFamily="{DynamicResource IconSolidFontFamily}"
FontSize="20" TextColor="White"
IsVisible="{Binding IsAnimateSliderVisible}"/>
<Slider Grid.Row="1" VerticalOptions="End"
Margin="{DynamicResource ScreenBottomPadding}"
MinimumTrackColor="{DynamicResource TintColor}"
IsEnabled="{Binding IsAnimateSliderEnabled}"
IsVisible="{Binding IsAnimateSliderVisible}"
Value="{Binding CurrentAnimeFrame, Mode=TwoWay}"
Maximum="{Binding MaximumFrame}"/>
</Grid>
</u:AdaptedPage>

View File

@ -0,0 +1,897 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Gallery.Resources;
using Gallery.UI;
using Gallery.UI.Theme;
using Gallery.Utils;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.Illust
{
[QueryProperty("IllustId", "id")]
public partial class ViewIllustPage : AdaptedPage
{
#region - Properties -
public static readonly BindableProperty FavoriteIconProperty = BindableProperty.Create(
nameof(FavoriteIcon), typeof(ImageSource), typeof(ViewIllustPage));
public ImageSource FavoriteIcon
{
get => (ImageSource)GetValue(FavoriteIconProperty);
set => SetValue(FavoriteIconProperty, value);
}
public static readonly BindableProperty IllustsProperty = BindableProperty.Create(
nameof(Illusts), typeof(IllustDetailItem[]), typeof(ViewIllustPage));
public static readonly BindableProperty IsPageVisibleProperty = BindableProperty.Create(
nameof(IsPageVisible), typeof(bool), typeof(ViewIllustPage));
public static readonly BindableProperty PagePositionTextProperty = BindableProperty.Create(
nameof(PagePositionText), typeof(string), typeof(ViewIllustPage));
public static readonly BindableProperty CurrentPageProperty = BindableProperty.Create(
nameof(CurrentPage), typeof(int), typeof(ViewIllustPage), propertyChanged: OnCurrentPagePropertyChanged);
public static readonly BindableProperty IsScrollAnimatedProperty = BindableProperty.Create(
nameof(IsScrollAnimated), typeof(bool), typeof(ViewIllustPage), true);
public IllustDetailItem[] Illusts
{
get => (IllustDetailItem[])GetValue(IllustsProperty);
set => SetValue(IllustsProperty, value);
}
public bool IsPageVisible
{
get => (bool)GetValue(IsPageVisibleProperty);
set => SetValue(IsPageVisibleProperty, value);
}
public string PagePositionText
{
get => (string)GetValue(PagePositionTextProperty);
set => SetValue(PagePositionTextProperty, value);
}
public int CurrentPage
{
get => (int)GetValue(CurrentPageProperty);
set => SetValue(CurrentPageProperty, value);
}
public bool IsScrollAnimated
{
get => (bool)GetValue(IsScrollAnimatedProperty);
set => SetValue(IsScrollAnimatedProperty, value);
}
private static void OnCurrentPagePropertyChanged(BindableObject obj, object old, object @new)
{
var page = (ViewIllustPage)obj;
var index = (int)@new;
var items = page.Illusts;
var length = items.Length;
page.PagePositionText = $"{index + 1}/{length}";
}
public static readonly BindableProperty AnimeStatusProperty = BindableProperty.Create(
nameof(AnimeStatus), typeof(string), typeof(ViewIllustPage), StyleDefinition.IconPlay);
public static readonly BindableProperty IsAnimateSliderVisibleProperty = BindableProperty.Create(
nameof(IsAnimateSliderVisible), typeof(bool), typeof(ViewIllustPage));
public static readonly BindableProperty IsAnimateSliderEnabledProperty = BindableProperty.Create(
nameof(IsAnimateSliderEnabled), typeof(bool), typeof(ViewIllustPage));
public static readonly BindableProperty CurrentAnimeFrameProperty = BindableProperty.Create(
nameof(CurrentAnimeFrame), typeof(double), typeof(ViewIllustPage), propertyChanged: OnCurrentAnimeFramePropertyChanged);
public static readonly BindableProperty MaximumFrameProperty = BindableProperty.Create(
nameof(MaximumFrame), typeof(double), typeof(ViewIllustPage), 1.0);
private static void OnCurrentAnimeFramePropertyChanged(BindableObject obj, object old, object @new)
{
var page = (ViewIllustPage)obj;
if (page.ugoira != null && page.IsAnimateSliderEnabled)
{
var frame = (double)@new;
page.ugoira.ToggleFrame((int)frame);
}
}
public string AnimeStatus
{
get => (string)GetValue(AnimeStatusProperty);
set => SetValue(AnimeStatusProperty, value);
}
public bool IsAnimateSliderVisible
{
get => (bool)GetValue(IsAnimateSliderVisibleProperty);
set => SetValue(IsAnimateSliderVisibleProperty, value);
}
public bool IsAnimateSliderEnabled
{
get => (bool)GetValue(IsAnimateSliderEnabledProperty);
set => SetValue(IsAnimateSliderEnabledProperty, value);
}
public double CurrentAnimeFrame
{
get => (double)GetValue(CurrentAnimeFrameProperty);
set => SetValue(CurrentAnimeFrameProperty, value);
}
public double MaximumFrame
{
get => (double)GetValue(MaximumFrameProperty);
set => SetValue(MaximumFrameProperty, value);
}
public static readonly BindableProperty ProgressVisibleProperty = BindableProperty.Create(
nameof(ProgressVisible), typeof(bool), typeof(ViewIllustPage));
public bool ProgressVisible
{
get => (bool)GetValue(ProgressVisibleProperty);
set => SetValue(ProgressVisibleProperty, value);
}
#endregion
public IllustItem IllustItem { get; private set; }
private readonly ImageSource fontIconLove;
private readonly ImageSource fontIconNotLove;
private readonly ImageSource fontIconCircleLove;
private bool favoriteChanged;
private IllustUgoiraData ugoiraData;
private Ugoira ugoira;
private ParallelTask task;
private readonly object sync = new object();
private int downloaded = 0;
private int pageCount;
private bool isPreloading;
public ViewIllustPage(IllustItem illust)
{
IllustItem = illust;
fontIconLove = (ImageSource)Application.Current.Resources[ThemeBase.FontIconLove];
fontIconNotLove = (ImageSource)Application.Current.Resources[ThemeBase.FontIconNotLove];
fontIconCircleLove = (ImageSource)Application.Current.Resources[ThemeBase.FontIconCircleLove];
if (illust != null)
{
pageCount = illust.PageCount;
RefreshInformation(illust, pageCount);
}
Resources.Add("carouselView", GetCarouseTemplate());
BindingContext = this;
InitializeComponent();
if (illust != null)
{
LoadIllust(illust);
}
}
protected override void OnAppearing()
{
base.OnAppearing();
Screen.SetHomeIndicatorAutoHidden(Shell.Current, true);
}
protected override void OnDisappearing()
{
Screen.SetHomeIndicatorAutoHidden(Shell.Current, false);
base.OnDisappearing();
if (ugoira != null)
{
IllustItem.IsPlaying = false;
ugoira.FrameChanged -= OnUgoiraFrameChanged;
ugoira.TogglePlay(false);
ugoira.Dispose();
ugoira = null;
}
if (favoriteChanged)
{
Stores.SaveFavoritesIllusts();
}
}
public override void OnUnload()
{
if (task != null)
{
task.Dispose();
task = null;
}
}
#if __IOS__
protected override void OnPageTopMarginChanged(Thickness old, Thickness @new)
{
var illusts = Illusts;
if (illusts != null)
{
for (var i = 0; i < illusts.Length; i++)
{
illusts[i].TopPadding = @new;
}
}
}
#endif
private DataTemplate GetCarouseTemplate()
{
return new DataTemplate(() =>
{
// image
var image = new Image
{
HorizontalOptions = LayoutOptions.Fill,
VerticalOptions = LayoutOptions.Fill,
Aspect = Aspect.AspectFit
}
.Binding(Image.SourceProperty, nameof(IllustDetailItem.Image));
// downloading
var downloading = new Frame
{
HasShadow = false,
Margin = default,
Padding = new Thickness(20),
CornerRadius = 8,
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center,
Content = new ActivityIndicator()
.Binding(IsVisibleProperty, nameof(IllustDetailItem.Loading))
.Binding(ActivityIndicator.IsRunningProperty, nameof(IllustDetailItem.Loading))
.DynamicResource(ActivityIndicator.ColorProperty, ThemeBase.WindowColor)
}
.Binding(IsVisibleProperty, nameof(IllustDetailItem.Loading))
.DynamicResource(BackgroundColorProperty, ThemeBase.MaskColor);
// loading original
var original = new ActivityIndicator
{
Margin = new Thickness(10),
HorizontalOptions = LayoutOptions.Start,
VerticalOptions = LayoutOptions.Start
}
.Binding(IsVisibleProperty, nameof(IllustDetailItem.Downloading))
.Binding(ActivityIndicator.IsRunningProperty, nameof(IllustDetailItem.Downloading))
.DynamicResource(ActivityIndicator.ColorProperty, ThemeBase.TextColor);
var tap = new TapGestureRecognizer();
tap.Tapped += Image_Tapped;
var tapPrevious = new TapGestureRecognizer();
tapPrevious.Tapped += TapPrevious_Tapped;
var tapNext = new TapGestureRecognizer();
tapNext.Tapped += TapNext_Tapped;
var grid = new Grid
{
Children =
{
// 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,
// loading original
original
}
};
#if __IOS__
grid.SetBinding(Xamarin.Forms.Layout.PaddingProperty, nameof(IllustDetailItem.TopPadding));
#endif
return grid;
});
}
private void LoadIllust(IllustItem illust)
{
if (illust == null)
{
return;
}
var items = new IllustDetailItem[illust.PageCount];
#if __IOS__
var topMargin = PageTopMargin;
#endif
for (var i = 0; i < items.Length; i++)
{
items[i] = new IllustDetailItem
{
#if __IOS__
TopPadding = topMargin,
#endif
Id = illust.Id
};
if (i == 0)
{
items[i].Loading = true;
items[i].Image = illust.Image;
}
}
Illusts = items;
Task.Run(() => DoLoadImages());
}
private void DoLoadImages(bool force = false)
{
var illustItem = IllustItem;
var pages = Stores.LoadIllustPageData(illustItem.Id, out string error, force);
if (pages == null)
{
App.DebugError("illustPage.load", $"failed to load illust page data, id: {illustItem.Id}");
if (error != null)
{
MainThread.BeginInvokeOnMainThread(() =>
{
DisplayAlert(ResourceHelper.Title, error, ResourceHelper.Ok);
});
}
return;
}
var items = Illusts;
var reload = false;
if (pages.body.Length > items.Length)
{
#if DEBUG
App.DebugPrint($"local page count ({items.Length}) is not equals the remote one ({pages.body.Length})");
#endif
var tmp = new IllustDetailItem[pages.body.Length];
items.CopyTo(items, 0);
#if __IOS__
var topMargin = PageTopMargin;
#endif
for (var i = items.Length; i < tmp.Length; i++)
{
tmp[i] = new IllustDetailItem
{
Id = illustItem.Id,
#if __IOS__
TopPadding = topMargin,
#endif
Loading = i == 0
};
}
Illusts = items = tmp;
reload = true;
}
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
var p = pages.body[i];
item.PreviewUrl = p.urls.regular;
item.OriginalUrl = p.urls.original;
if (i == 0 && illustItem.ImageUrl == null)
{
// maybe open from a link
reload = true;
}
}
if (reload)
{
DoForcePreload(false);
}
if (task != null)
{
task.Dispose();
task = null;
}
task = ParallelTask.Start("view.illusts", 0, items.Length, 2, i =>
{
DoLoadImage(i);
return true;
});
if (illustItem.IsAnimeVisible)
{
// anime
ugoiraData = Stores.LoadIllustUgoiraData(illustItem.Id);
if (ugoiraData != null && ugoiraData.body != null)
{
var length = ugoiraData.body.frames.Length;
MaximumFrame = length > 0 ? length : 1;
}
}
}
private void DoLoadImage(int index, bool force = false)
{
var items = Illusts;
if (index < 0 || (!force && index >= items.Length))
{
App.DebugPrint($"invalid index: {index}");
return;
}
var item = items[index];
if (index > 0 && !force)
{
if (item.Loading || item.Image != null)
{
#if DEBUG
App.DebugPrint($"skipped, loading or already loaded, index: {index}, loading: {item.Loading}");
#endif
return;
}
}
item.Loading = true;
var image = Stores.LoadPreviewImage(item.PreviewUrl, true, IllustItem.Id, force: force);
if (image != null)
{
item.Image = image;
if(index == 0)
{
IllustItem.Image = image;
}
}
item.Loading = false;
RefreshProgress();
}
private void RefreshProgress()
{
if (pageCount <= 1)
{
return;
}
lock (sync)
{
downloaded++;
}
if (downloaded >= pageCount)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
ViewExtensions.CancelAnimations(progress);
await progress.ProgressTo(1, 250, Easing.CubicIn);
await progress.FadeTo(0, easing: Easing.CubicIn);
ProgressVisible = false;
});
}
else
{
var val = downloaded / (float)pageCount;
MainThread.BeginInvokeOnMainThread(() =>
{
ViewExtensions.CancelAnimations(progress);
progress.ProgressTo(val, 250, Easing.CubicIn);
});
}
}
private void TapPrevious_Tapped(object sender, EventArgs e)
{
var index = CurrentPage;
if (index > 0)
{
IsScrollAnimated = false;
CurrentPage = index - 1;
IsScrollAnimated = true;
}
}
private void TapNext_Tapped(object sender, EventArgs e)
{
var index = CurrentPage;
var illusts = Illusts;
if (illusts != null && index < illusts.Length - 1)
{
IsScrollAnimated = false;
CurrentPage = index + 1;
IsScrollAnimated = true;
}
}
private async void Favorite_Clicked(object sender, EventArgs e)
{
var favorites = Stores.Favorites;
var illust = IllustItem;
var index = favorites.FindIndex(i => i.Id == illust.Id);
var bookmarkId = illust.BookmarkId;
bool add = index < 0 && bookmarkId == null;
if (add)
{
illust.IsFavorite = true;
illust.FavoriteDateUtc = DateTime.UtcNow;
favorites.Insert(0, illust);
FavoriteIcon = fontIconLove;
}
else
{
if (index >= 0)
{
var item = favorites[index];
if (bookmarkId == null && item.BookmarkId != null)
{
bookmarkId = item.BookmarkId;
illust.BookmarkId = bookmarkId;
}
favorites.RemoveAt(index);
}
illust.IsFavorite = false;
FavoriteIcon = bookmarkId == null ?
fontIconNotLove :
fontIconCircleLove;
}
favoriteChanged = true;
if (Configs.SyncFavType == SyncType.None)
{
return;
}
if (Configs.Cookie == null)
{
return;
}
if (!add && string.IsNullOrEmpty(bookmarkId))
{
return;
}
if (Configs.SyncFavType == SyncType.Prompt)
{
var ok = await DisplayAlert(
ResourceHelper.Title,
ResourceHelper.ConfirmSyncFavorite,
ResourceHelper.Yes,
ResourceHelper.No);
if (!ok)
{
return;
}
}
if (add)
{
var id = await Task.Run(() => Stores.AddBookmark(illust.Id));
if (id != null)
{
bookmarkId = id;
illust.BookmarkId = bookmarkId;
FavoriteIcon = fontIconCircleLove;
}
}
else
{
var result = await Task.Run(() => Stores.DeleteBookmark(bookmarkId));
if (result)
{
FavoriteIcon = fontIconNotLove;
}
}
// immediately save after changing remote
Stores.SaveFavoritesIllusts();
favoriteChanged = false;
}
private void Image_Tapped(object sender, EventArgs e)
{
if (ugoiraData == null)
{
return;
}
var illustItem = IllustItem;
if (ugoira != null)
{
var playing = !ugoira.IsPlaying;
AnimeStatus = playing ? StyleDefinition.IconPause : StyleDefinition.IconPlay;
IsAnimateSliderEnabled = !playing;
ugoira.TogglePlay(playing);
illustItem.IsPlaying = playing;
}
else if (((VisualElement)sender).BindingContext is IllustDetailItem item)
{
if (illustItem.IsPlaying || !illustItem.IsAnimeVisible)
{
return;
}
ugoira = new Ugoira(ugoiraData, item);
ugoira.FrameChanged += OnUgoiraFrameChanged;
AnimeStatus = StyleDefinition.IconPause;
illustItem.IsPlaying = true;
ugoira.TogglePlay(true);
}
}
private void OnUgoiraFrameChanged(object sender, UgoiraEventArgs e)
{
e.DetailItem.Image = e.Image;
CurrentAnimeFrame = e.FrameIndex;
}
private void RefreshInformation(IllustItem item, int count)
{
Title = item.Title;
FavoriteIcon = item.BookmarkId != null ?
fontIconCircleLove :
Stores.Favorites.Any(i => i.Id == item.Id) ?
fontIconLove :
fontIconNotLove;
IsAnimateSliderVisible = item.IsAnimeVisible;
if (count > 1)
{
IsPageVisible = true;
ProgressVisible = true;
PagePositionText = $"{CurrentPage + 1}/{count}";
}
}
private void DoForcePreload(bool force)
{
isPreloading = true;
var illustItem = IllustItem;
if (force)
{
var illusts = Illusts;
var currentPage = CurrentPage;
if (currentPage >= 0 && illusts != null && currentPage < illusts.Length)
{
illusts[currentPage].Loading = true;
}
}
// force to reload
var preload = Stores.LoadIllustPreloadData(illustItem.Id, true, force: force);
if (preload != null && preload.illust.TryGetValue(illustItem.Id, out var illust))
{
illust.CopyToItem(illustItem);
pageCount = illustItem.PageCount;
MainThread.BeginInvokeOnMainThread(() =>
{
RefreshInformation(illustItem, pageCount);
});
if (preload.user.TryGetValue(illust.userId, out var user))
{
illustItem.ProfileUrl = user.image;
}
}
isPreloading = false;
}
private void Refresh_Clicked(object sender, EventArgs e)
{
if (isPreloading)
{
return;
}
Task.Run(() =>
{
DoForcePreload(true);
DoLoadImage(CurrentPage, true);
});
}
private async void More_Clicked(object sender, EventArgs e)
{
int p = CurrentPage;
var illusts = Illusts;
if (illusts == null || p < 0 || p >= illusts.Length)
{
return;
}
var item = illusts[p];
List<string> extras = new List<string>();
var share = ResourceHelper.Share;
var preview = Stores.GetPreviewImagePath(item.PreviewUrl);
if (preview != null)
{
extras.Add(share);
}
var userDetail = ResourceHelper.UserDetail;
extras.Add(userDetail);
var related = ResourceHelper.RelatedIllusts;
extras.Add(related);
#if __IOS__
var exportVideo = ResourceHelper.ExportVideo;
if (IsAnimateSliderVisible)
{
extras.Add(exportVideo);
}
#endif
var saveOriginal = ResourceHelper.SaveOriginal;
var illustItem = IllustItem;
var result = await DisplayActionSheet(
$"{illustItem.Title} (id: {illustItem.Id})",
ResourceHelper.Cancel,
saveOriginal,
extras.ToArray());
if (result == saveOriginal)
{
SaveOriginalImage(item);
}
else if (result == share)
{
await Share.RequestAsync(new ShareFileRequest
{
Title = illustItem.Title,
File = new ShareFile(preview)
});
}
else if (result == userDetail)
{
var page = new UserIllustPage(illustItem);
await Navigation.PushAsync(page);
}
else if (result == related)
{
var page = new RelatedIllustsPage(illustItem);
await Navigation.PushAsync(page);
}
#if __IOS__
else if (result == exportVideo)
{
ExportVideo();
}
#endif
}
#if __IOS__
private async void ExportVideo()
{
string msg = ResourceHelper.CantExportVideo;
if (ugoira != null && ugoiraData != null && ugoiraData.body != null)
{
if (Stores.CheckUgoiraVideo(ugoiraData.body.originalSrc))
{
var flag = await DisplayAlert(ResourceHelper.Operation,
ResourceHelper.AlreadySavedVideo,
ResourceHelper.Yes,
ResourceHelper.No);
if (!flag)
{
return;
}
}
var status = await Permissions.CheckStatusAsync<Permissions.Photos>();
if (status != PermissionStatus.Granted)
{
status = await Permissions.RequestAsync<Permissions.Photos>();
if (status != PermissionStatus.Granted)
{
App.DebugPrint("access denied to gallery.");
return;
}
}
var success = await Task.Run(ugoira.ExportVideo); // ugoira.ExportGif
if (success != null)
{
#if DEBUG
msg = ResourceHelper.ExportSuccess;
#else
var result = await FileStore.SaveVideoToGalleryAsync(success);
msg = result ?? ResourceHelper.ExportSuccess;
#endif
}
}
await DisplayAlert(ResourceHelper.Title, msg, ResourceHelper.Ok);
}
#endif
private async void SaveOriginalImage(IllustDetailItem item)
{
if (Stores.CheckIllustImage(item.OriginalUrl))
{
var flag = await DisplayAlert(ResourceHelper.Operation,
ResourceHelper.AlreadySavedQuestion,
ResourceHelper.Yes,
ResourceHelper.No);
if (!flag)
{
return;
}
}
var status = await Permissions.CheckStatusAsync<Permissions.Photos>();
if (status != PermissionStatus.Granted)
{
status = await Permissions.RequestAsync<Permissions.Photos>();
if (status != PermissionStatus.Granted)
{
App.DebugPrint("access denied to gallery.");
return;
}
}
if (item == null || item.Downloading)
{
return;
}
item.Downloading = true;
_ = Task.Run(() => DoLoadOriginalImage(item));
}
private void DoLoadOriginalImage(IllustDetailItem item)
{
var image = Stores.LoadIllustImage(item.OriginalUrl);
if (image != null)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
var result = await FileStore.SaveImageToGalleryAsync(image);
string message = result ?? ResourceHelper.SaveSuccess;
await DisplayAlert(ResourceHelper.Title, message, ResourceHelper.Ok);
});
}
item.Downloading = false;
}
}
public class IllustDetailItem : BindableObject
{
public static readonly BindableProperty ImageProperty = BindableProperty.Create(
nameof(Image), typeof(ImageSource), typeof(IllustDetailItem));
public static readonly BindableProperty LoadingProperty = BindableProperty.Create(
nameof(Loading), typeof(bool), typeof(IllustDetailItem));
public static readonly BindableProperty DownloadingProperty = BindableProperty.Create(
nameof(Downloading), typeof(bool), typeof(IllustDetailItem));
#if __IOS__
public static readonly BindableProperty TopPaddingProperty = BindableProperty.Create(
nameof(TopPadding), typeof(Thickness), typeof(IllustDetailItem));
public Thickness TopPadding
{
get => (Thickness)GetValue(TopPaddingProperty);
set => SetValue(TopPaddingProperty, value);
}
#endif
public ImageSource Image
{
get => (ImageSource)GetValue(ImageProperty);
set => SetValue(ImageProperty, value);
}
public bool Loading
{
get => (bool)GetValue(LoadingProperty);
set => SetValue(LoadingProperty, value);
}
public bool Downloading
{
get => (bool)GetValue(DownloadingProperty);
set => SetValue(DownloadingProperty, value);
}
public string Id { get; set; }
public string PreviewUrl { get; set; }
public string OriginalUrl { get; set; }
}
}

39
Gallery/Login/HybridWebView.cs Executable file
View File

@ -0,0 +1,39 @@
using System;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.Login
{
public class HybridWebView : WebView
{
public static readonly BindableProperty UriProperty = BindableProperty.Create(
nameof(Uri),
typeof(string),
typeof(HybridWebView));
public static readonly BindableProperty UserAgentProperty = BindableProperty.Create(
nameof(UserAgent),
typeof(string),
typeof(HybridWebView));
public event EventHandler LoginHandle;
public string Uri
{
get => (string)GetValue(UriProperty);
set => SetValue(UriProperty, value);
}
public string UserAgent
{
get => (string)GetValue(UserAgentProperty);
set => SetValue(UserAgentProperty, value);
}
public void OnLoginHandle()
{
if (LoginHandle != null)
{
MainThread.BeginInvokeOnMainThread(() => LoginHandle(this, EventArgs.Empty));
}
}
}
}

22
Gallery/Login/LoginPage.xaml Executable file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<u:AdaptedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:r="clr-namespace:Gallery.Resources"
xmlns:l="clr-namespace:Gallery.Login"
x:Class="Gallery.Login.LoginPage"
Title="{r:Text Login}">
<Grid>
<l:HybridWebView x:Name="webView" VerticalOptions="Fill"
Uri="https://accounts.pixiv.net/login?lang=zh"
LoginHandle="WebView_LoginHandle"/>
<StackLayout HorizontalOptions="End" VerticalOptions="Start"
Margin="{Binding PanelTopMargin}">
<Button BackgroundColor="#64000000" CornerRadius="20"
TextColor="White" Clicked="Button_Clicked"
FontSize="22" Padding="10" Margin="8"
FontFamily="{DynamicResource IconSolidFontFamily}"
Text="{DynamicResource IconClose}"/>
</StackLayout>
</Grid>
</u:AdaptedPage>

31
Gallery/Login/LoginPage.xaml.cs Executable file
View File

@ -0,0 +1,31 @@
using System;
using Gallery.UI;
using Gallery.Utils;
namespace Gallery.Login
{
public partial class LoginPage : AdaptedPage
{
private readonly Action action;
public LoginPage(Action after)
{
InitializeComponent();
webView.UserAgent = Configs.UserAgent;
BindingContext = this;
action = after;
}
private async void Button_Clicked(object sender, EventArgs e)
{
await Navigation.PopModalAsync();
}
private async void WebView_LoginHandle(object sender, EventArgs e)
{
await Navigation.PopModalAsync();
action?.Invoke();
}
}
}

46
Gallery/OptionPage.xaml Executable file
View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<u:AdaptedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:u="clr-namespace:Gallery.UI"
xmlns:r="clr-namespace:Gallery.Resources"
x:Class="Gallery.OptionPage"
Title="{r:Text Option}">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="ShareCookie_Clicked"
IconImageSource="{DynamicResource FontIconShare}"/>
</ContentPage.ToolbarItems>
<TableView Intent="Settings" VerticalOptions="Start"
BackgroundColor="{DynamicResource OptionBackColor}">
<TableRoot>
<TableSection Title="{r:Text About}">
<u:OptionTextCell Title="{r:Text Version}" Detail="{Binding Version}"/>
</TableSection>
<TableSection Title="{r:Text Illusts}">
<u:OptionSwitchCell Title="{r:Text R18}"
IsToggled="{Binding IsOnR18, Mode=TwoWay}"/>
<u:OptionDropCell x:Name="optionFavSync" Title="{r:Text SyncType}"
SelectedIndex="{Binding SyncFavType, Mode=TwoWay}"/>
<u:OptionEntryCell Title="{r:Text DownloadIllustThreads}"
Text="{Binding DownloadIllustThreads, Mode=TwoWay}"
Keyboard="Numeric" Placeholder="1~10"/>
</TableSection>
<TableSection Title="{r:Text Proxy}">
<u:OptionSwitchCell Title="{r:Text Enabled}"
IsToggled="{Binding IsProxied, Mode=TwoWay}"/>
</TableSection>
<TableSection Title="{r:Text Detail}">
<u:OptionEntryCell Title="{r:Text Host}"
Text="{Binding Host, Mode=TwoWay}"
Keyboard="Url" Placeholder="www.example.com"/>
<u:OptionEntryCell Title="{r:Text Port}"
Text="{Binding Port, Mode=TwoWay}"
Keyboard="Numeric" Placeholder="8080"/>
</TableSection>
<TableSection Title="{r:Text Privacy}">
<u:OptionEntryCell Title="{r:Text Cookie}"
Text="{Binding Cookie, Mode=TwoWay}"
Keyboard="Text"/>
</TableSection>
</TableRoot>
</TableView>
</u:AdaptedPage>

244
Gallery/OptionPage.xaml.cs Executable file
View File

@ -0,0 +1,244 @@
using System;
using System.Threading.Tasks;
using Gallery.Resources;
using Gallery.UI;
using Gallery.Utils;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery
{
public partial class OptionPage : AdaptedPage
{
public static readonly BindableProperty VersionProperty = BindableProperty.Create(
nameof(Version), typeof(string), typeof(OptionPage));
public string Version
{
get => (string)GetValue(VersionProperty);
set => SetValue(VersionProperty, value);
}
public static readonly BindableProperty DownloadIllustThreadsProperty = BindableProperty.Create(
nameof(DownloadIllustThreads), typeof(string), typeof(OptionPage));
public static readonly BindableProperty IsOnR18Property = BindableProperty.Create(
nameof(IsOnR18), typeof(bool), typeof(OptionPage));
public static readonly BindableProperty SyncFavTypeProperty = BindableProperty.Create(
nameof(SyncFavType), typeof(int), typeof(OptionPage), -1);
public static readonly BindableProperty IsProxiedProperty = BindableProperty.Create(
nameof(IsProxied), typeof(bool), typeof(OptionPage));
public static readonly BindableProperty HostProperty = BindableProperty.Create(
nameof(Host), typeof(string), typeof(OptionPage));
public static readonly BindableProperty PortProperty = BindableProperty.Create(
nameof(Port), typeof(string), typeof(OptionPage));
public static readonly BindableProperty CookieProperty = BindableProperty.Create(
nameof(Cookie), typeof(string), typeof(OptionPage));
public string DownloadIllustThreads
{
get => (string)GetValue(DownloadIllustThreadsProperty);
set => SetValue(DownloadIllustThreadsProperty, value);
}
public bool IsOnR18
{
get => (bool)GetValue(IsOnR18Property);
set => SetValue(IsOnR18Property, value);
}
public int SyncFavType
{
get => (int)GetValue(SyncFavTypeProperty);
set => SetValue(SyncFavTypeProperty, value);
}
public bool IsProxied
{
get => (bool)GetValue(IsProxiedProperty);
set => SetValue(IsProxiedProperty, value);
}
public string Host
{
get => (string)GetValue(HostProperty);
set => SetValue(HostProperty, value);
}
public string Port
{
get => (string)GetValue(PortProperty);
set => SetValue(PortProperty, value);
}
public string Cookie
{
get => (string)GetValue(CookieProperty);
set => SetValue(CookieProperty, value);
}
public OptionPage()
{
BindingContext = this;
InitializeComponent();
optionFavSync.Items = new[]
{
ResourceHelper.SyncNo,
ResourceHelper.SyncPrompt,
ResourceHelper.SyncAuto,
};
SyncFavType = 0;
#if OBSOLETE
#if __IOS__
string version = Foundation.NSBundle.MainBundle.ObjectForInfoDictionary("CFBundleShortVersionString").ToString();
int build = int.Parse(Foundation.NSBundle.MainBundle.ObjectForInfoDictionary("CFBundleVersion").ToString());
#elif __ANDROID__
var context = Android.App.Application.Context;
var manager = context.PackageManager;
var info = manager.GetPackageInfo(context.PackageName, 0);
string version = info.VersionName;
long build = info.LongVersionCode;
#endif
Version = $"{version} ({build})";
#endif
}
protected override void OnAppearing()
{
base.OnAppearing();
Version = $"{AppInfo.VersionString} ({AppInfo.BuildString})";
IsOnR18 = Configs.IsOnR18;
SyncFavType = (int)Configs.SyncFavType;
DownloadIllustThreads = Configs.DownloadIllustThreads.ToString();
var proxy = Configs.Proxy;
if (proxy != null)
{
IsProxied = true;
Host = proxy.Address.Host;
Port = proxy.Address.Port.ToString();
}
else
{
IsProxied = false;
Host = Preferences.Get(Configs.HostKey, string.Empty);
Port = Preferences.Get(Configs.PortKey, string.Empty);
}
Cookie = Configs.Cookie;
}
protected override void OnDisappearing()
{
base.OnDisappearing();
var r18 = IsOnR18;
var syncType = SyncFavType;
var proxied = IsProxied;
if (Configs.IsOnR18 != r18)
{
Preferences.Set(Configs.IsOnR18Key, r18);
Configs.IsOnR18 = r18;
#if LOG
App.DebugPrint($"r-18 filter: {r18}");
#endif
}
if ((int)Configs.SyncFavType != syncType)
{
Preferences.Set(Configs.SyncFavTypeKey, syncType);
Configs.SyncFavType = (SyncType)syncType;
#if LOG
App.DebugPrint($"favorite sync type changed to {Configs.SyncFavType}");
#endif
}
if (int.TryParse(DownloadIllustThreads, out int threads) && threads > 0 && threads <= 10 && threads != Configs.DownloadIllustThreads)
{
Preferences.Set(Configs.DownloadIllustThreadsKey, threads);
Configs.DownloadIllustThreads = threads;
#if LOG
App.DebugPrint($"will use {threads} threads to download illust");
#endif
}
var proxy = Configs.Proxy;
var h = Host?.Trim();
_ = int.TryParse(Port, out int pt);
if (proxied)
{
if (proxy == null ||
proxy.Address.Host != h ||
proxy.Address.Port != pt)
{
Preferences.Set(Configs.IsProxiedKey, true);
Preferences.Set(Configs.HostKey, h);
if (pt > 0)
{
Preferences.Set(Configs.PortKey, pt);
}
if (!string.IsNullOrEmpty(h) && pt > 0)
{
try
{
if (h.IndexOf(':') >= 0)
{
h = $"[{h}]";
}
var uri = new Uri($"http://{h}:{pt}");
Configs.Proxy = new System.Net.WebProxy(uri);
#if LOG
App.DebugPrint($"set proxy to: {h}:{pt}");
#endif
}
catch (Exception ex)
{
App.DebugError("on.disappearing", $"failed to parse proxy: {h}:{pt}, error: {ex.Message}");
}
}
}
}
else
{
Preferences.Set(Configs.IsProxiedKey, false);
Preferences.Set(Configs.HostKey, h);
if (pt > 0)
{
Preferences.Set(Configs.PortKey, pt);
}
if (proxy != null)
{
Configs.Proxy = null;
#if LOG
App.DebugPrint("clear proxy");
#endif
}
}
var cookie = Cookie;
if (Configs.Cookie != cookie)
{
Configs.SetCookie(cookie, true);
var index = cookie.IndexOf("PHPSESSID=");
if (index >= 0)
{
var session = cookie.Substring(index + 10);
index = session.IndexOf('_');
if (index > 0)
{
session = session.Substring(0, index);
Configs.SetUserId(session, true);
#if LOG
App.DebugPrint($"cookie changed, user id: {session}");
#endif
Task.Run(() => AppShell.Current.DoLoginInformation(true));
}
}
}
}
private async void ShareCookie_Clicked(object sender, EventArgs e)
{
await Share.RequestAsync(new ShareTextRequest
{
Text = Cookie
});
}
}
}

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" ?>
<root>
<Title>Gallery</Title>
<Guest>游客</Guest>
<Ok>OK</Ok>
<Cancel>取消</Cancel>
<Yes></Yes>
<No></No>
<Login>登录</Login>
<About>关于</About>
<Version>版本</Version>
<Illusts>插画</Illusts>
<Proxy>代理</Proxy>
<Detail>详细</Detail>
<Enabled>启用</Enabled>
<Host>主机</Host>
<Port>端口</Port>
<Privacy>隐私</Privacy>
<Cookie>Cookie</Cookie>
<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>
<SyncType>收藏同步类型</SyncType>
<SyncNo>不同步</SyncNo>
<SyncPrompt>提示同步</SyncPrompt>
<SyncAuto>自动同步</SyncAuto>
<DownloadIllustThreads>下载线程数</DownloadIllustThreads>
<Follow>已关注</Follow>
<Recommends>推荐</Recommends>
<ByUser>按用户</ByUser>
<Ranking>排行榜</Ranking>
<Search>搜索</Search>
<Favorites>收藏夹</Favorites>
<Option>选项</Option>
<Preview>预览</Preview>
<Operation>操作</Operation>
<SaveOriginal>保存原图</SaveOriginal>
<Share>分享</Share>
<UserDetail>浏览用户</UserDetail>
<RelatedIllusts>相关推荐</RelatedIllusts>
<SaveSuccess>成功保存图片到照片库。</SaveSuccess>
<AlreadySavedQuestion>原图已保存,是否继续?</AlreadySavedQuestion>
<InvalidUrl>无法识别该 URL。</InvalidUrl>
<FavoritesOperation>请选择收藏夹操作</FavoritesOperation>
<FavoritesReplace>替换</FavoritesReplace>
<FavoritesCombine>合并</FavoritesCombine>
<ExportVideo>导出视频</ExportVideo>
<CantExportVideo>无法导出视频,请先下载完成。</CantExportVideo>
<AlreadySavedVideo>视频已保存,是否继续?</AlreadySavedVideo>
<ExportSuccess>视频已导出到照片库。</ExportSuccess>
<FailedResponse>无法获取返回结果。</FailedResponse>
<ConfirmSyncFavorite>要同步收藏吗?</ConfirmSyncFavorite>
<ConfirmLogin>当前身份为游客,是否跳转到登录页面?</ConfirmLogin>
<All>所有</All>
<Animation>动画</Animation>
<Online>在线</Online>
</root>

View File

@ -0,0 +1,43 @@
namespace Gallery.Resources
{
public class PlatformCulture
{
public string PlatformString { get; private set; }
public string LanguageCode { get; private set; }
public string LocaleCode { get; private set; }
public string Language
{
get { return string.IsNullOrEmpty(LocaleCode) ? LanguageCode : LanguageCode + "-" + LocaleCode; }
}
public PlatformCulture() : this(null) { }
public PlatformCulture(string cultureString)
{
if (string.IsNullOrEmpty(cultureString))
{
//throw new ArgumentNullException(nameof(cultureString), "Expected culture identieifer");
cultureString = "en";
}
PlatformString = cultureString.Replace('_', '-');
var index = PlatformString.IndexOf('-');
if (index > 0)
{
var parts = PlatformString.Split('-');
LanguageCode = parts[0];
LocaleCode = parts[parts.Length - 1];
}
else
{
LanguageCode = PlatformString;
LocaleCode = "";
}
}
public override string ToString()
{
return PlatformString;
}
}
}

View File

@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Gallery.Resources
{
public class ResourceHelper
{
public static string Title => GetResource(nameof(Title));
public static string Guest => GetResource(nameof(Guest));
public static string Ok => GetResource(nameof(Ok));
public static string Cancel => GetResource(nameof(Cancel));
public static string Yes => GetResource(nameof(Yes));
public static string No => GetResource(nameof(No));
public static string R18 => GetResource(nameof(R18));
public static string SyncNo => GetResource(nameof(SyncNo));
public static string SyncPrompt => GetResource(nameof(SyncPrompt));
public static string SyncAuto => GetResource(nameof(SyncAuto));
public static string Operation => GetResource(nameof(Operation));
public static string SaveOriginal => GetResource(nameof(SaveOriginal));
public static string Share => GetResource(nameof(Share));
public static string UserDetail => GetResource(nameof(UserDetail));
public static string RelatedIllusts => GetResource(nameof(RelatedIllusts));
public static string SaveSuccess => GetResource(nameof(SaveSuccess));
public static string AlreadySavedQuestion => GetResource(nameof(AlreadySavedQuestion));
public static string InvalidUrl => GetResource(nameof(InvalidUrl));
public static string Favorites => GetResource(nameof(Favorites));
public static string FavoritesOperation => GetResource(nameof(FavoritesOperation));
public static string FavoritesReplace => GetResource(nameof(FavoritesReplace));
public static string FavoritesCombine => GetResource(nameof(FavoritesCombine));
public static string ExportVideo => GetResource(nameof(ExportVideo));
public static string CantExportVideo => GetResource(nameof(CantExportVideo));
public static string AlreadySavedVideo => GetResource(nameof(AlreadySavedVideo));
public static string ExportSuccess => GetResource(nameof(ExportSuccess));
public static string FailedResponse => GetResource(nameof(FailedResponse));
public static string ConfirmSyncFavorite => GetResource(nameof(ConfirmSyncFavorite));
public static string ConfirmLogin => GetResource(nameof(ConfirmLogin));
static readonly Dictionary<string, LanguageResource> dict = new Dictionary<string, LanguageResource>();
public static string GetResource(string name, params object[] args)
{
if (!dict.TryGetValue(App.CurrentCulture.PlatformString, out LanguageResource lang))
{
lang = new LanguageResource(App.CurrentCulture);
dict.Add(App.CurrentCulture.PlatformString, lang);
}
if (args == null || args.Length == 0)
{
return lang[name];
}
return string.Format(lang[name], args);
}
private class LanguageResource
{
private readonly Dictionary<string, string> strings;
public string this[string key]
{
get
{
if (strings != null && strings.TryGetValue(key, out string val))
{
return val;
}
return key;
}
}
public LanguageResource(PlatformCulture lang)
{
try
{
#if __IOS__
var langId = $"Gallery.iOS.Resources.Languages.{lang.Language}.xml";
var langCodeId = $"Gallery.iOS.Resources.Languages.{lang.LanguageCode}.xml";
#elif __ANDROID__
var langId = $"Gallery.Droid.Resources.Languages.{lang.Language}.xml";
var langCodeId = $"Gallery.Droid.Resources.Languages.{lang.LanguageCode}.xml";
#endif
var assembly = IntrospectionExtensions.GetTypeInfo(typeof(LanguageResource)).Assembly;
var names = assembly.GetManifestResourceNames();
var name = names.FirstOrDefault(n
=> string.Equals(n, langId, StringComparison.OrdinalIgnoreCase)
|| string.Equals(n, langCodeId, StringComparison.OrdinalIgnoreCase));
if (name == null)
{
#if __IOS__
name = "Gallery.iOS.Resources.Languages.zh-CN.xml";
#elif __ANDROID__
name = "Gallery.Droid.Resources.Languages.zh-CN.xml";
#endif
}
var xml = new XmlDocument();
using (var stream = assembly.GetManifestResourceStream(name))
{
xml.Load(stream);
}
strings = new Dictionary<string, string>();
foreach (XmlElement ele in xml.DocumentElement) // .Root.Elements()
{
strings[ele.Name] = ele.InnerText; // .LocalName Value
}
}
catch (Exception ex)
{
// load failed
App.DebugError("language.ctor", $"failed to load xml resource: {ex.Message}");
}
}
}
}
[ContentProperty(nameof(Text))]
public class TextExtension : IMarkupExtension
{
public string Text { get; set; }
public object ProvideValue(IServiceProvider serviceProvider)
{
if (Text == null)
{
return string.Empty;
}
return ResourceHelper.GetResource(Text);
}
}
}

193
Gallery/UI/AdaptedPage.cs Normal file
View File

@ -0,0 +1,193 @@
using System;
using Gallery.UI.Theme;
using Gallery.Utils;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.UI
{
public class AdaptedPage : ContentPage
{
public static readonly BindableProperty PageTopMarginProperty = BindableProperty.Create(
nameof(PageTopMargin), typeof(Thickness), typeof(AdaptedPage));
public static readonly BindableProperty PanelTopMarginProperty = BindableProperty.Create(
nameof(PanelTopMargin), typeof(Thickness), typeof(AdaptedPage));
public Thickness PageTopMargin
{
get => (Thickness)GetValue(PageTopMarginProperty);
protected set => SetValue(PageTopMarginProperty, value);
}
public Thickness PanelTopMargin
{
get => (Thickness)GetValue(PanelTopMarginProperty);
protected set => SetValue(PanelTopMarginProperty, value);
}
public event EventHandler Load;
public event EventHandler Unload;
protected static readonly bool isPhone = DeviceInfo.Idiom == DeviceIdiom.Phone;
public AdaptedPage()
{
SetDynamicResource(Screen.StatusBarStyleProperty, ThemeBase.StatusBarStyle);
Shell.SetNavBarHasShadow(this, true);
}
public virtual void OnLoad()
{
Load?.Invoke(this, EventArgs.Empty);
}
public virtual void OnUnload()
{
Unload?.Invoke(this, EventArgs.Empty);
}
#if __IOS__
public virtual void OnOrientationChanged(bool landscape)
{
var oldMargin = PageTopMargin;
var oldPanelMargin = PanelTopMargin;
Thickness newMargin;
Thickness newPanelMargin;
if (StyleDefinition.IsFullscreenDevice)
{
var iPhone12 =
#if DEBUG
DeviceInfo.Name.StartsWith("iPhone 12") ||
#endif
DeviceInfo.Model.StartsWith("iPhone13,");
newMargin = landscape ?
AppShell.NavigationBarOffset :
AppShell.TotalBarOffset;
newPanelMargin = landscape ?
AppShell.HalfNavigationBarOffset :
AppShell.NavigationBarOffset;
}
else if (isPhone)
{
newMargin = landscape ?
StyleDefinition.TopOffset32 :
AppShell.TotalBarOffset;
newPanelMargin = landscape ?
StyleDefinition.TopOffset16 :
StyleDefinition.TopOffset32;
}
else
{
// iPad
newMargin = AppShell.TotalBarOffset;
newPanelMargin = StyleDefinition.TopOffset37;
}
if (oldMargin != newMargin)
{
PageTopMargin = newMargin;
OnPageTopMarginChanged(oldMargin, newMargin);
}
if (oldPanelMargin != newPanelMargin)
{
PanelTopMargin = newPanelMargin;
}
}
protected virtual void OnPageTopMarginChanged(Thickness old, Thickness @new) { }
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
OnOrientationChanged(width > height);
}
protected void AnimateToMargin(View element, Thickness margin, bool animate = true)
{
var m = margin;
var start = element.Margin.Top - m.Top;
#if DEBUG
if (start != 0)
{
App.DebugPrint($"{element.GetType()}, margin-top from {element.Margin.Top} to {margin.Top}");
}
#endif
element.Margin = m;
if (start > 0 && animate)
{
ViewExtensions.CancelAnimations(element);
element.Animate("margin", top =>
{
element.TranslationY = top;
},
start, 0,
#if DEBUG
length: 500,
#else
length: 300,
#endif
easing: Easing.SinInOut,
finished: (v, r) =>
{
element.TranslationY = 0;
});
}
}
#endif
protected void Start(Action action)
{
if (Tap.IsBusy)
{
#if LOG
App.DebugPrint("gesture recognizer is now busy...");
#endif
return;
}
using (Tap.Start())
{
action();
}
}
private class Tap : IDisposable
{
public static bool IsBusy
{
get
{
lock (sync)
{
return _instance?.isBusy == true;
}
}
}
private static readonly object sync = new object();
private static readonly Tap _instance = new Tap();
private Tap() { }
public static Tap Start()
{
lock (sync)
{
_instance.isBusy = true;
}
return _instance;
}
private bool isBusy = false;
public void Dispose()
{
isBusy = false;
}
}
}
public class ThicknessEventArgs : EventArgs
{
public Thickness OldMargin { get; set; }
public Thickness NewMargin { get; set; }
}
}

8
Gallery/UI/BlurryPanel.cs Executable file
View File

@ -0,0 +1,8 @@
using Xamarin.Forms;
namespace Gallery.UI
{
public class BlurryPanel : ContentView
{
}
}

57
Gallery/UI/CardView.cs Executable file
View File

@ -0,0 +1,57 @@
using Gallery.Illust;
using Xamarin.Forms;
namespace Gallery.UI
{
public class CardView : ContentView
{
public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(
nameof(CornerRadius), typeof(float), typeof(CardView));
public static readonly BindableProperty ShadowColorProperty = BindableProperty.Create(
nameof(ShadowColor), typeof(Color), typeof(CardView));
public static readonly BindableProperty ShadowRadiusProperty = BindableProperty.Create(
nameof(ShadowRadius), typeof(float), typeof(CardView), 3f);
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
{
get => (float)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public Color ShadowColor
{
get => (Color)GetValue(ShadowColorProperty);
set => SetValue(ShadowColorProperty, value);
}
public float ShadowRadius
{
get => (float)GetValue(ShadowRadiusProperty);
set => SetValue(ShadowRadiusProperty, value);
}
public Size ShadowOffset
{
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)
{
if (BindingContext is IllustItem illust &&
illust.Width > 0 &&
illust.ImageHeight.IsAuto)
{
illust.ImageHeight = widthConstraint * illust.Height / illust.Width;
}
return base.OnMeasure(widthConstraint, heightConstraint);
}
}
}

68
Gallery/UI/CircleUIs.cs Executable file
View File

@ -0,0 +1,68 @@
using Xamarin.Forms;
namespace Gallery.UI
{
public class CircleImage : Image { }
public class RoundImage : Image
{
public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(
nameof(CornerRadius), typeof(float), typeof(RoundImage));
public static readonly BindableProperty CornerMasksProperty = BindableProperty.Create(
nameof(CornerMasks), typeof(CornerMask), typeof(RoundImage));
public float CornerRadius
{
get => (float)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public CornerMask CornerMasks
{
get => (CornerMask)GetValue(CornerMasksProperty);
set => SetValue(CornerMasksProperty, value);
}
}
public enum CornerMask
{
None = 0,
LeftTop = 1,
RightTop = 2,
LeftBottom = 4,
RightBottom = 8,
Top = LeftTop | RightTop, // 3
Left = LeftTop | LeftBottom, // 5
Slash = RightTop | LeftBottom, // 6
BackSlash = LeftTop | RightBottom, // 9
Right = RightTop | RightBottom, // 10
Bottom = LeftBottom | RightBottom, // 12
ExceptRightBottom = LeftTop | RightTop | LeftBottom, // 7
ExceptLeftBottom = LeftTop | RightTop | RightBottom, // 11
ExceptRightTop = LeftTop | LeftBottom | RightBottom, // 13
ExceptLeftTop = RightTop | LeftBottom | RightBottom, // 14
All = LeftTop | RightTop | LeftBottom | RightBottom // 15
}
public class RoundLabel : Label
{
public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(
nameof(CornerRadius), typeof(float), typeof(RoundLabel));
public static new readonly BindableProperty BackgroundColorProperty = BindableProperty.Create(
nameof(BackgroundColor), typeof(Color), typeof(RoundLabel), Color.Transparent);
public float CornerRadius
{
get => (float)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public new Color BackgroundColor
{
get => (Color)GetValue(BackgroundColorProperty);
set => SetValue(BackgroundColorProperty, value);
}
}
}

269
Gallery/UI/FlowLayout.cs Executable file
View File

@ -0,0 +1,269 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms;
namespace Gallery.UI
{
public class FlowLayout : Layout<View>
{
public static readonly BindableProperty ColumnProperty = BindableProperty.Create(
nameof(Column), typeof(int), typeof(FlowLayout), 2, propertyChanged: OnColumnPropertyChanged);
public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create(
nameof(RowSpacing), typeof(double), typeof(FlowLayout), 10.0);
public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create(
nameof(ColumnSpacing), typeof(double), typeof(FlowLayout), 10.0);
private static void OnColumnPropertyChanged(BindableObject obj, object oldValue, object newValue)
{
var flowLayout = (FlowLayout)obj;
if (oldValue is int column && column != flowLayout.Column)
{
flowLayout.UpdateChildrenLayout();
flowLayout.InvalidateLayout();
}
}
public int Column
{
get => (int)GetValue(ColumnProperty);
set => SetValue(ColumnProperty, value);
}
public double RowSpacing
{
get => (double)GetValue(RowSpacingProperty);
set => SetValue(RowSpacingProperty, value);
}
public double ColumnSpacing
{
get => (double)GetValue(ColumnSpacingProperty);
set => SetValue(ColumnSpacingProperty, value);
}
public event EventHandler<HeightEventArgs> MaxHeightChanged;
public double ColumnWidth { get; private set; }
private bool freezed;
private double maximumHeight;
private readonly Dictionary<View, Rectangle> cachedLayout = new Dictionary<View, Rectangle>();
protected override void LayoutChildren(double x, double y, double width, double height)
{
if (freezed)
{
return;
}
var column = Column;
if (column <= 0)
{
return;
}
var source = ItemsSource;
if (source == null || source.Count <= 0)
{
return;
}
var columnSpacing = ColumnSpacing;
var rowSpacing = RowSpacing;
var columnHeights = new double[column];
var columnSpacingTotal = columnSpacing * (column - 1);
var columnWidth = (width - columnSpacingTotal) / column;
ColumnWidth = columnWidth;
foreach (var item in Children)
{
var measured = item.Measure(columnWidth, height, MeasureFlags.IncludeMargins);
var col = 0;
for (var i = 1; i < column; i++)
{
if (columnHeights[i] < columnHeights[col])
{
col = i;
}
}
var rect = new Rectangle(
col * (columnWidth + columnSpacing),
columnHeights[col],
columnWidth,
measured.Request.Height);
if (cachedLayout.TryGetValue(item, out var v))
{
if (v != rect)
{
cachedLayout[item] = rect;
item.Layout(rect);
}
}
else
{
cachedLayout.Add(item, rect);
item.Layout(rect);
}
columnHeights[col] += measured.Request.Height + rowSpacing;
}
}
private double lastWidth = -1;
private SizeRequest lastSizeRequest;
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
var column = Column;
if (column <= 0)
{
return base.OnMeasure(widthConstraint, heightConstraint);
}
if (lastWidth == widthConstraint)
{
return lastSizeRequest;
}
lastWidth = widthConstraint;
var columnSpacing = ColumnSpacing;
var rowSpacing = RowSpacing;
var columnHeights = new double[column];
var columnSpacingTotal = columnSpacing * (column - 1);
var columnWidth = (widthConstraint - columnSpacingTotal) / column;
ColumnWidth = columnWidth;
foreach (var item in Children)
{
var measured = item.Measure(columnWidth, heightConstraint, MeasureFlags.IncludeMargins);
var col = 0;
for (var i = 1; i < column; i++)
{
if (columnHeights[i] < columnHeights[col])
{
col = i;
}
}
columnHeights[col] += measured.Request.Height + rowSpacing;
}
maximumHeight = columnHeights.Max();
if (maximumHeight > 0)
{
MaxHeightChanged?.Invoke(this, new HeightEventArgs { ContentHeight = maximumHeight });
}
lastSizeRequest = new SizeRequest(new Size(widthConstraint, maximumHeight));
return lastSizeRequest;
}
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(
nameof(ItemTemplate), typeof(DataTemplate), typeof(FlowLayout));
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(
nameof(ItemsSource), typeof(IList), typeof(FlowLayout), propertyChanged: OnItemsSourcePropertyChanged);
public DataTemplate ItemTemplate
{
get => (DataTemplate)GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
public IList ItemsSource
{
get => (IList)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
private static void OnItemsSourcePropertyChanged(BindableObject obj, object oldValue, object newValue)
{
var flowLayout = (FlowLayout)obj;
if (oldValue is IIllustCollectionChanged oldNotify)
{
oldNotify.CollectionChanged -= flowLayout.OnCollectionChanged;
}
flowLayout.lastWidth = -1;
if (newValue == null)
{
flowLayout.cachedLayout.Clear();
flowLayout.Children.Clear();
flowLayout.InvalidateLayout();
}
else if (newValue is IList newList)
{
flowLayout.freezed = true;
flowLayout.cachedLayout.Clear();
flowLayout.Children.Clear();
for (var i = 0; i < newList.Count; i++)
{
var child = flowLayout.ItemTemplate.CreateContent();
if (child is View view)
{
view.BindingContext = newList[i];
flowLayout.Children.Add(view);
}
}
if (newValue is IIllustCollectionChanged newNotify)
{
newNotify.CollectionChanged += flowLayout.OnCollectionChanged;
}
flowLayout.freezed = false;
flowLayout.UpdateChildrenLayout();
flowLayout.InvalidateLayout();
}
}
private void OnCollectionChanged(object sender, CollectionChangedEventArgs e)
{
lastWidth = -1;
if (e.OldItems != null)
{
freezed = true;
cachedLayout.Clear();
var index = e.OldStartingIndex;
for (var i = index + e.OldItems.Count - 1; i >= index; i--)
{
Children.RemoveAt(i);
}
freezed = false;
UpdateChildrenLayout();
InvalidateLayout();
}
if (e.NewItems == null)
{
return;
}
freezed = true;
var start = e.NewStartingIndex;
for (var i = 0; i < e.NewItems.Count; i++)
{
var child = ItemTemplate.CreateContent();
if (child is View view)
{
view.BindingContext = e.NewItems[i];
Children.Insert(start + i, view);
}
}
freezed = false;
UpdateChildrenLayout();
//InvalidateLayout();
}
}
public interface IIllustCollectionChanged
{
event EventHandler<CollectionChangedEventArgs> CollectionChanged;
}
public class CollectionChangedEventArgs : EventArgs
{
public int OldStartingIndex { get; set; }
public IList OldItems { get; set; }
public int NewStartingIndex { get; set; }
public IList NewItems { get; set; }
}
public class HeightEventArgs : EventArgs
{
public double ContentHeight { get; set; }
}
}

159
Gallery/UI/OptionCell.cs Executable file
View File

@ -0,0 +1,159 @@
using System.Collections;
using Gallery.UI.Theme;
using Gallery.Utils;
using Xamarin.Forms;
namespace Gallery.UI
{
public class OptionEntry : Entry { }
public class OptionPicker : Picker { }
public abstract class OptionCell : ViewCell
{
public static readonly BindableProperty TitleProperty = BindableProperty.Create(
nameof(Title), typeof(string), typeof(OptionCell));
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
protected abstract View Content { get; }
public OptionCell()
{
View = new Grid
{
BindingContext = this,
Padding = new Thickness(20, 0),
ColumnDefinitions =
{
new ColumnDefinition { Width = new GridLength(.3, GridUnitType.Star) },
new ColumnDefinition { Width = new GridLength(.7, GridUnitType.Star) }
},
Children =
{
new Label
{
LineBreakMode = LineBreakMode.TailTruncation,
VerticalOptions = LayoutOptions.Center
}
.Binding(Label.TextProperty, nameof(Title))
.DynamicResource(Label.TextColorProperty, ThemeBase.TextColor),
Content.GridColumn(1)
}
}
.DynamicResource(VisualElement.BackgroundColorProperty, ThemeBase.OptionTintColor);
}
}
public class OptionTextCell : OptionCell
{
public static readonly BindableProperty DetailProperty = BindableProperty.Create(
nameof(Detail), typeof(string), typeof(OptionCell));
public string Detail
{
get => (string)GetValue(DetailProperty);
set => SetValue(DetailProperty, value);
}
protected override View Content => new Label
{
HorizontalOptions = LayoutOptions.End,
VerticalOptions = LayoutOptions.Center
}
.Binding(Label.TextProperty, nameof(Detail))
.DynamicResource(Label.TextColorProperty, ThemeBase.SubTextColor);
}
public class OptionSwitchCell : OptionCell
{
public static readonly BindableProperty IsToggledProperty = BindableProperty.Create(
nameof(IsToggled), typeof(bool), typeof(OptionSwitchCell));
public bool IsToggled
{
get => (bool)GetValue(IsToggledProperty);
set => SetValue(IsToggledProperty, value);
}
protected override View Content => new Switch
{
HorizontalOptions = LayoutOptions.End,
VerticalOptions = LayoutOptions.Center
}
.Binding(Switch.IsToggledProperty, nameof(IsToggled), BindingMode.TwoWay);
}
public class OptionDropCell : OptionCell
{
public static readonly BindableProperty ItemsProperty = BindableProperty.Create(
nameof(Items), typeof(IList), typeof(OptionDropCell));
public static readonly BindableProperty SelectedIndexProperty = BindableProperty.Create(
nameof(SelectedIndex), typeof(int), typeof(OptionDropCell));
public IList Items
{
get => (IList)GetValue(ItemsProperty);
set => SetValue(ItemsProperty, value);
}
public int SelectedIndex
{
get => (int)GetValue(SelectedIndexProperty);
set => SetValue(SelectedIndexProperty, value);
}
protected override View Content => new OptionPicker
{
HorizontalOptions = LayoutOptions.Fill,
VerticalOptions = LayoutOptions.Center
}
.Binding(Picker.ItemsSourceProperty, nameof(Items))
.Binding(Picker.SelectedIndexProperty, nameof(SelectedIndex), BindingMode.TwoWay)
.DynamicResource(Picker.TextColorProperty, ThemeBase.TextColor)
.DynamicResource(VisualElement.BackgroundColorProperty, ThemeBase.OptionTintColor);
}
public class OptionEntryCell : OptionCell
{
public static readonly BindableProperty TextProperty = BindableProperty.Create(
nameof(Text), typeof(string), typeof(OptionSwitchCell));
public static readonly BindableProperty KeyboardProperty = BindableProperty.Create(
nameof(Keyboard), typeof(Keyboard), typeof(OptionSwitchCell));
public static readonly BindableProperty PlaceholderProperty = BindableProperty.Create(
nameof(Placeholder), typeof(string), typeof(OptionSwitchCell));
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public Keyboard Keyboard
{
get => (Keyboard)GetValue(KeyboardProperty);
set => SetValue(KeyboardProperty, value);
}
public string Placeholder
{
get => (string)GetValue(PlaceholderProperty);
set => SetValue(PlaceholderProperty, value);
}
protected override View Content => new OptionEntry
{
HorizontalOptions = LayoutOptions.Fill,
HorizontalTextAlignment = TextAlignment.End,
VerticalOptions = LayoutOptions.Center,
ReturnType = ReturnType.Next
}
.Binding(Entry.TextProperty, nameof(Text), BindingMode.TwoWay)
.Binding(InputView.KeyboardProperty, nameof(Keyboard))
.Binding(Entry.PlaceholderProperty, nameof(Placeholder))
.DynamicResource(Entry.TextColorProperty, ThemeBase.TextColor)
.DynamicResource(Entry.PlaceholderColorProperty, ThemeBase.SubTextColor)
.DynamicResource(VisualElement.BackgroundColorProperty, ThemeBase.OptionTintColor);
}
}

75
Gallery/UI/SegmentedControl.cs Executable file
View File

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms;
namespace Gallery.UI
{
public class SegmentedControl : View, IViewContainer<SegmentedControlOption>
{
public IList<SegmentedControlOption> Children { get; set; }
public SegmentedControl()
{
Children = new List<SegmentedControlOption>();
}
public static readonly BindableProperty TintColorProperty = BindableProperty.Create(
nameof(TintColor), typeof(Color), typeof(SegmentedControl));
public static readonly BindableProperty DisabledColorProperty = BindableProperty.Create(
nameof(DisabledColor), typeof(Color), typeof(SegmentedControl));
public static readonly BindableProperty SelectedTextColorProperty = BindableProperty.Create(
nameof(SelectedTextColor), typeof(Color), typeof(SegmentedControl));
public static readonly BindableProperty SelectedSegmentIndexProperty = BindableProperty.Create(
nameof(SelectedSegmentIndex), typeof(int), typeof(SegmentedControl));
public Color TintColor
{
get => (Color)GetValue(TintColorProperty);
set => SetValue(TintColorProperty, value);
}
public Color DisabledColor
{
get => (Color)GetValue(DisabledColorProperty);
set => SetValue(DisabledColorProperty, value);
}
public Color SelectedTextColor
{
get => (Color)GetValue(SelectedTextColorProperty);
set => SetValue(SelectedTextColorProperty, value);
}
public int SelectedSegmentIndex
{
get => (int)GetValue(SelectedSegmentIndexProperty);
set => SetValue(SelectedSegmentIndexProperty, value);
}
public SegmentedControlOption SelectedSegment => Children[SelectedSegmentIndex];
public event EventHandler<ValueChangedEventArgs> ValueChanged;
//[EditorBrowsable(EditorBrowsableState.Never)]
public void SendValueChanged()
{
ValueChanged?.Invoke(this, new ValueChangedEventArgs { NewValue = SelectedSegmentIndex });
}
}
public class SegmentedControlOption : View
{
public static readonly BindableProperty TextProperty = BindableProperty.Create(
nameof(Text), typeof(string), typeof(SegmentedControlOption));
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public object Value { get; set; }
}
public class ValueChangedEventArgs : EventArgs
{
public int NewValue { get; set; }
}
}

View File

@ -0,0 +1,149 @@
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.UI
{
public static class StyleDefinition
{
public const double FontSizeTitle = 18.0;
public static readonly Thickness ScreenBottomPadding;
public static readonly Thickness TopOffset16 = new Thickness(0, 16, 0, 0);
public static readonly Thickness TopOffset32 = new Thickness(0, 32, 0, 0);
public static readonly Thickness TopOffset37 = new Thickness(0, 37, 0, 0);
public static readonly Color ColorLightShadow = Color.FromRgba(0, 0, 0, 0x20);
public static readonly Color ColorDeepShadow = Color.FromRgba(0, 0, 0, 0x50);
public static readonly Color ColorRedBackground = Color.FromRgb(0xfd, 0x43, 0x63);
public static readonly Color ColorDownloadBackground = Color.FromRgb(0xd7, 0xd9, 0xe0);
public static readonly ImageSource DownloadBackground = ImageSource.FromFile("download.png");
public static readonly ImageSource ProfileNone = ImageSource.FromFile("no_profile.png");
public static readonly double FontSizeMicro = Device.GetNamedSize(NamedSize.Micro, typeof(Label));
public static readonly double FontSizeSmall = Device.GetNamedSize(NamedSize.Small, typeof(Label));
#if __IOS__
public const string IconLightFontFamily = "FontAwesome5Pro-Light";
public const string IconRegularFontFamily = "FontAwesome5Pro-Regular";
public const string IconSolidFontFamily = "FontAwesome5Pro-Solid";
public const string IconLeft = "\uf104";
#elif __ANDROID__
public const string IconLightFontFamily = "fa-light-300.ttf#FontAwesome5Pro-Light";
public const string IconRegularFontFamily = "fa-regular-400.ttf#FontAwesome5Pro-Regular";
public const string IconSolidFontFamily = "fa-solid-900.ttf#FontAwesome5Pro-Solid";
public const string IconLeft = "\uf053";
#endif
public const string IconUser = "\uf007";
public const string IconSparkles = "\uf890";
public const string IconOrder = "\uf88f";
public const string IconLayer = "\uf302";
public const string IconRefresh = "\uf2f9";
public const string IconLove = "\uf004";
public const string IconCircleLove = "\uf4c7";
public const string IconOption = "\uf013";
public const string IconFavorite = "\uf02e";
public const string IconShare = "\uf1e0";
public const string IconCaretDown = "\uf0d7";
//public const string IconCaretUp = "\uf0d8";
public const string IconCircleCheck = "\uf058";
public const string IconPlay = "\uf04b";
public const string IconPause = "\uf04c";
public const string IconMore = "\uf142";
public const string IconCaretCircleLeft = "\uf32e";
public const string IconCaretCircleRight = "\uf330";
public const string IconCalendarDay = "\uf783";
public const string IconClose = "\uf057";
public const string IconCloudDownload = "\uf381";
static StyleDefinition()
{
_ = IsFullscreenDevice;
if (_isBottomPadding)
{
if (DeviceInfo.Idiom == DeviceIdiom.Phone)
{
ScreenBottomPadding = new Thickness(0, 0, 0, 26);
}
else
{
ScreenBottomPadding = new Thickness(0, 0, 0, 16);
}
}
}
private static bool _isBottomPadding;
private static bool? _isFullscreenDevice;
public static bool IsFullscreenDevice
{
get
{
if (_isFullscreenDevice != null)
{
return _isFullscreenDevice.Value;
}
#if __IOS__
try
{
var model = DeviceInfo.Model;
if (model == "iPhone10,3")
{
// iPhone X
_isFullscreenDevice = true;
_isBottomPadding = true;
}
else if (model.StartsWith("iPhone"))
{
var vs = model.Substring(6).Split(',');
if (vs.Length == 2 && int.TryParse(vs[0], out int main) && int.TryParse(vs[1], out int sub))
{
// iPhone X/XS/XR or iPhone 11
var flag = (main == 10 && sub >= 6) || (main > 10);
_isFullscreenDevice = flag;
_isBottomPadding = flag;
}
else
{
_isFullscreenDevice = false;
}
}
else if (model.StartsWith("iPad8,"))
{
// iPad 11-inch or 12.9-inch (3rd+)
//_isFullscreenDevice = true;
_isBottomPadding = true;
}
#if DEBUG
else
{
// iPad or Simulator
var name = DeviceInfo.Name;
var flag = name.StartsWith("iPhone X")
|| name.StartsWith("iPhone 11")
|| name.StartsWith("iPhone 12");
_isFullscreenDevice = flag;
_isBottomPadding = flag
|| name.StartsWith("iPad Pro (11-inch)")
|| name.StartsWith("iPad Pro (12.9-inch) (3rd generation)")
|| name.StartsWith("iPad Pro (12.9-inch) (4th generation)");
}
#endif
}
catch (System.Exception ex)
{
App.DebugError("device.get", $"failed to get the device model. {ex.Message}");
}
#else
// TODO:
_isFullscreenDevice = false;
_isBottomPadding = false;
#endif
if (_isFullscreenDevice == null)
{
_isFullscreenDevice = false;
}
return _isFullscreenDevice.Value;
}
}
}
}

43
Gallery/UI/Theme/DarkTheme.cs Executable file
View File

@ -0,0 +1,43 @@
using Gallery.Utils;
using Xamarin.Forms;
namespace Gallery.UI.Theme
{
public class DarkTheme : ThemeBase
{
private static DarkTheme _instance;
public static DarkTheme Instance
{
get
{
if (_instance == null)
{
_instance = new DarkTheme();
}
return _instance;
}
}
public DarkTheme()
{
InitColors();
InitResources();
}
private void InitColors()
{
Add(StatusBarStyle, StatusBarStyles.WhiteText);
Add(WindowColor, Color.Black);
Add(TintColor, Color.FromRgb(0x94, 0x95, 0x9a));
Add(TextColor, Color.White);
Add(SubTextColor, Color.LightGray);
Add(CardBackgroundColor, Color.FromRgb(0x33, 0x33, 0x33));
Add(MaskColor, Color.FromRgba(0xff, 0xff, 0xff, 0x64));
Add(NavColor, Color.FromRgb(0x11, 0x11, 0x11));
Add(NavSelectedColor, Color.FromRgb(0x22, 0x22, 0x22));
Add(OptionBackColor, Color.Black);
Add(OptionTintColor, Color.FromRgb(0x11, 0x11, 0x11));
}
}
}

43
Gallery/UI/Theme/LightTheme.cs Executable file
View File

@ -0,0 +1,43 @@
using Gallery.Utils;
using Xamarin.Forms;
namespace Gallery.UI.Theme
{
public class LightTheme : ThemeBase
{
private static LightTheme _instance;
public static LightTheme Instance
{
get
{
if (_instance == null)
{
_instance = new LightTheme();
}
return _instance;
}
}
public LightTheme()
{
InitColors();
InitResources();
}
private void InitColors()
{
Add(StatusBarStyle, StatusBarStyles.DarkText);
Add(WindowColor, Color.White);
Add(TintColor, Color.FromRgb(0x87, 0x87, 0x8b)); // 0x7f, 0x99, 0xc6
Add(TextColor, Color.Black);
Add(SubTextColor, Color.DimGray);
Add(CardBackgroundColor, Color.FromRgb(0xf3, 0xf3, 0xf3));
Add(MaskColor, Color.FromRgba(0, 0, 0, 0x64));
Add(NavColor, Color.FromRgb(0xf0, 0xf0, 0xf0));
Add(NavSelectedColor, Color.LightGray);
Add(OptionBackColor, Color.FromRgb(0xf0, 0xf0, 0xf0));
Add(OptionTintColor, Color.White);
}
}
}

97
Gallery/UI/Theme/ThemeBase.cs Executable file
View File

@ -0,0 +1,97 @@
using Xamarin.Forms;
namespace Gallery.UI.Theme
{
public abstract class ThemeBase : ResourceDictionary
{
public const string FontIconUserFlyout = nameof(FontIconUserFlyout);
public const string FontIconSparklesFlyout = nameof(FontIconSparklesFlyout);
public const string FontIconOrderFlyout = nameof(FontIconOrderFlyout);
public const string FontIconFavoriteFlyout = nameof(FontIconFavoriteFlyout);
public const string FontIconRefresh = nameof(FontIconRefresh);
public const string FontIconLove = nameof(FontIconLove);
public const string FontIconNotLove = nameof(FontIconNotLove);
public const string FontIconCircleLove = nameof(FontIconCircleLove);
public const string FontIconOption = nameof(FontIconOption);
public const string FontIconShare = nameof(FontIconShare);
public const string FontIconMore = nameof(FontIconMore);
public const string FontIconCaretCircleLeft = nameof(FontIconCaretCircleLeft);
public const string FontIconCaretCircleRight = nameof(FontIconCaretCircleRight);
public const string FontIconCalendarDay = nameof(FontIconCalendarDay);
public const string FontIconCloudDownload = nameof(FontIconCloudDownload);
public const string IconCircleCheck = nameof(IconCircleCheck);
public const string IconCaretDown = nameof(IconCaretDown);
public const string IconClose = nameof(IconClose);
public const string StatusBarStyle = nameof(StatusBarStyle);
public const string WindowColor = nameof(WindowColor);
public const string TintColor = nameof(TintColor);
public const string TextColor = nameof(TextColor);
public const string SubTextColor = nameof(SubTextColor);
public const string CardBackgroundColor = nameof(CardBackgroundColor);
public const string MaskColor = nameof(MaskColor);
public const string NavColor = nameof(NavColor);
public const string NavSelectedColor = nameof(NavSelectedColor);
public const string OptionBackColor = nameof(OptionBackColor);
public const string OptionTintColor = nameof(OptionTintColor);
public const string IconLightFontFamily = nameof(IconLightFontFamily);
public const string IconRegularFontFamily = nameof(IconRegularFontFamily);
public const string IconSolidFontFamily = nameof(IconSolidFontFamily);
//public const string Horizon10 = nameof(Horizon10);
public const string ScreenBottomPadding = nameof(ScreenBottomPadding);
protected void InitResources()
{
//Add(Horizon10, StyleDefinition.Horizon10);
Add(ScreenBottomPadding, StyleDefinition.ScreenBottomPadding);
Add(IconLightFontFamily, StyleDefinition.IconLightFontFamily);
Add(IconRegularFontFamily, StyleDefinition.IconRegularFontFamily);
Add(IconSolidFontFamily, StyleDefinition.IconSolidFontFamily);
var regularFontFamily = StyleDefinition.IconRegularFontFamily;
var solidFontFamily = StyleDefinition.IconSolidFontFamily;
#if __IOS__
Add(FontIconUserFlyout, GetSolidIcon(StyleDefinition.IconUser, solidFontFamily));
Add(FontIconSparklesFlyout, GetSolidIcon(StyleDefinition.IconSparkles, solidFontFamily));
Add(FontIconOrderFlyout, GetSolidIcon(StyleDefinition.IconOrder, solidFontFamily));
Add(FontIconFavoriteFlyout, GetSolidIcon(StyleDefinition.IconFavorite, solidFontFamily));
Add(FontIconOption, GetSolidIcon(StyleDefinition.IconOption, solidFontFamily));
#elif __ANDROID__
Add(FontIconUserFlyout, ImageSource.FromFile("ic_user"));
Add(FontIconSparklesFlyout, ImageSource.FromFile("ic_sparkles"));
Add(FontIconOrderFlyout, ImageSource.FromFile("ic_rank"));
Add(FontIconFavoriteFlyout, ImageSource.FromFile("ic_bookmark"));
Add(FontIconOption, ImageSource.FromFile("ic_option"));
#endif
Add(FontIconLove, GetSolidIcon(StyleDefinition.IconLove, solidFontFamily, StyleDefinition.ColorRedBackground));
Add(FontIconCircleLove, GetSolidIcon(StyleDefinition.IconCircleLove, solidFontFamily, StyleDefinition.ColorRedBackground));
Add(FontIconRefresh, GetSolidIcon(StyleDefinition.IconRefresh, solidFontFamily));
Add(FontIconNotLove, GetSolidIcon(StyleDefinition.IconLove, regularFontFamily));
Add(FontIconShare, GetSolidIcon(StyleDefinition.IconShare, solidFontFamily));
Add(FontIconMore, GetSolidIcon(StyleDefinition.IconMore, regularFontFamily));
Add(FontIconCaretCircleLeft, GetSolidIcon(StyleDefinition.IconCaretCircleLeft, solidFontFamily));
Add(FontIconCaretCircleRight, GetSolidIcon(StyleDefinition.IconCaretCircleRight, solidFontFamily));
Add(FontIconCalendarDay, GetSolidIcon(StyleDefinition.IconCalendarDay, regularFontFamily));
Add(FontIconCloudDownload, GetSolidIcon(StyleDefinition.IconCloudDownload, solidFontFamily));
Add(IconCircleCheck, StyleDefinition.IconCircleCheck);
Add(IconCaretDown, StyleDefinition.IconCaretDown);
Add(IconClose, StyleDefinition.IconClose);
}
private FontImageSource GetSolidIcon(string icon, string family, Color color = default)
{
return new FontImageSource
{
FontFamily = family,
Glyph = icon,
Size = StyleDefinition.FontSizeTitle,
Color = color
};
}
}
}

29
Gallery/Utils/Converters.cs Executable file
View File

@ -0,0 +1,29 @@
using System;
using System.Globalization;
using Gallery.UI;
using Xamarin.Forms;
namespace Gallery.Utils
{
public class FavoriteIconConverter : IValueConverter
{
private readonly bool isFavorite;
public FavoriteIconConverter(bool favorite)
{
isFavorite = favorite;
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value == null ?
isFavorite ? StyleDefinition.IconLove : string.Empty :
StyleDefinition.IconCircleLove;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,248 @@
using System.Globalization;
using System.Threading;
using Gallery.Resources;
#if __IOS__
using System.Diagnostics.CodeAnalysis;
using Foundation;
using UIKit;
#elif __ANDROID__
using Android.OS;
using Xamarin.Forms.Platform.Android;
#endif
using Xamarin.Forms;
namespace Gallery.Utils
{
public class EnvironmentService
{
#region - Theme -
/*
[SuppressMessage("Code Notifications", "XI0002:Notifies you from using newer Apple APIs when targeting an older OS version", Justification = "<Pending>")]
public AppTheme GetApplicationTheme()
{
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{
var currentController = Platform.GetCurrentUIViewController();
if (currentController == null)
{
return AppTheme.Unspecified;
}
var style = currentController.TraitCollection.UserInterfaceStyle;
if (style == UIUserInterfaceStyle.Dark)
{
return AppTheme.Dark;
}
else if (style == UIUserInterfaceStyle.Light)
{
return AppTheme.Light;
}
}
return AppTheme.Unspecified;
}
//*/
public static void SetStatusBarColor(Color color)
{
#if __ANDROID_21__
if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop)
{
Droid.MainActivity.Main.SetStatusBarColor(color.ToAndroid());
Droid.MainActivity.Main.Window.DecorView.SystemUiVisibility =
App.CurrentTheme == Xamarin.Essentials.AppTheme.Dark ?
Android.Views.StatusBarVisibility.Visible :
(Android.Views.StatusBarVisibility)Android.Views.SystemUiFlags.LightStatusBar;
}
#endif
}
public static void SetStatusBarStyle(StatusBarStyles style)
{
#if __IOS__
SetStatusBarStyle(ConvertStyle(style));
}
public static void SetStatusBarStyle(UIStatusBarStyle style)
{
if (UIApplication.SharedApplication.StatusBarStyle == style)
{
return;
}
if (style == UIStatusBarStyle.BlackOpaque)
{
UIApplication.SharedApplication.SetStatusBarHidden(true, true);
}
else
{
UIApplication.SharedApplication.SetStatusBarStyle(style, true);
UIApplication.SharedApplication.SetStatusBarHidden(false, true);
}
}
[SuppressMessage("Code Notifications", "XI0002:Notifies you from using newer Apple APIs when targeting an older OS version", Justification = "<Pending>")]
public static UIStatusBarStyle ConvertStyle(StatusBarStyles style)
{
switch (style)
{
case StatusBarStyles.DarkText:
return UIStatusBarStyle.DarkContent;
case StatusBarStyles.WhiteText:
return UIStatusBarStyle.LightContent;
case StatusBarStyles.Hidden:
return UIStatusBarStyle.BlackOpaque;
case StatusBarStyles.Default:
default:
return UIStatusBarStyle.Default;
}
}
#else
}
#endif
#endregion
#region - Culture Info -
public static void SetCultureInfo(CultureInfo ci)
{
Thread.CurrentThread.CurrentCulture = ci;
Thread.CurrentThread.CurrentUICulture = ci;
#if LOG
App.DebugPrint($"CurrentCulture set: {ci.Name}");
#endif
}
public static CultureInfo GetCurrentCultureInfo()
{
string lang;
#if __IOS__
if (NSLocale.PreferredLanguages.Length > 0)
{
var pref = NSLocale.PreferredLanguages[0];
lang = iOSToDotnetLanguage(pref);
}
else
{
lang = "zh-CN";
}
#elif __ANDROID__
var locale = Java.Util.Locale.Default;
lang = AndroidToDotnetLanguage(locale.ToString().Replace('_', '-'));
#endif
CultureInfo ci;
var platform = new PlatformCulture(lang);
try
{
ci = new CultureInfo(platform.Language);
}
catch (CultureNotFoundException e)
{
try
{
var fallback = ToDotnetFallbackLanguage(platform);
App.DebugPrint($"{lang} failed, trying {fallback} ({e.Message})");
ci = new CultureInfo(fallback);
}
catch (CultureNotFoundException e1)
{
App.DebugError("culture.get", $"{lang} couldn't be set, using 'zh-CN' ({e1.Message})");
ci = new CultureInfo("zh-CN");
}
}
return ci;
}
#if __IOS__
[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
private static string iOSToDotnetLanguage(string iOSLanguage)
{
string netLanguage;
//certain languages need to be converted to CultureInfo equivalent
switch (iOSLanguage)
{
case "ms-MY": // "Malaysian (Malaysia)" not supported .NET culture
case "ms-SG": // "Malaysian (Singapore)" not supported .NET culture
netLanguage = "ms"; // closest supported
break;
case "gsw-CH": // "Schwiizertüütsch (Swiss German)" not supported .NET culture
netLanguage = "de-CH"; // closest supported
break;
// add more application-specific cases here (if required)
// ONLY use cultures that have been tested and known to work
default:
netLanguage = iOSLanguage;
break;
}
#if DEBUG
App.DebugPrint($"iOS Language: {iOSLanguage}, .NET Language/Locale: {netLanguage}");
#endif
return netLanguage;
}
#elif __ANDROID__
private static string AndroidToDotnetLanguage(string androidLanguage)
{
string netLanguage;
//certain languages need to be converted to CultureInfo equivalent
switch (androidLanguage)
{
case "ms-BN": // "Malaysian (Brunei)" not supported .NET culture
case "ms-MY": // "Malaysian (Malaysia)" not supported .NET culture
case "ms-SG": // "Malaysian (Singapore)" not supported .NET culture
netLanguage = "ms"; // closest supported
break;
case "in-ID": // "Indonesian (Indonesia)" has different code in .NET
netLanguage = "id-ID"; // correct code for .NET
break;
case "gsw-CH": // "Schwiizertüütsch (Swiss German)" not supported .NET culture
netLanguage = "de-CH"; // closest supported
break;
// add more application-specific cases here (if required)
// ONLY use cultures that have been tested and known to work
default:
netLanguage = androidLanguage;
break;
}
#if DEBUG
App.DebugPrint($"Android Language: {androidLanguage}, .NET Language/Locale: {netLanguage}");
#endif
return netLanguage;
}
#endif
private static string ToDotnetFallbackLanguage(PlatformCulture platCulture)
{
string netLanguage;
switch (platCulture.LanguageCode)
{
//
case "pt":
netLanguage = "pt-PT"; // fallback to Portuguese (Portugal)
break;
case "gsw":
netLanguage = "de-CH"; // equivalent to German (Switzerland) for this app
break;
// add more application-specific cases here (if required)
// ONLY use cultures that have been tested and known to work
default:
netLanguage = platCulture.LanguageCode; // use the first part of the identifier (two chars, usually);
break;
}
#if DEBUG
App.DebugPrint($".NET Fallback Language/Locale: {platCulture.LanguageCode} to {netLanguage} (application-specific)");
#endif
return netLanguage;
}
#endregion
}
}

279
Gallery/Utils/Extensions.cs Executable file
View File

@ -0,0 +1,279 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace Gallery.Utils
{
public static class Extensions
{
public static T Binding<T>(this T view, BindableProperty property, string name,
BindingMode mode = BindingMode.Default, IValueConverter converter = null) where T : BindableObject
{
if (name == null)
{
view.SetValue(property, property.DefaultValue);
}
else
{
view.SetBinding(property, name, mode, converter);
}
return view;
}
public static T DynamicResource<T>(this T view, BindableProperty property, string key) where T : Element
{
view.SetDynamicResource(property, key);
return view;
}
public static T GridRow<T>(this T view, int row) where T : BindableObject
{
Grid.SetRow(view, row);
return view;
}
public static T GridColumn<T>(this T view, int column) where T : BindableObject
{
Grid.SetColumn(view, column);
return view;
}
public static T GridColumnSpan<T>(this T view, int columnSpan) where T : BindableObject
{
Grid.SetColumnSpan(view, columnSpan);
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 : IDisposable
{
public static ParallelTask Start(string tag, int from, int toExclusive, int maxCount, Predicate<int> action, int tagIndex = -1, Action complete = null)
{
if (toExclusive <= from)
{
if (complete != null)
{
Task.Run(complete);
}
return null;
}
var task = new ParallelTask(tag, from, toExclusive, maxCount, action, tagIndex, complete);
task.Start();
return task;
}
private readonly object sync = new object();
private int count;
private bool disposed;
public int TagIndex { get; private set; }
private readonly string tag;
private readonly int max;
private readonly int from;
private readonly int to;
private readonly Predicate<int> action;
private readonly Action complete;
private ParallelTask(string tag, int from, int to, int maxCount, Predicate<int> action, int tagIndex, Action complete)
{
if (maxCount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxCount));
}
max = maxCount;
if (from >= to)
{
throw new ArgumentOutOfRangeException(nameof(from));
}
TagIndex = tagIndex;
this.tag = tag;
this.from = from;
this.to = to;
this.action = action;
this.complete = complete;
}
public void Start()
{
_ = ThreadPool.QueueUserWorkItem(DoStart);
}
private void DoStart(object state)
{
#if LOG
var sw = new System.Diagnostics.Stopwatch();
long lastElapsed = 0;
sw.Start();
#endif
for (int i = from; i < to; i++)
{
var index = i;
while (true)
{
if (count < max)
{
break;
}
#if LOG
var elapsed = sw.ElapsedMilliseconds;
if (elapsed - lastElapsed > 60000)
{
lastElapsed = elapsed;
App.DebugPrint($"WARNING: parallel task ({tag}), {count} tasks in queue, cost too much time ({elapsed:n0}ms)");
}
#endif
if (disposed)
{
#if LOG
sw.Stop();
App.DebugPrint($"parallel task determinate, disposed ({tag}), cost time ({elapsed:n0}ms)");
#endif
return;
}
Thread.Sleep(16);
}
lock (sync)
{
count++;
}
ThreadPool.QueueUserWorkItem(o =>
//Task.Run(() =>
{
try
{
if (!action(index))
{
disposed = true;
}
}
catch (Exception ex)
{
App.DebugError($"parallel.start ({tag})", $"failed to run action, index: {index}, error: {ex}");
}
finally
{
lock (sync)
{
count--;
}
}
});
}
while (count > 0)
{
#if LOG
var elapsed = sw.ElapsedMilliseconds;
if (elapsed - lastElapsed > 60000)
{
lastElapsed = elapsed;
App.DebugPrint($"WARNING: parallel task ({tag}), {count} tasks are waiting for end, cost too much time ({elapsed:n0}ms)");
}
#endif
if (disposed)
{
#if LOG
sw.Stop();
App.DebugPrint($"parallel task determinate, disposed ({tag}), cost time ({elapsed:n0}ms)");
#endif
return;
}
Thread.Sleep(16);
}
#if LOG
sw.Stop();
App.DebugPrint($"parallel task done ({tag}), cost time ({sw.ElapsedMilliseconds:n0}ms)");
#endif
complete?.Invoke();
}
public void Dispose()
{
disposed = true;
}
}
public static class Screen
{
private const string StatusBarStyle = nameof(StatusBarStyle);
private const string HomeIndicatorAutoHidden = nameof(HomeIndicatorAutoHidden);
public static readonly BindableProperty StatusBarStyleProperty = BindableProperty.CreateAttached(
StatusBarStyle,
typeof(StatusBarStyles),
typeof(Page),
StatusBarStyles.WhiteText);
public static StatusBarStyles GetStatusBarStyle(VisualElement page) => (StatusBarStyles)page.GetValue(StatusBarStyleProperty);
public static void SetStatusBarStyle(VisualElement page, StatusBarStyles value) => page.SetValue(StatusBarStyleProperty, value);
public static readonly BindableProperty HomeIndicatorAutoHiddenProperty = BindableProperty.CreateAttached(
HomeIndicatorAutoHidden,
typeof(bool),
typeof(Shell),
false);
public static bool GetHomeIndicatorAutoHidden(VisualElement page) => (bool)page.GetValue(HomeIndicatorAutoHiddenProperty);
public static void SetHomeIndicatorAutoHidden(VisualElement page, bool value) => page.SetValue(HomeIndicatorAutoHiddenProperty, value);
}
public enum StatusBarStyles
{
Default,
// Will behave as normal.
// White text on black NavigationBar/in iOS Dark mode and
// Black text on white NavigationBar/in iOS Light mode
DarkText,
// Will switch the color of content of StatusBar to black.
WhiteText,
// Will switch the color of content of StatusBar to white.
Hidden
// Will hide the StatusBar
}
}

92
Gallery/Utils/FileStore.cs Executable file
View File

@ -0,0 +1,92 @@
using System.Threading.Tasks;
using Xamarin.Forms;
#if __IOS__
using UIKit;
using Xamarin.Forms.Platform.iOS;
#elif __ANDROID__
using Android.Content;
using Android.Net;
using System.IO;
using System.Linq;
#endif
namespace Gallery.Utils
{
public class FileStore
{
#if __IOS__
public static Task<string> SaveVideoToGalleryAsync(string file)
{
var task = new TaskCompletionSource<string>();
if (UIVideo.IsCompatibleWithSavedPhotosAlbum(file))
{
UIVideo.SaveToPhotosAlbum(file, (path, err) =>
{
task.SetResult(err?.ToString());
});
}
return task.Task;
}
#endif
public static Task<string> SaveImageToGalleryAsync(ImageSource image)
{
#if __IOS__
IImageSourceHandler renderer;
if (image is UriImageSource)
{
renderer = new ImageLoaderSourceHandler();
}
else if (image is FileImageSource)
{
renderer = new FileImageSourceHandler();
}
else
{
renderer = new StreamImagesourceHandler();
}
var photo = renderer.LoadImageAsync(image).Result;
var task = new TaskCompletionSource<string>();
if (photo == null)
{
task.SetResult(null);
}
else
{
photo.SaveToPhotosAlbum((img, error) =>
{
task.SetResult(error?.ToString());
});
}
return task.Task;
#elif __ANDROID__
Java.IO.File camera;
var dirs = Droid.MainActivity.Main.GetExternalMediaDirs();
camera = dirs.FirstOrDefault();
if (camera == null)
{
camera = Droid.MainActivity.Main.GetExternalFilesDir(Android.OS.Environment.DirectoryPictures);
}
if (!camera.Exists())
{
camera.Mkdirs();
}
var original = ((FileImageSource)image).File;
var filename = Path.GetFileName(original);
var imgFile = new Java.IO.File(camera, filename).AbsolutePath;
File.Copy(original, imgFile);
var uri = Uri.FromFile(new Java.IO.File(imgFile));
var intent = new Intent(Intent.ActionMediaScannerScanFile);
intent.SetData(uri);
Droid.MainActivity.Main.SendBroadcast(intent);
var task = new TaskCompletionSource<string>();
task.SetResult(null);
return task.Task;
#endif
}
}
}

View File

@ -0,0 +1,464 @@
using System;
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 System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Gallery.Utils
{
public class HttpUtility
{
public static T LoadObject<T>(string file, string url, string referer, out string error,
bool force = false,
bool nojson = false,
HttpContent post = null,
Action<HttpRequestHeaders> header = null,
Func<T, string> namehandler = null,
Func<string, string> action = null,
Func<string, T> @return = null)
{
string content = null;
if (post == null && !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)
{
bool noToken = string.IsNullOrEmpty(Configs.CsrfToken);
if (noToken)
{
post = 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);
var cookie = Configs.Cookie;
if (cookie != null)
{
headers.Add("Cookie", cookie);
}
if (post != null && !noToken)
{
headers.Add("Origin", Configs.Referer);
headers.Add("X-Csrf-Token", Configs.CsrfToken);
}
if (header == null)
{
var userId = Configs.UserId;
if (userId != null)
{
headers.Add("X-User-Id", userId);
}
}
else
{
header(headers);
}
}, post);
if (response == null)
{
error = "response is null";
return default;
}
if (!response.IsSuccessStatusCode)
{
App.DebugPrint($"http failed with code: {(int)response.StatusCode} - {response.StatusCode}");
error = response.StatusCode.ToString();
return default;
}
using (response)
{
try
{
content = response.Content.ReadAsStringAsync().Result;
if (action != null)
{
content = action(content);
}
if (@return != null)
{
error = null;
return @return(content);
}
}
catch (Exception ex)
{
App.DebugError("load.stream", $"failed to read stream, error: {ex.Message}");
error = ex.Message;
return default;
}
if (content == null)
{
content = string.Empty;
}
bool rtn = false;
T result = default;
if (namehandler != null)
{
try
{
result = JsonConvert.DeserializeObject<T>(content);
file = namehandler(result);
rtn = true;
}
catch (Exception ex)
{
var memo = content.Length < 20 ? content : content.Substring(0, 20) + "...";
App.DebugError("load", $"failed to parse illust JSON object, content: {memo}, error: {ex.Message}");
error = content;
return default;
}
}
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)
{
error = null;
return result;
}
}
}
try
{
error = null;
if (nojson)
{
return JsonConvert.DeserializeObject<T>("{}");
}
else
{
return JsonConvert.DeserializeObject<T>(content);
}
}
catch (Exception ex)
{
var memo = content.Length < 20 ? content : content.Substring(0, 20) + "...";
App.DebugError("load", $"failed to parse illust JSON object, content: {memo}, error: {ex.Message}");
error = content;
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));
var response = Download(url, headers =>
{
headers.Referrer = new Uri(Configs.Referer);
headers.Add("User-Agent", Configs.UserAgent);
headers.Add("Accept", Configs.AcceptPureImage);
});
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;
}
}
public static Task<string> DownloadImageAsync(string url, string id, 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));
var proxy = Configs.Proxy;
var referer = new Uri(string.Format(Configs.RefererIllust, id));
var handler = new HttpClientHandler
{
UseCookies = false
};
if (proxy != null)
{
handler.Proxy = proxy;
handler.UseProxy = true;
}
var client = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
long size;
DateTimeOffset lastModified;
using (var request = new HttpRequestMessage(HttpMethod.Head, url))
{
var headers = request.Headers;
headers.Add("Accept", Configs.AcceptPureImage);
headers.Add("Accept-Language", Configs.AcceptLanguage);
headers.Referrer = referer;
headers.Add("User-Agent", Configs.UserAgent);
using (var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result)
{
size = response.Content.Headers.ContentLength.Value;
lastModified = response.Content.Headers.LastModified.Value;
#if DEBUG
App.DebugPrint($"content length: {size:n0} bytes, last modified: {lastModified}");
#endif
}
}
// segments
const int SIZE = 150000;
var list = new List<(long from, long to)>();
for (var i = 0L; i < size; i += SIZE)
{
long to;
if (i + SIZE >= size)
{
to = size - 1;
}
else
{
to = i + SIZE - 1;
}
list.Add((i, to));
}
var data = new byte[size];
var task = new TaskCompletionSource<string>();
ParallelTask.Start($"download.async.{id}", 0, list.Count, Configs.DownloadIllustThreads, i =>
{
var (from, to) = list[i];
using (var request = new HttpRequestMessage(HttpMethod.Get, url))
{
var headers = request.Headers;
headers.Add("Accept", Configs.AcceptPureImage);
headers.Add("Accept-Language", Configs.AcceptLanguage);
headers.Add("Accept-Encoding", "identity");
headers.Referrer = referer;
headers.IfRange = new RangeConditionHeaderValue(lastModified);
headers.Range = new RangeHeaderValue(from, to);
headers.Add("User-Agent", Configs.UserAgent);
using (var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result)
using (var ms = new MemoryStream(data, (int)from, (int)(to - from + 1)))
{
response.Content.CopyToAsync(ms).Wait();
#if DEBUG
App.DebugPrint($"downloaded range: from ({from:n0}) to ({to:n0})");
#endif
}
}
return true;
},
complete: () =>
{
using (var fs = File.OpenWrite(file))
{
fs.Write(data, 0, data.Length);
}
task.SetResult(file);
});
return task.Task;
}
catch (Exception ex)
{
App.DebugError("image.download.async", ex.Message);
return Task.FromResult<string>(null);
}
}
private static HttpResponseMessage Download(string url, Action<HttpRequestHeaders> headerAction, HttpContent post = null)
{
#if DEBUG
var method = post == null ? "GET" : "POST";
App.DebugPrint($"{method}: {url}");
#endif
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(post == null ? HttpMethod.Get : HttpMethod.Post, uri.PathAndQuery)
{
Version = new Version(1, 1)
})
{
var headers = request.Headers;
headerAction(headers);
//if (proxy == null)
//{
// var time = BitConverter.GetBytes(DateTime.UtcNow.Ticks);
// headers.Add("X-Reverse-Ticks", Convert.ToBase64String(time));
// time = time.Concat(Encoding.UTF8.GetBytes("_reverse_for_pixiv_by_tsanie")).ToArray();
// var reverse = System.Security.Cryptography.SHA256.Create().ComputeHash(time);
// headers.Add("X-Reverse", Convert.ToBase64String(reverse));
//}
headers.Add("Accept-Language", Configs.AcceptLanguage);
//headers.Add("Accept-Encoding", Configs.AcceptEncoding);
if (post != null)
{
request.Content = post;
}
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(1, 1)
})
{
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(1, 1)
})
{
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);
}
}
}

321
Gallery/Utils/IllustData.cs Normal file
View File

@ -0,0 +1,321 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Gallery.Illust;
using Xamarin.Forms;
namespace Gallery.Utils
{
public class IllustResponse<T>
{
public bool error;
public string message;
public T body;
}
public class BookmarkResultData
{
public string last_bookmark_id;
public string stacc_status_id;
}
public class Illust
{
public string illustId;
public string illustTitle;
public string id;
public string title;
public int illustType;
public int xRestrict;
public string url;
public string description;
public string[] tags;
public string userId;
public string userName;
public int width;
public int height;
public int pageCount;
public IllustBookmark bookmarkData;
public string alt;
public IllustUrls urls;
public string seriesId;
public string seriesTitle;
public string profileImageUrl;
public class IllustUrls
{
[JsonProperty("250x250")]
public string x250;
[JsonProperty("360x360")]
public string x360;
[JsonProperty("540x540")]
public string x540;
}
public class IllustBookmark
{
public string id;
[JsonProperty("private")]
public bool isPrivate;
}
public IllustItem ConvertToItem(ImageSource image = null)
{
return new IllustItem
{
Id = illustId ?? id,
BookmarkId = bookmarkData?.id,
Title = illustTitle ?? title,
IllustType = (IllustType)illustType,
Image = image,
ImageUrl = urls?.x360 ?? url,
IsRestrict = xRestrict == 1,
Tags = tags ?? new string[0],
ProfileUrl = profileImageUrl,
UserId = userId,
UserName = userName,
Width = width,
Height = height,
PageCount = pageCount
};
}
}
public class User
{
public string userId;
public string name;
public string image;
public string imageBig;
public bool premium;
public bool isFollowed;
//public string background;
public int partial;
}
public class IllustFavoriteData : IllustResponse<IllustFavoriteBody> { }
public class IllustFavoriteBody
{
public int total;
public Illust[] works;
}
public class IllustData : IllustResponse<IllustBody> { }
public class IllustBody
{
public Page page;
public Thumbnail thumbnails;
public User[] users;
public class Page
{
public int[] follow;
public Recommends recommend;
public RecommendByTag[] recommendByTags;
public Ranking ranking;
public RecommendUser[] recommendUser;
public EditorRecommend[] editorRecommend;
public string[] newPost;
public class Recommends
{
public string[] ids;
}
public class RecommendByTag
{
public string tag;
public string[] ids;
}
public class Ranking
{
public RankingItem[] items;
public string date;
public class RankingItem
{
public string rank;
public string id;
}
}
public class RecommendUser
{
public int id;
public string[] illustIds;
}
public class EditorRecommend
{
public string illustId;
public string comment;
}
}
public class Thumbnail
{
public Illust[] illust;
}
}
public class IllustPreloadBody
{
public Dictionary<string, Illust> illust;
public Dictionary<string, User> user;
public class Illust
{
public string illustId;
public string illustTitle;
public string illustComment;
public string id;
public string title;
public string description;
public int illustType;
public DateTime createDate;
public DateTime uploadDate;
public int xRestrict;
public IllustUrls urls;
public IllustTag tags;
public string alt;
public string userId;
public string userName;
public string userAccount;
//public Dictionary<string, Illust> userIllusts;
public int width;
public int height;
public int pageCount;
public int bookmarkCount;
public int likeCount;
public int commentCount;
public int responseCount;
public int viewCount;
public bool isOriginal;
public IllustBookmark bookmarkData;
public IllustItem CopyToItem(IllustItem item)
{
item.BookmarkId = bookmarkData?.id;
item.Title = illustTitle ?? title;
item.IllustType = (IllustType)illustType;
item.ImageUrl = urls?.regular;
item.IsRestrict = xRestrict == 1;
if (tags != null && tags.tags != null)
{
item.Tags = tags.tags.Where(t => t.locked).Select(t => t.tag).ToArray();
}
else
{
item.Tags = new string[0];
}
item.UserId = userId;
item.UserName = userName;
item.Width = width;
item.Height = height;
item.PageCount = pageCount;
return item;
}
[JsonIgnore]
public string Url => urls.regular;
public class IllustBookmark
{
public string id;
[JsonProperty("private")]
public bool isPrivate;
}
public class IllustUrls
{
public string mini;
public string thumb;
public string small;
public string regular;
public string original;
}
public class IllustTag
{
public string authorId;
public bool isLocked;
public IllustTagItem[] tags;
public bool writable;
public class IllustTagItem
{
public string tag;
public bool locked;
public bool deletable;
public string userId;
public IllustTranslate translation;
public string userName;
public class IllustTranslate
{
public string en;
}
}
}
}
}
public class IllustPageData : IllustResponse<IllustPageBody[]> { }
public class IllustPageBody
{
public Urls urls;
public int width;
public int height;
public class Urls
{
public string thumb_mini;
public string small;
public string regular;
public string original;
}
}
public class IllustRecommendsData : IllustResponse<IllustRecommendsBody> { }
public class IllustRecommendsBody
{
public Illust[] illusts;
public string[] nextIds;
}
public class IllustUserListData : IllustResponse<IllustUserListBody> { }
public class IllustUserListBody
{
public Dictionary<string, object> illusts;
}
public class IllustUserData : IllustResponse<IllustUserBody> { }
public class IllustUserBody
{
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;
public string FilePath;
public bool Incompleted;
public int First;
public int Last;
public int Offset;
public int Length;
}
}
}

141
Gallery/Utils/IllustLegacy.cs Executable file
View File

@ -0,0 +1,141 @@
using System.Linq;
using Gallery.Illust;
namespace Gallery.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 string bookmark_id;
public string bookmark_illust_restrict;
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;
}
bool restrict;
if (tags != null && tags.Contains("R-18"))
{
restrict = true;
}
else
{
restrict = false;
}
return new IllustItem
{
Id = illust_id.ToString(),
BookmarkId = bookmark_id,
Title = title,
Rank = rank,
IllustType = (IllustType)type,
ImageUrl = url,
IsRestrict = restrict,
Tags = tags ?? new string[0],
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
};
}
}
}
public class IllustGlobalData
{
public string token;
public string oneSignalAppId;
public UserData userData;
public class UserData
{
public string id;
public string pixivId;
public string name;
public string profileImg;
public string profileImgBig;
public bool premium;
public int xRestrict;
public bool adult;
public bool safeMode;
}
}
}

View File

@ -0,0 +1,26 @@
using System.Windows.Input;
using Xamarin.Forms;
namespace Gallery.Utils
{
public class LongPressEffect : RoutingEffect
{
private const string Command = nameof(Command);
private const string CommandParameter = nameof(CommandParameter);
public static readonly BindableProperty CommandProperty = BindableProperty.CreateAttached(
Command, typeof(ICommand), typeof(LongPressEffect), null);
public static readonly BindableProperty CommandParameterProperty = BindableProperty.CreateAttached(
CommandParameter, typeof(object), typeof(LongPressEffect), null);
public static ICommand GetCommand(BindableObject view) => (ICommand)view.GetValue(CommandProperty);
public static void SetCommand(BindableObject view, ICommand command) => view.SetValue(CommandProperty, command);
public static object GetCommandParameter(BindableObject view) => view.GetValue(CommandParameterProperty);
public static void SetCommandParameter(BindableObject view, object value) => view.SetValue(CommandParameterProperty, value);
public LongPressEffect() : base("Gallery.LongPressEffect")
{
}
}
}

863
Gallery/Utils/Stores.cs Normal file
View File

@ -0,0 +1,863 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Gallery.Illust;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.Utils
{
public static class Stores
{
public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
public static readonly string CacheFolder = FileSystem.CacheDirectory;
private const string favoriteFile = "favorites.json";
private const string globalFile = "global.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";
private const string preloadsFolder = "preloads";
private const string thumbFolder = "img-thumb";
private const string userFolder = "user-profile";
//private const string recommendsFolder = "recommends";
public static bool NetworkAvailable
{
get
{
try
{
return Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.Internet;
}
catch
{
return false;
}
}
}
public static FavoriteList Favorites => GetFavoriteObject().Illusts;
public static string FavoritesPath => Path.Combine(PersonalFolder, favoriteFile);
public static DateTime FavoritesLastUpdated { get; set; } = DateTime.Now;
private static IllustFavorite favoriteObject;
public static IllustFavorite GetFavoriteObject(bool force = false)
{
if (force || favoriteObject == null)
{
var favorites = LoadFavoritesIllusts();
if (favorites != null)
{
favoriteObject = favorites;
}
else
{
favoriteObject = new IllustFavorite
{
Illusts = new FavoriteList()
};
}
}
return favoriteObject;
}
public static IllustFavorite LoadFavoritesIllusts(string file = null)
{
if (file == null)
{
file = FavoritesPath;
}
return ReadObject<IllustFavorite>(file);
}
public static void SaveFavoritesIllusts()
{
var file = FavoritesPath;
var data = GetFavoriteObject();
data.LastFavoriteUtc = DateTime.UtcNow;
WriteObject(file, data);
}
public static string LoadUgoiraImage(string zip, string frame)
{
var file = Path.Combine(PersonalFolder, ugoiraFolder, zip, frame);
if (File.Exists(file))
{
return file;
}
return null;
}
public static string SaveUgoiraImage(string zip, string frame, byte[] data)
{
try
{
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("save.ugoira", $"failed to save ugoira frame: {zip}/{frame}, error: {ex.Message}");
return null;
}
}
public static string GetUgoiraPath(string url, string ext)
{
return Path.Combine(PersonalFolder, ugoiraFolder, Path.GetFileNameWithoutExtension(url) + ext);
}
private static T ReadObject<T>(string file)
{
string content = null;
if (File.Exists(file))
{
try
{
content = File.ReadAllText(file);
}
catch (Exception ex)
{
App.DebugError("read", $"failed to read file: {file}, error: {ex.Message}");
}
}
else
{
//App.DebugError("read", $"file not found: {file}");
return default;
}
try
{
return JsonConvert.DeserializeObject<T>(content);
}
catch (Exception ex)
{
App.DebugError("read", $"failed to parse illust JSON object, error: {ex.Message}");
return default;
}
}
private static void WriteObject(string file, object obj)
{
var dir = Path.GetDirectoryName(file);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
string content;
try
{
content = JsonConvert.SerializeObject(obj, Formatting.None);
}
catch (Exception ex)
{
App.DebugError("write", $"failed to serialize object, error: {ex.Message}");
return;
}
try
{
File.WriteAllText(file, content, Encoding.UTF8);
}
catch (Exception ex)
{
App.DebugError("write", $"failed to write file: {file}, error: {ex.Message}");
}
}
public static IllustData LoadIllustData(bool force = false)
{
var file = Path.Combine(PersonalFolder, illustFile);
var result = HttpUtility.LoadObject<IllustData>(
file,
Configs.UrlIllustList,
Configs.Referer,
out _,
force: force);
if (result == null || result.error)
{
App.DebugPrint($"error when load illust data: {result?.message}, force({force})");
}
return result;
}
public static IllustRankingData LoadIllustRankingData(string mode, string date, int page, out string error, 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,
out error,
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;
}
public static IllustRecommendsData LoadIllustRecommendsInitData(string id)
{
//var file = Path.Combine(CacheFolder, recommendsFolder, $"{id}.json");
var result = HttpUtility.LoadObject<IllustRecommendsData>(
null,
string.Format(Configs.UrlIllustRecommendsInit, id),
string.Format(Configs.RefererIllust, id),
out _);
if (result == null || result.error)
{
App.DebugPrint($"error when load recommends init data: {result?.message}");
}
return result;
}
public static IllustRecommendsData LoadIllustRecommendsListData(string id, string[] ids)
{
if (ids == null || ids.Length == 0)
{
return null;
}
var ps = string.Concat(ids.Select(i => $"illust_ids%5B%5D={i}&"));
var result = HttpUtility.LoadObject<IllustRecommendsData>(
null,
string.Format(Configs.UrlIllustRecommendsList, ps),
string.Format(Configs.RefererIllust, id),
out _);
if (result == null || result.error)
{
App.DebugPrint($"error when load recommends list data: {result?.message}");
}
return result;
}
public static IllustGlobalData LoadGlobalData(bool force = false)
{
var file = Path.Combine(PersonalFolder, globalFile);
var result = HttpUtility.LoadObject<IllustGlobalData>(
file,
Configs.Prefix,
null,
out _,
force: force,
header: h => { },
action: content =>
{
var index = content.IndexOf(Configs.SuffixGlobal);
if (index > 0)
{
index += Configs.SuffixGlobalLength;
var end = content.IndexOf('\'', index);
if (end > index)
{
content = content.Substring(index, end - index);
}
}
return content;
});
if (result == null)
{
App.DebugPrint($"error when load global data, is null");
}
else
{
#if LOG
App.DebugPrint($"current csrf token: {result.token}");
#endif
Configs.CsrfToken = result.token;
}
return result;
}
public static string AddBookmark(string id)
{
if (string.IsNullOrEmpty(Configs.CsrfToken))
{
return null;
}
var content = new StringContent(
"{\"illust_id\":\"" + id + "\",\"restrict\":0,\"comment\":\"\",\"tags\":[]}",
Encoding.UTF8,
Configs.AcceptJson);
var result = HttpUtility.LoadObject<IllustResponse<BookmarkResultData>>(
null,
Configs.BookmarkAdd,
Configs.Referer,
out string error,
force: true,
post: content);
if (error != null)
{
App.DebugPrint($"failed to add bookmark, error: {error}");
}
else if (result == null || result.error || result.body == null)
{
App.DebugPrint($"failed to add bookmark, message: {result?.message}");
}
else
{
#if LOG
App.DebugPrint($"successs, bookmark id: {result.body.last_bookmark_id}, status: {result.body.stacc_status_id}");
#endif
return result.body.last_bookmark_id;
}
return null;
}
public static bool DeleteBookmark(string id)
{
if (string.IsNullOrEmpty(Configs.CsrfToken))
{
return false;
}
var content = new StringContent(
"mode=delete_illust_bookmark&bookmark_id=" + id,
Encoding.UTF8,
Configs.AcceptUrlEncoded);
var result = HttpUtility.LoadObject<object>(
null,
Configs.BookmarkRpc,
Configs.Referer,
out string error,
force: true,
nojson: true,
post: content);
if (error != null)
{
App.DebugPrint($"failed to delete bookmark, error: {error}");
return false;
}
else if (result == null)
{
App.DebugPrint("failed to delete bookmark, result is null");
return false;
}
#if LOG
App.DebugPrint($"successs, delete bookmark");
#endif
return true;
}
public static IllustPreloadBody LoadIllustPreloadData(string id, bool downloading, bool force = false)
{
var file = Path.Combine(CacheFolder, preloadsFolder, $"{id}.json");
IllustPreloadBody result;
if (!force)
{
result = ReadObject<IllustPreloadBody>(file);
if (result != null)
{
return result;
}
else if (!downloading)
{
return null;
}
}
if (downloading)
{
result = HttpUtility.LoadObject<IllustPreloadBody>(
file,
string.Format(Configs.UrlIllust, id),
null,
out _,
force: force,
action: content =>
{
var index = content.IndexOf(Configs.SuffixPreload);
if (index > 0)
{
index += Configs.SuffixPreloadLength;
var end = content.IndexOf('\'', index);
if (end > index)
{
content = content.Substring(index, end - index);
}
}
return content;
});
}
else
{
result = null;
}
if (result == null)
{
App.DebugPrint($"error when load preload data: force({force})");
}
return result;
}
public static IllustPageData LoadIllustPageData(string id, out string error, bool force = false)
{
var file = Path.Combine(CacheFolder, pagesFolder, $"{id}.json");
var result = HttpUtility.LoadObject<IllustPageData>(
file,
string.Format(Configs.UrlIllustPage, id),
string.Format(Configs.RefererIllust, id),
out _,
force: force);
if (result == null || result.error)
{
error = result?.message ?? "result is null";
App.DebugPrint($"error when load page data: {error}, force({force})");
return null;
}
error = null;
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),
out _,
force: force);
if (result == null || result.error)
{
App.DebugPrint($"error when load ugoira data: {result?.message}, force({force})");
}
return result;
}
public static IllustUserListData LoadIllustUserInitData(string userId)
{
var list = HttpUtility.LoadObject<IllustUserListData>(
null,
string.Format(Configs.UrlIllustUserAll, userId),
string.Format(Configs.RefererIllustUser, userId),
out _);
if (list == null || list.error)
{
App.DebugPrint($"error when load user data: {list?.message}");
}
return list;
}
public static IllustUserData LoadIllustUserData(string userId, string[] ids, bool firstPage)
{
if (ids == null || ids.Length == 0)
{
return null;
}
var ps = string.Concat(ids.Select(i => $"ids%5B%5D={i}&"));
var result = HttpUtility.LoadObject<IllustUserData>(
null,
string.Format(Configs.UrlIllustUserArtworks, userId, ps, firstPage ? 1 : 0),
string.Format(Configs.RefererIllustUser, userId),
out _);
if (result == null || result.error)
{
App.DebugPrint($"error when load user illust data: {result?.message}");
}
return result;
}
//private static readonly Regex regexIllust = new Regex(
// @"book_id\[\]"" value=""([0-9]+)"".*data-src=""([^""]+)"".*data-id=""([0-9]+)"".*" +
// @"data-tags=""([^""]+)"".*data-user-id=""([0-9]+)"".*" +
// @"class=""title"" title=""([^""]+)"".*data-user_name=""([^""]+)"".*" +
// @"_bookmark-icon-inline""></i>([0-9]+)</a>",
// RegexOptions.Compiled);
public static IllustItem[] LoadOnlineFavorites()
{
var userId = Configs.UserId;
var list = new List<IllustItem>();
int offset = 0;
while (offset >= 0)
{
var result = HttpUtility.LoadObject<IllustFavoriteData>(
null,
string.Format(Configs.UrlFavoriteList, userId, offset, 48),
string.Format(Configs.RefererFavorites, userId),
out _);
if (result == null || result.error)
{
App.DebugPrint($"error when load favorites data: {result?.message}");
}
else
{
if (offset + 48 < result.body.total)
{
offset += 48;
}
else
{
offset = -1;
}
list.AddRange(result.body.works.Select(i => i.ConvertToItem()));
}
}
return list.Where(l => l != null).ToArray();
}
public static ImageSource LoadIllustImage(string url)
{
return LoadImage(url, PersonalFolder, imageFolder, true);
}
public static ImageSource LoadPreviewImage(string url, bool downloading, string id = null, bool force = false)
{
if (downloading && Configs.DownloadIllustThreads > 1)
{
return LoadImageAsync(url, id, PersonalFolder, previewFolder, force).Result;
}
return LoadImage(url, PersonalFolder, previewFolder, downloading, force);
}
public static ImageSource LoadThumbnailImage(string url, bool downloading, bool force = false)
{
return LoadImage(url, CacheFolder, thumbFolder, downloading, force);
}
public static ImageSource LoadUserProfileImage(string url, bool downloading, bool force = false)
{
return LoadImage(url, CacheFolder, userFolder, downloading, force);
}
public static bool CheckIllustImage(string url)
{
var file = Path.Combine(PersonalFolder, imageFolder, Path.GetFileName(url));
return File.Exists(file);
}
public static bool CheckUgoiraVideo(string url)
{
var file = Path.Combine(PersonalFolder, ugoiraFolder, Path.GetFileNameWithoutExtension(url) + ".mp4");
return File.Exists(file);
}
public static string GetPreviewImagePath(string url)
{
var file = Path.Combine(PersonalFolder, previewFolder, Path.GetFileName(url));
if (File.Exists(file))
{
return file;
}
return null;
}
private static ImageSource LoadImage(string url, string working, string folder, bool downloading, bool force = false)
{
var file = Path.Combine(working, folder, Path.GetFileName(url));
ImageSource image;
if (!force && File.Exists(file))
{
try
{
image = ImageSource.FromFile(file);
}
catch (Exception ex)
{
App.DebugError("image.load", $"failed to load image from file: {file}, error: {ex.Message}");
image = null;
}
}
else
{
image = null;
}
if (downloading && image == null)
{
file = HttpUtility.DownloadImage(url, working, folder);
if (file != null)
{
return ImageSource.FromFile(file);
}
}
return image;
}
private static Task<ImageSource> LoadImageAsync(string url, string id, string working, string folder, bool force = false)
{
var file = Path.Combine(working, folder, Path.GetFileName(url));
ImageSource image;
if (!force && File.Exists(file))
{
try
{
image = ImageSource.FromFile(file);
}
catch (Exception ex)
{
App.DebugError("image.load", $"failed to load image from file: {file}, error: {ex.Message}");
image = null;
}
}
else
{
image = null;
}
if (image == null)
{
file = HttpUtility.DownloadImageAsync(url, id, working, folder).Result;
if (file != null)
{
image = ImageSource.FromFile(file);
}
}
return Task.FromResult(image);
}
}
public class IllustFavorite
{
public DateTime LastFavoriteUtc { get; set; }
public FavoriteList Illusts { get; set; }
}
public class FavoriteList : List<IllustItem>
{
public bool Changed { get; private set; }
public FavoriteList() : base() { }
public FavoriteList(IEnumerable<IllustItem> collection) : base(collection) { }
public new void Insert(int index, IllustItem item)
{
base.Insert(index, item);
Changed = true;
}
public new void InsertRange(int index, IEnumerable<IllustItem> collection)
{
base.InsertRange(index, collection);
Changed = true;
}
public new void RemoveAt(int index)
{
base.RemoveAt(index);
Changed = true;
}
public FavoriteList Reload()
{
Changed = false;
return this;
}
}
public enum SyncType
{
None = 0,
Prompt,
AutoSync
}
public static class Configs
{
public const string ProfileNameKey = "name";
public const string ProfileIdKey = "pixiv_id";
public const string ProfileImageKey = "profile_img";
public const string CookieKey = "cookies";
public const string UserIdKey = "user_id";
public const string DownloadIllustThreadsKey = "download_illust_threads";
public const string IsOnR18Key = "is_on_r18";
public const string SyncFavTypeKey = "sync_fav_type";
public const string IsProxiedKey = "is_proxied";
public const string HostKey = "host";
public const string PortKey = "port";
public const string QueryModeKey = "query_mode";
public const string QueryTypeKey = "query_type";
public const string QueryDateKey = "query_date";
public const string FavoriteTypeKey = "favorite_type";
public const int MaxPageThreads = 3;
public const int MaxThreads = 8;
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}/illustrations";
public const string RefererFavorites = "https://www.pixiv.net/users/{0}/bookmarks/artworks";
public static int DownloadIllustThreads;
public static bool IsOnR18;
public static SyncType SyncFavType;
public static WebProxy Proxy;
public static string Prefix => Proxy == null ?
"https://www.pixiv.net/" : // https://hk.tsanie.org/reverse/
"https://www.pixiv.net/";
public static string UserId { get; private set; }
public static string Cookie { get; private set; }
public static string CsrfToken;
public static void SetUserId(string userId, bool save = false)
{
UserId = userId;
if (!save)
{
return;
}
if (userId == null)
{
Preferences.Remove(UserIdKey);
}
else
{
Preferences.Set(UserIdKey, userId);
}
}
public static void SetCookie(string cookie, bool save = false)
{
Cookie = cookie;
if (!save)
{
return;
}
if (cookie == null)
{
Preferences.Remove(CookieKey);
}
else
{
Preferences.Set(CookieKey, cookie);
}
}
#if __IOS__
public static Task<bool> RequestCookieContainer(WebKit.WKHttpCookieStore cookieStore)
{
var task = new TaskCompletionSource<bool>();
cookieStore.GetAllCookies(cookies =>
{
var list = new List<string>();
foreach (var c in cookies)
{
#if DEBUG
App.DebugPrint($"domain: {c.Domain}, path: {c.Path}, {c.Name}={c.Value}, http only: {c.IsHttpOnly}, session only: {c.IsSessionOnly}");
#endif
var domain = c.Domain;
if (domain == null)
{
continue;
}
if (domain != "www.pixiv.net" && domain != ".pixiv.net")
{
continue;
}
list.Add($"{c.Name}={c.Value}");
}
var cookie = string.Join("; ", list);
Cookie = cookie;
Preferences.Set(CookieKey, cookie);
task.SetResult(true);
});
return task.Task;
}
#endif
public const string SuffixGlobal = " id=\"meta-global-data\" content='";
public const int SuffixGlobalLength = 32;
public const string SuffixPreload = " id=\"meta-preload-data\" content='";
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=illust&is_first_page={2}&lang=zh";
public static string UrlIllustPage => Prefix + "ajax/illust/{0}/pages?lang=zh";
public static string UrlIllustUgoira => Prefix + "ajax/illust/{0}/ugoira_meta?lang=zh";
public static string UrlIllustRecommendsInit => Prefix + "ajax/illust/{0}/recommend/init?limit=18&lang=zh";
public static string UrlIllustRecommendsList => Prefix + "ajax/illust/recommend/illusts?{0}lang=zh";
public static string UrlFavoriteList => Prefix + "ajax/user/{0}/illusts/bookmarks?tag=&offset={1}&limit={2}&rest=show&lang=zh";
public static string BookmarkAdd => Prefix + "ajax/illusts/bookmarks/add";
public static string BookmarkRpc => Prefix + "rpc/index.php";
public const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 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 AcceptPureImage = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8";
public const string AcceptJson = "application/json";
public const string AcceptUrlEncoded = "application/x-www-form-urlencoded";
//public const string AcceptEncoding = "gzip, deflate";
public const string AcceptLanguage = "zh-cn";
private const string URL_PREVIEW = "https://i.pximg.net/c/360x360_70";
public static string GetThumbnailUrl(string url)
{
if (url == null)
{
return null;
}
url = url.ToLower().Replace("/custom-thumb/", "/img-master/");
var index = url.LastIndexOf("_square1200.jpg");
if (index < 0)
{
index = url.LastIndexOf("_custom1200.jpg");
}
if (index > 0)
{
url = url.Substring(0, index) + "_master1200.jpg";
}
var start = url.IndexOf("/img-master/");
if (start > 0)
{
url = URL_PREVIEW + url.Substring(start);
}
return url;
}
}
public static class Routes
{
public const string Illust = "illust";
public const string Detail = "detail";
public const string Follow = "follow";
public const string Recommends = "recommends";
public const string ByUser = "byuser";
public const string Ranking = "ranking";
public const string Favorites = "favorites";
public const string Option = "option";
}
}

605
Gallery/Utils/Ugoira.cs Executable file
View File

@ -0,0 +1,605 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Gallery.Illust;
using Xamarin.Forms;
using System.Linq;
#if __IOS__
using AVFoundation;
using CoreGraphics;
using CoreMedia;
using CoreVideo;
using Foundation;
using UIKit;
#endif
namespace Gallery.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;
private ParallelTask downloadTask;
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;
Task.Run(LoadFrames);
}
public void Dispose()
{
if (IsPlaying)
{
TogglePlay(false);
}
if (downloadTask != null)
{
downloadTask.Dispose();
downloadTask = null;
}
ClearTimer();
}
private void ClearTimer()
{
lock (sync)
{
if (timer != null)
{
timer.Dispose();
timer = null;
}
}
}
public void TogglePlay(bool flag)
{
if (IsPlaying == flag)
{
return;
}
ClearTimer();
if (flag)
{
timer = new Timer(OnTimerCallback, null, 0, Timeout.Infinite);
IsPlaying = true;
}
else
{
IsPlaying = false;
}
}
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)
{
lock (sync)
{
if (timer == null)
{
return;
}
}
// 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;
if (timer != null && IsPlaying)
{
timer.Change(info.delay, Timeout.Infinite);
}
}
private void LoadFrames()
{
var zip = Path.GetFileName(ugoira.originalSrc);
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)
{
frame.FilePath = image;
frames[i] = image;
}
else
{
frame.Incompleted = true;
download = true;
}
}
if (download)
{
// need download
var url = ugoira.originalSrc;
var id = detailItem.Id;
var (size, lastModified, client) = HttpUtility.GetUgoiraHeader(url, id);
#if LOG
App.DebugPrint($"starting download ugoira: {size} bytes, last modified: {lastModified}");
#endif
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);
}
}
}
}
if (downloadTask != null)
{
downloadTask.Dispose();
downloadTask = null;
}
downloadTask = ParallelTask.Start("ugoira.download", 0, inSegs.Count, 3, i =>
{
var seg = inSegs[i];
#if DEBUG
App.DebugPrint($"start to download segment #{seg.Index}, from {seg.From} to {seg.To} / {size}");
#endif
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))
{
lock (sync)
{
if (timer == null)
{
return;
}
}
Thread.Sleep(DELAY);
}
var file = ExtractImage(zip, data, frame.Offset);
if (file != null)
{
frame.FilePath = file;
frames[i] = ImageSource.FromFile(file);
frame.Incompleted = false;
}
}
}
#if LOG
App.DebugPrint("load frames over");
#endif
}
}
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;
}
}
#if __IOS__
public async Task<string> ExportVideo()
{
if (ugoira == null || ugoira.frames.Any(f => f.Incompleted))
{
return null;
}
var file = Stores.GetUgoiraPath(ugoira.originalSrc, ".mp4");
if (File.Exists(file))
{
File.Delete(file);
}
var fileURL = NSUrl.FromFilename(file);
var images = ugoira.frames.Select(f => UIImage.FromFile(f.FilePath)).ToArray();
var videoSettings = new NSMutableDictionary
{
{ AVVideo.CodecKey, AVVideo.CodecH264 },
{ AVVideo.WidthKey, NSNumber.FromNFloat(images[0].Size.Width) },
{ AVVideo.HeightKey, NSNumber.FromNFloat(images[0].Size.Height) }
};
var videoWriter = new AVAssetWriter(fileURL, AVFileType.Mpeg4, out var err);
if (err != null)
{
App.DebugError("export.video", $"failed to create an AVAssetWriter: {err}");
return null;
}
var writerInput = new AVAssetWriterInput(AVMediaType.Video, new AVVideoSettingsCompressed(videoSettings));
var sourcePixelBufferAttributes = new NSMutableDictionary
{
{ CVPixelBuffer.PixelFormatTypeKey, NSNumber.FromInt32((int)CVPixelFormatType.CV32ARGB) }
};
var pixelBufferAdaptor = new AVAssetWriterInputPixelBufferAdaptor(writerInput, sourcePixelBufferAttributes);
videoWriter.AddInput(writerInput);
if (videoWriter.StartWriting())
{
videoWriter.StartSessionAtSourceTime(CMTime.Zero);
var lastTime = CMTime.Zero;
#if DEBUG
bool log = false;
#endif
for (int i = 0; i < images.Length; i++)
{
while (!writerInput.ReadyForMoreMediaData)
{
lock (sync)
{
if (timer == null)
{
return null;
}
}
Thread.Sleep(50);
}
// get pixel buffer and fill it with the image
using (CVPixelBuffer pixelBufferImage = pixelBufferAdaptor.PixelBufferPool.CreatePixelBuffer())
{
pixelBufferImage.Lock(CVPixelBufferLock.None);
try
{
IntPtr pxdata = pixelBufferImage.BaseAddress;
if (pxdata != IntPtr.Zero)
{
using (var rgbColorSpace = CGColorSpace.CreateDeviceRGB())
{
var cgImage = images[i].CGImage;
var width = cgImage.Width;
var height = cgImage.Height;
var bitsPerComponent = cgImage.BitsPerComponent;
var bytesPerRow = cgImage.BytesPerRow;
// padding to 64
var bytes = bytesPerRow >> 6 << 6;
if (bytes < bytesPerRow)
{
bytes += 64;
}
#if DEBUG
if (!log)
{
log = true;
App.DebugPrint($"animation, width: {width}, height: {height}, type: {cgImage.UTType}\n" +
$"bitmapInfo: {cgImage.BitmapInfo}\n" +
$"bpc: {bitsPerComponent}\n" +
$"bpp: {cgImage.BitsPerPixel}\n" +
$"calculated: {bytesPerRow} => {bytes}");
}
#endif
using (CGBitmapContext bitmapContext = new CGBitmapContext(
pxdata, width, height,
bitsPerComponent,
bytes,
rgbColorSpace,
CGImageAlphaInfo.NoneSkipFirst))
{
if (bitmapContext != null)
{
bitmapContext.DrawImage(new CGRect(0, 0, width, height), cgImage);
}
}
}
}
}
finally
{
pixelBufferImage.Unlock(CVPixelBufferLock.None);
}
// and finally append buffer to adapter
if (pixelBufferAdaptor.AssetWriterInput.ReadyForMoreMediaData && pixelBufferImage != null)
{
pixelBufferAdaptor.AppendPixelBufferWithPresentationTime(pixelBufferImage, lastTime);
}
}
var frameTime = new CMTime(ugoira.frames[i].delay, 1000);
lastTime = CMTime.Add(lastTime, frameTime);
}
writerInput.MarkAsFinished();
await videoWriter.FinishWritingAsync();
return file;
}
return null;
}
public Task<string> ExportGif()
{
if (ugoira == null || ugoira.frames.Any(f => f.Incompleted))
{
return null;
}
var file = Stores.GetUgoiraPath(ugoira.originalSrc, ".gif");
if (File.Exists(file))
{
File.Delete(file);
}
var fileURL = NSUrl.FromFilename(file);
var dictFile = new NSMutableDictionary();
var gifDictionaryFile = new NSMutableDictionary
{
{ ImageIO.CGImageProperties.GIFLoopCount, NSNumber.FromFloat(0f) }
};
dictFile.Add(ImageIO.CGImageProperties.GIFDictionary, gifDictionaryFile);
var dictFrame = new NSMutableDictionary();
var gifDictionaryFrame = new NSMutableDictionary
{
{ ImageIO.CGImageProperties.GIFDelayTime, NSNumber.FromFloat(ugoira.frames[0].delay / 1000f) }
};
dictFrame.Add(ImageIO.CGImageProperties.GIFDictionary, gifDictionaryFrame);
var task = new TaskCompletionSource<string>();
Xamarin.Essentials.MainThread.BeginInvokeOnMainThread(() =>
{
var images = ugoira.frames.Select(f => UIImage.FromFile(f.FilePath)).ToArray();
var imageDestination = ImageIO.CGImageDestination.Create(fileURL, MobileCoreServices.UTType.GIF, images.Length);
imageDestination.SetProperties(dictFile);
for (int i = 0; i < images.Length; i++)
{
imageDestination.AddImage(images[i].CGImage, dictFrame);
}
imageDestination.Close();
task.SetResult(file);
});
return task.Task;
}
#endif
}
public class UgoiraEventArgs : EventArgs
{
public IllustDetailItem DetailItem { get; set; }
public ImageSource Image { get; set; }
public int FrameIndex { get; set; }
}
}