combine projects into one
This commit is contained in:
@@ -18,16 +18,14 @@ namespace Gallery
|
||||
public static Dictionary<string, System.DateTime> RefreshTimes { get; } = new();
|
||||
public static List<IGallerySource> GallerySources { get; } = new()
|
||||
{
|
||||
new Yandere.GallerySource(), // https://yande.re
|
||||
new Danbooru.GallerySource(), // https://danbooru.donmai.us
|
||||
new Gelbooru.GallerySource() // https://gelbooru.com
|
||||
new Sources.Yandere.GallerySource(), // https://yande.re
|
||||
new Sources.Danbooru.GallerySource(), // https://danbooru.donmai.us
|
||||
new Sources.Gelbooru.GallerySource() // https://gelbooru.com
|
||||
};
|
||||
|
||||
public App()
|
||||
{
|
||||
Preferences.Set(Config.IsProxiedKey, true);
|
||||
Preferences.Set(Config.ProxyHostKey, "192.168.25.9");
|
||||
Preferences.Set(Config.ProxyPortKey, 1081);
|
||||
//Device.SetFlags(new string[0]);
|
||||
}
|
||||
|
||||
private void InitResource()
|
||||
@@ -44,6 +42,7 @@ namespace Gallery
|
||||
private void InitPreference()
|
||||
{
|
||||
Config.Proxy = null;
|
||||
Config.DownloadThreads = Preferences.Get(Config.DownloadThreadsKey, 1);
|
||||
|
||||
var isProxied = Preferences.Get(Config.IsProxiedKey, false);
|
||||
if (isProxied)
|
||||
|
@@ -4,7 +4,8 @@
|
||||
xmlns:local="clr-namespace:Gallery"
|
||||
xmlns:r="clr-namespace:Gallery.Resources"
|
||||
xmlns:ui="clr-namespace:Gallery.Resources.UI"
|
||||
xmlns:util="clr-namespace:Gallery.Util;assembly=Gallery.Util"
|
||||
xmlns:util="clr-namespace:Gallery.Util"
|
||||
xmlns:v="clr-namespace:Gallery.Views"
|
||||
x:Class="Gallery.AppShell"
|
||||
x:Name="appShell"
|
||||
BackgroundColor="{DynamicResource NavigationColor}"
|
||||
@@ -15,18 +16,18 @@
|
||||
<Shell.Resources>
|
||||
<ResourceDictionary>
|
||||
<Style x:Key="BaseStyle" TargetType="Element">
|
||||
<Setter Property="Shell.BackgroundColor" Value="{DynamicResource NavigationColor}" />
|
||||
<Setter Property="Shell.ForegroundColor" Value="{DynamicResource TintColor}" />
|
||||
<Setter Property="Shell.TitleColor" Value="{DynamicResource TextColor}" />
|
||||
<Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" />
|
||||
<Setter Property="Shell.UnselectedColor" Value="{DynamicResource TintColor}" />
|
||||
<Setter Property="Shell.TabBarBackgroundColor" Value="{DynamicResource NavigationColor}" />
|
||||
<Setter Property="Shell.BackgroundColor" Value="{DynamicResource NavigationColor}"/>
|
||||
<Setter Property="Shell.ForegroundColor" Value="{DynamicResource TintColor}"/>
|
||||
<Setter Property="Shell.TitleColor" Value="{DynamicResource TextColor}"/>
|
||||
<Setter Property="Shell.DisabledColor" Value="#B4FFFFFF"/>
|
||||
<Setter Property="Shell.UnselectedColor" Value="{DynamicResource TintColor}"/>
|
||||
<Setter Property="Shell.TabBarBackgroundColor" Value="{DynamicResource NavigationColor}"/>
|
||||
<Setter Property="Shell.TabBarForegroundColor" Value="{DynamicResource TintColor}"/>
|
||||
<Setter Property="Shell.TabBarUnselectedColor" Value="{DynamicResource TintColor}"/>
|
||||
<Setter Property="Shell.TabBarTitleColor" Value="{DynamicResource TextColor}"/>
|
||||
</Style>
|
||||
<Style TargetType="TabBar" BasedOn="{StaticResource BaseStyle}" />
|
||||
<Style TargetType="FlyoutItem" BasedOn="{StaticResource BaseStyle}" />
|
||||
<Style TargetType="TabBar" BasedOn="{StaticResource BaseStyle}"/>
|
||||
<Style TargetType="FlyoutItem" BasedOn="{StaticResource BaseStyle}"/>
|
||||
<!--
|
||||
Custom Style you can apply to any Flyout Item
|
||||
<Style Class="MenuItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
|
||||
@@ -35,7 +36,7 @@
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal">
|
||||
<VisualState.Setters>
|
||||
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{DynamicResource Primary}" />
|
||||
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{DynamicResource Primary}"/>
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
@@ -109,10 +110,17 @@
|
||||
-->
|
||||
<FlyoutItem x:Name="flyoutItems"
|
||||
FlyoutDisplayOptions="AsMultipleItems"
|
||||
Route="{x:Static util:Routes.Gallery}" />
|
||||
Route="{x:Static util:Routes.Gallery}"/>
|
||||
<FlyoutItem FlyoutIcon="{DynamicResource FontIconOption}"
|
||||
Title="{r:Text Option}"
|
||||
Route="{x:Static util:Routes.Option}">
|
||||
<Tab>
|
||||
<ShellContent ContentTemplate="{DataTemplate v:OptionPage}"/>
|
||||
</Tab>
|
||||
</FlyoutItem>
|
||||
|
||||
<!-- When the Flyout is visible this will be a menu item you can tie a click behavior to -->
|
||||
<!--<MenuItem Text="Logout" StyleClass="MenuItemLayoutStyle" Clicked="OnMenuItemClicked" />-->
|
||||
<!--<MenuItem Text="Logout" StyleClass="MenuItemLayoutStyle" Clicked="OnMenuItemClicked"/>-->
|
||||
|
||||
<!--
|
||||
TabBar lets you define content that won't show up in a flyout menu. When this content is active
|
||||
@@ -121,7 +129,7 @@
|
||||
content you can do so by calling
|
||||
await Shell.Current.GoToAsync("//LoginPage");
|
||||
<TabBar>
|
||||
<ShellContent Route="LoginPage" ContentTemplate="{DataTemplate local:LoginPage}" />
|
||||
<ShellContent Route="LoginPage" ContentTemplate="{DataTemplate local:LoginPage}"/>
|
||||
</TabBar>
|
||||
-->
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using Gallery.Resources;
|
||||
using Gallery.Resources.UI;
|
||||
using Gallery.Resources.UI;
|
||||
using Gallery.Util;
|
||||
using Gallery.Util.Interface;
|
||||
using Gallery.Views;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@@ -27,9 +26,14 @@ namespace Gallery
|
||||
|
||||
private void InitFlyouts()
|
||||
{
|
||||
IGallerySource @default = null;
|
||||
foreach (var source in App.GallerySources)
|
||||
{
|
||||
var s = source;
|
||||
if (@default == null)
|
||||
{
|
||||
@default = s;
|
||||
}
|
||||
var tab = new Tab
|
||||
{
|
||||
Title = source.Name,
|
||||
@@ -45,6 +49,9 @@ namespace Gallery
|
||||
.DynamicResource(BaseShellItem.FlyoutIconProperty, source.FlyoutIconKey);
|
||||
flyoutItems.Items.Add(tab);
|
||||
}
|
||||
|
||||
var route = @default?.Route ?? Routes.Option;
|
||||
GoToAsync($"//{route}");
|
||||
}
|
||||
|
||||
public void SetNavigationBarHeight(double height)
|
||||
|
@@ -28,6 +28,23 @@
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Views\GalleryPage.xaml.cs">
|
||||
<DependentUpon>GalleryPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Views\OptionPage.xaml.cs">
|
||||
<DependentUpon>OptionPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Views\GalleryItemPage.xaml.cs">
|
||||
<DependentUpon>GalleryItemPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Util\Interface\IGallerySource.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Util\Model\GalleryItem.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Util\Extensions.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Util\Log.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Util\NetHelper.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Util\ParallelTask.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Util\Store.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Sources\Yandere\GallerySource.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Sources\Yandere\YandereItem.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Sources\Danbooru\GallerySource.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Sources\Gelbooru\GallerySource.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Services\" />
|
||||
@@ -36,6 +53,13 @@
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Resources\Theme\" />
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Resources\Languages\" />
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Resources\UI\" />
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Util\" />
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Util\Interface\" />
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Util\Model\" />
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Sources\" />
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Sources\Yandere\" />
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Sources\Danbooru\" />
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Sources\Gelbooru\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="$(MSBuildThisFileDirectory)AppShell.xaml">
|
||||
@@ -47,5 +71,13 @@
|
||||
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Resources\Languages\zh-CN.xml" />
|
||||
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\OptionPage.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\GalleryItemPage.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
@@ -1,4 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<root>
|
||||
<Title>Gallery</Title>
|
||||
<Option>选项</Option>
|
||||
<About>关于</About>
|
||||
<Version>版本</Version>
|
||||
<Gallery>图库</Gallery>
|
||||
<DownloadThreads>多线程下载</DownloadThreads>
|
||||
<Proxy>代理</Proxy>
|
||||
<Enabled>启用</Enabled>
|
||||
<Detail>详细</Detail>
|
||||
<ProxyHost>代理主机</ProxyHost>
|
||||
<ProxyPort>代理端口</ProxyPort>
|
||||
</root>
|
@@ -22,6 +22,7 @@ namespace Gallery.Resources.Theme
|
||||
public const string ScreenBottomPadding = nameof(ScreenBottomPadding);
|
||||
|
||||
public const string IconClose = nameof(IconClose);
|
||||
public const string FontIconOption = nameof(FontIconOption);
|
||||
public const string FontIconRefresh = nameof(FontIconRefresh);
|
||||
|
||||
protected void InitResources()
|
||||
@@ -31,6 +32,7 @@ namespace Gallery.Resources.Theme
|
||||
Add(IconSolidFamily, Definition.IconSolidFamily);
|
||||
Add(ScreenBottomPadding, Definition.ScreenBottomPadding);
|
||||
|
||||
Add(FontIconOption, GetFontIcon(Definition.IconOption, Definition.IconSolidFamily));
|
||||
Add(FontIconRefresh, GetFontIcon(Definition.IconRefresh, Definition.IconSolidFamily));
|
||||
|
||||
Add(IconClose, Definition.IconClose);
|
||||
|
@@ -31,6 +31,7 @@ namespace Gallery.Resources.UI
|
||||
public const string IconLeft = "\uf053";
|
||||
#endif
|
||||
|
||||
public const string IconOption = "\uf013";
|
||||
public const string IconRefresh = "\uf2f9";
|
||||
public const string IconLove = "\uf004";
|
||||
public const string IconCircleLove = "\uf4c7";
|
||||
|
75
Gallery.Share/Sources/Danbooru/GallerySource.cs
Normal file
75
Gallery.Share/Sources/Danbooru/GallerySource.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Gallery.Util;
|
||||
using Gallery.Util.Interface;
|
||||
using Gallery.Util.Model;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Gallery.Sources.Danbooru
|
||||
{
|
||||
public class GallerySource : IGallerySource
|
||||
{
|
||||
public string Name => "Danbooru";
|
||||
public string Route => "danbooru";
|
||||
public string FlyoutIconKey => "Danbooru";
|
||||
public string HomePage => "https://danbooru.donmai.us";
|
||||
|
||||
public async Task<GalleryItem[]> GetRecentItemsAsync(int page)
|
||||
{
|
||||
var url = $"https://danbooru.donmai.us/posts?page={page}";
|
||||
var (result, error) = await NetHelper.RequestObject(url, @return: content => ResolveGalleryItems(content));
|
||||
|
||||
if (result == null || !string.IsNullOrEmpty(error))
|
||||
{
|
||||
Log.Error("danbooru.content.load", $"failed to load content array, error: {error}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private GalleryItem[] ResolveGalleryItems(string content)
|
||||
{
|
||||
var regex = new Regex(
|
||||
@"<article id=""post_\d+"".+?data-id=""(\d+)"".+?data-tags=""([^""]+?)"".+?" +
|
||||
@"data-width=""(\d+?)"" data-height=""(\d+?)"".+?data-source=""([^""]+?)"".+?" +
|
||||
@"data-uploader-id=""(\d+?)"".+?data-normalized-source=""([^""]+?)"".+?" +
|
||||
@"data-file-url=""([^""]+?)"".+?data-large-file-url=""([^""]+?)""", RegexOptions.Multiline);
|
||||
var matches = regex.Matches(content);
|
||||
var items = new GalleryItem[matches.Count];
|
||||
for (var i = 0; i < items.Length; i++)
|
||||
{
|
||||
var g = matches[i].Groups;
|
||||
items[i] = new GalleryItem(int.Parse(g[1].Value))
|
||||
{
|
||||
Tags = g[2].Value.Split(' '),
|
||||
Width = int.Parse(g[3].Value),
|
||||
Height = int.Parse(g[4].Value),
|
||||
UserId = g[6].Value,
|
||||
Source = g[7].Value,
|
||||
RawUrl = g[8].Value,
|
||||
PreviewUrl = g[9].Value
|
||||
};
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public void SetCookie()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark)
|
||||
{
|
||||
var icon = new FontImageSource
|
||||
{
|
||||
FontFamily = family,
|
||||
Glyph = "\uf5d2",
|
||||
Size = 18.0
|
||||
};
|
||||
light.Add(FlyoutIconKey, icon);
|
||||
dark.Add(FlyoutIconKey, icon);
|
||||
}
|
||||
}
|
||||
}
|
71
Gallery.Share/Sources/Gelbooru/GallerySource.cs
Normal file
71
Gallery.Share/Sources/Gelbooru/GallerySource.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Gallery.Util;
|
||||
using Gallery.Util.Interface;
|
||||
using Gallery.Util.Model;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Gallery.Sources.Gelbooru
|
||||
{
|
||||
public class GallerySource : IGallerySource
|
||||
{
|
||||
public string Name => "Gelbooru";
|
||||
public string Route => "gelbooru";
|
||||
public string FlyoutIconKey => "Gelbooru";
|
||||
public string HomePage => "https://gelbooru.com";
|
||||
|
||||
public async Task<GalleryItem[]> GetRecentItemsAsync(int page)
|
||||
{
|
||||
var offset = (page - 1) * 42;
|
||||
var url = $"https://gelbooru.com/index.php?page=post&s=list&tags=all&pid={offset}";
|
||||
var (result, error) = await NetHelper.RequestObject(url, @return: content => ResolveGalleryItems(content));
|
||||
|
||||
if (result == null || !string.IsNullOrEmpty(error))
|
||||
{
|
||||
Log.Error("gelbooru.content.load", $"failed to load content array, error: {error}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private GalleryItem[] ResolveGalleryItems(string content)
|
||||
{
|
||||
var regex = new Regex(
|
||||
@"<article(.|\n)+?<a id=""p(\d+?)"" href=""([^""]+?)"">(.|\n)+?<img src=""([^""]+?)"" title=""([^""]+?)""",
|
||||
RegexOptions.Multiline);
|
||||
var matches = regex.Matches(content);
|
||||
var items = new GalleryItem[matches.Count];
|
||||
for (var i = 0; i < items.Length; i++)
|
||||
{
|
||||
var g = matches[i].Groups;
|
||||
items[i] = new GalleryItem(int.Parse(g[2].Value))
|
||||
{
|
||||
RawUrl = g[3].Value,
|
||||
PreviewUrl = g[5].Value,
|
||||
Tags = g[6].Value.Split(' '),
|
||||
IsRawPage = true
|
||||
};
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public void SetCookie()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark)
|
||||
{
|
||||
var icon = new FontImageSource
|
||||
{
|
||||
FontFamily = family,
|
||||
Glyph = "\uf5d2",
|
||||
Size = 18.0
|
||||
};
|
||||
light.Add(FlyoutIconKey, icon);
|
||||
dark.Add(FlyoutIconKey, icon);
|
||||
}
|
||||
}
|
||||
}
|
80
Gallery.Share/Sources/Yandere/GallerySource.cs
Normal file
80
Gallery.Share/Sources/Yandere/GallerySource.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Gallery.Util;
|
||||
using Gallery.Util.Interface;
|
||||
using Gallery.Util.Model;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Gallery.Sources.Yandere
|
||||
{
|
||||
public class GallerySource : IGallerySource
|
||||
{
|
||||
public string Name => "Yande.re";
|
||||
public string Route => "yandere";
|
||||
public string FlyoutIconKey => "Yandere";
|
||||
public string HomePage => "https://yande.re";
|
||||
|
||||
public async Task<GalleryItem[]> GetRecentItemsAsync(int page)
|
||||
{
|
||||
var url = $"https://yande.re/post?page={page}";
|
||||
var (result, error) = await NetHelper.RequestObject<YandereItem[]>(url, contentHandler: ContentHandler);
|
||||
|
||||
if (result == null || !string.IsNullOrEmpty(error))
|
||||
{
|
||||
Log.Error("yandere.content.load", $"failed to load content array, error: {error}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var items = new GalleryItem[result.Length];
|
||||
for (var i = 0; i < items.Length; i++)
|
||||
{
|
||||
var y = result[i];
|
||||
var item = new GalleryItem(y.id)
|
||||
{
|
||||
Tags = y.tags?.Split(' '),
|
||||
CreatedTime = y.created_at.ToLocalTime(),
|
||||
UpdatedTime = y.updated_at.ToLocalTime(),
|
||||
UserId = y.creator_id.ToString(),
|
||||
UserName = y.author,
|
||||
Source = y.source,
|
||||
PreviewUrl = y.preview_url,
|
||||
RawUrl = y.file_url,
|
||||
Width = y.width,
|
||||
Height = y.height
|
||||
};
|
||||
items[i] = item;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private string ContentHandler(string content)
|
||||
{
|
||||
var regex = new Regex(@"Post\.register\((\{.+\})\)\s*$", RegexOptions.Multiline);
|
||||
var matches = regex.Matches(content);
|
||||
var array = new string[matches.Count];
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
array[i] = matches[i].Groups[1].Value;
|
||||
}
|
||||
return $"[{string.Join(',', array)}]";
|
||||
}
|
||||
|
||||
public void SetCookie()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark)
|
||||
{
|
||||
var icon = new FontImageSource
|
||||
{
|
||||
FontFamily = family,
|
||||
Glyph = "\uf302",
|
||||
Size = 18.0
|
||||
};
|
||||
light.Add(FlyoutIconKey, icon);
|
||||
dark.Add(FlyoutIconKey, icon);
|
||||
}
|
||||
}
|
||||
}
|
29
Gallery.Share/Sources/Yandere/YandereItem.cs
Normal file
29
Gallery.Share/Sources/Yandere/YandereItem.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace Gallery.Sources.Yandere
|
||||
{
|
||||
public class YandereItem
|
||||
{
|
||||
#pragma warning disable IDE1006 // Naming Styles
|
||||
|
||||
public long id { get; set; }
|
||||
public string tags { get; set; }
|
||||
public long created_at { get; set; }
|
||||
public long updated_at { get; set; }
|
||||
public long creator_id { get; set; }
|
||||
public string author { get; set; }
|
||||
public string source { get; set; }
|
||||
public int score { get; set; }
|
||||
public int file_size { get; set; }
|
||||
public string file_ext { get; set; }
|
||||
public string file_url { get; set; }
|
||||
public string preview_url { get; set; }
|
||||
public int actual_preview_width { get; set; }
|
||||
public int actual_preview_height { get; set; }
|
||||
public string rating { get; set; }
|
||||
public int width { get; set; }
|
||||
public int height { get; set; }
|
||||
|
||||
#pragma warning restore IDE1006 // Naming Styles
|
||||
}
|
||||
}
|
102
Gallery.Share/Util/Converter.cs
Normal file
102
Gallery.Share/Util/Converter.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Gallery.Util.Model;
|
||||
|
||||
namespace Gallery.Util
|
||||
{
|
||||
public class GalleryItemConverter : JsonConverter<GalleryItem>
|
||||
{
|
||||
public override GalleryItem Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var item = new GalleryItem();
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndObject)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||
{
|
||||
var name = reader.GetString();
|
||||
reader.Read();
|
||||
switch (name)
|
||||
{
|
||||
case nameof(GalleryItem.Id): item.Id = reader.GetInt64(); break;
|
||||
case nameof(GalleryItem.Tags):
|
||||
if (reader.TokenType == JsonTokenType.StartArray)
|
||||
{
|
||||
var tags = new List<string>();
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndArray)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
tags.Add(reader.GetString());
|
||||
}
|
||||
}
|
||||
item.Tags = tags.ToArray();
|
||||
}
|
||||
break;
|
||||
case nameof(GalleryItem.CreatedTime): item.CreatedTime = reader.GetDateTime(); break;
|
||||
case nameof(GalleryItem.UpdatedTime): item.UpdatedTime = reader.GetDateTime(); break;
|
||||
case nameof(GalleryItem.UserId): item.UserId = reader.GetString(); break;
|
||||
case nameof(GalleryItem.UserName): item.UserName = reader.GetString(); break;
|
||||
case nameof(GalleryItem.Source): item.Source = reader.GetString(); break;
|
||||
case nameof(GalleryItem.PreviewUrl): item.PreviewUrl = reader.GetString(); break;
|
||||
case nameof(GalleryItem.RawUrl): item.RawUrl = reader.GetString(); break;
|
||||
case nameof(GalleryItem.Width): item.Width = reader.GetInt32(); break;
|
||||
case nameof(GalleryItem.Height): item.Height = reader.GetInt32(); break;
|
||||
case nameof(GalleryItem.BookmarkId): item.BookmarkId = reader.GetString(); break;
|
||||
case nameof(GalleryItem.IsRawPage): item.IsRawPage = reader.GetBoolean(); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, GalleryItem value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteStartObject();
|
||||
|
||||
writer.WriteNumber(nameof(GalleryItem.Id), value.Id);
|
||||
if (value.Tags != null)
|
||||
{
|
||||
writer.WritePropertyName(nameof(GalleryItem.Tags));
|
||||
writer.WriteStartArray();
|
||||
for (var i = 0; i < value.Tags.Length; i++)
|
||||
{
|
||||
writer.WriteStringValue(value.Tags[i]);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
writer.WriteString(nameof(GalleryItem.CreatedTime), value.CreatedTime);
|
||||
writer.WriteString(nameof(GalleryItem.UpdatedTime), value.UpdatedTime);
|
||||
writer.WriteString(nameof(GalleryItem.UserId), value.UserId);
|
||||
writer.WriteString(nameof(GalleryItem.UserName), value.UserName);
|
||||
writer.WriteString(nameof(GalleryItem.Source), value.Source);
|
||||
writer.WriteString(nameof(GalleryItem.PreviewUrl), value.PreviewUrl);
|
||||
writer.WriteString(nameof(GalleryItem.RawUrl), value.RawUrl);
|
||||
writer.WriteNumber(nameof(GalleryItem.Width), value.Width);
|
||||
writer.WriteNumber(nameof(GalleryItem.Height), value.Height);
|
||||
writer.WriteString(nameof(GalleryItem.BookmarkId), value.BookmarkId);
|
||||
writer.WriteBoolean(nameof(GalleryItem.IsRawPage), value.IsRawPage);
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
}
|
67
Gallery.Share/Util/Extensions.cs
Normal file
67
Gallery.Share/Util/Extensions.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
|
||||
namespace Gallery.Util
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
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 static DateTime ToLocalTime(this long time)
|
||||
{
|
||||
//return new DateTime(1970, 1, 1, 0, 0, 0).AddMilliseconds(time).ToLocalTime();
|
||||
return new DateTime(621355968000000000L + time * 10000000).ToLocalTime();
|
||||
}
|
||||
|
||||
public static long ToTimestamp(this DateTime datetime)
|
||||
{
|
||||
var ticks = datetime.Ticks;
|
||||
return (ticks - 621355968000000000L) / 10000000;
|
||||
}
|
||||
}
|
||||
}
|
23
Gallery.Share/Util/Interface/IGallerySource.cs
Normal file
23
Gallery.Share/Util/Interface/IGallerySource.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Threading.Tasks;
|
||||
using Gallery.Util.Model;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Gallery.Util.Interface
|
||||
{
|
||||
public interface IGallerySource
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
string Route { get; }
|
||||
|
||||
string FlyoutIconKey { get; }
|
||||
|
||||
string HomePage { get; }
|
||||
|
||||
void SetCookie();
|
||||
|
||||
void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark);
|
||||
|
||||
Task<GalleryItem[]> GetRecentItemsAsync(int page);
|
||||
}
|
||||
}
|
41
Gallery.Share/Util/Log.cs
Normal file
41
Gallery.Share/Util/Log.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Gallery.Util
|
||||
{
|
||||
public static class Log
|
||||
{
|
||||
public static ILog Logger { get; set; } = new DefaultLogger();
|
||||
|
||||
public static void Print(string message)
|
||||
{
|
||||
Logger?.Print(message);
|
||||
}
|
||||
|
||||
public static void Error(string category, string message)
|
||||
{
|
||||
Logger?.Error(category, message);
|
||||
}
|
||||
}
|
||||
|
||||
public class DefaultLogger : ILog
|
||||
{
|
||||
public void Print(string message)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[{0:HH:mm:ss.fff}] - {1}", DateTime.Now, message);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Error(string category, string message)
|
||||
{
|
||||
Debug.Fail(string.Format("[{0:HH:mm:ss.fff}] - {1} - {2}", DateTime.Now, category, message));
|
||||
}
|
||||
}
|
||||
|
||||
public interface ILog
|
||||
{
|
||||
void Print(string message);
|
||||
void Error(string category, string message);
|
||||
}
|
||||
}
|
131
Gallery.Share/Util/Model/GalleryItem.cs
Normal file
131
Gallery.Share/Util/Model/GalleryItem.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
//using System.Text.Json.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Gallery.Util.Model
|
||||
{
|
||||
//[JsonConverter(typeof(GalleryItemConverter))]
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public class GalleryItem : BindableObject
|
||||
{
|
||||
const double PREVIEW_WIDTH = 200.0;
|
||||
|
||||
public static readonly BindableProperty TagDescriptionProperty = BindableProperty.Create(nameof(TagDescription), typeof(string), typeof(GalleryItem));
|
||||
public static readonly BindableProperty PreviewImageProperty = BindableProperty.Create(nameof(PreviewImage), typeof(ImageSource), typeof(GalleryItem));
|
||||
public static readonly BindableProperty UserNameProperty = BindableProperty.Create(nameof(UserName), typeof(string), typeof(GalleryItem));
|
||||
public static readonly BindableProperty CreatedTimeProperty = BindableProperty.Create(nameof(CreatedTime), typeof(DateTime), typeof(GalleryItem));
|
||||
public static readonly BindableProperty UpdatedTimeProperty = BindableProperty.Create(nameof(UpdatedTime), typeof(DateTime), typeof(GalleryItem));
|
||||
public static readonly BindableProperty ImageHeightProperty = BindableProperty.Create(nameof(ImageHeight), typeof(GridLength), typeof(GalleryItem),
|
||||
defaultValue: GridLength.Auto);
|
||||
public static readonly BindableProperty IsFavoriteProperty = BindableProperty.Create(nameof(IsFavorite), typeof(bool), typeof(GalleryItem));
|
||||
public static readonly BindableProperty BookmarkIdProperty = BindableProperty.Create(nameof(BookmarkId), typeof(string), typeof(GalleryItem));
|
||||
|
||||
public string TagDescription
|
||||
{
|
||||
get => (string)GetValue(TagDescriptionProperty);
|
||||
set => SetValue(TagDescriptionProperty, value);
|
||||
}
|
||||
public ImageSource PreviewImage
|
||||
{
|
||||
get => (ImageSource)GetValue(PreviewImageProperty);
|
||||
set => SetValue(PreviewImageProperty, value);
|
||||
}
|
||||
public GridLength ImageHeight
|
||||
{
|
||||
get => (GridLength)GetValue(ImageHeightProperty);
|
||||
set => SetValue(ImageHeightProperty, value);
|
||||
}
|
||||
public bool IsFavorite
|
||||
{
|
||||
get => (bool)GetValue(IsFavoriteProperty);
|
||||
set => SetValue(IsFavoriteProperty, value);
|
||||
}
|
||||
|
||||
[JsonProperty]
|
||||
public string BookmarkId
|
||||
{
|
||||
get => (string)GetValue(BookmarkIdProperty);
|
||||
set => SetValue(BookmarkIdProperty, value);
|
||||
}
|
||||
|
||||
[JsonProperty]
|
||||
public long Id { get; internal set; }
|
||||
private string[] tags;
|
||||
[JsonProperty]
|
||||
public string[] Tags
|
||||
{
|
||||
get => tags;
|
||||
set
|
||||
{
|
||||
tags = value;
|
||||
if (value != null)
|
||||
{
|
||||
TagDescription = string.Join(' ', tags);
|
||||
}
|
||||
else
|
||||
{
|
||||
TagDescription = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
[JsonProperty]
|
||||
public DateTime CreatedTime { get; set; }
|
||||
[JsonProperty]
|
||||
public DateTime UpdatedTime { get; set; }
|
||||
[JsonProperty]
|
||||
public string UserId { get; set; }
|
||||
[JsonProperty]
|
||||
public string UserName { get; set; }
|
||||
[JsonProperty]
|
||||
public string Source { get; set; }
|
||||
[JsonProperty]
|
||||
public string PreviewUrl { get; set; }
|
||||
[JsonProperty]
|
||||
public string RawUrl { get; set; }
|
||||
[JsonProperty]
|
||||
public bool IsRawPage { get; set; }
|
||||
|
||||
private int width;
|
||||
private int height;
|
||||
[JsonProperty]
|
||||
public int Width
|
||||
{
|
||||
get => width;
|
||||
set
|
||||
{
|
||||
width = value;
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
ImageHeight = new GridLength(PREVIEW_WIDTH * height / width);
|
||||
}
|
||||
}
|
||||
}
|
||||
[JsonProperty]
|
||||
public int Height
|
||||
{
|
||||
get => height;
|
||||
set
|
||||
{
|
||||
height = value;
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
ImageHeight = new GridLength(PREVIEW_WIDTH * height / width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal GalleryItem() { }
|
||||
|
||||
public GalleryItem(long id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var source = string.IsNullOrEmpty(Source) ? RawUrl : Source;
|
||||
return $"{Id}, {source}";
|
||||
}
|
||||
}
|
||||
}
|
283
Gallery.Share/Util/NetHelper.cs
Normal file
283
Gallery.Share/Util/NetHelper.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Xamarin.Essentials;
|
||||
|
||||
namespace Gallery.Util
|
||||
{
|
||||
public static class NetHelper
|
||||
{
|
||||
public static bool NetworkAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return Connectivity.NetworkAccess == NetworkAccess.Internet
|
||||
|| Connectivity.NetworkAccess == NetworkAccess.ConstrainedInternet;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<(T result, string error)> RequestObject<T>(string url,
|
||||
string referer = null,
|
||||
HttpContent post = null,
|
||||
Action<HttpRequestHeaders> headerHandler = null,
|
||||
Func<string, string> contentHandler = null,
|
||||
Func<string, T> @return = null)
|
||||
{
|
||||
var response = await Request(url, headers =>
|
||||
{
|
||||
if (referer != null)
|
||||
{
|
||||
headers.Referrer = new Uri(referer);
|
||||
}
|
||||
headers.Add("User-Agent", Config.UserAgent);
|
||||
headerHandler?.Invoke(headers);
|
||||
}, post);
|
||||
if (response == null)
|
||||
{
|
||||
return (default, "response is null");
|
||||
}
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Log.Print($"http failed with code: {(int)response.StatusCode} - {response.StatusCode}");
|
||||
return (default, response.StatusCode.ToString());
|
||||
}
|
||||
string content;
|
||||
using (response)
|
||||
{
|
||||
try
|
||||
{
|
||||
content = await response.Content.ReadAsStringAsync();
|
||||
if (contentHandler != null)
|
||||
{
|
||||
content = contentHandler(content);
|
||||
}
|
||||
if (@return != null)
|
||||
{
|
||||
return (@return(content), null);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("stream.load", $"failed to read stream, error: {ex.Message}");
|
||||
return (default, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
if (content == null)
|
||||
{
|
||||
content = string.Empty;
|
||||
}
|
||||
try
|
||||
{
|
||||
var result = JsonConvert.DeserializeObject<T>(content);
|
||||
return (result, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var memo = content.Length < 20 ? content : content[0..20] + "...";
|
||||
Log.Error("content.deserialize", $"failed to parse JSON object, content: {memo}, error: {ex.Message}");
|
||||
return (default, content);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<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 = await Request(url, headers =>
|
||||
{
|
||||
headers.Add("User-Agent", Config.UserAgent);
|
||||
headers.Add("Accept", Config.AcceptImage);
|
||||
});
|
||||
if (response == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
using (response)
|
||||
using (var fs = File.OpenWrite(file))
|
||||
{
|
||||
await response.Content.CopyToAsync(fs);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("image.download", ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async 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 = Config.Proxy;
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
UseCookies = false
|
||||
};
|
||||
if (proxy != null)
|
||||
{
|
||||
handler.Proxy = proxy;
|
||||
handler.UseProxy = true;
|
||||
}
|
||||
var client = new HttpClient(handler, true)
|
||||
{
|
||||
Timeout = Config.Timeout
|
||||
};
|
||||
long size;
|
||||
DateTimeOffset lastModified;
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Head, url))
|
||||
{
|
||||
var headers = request.Headers;
|
||||
headers.Add("Accept", Config.AcceptImage);
|
||||
headers.Add("Accept-Language", Config.AcceptLanguage);
|
||||
headers.Add("User-Agent", Config.UserAgent);
|
||||
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
size = response.Content.Headers.ContentLength.Value;
|
||||
lastModified = response.Content.Headers.LastModified.Value;
|
||||
#if DEBUG
|
||||
Log.Print($"content length: {size:n0} bytes, last modified: {lastModified}");
|
||||
#endif
|
||||
}
|
||||
|
||||
// segments
|
||||
const int SIZE = 150000;
|
||||
var list = new List<(long from, long to)>();
|
||||
for (long i = 0; 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, 2, i =>
|
||||
{
|
||||
var (from, to) = list[i];
|
||||
using (var request = new HttpRequestMessage(HttpMethod.Get, url))
|
||||
{
|
||||
var headers = request.Headers;
|
||||
headers.Add("Accept", Config.AcceptImage);
|
||||
headers.Add("Accept-Language", Config.AcceptLanguage);
|
||||
headers.Add("Accept-Encoding", "identity");
|
||||
headers.IfRange = new RangeConditionHeaderValue(lastModified);
|
||||
headers.Range = new RangeHeaderValue(from, to);
|
||||
headers.Add("User-Agent", Config.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
|
||||
Log.Print($"downloaded range: from({from:n0}) to ({to:n0})");
|
||||
#endif
|
||||
}
|
||||
return true;
|
||||
},
|
||||
complete: o =>
|
||||
{
|
||||
using (var fs = File.OpenWrite(file))
|
||||
{
|
||||
fs.Write(data, 0, data.Length);
|
||||
}
|
||||
task.SetResult(file);
|
||||
});
|
||||
|
||||
return await task.Task;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("image.download.async", $"failed to download image, error: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<HttpResponseMessage> Request(string url, Action<HttpRequestHeaders> headerHandler, HttpContent post = null)
|
||||
{
|
||||
#if DEBUG
|
||||
var method = post == null ? "GET" : "POST";
|
||||
Log.Print($"{method}: {url}");
|
||||
#endif
|
||||
var uri = new Uri(url);
|
||||
var proxy = Config.Proxy;
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate,
|
||||
UseCookies = false
|
||||
};
|
||||
if (proxy != null)
|
||||
{
|
||||
handler.Proxy = proxy;
|
||||
handler.UseProxy = true;
|
||||
}
|
||||
var client = new HttpClient(handler, true)
|
||||
{
|
||||
BaseAddress = new Uri($"{uri.Scheme}://{uri.Host}:{uri.Port}"),
|
||||
Timeout = Config.Timeout
|
||||
};
|
||||
return await TryCount(() =>
|
||||
{
|
||||
using var request = new HttpRequestMessage(post == null ? HttpMethod.Get : HttpMethod.Post, uri.PathAndQuery);
|
||||
var headers = request.Headers;
|
||||
headerHandler?.Invoke(headers);
|
||||
headers.Add("Accept-Language", Config.AcceptLanguage);
|
||||
if (post != null)
|
||||
{
|
||||
request.Content = post;
|
||||
}
|
||||
return client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
Log.Error("try.do", $"tries: {tries}, error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
148
Gallery.Share/Util/ParallelTask.cs
Normal file
148
Gallery.Share/Util/ParallelTask.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Gallery.Util
|
||||
{
|
||||
public class ParallelTask : IDisposable
|
||||
{
|
||||
public static ParallelTask Start(string tag, int from, int toExclusive, int maxCount, Predicate<int> action, int tagIndex = -1, WaitCallback complete = null)
|
||||
{
|
||||
if (toExclusive <= from)
|
||||
{
|
||||
if (complete != null)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(complete);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
var task = new ParallelTask(tag, from, toExclusive, maxCount, action, tagIndex, complete);
|
||||
task.Start();
|
||||
return task;
|
||||
}
|
||||
|
||||
private readonly object sync = new();
|
||||
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 WaitCallback complete;
|
||||
|
||||
private ParallelTask(string tag, int from, int to, int maxCount, Predicate<int> action, int tagIndex, WaitCallback 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);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
private void DoStart(object state)
|
||||
{
|
||||
#if DEBUG
|
||||
const long TIMEOUT = 60000L;
|
||||
var sw = new System.Diagnostics.Stopwatch();
|
||||
long lastElapsed = 0;
|
||||
sw.Start();
|
||||
#endif
|
||||
for (int i = from; i < to; i++)
|
||||
{
|
||||
var index = i;
|
||||
while (count >= max)
|
||||
{
|
||||
#if DEBUG
|
||||
var elapsed = sw.ElapsedMilliseconds;
|
||||
if (elapsed - lastElapsed > TIMEOUT)
|
||||
{
|
||||
lastElapsed = elapsed;
|
||||
Log.Print($"WARNING: parallel task ({tag}), {count} tasks in queue, cost too much time ({elapsed:n0}ms)");
|
||||
}
|
||||
#endif
|
||||
if (disposed)
|
||||
{
|
||||
#if DEBUG
|
||||
sw.Stop();
|
||||
Log.Print($"parallel task determinate, disposed ({tag}), cost time ({elapsed:n0}ms)");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
Thread.Sleep(16);
|
||||
}
|
||||
lock (sync)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
ThreadPool.QueueUserWorkItem(o =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!action(index))
|
||||
{
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"parallel.start ({tag})", $"failed to run action, index: {index}, error: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (sync)
|
||||
{
|
||||
count--;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
while (count > 0)
|
||||
{
|
||||
#if DEBUG
|
||||
var elapsed = sw.ElapsedMilliseconds;
|
||||
if (elapsed - lastElapsed > TIMEOUT)
|
||||
{
|
||||
lastElapsed = elapsed;
|
||||
Log.Print($"WARNING: parallel task ({tag}), {count} ending tasks in queue, cost too much time ({elapsed:n0}ms)");
|
||||
}
|
||||
#endif
|
||||
if (disposed)
|
||||
{
|
||||
#if DEBUG
|
||||
sw.Stop();
|
||||
Log.Print($"parallel task determinate, disposed ({tag}), ending cost time ({elapsed:n0}ms)");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
Thread.Sleep(16);
|
||||
}
|
||||
#if DEBUG
|
||||
sw.Stop();
|
||||
Log.Print($"parallel task done ({tag}), cost time ({sw.ElapsedMilliseconds:n0}ms)");
|
||||
#endif
|
||||
complete?.Invoke(null);
|
||||
}
|
||||
}
|
||||
}
|
98
Gallery.Share/Util/Store.cs
Normal file
98
Gallery.Share/Util/Store.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Gallery.Util
|
||||
{
|
||||
public static class Store
|
||||
{
|
||||
public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
|
||||
public static readonly string CacheFolder = FileSystem.CacheDirectory;
|
||||
|
||||
private const string imageFolder = "img-original";
|
||||
private const string previewFolder = "img-preview";
|
||||
|
||||
public static async Task<ImageSource> LoadRawImage(string url)
|
||||
{
|
||||
return await LoadImageAsync(url, null, PersonalFolder, imageFolder, force: true);
|
||||
}
|
||||
|
||||
public static async Task<ImageSource> LoadPreviewImage(string url, bool downloading, bool force = false)
|
||||
{
|
||||
return await LoadImage(url, CacheFolder, previewFolder, downloading, force: force);
|
||||
}
|
||||
|
||||
private static async Task<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))
|
||||
{
|
||||
image = ImageSource.FromFile(file);
|
||||
}
|
||||
else
|
||||
{
|
||||
image = null;
|
||||
}
|
||||
if (downloading && image == null)
|
||||
{
|
||||
file = await NetHelper.DownloadImage(url, working, folder);
|
||||
if (file != null)
|
||||
{
|
||||
return ImageSource.FromFile(file);
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private static async 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))
|
||||
{
|
||||
image = ImageSource.FromFile(file);
|
||||
}
|
||||
else
|
||||
{
|
||||
image = null;
|
||||
}
|
||||
if (image == null)
|
||||
{
|
||||
file = await NetHelper.DownloadImageAsync(url, id, working, folder);
|
||||
if (file != null)
|
||||
{
|
||||
image = ImageSource.FromFile(file);
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Config
|
||||
{
|
||||
public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
public const string DownloadThreadsKey = "download_threads";
|
||||
public const string IsProxiedKey = "is_proxied";
|
||||
public const string ProxyHostKey = "proxy_host";
|
||||
public const string ProxyPortKey = "proxy_port";
|
||||
|
||||
public const int MaxThreads = 8;
|
||||
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 AcceptLanguage = "zh-cn";
|
||||
public const string AcceptImage = "image/png,image/*,*/*;q=0.8";
|
||||
|
||||
public static int DownloadThreads;
|
||||
public static WebProxy Proxy;
|
||||
}
|
||||
|
||||
public static class Routes
|
||||
{
|
||||
public const string Gallery = "gallery";
|
||||
public const string Option = "option";
|
||||
}
|
||||
}
|
12
Gallery.Share/Views/GalleryItemPage.xaml
Normal file
12
Gallery.Share/Views/GalleryItemPage.xaml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<ui:AdaptedPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:ui="clr-namespace:Gallery.Resources.UI"
|
||||
x:Class="Gallery.Views.GalleryItemPage"
|
||||
x:Name="galleryItemPage"
|
||||
BindingContext="{x:Reference galleryItemPage}">
|
||||
<ContentPage.Content>
|
||||
|
||||
</ContentPage.Content>
|
||||
</ui:AdaptedPage>
|
15
Gallery.Share/Views/GalleryItemPage.xaml.cs
Normal file
15
Gallery.Share/Views/GalleryItemPage.xaml.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Gallery.Resources.UI;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Gallery.Views
|
||||
{
|
||||
public partial class GalleryItemPage : AdaptedPage
|
||||
{
|
||||
public GalleryItemPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,7 +6,8 @@
|
||||
x:Class="Gallery.Views.GalleryPage"
|
||||
x:Name="yanderePage"
|
||||
BackgroundColor="{DynamicResource WindowColor}"
|
||||
BindingContext="{x:Reference yanderePage}">
|
||||
BindingContext="{x:Reference yanderePage}"
|
||||
Title="{Binding Source.Name}">
|
||||
<ContentPage.Content>
|
||||
<Grid>
|
||||
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"
|
||||
|
38
Gallery.Share/Views/OptionPage.xaml
Normal file
38
Gallery.Share/Views/OptionPage.xaml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<ui:AdaptedPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:r="clr-namespace:Gallery.Resources"
|
||||
xmlns:ui="clr-namespace:Gallery.Resources.UI"
|
||||
x:Class="Gallery.Views.OptionPage"
|
||||
x:Name="optionPage"
|
||||
BindingContext="{x:Reference optionPage}"
|
||||
Title="{r:Text Option}">
|
||||
<ContentPage.Content>
|
||||
<TableView Intent="Settings" VerticalOptions="Start"
|
||||
BackgroundColor="{DynamicResource OptionBackColor}">
|
||||
<TableRoot>
|
||||
<TableSection Title="{r:Text About}">
|
||||
<ui:OptionTextCell Title="{r:Text Version}" Detail="{Binding Version}"/>
|
||||
</TableSection>
|
||||
<TableSection Title="{r:Text Gallery}">
|
||||
<ui:OptionEntryCell Title="{r:Text DownloadThreads}"
|
||||
Text="{Binding DownloadThreads, Mode=TwoWay}"
|
||||
Keyboard="Numeric" Placeholder="1~10"/>
|
||||
</TableSection>
|
||||
<TableSection Title="{r:Text Proxy}">
|
||||
<ui:OptionSwitchCell Title="{r:Text Enabled}"
|
||||
IsToggled="{Binding IsProxied, Mode=TwoWay}"/>
|
||||
</TableSection>
|
||||
<TableSection Title="{r:Text Detail}">
|
||||
<ui:OptionEntryCell Title="{r:Text ProxyHost}"
|
||||
Text="{Binding ProxyHost, Mode=TwoWay}"
|
||||
Keyboard="Url" Placeholder="www.example.com"/>
|
||||
<ui:OptionEntryCell Title="{r:Text ProxyPort}"
|
||||
Text="{Binding ProxyPort, Mode=TwoWay}"
|
||||
Keyboard="Numeric" Placeholder="8080"/>
|
||||
</TableSection>
|
||||
</TableRoot>
|
||||
</TableView>
|
||||
</ContentPage.Content>
|
||||
</ui:AdaptedPage>
|
152
Gallery.Share/Views/OptionPage.xaml.cs
Normal file
152
Gallery.Share/Views/OptionPage.xaml.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using Gallery.Resources.UI;
|
||||
using Gallery.Util;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Gallery.Views
|
||||
{
|
||||
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 DownloadThreadsProperty = BindableProperty.Create(nameof(DownloadThreads), typeof(string), typeof(OptionPage));
|
||||
public static readonly BindableProperty IsProxiedProperty = BindableProperty.Create(nameof(IsProxied), typeof(bool), typeof(OptionPage));
|
||||
public static readonly BindableProperty ProxyHostProperty = BindableProperty.Create(nameof(ProxyHost), typeof(string), typeof(OptionPage));
|
||||
public static readonly BindableProperty ProxyPortProperty = BindableProperty.Create(nameof(ProxyPort), typeof(string), typeof(OptionPage));
|
||||
|
||||
public string DownloadThreads
|
||||
{
|
||||
get => (string)GetValue(DownloadThreadsProperty);
|
||||
set => SetValue(DownloadThreadsProperty, value);
|
||||
}
|
||||
public bool IsProxied
|
||||
{
|
||||
get => (bool)GetValue(IsProxiedProperty);
|
||||
set => SetValue(IsProxiedProperty, value);
|
||||
}
|
||||
public string ProxyHost
|
||||
{
|
||||
get => (string)GetValue(ProxyHostProperty);
|
||||
set => SetValue(ProxyHostProperty, value);
|
||||
}
|
||||
public string ProxyPort
|
||||
{
|
||||
get => (string)GetValue(ProxyPortProperty);
|
||||
set => SetValue(ProxyPortProperty, value);
|
||||
}
|
||||
|
||||
|
||||
public OptionPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
#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})";
|
||||
DownloadThreads = Config.DownloadThreads.ToString();
|
||||
var proxy = Config.Proxy;
|
||||
if (proxy != null)
|
||||
{
|
||||
IsProxied = true;
|
||||
ProxyHost = proxy.Address.Host;
|
||||
ProxyPort = proxy.Address.Port.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
IsProxied = false;
|
||||
ProxyHost = Preferences.Get(Config.ProxyHostKey, string.Empty);
|
||||
ProxyPort = Preferences.Get(Config.ProxyPortKey, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
base.OnDisappearing();
|
||||
|
||||
var proxied = IsProxied;
|
||||
|
||||
if (int.TryParse(DownloadThreads, out int threads) && threads > 0 && threads <= 10 && threads != Config.DownloadThreads)
|
||||
{
|
||||
Preferences.Set(Config.DownloadThreadsKey, threads);
|
||||
Config.DownloadThreads = threads;
|
||||
#if DEBUG
|
||||
Log.Print($"will use {threads} threads to download image");
|
||||
#endif
|
||||
}
|
||||
|
||||
var proxy = Config.Proxy;
|
||||
var h = ProxyHost?.Trim();
|
||||
int.TryParse(ProxyPort, out int pt);
|
||||
if (proxied &&
|
||||
!string.IsNullOrEmpty(h) &&
|
||||
pt > 0 && pt < 65535)
|
||||
{
|
||||
if (proxy == null ||
|
||||
proxy.Address.Host != h ||
|
||||
proxy.Address.Port != pt)
|
||||
{
|
||||
Preferences.Set(Config.IsProxiedKey, true);
|
||||
Preferences.Set(Config.ProxyHostKey, h);
|
||||
Preferences.Set(Config.ProxyPortKey, pt);
|
||||
try
|
||||
{
|
||||
if (h.IndexOf(':') >= 0)
|
||||
{
|
||||
h = $"[{h}]";
|
||||
}
|
||||
var uri = new Uri($"http://{h}:{pt}");
|
||||
Config.Proxy = new System.Net.WebProxy(uri);
|
||||
#if DEBUG
|
||||
Log.Print($"set proxy to: {uri}");
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("on.disappearing", $"failed to parse proxy: {h}:{pt}, error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Preferences.Set(Config.IsProxiedKey, false);
|
||||
Preferences.Set(Config.ProxyHostKey, h);
|
||||
if (pt > 0)
|
||||
{
|
||||
Preferences.Set(Config.ProxyPortKey, pt);
|
||||
}
|
||||
if (proxy != null)
|
||||
{
|
||||
Config.Proxy = null;
|
||||
#if DEBUG
|
||||
Log.Print("clear proxy");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user