rename from Pixiview to Gallery
This commit is contained in:
257
Gallery/App.cs
Executable file
257
Gallery/App.cs
Executable 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
104
Gallery/AppShell.xaml
Normal 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
187
Gallery/AppShell.xaml.cs
Normal 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
3
Gallery/AssemblyInfo.cs
Executable file
@ -0,0 +1,3 @@
|
||||
using Xamarin.Forms.Xaml;
|
||||
|
||||
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
|
122
Gallery/Gallery.projitems
Executable file
122
Gallery/Gallery.projitems
Executable 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
11
Gallery/Gallery.shproj
Executable 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>
|
81
Gallery/Illust/FavoritesPage.xaml
Executable file
81
Gallery/Illust/FavoritesPage.xaml
Executable 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>
|
463
Gallery/Illust/FavoritesPage.xaml.cs
Normal file
463
Gallery/Illust/FavoritesPage.xaml.cs
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
1002
Gallery/Illust/IllustCollectionPage.cs
Normal file
1002
Gallery/Illust/IllustCollectionPage.cs
Normal file
File diff suppressed because it is too large
Load Diff
43
Gallery/Illust/MainPage.xaml
Normal file
43
Gallery/Illust/MainPage.xaml
Normal 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>
|
138
Gallery/Illust/MainPage.xaml.cs
Normal file
138
Gallery/Illust/MainPage.xaml.cs
Normal 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
116
Gallery/Illust/RankingPage.xaml
Executable 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>
|
406
Gallery/Illust/RankingPage.xaml.cs
Executable file
406
Gallery/Illust/RankingPage.xaml.cs
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
32
Gallery/Illust/RecommendsPage.xaml
Executable file
32
Gallery/Illust/RecommendsPage.xaml
Executable 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>
|
369
Gallery/Illust/RecommendsPage.xaml.cs
Executable file
369
Gallery/Illust/RecommendsPage.xaml.cs
Executable 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; }
|
||||
}
|
||||
}
|
32
Gallery/Illust/RelatedIllustsPage.xaml
Executable file
32
Gallery/Illust/RelatedIllustsPage.xaml
Executable 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>
|
120
Gallery/Illust/RelatedIllustsPage.xaml.cs
Normal file
120
Gallery/Illust/RelatedIllustsPage.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
44
Gallery/Illust/UserIllustPage.xaml
Executable file
44
Gallery/Illust/UserIllustPage.xaml
Executable 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>
|
133
Gallery/Illust/UserIllustPage.xaml.cs
Executable file
133
Gallery/Illust/UserIllustPage.xaml.cs
Executable 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);
|
||||
}
|
||||
}
|
||||
}
|
66
Gallery/Illust/ViewIllustPage.xaml
Executable file
66
Gallery/Illust/ViewIllustPage.xaml
Executable 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>
|
897
Gallery/Illust/ViewIllustPage.xaml.cs
Executable file
897
Gallery/Illust/ViewIllustPage.xaml.cs
Executable 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
39
Gallery/Login/HybridWebView.cs
Executable 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
22
Gallery/Login/LoginPage.xaml
Executable 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
31
Gallery/Login/LoginPage.xaml.cs
Executable 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
46
Gallery/OptionPage.xaml
Executable 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
244
Gallery/OptionPage.xaml.cs
Executable 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
68
Gallery/Resources/Languages/zh-CN.xml
Executable file
68
Gallery/Resources/Languages/zh-CN.xml
Executable 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>
|
43
Gallery/Resources/PlatformCulture.cs
Executable file
43
Gallery/Resources/PlatformCulture.cs
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
134
Gallery/Resources/ResourceHelper.cs
Executable file
134
Gallery/Resources/ResourceHelper.cs
Executable 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
193
Gallery/UI/AdaptedPage.cs
Normal 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
8
Gallery/UI/BlurryPanel.cs
Executable file
@ -0,0 +1,8 @@
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Gallery.UI
|
||||
{
|
||||
public class BlurryPanel : ContentView
|
||||
{
|
||||
}
|
||||
}
|
57
Gallery/UI/CardView.cs
Executable file
57
Gallery/UI/CardView.cs
Executable 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
68
Gallery/UI/CircleUIs.cs
Executable 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
269
Gallery/UI/FlowLayout.cs
Executable 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
159
Gallery/UI/OptionCell.cs
Executable 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
75
Gallery/UI/SegmentedControl.cs
Executable 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; }
|
||||
}
|
||||
}
|
149
Gallery/UI/StyleDefinition.cs
Normal file
149
Gallery/UI/StyleDefinition.cs
Normal 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
43
Gallery/UI/Theme/DarkTheme.cs
Executable 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
43
Gallery/UI/Theme/LightTheme.cs
Executable 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
97
Gallery/UI/Theme/ThemeBase.cs
Executable 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
29
Gallery/Utils/Converters.cs
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
248
Gallery/Utils/EnvironmentService.cs
Executable file
248
Gallery/Utils/EnvironmentService.cs
Executable 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
279
Gallery/Utils/Extensions.cs
Executable 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
92
Gallery/Utils/FileStore.cs
Executable 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
|
||||
}
|
||||
}
|
||||
}
|
464
Gallery/Utils/HttpUtility.cs
Normal file
464
Gallery/Utils/HttpUtility.cs
Normal 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
321
Gallery/Utils/IllustData.cs
Normal 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
141
Gallery/Utils/IllustLegacy.cs
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
26
Gallery/Utils/LongPressEffect.cs
Executable file
26
Gallery/Utils/LongPressEffect.cs
Executable 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
863
Gallery/Utils/Stores.cs
Normal 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
605
Gallery/Utils/Ugoira.cs
Executable 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; }
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user