feature: refresh list & save to gallery

This commit is contained in:
Tsanie Lily 2020-05-06 02:21:24 +08:00
parent 190615ab03
commit 8746d311d2
14 changed files with 335 additions and 45 deletions

View File

@ -44,5 +44,7 @@
<false/> <false/>
<key>UIStatusBarStyle</key> <key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string> <string>UIStatusBarStyleDefault</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问您的图片库</string>
</dict> </dict>
</plist> </plist>

View File

@ -76,6 +76,7 @@
<Compile Include="Renderers\RoundImageRenderer.cs" /> <Compile Include="Renderers\RoundImageRenderer.cs" />
<Compile Include="GlobalSuppressions.cs" /> <Compile Include="GlobalSuppressions.cs" />
<Compile Include="Renderers\AdaptedNavigationPageRenderer.cs" /> <Compile Include="Renderers\AdaptedNavigationPageRenderer.cs" />
<Compile Include="Services\FileStore.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<InterfaceDefinition Include="Resources\LaunchScreen.storyboard" /> <InterfaceDefinition Include="Resources\LaunchScreen.storyboard" />

View File

@ -0,0 +1,45 @@
using System.Threading.Tasks;
using Pixiview.iOS.Services;
using Pixiview.Utils;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: Dependency(typeof(FileStore))]
namespace Pixiview.iOS.Services
{
public class FileStore : IFileStore
{
public Task<string> SaveImageToGalleryAsync(ImageSource image)
{
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;
}
}
}

View File

@ -14,8 +14,8 @@
Title="{r:Text Follow}" Title="{r:Text Follow}"
OrientationChanged="Page_OrientationChanged"> OrientationChanged="Page_OrientationChanged">
<ContentPage.ToolbarItems> <ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="NavigationTitle_RightButtonClicked" <ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconOption}"/> IconImageSource="{DynamicResource FontIconRefresh}"/>
</ContentPage.ToolbarItems> </ContentPage.ToolbarItems>
<Grid> <Grid>
<ScrollView HorizontalOptions="Fill" Padding="{Binding StatusBarPadding}"> <ScrollView HorizontalOptions="Fill" Padding="{Binding StatusBarPadding}">

View File

@ -33,7 +33,7 @@ namespace Pixiview
if (!page.loaded && now && Stores.NetworkAvailable) if (!page.loaded && now && Stores.NetworkAvailable)
{ {
page.loaded = true; page.loaded = true;
Task.Run(page.DoLoadIllusts); Task.Run(() => page.DoLoadIllusts());
} }
} }
@ -74,10 +74,7 @@ namespace Pixiview
public override void OnLoad() public override void OnLoad()
{ {
App.DebugPrint($"folder: {Stores.PersonalFolder}"); App.DebugPrint($"folder: {Stores.PersonalFolder}");
if (!loaded) Loading = true;
{
Loading = true;
}
} }
protected override void OnAppearing() protected override void OnAppearing()
@ -109,9 +106,9 @@ namespace Pixiview
#region - Illust Tasks - #region - Illust Tasks -
async void DoLoadIllusts() void DoLoadIllusts(bool force = false)
{ {
illustData = await Stores.LoadIllustData(); illustData = Stores.LoadIllustData(force);
if (illustData == null) if (illustData == null)
{ {
App.DebugError("illusts.load", "failed to load illusts data."); App.DebugError("illusts.load", "failed to load illusts data.");
@ -215,9 +212,14 @@ namespace Pixiview
} }
} }
private void NavigationTitle_RightButtonClicked(object sender, EventArgs e) private void Refresh_Clicked(object sender, EventArgs e)
{ {
DisplayAlert("title", "message", "Ok"); if (Loading)
{
return;
}
Loading = true;
Task.Run(() => DoLoadIllusts(true));
} }
} }

View File

@ -1,4 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<root> <root>
<Title>Pixiview</Title>
<Ok>OK</Ok>
<Follow>已关注</Follow> <Follow>已关注</Follow>
<Preview>预览</Preview>
<SaveSuccess>成功保存图片到照片库。</SaveSuccess>
</root> </root>

View File

