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/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问您的图片库</string>
</dict>
</plist>

View File

@ -76,6 +76,7 @@
<Compile Include="Renderers\RoundImageRenderer.cs" />
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="Renderers\AdaptedNavigationPageRenderer.cs" />
<Compile Include="Services\FileStore.cs" />
</ItemGroup>
<ItemGroup>
<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}"
OrientationChanged="Page_OrientationChanged">
<ContentPage.ToolbarItems>
<ToolbarItem Order="Primary" Clicked="NavigationTitle_RightButtonClicked"
IconImageSource="{DynamicResource FontIconOption}"/>
<ToolbarItem Order="Primary" Clicked="Refresh_Clicked"
IconImageSource="{DynamicResource FontIconRefresh}"/>
</ContentPage.ToolbarItems>
<Grid>
<ScrollView HorizontalOptions="Fill" Padding="{Binding StatusBarPadding}">

View File

@ -33,7 +33,7 @@ namespace Pixiview
if (!page.loaded && now && Stores.NetworkAvailable)
{
page.loaded = true;
Task.Run(page.DoLoadIllusts);
Task.Run(() => page.DoLoadIllusts());
}
}
@ -74,10 +74,7 @@ namespace Pixiview
public override void OnLoad()
{
App.DebugPrint($"folder: {Stores.PersonalFolder}");
if (!loaded)
{
Loading = true;
}
Loading = true;
}
protected override void OnAppearing()
@ -109,9 +106,9 @@ namespace Pixiview
#region - Illust Tasks -
async void DoLoadIllusts()
void DoLoadIllusts(bool force = false)
{
illustData = await Stores.LoadIllustData();
illustData = Stores.LoadIllustData(force);
if (illustData == null)
{
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" ?>
<root>
<Title>Pixiview</Title>
<Ok>OK</Ok>
<Follow>已关注</Follow>
<Preview>预览</Preview>
<SaveSuccess>成功保存图片到照片库。</SaveSuccess>
</root>

View File

@ -10,6 +10,10 @@ namespace Pixiview.Resources
{
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>();
public static string GetResource(string name, params object[] args)

View File

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

View File

@ -6,6 +6,7 @@ namespace Pixiview.UI.Theme
{
public const string TitleButton = nameof(TitleButton);
public const string TitleLabel = nameof(TitleLabel);
public const string FontIconRefresh = nameof(FontIconRefresh);
public const string FontIconOption = nameof(FontIconOption);
public const string FontIconDownload = nameof(FontIconDownload);
@ -27,6 +28,7 @@ namespace Pixiview.UI.Theme
//public const string Horizon10 = nameof(Horizon10);
public const string ScreenBottomPadding = nameof(ScreenBottomPadding);
public const string NavigationBarHeight = nameof(NavigationBarHeight);
public const string IconRefresh = nameof(IconRefresh);
public const string IconOption = nameof(IconOption);
public const string IconDownload = nameof(IconDownload);
@ -36,6 +38,7 @@ namespace Pixiview.UI.Theme
Add(FontSizeTitleIcon, StyleDefinition.FontSizeTitleIcon);
//Add(Horizon10, StyleDefinition.Horizon10);
Add(ScreenBottomPadding, StyleDefinition.ScreenBottomPadding);
Add(IconRefresh, StyleDefinition.IconRefresh);
Add(IconOption, StyleDefinition.IconOption);
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
{
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.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Xamarin.Essentials;
using Xamarin.Forms;
@ -15,8 +14,9 @@ namespace Pixiview.Utils
{
public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
public static readonly string CacheFolder = Environment.GetFolderPath(Environment.SpecialFolder.InternetCache);
private const string imageFolder = "img-master";
private const string previewFolder = "img-preview";
private const string pagesFolder = "pages";
private const string imageFolder = "img-original";
private const string previewFolder = "img-master";
private const string thumbFolder = "img-thumb";
private const string userFolder = "user-profile";
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;
if (File.Exists(file))
if (!force && File.Exists(file))
{
try
{
@ -49,44 +48,76 @@ namespace Pixiview.Utils
}
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)
{
var response = Download(Configs.UrlIllust, headers: new[]
{
("cookie", Configs.Cookie),
("x-user-id", Configs.UserId)
});
var response = Download(url, referer, headers);
if (response == null)
{
return null;
return default;
}
using (response)
{
content = await response.Content.ReadAsStringAsync();
content = response.Content.ReadAsStringAsync().Result;
try
{
var folder = Path.GetDirectoryName(file);
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
File.WriteAllText(file, content, Encoding.UTF8);
}
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
{
return JsonConvert.DeserializeObject<IllustData>(content);
return JsonConvert.DeserializeObject<T>(content);
}
catch (Exception ex)
{
App.DebugError("illust.load", $"failed to parse illust JSON object, error: {ex.Message}");
return null;
App.DebugError("load", $"failed to parse illust JSON object, error: {ex.Message}");
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)
{
return LoadImage(url, PersonalFolder, imageFolder);
@ -110,15 +141,32 @@ namespace Pixiview.Utils
private static ImageSource LoadImage(string url, string working, string folder)
{
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);
if (file != null)
{
return ImageSource.FromFile(file);
}
}
if (file != null)
{
return ImageSource.FromFile(file);
}
return null;
return image;
}
private static string DownloadImage(string url, string working, string folder)
@ -216,7 +264,9 @@ namespace Pixiview.Utils
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 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;" +
" 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" +

View File

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

View File

@ -1,6 +1,8 @@
using System;
using System.Threading.Tasks;
using Pixiview.Resources;
using Pixiview.UI;
using Pixiview.UI.Theme;
using Pixiview.Utils;
using Xamarin.Essentials;
using Xamarin.Forms;
@ -45,6 +47,8 @@ namespace Pixiview
private set => SetValue(IllustItemProperty, value);
}
public int CurrentPage { get; private set; }
public ViewIllustPage(IllustItem illust)
{
IllustItem = illust;
@ -62,25 +66,51 @@ namespace Pixiview
}
var items = new IllustDetailItem[illust.PageCount];
if (items.Length > 0)
if (items.Length > 1)
{
IsPageVisible = true;
PagePositionText = $"1/{items.Length}";
}
items[0] = new IllustDetailItem
else
{
Image = illust.Image,
Loading = true
};
IsPageVisible = false;
}
for (var i = 0; i < items.Length; i++)
{
items[i] = new IllustDetailItem();
if (i == 0)
{
items[i].Image = illust.Image;
}
}
Illusts = items;
UpdatePageTopMargin(CurrentOrientation);
Task.Run(DoLoadImages);
}
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)
@ -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));
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));
public ImageSource Image
{
@ -137,5 +263,12 @@ namespace Pixiview
get => (bool)GetValue(LoadingProperty);
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; }
}
}