diff --git a/Pixiview.iOS/Info.plist b/Pixiview.iOS/Info.plist index 8e84970..c6a9565 100644 --- a/Pixiview.iOS/Info.plist +++ b/Pixiview.iOS/Info.plist @@ -44,5 +44,7 @@ UIStatusBarStyle UIStatusBarStyleDefault + NSPhotoLibraryUsageDescription + 需要访问您的图片库 diff --git a/Pixiview.iOS/Pixiview.iOS.csproj b/Pixiview.iOS/Pixiview.iOS.csproj index dc9916f..35005d7 100644 --- a/Pixiview.iOS/Pixiview.iOS.csproj +++ b/Pixiview.iOS/Pixiview.iOS.csproj @@ -76,6 +76,7 @@ + diff --git a/Pixiview.iOS/Services/FileStore.cs b/Pixiview.iOS/Services/FileStore.cs new file mode 100644 index 0000000..3e29a7c --- /dev/null +++ b/Pixiview.iOS/Services/FileStore.cs @@ -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 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(); + if (photo == null) + { + task.SetResult(null); + } + else + { + photo.SaveToPhotosAlbum((img, error) => + { + task.SetResult(error?.ToString()); + }); + } + return task.Task; + } + } +} diff --git a/Pixiview/MainPage.xaml b/Pixiview/MainPage.xaml index a24ee7d..1055071 100644 --- a/Pixiview/MainPage.xaml +++ b/Pixiview/MainPage.xaml @@ -14,8 +14,8 @@ Title="{r:Text Follow}" OrientationChanged="Page_OrientationChanged"> - + diff --git a/Pixiview/MainPage.xaml.cs b/Pixiview/MainPage.xaml.cs index c3756d6..316badc 100644 --- a/Pixiview/MainPage.xaml.cs +++ b/Pixiview/MainPage.xaml.cs @@ -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)); } } diff --git a/Pixiview/Resources/Languages/zh-CN.xml b/Pixiview/Resources/Languages/zh-CN.xml index d12a3ae..b6a1bcf 100644 --- a/Pixiview/Resources/Languages/zh-CN.xml +++ b/Pixiview/Resources/Languages/zh-CN.xml @@ -1,4 +1,8 @@  + Pixiview + OK 已关注 + 预览 + 成功保存图片到照片库。 \ No newline at end of file diff --git a/Pixiview/Resources/ResourceHelper.cs b/Pixiview/Resources/ResourceHelper.cs index 46d31e3..6839cd6 100644 --- a/Pixiview/Resources/ResourceHelper.cs +++ b/Pixiview/Resources/ResourceHelper.cs @@ -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 dict = new Dictionary(); public static string GetResource(string name, params object[] args) diff --git a/Pixiview/UI/StyleDefinition.cs b/Pixiview/UI/StyleDefinition.cs index 950df87..bfbebb3 100644 --- a/Pixiview/UI/StyleDefinition.cs +++ b/Pixiview/UI/StyleDefinition.cs @@ -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"; diff --git a/Pixiview/UI/Theme/ThemeBase.cs b/Pixiview/UI/Theme/ThemeBase.cs index 98a03c7..8a0d40c 100644 --- a/Pixiview/UI/Theme/ThemeBase.cs +++ b/Pixiview/UI/Theme/ThemeBase.cs @@ -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, diff --git a/Pixiview/Utils/IFileStore.cs b/Pixiview/Utils/IFileStore.cs new file mode 100644 index 0000000..bfba038 --- /dev/null +++ b/Pixiview/Utils/IFileStore.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Pixiview.Utils +{ + public interface IFileStore + { + Task SaveImageToGalleryAsync(ImageSource image); + } +} diff --git a/Pixiview/Utils/IllustData.cs b/Pixiview/Utils/IllustData.cs index dbfab17..2442de9 100644 --- a/Pixiview/Utils/IllustData.cs +++ b/Pixiview/Utils/IllustData.cs @@ -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; + } + } + } } diff --git a/Pixiview/Utils/Stores.cs b/Pixiview/Utils/Stores.cs index b7cb81f..3cb0b4e 100644 --- a/Pixiview/Utils/Stores.cs +++ b/Pixiview/Utils/Stores.cs @@ -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 LoadIllustData() + private static T LoadObject(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(content); + return JsonConvert.DeserializeObject(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( + 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( + 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" + diff --git a/Pixiview/ViewIllustPage.xaml b/Pixiview/ViewIllustPage.xaml index 3025028..482cd67 100644 --- a/Pixiview/ViewIllustPage.xaml +++ b/Pixiview/ViewIllustPage.xaml @@ -18,6 +18,9 @@ + + + @@ -26,6 +29,10 @@ Aspect="AspectFit"/> + diff --git a/Pixiview/ViewIllustPage.xaml.cs b/Pixiview/ViewIllustPage.xaml.cs index c8bd921..149163b 100644 --- a/Pixiview/ViewIllustPage.xaml.cs +++ b/Pixiview/ViewIllustPage.xaml.cs @@ -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(); + if (status != PermissionStatus.Granted) + { + status = await Permissions.RequestAsync(); + 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(); + 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; } } }