@ -10,6 +10,10 @@ namespace Pixiview.Resources
{ {
public class ResourceHelper public class ResourceHelper
{ {
public static string Title => GetResource(nameof(Title));
public static string Ok => GetResource(nameof(Ok));
public static string SaveSuccess => GetResource(nameof(SaveSuccess));
static readonly Dictionary<string, LanguageResource> dict = new Dictionary<string, LanguageResource>(); static readonly Dictionary<string, LanguageResource> dict = new Dictionary<string, LanguageResource>();
public static string GetResource(string name, params object[] args) public static string GetResource(string name, params object[] args)

View File

@ -20,6 +20,7 @@ namespace Pixiview.UI
public static Thickness TotalBarOffset; public static Thickness TotalBarOffset;
public const string IconLayer = "\uf302"; public const string IconLayer = "\uf302";
public const string IconRefresh = "\uf2f1";
public const string IconOption = "\uf013"; public const string IconOption = "\uf013";
public const string IconDownload = "\uf019"; public const string IconDownload = "\uf019";

View File

@ -6,6 +6,7 @@ namespace Pixiview.UI.Theme
{ {
public const string TitleButton = nameof(TitleButton); public const string TitleButton = nameof(TitleButton);
public const string TitleLabel = nameof(TitleLabel); public const string TitleLabel = nameof(TitleLabel);
public const string FontIconRefresh = nameof(FontIconRefresh);
public const string FontIconOption = nameof(FontIconOption); public const string FontIconOption = nameof(FontIconOption);
public const string FontIconDownload = nameof(FontIconDownload); public const string FontIconDownload = nameof(FontIconDownload);
@ -27,6 +28,7 @@ namespace Pixiview.UI.Theme
//public const string Horizon10 = nameof(Horizon10); //public const string Horizon10 = nameof(Horizon10);
public const string ScreenBottomPadding = nameof(ScreenBottomPadding); public const string ScreenBottomPadding = nameof(ScreenBottomPadding);
public const string NavigationBarHeight = nameof(NavigationBarHeight); public const string NavigationBarHeight = nameof(NavigationBarHeight);
public const string IconRefresh = nameof(IconRefresh);
public const string IconOption = nameof(IconOption); public const string IconOption = nameof(IconOption);
public const string IconDownload = nameof(IconDownload); public const string IconDownload = nameof(IconDownload);
@ -36,6 +38,7 @@ namespace Pixiview.UI.Theme
Add(FontSizeTitleIcon, StyleDefinition.FontSizeTitleIcon); Add(FontSizeTitleIcon, StyleDefinition.FontSizeTitleIcon);
//Add(Horizon10, StyleDefinition.Horizon10); //Add(Horizon10, StyleDefinition.Horizon10);
Add(ScreenBottomPadding, StyleDefinition.ScreenBottomPadding); Add(ScreenBottomPadding, StyleDefinition.ScreenBottomPadding);
Add(IconRefresh, StyleDefinition.IconRefresh);
Add(IconOption, StyleDefinition.IconOption); Add(IconOption, StyleDefinition.IconOption);
Add(IconDownload, StyleDefinition.IconDownload); Add(IconDownload, StyleDefinition.IconDownload);
@ -74,6 +77,12 @@ namespace Pixiview.UI.Theme
} }
}); });
Add(FontIconRefresh, new FontImageSource
{
FontFamily = iconSolidFontFamily,
Glyph = StyleDefinition.IconRefresh,
Size = StyleDefinition.FontSizeTitle
});
Add(FontIconOption, new FontImageSource Add(FontIconOption, new FontImageSource
{ {
FontFamily = iconSolidFontFamily, FontFamily = iconSolidFontFamily,

View File

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Xamarin.Forms;
namespace Pixiview.Utils
{
public interface IFileStore
{
Task<string> SaveImageToGalleryAsync(ImageSource image);
}
}

View File

@ -105,4 +105,26 @@ namespace Pixiview.Utils
} }
} }
} }
public class IllustPageData
{
public bool error;
public string message;
public Body[] body;
public class Body
{
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;
}
}
}
} }

View File

@ -4,7 +4,6 @@ using System.IO;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using Xamarin.Essentials; using Xamarin.Essentials;
using Xamarin.Forms; using Xamarin.Forms;
@ -15,8 +14,9 @@ namespace Pixiview.Utils
{ {
public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal); public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
public static readonly string CacheFolder = Environment.GetFolderPath(Environment.SpecialFolder.InternetCache); public static readonly string CacheFolder = Environment.GetFolderPath(Environment.SpecialFolder.InternetCache);
private const string imageFolder = "img-master"; private const string pagesFolder = "pages";
private const string previewFolder = "img-preview"; private const string imageFolder = "img-original";
private const string previewFolder = "img-master";
private const string thumbFolder = "img-thumb"; private const string thumbFolder = "img-thumb";
private const string userFolder = "user-profile"; private const string userFolder = "user-profile";
private const string illustFile = "illust.json"; private const string illustFile = "illust.json";
@ -37,11 +37,10 @@ namespace Pixiview.Utils
} }
} }
public static async Task<IllustData> LoadIllustData() private static T LoadObject<T>(string file, string url, string referer = null, bool force = false, IEnumerable<(string header, string value)> headers = null)
{ {
var file = Path.Combine(PersonalFolder, illustFile);
string content = null; string content = null;
if (File.Exists(file)) if (!force && File.Exists(file))
{ {
try try
{ {
@ -49,44 +48,76 @@ namespace Pixiview.Utils
} }
catch (Exception ex) catch (Exception ex)
{ {
App.DebugError("illust.load", $"failed to read file: {file}, error: {ex.Message}"); App.DebugError("load", $"failed to read file: {file}, error: {ex.Message}");
} }
} }
if (content == null) if (content == null)
{ {
var response = Download(Configs.UrlIllust, headers: new[] var response = Download(url, referer, headers);
{
("cookie", Configs.Cookie),
("x-user-id", Configs.UserId)
});
if (response == null) if (response == null)
{ {
return null; return default;
} }
using (response) using (response)
{ {
content = await response.Content.ReadAsStringAsync(); content = response.Content.ReadAsStringAsync().Result;
try try
{ {
var folder = Path.GetDirectoryName(file);
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
File.WriteAllText(file, content, Encoding.UTF8); File.WriteAllText(file, content, Encoding.UTF8);
} }
catch (Exception ex) catch (Exception ex)
{ {
App.DebugError("illust.save", $"failed to save illust JSON object, error: {ex.Message}"); App.DebugError("save", $"failed to save illust JSON object, error: {ex.Message}");
} }
} }
} }
try try
{ {
return JsonConvert.DeserializeObject<IllustData>(content); return JsonConvert.DeserializeObject<T>(content);
} }
catch (Exception ex) catch (Exception ex)
{ {
App.DebugError("illust.load", $"failed to parse illust JSON object, error: {ex.Message}"); App.DebugError("load", $"failed to parse illust JSON object, error: {ex.Message}");
return null; return default;
} }
} }
public static IllustData LoadIllustData(bool force = false)
{
var file = Path.Combine(PersonalFolder, illustFile);
var result = LoadObject<IllustData>(
file,
Configs.UrlIllustList,
force: force,
headers: new[]
{
("cookie", Configs.Cookie),
("x-user-id", Configs.UserId)
});
return result;
}
public static IllustPageData LoadIllustPageData(string id, bool force = false)
{
var file = Path.Combine(PersonalFolder, pagesFolder, $"{id}.json");
var result = LoadObject<IllustPageData>(
file,
string.Format(Configs.UrlIllustPage, id),
string.Format(Configs.UrlIllust, id),
force: force,
headers: new[]
{
("cookie", Configs.Cookie),
("x-user-id", Configs.UserId)
});
return result;
}
public static ImageSource LoadIllustImage(string url) public static ImageSource LoadIllustImage(string url)
{ {
return LoadImage(url, PersonalFolder, imageFolder); return LoadImage(url, PersonalFolder, imageFolder);
@ -110,15 +141,32 @@ namespace Pixiview.Utils
private static ImageSource LoadImage(string url, string working, string folder) private static ImageSource LoadImage(string url, string working, string folder)
{ {
var file = Path.Combine(working, folder, Path.GetFileName(url)); var file = Path.Combine(working, folder, Path.GetFileName(url));
if (!File.Exists(file)) ImageSource image;
if (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 = DownloadImage(url, working, folder); file = DownloadImage(url, working, folder);
if (file != null)
{
return ImageSource.FromFile(file);
}
} }
if (file != null) return image;
{
return ImageSource.FromFile(file);
}
return null;
} }
private static string DownloadImage(string url, string working, string folder) private static string DownloadImage(string url, string working, string folder)
@ -216,7 +264,9 @@ namespace Pixiview.Utils
public const int MaxThreads = 3; public const int MaxThreads = 3;
public const string UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"; public const string UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36";
public const string UrlIllust = "https://www.pixiv.net/ajax/top/illust?mode=all&lang=zh"; public const string UrlIllustList = "https://www.pixiv.net/ajax/top/illust?mode=all&lang=zh";
public const string UrlIllust = "https://www.pixiv.net/artworks/{0}";
public const string UrlIllustPage = "https://www.pixiv.net/ajax/illust/{0}/pages?lang=zh";
public const string Cookie = "first_visit_datetime_pc=2019-10-29+22%3A05%3A30; p_ab_id=2; p_ab_id_2=6;" + public const string Cookie = "first_visit_datetime_pc=2019-10-29+22%3A05%3A30; p_ab_id=2; p_ab_id_2=6;" +
" p_ab_d_id=1155161977; a_type=0; b_type=1; d_type=2; module_orders_mypage=%5B%7B%22name%22%3A%22s" + " p_ab_d_id=1155161977; a_type=0; b_type=1; d_type=2; module_orders_mypage=%5B%7B%22name%22%3A%22s" +
"ketch_live%22%2C%22visible%22%3Atrue%7D%2C%7B%22name%22%3A%22tag_follow%22%2C%22visible%22%3Atrue" + "ketch_live%22%2C%22visible%22%3Atrue%7D%2C%7B%22name%22%3A%22tag_follow%22%2C%22visible%22%3Atrue" +

View File

@ -18,6 +18,9 @@
<Grid Margin="{Binding PageTopMargin}"> <Grid Margin="{Binding PageTopMargin}">
<CarouselView ItemsSource="{Binding Illusts}" HorizontalScrollBarVisibility="Never" <CarouselView ItemsSource="{Binding Illusts}" HorizontalScrollBarVisibility="Never"
PositionChanged="CarouselView_PositionChanged"> PositionChanged="CarouselView_PositionChanged">
<CarouselView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal" ItemSpacing="20"/>
</CarouselView.ItemsLayout>
<CarouselView.ItemTemplate> <CarouselView.ItemTemplate>
<DataTemplate x:DataType="p:IllustDetailItem"> <DataTemplate x:DataType="p:IllustDetailItem">
<Grid> <Grid>
@ -26,6 +29,10 @@
Aspect="AspectFit"/> Aspect="AspectFit"/>
<ActivityIndicator IsRunning="True" IsEnabled="True" IsVisible="{Binding Loading}" <ActivityIndicator IsRunning="True" IsEnabled="True" IsVisible="{Binding Loading}"
Color="{DynamicResource TextColor}"/> Color="{DynamicResource TextColor}"/>
<ActivityIndicator IsRunning="True" IsEnabled="True" IsVisible="{Binding Downloading}"
Margin="10"
HorizontalOptions="Start" VerticalOptions="Start"
Color="{DynamicResource TextColor}"/>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</CarouselView.ItemTemplate> </CarouselView.ItemTemplate>

View File

@ -1,6 +1,8 @@
using System; using System;
using System.Threading.Tasks;
using Pixiview.Resources;
using Pixiview.UI; using Pixiview.UI;
using Pixiview.UI.Theme; using Pixiview.Utils;
using Xamarin.Essentials; using Xamarin.Essentials;
using Xamarin.Forms; using Xamarin.Forms;
@ -45,6 +47,8 @@ namespace Pixiview
private set => SetValue(IllustItemProperty, value); private set => SetValue(IllustItemProperty, value);
} }
public int CurrentPage { get; private set; }
public ViewIllustPage(IllustItem illust) public ViewIllustPage(IllustItem illust)
{ {
IllustItem = illust; IllustItem = illust;
@ -62,25 +66,51 @@ namespace Pixiview
} }
var items = new IllustDetailItem[illust.PageCount]; var items = new IllustDetailItem[illust.PageCount];
if (items.Length > 0) if (items.Length > 1)
{ {
IsPageVisible = true; IsPageVisible = true;
PagePositionText = $"1/{items.Length}"; PagePositionText = $"1/{items.Length}";
} }
else
items[0] = new IllustDetailItem
{ {
Image = illust.Image, IsPageVisible = false;
Loading = true }
};
for (var i = 0; i < items.Length; i++)
{
items[i] = new IllustDetailItem();
if (i == 0)
{
items[i].Image = illust.Image;
}
}
Illusts = items; Illusts = items;
UpdatePageTopMargin(CurrentOrientation); UpdatePageTopMargin(CurrentOrientation);
Task.Run(DoLoadImages);
} }
private void CarouselView_PositionChanged(object sender, PositionChangedEventArgs e) private void CarouselView_PositionChanged(object sender, PositionChangedEventArgs e)
{ {
PagePositionText = $"{e.CurrentPosition + 1}/{Illusts.Length}"; var index = e.CurrentPosition;
CurrentPage = index;
var items = Illusts;
var length = items.Length;
PagePositionText = $"{index + 1}/{length}";
var item = items[index];
if (!item.Loading && item.Image == null)
{
Task.Run(() => DoLoadImage(index));
}
if (index < length - 1)
{
item = items[index + 1];
if (!item.Loading && item.Image == null)
{
Task.Run(() => DoLoadImage(index + 1));
}
}
} }
private void Page_OrientationChanged(object sender, OrientationEventArgs e) private void Page_OrientationChanged(object sender, OrientationEventArgs e)
@ -114,9 +144,103 @@ namespace Pixiview
} }
} }
private void Download_Clicked(object sender, EventArgs e) private void DoLoadImages()
{ {
var pages = Stores.LoadIllustPageData(IllustItem.Id);
if (pages == null)
{
App.DebugError("illustPage.load", $"failed to load illust page data, id: {IllustItem.Id}");
return;
}
var items = Illusts;
if (pages.body.Length > items.Length)
{
App.DebugPrint($"local page count ({items.Length}) is not equals the remote one ({pages.body.Length})");
var tmp = new IllustDetailItem[pages.body.Length];
items.CopyTo(items, 0);
for (var i = items.Length; i < tmp.Length; i++)
{
tmp[i] = new IllustDetailItem();
}
items = tmp;
}
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;
}
DoLoadImage(0);
if (items.Length > 1)
{
DoLoadImage(1);
}
}
private void DoLoadImage(int index)
{
var items = Illusts;
if (index < 0 || index >= items.Length)
{
App.DebugPrint($"invalid index: {index}");
return;
}
var item = items[index];
if (item.Loading || (index > 0 && item.Image != null))
{
App.DebugPrint($"skipped, loading or already loaded, index: {index}, loading: {item.Loading}");
return;
}
item.Loading = true;
var image = Stores.LoadPreviewImage(item.PreviewUrl);
if (image != null)
{
item.Image = image;
}
item.Loading = false;
}
private async void Download_Clicked(object sender, EventArgs e)
{
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 item = Illusts[CurrentPage];
if (item.Downloading)
{
return;
}
item.Downloading = true;
_ = Task.Run(() => DoLoadOriginalImage(item));
}
private void DoLoadOriginalImage(IllustDetailItem item)
{
var image = Stores.LoadIllustImage(item.OriginalUrl);
if (image != null)
{
Device.BeginInvokeOnMainThread(async () =>
{
var service = DependencyService.Get<IFileStore>();
var result = await service.SaveImageToGalleryAsync(image);
string message = result ?? ResourceHelper.SaveSuccess;
await DisplayAlert(ResourceHelper.Title, message, ResourceHelper.Ok);
});
}
item.Downloading = false;
} }
} }
@ -126,6 +250,8 @@ namespace Pixiview
nameof(Image), typeof(ImageSource), typeof(IllustDetailItem)); nameof(Image), typeof(ImageSource), typeof(IllustDetailItem));
public static readonly BindableProperty LoadingProperty = BindableProperty.Create( public static readonly BindableProperty LoadingProperty = BindableProperty.Create(
nameof(Loading), typeof(bool), typeof(IllustDetailItem)); nameof(Loading), typeof(bool), typeof(IllustDetailItem));
public static readonly BindableProperty DownloadingProperty = BindableProperty.Create(
nameof(Downloading), typeof(bool), typeof(IllustDetailItem));
public ImageSource Image public ImageSource Image
{ {
@ -137,5 +263,12 @@ namespace Pixiview
get => (bool)GetValue(LoadingProperty); get => (bool)GetValue(LoadingProperty);
set => SetValue(LoadingProperty, value); set => SetValue(LoadingProperty, value);
} }
public bool Downloading
{
get => (bool)GetValue(DownloadingProperty);
set => SetValue(DownloadingProperty, value);
}
public string PreviewUrl { get; set; }
public string OriginalUrl { get; set; }
} }
} }