diff --git a/TsubameViewer/TsubameViewer.Shared/App.xaml b/TsubameViewer/TsubameViewer.Shared/App.xaml index a5aa7411..ae99127e 100644 --- a/TsubameViewer/TsubameViewer.Shared/App.xaml +++ b/TsubameViewer/TsubameViewer.Shared/App.xaml @@ -12,7 +12,9 @@ xmlns:uwpControls="using:Microsoft.Toolkit.Uwp.UI.Controls" xmlns:uwpConv="using:Microsoft.Toolkit.Uwp.UI.Converters" xmlns:folderListup="using:TsubameViewer.Presentation.Views.FolderListup" - xmlns:muxc="using:Microsoft.UI.Xaml.Controls"> + xmlns:muxc="using:Microsoft.UI.Xaml.Controls" + xmlns:myFlyouts="using:TsubameViewer.Presentation.Views.Flyouts" + > @@ -34,7 +36,10 @@ False - + + + + @@ -53,6 +58,24 @@ + + + False + + + True + + + + + + True + + + False + + + diff --git a/TsubameViewer/TsubameViewer.Shared/App.xaml.cs b/TsubameViewer/TsubameViewer.Shared/App.xaml.cs index 169eee03..bf7967a3 100644 --- a/TsubameViewer/TsubameViewer.Shared/App.xaml.cs +++ b/TsubameViewer/TsubameViewer.Shared/App.xaml.cs @@ -1,6 +1,7 @@ using LiteDB; using Microsoft.Extensions.Logging; using Microsoft.IO; +using Microsoft.Toolkit.Mvvm.Messaging; using Prism; using Prism.Ioc; using Prism.Mvvm; @@ -13,11 +14,13 @@ using System.IO; using System.Linq; using System.Reactive.Concurrency; +using System.Threading; using System.Threading.Tasks; using TsubameViewer.Models.Domain; using TsubameViewer.Models.Domain.FolderItemListing; using TsubameViewer.Models.Domain.SourceFolders; using TsubameViewer.Presentation.Services.UWP; +using TsubameViewer.Presentation.ViewModels; using TsubameViewer.Presentation.ViewModels.PageNavigation; using TsubameViewer.Presentation.Views; using Unity; @@ -122,6 +125,7 @@ protected override void RegisterRequiredTypes(IContainerRegistry container) public override void RegisterTypes(IContainerRegistry container) { + container.RegisterInstance(WeakReferenceMessenger.Default); container.RegisterSingleton(); container.RegisterSingleton(); @@ -131,6 +135,13 @@ public override void RegisterTypes(IContainerRegistry container) container.RegisterSingleton(); container.RegisterInstance(new RecyclableMemoryStreamManager()); + container.RegisterSingleton(); + container.RegisterSingleton(); + container.RegisterSingleton(); + container.RegisterSingleton(); + container.RegisterSingleton(); + container.RegisterSingleton(); + container.RegisterForNavigation(); container.RegisterForNavigation(); container.RegisterForNavigation(); @@ -261,6 +272,7 @@ public override async void OnInitialized() var ns = shell.GetNavigationService(); var unityContainer = Container.GetContainer(); unityContainer.RegisterInstance("PrimaryWindowNavigationService", ns); + unityContainer.RegisterInstance(ns); Window.Current.Content = shell; Window.Current.Activate(); @@ -421,7 +433,7 @@ async Task NavigateAsync(PageNavigationInfo info) if (item is StorageFolder itemFolder) { var containerTypeManager = Container.Resolve(); - if (await containerTypeManager.GetFolderContainerTypeWithCacheAsync(itemFolder) == FolderContainerType.OnlyImages) + if (await containerTypeManager.GetFolderContainerTypeWithCacheAsync(itemFolder, CancellationToken.None) == FolderContainerType.OnlyImages) { return await navigationService.NavigateAsync(nameof(Presentation.Views.ImageViewerPage), parameters, new SuppressNavigationTransitionInfo()); } diff --git a/TsubameViewer/TsubameViewer.Shared/IsExternalInit.cs b/TsubameViewer/TsubameViewer.Shared/IsExternalInit.cs new file mode 100644 index 00000000..7f2e2d39 --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/IsExternalInit.cs @@ -0,0 +1,6 @@ + +namespace System.Runtime.CompilerServices +{ + internal class IsExternalInit { } +} + diff --git a/TsubameViewer/TsubameViewer.Shared/Locales/en-US.txt b/TsubameViewer/TsubameViewer.Shared/Locales/en-US.txt index cb16a2ce..12317946 100644 --- a/TsubameViewer/TsubameViewer.Shared/Locales/en-US.txt +++ b/TsubameViewer/TsubameViewer.Shared/Locales/en-US.txt @@ -73,18 +73,28 @@ ChangeViewToImageListupPage = Switch to image list view FileSortTitle = Sorting -FileSortType.TitleAscending = File name (ascending order) -FileSortType.TitleDecending = File name (descending order) -FileSortType.UpdateTimeAscending = Recent first -FileSortType.UpdateTimeDecending = Latest first +FileSortType.UpdateTimeDescThenTitleAsc = Latest & Title Asc +FileSortType.TitleAscending = Title Asc +FileSortType.TitleDecending = Title Desc +FileSortType.UpdateTimeAscending = Recent +FileSortType.UpdateTimeDecending = Latest + +Sort_InheritancePath = Sort inheritance source +Sort_Unselected = Unspecified +Sort_DefaultChildItemSort = Sorting images in child folders +TitleDigitCompletion = Title digit completion + OpenImageViewer = Open in reader OpenFolderListup = List files OpenWithExplorer = Open in explorer +OpenWithExternalApplication = Open with AddSecondaryTile = Pin to start RemoveSecondaryTile = Unpin the start +SetThumbnailImage = Set as thumbnail image + FolderItemMenuNoActionSuggested = No action available #### ImageViewerPage @@ -96,6 +106,7 @@ IsEnableSpreadDisplay = Spread display IsLeftBindingView = Left binding display SwitchFullScreen = Full screen switching +DoubleViewCorrectPaging = Correct the page shift of the spread #### EBookReaderPage diff --git a/TsubameViewer/TsubameViewer.Shared/Locales/ja-JP.txt b/TsubameViewer/TsubameViewer.Shared/Locales/ja-JP.txt index 05e9af1a..d862232f 100644 --- a/TsubameViewer/TsubameViewer.Shared/Locales/ja-JP.txt +++ b/TsubameViewer/TsubameViewer.Shared/Locales/ja-JP.txt @@ -72,19 +72,29 @@ ChangeViewToImageListupPage = 画像一覧ビューに切替 -FileSortTitle = 並べ替え -FileSortType.TitleAscending = ファイル名(昇順) -FileSortType.TitleDecending = ファイル名(降順) +FileSortTitle = 並び替え +FileSortType.UpdateTimeDescThenTitleAsc = 新しい順&タイトル昇順 +FileSortType.TitleAscending = タイトル昇順 +FileSortType.TitleDecending = タイトル降順 FileSortType.UpdateTimeAscending = 古い順 FileSortType.UpdateTimeDecending = 新しい順 +Sort_InheritancePath = 並び替え継承元 +Sort_Unselected = 未指定 +Sort_DefaultChildItemSort = 子フォルダの画像並び替え +TitleDigitCompletion = タイトルの桁数補完 + + OpenImageViewer = リーダーで開く OpenFolderListup = ファイルを一覧表示 OpenWithExplorer = エクスプローラーで開く +OpenWithExternalApplication = 別アプリで開く AddSecondaryTile = スタートにピン留めする RemoveSecondaryTile = スタートのピン留めを解除 +SetThumbnailImage = サムネイル画像に設定 + FolderItemMenuNoActionSuggested = 選択可能なアクションはありません #### ImageViewerPage @@ -96,6 +106,7 @@ IsEnableSpreadDisplay = 見開き表示 IsLeftBindingView = 左綴じ表示 SwitchFullScreen = 全画面切替 +DoubleViewCorrectPaging = 見開きのページズレを補正する #### EBookReaderPage diff --git a/TsubameViewer/TsubameViewer.Shared/Locales/zh-CHS.txt b/TsubameViewer/TsubameViewer.Shared/Locales/zh-CHS.txt index e29825f1..0405a47f 100644 --- a/TsubameViewer/TsubameViewer.Shared/Locales/zh-CHS.txt +++ b/TsubameViewer/TsubameViewer.Shared/Locales/zh-CHS.txt @@ -73,18 +73,28 @@ ChangeViewToImageListupPage = 切换到图像列表视图 FileSortTitle = 排序 +FileSortType.UpdateTimeDescThenTitleAsc = 新订单和标题升序 FileSortType.TitleAscending = 文件名(升序) FileSortType.TitleDecending = 文件名(降序) FileSortType.UpdateTimeAscending = 最旧的优先 FileSortType.UpdateTimeDecending = 新命令 +Sort_InheritancePath = 排序继承源 +Sort_Unselected = 未指定 +Sort_DefaultChildItemSort = 对子文件夹中的图像进行排序 +TitleDigitCompletion = 标题数字完成 + + OpenImageViewer = 在阅读器中打开 OpenFolderListup = 列出档案 OpenWithExplorer = 在资源管理器中打开 +OpenWithExternalApplication = 在另一个应用程序中打开 AddSecondaryTile = 固定即可开始 RemoveSecondaryTile = 取消开始 +SetThumbnailImage = 设置为缩略图 + FolderItemMenuNoActionSuggested = 没有可用的动作 #### ImageViewerPage @@ -96,6 +106,7 @@ IsEnableSpreadDisplay = 点差显示 IsLeftBindingView = 左绑定显示 SwitchFullScreen = 全屏切换 +DoubleViewCorrectPaging = 纠正传播的页面偏移 #### EBookReaderPage diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/DisplaySettingsByPathRepository.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/DisplaySettingsByPathRepository.cs new file mode 100644 index 00000000..5558dee3 --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/DisplaySettingsByPathRepository.cs @@ -0,0 +1,146 @@ +using LiteDB; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using TsubameViewer.Models.Infrastructure; + +namespace TsubameViewer.Models.Domain.FolderItemListing +{ + public record FolderAndArchiveDisplaySettingEntry + { + [BsonId] + public string Path { get; init; } + + public FileSortType Sort { get; init; } + + public bool IsTitleDigitInterpolation { get; init; } + } + + public record FolderAndArchiveChildFileDisplaySettingEntry + { + [BsonId] + public string Path { get; init; } + + public FileSortType? ChildItemDefaultSort { get; init; } + } + + public record FileDisplaySettingEntry + { + [BsonId] + public string Path { get; init; } + + + public FileSortType Sort { get; init; } + + public bool IsTitleDigitInterpolation { get; init; } + } + + public sealed class DisplaySettingsByPathRepository + { + public sealed class InternalFolderAndArchiveDisplaySettingsByPathRepository : LiteDBServiceBase + { + public InternalFolderAndArchiveDisplaySettingsByPathRepository(ILiteDatabase liteDatabase) : base(liteDatabase) + { + } + + public FolderAndArchiveDisplaySettingEntry FindById(string path) + { + return _collection.FindById(path); + } + + public int DeleteUnderPath(string path) + { + return _collection.DeleteMany(x => x.Path.StartsWith(path)); + } + } + + public sealed class InternalFolderAndArchiveChildFileDisplaySettingsByPathRepository : LiteDBServiceBase + { + public InternalFolderAndArchiveChildFileDisplaySettingsByPathRepository(ILiteDatabase liteDatabase) : base(liteDatabase) + { + } + + public FolderAndArchiveChildFileDisplaySettingEntry FindById(string path) + { + return _collection.FindById(path); + } + + public int DeleteUnderPath(string path) + { + return _collection.DeleteMany(x => x.Path.StartsWith(path)); + } + } + + + private readonly InternalFolderAndArchiveDisplaySettingsByPathRepository _internalFolderAndArchiveRepository; + private readonly InternalFolderAndArchiveChildFileDisplaySettingsByPathRepository _internalChildFileRepository; + + public DisplaySettingsByPathRepository( + InternalFolderAndArchiveDisplaySettingsByPathRepository folderAndArchiveRepository, + InternalFolderAndArchiveChildFileDisplaySettingsByPathRepository childFileRepository + ) + { + _internalFolderAndArchiveRepository = folderAndArchiveRepository; + _internalChildFileRepository = childFileRepository; + } + + public FolderAndArchiveDisplaySettingEntry GetFolderAndArchiveSettings(string path) + { + return _internalFolderAndArchiveRepository.FindById(path); + } + + public void SetFolderAndArchiveSettings(string path, FileSortType sortType, bool withTitleDigitInterpolation) + { + _internalFolderAndArchiveRepository.UpdateItem(new FolderAndArchiveDisplaySettingEntry() + { + Path = path, + Sort = sortType, + IsTitleDigitInterpolation = withTitleDigitInterpolation + }); + } + + public void ClearFolderAndArchiveSettings(string path) + { + _internalFolderAndArchiveRepository.DeleteUnderPath(path); + } + + + public FileSortType? GetFileParentSettings(string path) + { + return _internalChildFileRepository.FindById(path)?.ChildItemDefaultSort; + } + + public FolderAndArchiveChildFileDisplaySettingEntry GetFileParentSettingsUpStreamToRoot(string path) + { + while (!string.IsNullOrEmpty(path)) + { + if (_internalChildFileRepository.FindById(path) is not null and var entry) + { + return entry; + } + + path = Path.GetDirectoryName(path); + } + + return null; + } + + + public void SetFileParentSettings(string path, FileSortType? sort) + { + _internalChildFileRepository.UpdateItem(new FolderAndArchiveChildFileDisplaySettingEntry() + { + Path = path, + ChildItemDefaultSort = sort, + }); + } + + + public void DeleteUnderPath(string path) + { + _internalFolderAndArchiveRepository.DeleteUnderPath(path); + _internalChildFileRepository.DeleteUnderPath(path); + } + } +} diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/FileSortType.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/FileSortType.cs index 1bdc82ca..5bb86b6e 100644 --- a/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/FileSortType.cs +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/FileSortType.cs @@ -6,6 +6,7 @@ namespace TsubameViewer.Models.Domain.FolderItemListing { public enum FileSortType { + UpdateTimeDescThenTitleAsc, TitleAscending, TitleDecending, UpdateTimeAscending, diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/FolderContainerTypeRepository.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/FolderContainerTypeRepository.cs index b321f647..50af3d21 100644 --- a/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/FolderContainerTypeRepository.cs +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/FolderContainerTypeRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; using TsubameViewer.Models.Infrastructure; using Windows.Storage; @@ -24,26 +25,26 @@ public FolderContainerTypeManager(FolderContainerTypeRepository folderContainerT _folderContainerTypeRepository = folderContainerTypeRepository; } - public async ValueTask GetFolderContainerTypeWithCacheAsync(StorageFolder folder) + public async ValueTask GetFolderContainerTypeWithCacheAsync(StorageFolder folder, CancellationToken ct) { var containerType = _folderContainerTypeRepository.GetContainerType(folder.Path); if (containerType != null) { return containerType.Value; } - return await GetLatestFolderContainerTypeAndUpdateCacheAsync(folder); + return await GetLatestFolderContainerTypeAndUpdateCacheAsync(folder, ct); } - public async Task GetLatestFolderContainerTypeAndUpdateCacheAsync(StorageFolder folder) + public async Task GetLatestFolderContainerTypeAndUpdateCacheAsync(StorageFolder folder, CancellationToken ct) { var query = folder.CreateFileQueryWithOptions(new Windows.Storage.Search.QueryOptions(Windows.Storage.Search.CommonFileQuery.DefaultQuery, SupportedFileTypesHelper.GetAllSupportedFileExtensions()) { FolderDepth = Windows.Storage.Search.FolderDepth.Shallow }); - var count = await query.GetItemCountAsync(); + var count = await query.GetItemCountAsync().AsTask(ct); if (count == 0) { _folderContainerTypeRepository.SetContainerType(folder.Path, FolderContainerType.Other); return FolderContainerType.Other; } - var items = await query.GetFilesAsync(0, count); + var items = await query.GetFilesAsync(0, count).AsTask(ct); var containerType = items.All(x => SupportedFileTypesHelper.IsSupportedImageFileExtension(x.FileType)) ? FolderContainerType.OnlyImages : FolderContainerType.Other diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/FolderItemsCacheRepository.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/FolderItemsCacheRepository.cs new file mode 100644 index 00000000..886d2074 --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/FolderItemsCacheRepository.cs @@ -0,0 +1,79 @@ +using LiteDB; +using Microsoft.Toolkit.Diagnostics; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using TsubameViewer.Models.Infrastructure; + +namespace TsubameViewer.Models.Domain.FolderItemListing +{ + public class FolderEntry + { + public string FolderPath { get; init; } + public List ItemPaths { get; init; } + } + + + + public sealed class FolderItemsCacheRepository + { + private readonly InternalFolderItemsCacheRepository _internalFolderItemsCacheRepository; + + public sealed class InternalFolderItemsCacheRepository : LiteDBServiceBase + { + public InternalFolderItemsCacheRepository(ILiteDatabase liteDatabase) : base(liteDatabase) + { + } + + public FolderEntry FindById(string path) + { + return _collection.FindById(path); + } + } + + public FolderItemsCacheRepository(InternalFolderItemsCacheRepository internalFolderItemsCacheRepository) + { + _internalFolderItemsCacheRepository = internalFolderItemsCacheRepository; + } + + + public void AddOrUpdateItem(string folderPath, IEnumerable paths) + { + AddOrUpdateItem(new FolderEntry() { FolderPath = folderPath, ItemPaths = paths.ToList() }); + } + + public void AddOrUpdateItem(FolderEntry entry) + { + Guard.IsNotNullOrEmpty(entry.FolderPath, nameof(entry.FolderPath)); + Guard.IsNotNull(entry.ItemPaths, nameof(entry.ItemPaths)); + _internalFolderItemsCacheRepository.UpdateItem(entry); + } + + + public FolderEntry GetItem(string path) + { + return _internalFolderItemsCacheRepository.FindById(path); + } + + public void DeleteItem(string folderPath) + { + Queue queue = new(); + queue.Enqueue(folderPath); + + while (queue.TryDequeue(out string path)) + { + var item = _internalFolderItemsCacheRepository.FindById(path); + if (item is null) { continue; } + + foreach (var childPath in item.ItemPaths) + { + queue.Enqueue(childPath); + } + + _internalFolderItemsCacheRepository.DeleteItem(path); + } + } + } +} diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/ThumbnailManager.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/ThumbnailManager.cs index 79812d0e..45ad7dab 100644 --- a/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/ThumbnailManager.cs +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/FolderItemListing/ThumbnailManager.cs @@ -1,5 +1,7 @@ using LiteDB; +using Microsoft.IO; using Microsoft.Toolkit.Uwp.Helpers; +using SharpCompress.Archives; using SharpCompress.Archives.Rar; using SharpCompress.Archives.SevenZip; using SharpCompress.Archives.Tar; @@ -24,6 +26,7 @@ using Windows.Storage.FileProperties; using Windows.Storage.Search; using Windows.Storage.Streams; +using Windows.UI.Xaml.Media.Imaging; namespace TsubameViewer.Models.Domain.FolderItemListing { @@ -31,16 +34,18 @@ public sealed class ThumbnailManager { private readonly FolderListingSettings _folderListingSettings; private readonly ThumbnailImageInfoRepository _thumbnailImageInfoRepository; - + private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; private readonly FastAsyncLock _fileReadWriteLock = new FastAsyncLock(); public ThumbnailManager( FolderListingSettings folderListingSettings, - ThumbnailImageInfoRepository thumbnailImageInfoRepository + ThumbnailImageInfoRepository thumbnailImageInfoRepository, + RecyclableMemoryStreamManager recyclableMemoryStreamManager ) { _folderListingSettings = folderListingSettings; _thumbnailImageInfoRepository = thumbnailImageInfoRepository; + _recyclableMemoryStreamManager = recyclableMemoryStreamManager; } @@ -66,6 +71,32 @@ public static async ValueTask GetSecondaryTileThumbnailFolderAsyn } + public async Task SetThumbnailAsync(IStorageItem targetItem, IRandomAccessStream bitmapImage, CancellationToken ct) + { + var tempFolder = await GetTempFolderAsync(); + var itemId = GetStorageItemId(targetItem.Path); + var thumbnailFile = await tempFolder.CreateFileAsync(itemId, CreationCollisionOption.ReplaceExisting); + using (await _fileReadWriteLock.LockAsync(ct)) + { + await CopyImageToFile(targetItem.Path, bitmapImage, thumbnailFile, EncodingForFolderOrArchiveFileThumbnailBitmap, ct); + } + } + + + public async Task SetArchiveEntryThumbnailAsync(StorageFile targetItem, IArchiveEntry entry, IRandomAccessStream bitmapImage, CancellationToken ct) + { + var tempFolder = await GetTempFolderAsync(); + var path = GetArchiveEntryPath(targetItem, entry); + var itemId = GetStorageItemId(path); + var thumbnailFile = await tempFolder.CreateFileAsync(itemId, CreationCollisionOption.ReplaceExisting); + using (await _fileReadWriteLock.LockAsync(ct)) + { + await CopyImageToFile(path, bitmapImage, thumbnailFile, entry.IsDirectory ? EncodingForFolderOrArchiveFileThumbnailBitmap : EncodingForImageFileThumbnailBitmap, ct); + } + + return thumbnailFile; + } + public async Task DeleteAllThumnnailsAsync() { await Task.Run(async () => @@ -103,7 +134,18 @@ public async Task DeleteFromPath(string path) Dictionary _FilePathToHashCodeStringMap = new Dictionary(); - + public string GetArchiveEntryPath(StorageFile file, IArchiveEntry entry) + { + return Path.Combine(file.Path, entry.Key); + } + public string GetArchiveEntryPath(StorageFile file, PdfPage pdfPage) + { + return Path.Combine(file.Path, pdfPage.Index.ToString()); + } + public string GetStorageItemId(StorageFile file, IArchiveEntry entry) + { + return GetStorageItemId(GetArchiveEntryPath(file, entry)); + } public string GetStorageItemId(IStorageItem item) { return GetStorageItemId(item.Path); @@ -117,29 +159,104 @@ public string GetStorageItemId(string path) ; } - public async Task GetFileThumbnailImageAsync(StorageFile file, CancellationToken ct = default) + public async Task GetFileThumbnailImageAsync(StorageFile file, CancellationToken ct) { var tempFolder = await GetTempFolderAsync(); var itemId = GetStorageItemId(file); - if (await ApplicationData.Current.TemporaryFolder.FileExistsAsync(itemId)) + if (await tempFolder.FileExistsAsync(itemId)) { - return await ApplicationData.Current.TemporaryFolder.GetFileAsync(itemId); + var cachedThumbnailFile = await tempFolder.GetFileAsync(itemId); + using (var stream = await cachedThumbnailFile.OpenReadAsync()) + { + if (stream.Size != 0) + { + return cachedThumbnailFile; + } + } + } + + var thumbnailFile = await tempFolder.CreateFileAsync(itemId, CreationCollisionOption.ReplaceExisting); + if (SupportedFileTypesHelper.IsSupportedArchiveFileExtension(file.FileType) + ) + { + return await GenerateThumbnailImageAsync(file, thumbnailFile, EncodingForFolderOrArchiveFileThumbnailBitmap, ct); } else { - var thumbnailFile = await tempFolder.CreateFileAsync(itemId, CreationCollisionOption.ReplaceExisting); - if (SupportedFileTypesHelper.IsSupportedArchiveFileExtension(file.FileType) - ) + return await GenerateThumbnailImageAsync(file, thumbnailFile, EncodingForImageFileThumbnailBitmap, ct); + } + } + + public async Task GetArchiveEntryThumbnailImageAsync(StorageFile sourceFile, IArchiveEntry archiveEntry, CancellationToken ct) + { + var tempFolder = await GetTempFolderAsync(); + var path = GetArchiveEntryPath(sourceFile, archiveEntry); + var itemId = GetStorageItemId(path); + if (await tempFolder.FileExistsAsync(itemId)) + { + var cachedThumbnailFile = await tempFolder.GetFileAsync(itemId).AsTask(ct); + using (var stream = await cachedThumbnailFile.OpenReadAsync()) { - return await GenerateThumbnailImageAsync(file, thumbnailFile, EncodingForFolderOrArchiveFileThumbnailBitmap, ct); + if (stream.Size != 0) + { + return cachedThumbnailFile; + } } - else + } + + if (archiveEntry.IsDirectory) { return null; } + + var thumbnailFile = await tempFolder.CreateFileAsync(itemId, CreationCollisionOption.ReplaceExisting); + using (var memoryStream = _recyclableMemoryStreamManager.GetStream()) + using (await _fileReadWriteLock.LockAsync(ct)) + { + using (var entryStream = archiveEntry.OpenEntryStream()) { - return await GenerateThumbnailImageAsync(file, thumbnailFile, EncodingForImageFileThumbnailBitmap, ct); + await entryStream.CopyToAsync(memoryStream, 81920, ct); + memoryStream.Seek(0, SeekOrigin.Begin); + + ct.ThrowIfCancellationRequested(); } + + await CopyImageToFile(path, memoryStream.AsRandomAccessStream(), thumbnailFile, archiveEntry.IsDirectory ? EncodingForFolderOrArchiveFileThumbnailBitmap : EncodingForImageFileThumbnailBitmap, ct); } + + return thumbnailFile; } + public async Task GetPdfPageThumbnailImageAsync(StorageFile sourceFile, PdfPage pdfPage, CancellationToken ct) + { + var tempFolder = await GetTempFolderAsync(); + var path = GetArchiveEntryPath(sourceFile, pdfPage); + var itemId = GetStorageItemId(path); + if (await tempFolder.FileExistsAsync(itemId)) + { + var cachedThumbnailFile = await tempFolder.GetFileAsync(itemId); + using (var stream = await cachedThumbnailFile.OpenReadAsync()) + { + if (stream.Size != 0) + { + return cachedThumbnailFile; + } + } + } + + var thumbnailFile = await tempFolder.CreateFileAsync(itemId, CreationCollisionOption.ReplaceExisting).AsTask(ct); + using (var memoryStream = _recyclableMemoryStreamManager.GetStream()) + using (await _fileReadWriteLock.LockAsync(ct)) + { + await pdfPage.RenderToStreamAsync(memoryStream.AsRandomAccessStream()).AsTask(ct); + memoryStream.Seek(0, SeekOrigin.Begin); + + ct.ThrowIfCancellationRequested(); + + await CopyImageToFile(path, memoryStream.AsRandomAccessStream(), thumbnailFile, EncodingForImageFileThumbnailBitmap, ct); + } + + return thumbnailFile; + } + + private void EncodingForImageFileThumbnailBitmap(BitmapDecoder decoder, BitmapEncoder encoder) { // 縦横比を維持したまま 高さ = LargeFileThumbnailImageHeight になるようにスケーリング @@ -155,69 +272,42 @@ private Task GenerateThumbnailImageAsync(StorageFile file, StorageF { try { - using (var stream = new InMemoryRandomAccessStream()) - { - using (await _fileReadWriteLock.LockAsync(ct)) + using (var stream = _recyclableMemoryStreamManager.GetStream()) + using (await _fileReadWriteLock.LockAsync(ct)) + { + var result = await (file.FileType switch { - var result = await (file.FileType switch - { - SupportedFileTypesHelper.ZipFileType => ZipFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.RarFileType => RarFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.PdfFileType => PdfFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.CbzFileType => ZipFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.CbrFileType => RarFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.SevenZipFileType => SevenZipFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.Cb7FileType => SevenZipFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.TarFileType => TarFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - - - SupportedFileTypesHelper.JpgFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.JpegFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.PngFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.BmpFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.GifFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.TifFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.TiffFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.SvgFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - - SupportedFileTypesHelper.EPubFileType => EPubFileThubnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - _ => throw new NotSupportedException(file.FileType) - }); - - if (!result || stream.Size == 0) { return null; } - } + SupportedFileTypesHelper.ZipFileType => ZipFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.RarFileType => RarFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.PdfFileType => PdfFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.CbzFileType => ZipFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.CbrFileType => RarFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.SevenZipFileType => SevenZipFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.Cb7FileType => SevenZipFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.TarFileType => TarFileThumbnailImageWriteToStreamAsync(file, stream, ct), + + + SupportedFileTypesHelper.JpgFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.JpegFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.JfifFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.PngFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.BmpFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.GifFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.TifFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.TiffFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream, ct), + SupportedFileTypesHelper.SvgFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream, ct), + + SupportedFileTypesHelper.EPubFileType => EPubFileThubnailImageWriteToStreamAsync(file, stream, ct), + _ => throw new NotSupportedException(file.FileType) + }); + if (!result || stream.Length == 0) { return null; } ct.ThrowIfCancellationRequested(); + await CopyImageToFile(file.Path, stream.AsRandomAccessStream(), outputFile, setupEncoder, ct); - var decoder = await BitmapDecoder.CreateAsync(stream); - using (var memStream = new InMemoryRandomAccessStream()) - { - var encoder = await BitmapEncoder.CreateForTranscodingAsync(memStream, decoder); - - // サムネイルサイズ情報を記録 - _thumbnailImageInfoRepository.UpdateItem(new ThumbnailImageInfo() - { - Path = file.Path, - ImageWidth = decoder.PixelWidth, - ImageHeight = decoder.PixelHeight - }); - - setupEncoder(decoder, encoder); - - Debug.WriteLine($"thumb out <{file.Path}> size: w= {encoder.BitmapTransform.ScaledWidth} h= {encoder.BitmapTransform.ScaledHeight}"); - - await encoder.FlushAsync(); - - memStream.Seek(0); - using (await _fileReadWriteLock.LockAsync(ct)) - using (var fileStream = await outputFile.OpenAsync(FileAccessMode.ReadWrite)) - { - await RandomAccessStream.CopyAsync(memStream, fileStream); - } - } + return outputFile; } - return outputFile; } catch { @@ -226,26 +316,61 @@ private Task GenerateThumbnailImageAsync(StorageFile file, StorageF } }, ct); } + private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManagerForCopy = new RecyclableMemoryStreamManager(); + private async Task CopyImageToFile(string path, IRandomAccessStream stream, StorageFile outputFile, Action setupEncoder, CancellationToken ct) + { + var decoder = await BitmapDecoder.CreateAsync(stream); + + + //using (var memStream = _recyclableMemoryStreamManagerForCopy.GetStream().AsRandomAccessStream()) + using (var memStream = new InMemoryRandomAccessStream()) + { + var encoder = await BitmapEncoder.CreateForTranscodingAsync(memStream, decoder); + + // サムネイルサイズ情報を記録 + _thumbnailImageInfoRepository.UpdateItem(new ThumbnailImageInfo() + { + Path = path, + ImageWidth = decoder.PixelWidth, + ImageHeight = decoder.PixelHeight + }); + + setupEncoder(decoder, encoder); + + Debug.WriteLine($"thumb out <{path}> size: w= {encoder.BitmapTransform.ScaledWidth} h= {encoder.BitmapTransform.ScaledHeight}"); + + await encoder.FlushAsync().AsTask(ct); + + memStream.Seek(0); + using (await _fileReadWriteLock.LockAsync(ct)) + using (var fileStream = await outputFile.OpenAsync(FileAccessMode.ReadWrite).AsTask(ct)) + { + await RandomAccessStream.CopyAsync(memStream, fileStream).AsTask(ct); + } + } + } - private static async Task ImageFileThumbnailImageWriteToStreamAsync(StorageFile file, Stream outputStream) + private static async Task ImageFileThumbnailImageWriteToStreamAsync(StorageFile file, Stream outputStream, CancellationToken ct) { - using (var fileStream = await file.OpenStreamForReadAsync()) + using (var fileStream = await file.OpenReadAsync().AsTask(ct)) { - await RandomAccessStream.CopyAsync(fileStream.AsRandomAccessStream(), outputStream.AsOutputStream()); + await RandomAccessStream.CopyAsync(fileStream, outputStream.AsOutputStream()).AsTask(ct); return true; } } - private static async Task ZipFileThumbnailImageWriteToStreamAsync(StorageFile file, Stream outputStream) + private static async Task ZipFileThumbnailImageWriteToStreamAsync(StorageFile file, Stream outputStream, CancellationToken ct) { - using (var archiveStream = await file.OpenStreamForReadAsync()) + using (var archiveStream = (await file.OpenReadAsync().AsTask(ct)).AsStreamForRead()) using (var zipArchive = new ZipArchive(archiveStream)) { + ct.ThrowIfCancellationRequested(); var archiveImageItem = zipArchive.Entries.OrderBy(x=> x.Name).FirstOrDefault(x => SupportedFileTypesHelper.IsSupportedImageFileExtension(x.Name)); if (archiveImageItem == null) { return false; } using (var inputStream = archiveImageItem.Open()) { - await inputStream.CopyToAsync(outputStream); + await inputStream.CopyToAsync(outputStream, 81920, ct); + ct.ThrowIfCancellationRequested(); await outputStream.FlushAsync(); } @@ -253,17 +378,17 @@ private static async Task ZipFileThumbnailImageWriteToStreamAsync(StorageF } } - private static async Task RarFileThumbnailImageWriteToStreamAsync(StorageFile file, Stream outputStream) + private static async Task RarFileThumbnailImageWriteToStreamAsync(StorageFile file, Stream outputStream, CancellationToken ct) { - using (var archiveStream = await file.OpenAsync(FileAccessMode.Read)) - using (var rarArchive = RarArchive.Open(archiveStream.AsStreamForRead())) + using (var archiveStream = (await file.OpenReadAsync().AsTask(ct)).AsStreamForRead()) + using (var rarArchive = RarArchive.Open(archiveStream)) { var archiveImageItem = rarArchive.Entries.OrderBy(x => x.Key).FirstOrDefault(x => SupportedFileTypesHelper.IsSupportedImageFileExtension(x.Key)); if (archiveImageItem == null) { return false; } using (var inputStream = archiveImageItem.OpenEntryStream()) { - await inputStream.CopyToAsync(outputStream); + await inputStream.CopyToAsync(outputStream, 81920, ct); await outputStream.FlushAsync(); } @@ -271,9 +396,9 @@ private static async Task RarFileThumbnailImageWriteToStreamAsync(StorageF } } - private static async Task SevenZipFileThumbnailImageWriteToStreamAsync(StorageFile file, Stream outputStream) + private static async Task SevenZipFileThumbnailImageWriteToStreamAsync(StorageFile file, Stream outputStream, CancellationToken ct) { - using (var archiveStream = await file.OpenStreamForReadAsync()) + using (var archiveStream = (await file.OpenReadAsync().AsTask(ct)).AsStreamForRead()) using (var zipArchive = SevenZipArchive.Open(archiveStream)) { var archiveImageItem = zipArchive.Entries.OrderBy(x => x.Key).FirstOrDefault(x => SupportedFileTypesHelper.IsSupportedImageFileExtension(x.Key)); @@ -281,7 +406,7 @@ private static async Task SevenZipFileThumbnailImageWriteToStreamAsync(Sto using (var inputStream = archiveImageItem.OpenEntryStream()) { - await inputStream.CopyToAsync(outputStream); + await inputStream.CopyToAsync(outputStream, 81920, ct); await outputStream.FlushAsync(); } @@ -289,9 +414,9 @@ private static async Task SevenZipFileThumbnailImageWriteToStreamAsync(Sto } } - private static async Task TarFileThumbnailImageWriteToStreamAsync(StorageFile file, Stream outputStream) + private static async Task TarFileThumbnailImageWriteToStreamAsync(StorageFile file, Stream outputStream, CancellationToken ct) { - using (var archiveStream = await file.OpenStreamForReadAsync()) + using (var archiveStream = (await file.OpenReadAsync().AsTask(ct)).AsStreamForRead()) using (var zipArchive = TarArchive.Open(archiveStream)) { var archiveImageItem = zipArchive.Entries.OrderBy(x => x.Key).FirstOrDefault(x => SupportedFileTypesHelper.IsSupportedImageFileExtension(x.Key)); @@ -299,7 +424,7 @@ private static async Task TarFileThumbnailImageWriteToStreamAsync(StorageF using (var inputStream = archiveImageItem.OpenEntryStream()) { - await inputStream.CopyToAsync(outputStream); + await inputStream.CopyToAsync(outputStream, 81920, ct); await outputStream.FlushAsync(); } @@ -307,26 +432,25 @@ private static async Task TarFileThumbnailImageWriteToStreamAsync(StorageF } } - private static async Task PdfFileThumbnailImageWriteToStreamAsync(StorageFile file, Stream outputStream) + private static async Task PdfFileThumbnailImageWriteToStreamAsync(StorageFile file, Stream outputStream, CancellationToken ct) { - var pdfDocument = await PdfDocument.LoadFromFileAsync(file); + var pdfDocument = await PdfDocument.LoadFromFileAsync(file).AsTask(ct); if (pdfDocument.PageCount == 0) { return false; } - var page = pdfDocument.GetPage(0); - await page.RenderToStreamAsync(outputStream.AsRandomAccessStream()); + using var page = pdfDocument.GetPage(0); + await page.RenderToStreamAsync(outputStream.AsRandomAccessStream()).AsTask(ct); return true; } - public async Task GetThumbnailAsync(IStorageItem storageItem) + public async Task GetThumbnailAsync(IStorageItem storageItem, CancellationToken ct) { if (storageItem is StorageFolder folder) { - return await GetFolderThumbnailAsync(folder); + return await GetFolderThumbnailAsync(folder, ct); } else if (storageItem is StorageFile file) { - var thumb = await GetFileThumbnailImageAsync(file); - return new Uri(thumb.Path); + return await GetFileThumbnailImageAsync(file, ct); } else { @@ -334,13 +458,12 @@ public async Task GetThumbnailAsync(IStorageItem storageItem) } } - public async Task GetFolderThumbnailAsync(StorageFolder folder, CancellationToken ct = default) + public async Task GetFolderThumbnailAsync(StorageFolder folder, CancellationToken ct) { var itemId = GetStorageItemId(folder); if (await ApplicationData.Current.TemporaryFolder.FileExistsAsync(itemId)) { - var cachedFile = await ApplicationData.Current.TemporaryFolder.GetFileAsync(itemId); - return new Uri(cachedFile.Path, UriKind.Absolute); + return await ApplicationData.Current.TemporaryFolder.GetFileAsync(itemId); } else { @@ -354,8 +477,7 @@ public async Task GetFolderThumbnailAsync(StorageFolder folder, Cancellatio var thumbnailFile = await tempFolder.CreateFileAsync(itemId); var files = await query.GetFilesAsync(0, 1); - var outputFile = await GenerateThumbnailImageAsync(files[0], thumbnailFile, EncodingForFolderOrArchiveFileThumbnailBitmap, ct); - return new Uri(outputFile.Path); + return await GenerateThumbnailImageAsync(files[0], thumbnailFile, EncodingForFolderOrArchiveFileThumbnailBitmap, ct); #else return null; #endif @@ -383,9 +505,9 @@ private void EncodingForFolderOrArchiveFileThumbnailBitmap(BitmapDecoder decoder - private async Task EPubFileThubnailImageWriteToStreamAsync(StorageFile file, Stream outputStream) + private async Task EPubFileThubnailImageWriteToStreamAsync(StorageFile file, Stream outputStream, CancellationToken ct) { - using var fileStream = await file.OpenStreamForReadAsync(); + using var fileStream = (await file.OpenReadAsync().AsTask(ct)).AsStreamForRead(); var epubBook = await EpubReader.OpenBookAsync(fileStream); @@ -500,28 +622,28 @@ public Task GenerateSecondaryThumbnailImag } } - public async Task GenerateSecondaryThumbnailImageAsync(StorageFolder folder) + public async Task GenerateSecondaryThumbnailImageAsync(StorageFolder folder, CancellationToken ct) { var itemId = GetStorageItemId(folder); #if WINDOWS_UWP var query = folder.CreateFileQueryWithOptions(new QueryOptions(CommonFileQuery.OrderByName, SupportedFileTypesHelper.GetAllSupportedFileExtensions()) { FolderDepth = FolderDepth.Deep }); - var count = await query.GetItemCountAsync(); + var count = await query.GetItemCountAsync().AsTask(ct); if (count == 0) { return null; } - var files = await query.GetFilesAsync(0, 1); - return await GenerateSecondaryThumbnailImageAsync(files[0], itemId); + var files = await query.GetFilesAsync(0, 1).AsTask(ct); + return await GenerateSecondaryThumbnailImageAsync(files[0], itemId, ct); #endif } - public Task GenerateSecondaryThumbnailImageAsync(StorageFile file) + public Task GenerateSecondaryThumbnailImageAsync(StorageFile file, CancellationToken ct) { var itemId = GetStorageItemId(file); - return GenerateSecondaryThumbnailImageAsync(file, itemId); + return GenerateSecondaryThumbnailImageAsync(file, itemId, ct); } - private Task GenerateSecondaryThumbnailImageAsync(StorageFile file, string itemId) + private Task GenerateSecondaryThumbnailImageAsync(StorageFile file, string itemId, CancellationToken ct) { return Task.Run(async () => { @@ -537,18 +659,19 @@ private Task GenerateSecondaryThumbnailIm { var result = await (file.FileType switch { - SupportedFileTypesHelper.ZipFileType => ZipFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.RarFileType => RarFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.PdfFileType => PdfFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.JpgFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.JpegFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.PngFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.BmpFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.GifFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.TifFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.TiffFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.SvgFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), - SupportedFileTypesHelper.EPubFileType => EPubFileThubnailImageWriteToStreamAsync(file, stream.AsStreamForWrite()), + SupportedFileTypesHelper.ZipFileType => ZipFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite(), ct), + SupportedFileTypesHelper.RarFileType => RarFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite(), ct), + SupportedFileTypesHelper.PdfFileType => PdfFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite(), ct), + SupportedFileTypesHelper.JpgFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite(), ct), + SupportedFileTypesHelper.JpegFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite(), ct), + SupportedFileTypesHelper.JfifFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite(), ct), + SupportedFileTypesHelper.PngFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite(), ct), + SupportedFileTypesHelper.BmpFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite(), ct), + SupportedFileTypesHelper.GifFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite(), ct), + SupportedFileTypesHelper.TifFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite(), ct), + SupportedFileTypesHelper.TiffFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite(), ct), + SupportedFileTypesHelper.SvgFileType => ImageFileThumbnailImageWriteToStreamAsync(file, stream.AsStreamForWrite(), ct), + SupportedFileTypesHelper.EPubFileType => EPubFileThubnailImageWriteToStreamAsync(file, stream.AsStreamForWrite(), ct), _ => throw new NotSupportedException(file.FileType) }); @@ -610,7 +733,7 @@ private Task GenerateSecondaryThumbnailIm await itemFolder.DeleteAsync(StorageDeleteOption.PermanentDelete); throw; } - }); + }, ct); } #endregion diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ArchiveTextEncodingRepository.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ArchiveTextEncodingRepository.cs new file mode 100644 index 00000000..e84bfdb3 --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ArchiveTextEncodingRepository.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TsubameViewer.Models.Domain.ImageViewer +{ + public sealed class ArchiveTextEncodingRepository + { + } +} diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/IImageSource.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/IImageSource.cs index 57d95418..447c41b0 100644 --- a/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/IImageSource.cs +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/IImageSource.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Windows.Storage; +using Windows.Storage.Streams; using Windows.UI.Xaml.Media.Imaging; namespace TsubameViewer.Models.Domain.ImageViewer @@ -12,8 +13,11 @@ public interface IImageSource { IStorageItem StorageItem { get; } string Name { get; } + + string Path { get; } DateTime DateCreated { get; } - Task GenerateBitmapImageAsync(CancellationToken ct = default); - Task GenerateThumbnailBitmapImageAsync(CancellationToken ct = default); + Task GetThumbnailImageStreamAsync(CancellationToken ct = default); + + Task GetImageStreamAsync(CancellationToken ct = default); } } diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageCollectionManager.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageCollectionManager.cs index 1a22f9cd..aae5c7c0 100644 --- a/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageCollectionManager.cs +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageCollectionManager.cs @@ -1,6 +1,8 @@ using Microsoft.IO; +using Microsoft.Toolkit.Diagnostics; using Reactive.Bindings; using Reactive.Bindings.Extensions; +using SharpCompress.Archives; using SharpCompress.Archives.Rar; using SharpCompress.Archives.SevenZip; using SharpCompress.Archives.Tar; @@ -12,7 +14,9 @@ using System.IO; //using System.IO.Compression; using System.Linq; +using System.Reactive; using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading; @@ -35,277 +39,286 @@ public class ImageCollectionResult } + public interface IImageCollectionContext + { + string Name { get; } + + Task IsExistImageFileAsync(CancellationToken ct); + Task IsExistFolderOrArchiveFileAsync(CancellationToken ct); + Task> GetAllImageFilesAsync(CancellationToken ct); + + Task> GetImageFilesAsync(CancellationToken ct); + + Task> GetFolderOrArchiveFilesAsync(CancellationToken ct); + Task> GetLeafFoldersAsync(CancellationToken ct); + + bool IsSupportedFolderContentsChanged { get; } + + IObservable CreateFolderAndArchiveFileChangedObserver(); + IObservable CreateImageFileChangedObserver(); + } + + public sealed class ImageCollectionManager { - private readonly ThumbnailManager _thumbnailManager; - private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; - public ImageCollectionManager( - ThumbnailManager thumbnailManager, - RecyclableMemoryStreamManager recyclableMemoryStreamManager - ) + public sealed class ArchiveImageCollectionContext : IImageCollectionContext, IDisposable { - _thumbnailManager = thumbnailManager; - _recyclableMemoryStreamManager = recyclableMemoryStreamManager; - } + public ArchiveImageCollection ArchiveImageCollection { get; } + public ArchiveDirectoryToken ArchiveDirectoryToken { get; } + private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; + private readonly ThumbnailManager _thumbnailManager; - public async Task IsExistImageFileAsync(IStorageItem storageItem, CancellationToken ct) - { - if (storageItem is StorageFolder folder) + public string Name => ArchiveImageCollection.Name; + + + public ArchiveImageCollectionContext(ArchiveImageCollection archiveImageCollection, ArchiveDirectoryToken archiveDirectoryToken, RecyclableMemoryStreamManager recyclableMemoryStreamManager, ThumbnailManager thumbnailManager) { - var query = MakeImageFileSearchQueryResult(folder); - var count = await query.GetItemCountAsync().AsTask(ct); - return count > 0; + ArchiveImageCollection = archiveImageCollection; + ArchiveDirectoryToken = archiveDirectoryToken; + _recyclableMemoryStreamManager = recyclableMemoryStreamManager; + _thumbnailManager = thumbnailManager; + } + + public Task> GetFolderOrArchiveFilesAsync(CancellationToken ct) + { + // アーカイブファイルは内部にフォルダ構造を持っている可能性がある + // アーカイブ内のアーカイブは対応しない + return Task.FromResult(ArchiveImageCollection.GetSubDirectories(ArchiveDirectoryToken) + .Select(x => (IImageSource)new ArchiveDirectoryImageSource(ArchiveImageCollection, x, _thumbnailManager)) + .ToList() + ); } - else if (storageItem is StorageFile file && file.IsSupportedMangaFile()) + + public Task> GetLeafFoldersAsync(CancellationToken ct) { - return true; + return Task.FromResult(ArchiveImageCollection.GetLeafFolders() + .Select(x => (IImageSource)new ArchiveDirectoryImageSource(ArchiveImageCollection, x, _thumbnailManager)) + .ToList()); } - else + + public Task> GetAllImageFilesAsync(CancellationToken ct) { - return false; + return Task.FromResult(ArchiveImageCollection.GetAllImages()); } - - } - public async Task IsExistFolderOrArchiveFileAsync(StorageFolder folder, CancellationToken ct) - { - var query = MakeFoldersAndArchiveFileSearchQueryResult(folder); - var count = await query.GetItemCountAsync().AsTask(ct); - return count > 0; - } + public Task> GetImageFilesAsync(CancellationToken ct) + { + return Task.FromResult(ArchiveImageCollection.GetImagesFromDirectory(ArchiveDirectoryToken)); + } - public Task GetImagesAsync(IStorageItem storageItem, CancellationToken ct = default) - { - if (storageItem is StorageFile file) + public Task IsExistFolderOrArchiveFileAsync(CancellationToken ct) { - return GetImagesFromFileAsync(file, ct); + return Task.FromResult(ArchiveImageCollection.GetSubDirectories(ArchiveDirectoryToken).Any()); } - // フォルダ内のフォルダ、画像ファイル、圧縮ファイルを列挙して返す - else if (storageItem is StorageFolder folder) + + public Task IsExistImageFileAsync(CancellationToken ct) { - return GetImagesFromFolderAsync(folder, ct); + return Task.FromResult(ArchiveImageCollection.GetImagesFromDirectory(ArchiveDirectoryToken).Any()); } - else + + public bool IsSupportedFolderContentsChanged => false; + + public IObservable CreateFolderAndArchiveFileChangedObserver() => throw new NotSupportedException(); + + public IObservable CreateImageFileChangedObserver() => throw new NotSupportedException(); + + public void Dispose() { - throw new NotSupportedException(); + ((IDisposable)ArchiveImageCollection).Dispose(); } } - public async Task GetFolderOrArchiveFileAsync(IStorageItem storageItem, CancellationToken ct) + + public sealed class PdfImageCollectionContext : IImageCollectionContext { - // フォルダ内のフォルダを列挙して返す - if (storageItem is StorageFolder folder) + private readonly PdfImageCollection _pdfImageCollection; + + public PdfImageCollectionContext(PdfImageCollection pdfImageCollection) { - var result = await GetSubFoldersAndArchiveFileAsync(folder, ct); + _pdfImageCollection = pdfImageCollection; + } - try - { - var images = new IImageSource[result.ItemsCount]; - int index = 0; - - bool isAllImageFile = true; - await foreach (var item in result.Images.WithCancellation(ct)) - { - images[index] = item; - index++; - - isAllImageFile &= (item as StorageItemImageSource)?.ItemTypes == StorageItemTypes.Image; - } - - return new ImageCollectionResult() - { - Images = images, - ItemsEnumeratorDisposer = Disposable.Empty, - FirstSelectedIndex = 0, - ParentFolderOrArchiveName = String.Empty, - }; - } - catch (OperationCanceledException) - { - return null; - } + public string Name => _pdfImageCollection.Name; + + public bool IsSupportedFolderContentsChanged => false; + public IObservable CreateFolderAndArchiveFileChangedObserver() => throw new NotSupportedException(); + public IObservable CreateImageFileChangedObserver() => throw new NotSupportedException(); + + public Task> GetFolderOrArchiveFilesAsync(CancellationToken ct) + { + return Task.FromResult(new List()); } - else + + public Task> GetLeafFoldersAsync(CancellationToken ct) { - throw new NotSupportedException(); + return Task.FromResult(new List()); } - } - private Task GetImagesFromFileAsync(StorageFile file, CancellationToken ct) - { - // 画像ファイルを指定された場合だけ特殊対応として、その親フォルダの内容を列挙して返す - if (SupportedFileTypesHelper.IsSupportedImageFileExtension(file.FileType)) + public Task> GetAllImageFilesAsync(CancellationToken ct) { - return GetParentFolderImagesAsync(file, ct); + return Task.FromResult(_pdfImageCollection.GetAllImages()); } - // 圧縮ファイルを展開した中身を列挙して返す - else if (SupportedFileTypesHelper.IsSupportedArchiveFileExtension(file.FileType)) + + public Task> GetImageFilesAsync(CancellationToken ct) { - return GetArchiveFileImagesAsync(file, ct); + return Task.FromResult(_pdfImageCollection.GetAllImages()); } - else + + public Task IsExistFolderOrArchiveFileAsync(CancellationToken ct) { - throw new NotSupportedException(); + return Task.FromResult(false); + } + + public Task IsExistImageFileAsync(CancellationToken ct) + { + return Task.FromResult(_pdfImageCollection.GetAllImages().Any()); } } - private async Task GetParentFolderImagesAsync(StorageFile file, CancellationToken ct) + + public sealed class FolderImageCollectionContext : IImageCollectionContext { - // Note: 親フォルダへのアクセス権限無い場合が想定されるが - //    アプリとしてユーザーに選択可能としているのはフォルダのみなので無視できる? + private readonly ThumbnailManager _thumbnailManager; + private StorageItemQueryResult _folderAndArchiveFileSearchQuery; + private StorageItemQueryResult FolderAndArchiveFileSearchQuery => _folderAndArchiveFileSearchQuery ??= Folder.CreateItemQueryWithOptions(ImageCollectionManager.FoldersAndArchiveFileSearchQueryOptions); - // TODO: 外部からアプリに画像が渡された時、親フォルダへのアクセス権が無いケースに対応する - var parentFolder = await file.GetParentAsync(); + private StorageFileQueryResult _imageFileSearchQuery; + private StorageFileQueryResult ImageFileSearchQuery => _imageFileSearchQuery ??= Folder.CreateFileQueryWithOptions(ImageCollectionManager.ImageFileSearchQueryOptions); - // 画像ファイルが選ばれた時、そのファイルの所属フォルダをコレクションとして表示する - var result = await Task.Run(async () => await GetFolderImagesAsync(parentFolder, ct)); - try + public string Name => Folder.Name; + + public FolderImageCollectionContext(StorageFolder storageFolder, ThumbnailManager thumbnailManager) { - List images = new List(); - int firstSelectedIndex = 0; - if (result.Images != null) - { - int index = 0; - await foreach (var item in result.Images?.WithCancellation(ct)) - { - images.Add(item); - if (item.Name == file.Name) - { - firstSelectedIndex = index; - } - index++; - } - - images.Sort(ImageSourceNameInterporatedComparer.Default); - } - else - { - images = new List() { new StorageItemImageSource(file, _thumbnailManager) }; - } + Folder = storageFolder; + _thumbnailManager = thumbnailManager; + } - return new ImageCollectionResult() - { - Images = images.ToArray(), - ItemsEnumeratorDisposer = Disposable.Empty, - FirstSelectedIndex = firstSelectedIndex, - ParentFolderOrArchiveName = parentFolder?.Name - }; + public StorageFolder Folder { get; } + + public async Task> GetFolderOrArchiveFilesAsync(CancellationToken ct) + { + var items = await FolderAndArchiveFileSearchQuery.GetItemsAsync().AsTask(ct); + return items.Select(x => new StorageItemImageSource(x, _thumbnailManager) as IImageSource).ToList(); } - catch (OperationCanceledException) + + public Task> GetLeafFoldersAsync(CancellationToken ct) { - return null; + return Task.FromResult(new List()); } - } + public Task> GetAllImageFilesAsync(CancellationToken ct) + { + return GetImageFilesAsync(ct); + } - private async Task GetArchiveFileImagesAsync(StorageFile file, CancellationToken ct) - { - try + public async Task> GetImageFilesAsync(CancellationToken ct) { - var result = await Task.Run(async () => await GetImagesFromArchiveFileAsync(file, ct)); + var items = await ImageFileSearchQuery.GetFilesAsync().AsTask(ct); + return items.Select(x => new StorageItemImageSource(x, _thumbnailManager) as IImageSource).ToList(); + } - result.Images.Sort(ImageSourceNameInterporatedComparer.Default); + public async Task IsExistFolderOrArchiveFileAsync(CancellationToken ct) + { + var count = await FolderAndArchiveFileSearchQuery.GetItemCountAsync().AsTask(ct); + return count > 0; + } - return new ImageCollectionResult() - { - Images = result.Images.ToArray(), - ItemsEnumeratorDisposer = result.Disposer, - FirstSelectedIndex = 0, - ParentFolderOrArchiveName = file.Name - }; + public async Task IsExistImageFileAsync(CancellationToken ct) + { + var count = await ImageFileSearchQuery.GetItemCountAsync().AsTask(ct); + return count > 0; } - catch (OperationCanceledException) + + public bool IsSupportedFolderContentsChanged => true; + + + public IObservable CreateFolderAndArchiveFileChangedObserver() + { + return WindowsObservable.FromEventPattern(h => FolderAndArchiveFileSearchQuery.ContentsChanged += h, h => FolderAndArchiveFileSearchQuery.ContentsChanged -= h).ToUnit(); + } + + public IObservable CreateImageFileChangedObserver() { - return null; + return WindowsObservable.FromEventPattern(h => ImageFileSearchQuery.ContentsChanged += h, h => ImageFileSearchQuery.ContentsChanged -= h).ToUnit(); } + } + private readonly ThumbnailManager _thumbnailManager; + private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; + public ImageCollectionManager( + ThumbnailManager thumbnailManager, + RecyclableMemoryStreamManager recyclableMemoryStreamManager + ) + { + _thumbnailManager = thumbnailManager; + _recyclableMemoryStreamManager = recyclableMemoryStreamManager; + } - private async Task GetImagesFromFolderAsync(StorageFolder folder, CancellationToken ct) + public bool IsSupportGetArchiveImageCollectionContext(IStorageItem storageItem, CancellationToken ct) { - var result = await GetFolderImagesAsync(folder, ct); - try + if (storageItem is StorageFile file) { - var images = new IImageSource[result.ItemsCount]; - int index = 0; - - await foreach (var item in result.Images.WithCancellation(ct)) - { - images[index] = item; - index++; - } - - return new ImageCollectionResult() - { - Images = images, - ItemsEnumeratorDisposer = Disposable.Empty, - FirstSelectedIndex = 0, - ParentFolderOrArchiveName = String.Empty, - }; + return file.IsSupportedMangaFile(); } - catch (OperationCanceledException) + else { - return null; + return false; } } - public struct GetImagesFromArchiveResult + public bool IsSupportGetImageCollectionContext(IStorageItem storageItem, CancellationToken ct) { - public uint ItemsCount { get; set; } - public IDisposable Disposer { get; set; } - public List Images { get; set; } + if (storageItem is StorageFile file) + { + return file.IsSupportedMangaFile(); + } + else if (storageItem is StorageFolder folder) + { + return true; + } + else + { + throw new NotSupportedException(); + } } - - private class ImageSourceNameInterporatedComparer : IComparer + public async Task GetArchiveImageCollectionContextAsync(StorageFile file, string? archiveDirectoryPath, CancellationToken ct) { - public static readonly ImageSourceNameInterporatedComparer Default = new ImageSourceNameInterporatedComparer(); - private ImageSourceNameInterporatedComparer() { } - public int Compare(IImageSource x, IImageSource y) - { - var xDictPath = Path.GetDirectoryName(x.Name); - var yDictPath = Path.GetDirectoryName(y.Name); + Guard.IsTrue(file.IsSupportedMangaFile(), "file.IsSupportedMangaFile"); - if (xDictPath != yDictPath) + // Task.Runで包まないとUIが固まる + var imageCollection = await Task.Run(async () => await GetImagesFromArchiveFileAsync(file, ct)); + if (imageCollection is ArchiveImageCollection aic) + { + var directoryToken = archiveDirectoryPath is not null ? aic.GetDirectoryTokenFromPath(archiveDirectoryPath) : null; + if (archiveDirectoryPath is not null && directoryToken is null) { - return String.CompareOrdinal(x.Name, y.Name); + throw new ArgumentException("not found directory in Archive file : " + archiveDirectoryPath); } + return new ArchiveImageCollectionContext(aic, directoryToken, _recyclableMemoryStreamManager, _thumbnailManager); + } + else if (imageCollection is PdfImageCollection pdfImageCollection) + { + return new PdfImageCollectionContext(pdfImageCollection); + } + else + { + throw new NotSupportedException(); + } - static bool TryGetPageNumber(string name, out int pageNumber) - { - int keta = 1; - int number = 0; - foreach (var i in name.Reverse().SkipWhile(c => !char.IsDigit(c)).TakeWhile(c => char.IsDigit(c))) - { - number += i * keta; - keta *= 10; - } - - pageNumber = number; - return number > 0; - } + } - var xName = Path.GetFileNameWithoutExtension(x.Name); - if (!TryGetPageNumber(xName, out int xPageNumber)) { return String.CompareOrdinal(x.Name, y.Name); } + public Task GetFolderImageCollectionContextAsync(StorageFolder folder, CancellationToken ct) + { + return Task.FromResult(new FolderImageCollectionContext(folder, _thumbnailManager)); + } - var yName = Path.GetFileNameWithoutExtension(y.Name); - if (!TryGetPageNumber(yName, out int yPageNumber)) { return String.CompareOrdinal(x.Name, y.Name); } - return xPageNumber - yPageNumber; - } - } + + public static readonly QueryOptions ImageFileSearchQueryOptions = new QueryOptions(CommonFileQuery.DefaultQuery, SupportedFileTypesHelper.SupportedImageFileExtensions); - private async Task<(uint ItemsCount, IAsyncEnumerable Images)> GetFolderItemsAsync(StorageFolder storageFolder, CancellationToken ct) - { -#if WINDOWS_UWP - var query = storageFolder.CreateItemQueryWithOptions(new QueryOptions(CommonFileQuery.DefaultQuery, SupportedFileTypesHelper.GetAllSupportedFileExtensions())); - var itemsCount = await query.GetItemCountAsync(); - return (itemsCount, AsyncEnumerableItems(itemsCount, query, ct)); -#else - return (itemsCount, AsyncEnumerableImages( -#endif - } #if WINDOWS_UWP private async IAsyncEnumerable AsyncEnumerableItems(uint count, StorageItemQueryResult queryResult, [EnumeratorCancellation] CancellationToken ct = default) { @@ -318,21 +331,8 @@ private async IAsyncEnumerable AsyncEnumerableItems(uint count, St #endif - private StorageItemQueryResult MakeFoldersAndArchiveFileSearchQueryResult(StorageFolder folder) - { - return folder.CreateItemQueryWithOptions(new QueryOptions(CommonFileQuery.DefaultQuery, Enumerable.Concat(SupportedFileTypesHelper.SupportedArchiveFileExtensions, SupportedFileTypesHelper.SupportedEBookFileExtensions))); - } + public static readonly QueryOptions FoldersAndArchiveFileSearchQueryOptions = new QueryOptions(CommonFileQuery.DefaultQuery, Enumerable.Concat(SupportedFileTypesHelper.SupportedArchiveFileExtensions, SupportedFileTypesHelper.SupportedEBookFileExtensions)); - private async Task<(uint ItemsCount, IAsyncEnumerable Images)> GetSubFoldersAndArchiveFileAsync(StorageFolder storageFolder, CancellationToken ct) - { -#if WINDOWS_UWP - var query = MakeFoldersAndArchiveFileSearchQueryResult(storageFolder); - var itemsCount = await query.GetItemCountAsync(); - return (itemsCount, AsyncEnumerableItems(itemsCount, query, ct)); -#else - return (itemsCount, AsyncEnumerableImages( -#endif - } private StorageFileQueryResult MakeImageFileSearchQueryResult(StorageFolder storageFolder) { @@ -364,19 +364,19 @@ private async IAsyncEnumerable AsyncEnumerableImages(uint count, S - private async Task GetImagesFromArchiveFileAsync(StorageFile file, CancellationToken ct) + private async Task GetImagesFromArchiveFileAsync(StorageFile file, CancellationToken ct) { var fileType = file.FileType.ToLower(); - var result = fileType switch - { - SupportedFileTypesHelper.ZipFileType => await GetImagesFromZipFileAsync(file), - SupportedFileTypesHelper.RarFileType => await GetImagesFromRarFileAsync(file), - SupportedFileTypesHelper.PdfFileType => await GetImagesFromPdfFileAsync(file), - SupportedFileTypesHelper.CbzFileType => await GetImagesFromZipFileAsync(file), - SupportedFileTypesHelper.CbrFileType => await GetImagesFromRarFileAsync(file), - SupportedFileTypesHelper.SevenZipFileType => await GetImagesFromSevenZipFileAsync(file), - SupportedFileTypesHelper.Cb7FileType => await GetImagesFromSevenZipFileAsync(file), - SupportedFileTypesHelper.TarFileType => await GetImagesFromTarFileAsync(file), + IImageCollection result = fileType switch + { + SupportedFileTypesHelper.ZipFileType => await GetImagesFromZipFileAsync(file, ct), + SupportedFileTypesHelper.RarFileType => await GetImagesFromRarFileAsync(file, ct), + SupportedFileTypesHelper.PdfFileType => await GetImagesFromPdfFileAsync(file, ct), + SupportedFileTypesHelper.CbzFileType => await GetImagesFromZipFileAsync(file, ct), + SupportedFileTypesHelper.CbrFileType => await GetImagesFromRarFileAsync(file, ct), + SupportedFileTypesHelper.SevenZipFileType => await GetImagesFromSevenZipFileAsync(file, ct), + SupportedFileTypesHelper.Cb7FileType => await GetImagesFromSevenZipFileAsync(file, ct), + SupportedFileTypesHelper.TarFileType => await GetImagesFromTarFileAsync(file, ct), _ => throw new NotSupportedException("not supported file type: " + file.FileType), }; @@ -386,109 +386,420 @@ private async Task GetImagesFromArchiveFileAsync(Sto - private async Task GetImagesFromZipFileAsync(StorageFile file) + private async Task GetImagesFromZipFileAsync(StorageFile file, CancellationToken ct) { CompositeDisposable disposables = new CompositeDisposable(); - var stream = await file.OpenStreamForReadAsync() - .AddTo(disposables); - var zipArchive = ZipArchive.Open(stream) - .AddTo(disposables); - - var supportedEntries = zipArchive.Entries - .Where(x => SupportedFileTypesHelper.IsSupportedImageFileExtension(x.Key)) - .Select(x => (IImageSource)new ArchiveEntryImageSource(x, file, _recyclableMemoryStreamManager)) - .ToList(); + var stream = await file.OpenReadAsync().AsTask(ct); + disposables.Add(stream); - return new GetImagesFromArchiveResult() + try { - ItemsCount = (uint)supportedEntries.Count, - Disposer = disposables, - Images = supportedEntries, - }; + ct.ThrowIfCancellationRequested(); + + var zipArchive = ZipArchive.Open(stream.AsStreamForRead()) + .AddTo(disposables); + + ct.ThrowIfCancellationRequested(); + return new ArchiveImageCollection(file, zipArchive, disposables, _recyclableMemoryStreamManager, _thumbnailManager); + } + catch + { + disposables.Dispose(); + throw; + } } - private async Task GetImagesFromPdfFileAsync(StorageFile file) + private async Task GetImagesFromPdfFileAsync(StorageFile file, CancellationToken ct) { - var pdfDocument = await PdfDocument.LoadFromFileAsync(file); + var pdfDocument = await PdfDocument.LoadFromFileAsync(file).AsTask(ct); + return new PdfImageCollection(file, pdfDocument, _recyclableMemoryStreamManager, _thumbnailManager); + } - var supportedEntries = Enumerable.Range(0, (int)pdfDocument.PageCount) - .Select(x => pdfDocument.GetPage((uint)x)) - .Select(x => (IImageSource)new PdfPageImageSource(x, file, _recyclableMemoryStreamManager)) - .ToList(); - return new GetImagesFromArchiveResult() + private async Task GetImagesFromRarFileAsync(StorageFile file, CancellationToken ct) + { + CompositeDisposable disposables = new CompositeDisposable(); + var stream = await file.OpenReadAsync().AsTask(ct); + disposables.Add(stream); + + try { - ItemsCount = pdfDocument.PageCount, - Disposer = Disposable.Empty, - Images = supportedEntries, - }; + var rarArchive = RarArchive.Open(stream.AsStreamForRead()) + .AddTo(disposables); + + ct.ThrowIfCancellationRequested(); + return new ArchiveImageCollection(file, rarArchive, disposables, _recyclableMemoryStreamManager, _thumbnailManager); + } + catch + { + disposables.Dispose(); + throw; + } } - private async Task GetImagesFromRarFileAsync(StorageFile file) + private async Task GetImagesFromSevenZipFileAsync(StorageFile file, CancellationToken ct) { CompositeDisposable disposables = new CompositeDisposable(); - var stream = await file.OpenStreamForReadAsync() - .AddTo(disposables); - var rarArchive = RarArchive.Open(stream) - .AddTo(disposables); + var stream = await file.OpenReadAsync().AsTask(ct); + disposables.Add(stream); + try + { + var szArchive = SevenZipArchive.Open(stream.AsStreamForRead()) + .AddTo(disposables); - var supportedEntries = rarArchive.Entries - .Where(x => SupportedFileTypesHelper.IsSupportedImageFileExtension(x.Key)) - .Select(x => (IImageSource)new ArchiveEntryImageSource(x, file, _recyclableMemoryStreamManager)) - .ToList(); + ct.ThrowIfCancellationRequested(); + return new ArchiveImageCollection(file, szArchive, disposables, _recyclableMemoryStreamManager, _thumbnailManager); + } + catch + { + disposables.Dispose(); + throw; + } + } - return new GetImagesFromArchiveResult() + private async Task GetImagesFromTarFileAsync(StorageFile file, CancellationToken ct) + { + CompositeDisposable disposables = new CompositeDisposable(); + var stream = await file.OpenReadAsync().AsTask(ct); + disposables.Add(stream); + + try { - ItemsCount = (uint)supportedEntries.Count, - Disposer = disposables, - Images = supportedEntries, - }; + var tarArchive = TarArchive.Open(stream.AsStreamForRead()) + .AddTo(disposables); + + ct.ThrowIfCancellationRequested(); + return new ArchiveImageCollection(file, tarArchive, disposables, _recyclableMemoryStreamManager, _thumbnailManager); + } + catch + { + disposables.Dispose(); + throw; + } + } + } + + public interface IImageCollection + { + string Name { get; } + List GetAllImages(); + } + + public interface IImageCollectionWithDirectory : IImageCollection + { + ArchiveDirectoryToken GetDirectoryTokenFromPath(string path); + IEnumerable GetDirectoryPaths(); + List GetImagesFromDirectory(ArchiveDirectoryToken token); + } + + public interface IImageCollectionDirectoryToken + { + string Key { get; } + } + public record ArchiveDirectoryToken(IArchive Archive, IArchiveEntry Entry) : IImageCollectionDirectoryToken + { + private string _key; + public string Key => _key ??= ( Entry?.Key is not null ? (Entry.IsDirectory ? Entry.Key : Path.GetDirectoryName(Entry.Key)) : null); + } + + public sealed class PdfImageCollection : IImageCollection + { + private readonly PdfDocument _pdfDocument; + private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; + private readonly ThumbnailManager _thumbnailManager; + + public PdfImageCollection(StorageFile file, PdfDocument pdfDocument, RecyclableMemoryStreamManager recyclableMemoryStreamManager, ThumbnailManager thumbnailManager) + { + _pdfDocument = pdfDocument; + File = file; + _recyclableMemoryStreamManager = recyclableMemoryStreamManager; + _thumbnailManager = thumbnailManager; } + public string Name => File.Name; + public StorageFile File { get; } - private async Task GetImagesFromSevenZipFileAsync(StorageFile file) + public List GetAllImages() { - CompositeDisposable disposables = new CompositeDisposable(); - var stream = await file.OpenStreamForReadAsync() - .AddTo(disposables); - var zipArchive = SevenZipArchive.Open(stream) - .AddTo(disposables); + return Enumerable.Range(0, (int)_pdfDocument.PageCount) + .Select(x => _pdfDocument.GetPage((uint)x)) + .Select(x => (IImageSource)new PdfPageImageSource(x, File, _recyclableMemoryStreamManager, _thumbnailManager)) + .ToList(); + } + } - var supportedEntries = zipArchive.Entries - .Where(x => SupportedFileTypesHelper.IsSupportedImageFileExtension(x.Key)) - .Select(x => (IImageSource)new ArchiveEntryImageSource(x, file, _recyclableMemoryStreamManager)) - .ToList(); + public static class DirectoryPathHelper + { + public static bool IsSameDirectoryPath(string pathA, string pathB) + { + if (pathA == pathB) { return true; } - return new GetImagesFromArchiveResult() + bool pathAEmpty = string.IsNullOrEmpty(pathA); + bool pathBEmpty = string.IsNullOrEmpty(pathB); + if (pathAEmpty && pathBEmpty) { return true; } + else if (pathAEmpty ^ pathBEmpty) { return false; } + + bool isSkipALastChar = pathA.EndsWith(Path.DirectorySeparatorChar) || pathA.EndsWith(Path.AltDirectorySeparatorChar); + bool isSkipBLastChar = pathB.EndsWith(Path.DirectorySeparatorChar) || pathB.EndsWith(Path.AltDirectorySeparatorChar); + if (isSkipALastChar && isSkipBLastChar) { - ItemsCount = (uint)supportedEntries.Count, - Disposer = disposables, - Images = supportedEntries, - }; + if (Enumerable.SequenceEqual(pathA.SkipLast(1), pathB.SkipLast(1))) { return true; } + } + else if (isSkipALastChar) + { + if (Enumerable.SequenceEqual(pathA.SkipLast(1), pathB)) { return true; } + } + else if (isSkipBLastChar) + { + if (Enumerable.SequenceEqual(pathA, pathB.SkipLast(1))) { return true; } + } + + return false; } - private async Task GetImagesFromTarFileAsync(StorageFile file) + public static bool IsSameDirectoryPath(IArchiveEntry x, IArchiveEntry y) { - CompositeDisposable disposables = new CompositeDisposable(); - var stream = await file.OpenStreamForReadAsync() - .AddTo(disposables); - var zipArchive = TarArchive.Open(stream) - .AddTo(disposables); + if (x == null && y == null) { throw new NotSupportedException(); } + + if (x == null) + { + return IsRootDirectoryEntry(y); + } + else if (y == null) + { + return IsRootDirectoryEntry(x); + } + else + { + var pathX = x.IsDirectory ? x.Key : Path.GetDirectoryName(x.Key); + var pathY = y.IsDirectory ? y.Key : Path.GetDirectoryName(y.Key); + + //ReadOnlySpan + return IsSameDirectoryPath(pathX, pathY); + } + } + + public static bool IsSameDirectoryPath(ArchiveDirectoryToken x, ArchiveDirectoryToken y) + { + if (x == null && y == null) { throw new NotSupportedException(); } + + if (x.Key == null) + { + return IsRootDirectoryEntry(y); + } + else if (x.Key == null) + { + return IsRootDirectoryEntry(x); + } + else + { + return IsSameDirectoryPath(x.Key, y.Key); + } + } + + public static bool IsChildDirectoryPath(ArchiveDirectoryToken parent, ArchiveDirectoryToken target) + { + if (parent.Key == null) + { + return IsRootDirectoryEntry(target); + } + + return IsSameDirectoryPath(parent.Key, Path.GetDirectoryName(target.Key)); + } + + public static bool IsChildDirectoryPath(string parent, string target) + { + return IsSameDirectoryPath(parent, Path.GetDirectoryName(target)); + } + + public static bool IsRootDirectoryEntry(ArchiveDirectoryToken token) + { + if (token.Key == null) { return true; } + else { return IsRootDirectoryEntry(token.Entry); } + } + + public static bool IsRootDirectoryEntry(IArchiveEntry entry) + { + //return IsRootDirectoryPath(entry.IsDirectory ? entry.Key : Path.GetDirectoryName(entry.Key)); + return IsRootDirectoryPath(Path.GetDirectoryName(entry.Key)); + } - var supportedEntries = zipArchive.Entries + public static bool IsRootDirectoryPath(string path) + { + if (path == String.Empty) + { + return true; + } + else if (path.EndsWith(Path.DirectorySeparatorChar) + || path.EndsWith(Path.AltDirectorySeparatorChar) + ) + { + return true; + } + else + { + return false; + } + } + + + + public static int GetDirectoryDepth(string path) + { + return path.Count(c => c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar); + } + + } + + public class ArchiveDirectoryEqualityComparer : IEqualityComparer + { + public static readonly ArchiveDirectoryEqualityComparer Default = new ArchiveDirectoryEqualityComparer(); + private ArchiveDirectoryEqualityComparer() { } + + public bool Equals(IArchiveEntry x, IArchiveEntry y) + { + return DirectoryPathHelper.IsSameDirectoryPath(x, y); + } + + public int GetHashCode(IArchiveEntry obj) + { + var pathX = obj.IsDirectory ? obj.Key : Path.GetDirectoryName(obj.Key); + if (pathX.EndsWith(Path.DirectorySeparatorChar)) + { + return pathX.GetHashCode(); + } + else if (pathX.EndsWith(Path.AltDirectorySeparatorChar)) + { + return (pathX.Remove(pathX.Length - 1) + Path.DirectorySeparatorChar).GetHashCode(); + } + else + { + return (pathX + Path.DirectorySeparatorChar).GetHashCode(); + } + } + } + + public sealed class ArchiveImageCollection : IImageCollectionWithDirectory, IDisposable + { + + public StorageFile File { get; } + public IArchive Archive { get; } + + private readonly CompositeDisposable _disposables; + private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; + private readonly ThumbnailManager _thumbnailManager; + private readonly ImmutableList _directories; + + private readonly Dictionary> _entriesCacheByDirectory = new (); + public ArchiveImageCollection(StorageFile file, IArchive archive, CompositeDisposable disposables, RecyclableMemoryStreamManager recyclableMemoryStreamManager, ThumbnailManager thumbnailManager) + { + File = file; + Archive = archive; + _disposables = disposables; + _recyclableMemoryStreamManager = recyclableMemoryStreamManager; + _thumbnailManager = thumbnailManager; + _rootDirectoryToken = new ArchiveDirectoryToken(Archive, null); + + // ディレクトリベースでフォルダ構造を見つける + + var dir = Enumerable.Concat( + Archive.Entries.Where(x => x.IsDirectory), + Archive.Entries.Where(x => !x.IsDirectory) + .Where(x => DirectoryPathHelper.GetDirectoryDepth(x.Key) >= 1 && SupportedFileTypesHelper.IsSupportedImageFileExtension(x.Key)) + ) + .Distinct(ArchiveDirectoryEqualityComparer.Default); + + // もしディレクトリベースのフォルダ構造が無い場合はファイル構造から見つける + _directories = dir.Select(x => new ArchiveDirectoryToken(Archive, x)).OrderBy(x => x.Key).ToImmutableList(); + if (_rootDirectoryToken == null || + (_directories.Count == 1 && DirectoryPathHelper.IsRootDirectoryEntry(_directories[0].Entry)) + ) + { + _rootDirectoryToken = new ArchiveDirectoryToken(Archive, null); + } + } + + + + private readonly ArchiveDirectoryToken _rootDirectoryToken; + + public string Name => File.Name; + + public ArchiveDirectoryToken GetDirectoryTokenFromPath(string path) + { + if (string.IsNullOrEmpty(path)) { return _rootDirectoryToken; } + + return _directories.FirstOrDefault(x => x.Key == path); + } + + + public IEnumerable GetSubDirectories(ArchiveDirectoryToken token) + { + token ??= _rootDirectoryToken; + return _directories + .Where(x => token != x) + .Where(x => DirectoryPathHelper.IsChildDirectoryPath(token, x)); + } + + public IEnumerable GetLeafFolders() + { + return _directories.Where(x => !GetSubDirectories(x).Any()); + } + + public IEnumerable GetDirectoryPaths() + { + return _directories; + } + + public IImageSource GetThumbnailImageFromDirectory(ArchiveDirectoryToken token) + { + return GetImagesFromDirectory(token).FirstOrDefault(); + } + + public List GetImagesFromDirectory(ArchiveDirectoryToken token) + { + token ??= _rootDirectoryToken; + + if (_entriesCacheByDirectory.TryGetValue(token, out var entries)) { return entries; } + if (token != _rootDirectoryToken && _directories.Contains(token) is false) { throw new InvalidOperationException(); } + + var imageSourceItems = (token?.Key is null + ? Archive.Entries.Where(x => DirectoryPathHelper.IsRootDirectoryEntry(x)) + : Archive.Entries.Where(x => DirectoryPathHelper.IsSameDirectoryPath(x, token.Entry)) + ) .Where(x => SupportedFileTypesHelper.IsSupportedImageFileExtension(x.Key)) - .Select(x => (IImageSource)new ArchiveEntryImageSource(x, file, _recyclableMemoryStreamManager)) + .Select(x => (IImageSource)new ArchiveEntryImageSource(x, token, this, _recyclableMemoryStreamManager, _thumbnailManager)) .ToList(); - return new GetImagesFromArchiveResult() - { - ItemsCount = (uint)supportedEntries.Count, - Disposer = disposables, - Images = supportedEntries, - }; + _entriesCacheByDirectory.Add(token, imageSourceItems); + return imageSourceItems; + } + + + public void Dispose() + { + ((IDisposable)_disposables).Dispose(); } + public List GetAllImages() + { + if (_directories.Count == 0) + { + return GetImagesFromDirectory(_rootDirectoryToken); + } + /* + else if (_directories.Count == 1 && IsRootDirectoryEntry(_directories[0].Entry)) + { + return GetImagesFromDirectory(_rootDirectoryToken); + } + */ + else + { + return _directories.SelectMany(x => GetImagesFromDirectory(x)).ToList(); + } + } } + } diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/ArchiveDirectoryImageSource.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/ArchiveDirectoryImageSource.cs new file mode 100644 index 00000000..df3a5a0c --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/ArchiveDirectoryImageSource.cs @@ -0,0 +1,122 @@ +using Microsoft.IO; +using Microsoft.Toolkit.Diagnostics; +using SharpCompress.Archives; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using TsubameViewer.Models.Domain.FolderItemListing; +using Windows.Storage; +using Windows.Storage.Streams; + +namespace TsubameViewer.Models.Domain.ImageViewer.ImageSource +{ + public sealed class ArchiveDirectoryImageSource : IImageSource + { + private readonly ArchiveImageCollection _imageCollection; + private readonly ArchiveDirectoryToken _directoryToken; + private readonly ThumbnailManager _thumbnailManager; + + public ArchiveDirectoryImageSource(ArchiveImageCollection archiveImageCollection, ArchiveDirectoryToken directoryToken, ThumbnailManager thumbnailManager) + { + Guard.IsNotNull(directoryToken.Key, nameof(directoryToken.Key)); + _imageCollection = archiveImageCollection; + _directoryToken = directoryToken; + _thumbnailManager = thumbnailManager; + Name = _directoryToken.Key is not null ? new string(_directoryToken.Key.Reverse().TakeWhile(c => c != System.IO.Path.DirectorySeparatorChar).Reverse().ToArray()) : _imageCollection.Name; + } + + public IArchiveEntry ArchiveEntry => _directoryToken?.Entry; + + public StorageFile StorageItem => _imageCollection.File; + + IStorageItem IImageSource.StorageItem => _imageCollection.File; + + public string Name { get; } + + public string Path => _directoryToken?.Key; + + public DateTime DateCreated => _imageCollection.File.DateCreated.DateTime; + + public async Task GetImageStreamAsync(CancellationToken ct = default) + { + using var mylock = await ArchiveEntryImageSource.ArchiveEntryAccessLock.LockAsync(ct); + + var imageSource = GetNearestImageFromDirectory(_directoryToken); + if (imageSource == null) { return null; } + + return await imageSource.GetImageStreamAsync(ct); + } + + public async Task GetThumbnailImageStreamAsync(CancellationToken ct = default) + { + using var mylock = await ArchiveEntryImageSource.ArchiveEntryAccessLock.LockAsync(ct); + + var file = await _thumbnailManager.GetArchiveEntryThumbnailImageAsync(StorageItem, ArchiveEntry, ct); + if (file is null) + { + var imageSource = GetNearestImageFromDirectory(_directoryToken); + if (imageSource == null) { return null; } + + var stream = await imageSource.GetThumbnailImageStreamAsync(ct); + { + file = await _thumbnailManager.SetArchiveEntryThumbnailAsync(StorageItem, ArchiveEntry, stream, ct); + } + + stream.Seek(0); + return stream; + } + + if (file == null) { return null; } + + var fileStream = await file.OpenStreamForReadAsync(); + return fileStream.AsRandomAccessStream(); + } + + private IImageSource GetNearestImageFromDirectory(ArchiveDirectoryToken firstToken) + { + Stack archiveDirectoryTokens = new Stack(); + archiveDirectoryTokens.Push(firstToken); + IImageSource imageSource = null; + while (archiveDirectoryTokens.TryPop(out var token)) + { + imageSource = _imageCollection.GetThumbnailImageFromDirectory(token); + if (imageSource != null) { break; } + else + { + foreach (var subDir in _imageCollection.GetSubDirectories(token).Reverse()) + { + archiveDirectoryTokens.Push(subDir); + } + } + } + + return imageSource; + } + + public bool IsContainsImage() + { + return _imageCollection.GetImagesFromDirectory(_directoryToken).Any(); + } + + public bool IsContainsSubDirectory() + { + return _imageCollection.GetSubDirectories(_directoryToken).Any(); + } + + public IArchiveEntry GetParentDirectoryEntry() + { + var entry = _imageCollection.GetDirectoryTokenFromPath(System.IO.Path.GetDirectoryName(_directoryToken?.Key))?.Entry; + if (entry == null + || !(entry.Key.Contains(System.IO.Path.DirectorySeparatorChar) || entry.Key.Contains(System.IO.Path.AltDirectorySeparatorChar)) + ) + { + return null; + } + else { return entry; } + } + } +} diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/ArchiveEntryImageSource.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/ArchiveEntryImageSource.cs index ff8ab2ef..074065c0 100644 --- a/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/ArchiveEntryImageSource.cs +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/ArchiveEntryImageSource.cs @@ -14,84 +14,84 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using TsubameViewer.Models.Domain.FolderItemListing; using Uno.Extensions; +using Uno.Threading; using Windows.Storage; +using Windows.Storage.Streams; using Windows.UI.Xaml.Media.Imaging; namespace TsubameViewer.Models.Domain.ImageViewer.ImageSource { - public sealed class ArchiveEntryImageSource : IImageSource, IDisposable + public sealed class ArchiveEntryImageSource : IImageSource { private readonly IArchiveEntry _entry; + private readonly ArchiveDirectoryToken _archiveDirectoryToken; + private readonly ArchiveImageCollection _archiveImageCollection; private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; + private readonly ThumbnailManager _thumbnailManager; - public ArchiveEntryImageSource(IArchiveEntry entry, IStorageItem storageItem, RecyclableMemoryStreamManager recyclableMemoryStreamManager) + public ArchiveEntryImageSource(IArchiveEntry entry, ArchiveDirectoryToken archiveDirectoryToken, ArchiveImageCollection archiveImageCollection, RecyclableMemoryStreamManager recyclableMemoryStreamManager, ThumbnailManager thumbnailManager) { _entry = entry; - StorageItem = storageItem; + _archiveDirectoryToken = archiveDirectoryToken; + _archiveImageCollection = archiveImageCollection; + StorageItem = _archiveImageCollection.File; _recyclableMemoryStreamManager = recyclableMemoryStreamManager; + _thumbnailManager = thumbnailManager; DateCreated = entry.CreatedTime ?? entry.LastModifiedTime ?? entry.ArchivedTime ?? DateTime.Now; } - public IStorageItem StorageItem { get; } + public StorageFile StorageItem { get; } + IStorageItem IImageSource.StorageItem => StorageItem; - public string Name => _entry.Key; + private string _name; + public string Name => _name ??= System.IO.Path.GetFileName(_entry.Key); + + public string Path => _entry.Key; public DateTime DateCreated { get; } - CancellationTokenSource _cts = new CancellationTokenSource(); - - public async Task GenerateBitmapImageAsync(CancellationToken ct) + public async Task GetImageStreamAsync(CancellationToken ct) { + using var mylock = await ArchiveEntryAccessLock.LockAsync(ct); + + var memoryStream = _recyclableMemoryStreamManager.GetStream(); using (var entryStream = _entry.OpenEntryStream()) - using (var memoryStream = _recyclableMemoryStreamManager.GetStream()) { entryStream.CopyTo(memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); ct.ThrowIfCancellationRequested(); - - var bitmapImage = new BitmapImage(); - await bitmapImage.SetSourceAsync(memoryStream.AsRandomAccessStream()).AsTask(ct); - - ct.ThrowIfCancellationRequested(); - - return bitmapImage; } - } - public void CancelLoading() - { - _cts?.Cancel(); - _cts?.Dispose(); + return memoryStream.AsRandomAccessStream(); } - public void Dispose() - { - ((IDisposable)_cts).Dispose(); - } + internal static FastAsyncLock ArchiveEntryAccessLock = new FastAsyncLock(); - public async Task GenerateThumbnailBitmapImageAsync(CancellationToken ct = default) + public async Task GetThumbnailImageStreamAsync(CancellationToken ct) { - using (var entryStream = _entry.OpenEntryStream()) - using (var memoryStream = _recyclableMemoryStreamManager.GetStream()) - { - entryStream.CopyTo(memoryStream); - memoryStream.Seek(0, SeekOrigin.Begin); - - ct.ThrowIfCancellationRequested(); - - var bitmapImage = new BitmapImage(); - bitmapImage.DecodePixelWidth = FolderItemListing.ListingImageConstants.MidiumFileThumbnailImageWidth; - await bitmapImage.SetSourceAsync(memoryStream.AsRandomAccessStream()).AsTask(ct); + using var mylock = await ArchiveEntryAccessLock.LockAsync(ct); - ct.ThrowIfCancellationRequested(); + var thumbnailFile = await _thumbnailManager.GetArchiveEntryThumbnailImageAsync(StorageItem, _entry, ct); + var stream = await thumbnailFile.OpenStreamForReadAsync(); + return stream.AsRandomAccessStream(); + } - return bitmapImage; + public IArchiveEntry GetParentDirectoryEntry() + { + if (_archiveDirectoryToken.Key == null + || !(_archiveDirectoryToken.Key.Contains(System.IO.Path.DirectorySeparatorChar) || _archiveDirectoryToken.Entry.Key.Contains(System.IO.Path.AltDirectorySeparatorChar)) + ) + { + return null; } + + return _archiveDirectoryToken.Entry; } } } diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/PdfPageImageSource.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/PdfPageImageSource.cs index 9ea7a872..245d370d 100644 --- a/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/PdfPageImageSource.cs +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/PdfPageImageSource.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using TsubameViewer.Models.Domain.FolderItemListing; using Uno.Disposables; using Windows.Data.Pdf; using Windows.Storage; @@ -20,46 +21,39 @@ public sealed class PdfPageImageSource : IImageSource, IDisposable { private readonly PdfPage _pdfPage; private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; + private readonly ThumbnailManager _thumbnailManager; - public PdfPageImageSource(PdfPage pdfPage, IStorageItem storageItem, RecyclableMemoryStreamManager recyclableMemoryStreamManager) + public PdfPageImageSource(PdfPage pdfPage, StorageFile storageItem, RecyclableMemoryStreamManager recyclableMemoryStreamManager, ThumbnailManager thumbnailManager) { _pdfPage = pdfPage; Name = (_pdfPage.Index + 1).ToString(); DateCreated = storageItem.DateCreated.DateTime; StorageItem = storageItem; _recyclableMemoryStreamManager = recyclableMemoryStreamManager; + _thumbnailManager = thumbnailManager; } public string Name { get; } - public DateTime DateCreated { get; } - public IStorageItem StorageItem { get; } - - public async Task GenerateBitmapImageAsync(CancellationToken ct = default) - { - using (var memoryStream = _recyclableMemoryStreamManager.GetStream()) - using (var streamWrite = new StreamWriter(memoryStream)) - { - await _pdfPage.RenderToStreamAsync(memoryStream.AsRandomAccessStream()).AsTask(ct); - ct.ThrowIfCancellationRequested(); - - await memoryStream.FlushAsync(); - memoryStream.Seek(0, SeekOrigin.Begin); + public string Path => Name; + public DateTime DateCreated { get; } + public StorageFile StorageItem { get; } - ct.ThrowIfCancellationRequested(); + IStorageItem IImageSource.StorageItem => StorageItem; - var bitmapImage = new BitmapImage(); - bitmapImage.SetSource(memoryStream.AsRandomAccessStream()); - return bitmapImage; - } + public async Task GetThumbnailImageStreamAsync(CancellationToken ct) + { + var thumbnailFile = await _thumbnailManager.GetPdfPageThumbnailImageAsync(StorageItem, _pdfPage, ct); + var stream = await thumbnailFile.OpenStreamForReadAsync(); + return stream.AsRandomAccessStream(); } - public async Task GenerateThumbnailBitmapImageAsync(CancellationToken ct = default) + public async Task GetImageStreamAsync(CancellationToken ct) { - using (var memoryStream = _recyclableMemoryStreamManager.GetStream()) - using (var streamWrite = new StreamWriter(memoryStream)) + var memoryStream = _recyclableMemoryStreamManager.GetStream(); + var stream = memoryStream.AsRandomAccessStream(); { - await _pdfPage.RenderToStreamAsync(memoryStream.AsRandomAccessStream()).AsTask(ct); + await _pdfPage.RenderToStreamAsync(stream).AsTask(ct); ct.ThrowIfCancellationRequested(); @@ -67,14 +61,9 @@ public async Task GenerateThumbnailBitmapImageAsync(CancellationTok memoryStream.Seek(0, SeekOrigin.Begin); ct.ThrowIfCancellationRequested(); - - var bitmapImage = new BitmapImage(); - bitmapImage.SetSource(memoryStream.AsRandomAccessStream()); - - bitmapImage.DecodePixelWidth = FolderItemListing.ListingImageConstants.MidiumFileThumbnailImageWidth; - - return bitmapImage; } + + return stream; } public void Dispose() diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/StorageItemImageSource.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/StorageItemImageSource.cs index 1a59618a..ce6cf0be 100644 --- a/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/StorageItemImageSource.cs +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/ImageViewer/ImageSource/StorageItemImageSource.cs @@ -9,11 +9,12 @@ using TsubameViewer.Models.Domain.FolderItemListing; using Windows.Storage; using Windows.Storage.Search; +using Windows.Storage.Streams; using Windows.UI.Xaml.Media.Imaging; namespace TsubameViewer.Models.Domain.ImageViewer.ImageSource { - public sealed class StorageItemImageSource : IImageSource, IDisposable + public sealed class StorageItemImageSource : IImageSource { private readonly ThumbnailManager _thumbnailManager; @@ -40,24 +41,12 @@ public StorageItemImageSource(IStorageItem storageItem, ThumbnailManager thumbna ItemTypes = SupportedFileTypesHelper.StorageItemToStorageItemTypes(StorageItem); } - public void Dispose() - { - } - - public async Task GenerateBitmapImageAsync(CancellationToken ct) + public async Task GetImageStreamAsync(CancellationToken ct) { if (StorageItem is StorageFile file && SupportedFileTypesHelper.IsSupportedImageFileExtension(file.FileType)) { - using (var stream = await file.OpenReadAsync().AsTask(ct)) - { - var bitmapImage = new BitmapImage(); - await bitmapImage.SetSourceAsync(stream).AsTask(ct); - - ct.ThrowIfCancellationRequested(); - - return bitmapImage; - } + return await file.OpenReadAsync().AsTask(ct); } else { @@ -65,51 +54,39 @@ public async Task GenerateBitmapImageAsync(CancellationToken ct) } } - public async Task GenerateThumbnailBitmapImageAsync(CancellationToken ct) + public async Task GetThumbnailImageStreamAsync(CancellationToken ct) { if (StorageItem is StorageFile file) { if (SupportedFileTypesHelper.IsSupportedImageFileExtension(file.FileType)) { - //var image = new BitmapImage(new Uri(file.Path, UriKind.Absolute)); - using (var thumbImage = await file.GetScaledImageAsThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode.SingleItem, (uint)ListingImageConstants.LargeFileThumbnailImageHeight)) - { - var image = new BitmapImage(); - image.SetSource(thumbImage); - return image; - } + var thumbnailFile = await _thumbnailManager.GetFileThumbnailImageAsync(file, ct); + if (thumbnailFile == null) { return null; } + return await thumbnailFile.OpenReadAsync().AsTask(ct); } else if (SupportedFileTypesHelper.IsSupportedArchiveFileExtension(file.FileType) || SupportedFileTypesHelper.IsSupportedEBookFileExtension(file.FileType) ) { - var thumbnailFile = await _thumbnailManager.GetFileThumbnailImageAsync(file); + var thumbnailFile = await _thumbnailManager.GetFileThumbnailImageAsync(file, ct); if (thumbnailFile == null) { return null; } - var image = new BitmapImage(); - - using (var stream = await thumbnailFile.OpenStreamForReadAsync()) - { - image.SetSource(stream.AsRandomAccessStream()); - } - - return image; + return await thumbnailFile.OpenReadAsync().AsTask(ct); + } + else + { + throw new NotSupportedException(); } } else if (StorageItem is StorageFolder folder) { - var uri = await _thumbnailManager.GetFolderThumbnailAsync(folder); - if (uri == null) { return null; } - var image = new BitmapImage(uri); - return image; + var thumbnailFile = await _thumbnailManager.GetFolderThumbnailAsync(folder, ct); + if (thumbnailFile == null) { return null; } + return await thumbnailFile.OpenReadAsync().AsTask(ct); + } + else + { + throw new NotSupportedException(); } - - return new BitmapImage(); - } - - public void CancelLoading() - { - } - } } diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/StorageItemTypes.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/StorageItemTypes.cs index 03483ca5..6b85fa0f 100644 --- a/TsubameViewer/TsubameViewer.Shared/Models.Domain/StorageItemTypes.cs +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/StorageItemTypes.cs @@ -10,6 +10,7 @@ public enum StorageItemTypes Folder, Image, Archive, + ArchiveFolder, EBook, } } diff --git a/TsubameViewer/TsubameViewer.Shared/Models.Domain/SupportedFileTypesHelper.cs b/TsubameViewer/TsubameViewer.Shared/Models.Domain/SupportedFileTypesHelper.cs index 5dbbc35f..3ce6f292 100644 --- a/TsubameViewer/TsubameViewer.Shared/Models.Domain/SupportedFileTypesHelper.cs +++ b/TsubameViewer/TsubameViewer.Shared/Models.Domain/SupportedFileTypesHelper.cs @@ -30,6 +30,7 @@ static SupportedFileTypesHelper() { JpgFileType, JpegFileType, + JfifFileType, PngFileType, BmpFileType, GifFileType, @@ -60,6 +61,7 @@ static SupportedFileTypesHelper() public const string JpgFileType = ".jpg"; public const string JpegFileType = ".jpeg"; + public const string JfifFileType = ".jfif"; public const string PngFileType = ".png"; public const string BmpFileType = ".bmp"; public const string GifFileType = ".gif"; @@ -153,6 +155,7 @@ public static StorageItemTypes StorageItemToStorageItemTypes(IImageSource item) StorageItemImageSource storageItem => storageItem.ItemTypes, PdfPageImageSource _ => StorageItemTypes.Image, ArchiveEntryImageSource _ => StorageItemTypes.Image, + ArchiveDirectoryImageSource _ => StorageItemTypes.ArchiveFolder, _ => StorageItemTypes.None }; } diff --git a/TsubameViewer/TsubameViewer.Shared/Models.UseCase/ApplicationDataUpdateWhenPathReferenceCountChanged.cs b/TsubameViewer/TsubameViewer.Shared/Models.UseCase/ApplicationDataUpdateWhenPathReferenceCountChanged.cs index f6c98774..ed8a484d 100644 --- a/TsubameViewer/TsubameViewer.Shared/Models.UseCase/ApplicationDataUpdateWhenPathReferenceCountChanged.cs +++ b/TsubameViewer/TsubameViewer.Shared/Models.UseCase/ApplicationDataUpdateWhenPathReferenceCountChanged.cs @@ -23,6 +23,7 @@ public sealed class ApplicationDataUpdateWhenPathReferenceCountChanged private readonly FolderContainerTypeManager _folderContainerTypeManager; private readonly ThumbnailManager _thumbnailManager; private readonly FolderLastIntractItemManager _folderLastIntractItemManager; + private readonly DisplaySettingsByPathRepository _displaySettingsByPathRepository; CompositeDisposable _disposables = new CompositeDisposable(); public ApplicationDataUpdateWhenPathReferenceCountChanged( @@ -33,7 +34,8 @@ public ApplicationDataUpdateWhenPathReferenceCountChanged( FolderContainerTypeManager folderContainerTypeManager, ThumbnailManager thumbnailManager, SecondaryTileManager secondaryTileManager, - FolderLastIntractItemManager folderLastIntractItemManager + FolderLastIntractItemManager folderLastIntractItemManager, + DisplaySettingsByPathRepository displaySettingsByPathRepository ) { _eventAggregator = eventAggregator; @@ -43,6 +45,7 @@ FolderLastIntractItemManager folderLastIntractItemManager _folderContainerTypeManager = folderContainerTypeManager; _thumbnailManager = thumbnailManager; _folderLastIntractItemManager = folderLastIntractItemManager; + _displaySettingsByPathRepository = displaySettingsByPathRepository; _eventAggregator.GetEvent() .Subscribe(args => { @@ -61,6 +64,7 @@ FolderLastIntractItemManager folderLastIntractItemManager _folderLastIntractItemManager.Remove(args.Path); await _thumbnailManager.DeleteFromPath(args.Path); await secondaryTileManager.RemoveSecondaryTile(args.Path); + _displaySettingsByPathRepository.DeleteUnderPath(args.Path); } , keepSubscriberReferenceAlive: true ) diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.Services/UWP/ApplicationLifecycle.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.Services/UWP/ApplicationLifecycle.cs new file mode 100644 index 00000000..a6dd25c1 --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.Services/UWP/ApplicationLifecycle.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Text; +using Windows.UI.Core; +using Windows.UI.Xaml; + +namespace TsubameViewer.Presentation.Services.UWP +{ + public static class ApplicationLifecycleObservable + { + public static IObservable VisibilityChanged() + { + return Observable.FromEventPattern(h => Window.Current.VisibilityChanged += h, h => Window.Current.VisibilityChanged -= h) + .Select(args => args.EventArgs.Visible); + } + + public static IObservable WindowActivationStateChanged() + { + return Observable.FromEventPattern(h => Window.Current.Activated += h, h => Window.Current.Activated -= h) + .Select(args => args.EventArgs.WindowActivationState != CoreWindowActivationState.Deactivated) + .DistinctUntilChanged(); + } + } +} diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/EBookReaderPageViewModel.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/EBookReaderPageViewModel.cs index 34f51b4b..750dc7ef 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/EBookReaderPageViewModel.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/EBookReaderPageViewModel.cs @@ -569,7 +569,7 @@ private async Task RefreshItems(CancellationToken ct) SelectedTocItem = TocItems.FirstOrDefault(); - var thumbnailFile = await _thumbnailManager.GetFileThumbnailImageAsync(_currentFolderItem); + var thumbnailFile = await _thumbnailManager.GetFileThumbnailImageAsync(_currentFolderItem, ct); if (thumbnailFile != null) { CoverImage = new BitmapImage(new Uri(thumbnailFile.Path)); diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/FolderListupPageViewModel.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/FolderListupPageViewModel.cs index 7b8e2bc1..be007c0d 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/FolderListupPageViewModel.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/FolderListupPageViewModel.cs @@ -33,9 +33,13 @@ using Windows.Storage.Search; using Windows.UI.Xaml.Media.Animation; using TsubameViewer.Models.Domain.ImageViewer.ImageSource; +using TsubameViewer.Presentation.ViewModels.SourceFolders.Commands; +using System.Collections; +using System.Reactive.Concurrency; namespace TsubameViewer.Presentation.ViewModels { + using static TsubameViewer.Models.Domain.ImageViewer.ImageCollectionManager; using StorageItemTypes = TsubameViewer.Models.Domain.StorageItemTypes; @@ -56,12 +60,6 @@ public void DisposeItems() public sealed class FolderListupPageViewModel : ViewModelBase { - const int FolderListupItemsCacheCount = 200; - static List _CacheFolderListupItemsOrder = new List(); - - static Dictionary _CachedFolderListupItems = new Dictionary(); - - private bool _NowProcessing; public bool NowProcessing { @@ -69,6 +67,7 @@ public bool NowProcessing set { SetProperty(ref _NowProcessing, value); } } + private readonly IScheduler _scheduler; private readonly BookmarkManager _bookmarkManager; private readonly ImageCollectionManager _imageCollectionManager; private readonly SourceStorageItemsRepository _sourceStorageItemsRepository; @@ -76,6 +75,7 @@ public bool NowProcessing private readonly FolderLastIntractItemManager _folderLastIntractItemManager; private readonly ThumbnailManager _thumbnailManager; private readonly FolderListingSettings _folderListingSettings; + private readonly DisplaySettingsByPathRepository _displaySettingsByPathRepository; public SecondaryTileManager SecondaryTileManager { get; } public OpenPageCommand OpenPageCommand { get; } @@ -89,8 +89,17 @@ public bool NowProcessing public OpenWithExplorerCommand OpenWithExplorerCommand { get; } public SecondaryTileAddCommand SecondaryTileAddCommand { get; } public SecondaryTileRemoveCommand SecondaryTileRemoveCommand { get; } + public ChangeStorageItemThumbnailImageCommand ChangeStorageItemThumbnailImageCommand { get; } + public OpenWithExternalApplicationCommand OpenWithExternalApplicationCommand { get; } public ObservableCollection FolderItems { get; private set; } + private AdvancedCollectionView _FileItemsView; + public AdvancedCollectionView FileItemsView + { + get { return _FileItemsView; } + set { SetProperty(ref _FileItemsView, value); } + } + private bool _HasFileItem; public bool HasFileItem { @@ -99,6 +108,13 @@ public bool HasFileItem } + public ReactivePropertySlim SelectedFileSortType { get; } + public ReactivePropertySlim SelectedChildFileSortType { get; } + + public ReactivePropertySlim IsSortWithTitleDigitCompletion { get; } + + + public ReactivePropertySlim FolderLastIntractItem { get; } static FastAsyncLock _NavigationLock = new FastAsyncLock(); @@ -124,8 +140,9 @@ public StorageItemViewModel CurrentFolderItem set { SetProperty(ref _CurrentFolderItem, value); } } + IDisposable _ImageCollectionDisposer; - string _currentItemRootFolderToken; + StorageItemToken _currentItemRootFolderToken; public string FoldersManagementPageName => nameof(Views.SourceStorageItemsPage); @@ -134,7 +151,21 @@ public StorageItemViewModel CurrentFolderItem static bool _LastIsArchiveFileThumbnailEnabled; static bool _LastIsFolderThumbnailEnabled; + private string _currentArchiveFolderName; + + private string _DisplayCurrentArchiveFolderName; + public string DisplayCurrentArchiveFolderName + { + get { return _DisplayCurrentArchiveFolderName; } + private set { SetProperty(ref _DisplayCurrentArchiveFolderName, value); } + } + + CompositeDisposable _disposables = new CompositeDisposable(); + CompositeDisposable _navigationDisposables; + + public FolderListupPageViewModel( + IScheduler scheduler, BookmarkManager bookmarkManager, ImageCollectionManager imageCollectionManager, SourceStorageItemsRepository sourceStorageItemsRepository, @@ -143,6 +174,7 @@ public FolderListupPageViewModel( FolderLastIntractItemManager folderLastIntractItemManager, ThumbnailManager thumbnailManager, FolderListingSettings folderListingSettings, + DisplaySettingsByPathRepository displaySettingsByPathRepository, OpenPageCommand openPageCommand, OpenListupCommand openListupCommand, OpenFolderItemCommand openFolderItemCommand, @@ -152,9 +184,12 @@ public FolderListupPageViewModel( OpenImageListupCommand openImageListupCommand, OpenWithExplorerCommand openWithExplorerCommand, SecondaryTileAddCommand secondaryTileAddCommand, - SecondaryTileRemoveCommand secondaryTileRemoveCommand + SecondaryTileRemoveCommand secondaryTileRemoveCommand, + ChangeStorageItemThumbnailImageCommand changeStorageItemThumbnailImageCommand, + OpenWithExternalApplicationCommand openWithExternalApplicationCommand ) { + _scheduler = scheduler; _bookmarkManager = bookmarkManager; _imageCollectionManager = imageCollectionManager; _sourceStorageItemsRepository = sourceStorageItemsRepository; @@ -163,6 +198,7 @@ SecondaryTileRemoveCommand secondaryTileRemoveCommand _folderLastIntractItemManager = folderLastIntractItemManager; _thumbnailManager = thumbnailManager; _folderListingSettings = folderListingSettings; + _displaySettingsByPathRepository = displaySettingsByPathRepository; OpenPageCommand = openPageCommand; OpenListupCommand = openListupCommand; OpenFolderItemCommand = openFolderItemCommand; @@ -173,24 +209,20 @@ SecondaryTileRemoveCommand secondaryTileRemoveCommand OpenWithExplorerCommand = openWithExplorerCommand; SecondaryTileAddCommand = secondaryTileAddCommand; SecondaryTileRemoveCommand = secondaryTileRemoveCommand; + ChangeStorageItemThumbnailImageCommand = changeStorageItemThumbnailImageCommand; + OpenWithExternalApplicationCommand = openWithExternalApplicationCommand; FolderItems = new ObservableCollection(); + FileItemsView = new AdvancedCollectionView(FolderItems); + FolderLastIntractItem = new ReactivePropertySlim() + .AddTo(_disposables); - FolderLastIntractItem = new ReactivePropertySlim(); - /* - _currentQueryOptions = Observable.CombineLatest( - SelectedFolderViewFirstSort, - (queryType, sort) => (queryType, sort) - ) - .Select(_ => - { - var options = new QueryOptions(); - options.FolderDepth = FolderDepth.Shallow; - options.SetPropertyPrefetch(Windows.Storage.FileProperties.PropertyPrefetchOptions.ImageProperties, Enumerable.Empty()); - return options; - }) - .ToReadOnlyReactivePropertySlim(); - */ + SelectedFileSortType = new ReactivePropertySlim(FileSortType.UpdateTimeDescThenTitleAsc) + .AddTo(_disposables); + IsSortWithTitleDigitCompletion = new ReactivePropertySlim(true) + .AddTo(_disposables); + SelectedChildFileSortType = new ReactivePropertySlim(null) + .AddTo(_disposables); } @@ -202,7 +234,7 @@ public override async void OnNavigatedFrom(INavigationParameters parameters) _leavePageCancellationTokenSource?.Dispose(); _leavePageCancellationTokenSource = null; - FolderItems.Reverse().ForEach(x => x.StopImageLoading()); + FolderItems.AsParallel().ForEach(x => x.Dispose()); _LastIsImageFileThumbnailEnabled = _folderListingSettings.IsImageFileThumbnailEnabled; _LastIsArchiveFileThumbnailEnabled = _folderListingSettings.IsArchiveFileThumbnailEnabled; @@ -213,26 +245,13 @@ public override async void OnNavigatedFrom(INavigationParameters parameters) _folderLastIntractItemManager.SetLastIntractItemName(_currentPath, Uri.UnescapeDataString(path)); } + FolderItems.Clear(); - // - _CachedFolderListupItems.Add(_currentPath, new CachedFolderListupItems() - { - FolderItems = FolderItems, - }); - - _CacheFolderListupItemsOrder.Remove(_currentPath); - _CacheFolderListupItemsOrder.Add(_currentPath); - - while (_CachedFolderListupItems.Select(x => x.Value.GetTotalCount()).Sum() > FolderListupItemsCacheCount) - { - var item = _CacheFolderListupItemsOrder.First(); - if (_CachedFolderListupItems.Remove(item, out var cachedItems)) - { - cachedItems.DisposeItems(); - } - _CacheFolderListupItemsOrder.Remove(item); - } + _ImageCollectionDisposer?.Dispose(); + _ImageCollectionDisposer = null; + _navigationDisposables?.Dispose(); + _navigationDisposables = null; base.OnNavigatedFrom(parameters); } } @@ -242,19 +261,16 @@ public override void OnNavigatingTo(INavigationParameters parameters) PrimaryWindowCoreLayout.SetCurrentNavigationParameters(parameters); base.OnNavigatingTo(parameters); - } + } public override async Task OnNavigatedToAsync(INavigationParameters parameters) { + _navigationDisposables = new CompositeDisposable(); + NowProcessing = true; try { - // Note: ファイル表示用のItemsRepeaterのItemTemplateが - // VisualStateによって変更されるのを待つ - await Task.Delay(50); - var mode = parameters.GetNavigationMode(); - if (mode == NavigationMode.Refresh) { parameters = PrimaryWindowCoreLayout.GetCurrentNavigationParameter(); @@ -262,6 +278,11 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) _leavePageCancellationTokenSource = new CancellationTokenSource(); + _currentArchiveFolderName = parameters.TryGetValue(PageNavigationConstants.ArchiveFolderName, out string archiveFolderName) + ? Uri.UnescapeDataString(archiveFolderName) + : null + ; + if (mode == NavigationMode.New || mode == NavigationMode.Forward || mode == NavigationMode.Back @@ -308,29 +329,40 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) } } - _currentItemRootFolderToken = token; + _currentItemRootFolderToken = new StorageItemToken(_currentPath, token); var currentPathItem = await _sourceStorageItemsRepository.GetStorageItemFromPath(token, _currentPath); _currentItem = currentPathItem; DisplayCurrentPath = _currentItem.Path; - CurrentFolderItem = new StorageItemViewModel(new StorageItemImageSource(_currentItem, _thumbnailManager), token, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); - } - if (_CachedFolderListupItems.Remove(_currentPath, out var cachedItems)) - { - FolderItems = cachedItems.FolderItems; - - // 最後に読んだ位置を更新 - FolderItems.ForEach(x => x.UpdateLastReadPosition()); + var settingPath = _currentPath; + var settings = _displaySettingsByPathRepository.GetFolderAndArchiveSettings(settingPath); + if (settings != null) + { + SelectedFileSortType.Value = settings.Sort; + IsSortWithTitleDigitCompletion.Value = settings.IsTitleDigitInterpolation; + SetSortAsyncUnsafe(SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value); + } + else + { + if (_currentItem is StorageFolder) + { + SelectedFileSortType.Value = FileSortType.TitleAscending; + IsSortWithTitleDigitCompletion.Value = false; + SetSortAsyncUnsafe(SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value); + } + else if (_currentItem is StorageFile file && file.IsSupportedMangaFile()) + { + SelectedFileSortType.Value = FileSortType.UpdateTimeDescThenTitleAsc; + IsSortWithTitleDigitCompletion.Value = false; + SetSortAsyncUnsafe(SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value); + } + } - RaisePropertyChanged(nameof(FolderItems)); - } - else - { - await RefreshFolderItems(_leavePageCancellationTokenSource.Token); + SelectedChildFileSortType.Value = _displaySettingsByPathRepository.GetFileParentSettings(_currentPath); } - HasFileItem = await _imageCollectionManager.IsExistImageFileAsync(_currentItem, _leavePageCancellationTokenSource.Token); + await RefreshFolderItems(_leavePageCancellationTokenSource.Token); } } else if (!_isCompleteEnumeration @@ -339,19 +371,7 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) || _LastIsFolderThumbnailEnabled != _folderListingSettings.IsFolderThumbnailEnabled ) { - if (_CachedFolderListupItems.Remove(_currentPath, out var cachedItems)) - { - FolderItems = cachedItems.FolderItems; - - // 最後に読んだ位置を更新 - FolderItems.ForEach(x => x.UpdateLastReadPosition()); - - RaisePropertyChanged(nameof(FolderItems)); - } - else - { - await RefreshFolderItems(_leavePageCancellationTokenSource.Token); - } + await RefreshFolderItems(_leavePageCancellationTokenSource.Token); } else { @@ -379,6 +399,7 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) } } + lastIntractItemVM?.ThumbnailChanged(); FolderLastIntractItem.Value = lastIntractItemVM; } else @@ -392,29 +413,104 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) NowProcessing = false; } + Observable.CombineLatest( + SelectedFileSortType, + IsSortWithTitleDigitCompletion, + (sortType, withInterpolation) => (sortType, withInterpolation) + ) + .Pairwise() + .Where(x => x.NewItem != x.OldItem) + .Select(x => x.NewItem) + .Subscribe(x => _ = SetSort(x.sortType, x.withInterpolation, _leavePageCancellationTokenSource?.Token ?? CancellationToken.None)) + .AddTo(_navigationDisposables); + + if (_imageCollectionContext?.IsSupportedFolderContentsChanged ?? false) + { + // アプリ内部操作も含めて変更を検知する + bool requireRefresh = false; + _imageCollectionContext.CreateFolderAndArchiveFileChangedObserver() + .Subscribe(_ => + { + requireRefresh = true; + Debug.WriteLine("Folder andor Archive Update required. " + _currentPath); + }) + .AddTo(_navigationDisposables); + + ApplicationLifecycleObservable.WindowActivationStateChanged() + .Subscribe(async visible => + { + if (visible && requireRefresh && _imageCollectionContext is not null) + { + requireRefresh = false; + await ReloadItemsAsync(_imageCollectionContext, _leavePageCancellationTokenSource?.Token ?? CancellationToken.None); + Debug.WriteLine("Folder andor Archive Updated. " + _currentPath); + } + }) + .AddTo(_navigationDisposables); + } + await base.OnNavigatedToAsync(parameters); } + IImageCollectionContext _imageCollectionContext; #region Refresh Item static FastAsyncLock _RefreshLock = new FastAsyncLock(); private async Task RefreshFolderItems(CancellationToken ct) { - using var _ = await _RefreshLock.LockAsync(ct); + using var lockObject = await _RefreshLock.LockAsync(ct); + + FolderItems.Clear(); + DisplayCurrentArchiveFolderName = null; + CurrentFolderItem = null; + _imageCollectionContext = null; _isCompleteEnumeration = false; + IImageCollectionContext imageCollectionContext = null; try { if (_currentItem is StorageFolder folder) { Debug.WriteLine(folder.Path); - await RefreshFolderItems(folder, ct); + imageCollectionContext = await _imageCollectionManager.GetFolderImageCollectionContextAsync(folder, ct); + CurrentFolderItem = new StorageItemViewModel(new StorageItemImageSource(_currentItem, _thumbnailManager), _currentItemRootFolderToken, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); } else if (_currentItem is StorageFile file) { Debug.WriteLine(file.Path); - await RefreshFolderItems(file, ct); + if (file.IsSupportedImageFile()) + { + try + { + var parentFolder = await file.GetParentAsync(); + imageCollectionContext = await _imageCollectionManager.GetFolderImageCollectionContextAsync(parentFolder, ct); + } + catch (UnauthorizedAccessException) + { + var parentItem = await _sourceStorageItemsRepository.GetStorageItemFromPath(_currentItemRootFolderToken.TokenString, Path.GetDirectoryName(_currentPath)); + if (parentItem is StorageFolder parentFolder) + { + imageCollectionContext = await _imageCollectionManager.GetFolderImageCollectionContextAsync(parentFolder, ct); + } + } + + CurrentFolderItem = new StorageItemViewModel(new StorageItemImageSource(_currentItem, _thumbnailManager), _currentItemRootFolderToken, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); + } + else if (file.IsSupportedMangaFile()) + { + // string.Emptyを渡すことでルートフォルダのフォルダ取得を行える + imageCollectionContext = await _imageCollectionManager.GetArchiveImageCollectionContextAsync(file, _currentArchiveFolderName ?? string.Empty, ct); + DisplayCurrentArchiveFolderName = _currentArchiveFolderName; + if (_currentArchiveFolderName == null) + { + CurrentFolderItem = new StorageItemViewModel(new StorageItemImageSource(_currentItem, _thumbnailManager), _currentItemRootFolderToken, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); + } + else if (imageCollectionContext is ArchiveImageCollectionContext aic) + { + CurrentFolderItem = new StorageItemViewModel(new ArchiveDirectoryImageSource(aic.ArchiveImageCollection, aic.ArchiveDirectoryToken, _thumbnailManager), _currentItemRootFolderToken, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); + } + } } else { @@ -425,47 +521,158 @@ private async Task RefreshFolderItems(CancellationToken ct) } catch (OperationCanceledException) { - + CurrentFolderItem = null; + DisplayCurrentArchiveFolderName = _currentArchiveFolderName; + return; } - } + catch + { + CurrentFolderItem = null; + DisplayCurrentArchiveFolderName = _currentArchiveFolderName; + throw; + } + + if (imageCollectionContext == null) { return; } + _imageCollectionContext = imageCollectionContext; + _ImageCollectionDisposer = imageCollectionContext as IDisposable; + + await ReloadItemsAsync(_imageCollectionContext, ct); + } - private async ValueTask RefreshFolderItems(IStorageItem storageItem, CancellationToken ct) + private async Task ReloadItemsAsync(IImageCollectionContext imageCollectionContext, CancellationToken ct) { - FolderItems.Clear(); - var result = await _imageCollectionManager.GetFolderOrArchiveFileAsync(storageItem, ct); - - if (result.Images?.Any() != true) + var oldItemPathMap = FolderItems.Select(x => x.Path).ToHashSet(); + var newItems = await imageCollectionContext.GetFolderOrArchiveFilesAsync(ct); + var deletedItems = Enumerable.Except(oldItemPathMap, newItems.Select(x => x.Path)) + .Where(x => oldItemPathMap.Contains(x)) + .ToHashSet(); + + using (FileItemsView.DeferRefresh()) { - return; + // 削除アイテム + Debug.WriteLine($"items count : {FolderItems.Count}"); + FolderItems.Remove(itemVM => + { + var delete = deletedItems.Contains(itemVM.Path); + if (delete) { itemVM.Dispose(); } + return delete; + }); + + Debug.WriteLine($"after deleted : {FolderItems.Count}"); + // 新規アイテム + foreach (var item in newItems.Where(x => oldItemPathMap.Contains(x.Path) is false)) + { + FolderItems.Add(new StorageItemViewModel(item, _currentItemRootFolderToken, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager)); + } + Debug.WriteLine($"after added : {FolderItems.Count}"); } - foreach (var folderItem in result.Images) + ct.ThrowIfCancellationRequested(); + + _ = Task.Run(async () => + { + bool exist = await imageCollectionContext.IsExistImageFileAsync(ct); + _scheduler.Schedule(() => HasFileItem = exist); + + foreach (var item in newItems) + { + _PathReferenceCountManager.Upsert(item.Path, _currentItemRootFolderToken.TokenString); + } + }, ct); + } + +#endregion + + + +#region FileSortType + + + public static IEnumerable ToSortDescription(FileSortType fileSortType, bool withTitleDigitCompletion) + { + IComparer TitleDigitCompletionComparer = withTitleDigitCompletion ? Sorting.TitleDigitCompletionComparer.Default : null; + return fileSortType switch + { + FileSortType.UpdateTimeDescThenTitleAsc => new[] { new SortDescription(nameof(StorageItemViewModel.DateCreated), SortDirection.Descending), new SortDescription(nameof(StorageItemViewModel.Name), SortDirection.Ascending, TitleDigitCompletionComparer) }, + FileSortType.TitleAscending => new[] { new SortDescription(nameof(StorageItemViewModel.Name), SortDirection.Ascending, TitleDigitCompletionComparer) }, + FileSortType.TitleDecending => new[] { new SortDescription(nameof(StorageItemViewModel.Name), SortDirection.Descending, TitleDigitCompletionComparer) }, + FileSortType.UpdateTimeAscending => new[] { new SortDescription(nameof(StorageItemViewModel.DateCreated), SortDirection.Ascending) }, + FileSortType.UpdateTimeDecending => new[] { new SortDescription(nameof(StorageItemViewModel.DateCreated), SortDirection.Descending) }, + _ => throw new NotSupportedException(), + }; + } + + private DelegateCommand _ChangeFileSortCommand; + public DelegateCommand ChangeFileSortCommand => + _ChangeFileSortCommand ??= new DelegateCommand(async sort => { - _PathReferenceCountManager.Upsert(folderItem.StorageItem.Path, _currentItemRootFolderToken); - ct.ThrowIfCancellationRequested(); - var item = new StorageItemViewModel(folderItem, null, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); - if (item.Type == StorageItemTypes.Folder) + FileSortType? sortType = null; + if (sort is int num) { - FolderItems.Add(item); + sortType = (FileSortType)num; } - else if (item.Type == StorageItemTypes.Archive) + else if (sort is FileSortType sortTypeExact) { - FolderItems.Add(item); + sortType = sortTypeExact; } - else if (item.Type == StorageItemTypes.EBook) + + if (sortType.HasValue) { - FolderItems.Add(item); + SelectedFileSortType.Value = sortType.Value; } + }); + + private async Task SetSort(FileSortType fileSort, bool withNameInterpolation, CancellationToken ct) + { + using (await _RefreshLock.LockAsync(ct)) + { + SetSortAsyncUnsafe(fileSort, withNameInterpolation); } } + private void SetSortAsyncUnsafe(FileSortType fileSort, bool withNameInterpolation) + { + var sortDescriptions = ToSortDescription(fileSort, withNameInterpolation); + using (FileItemsView.DeferRefresh()) + { + FileItemsView.SortDescriptions.Clear(); + FileItemsView.SortDescriptions.Add(new SortDescription(nameof(StorageItemViewModel.Type), SortDirection.Ascending)); + foreach (var sort in sortDescriptions) + { + FileItemsView.SortDescriptions.Add(sort); + } + } - #endregion + FolderLastIntractItem.Value = FileItemsView.FirstOrDefault() as StorageItemViewModel; + FolderLastIntractItem.Value = null; + _displaySettingsByPathRepository.SetFolderAndArchiveSettings( + _currentPath, + fileSort, + withNameInterpolation + ); + } + private DelegateCommand _ChangeChildFileSortCommand; + public DelegateCommand ChangeChildFileSortCommand => + _ChangeChildFileSortCommand ??= new DelegateCommand(sort => + { + FileSortType? sortType = null; + if (sort is int num) + { + sortType = (FileSortType)num; + } + else if (sort is FileSortType sortTypeExact) + { + sortType = sortTypeExact; + } + + SelectedChildFileSortType.Value = sortType; + _displaySettingsByPathRepository.SetFileParentSettings(_currentPath, sortType); + }); - +#endregion } public abstract class FolderItemsGroupBase diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/ImageListupPageViewModel.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/ImageListupPageViewModel.cs index 4fa7689a..cc696f0c 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/ImageListupPageViewModel.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/ImageListupPageViewModel.cs @@ -5,10 +5,15 @@ using Reactive.Bindings; using Reactive.Bindings.Extensions; using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Reactive.Concurrency; +using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -22,10 +27,12 @@ using TsubameViewer.Presentation.Services.UWP; using TsubameViewer.Presentation.ViewModels.PageNavigation; using TsubameViewer.Presentation.ViewModels.PageNavigation.Commands; +using TsubameViewer.Presentation.ViewModels.SourceFolders.Commands; using TsubameViewer.Presentation.Views; using Uno.Extensions; using Uno.Threading; using Windows.Storage; +using static TsubameViewer.Models.Domain.ImageViewer.ImageCollectionManager; using StorageItemTypes = TsubameViewer.Models.Domain.StorageItemTypes; namespace TsubameViewer.Presentation.ViewModels @@ -36,8 +43,7 @@ public sealed class ImageListupPageViewModel : ViewModelBase static List _CacheFolderListupItemsOrder = new List(); static Dictionary _CachedFolderListupItems = new Dictionary(); - - + private readonly IScheduler _scheduler; private readonly BookmarkManager _bookmarkManager; private readonly ImageCollectionManager _imageCollectionManager; private readonly SourceStorageItemsRepository _sourceStorageItemsRepository; @@ -45,7 +51,7 @@ public sealed class ImageListupPageViewModel : ViewModelBase private readonly PathReferenceCountManager _PathReferenceCountManager; private readonly FolderLastIntractItemManager _folderLastIntractItemManager; private readonly FolderListingSettings _folderListingSettings; - + private readonly DisplaySettingsByPathRepository _displaySettingsByPathRepository; private bool _NowProcessing; public bool NowProcessing { @@ -61,6 +67,8 @@ public bool NowProcessing public OpenWithExplorerCommand OpenWithExplorerCommand { get; } public SecondaryTileAddCommand SecondaryTileAddCommand { get; } public SecondaryTileRemoveCommand SecondaryTileRemoveCommand { get; } + public ChangeStorageItemThumbnailImageCommand ChangeStorageItemThumbnailImageCommand { get; } + public OpenWithExternalApplicationCommand OpenWithExternalApplicationCommand { get; } public ObservableCollection ImageFileItems { get; private set; } @@ -91,6 +99,18 @@ public bool HasFolderOrBookItem public ReactivePropertySlim SelectedFileSortType { get; } + private readonly FileSortType DefaultFileSortType = FileSortType.TitleAscending; + + public ReactivePropertySlim IsSortWithTitleDigitCompletion { get; } + + private string _DisplaySortTypeInheritancePath; + public string DisplaySortTypeInheritancePath + { + get { return _DisplaySortTypeInheritancePath; } + private set { SetProperty(ref _DisplaySortTypeInheritancePath, value); } + } + + public ReactivePropertySlim ImageLastIntractItem { get; } static FastAsyncLock _NavigationLock = new FastAsyncLock(); @@ -116,10 +136,16 @@ public StorageItemViewModel CurrentFolderItem set { SetProperty(ref _CurrentFolderItem, value); } } - string _currentItemRootFolderToken; - + StorageItemToken _currentItemRootFolderToken; + private string _currentArchiveFolderName; + private string _DisplayCurrentArchiveFolderName; + public string DisplayCurrentArchiveFolderName + { + get { return _DisplayCurrentArchiveFolderName; } + private set { SetProperty(ref _DisplayCurrentArchiveFolderName, value); } + } private bool _IsRestrictImageFileThumbnail; @@ -142,8 +168,13 @@ public bool IsRestrictImageFileThumbnail public string FoldersManagementPageName => nameof(Views.SourceStorageItemsPage); + IDisposable _ImageCollectionDisposer; + + CompositeDisposable _disposables = new CompositeDisposable(); + CompositeDisposable _navigationDisposables; public ImageListupPageViewModel( + IScheduler scheduler, BookmarkManager bookmarkManager, ImageCollectionManager imageCollectionManager, SourceStorageItemsRepository sourceStorageItemsRepository, @@ -152,15 +183,19 @@ public ImageListupPageViewModel( SecondaryTileManager secondaryTileManager, FolderLastIntractItemManager folderLastIntractItemManager, FolderListingSettings folderListingSettings, + DisplaySettingsByPathRepository displaySettingsByPathRepository, OpenPageCommand openPageCommand, OpenFolderItemCommand openFolderItemCommand, OpenImageViewerCommand openImageViewerCommand, OpenFolderListupCommand openFolderListupCommand, OpenWithExplorerCommand openWithExplorerCommand, SecondaryTileAddCommand secondaryTileAddCommand, - SecondaryTileRemoveCommand secondaryTileRemoveCommand + SecondaryTileRemoveCommand secondaryTileRemoveCommand, + ChangeStorageItemThumbnailImageCommand changeStorageItemThumbnailImageCommand, + OpenWithExternalApplicationCommand openWithExternalApplicationCommand ) { + _scheduler = scheduler; _bookmarkManager = bookmarkManager; _imageCollectionManager = imageCollectionManager; _sourceStorageItemsRepository = sourceStorageItemsRepository; @@ -169,6 +204,7 @@ SecondaryTileRemoveCommand secondaryTileRemoveCommand SecondaryTileManager = secondaryTileManager; _folderLastIntractItemManager = folderLastIntractItemManager; _folderListingSettings = folderListingSettings; + _displaySettingsByPathRepository = displaySettingsByPathRepository; OpenPageCommand = openPageCommand; OpenFolderItemCommand = openFolderItemCommand; OpenImageViewerCommand = openImageViewerCommand; @@ -176,15 +212,32 @@ SecondaryTileRemoveCommand secondaryTileRemoveCommand OpenWithExplorerCommand = openWithExplorerCommand; SecondaryTileAddCommand = secondaryTileAddCommand; SecondaryTileRemoveCommand = secondaryTileRemoveCommand; + ChangeStorageItemThumbnailImageCommand = changeStorageItemThumbnailImageCommand; + OpenWithExternalApplicationCommand = openWithExternalApplicationCommand; ImageFileItems = new ObservableCollection(); FileItemsView = new AdvancedCollectionView(ImageFileItems); - SelectedFileSortType = new ReactivePropertySlim(FileSortType.TitleAscending); + SelectedFileSortType = new ReactivePropertySlim(FileSortType.TitleAscending) + .AddTo(_disposables); + IsSortWithTitleDigitCompletion = new ReactivePropertySlim(true) + .AddTo(_disposables); + + FileDisplayMode = _folderListingSettings.ToReactivePropertyAsSynchronized(x => x.FileDisplayMode) + .AddTo(_disposables); + ImageLastIntractItem = new ReactivePropertySlim() + .AddTo(_disposables); - FileDisplayMode = _folderListingSettings.ToReactivePropertyAsSynchronized(x => x.FileDisplayMode); - ImageLastIntractItem = new ReactivePropertySlim(); + } + public override void OnNavigatedFrom(INavigationParameters parameters) + { + _ImageCollectionDisposer?.Dispose(); + _ImageCollectionDisposer = null; + _navigationDisposables?.Dispose(); + + base.OnNavigatedFrom(parameters); + } public override void OnNavigatingTo(INavigationParameters parameters) { @@ -195,6 +248,7 @@ public override void OnNavigatingTo(INavigationParameters parameters) public override async Task OnNavigatedToAsync(INavigationParameters parameters) { + _navigationDisposables = new CompositeDisposable(); IsRestrictImageFileThumbnail = !_folderListingSettings.IsImageFileThumbnailEnabled; NowProcessing = true; @@ -213,6 +267,12 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) _leavePageCancellationTokenSource = new CancellationTokenSource(); + _currentArchiveFolderName = parameters.TryGetValue(PageNavigationConstants.ArchiveFolderName, out string archiveFolderName) + ? Uri.UnescapeDataString(archiveFolderName) + : null + ; + + if (mode == NavigationMode.New || mode == NavigationMode.Forward || mode == NavigationMode.Back @@ -259,12 +319,34 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) } } - _currentItemRootFolderToken = token; + _currentItemRootFolderToken = new StorageItemToken(_currentPath, token); var currentPathItem = await _sourceStorageItemsRepository.GetStorageItemFromPath(token, _currentPath); _currentItem = currentPathItem; DisplayCurrentPath = _currentItem.Path; - CurrentFolderItem = new StorageItemViewModel(new StorageItemImageSource(_currentItem, _thumbnailManager), token, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); + + + var settings = _displaySettingsByPathRepository.GetFolderAndArchiveSettings(_currentPath); + if (settings != null) + { + DisplaySortTypeInheritancePath = null; + SelectedFileSortType.Value = settings.Sort; + IsSortWithTitleDigitCompletion.Value = settings.IsTitleDigitInterpolation; + } + else if (_displaySettingsByPathRepository.GetFileParentSettingsUpStreamToRoot(_currentPath) is not null and var parentSort + && parentSort.ChildItemDefaultSort != null + ) + { + DisplaySortTypeInheritancePath = parentSort.Path; + SelectedFileSortType.Value = parentSort.ChildItemDefaultSort.Value; + IsSortWithTitleDigitCompletion.Value = true; + } + else + { + DisplaySortTypeInheritancePath = null; + SelectedFileSortType.Value = DefaultFileSortType; + IsSortWithTitleDigitCompletion.Value = true; + } } if (_currentPath != null && _CachedFolderListupItems.Remove(_currentPath, out var cachedItems)) @@ -276,17 +358,13 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) ImageFileItems.ForEach(x => x.UpdateLastReadPosition()); _FileItemsView = new AdvancedCollectionView(ImageFileItems); - using (FileItemsView.DeferRefresh()) - { - var sortDescription = ToSortDescription(SelectedFileSortType.Value); - - FileItemsView.SortDescriptions.Clear(); - FileItemsView.SortDescriptions.Add(sortDescription); - } - RaisePropertyChanged(nameof(FileItemsView)); + + SetSortAsyncUnsafe(SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value); } else { + SetSortAsyncUnsafe(SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value); + await RefreshFolderItems(_leavePageCancellationTokenSource.Token); } @@ -305,21 +383,17 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) ImageFileItems.ForEach(x => x.UpdateLastReadPosition()); _FileItemsView = new AdvancedCollectionView(ImageFileItems); - using (FileItemsView.DeferRefresh()) - { - var sortDescription = ToSortDescription(SelectedFileSortType.Value); - FileItemsView.SortDescriptions.Clear(); - FileItemsView.SortDescriptions.Add(sortDescription); - } - RaisePropertyChanged(nameof(FileItemsView)); + SetSortAsyncUnsafe(SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value); } else { await RefreshFolderItems(_leavePageCancellationTokenSource.Token); + + SetSortAsyncUnsafe(SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value); } } - else + else { // 前回読み込みキャンセルしていたものを改めて読み込むように ImageFileItems.ForEach(x => x.RestoreThumbnailLoadingTask()); @@ -349,82 +423,182 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) NowProcessing = false; } + Observable.CombineLatest( + IsSortWithTitleDigitCompletion, + SelectedFileSortType, + (x, y) => (x, y) + ) + .Pairwise() + .Where(x => x.NewItem != x.OldItem) + .Subscribe(async _ => + { + await SetSort(SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value, _leavePageCancellationTokenSource?.Token ?? default); + }) + .AddTo(_navigationDisposables); + + IsSortWithTitleDigitCompletion + .Pairwise() + .Where(x => x.NewItem != x.OldItem) + .Subscribe(x => _displaySettingsByPathRepository.SetFolderAndArchiveSettings(_currentPath, SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value)) + .AddTo(_navigationDisposables); + + + if (_imageCollectionContext?.IsSupportedFolderContentsChanged ?? false) + { + // アプリ内部操作も含めて変更を検知する + bool requireRefresh = false; + _imageCollectionContext.CreateImageFileChangedObserver() + .Subscribe(_ => + { + requireRefresh = true; + Debug.WriteLine("Images Update required. " + _currentPath); + }) + .AddTo(_navigationDisposables); + + ApplicationLifecycleObservable.WindowActivationStateChanged() + .Subscribe(async visible => + { + if (visible && requireRefresh && _imageCollectionContext is not null) + { + requireRefresh = false; + await ReloadItemsAsync(_imageCollectionContext, _leavePageCancellationTokenSource?.Token ?? CancellationToken.None); + Debug.WriteLine("Images Updated. " + _currentPath); + } + }) + .AddTo(_navigationDisposables); + } + await base.OnNavigatedToAsync(parameters); } #region Refresh Item + IImageCollectionContext _imageCollectionContext; static FastAsyncLock _RefreshLock = new FastAsyncLock(); private async Task RefreshFolderItems(CancellationToken ct) { - using var _ = await _RefreshLock.LockAsync(ct); + using var lockObject = await _RefreshLock.LockAsync(ct); + _ImageCollectionDisposer?.Dispose(); + _ImageCollectionDisposer = null; + ImageFileItems.Clear(); + + _imageCollectionContext = null; _isCompleteEnumeration = false; - try + IImageCollectionContext imageCollectionContext = null; + if (_currentItem is StorageFolder folder) { - if (_currentItem is StorageFolder folder) - { - Debug.WriteLine(folder.Path); - await RefreshFolderItems(folder, ct); - } - else if (_currentItem is StorageFile file) + Debug.WriteLine(folder.Path); + imageCollectionContext = await _imageCollectionManager.GetFolderImageCollectionContextAsync(folder, ct); + CurrentFolderItem = new StorageItemViewModel(new StorageItemImageSource(_currentItem, _thumbnailManager), _currentItemRootFolderToken, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); + } + else if (_currentItem is StorageFile file) + { + Debug.WriteLine(file.Path); + if (file.IsSupportedImageFile()) { - Debug.WriteLine(file.Path); - await RefreshFolderItems(file, ct); + try + { + var parentFolder = await file.GetParentAsync(); + imageCollectionContext = await _imageCollectionManager.GetFolderImageCollectionContextAsync(parentFolder, ct); + } + catch (UnauthorizedAccessException) + { + var parentItem = await _sourceStorageItemsRepository.GetStorageItemFromPath(_currentItemRootFolderToken.TokenString, Path.GetDirectoryName(_currentPath)); + if (parentItem is StorageFolder parentFolder) + { + imageCollectionContext = await _imageCollectionManager.GetFolderImageCollectionContextAsync(parentFolder, ct); + } + } + + CurrentFolderItem = new StorageItemViewModel(new StorageItemImageSource(_currentItem, _thumbnailManager), _currentItemRootFolderToken, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); } - else + else if (file.IsSupportedMangaFile()) { - throw new NotSupportedException(); + imageCollectionContext = await _imageCollectionManager.GetArchiveImageCollectionContextAsync(file, _currentArchiveFolderName ?? String.Empty, ct); + DisplayCurrentArchiveFolderName = _currentArchiveFolderName; + if (_currentArchiveFolderName == null) + { + CurrentFolderItem = new StorageItemViewModel(new StorageItemImageSource(_currentItem, _thumbnailManager), _currentItemRootFolderToken, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); + } + else if (imageCollectionContext is ArchiveImageCollectionContext aic) + { + CurrentFolderItem = new StorageItemViewModel(new ArchiveDirectoryImageSource(aic.ArchiveImageCollection, aic.ArchiveDirectoryToken, _thumbnailManager), _currentItemRootFolderToken, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); + } } - - _isCompleteEnumeration = true; } - catch (OperationCanceledException) + else { - + throw new NotSupportedException(); } + + _isCompleteEnumeration = true; + + if (imageCollectionContext == null) { return; } + + _imageCollectionContext = imageCollectionContext; + _ImageCollectionDisposer = imageCollectionContext as IDisposable; + + await ReloadItemsAsync(_imageCollectionContext, ct); } - private async ValueTask RefreshFolderItems(IStorageItem storageItem, CancellationToken ct) - { - ImageFileItems.Clear(); - var result = await _imageCollectionManager.GetImagesAsync(storageItem, ct); - if (result.Images?.Any() != true) - { - return; - } + private async Task ReloadItemsAsync(IImageCollectionContext imageCollectionContext, CancellationToken ct) + { + var oldItemPathMap = ImageFileItems.Select(x => x.Path).ToHashSet(); + var newItems = await imageCollectionContext.GetImageFilesAsync(ct); + var deletedItems = Enumerable.Except(oldItemPathMap, newItems.Select(x => x.Path)) + .Where(x => oldItemPathMap.Contains(x)) + .ToHashSet(); - foreach (var folderItem in result.Images) + using (FileItemsView.DeferRefresh()) { - _PathReferenceCountManager.Upsert(folderItem.StorageItem.Path, _currentItemRootFolderToken); - ct.ThrowIfCancellationRequested(); - var item = new StorageItemViewModel(folderItem, null, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); - if (item.Type == StorageItemTypes.Image) + // 削除アイテム + Debug.WriteLine($"items count : {ImageFileItems.Count}"); + ImageFileItems.Remove(itemVM => { - ImageFileItems.Add(item); + var delete = deletedItems.Contains(itemVM.Path); + if (delete) { itemVM.Dispose(); } + return delete; + }); + Debug.WriteLine($"after deleted : {ImageFileItems.Count}"); + // 新規アイテム + foreach (var item in newItems.Where(x => oldItemPathMap.Contains(x.Path) is false)) + { + ImageFileItems.Add(new StorageItemViewModel(item, _currentItemRootFolderToken, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager)); } + Debug.WriteLine($"after added : {ImageFileItems.Count}"); } + ct.ThrowIfCancellationRequested(); + HasFileItem = ImageFileItems.Any(); - HasFolderOrBookItem = storageItem is StorageFolder folder - ? await _imageCollectionManager.IsExistFolderOrArchiveFileAsync(folder, ct) - : false - ; + _ = Task.Run(async () => + { + bool exist = await imageCollectionContext.IsExistFolderOrArchiveFileAsync(ct); + _scheduler.Schedule(() => HasFolderOrBookItem = exist); + + foreach (var item in newItems) + { + _PathReferenceCountManager.Upsert(item.Path, _currentItemRootFolderToken.TokenString); + } + }, ct); } - #endregion +#endregion - #region FileSortType +#region FileSortType - public static SortDescription ToSortDescription(FileSortType fileSortType) + public static IEnumerable ToSortDescription(FileSortType fileSortType, bool withTitleDigitCompletion) { + IComparer TitleDigitCompletionComparer = withTitleDigitCompletion ? Sorting.TitleDigitCompletionComparer.Default : null; return fileSortType switch { - FileSortType.TitleAscending => new SortDescription(nameof(StorageItemViewModel.Name), SortDirection.Ascending), - FileSortType.TitleDecending => new SortDescription(nameof(StorageItemViewModel.Name), SortDirection.Descending), - FileSortType.UpdateTimeAscending => new SortDescription(nameof(StorageItemViewModel.DateCreated), SortDirection.Ascending), - FileSortType.UpdateTimeDecending => new SortDescription(nameof(StorageItemViewModel.DateCreated), SortDirection.Descending), + FileSortType.UpdateTimeDescThenTitleAsc => new[] { new SortDescription(nameof(StorageItemViewModel.DateCreated), SortDirection.Descending), new SortDescription(nameof(StorageItemViewModel.Name), SortDirection.Ascending, TitleDigitCompletionComparer) }, + FileSortType.TitleAscending => new[] { new SortDescription(nameof(StorageItemViewModel.Name), SortDirection.Ascending, TitleDigitCompletionComparer) }, + FileSortType.TitleDecending => new[] { new SortDescription(nameof(StorageItemViewModel.Name), SortDirection.Descending, TitleDigitCompletionComparer) }, + FileSortType.UpdateTimeAscending => new[] { new SortDescription(nameof(StorageItemViewModel.DateCreated), SortDirection.Ascending) }, + FileSortType.UpdateTimeDecending => new[] { new SortDescription(nameof(StorageItemViewModel.DateCreated), SortDirection.Descending) }, _ => throw new NotSupportedException(), }; } @@ -445,27 +619,58 @@ public static SortDescription ToSortDescription(FileSortType fileSortType) if (sortType.HasValue) { + DisplaySortTypeInheritancePath = null; SelectedFileSortType.Value = sortType.Value; - - using (await _RefreshLock.LockAsync(_leavePageCancellationTokenSource.Token)) + _displaySettingsByPathRepository.SetFolderAndArchiveSettings(_currentPath, SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value); + } + else + { + _displaySettingsByPathRepository.ClearFolderAndArchiveSettings(_currentPath); + if (_displaySettingsByPathRepository.GetFileParentSettingsUpStreamToRoot(_currentPath) is not null and var parentSort + && parentSort.ChildItemDefaultSort != null + ) { - if (ImageFileItems.Any()) - { - using (FileItemsView.DeferRefresh()) - { - var sortDescription = ToSortDescription(SelectedFileSortType.Value); + DisplaySortTypeInheritancePath = parentSort.Path; + SelectedFileSortType.Value = parentSort.ChildItemDefaultSort.Value; + } + else + { + DisplaySortTypeInheritancePath = null; + SelectedFileSortType.Value = DefaultFileSortType; + } + } + }); - FileItemsView.SortDescriptions.Clear(); - FileItemsView.SortDescriptions.Add(sortDescription); - } - } + private async Task SetSort(FileSortType fileSort, bool withNameInterpolation, CancellationToken ct) + { + using (await _RefreshLock.LockAsync(ct)) + { + SetSortAsyncUnsafe(fileSort, withNameInterpolation); + } + } - RaisePropertyChanged(nameof(ImageFileItems)); - } + private void SetSortAsyncUnsafe(FileSortType fileSort, bool withNameInterpolation) + { + var sortDescriptions = ToSortDescription(fileSort, withNameInterpolation); + using (FileItemsView.DeferRefresh()) + { + FileItemsView.SortDescriptions.Clear(); + foreach (var sort in sortDescriptions) + { + FileItemsView.SortDescriptions.Add(sort); } + } + } + + private DelegateCommand _SetParentFileSortWithCurrentSettingCommand; + public DelegateCommand SetParentFileSortWithCurrentSettingCommand => + _SetParentFileSortWithCurrentSettingCommand ??= new DelegateCommand(() => + { + _displaySettingsByPathRepository.SetFileParentSettings(Path.GetDirectoryName(_currentPath), SelectedFileSortType.Value); }); - #endregion + +#endregion } } diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/ImageViewerPageViewModel.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/ImageViewerPageViewModel.cs index f6759cab..90eb00e1 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/ImageViewerPageViewModel.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/ImageViewerPageViewModel.cs @@ -39,6 +39,14 @@ using Windows.UI.Xaml; using Windows.UI.Xaml.Media.Imaging; using StorageItemTypes = TsubameViewer.Models.Domain.StorageItemTypes; +using TsubameViewer.Models.Domain.ImageViewer.ImageSource; +using static TsubameViewer.Models.Domain.ImageViewer.ImageCollectionManager; +using TsubameViewer.Presentation.ViewModels.Sorting; +using Microsoft.Toolkit.Mvvm.Messaging; +using System.Windows.Input; +using TsubameViewer.Presentation.Services.UWP; +using Uno.Disposables; +using CompositeDisposable = System.Reactive.Disposables.CompositeDisposable; namespace TsubameViewer.Presentation.ViewModels { @@ -52,6 +60,8 @@ public sealed class ImageViewerPageViewModel : ViewModelBase, IDisposable IDisposable _ImageEnumerationDisposer; private IImageSource[] _Images; + private string _currentItemRootFolderToken; + public IImageSource[] Images { get { return _Images; } @@ -80,6 +90,18 @@ public string ParentFolderOrArchiveName } public IReadOnlyReactiveProperty DisplayCurrentImageIndex { get; } + public ReactivePropertySlim SelectedFileSortType { get; } + + private readonly FileSortType DefaultFileSortType = FileSortType.TitleAscending; + + public ReactivePropertySlim IsSortWithTitleDigitCompletion { get; } + + private string _DisplaySortTypeInheritancePath; + public string DisplaySortTypeInheritancePath + { + get { return _DisplaySortTypeInheritancePath; } + private set { SetProperty(ref _DisplaySortTypeInheritancePath, value); } + } public ReactiveProperty CanvasWidth { get; } public ReactiveProperty CanvasHeight { get; } @@ -141,16 +163,19 @@ public bool NowImageLoadingLongRunning public ImageViewerSettings ImageViewerSettings { get; } private readonly IScheduler _scheduler; + private readonly IMessenger _messenger; private readonly SourceStorageItemsRepository _sourceStorageItemsRepository; private readonly PathReferenceCountManager _PathReferenceCountManager; private readonly ImageCollectionManager _imageCollectionManager; private readonly BookmarkManager _bookmarkManager; private readonly RecentlyAccessManager _recentlyAccessManager; private readonly FolderLastIntractItemManager _folderLastIntractItemManager; + private readonly DisplaySettingsByPathRepository _displaySettingsByPathRepository; CompositeDisposable _disposables = new CompositeDisposable(); public ImageViewerPageViewModel( IScheduler scheduler, + IMessenger messenger, SourceStorageItemsRepository sourceStorageItemsRepository, PathReferenceCountManager PathReferenceCountManager, ImageCollectionManager imageCollectionManager, @@ -159,11 +184,13 @@ public ImageViewerPageViewModel( RecentlyAccessManager recentlyAccessManager, RecyclableMemoryStreamManager recyclableMemoryStreamManager, FolderLastIntractItemManager folderLastIntractItemManager, + DisplaySettingsByPathRepository displaySettingsByPathRepository, ToggleFullScreenCommand toggleFullScreenCommand, BackNavigationCommand backNavigationCommand ) { _scheduler = scheduler; + _messenger = messenger; _sourceStorageItemsRepository = sourceStorageItemsRepository; _PathReferenceCountManager = PathReferenceCountManager; _imageCollectionManager = imageCollectionManager; @@ -173,6 +200,7 @@ BackNavigationCommand backNavigationCommand _bookmarkManager = bookmarkManager; _recentlyAccessManager = recentlyAccessManager; _folderLastIntractItemManager = folderLastIntractItemManager; + _displaySettingsByPathRepository = displaySettingsByPathRepository; DisplayCurrentImageIndex = this.ObserveProperty(x => x.CurrentImageIndex) .Select(x => x + 1) .Do(_ => @@ -180,7 +208,7 @@ BackNavigationCommand backNavigationCommand if (Images == null || !Images.Any()) { return; } var imageSource = Images[CurrentImageIndex]; - var names = imageSource.Name.Split(SeparateChars); + var names = imageSource.Path.Split(SeparateChars); PageName = names[names.Length - 1]; PageFolderName = names.Length >= 2 ? names[names.Length - 2] : string.Empty; _bookmarkManager.AddBookmark(_currentFolderItem.Path, imageSource.Name, new NormalizedPagePosition(Images.Length, _CurrentImageIndex)); @@ -196,6 +224,12 @@ BackNavigationCommand backNavigationCommand _appView = ApplicationView.GetForCurrentView(); + SelectedFileSortType = new ReactivePropertySlim(DefaultFileSortType) + .AddTo(_disposables); + IsSortWithTitleDigitCompletion = new ReactivePropertySlim(true) + .AddTo(_disposables); + + _SizeChangedSubject .Where(x => x >= 0 && Images != null) .Throttle(TimeSpan.FromMilliseconds(50), _scheduler) @@ -224,7 +258,7 @@ public override void OnNavigatedFrom(INavigationParameters parameters) if (Images?.Any() ?? false) { - Images.ForEach(x => (x as IDisposable)?.Dispose()); + Images.ForEach((IImageSource x) => x.TryDispose()); Images = null; } @@ -256,6 +290,11 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) _navigationDisposables = new CompositeDisposable(); _leavePageCancellationTokenSource = new CancellationTokenSource() .AddTo(_navigationDisposables); + _imageLoadingCts = new CancellationTokenSource(); + _imageCollectionContext = null; + PageName = null; + Title = null; + PageFolderName = null; // 一旦ボタン類を押せないように変更通知 GoNextImageCommand.RaiseCanExecuteChanged(); @@ -328,8 +367,31 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) } } + _currentItemRootFolderToken = token; Images = default; CurrentImageIndex = 0; + + _appView.Title = _currentFolderItem.Name; + Title = _currentFolderItem.Name; + + DisplaySortTypeInheritancePath = null; + var settings = _displaySettingsByPathRepository.GetFolderAndArchiveSettings(_currentPath); + if (settings != null) + { + SelectedFileSortType.Value = settings.Sort; + IsSortWithTitleDigitCompletion.Value = settings.IsTitleDigitInterpolation; + } + else if (_displaySettingsByPathRepository.GetFileParentSettingsUpStreamToRoot(_currentPath) is not null and var parentSort && parentSort.ChildItemDefaultSort != null) + { + DisplaySortTypeInheritancePath = parentSort.Path; + SelectedFileSortType.Value = parentSort.ChildItemDefaultSort.Value; + IsSortWithTitleDigitCompletion.Value = true; + } + else + { + SelectedFileSortType.Value = DefaultFileSortType; + IsSortWithTitleDigitCompletion.Value = true; + } } } } @@ -339,7 +401,18 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) // 2. 前回の更新が未完了だった場合 if (_currentFolderItem != null) { - await RefreshItems(_leavePageCancellationTokenSource.Token); + try + { +#if DEBUG + //await _messenger.WorkWithBusyWallAsync(async ct => await Task.Delay(TimeSpan.FromSeconds(5), ct), _leavePageCancellationTokenSource.Token); +#endif + await _messenger.WorkWithBusyWallAsync(RefreshItems, _leavePageCancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + (BackNavigationCommand as ICommand).Execute(null); + return; + } } // 表示する画像を決める @@ -361,8 +434,7 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) } } } - else if (mode == NavigationMode.New && parameters.ContainsKey(PageNavigationConstants.PageName) - ) + else if (mode == NavigationMode.New && parameters.ContainsKey(PageNavigationConstants.PageName)) { if (parameters.TryGetValue(PageNavigationConstants.PageName, out string pageName)) { @@ -380,6 +452,21 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) } } + + if (mode == NavigationMode.New && parameters.ContainsKey(PageNavigationConstants.ArchiveFolderName)) + { + if (parameters.TryGetValue(PageNavigationConstants.ArchiveFolderName, out string folderName)) + { + var unescapedFolderName = Uri.UnescapeDataString(folderName); + var pageFirstItem = Images.FirstOrDefault(x => x.Path.Contains(unescapedFolderName)); + if (pageFirstItem != null) + { + CurrentImageIndex = Images.IndexOf(pageFirstItem); + } + } + } + + RaisePropertyChanged(nameof(CurrentImageIndex)); // 表示画像が揃ったら改めてボタンを有効化 GoNextImageCommand.RaiseCanExecuteChanged(); @@ -388,7 +475,7 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) // 画像更新 new [] { - ImageViewerSettings.ObserveProperty(x => x.IsEnableSpreadDisplay).ToUnit() + ImageViewerSettings.ObserveProperty(x => x.IsEnableSpreadDisplay).ToUnit(), } .Merge() .Throttle(TimeSpan.FromMilliseconds(50), _scheduler) @@ -399,6 +486,62 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) }) .AddTo(_navigationDisposables); + Observable.CombineLatest( + IsSortWithTitleDigitCompletion, + SelectedFileSortType, + (x, y) => (x, y) + ) + .Pairwise() + .Where(x => x.NewItem != x.OldItem) + .Subscribe(async _ => + { + if (Images == null) { return; } + + var currentItemPath = Images[CurrentImageIndex].Path; + Images = ToSortedImages(Images, SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value).ToArray(); + await ResetImageIndex(Images.IndexOf(null, (x, y) => y.Path == currentItemPath)); + }) + .AddTo(_navigationDisposables); + + IsSortWithTitleDigitCompletion + .Pairwise() + .Where(x => x.NewItem != x.OldItem) + .Subscribe(x => _displaySettingsByPathRepository.SetFolderAndArchiveSettings(_currentPath, SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value)) + .AddTo(_navigationDisposables); + + if (_imageCollectionContext?.IsSupportedFolderContentsChanged ?? false) + { + // アプリ内部操作も含めて変更を検知する + bool requireRefresh = false; + _imageCollectionContext.CreateImageFileChangedObserver() + .Subscribe(_ => + { + requireRefresh = true; + Debug.WriteLine("Images Update required. " + _currentPath); + }) + .AddTo(_navigationDisposables); + + ApplicationLifecycleObservable.WindowActivationStateChanged() + .Subscribe(async visible => + { + if (visible && requireRefresh && _imageCollectionContext is not null) + { + requireRefresh = false; + var currentItemPath = Images[CurrentImageIndex].Path; + await ReloadItemsAsync(_imageCollectionContext, _leavePageCancellationTokenSource?.Token ?? CancellationToken.None); + + var index = Images.IndexOf(null, (x, y) => y.Path == currentItemPath); + if (index < 0) + { + index = 0; + } + await ResetImageIndex(index); + Debug.WriteLine("Images Updated. " + _currentPath); + } + }) + .AddTo(_navigationDisposables); + } + await base.OnNavigatedToAsync(parameters); } @@ -409,7 +552,7 @@ async Task MoveImageIndex(IndexMoveDirection direction, int? request = null) { if (Images == null || Images.Length == 0) { return; } - if (direction != IndexMoveDirection.Backward) + if (direction != IndexMoveDirection.Forward) { ClearPrefetch(); } @@ -623,8 +766,17 @@ async Task ResetImageIndex(int requestIndex) async Task MakeBitmapImageAsync(IImageSource imageSource, int canvasWidth, int canvasHeight, CancellationToken ct) { - var bitmapImage = await GetImageIfPrefetched(imageSource) - ?? await imageSource.GenerateBitmapImageAsync(ct); + var bitmapImage = await GetImageIfPrefetched(imageSource); + + if (bitmapImage == null) + { + using (var stream = await imageSource.GetImageStreamAsync(ct)) + { + bitmapImage = new BitmapImage(); + bitmapImage.SetSource(stream); + } + } + // 画面より小さい画像を表示するときはアンチエイリアスと省メモリのため画面サイズにまで縮小 if (bitmapImage.PixelHeight > bitmapImage.PixelWidth) @@ -653,6 +805,7 @@ public enum IndexMoveDirection Backward, } + IImageCollectionContext _imageCollectionContext; CancellationTokenSource _imageLoadingCts; FastAsyncLock _imageLoadingLock = new FastAsyncLock(); @@ -661,40 +814,117 @@ private async Task RefreshItems(CancellationToken ct) _ImageEnumerationDisposer?.Dispose(); _ImageEnumerationDisposer = null; - var result = await _imageCollectionManager.GetImagesAsync(_currentFolderItem); - if (result != null) + IImageCollectionContext imageCollectionContext = null; + if (_currentFolderItem is StorageFolder folder) + { + Debug.WriteLine(folder.Path); + imageCollectionContext = await _imageCollectionManager.GetFolderImageCollectionContextAsync(folder, ct); + } + else if (_currentFolderItem is StorageFile file) + { + Debug.WriteLine(file.Path); + if (file.IsSupportedImageFile()) + { + try + { + var parentFolder = await file.GetParentAsync(); + imageCollectionContext = await _imageCollectionManager.GetFolderImageCollectionContextAsync(parentFolder, ct); + } + catch (UnauthorizedAccessException) + { + var parentItem = await _sourceStorageItemsRepository.GetStorageItemFromPath(_currentItemRootFolderToken, Path.GetDirectoryName(_currentPath)); + if (parentItem is StorageFolder parentFolder) + { + imageCollectionContext = await _imageCollectionManager.GetFolderImageCollectionContextAsync(parentFolder, ct); + } + } + } + else if (file.IsSupportedMangaFile()) + { + imageCollectionContext = await _imageCollectionManager.GetArchiveImageCollectionContextAsync(file, null, ct); + } + } + else + { + throw new NotSupportedException(); + } + + if (imageCollectionContext == null) { return; } + + _ImageEnumerationDisposer = imageCollectionContext as IDisposable; + _imageCollectionContext = imageCollectionContext; + + ParentFolderOrArchiveName = imageCollectionContext.Name; + ItemType = SupportedFileTypesHelper.StorageItemToStorageItemTypes(_currentFolderItem); + _appView.Title = _currentFolderItem.Name; + Title = ItemType == StorageItemTypes.Image ? ParentFolderOrArchiveName : _currentFolderItem.Name; + + await ReloadItemsAsync(imageCollectionContext, ct); + { - Images = result.Images; - CurrentImageIndex = result.FirstSelectedIndex; - _ImageEnumerationDisposer = result.ItemsEnumeratorDisposer; - ParentFolderOrArchiveName = result.ParentFolderOrArchiveName; + if (_currentFolderItem is StorageFile file && file.IsSupportedImageFile()) + { + var item = Images.FirstOrDefault(x => x.StorageItem.Path == _currentFolderItem.Path); + CurrentImageIndex = Images.IndexOf(item); + } + else { CurrentImageIndex = 0; } + } - if (_currentFolderItem is StorageFolder || - (_currentFolderItem is StorageFile file && SupportedFileTypesHelper.IsSupportedArchiveFileExtension(file.FileType)) - ) + if (await imageCollectionContext.IsExistFolderOrArchiveFileAsync(ct)) + { + var folders = await imageCollectionContext.GetLeafFoldersAsync(ct); + if (folders.Count <= 1) { - PageFolderNames = Images.Select(x => SeparateChars.Any(sc => x.Name.Contains(sc)) ? x.Name.Split(SeparateChars).TakeLast(2).First() : string.Empty).Distinct().Where(x => !string.IsNullOrEmpty(x)).ToArray(); + PageFolderNames = new string[0]; } else { - PageFolderNames = new string[0]; + PageFolderNames = folders.Select(x => x.Name).ToArray(); } + } + else + { + PageFolderNames = new string[0]; + } + GoNextImageCommand.RaiseCanExecuteChanged(); + GoPrevImageCommand.RaiseCanExecuteChanged(); - ItemType = SupportedFileTypesHelper.StorageItemToStorageItemTypes(_currentFolderItem); - - _appView.Title = _currentFolderItem.Name; - Title = ItemType == StorageItemTypes.Image ? ParentFolderOrArchiveName : _currentFolderItem.Name; + _recentlyAccessManager.AddWatched(_currentPath, DateTimeOffset.Now); + } - GoNextImageCommand.RaiseCanExecuteChanged(); - GoPrevImageCommand.RaiseCanExecuteChanged(); - _recentlyAccessManager.AddWatched(_currentPath, DateTimeOffset.Now); + private async Task ReloadItemsAsync(IImageCollectionContext imageCollectionContext, CancellationToken ct) + { + foreach (var oldImage in Images ?? Enumerable.Empty()) + { + oldImage.TryDispose(); } + + var images = await imageCollectionContext.GetAllImageFilesAsync(ct); + Images = ToSortedImages(images, SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value).ToArray(); } - #region Prefetch Images + IOrderedEnumerable ToSortedImages(IEnumerable images, FileSortType sort, bool withTitleDigitCompletion) + { + return sort switch + { + FileSortType.UpdateTimeDescThenTitleAsc => withTitleDigitCompletion + ? images.OrderBy(x => x.DateCreated).ThenBy(x => x.Path, TitleDigitCompletionComparer.Default) + : images.OrderBy(x => x.DateCreated).ThenBy(x => x.Path), + FileSortType.TitleAscending => withTitleDigitCompletion + ? images.OrderBy(x => x.Path, TitleDigitCompletionComparer.Default) + : images.OrderBy(x => x.Path), + FileSortType.TitleDecending => withTitleDigitCompletion + ? images.OrderByDescending(x => x.Path, TitleDigitCompletionComparer.Default) + : images.OrderByDescending(x => x.Path), + FileSortType.UpdateTimeAscending => images.OrderBy(x => x.DateCreated), + FileSortType.UpdateTimeDecending => images.OrderByDescending(x => x.DateCreated), + _ => throw new NotSupportedException(sort.ToString()), + }; + } +#region Prefetch Images PrefetchImageInfo[] _PrefetchImageDatum = new PrefetchImageInfo[2]; @@ -729,10 +959,10 @@ void ClearPrefetch() } - #endregion +#endregion - #region Commands +#region Commands public ToggleFullScreenCommand ToggleFullScreenCommand { get; } public BackNavigationCommand BackNavigationCommand { get; } @@ -785,7 +1015,7 @@ private bool CanGoPrevCommand() void ExecuteChangePageFolderCommand(string pageName) { - var pageFirstItem = Images.FirstOrDefault(x => x.Name.Contains(pageName)); + var pageFirstItem = Images.FirstOrDefault(x => x.Path.Contains(pageName)); if (pageFirstItem == null) { return; } var pageFirstItemIndex = Images.IndexOf(pageFirstItem); @@ -813,9 +1043,48 @@ void ExecuteDoubleViewCorrectCommand() } - #endregion - #region Single/Double View + private DelegateCommand _ChangeFileSortCommand; + public DelegateCommand ChangeFileSortCommand => + _ChangeFileSortCommand ??= new DelegateCommand(async sort => + { + FileSortType? sortType = null; + if (sort is int num) + { + sortType = (FileSortType)num; + } + else if (sort is FileSortType sortTypeExact) + { + sortType = sortTypeExact; + } + + if (sortType.HasValue) + { + DisplaySortTypeInheritancePath = null; + SelectedFileSortType.Value = sortType.Value; + _displaySettingsByPathRepository.SetFolderAndArchiveSettings(_currentPath, SelectedFileSortType.Value, IsSortWithTitleDigitCompletion.Value); + } + else + { + _displaySettingsByPathRepository.ClearFolderAndArchiveSettings(_currentPath); + if (_displaySettingsByPathRepository.GetFileParentSettingsUpStreamToRoot(_currentPath) is not null and var parentSort + && parentSort.ChildItemDefaultSort != null + ) + { + DisplaySortTypeInheritancePath = parentSort.Path; + SelectedFileSortType.Value = parentSort.ChildItemDefaultSort.Value; + } + else + { + DisplaySortTypeInheritancePath = null; + SelectedFileSortType.Value = DefaultFileSortType; + } + } + }); + +#endregion + +#region Single/Double View private bool _NowDoubleImageView; public bool NowDoubleImageView @@ -834,7 +1103,7 @@ public int ViewResponsibleImageAmount void CalcViewResponsibleImageAmount(double canvasWidth, double canvasHeight) { - var images = _CurrentImages.ToArray(); + var images = _CurrentImages?.ToArray(); if (ImageViewerSettings.IsEnableSpreadDisplay) { var aspectRatio = canvasWidth / canvasHeight; @@ -844,7 +1113,7 @@ void CalcViewResponsibleImageAmount(double canvasWidth, double canvasHeight) ViewResponsibleImageAmount = 2; if (CurrentImages?.Length != 2) { - CurrentImages = new BitmapImage[2] { images.ElementAtOrDefault(0), null }; + CurrentImages = new BitmapImage[2] { images?.ElementAtOrDefault(0), null }; } } else @@ -852,7 +1121,7 @@ void CalcViewResponsibleImageAmount(double canvasWidth, double canvasHeight) ViewResponsibleImageAmount = 1; if (CurrentImages?.Length != 1) { - CurrentImages = new BitmapImage[1] { images.ElementAtOrDefault(0) }; + CurrentImages = new BitmapImage[1] { images?.ElementAtOrDefault(0) }; } } } @@ -861,14 +1130,14 @@ void CalcViewResponsibleImageAmount(double canvasWidth, double canvasHeight) ViewResponsibleImageAmount = 1; if (CurrentImages?.Length != 1) { - CurrentImages = new BitmapImage[1] { images.ElementAtOrDefault(0) }; + CurrentImages = new BitmapImage[1] { images?.ElementAtOrDefault(0) }; } } } - #endregion +#endregion } @@ -901,9 +1170,18 @@ public void Cancel() public async Task StartPrefetchAsync() { - using (await _lock.LockAsync(_PrefetchCts.Token)) - { - Image ??= await ImageSource.GenerateBitmapImageAsync(_PrefetchCts.Token); + var ct = _PrefetchCts.Token; + using (await _lock.LockAsync(ct)) + { + if (Image == null) + { + using (var stream = await ImageSource.GetImageStreamAsync(ct)) + { + Image = new BitmapImage(); + Image.SetSource(stream); + } + } + IsCompleted = true; Debug.WriteLine("prefetch done: " + ImageSource.Name); diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/DeleteStoredFolderCommand.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/DeleteStoredFolderCommand.cs new file mode 100644 index 00000000..95301826 --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/DeleteStoredFolderCommand.cs @@ -0,0 +1,31 @@ +using Prism.Commands; +using System; +using System.Collections.Generic; +using System.Text; +using TsubameViewer.Models.Domain.SourceFolders; + +namespace TsubameViewer.Presentation.ViewModels.PageNavigation.Commands +{ + public sealed class DeleteStoredFolderCommand : DelegateCommandBase + { + private readonly SourceStorageItemsRepository _sourceStorageItemsRepository; + + public DeleteStoredFolderCommand(SourceStorageItemsRepository sourceStorageItemsRepository) + { + _sourceStorageItemsRepository = sourceStorageItemsRepository; + } + + protected override bool CanExecute(object parameter) + { + return parameter is StorageItemViewModel; + } + + protected override void Execute(object parameter) + { + if (parameter is StorageItemViewModel itemVM) + { + _sourceStorageItemsRepository.RemoveFolder(itemVM.Token.TokenString); + } + } + } +} diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenFolderItemCommand.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenFolderItemCommand.cs index 61d7b16a..f35246fb 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenFolderItemCommand.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenFolderItemCommand.cs @@ -1,9 +1,11 @@ -using Microsoft.Xaml.Interactivity; +using Microsoft.Toolkit.Mvvm.Messaging; +using Microsoft.Xaml.Interactivity; using Prism.Commands; using Prism.Navigation; using System; using System.Collections.Generic; using System.Text; +using System.Threading; using System.Windows.Input; using TsubameViewer.Models.Domain; using TsubameViewer.Models.Domain.FolderItemListing; @@ -18,16 +20,19 @@ namespace TsubameViewer.Presentation.ViewModels.PageNavigation.Commands public sealed class OpenFolderItemCommand : DelegateCommandBase { private readonly INavigationService _navigationService; + private readonly IMessenger _messenger; private readonly FolderContainerTypeManager _folderContainerTypeManager; private readonly SourceChoiceCommand _sourceChoiceCommand; public OpenFolderItemCommand( INavigationService navigationService, + IMessenger messenger, FolderContainerTypeManager folderContainerTypeManager, SourceChoiceCommand sourceChoiceCommand ) { _navigationService = navigationService; + _messenger = messenger; _folderContainerTypeManager = folderContainerTypeManager; _sourceChoiceCommand = sourceChoiceCommand; } @@ -41,14 +46,14 @@ protected override async void Execute(object parameter) { if (parameter is StorageItemViewModel item) { - if (item.Type == StorageItemTypes.Image || item.Type == StorageItemTypes.Archive) + if (item.Type is StorageItemTypes.Image or StorageItemTypes.Archive or StorageItemTypes.ArchiveFolder) { var parameters = StorageItemViewModel.CreatePageParameter(item); var result = await _navigationService.NavigateAsync(nameof(Presentation.Views.ImageViewerPage), parameters, new SuppressNavigationTransitionInfo()); } else if (item.Type == StorageItemTypes.Folder) { - var containerType = await _folderContainerTypeManager.GetFolderContainerTypeWithCacheAsync((item.Item as StorageItemImageSource).StorageItem as StorageFolder); + var containerType = await _messenger.WorkWithBusyWallAsync(async ct => await _folderContainerTypeManager.GetFolderContainerTypeWithCacheAsync((item.Item as StorageItemImageSource).StorageItem as StorageFolder, ct), CancellationToken.None); if (containerType == FolderContainerType.Other) { var parameters = StorageItemViewModel.CreatePageParameter(item); diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenFolderItemSecondaryCommand.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenFolderItemSecondaryCommand.cs index 000c271a..edd9b71a 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenFolderItemSecondaryCommand.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenFolderItemSecondaryCommand.cs @@ -1,9 +1,11 @@ -using Microsoft.Xaml.Interactivity; +using Microsoft.Toolkit.Mvvm.Messaging; +using Microsoft.Xaml.Interactivity; using Prism.Commands; using Prism.Navigation; using System; using System.Collections.Generic; using System.Text; +using System.Threading; using System.Windows.Input; using TsubameViewer.Models.Domain; using TsubameViewer.Models.Domain.FolderItemListing; @@ -18,16 +20,19 @@ namespace TsubameViewer.Presentation.ViewModels.PageNavigation.Commands public sealed class OpenFolderItemSecondaryCommand : DelegateCommandBase { private readonly INavigationService _navigationService; + private readonly IMessenger _messenger; private readonly FolderContainerTypeManager _folderContainerTypeManager; private readonly SourceChoiceCommand _sourceChoiceCommand; public OpenFolderItemSecondaryCommand( INavigationService navigationService, + IMessenger messenger, FolderContainerTypeManager folderContainerTypeManager, SourceChoiceCommand sourceChoiceCommand ) { _navigationService = navigationService; + _messenger = messenger; _folderContainerTypeManager = folderContainerTypeManager; _sourceChoiceCommand = sourceChoiceCommand; } @@ -48,7 +53,7 @@ protected override async void Execute(object parameter) } else if (item.Type == StorageItemTypes.Folder) { - var containerType = await _folderContainerTypeManager.GetLatestFolderContainerTypeAndUpdateCacheAsync((item.Item as StorageItemImageSource).StorageItem as StorageFolder); + var containerType = await _messenger.WorkWithBusyWallAsync(async ct => await _folderContainerTypeManager.GetLatestFolderContainerTypeAndUpdateCacheAsync((item.Item as StorageItemImageSource).StorageItem as StorageFolder, ct), CancellationToken.None); if (containerType == FolderContainerType.Other) { var parameters = StorageItemViewModel.CreatePageParameter(item); diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenFolderListupCommand.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenFolderListupCommand.cs index 6ef6159c..5ecb772f 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenFolderListupCommand.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenFolderListupCommand.cs @@ -28,23 +28,11 @@ protected override async void Execute(object parameter) { if (parameter is StorageItemViewModel item) { - if (item.Type == StorageItemTypes.Archive) + if (item.Type is StorageItemTypes.Archive or StorageItemTypes.Folder or StorageItemTypes.ArchiveFolder) { var parameters = StorageItemViewModel.CreatePageParameter(item); var result = await _navigationService.NavigateAsync(nameof(Presentation.Views.FolderListupPage), parameters, new DrillInNavigationTransitionInfo()); } - else if (item.Type == StorageItemTypes.Folder) - { - var parameters = StorageItemViewModel.CreatePageParameter(item); - var result = await _navigationService.NavigateAsync(nameof(Presentation.Views.FolderListupPage), parameters, new DrillInNavigationTransitionInfo()); - } - else if (item.Type == StorageItemTypes.EBook) - { - - } - else if (item.Type == StorageItemTypes.None) - { - } } } } diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenListupCommand.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenListupCommand.cs index 54e028d5..9faa781e 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenListupCommand.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenListupCommand.cs @@ -5,24 +5,35 @@ using System.Text; using TsubameViewer.Models.Domain; using TsubameViewer.Models.Domain.FolderItemListing; +using TsubameViewer.Models.Domain.ImageViewer; using TsubameViewer.Models.Domain.ImageViewer.ImageSource; using Windows.Storage; using Windows.UI.Xaml.Media.Animation; +using Prism.Ioc; using StorageItemTypes = TsubameViewer.Models.Domain.StorageItemTypes; +using System.Threading; +using Uno.Disposables; +using System.Linq; +using System.IO; +using Microsoft.Toolkit.Mvvm.Messaging; +using System.Threading.Tasks; namespace TsubameViewer.Presentation.ViewModels.PageNavigation.Commands { public sealed class OpenListupCommand : DelegateCommandBase { private INavigationService _navigationService; + private readonly IMessenger _messenger; private readonly FolderContainerTypeManager _folderContainerTypeManager; public OpenListupCommand( INavigationService navigationService, + IMessenger messenger, FolderContainerTypeManager folderContainerTypeManager ) { _navigationService = navigationService; + _messenger = messenger; _folderContainerTypeManager = folderContainerTypeManager; } @@ -37,12 +48,55 @@ protected override async void Execute(object parameter) { if (item.Type == StorageItemTypes.Archive) { - var parameters = StorageItemViewModel.CreatePageParameter(item); - var result = await _navigationService.NavigateAsync(nameof(Presentation.Views.FolderListupPage), parameters, new DrillInNavigationTransitionInfo()); + var imageCollectionManager = App.Current.Container.Resolve(); + CancellationToken ct = CancellationToken.None; + var collectionContext = await _messenger.WorkWithBusyWallAsync(ct => imageCollectionManager.GetArchiveImageCollectionContextAsync((item.Item as StorageItemImageSource).StorageItem as StorageFile, null, ct), ct); + try + { + if (await collectionContext.IsExistImageFileAsync(ct)) + { + var parameters = StorageItemViewModel.CreatePageParameter(item); + var result = await _navigationService.NavigateAsync(nameof(Presentation.Views.ImageListupPage), parameters, new DrillInNavigationTransitionInfo()); + } + else + { + var leaves = await collectionContext.GetLeafFoldersAsync(ct); + if (leaves.Count == 0) + { + var parameters = StorageItemViewModel.CreatePageParameter(item); + var result = await _navigationService.NavigateAsync(nameof(Presentation.Views.ImageListupPage), parameters, new DrillInNavigationTransitionInfo()); + } + else if (leaves.Count == 1) + { + var leaf = leaves[0] as ArchiveDirectoryImageSource; + var parameters = StorageItemViewModel.CreateArchiveFolderPageParameter(Uri.EscapeDataString(item.Path), Uri.EscapeDataString(leaf.Path)); + var result = await _navigationService.NavigateAsync(nameof(Presentation.Views.ImageListupPage), parameters, new DrillInNavigationTransitionInfo()); + } + else + { + // 圧縮フォルダにスキップ可能なルートフォルダを含んでいる場合 + var distinct = leaves.Select(x => new string(x.Path.TakeWhile(c => c != Path.DirectorySeparatorChar && c != Path.AltDirectorySeparatorChar).ToArray())).Distinct().ToList(); + if (distinct.Count == 1) + { + var parameters = StorageItemViewModel.CreateArchiveFolderPageParameter(Uri.EscapeDataString(item.Path), Uri.EscapeDataString(distinct[0])); + var result = await _navigationService.NavigateAsync(nameof(Presentation.Views.FolderListupPage), parameters, new DrillInNavigationTransitionInfo()); + } + else + { + var parameters = StorageItemViewModel.CreatePageParameter(item); + var result = await _navigationService.NavigateAsync(nameof(Presentation.Views.FolderListupPage), parameters, new DrillInNavigationTransitionInfo()); + } + } + } + } + finally + { + collectionContext.TryDispose(); + } } else if (item.Type == StorageItemTypes.Folder) { - var containerType = await _folderContainerTypeManager.GetFolderContainerTypeWithCacheAsync((item.Item as StorageItemImageSource).StorageItem as StorageFolder); + var containerType = await _messenger.WorkWithBusyWallAsync(async ct => await _folderContainerTypeManager.GetFolderContainerTypeWithCacheAsync((item.Item as StorageItemImageSource).StorageItem as StorageFolder, ct), CancellationToken.None); if (containerType == FolderContainerType.Other) { var parameters = StorageItemViewModel.CreatePageParameter(item); @@ -54,6 +108,26 @@ protected override async void Execute(object parameter) var result = await _navigationService.NavigateAsync(nameof(Presentation.Views.ImageListupPage), parameters, new SuppressNavigationTransitionInfo()); } } + else if (item.Type == StorageItemTypes.ArchiveFolder) + { + if (item.Item is ArchiveDirectoryImageSource archiveFolderItem) + { + if (archiveFolderItem.IsContainsSubDirectory()) + { + var parameters = StorageItemViewModel.CreatePageParameter(item); + var result = await _navigationService.NavigateAsync(nameof(Presentation.Views.FolderListupPage), parameters, new DrillInNavigationTransitionInfo()); + } + else + { + var parameters = StorageItemViewModel.CreatePageParameter(item); + var result = await _navigationService.NavigateAsync(nameof(Presentation.Views.ImageListupPage), parameters, new DrillInNavigationTransitionInfo()); + } + } + else + { + throw new NotSupportedException(); + } + } else if (item.Type == StorageItemTypes.EBook) { diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenWithExternalApplicationCommand.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenWithExternalApplicationCommand.cs new file mode 100644 index 00000000..29e36a0f --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation.Commands/OpenWithExternalApplicationCommand.cs @@ -0,0 +1,37 @@ +using Prism.Commands; +using System; +using System.Collections.Generic; +using System.Text; +using TsubameViewer.Models.Domain.ImageViewer.ImageSource; +using Windows.Storage; +using Windows.System; + +namespace TsubameViewer.Presentation.ViewModels.PageNavigation.Commands +{ + public sealed class OpenWithExternalApplicationCommand : DelegateCommandBase + { + protected override bool CanExecute(object parameter) + { + return parameter is StorageItemViewModel item + && item.Type is not Models.Domain.StorageItemTypes.Folder and not Models.Domain.StorageItemTypes.None + ; + } + + protected override void Execute(object parameter) + { + if (parameter is StorageItemViewModel itemVM) + { + if (itemVM.Type is Models.Domain.StorageItemTypes.Image + && itemVM.Item is ArchiveEntryImageSource or PdfPageImageSource) + { + return; + } + + if (itemVM.Item.StorageItem is StorageFile file) + { + _ = Launcher.LaunchFileAsync(file, new LauncherOptions() { DisplayApplicationPicker = true }); + } + } + } + } +} diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation/BusyWallRequestEvent.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation/BusyWallRequestEvent.cs new file mode 100644 index 00000000..0838e09d --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation/BusyWallRequestEvent.cs @@ -0,0 +1,90 @@ +using Microsoft.Toolkit.Mvvm.Messaging; +using Microsoft.Toolkit.Mvvm.Messaging.Messages; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace TsubameViewer.Presentation.ViewModels.PageNavigation +{ + public class BusyWallStartRequestMessageData + { + } + + public sealed class BusyWallStartRequestMessage : ValueChangedMessage + { + public BusyWallStartRequestMessage() : base(new BusyWallStartRequestMessageData()) + { + } + } + + public class BusyWallExitRequestMessageData + { + } + + public sealed class BusyWallExitRequestMessage : ValueChangedMessage + { + public BusyWallExitRequestMessage() : base(new BusyWallExitRequestMessageData()) + { + } + } + + public class BusyWallCanceledMessageData + { + } + + public sealed class BusyWallCanceledMessage : ValueChangedMessage + { + public BusyWallCanceledMessage() : base(new BusyWallCanceledMessageData()) + { + } + } + + + public static class BusyWallCanceledMessageExtensions + { + public static async Task WorkWithBusyWallAsync(this IMessenger messenger, Func> action, CancellationToken actionCt) + { + object dummy = new object(); + using (var manualCts = new CancellationTokenSource()) + using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(manualCts.Token, actionCt)) + { + var ct = linkedCts.Token; + messenger.Register(dummy, (r, m) => { manualCts.Cancel(); }); + try + { + messenger.Send(); + return await action(ct); + } + finally + { + messenger.Send(); + messenger.Unregister(dummy); + } + } + } + + public static async Task WorkWithBusyWallAsync(this IMessenger messenger, Func action, CancellationToken actionCt) + { + object dummy = new object(); + using (var manualCts = new CancellationTokenSource()) + using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(manualCts.Token, actionCt)) + { + var ct = linkedCts.Token; + messenger.Register(dummy, (r, m) => { manualCts.Cancel(); }); + try + { + messenger.Send(); + await action(ct); + } + finally + { + messenger.Send(); + messenger.Unregister(dummy); + } + } + } + } +} diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation/PageNavigationConstants.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation/PageNavigationConstants.cs index 37e15033..63455ae4 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation/PageNavigationConstants.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation/PageNavigationConstants.cs @@ -8,6 +8,7 @@ public static class PageNavigationConstants { public const string Path = "path"; public const string PageName = "pageName"; + public const string ArchiveFolderName = "archiveFolderName"; public const string Restored = "__restored"; } diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation/StorageItemViewModel.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation/StorageItemViewModel.cs index ae4f08a8..d0a35c0b 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation/StorageItemViewModel.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PageNavigation/StorageItemViewModel.cs @@ -17,11 +17,17 @@ using Windows.Storage; using Windows.Storage.AccessCache; using Windows.UI.Xaml.Media.Imaging; +using Uno.Disposables; namespace TsubameViewer.Presentation.ViewModels.PageNavigation { using StorageItemTypes = TsubameViewer.Models.Domain.StorageItemTypes; + public record StorageItemToken(string RootItemPath, string TokenString) + { + } + + public sealed class StorageItemViewModel : BindableBase, IDisposable { #region Navigation Parameters @@ -33,12 +39,23 @@ public static NavigationParameters CreatePageParameter(StorageItemViewModel vm) { return new NavigationParameters((PageNavigationConstants.Path, escapedPath), (PageNavigationConstants.PageName, Uri.EscapeDataString(vm.Name))); } + else if (vm.Type == StorageItemTypes.ArchiveFolder) + { + var archiveFolderImageSource = vm.Item as ArchiveDirectoryImageSource; + + return CreateArchiveFolderPageParameter(escapedPath, Uri.EscapeDataString(archiveFolderImageSource.Path)); + } else { return new NavigationParameters((PageNavigationConstants.Path, escapedPath)); } } + public static NavigationParameters CreateArchiveFolderPageParameter(string filePath, string archiveFolderPath) + { + return new NavigationParameters((PageNavigationConstants.Path, filePath), (PageNavigationConstants.ArchiveFolderName, archiveFolderPath)); + } + #endregion @@ -49,7 +66,7 @@ public static NavigationParameters CreatePageParameter(StorageItemViewModel vm) public IImageSource Item { get; } - public string Token { get; } + public StorageItemToken Token { get; } public string Name { get; } @@ -69,8 +86,7 @@ public BitmapImage Image public StorageItemTypes Type { get; } private CancellationTokenSource _cts = new CancellationTokenSource(); - private static SemaphoreSlim _loadinLock = new SemaphoreSlim(3, 3); - + private static FastAsyncLock _imageLoadingLock = new FastAsyncLock(); private double _ReadParcentage; public double ReadParcentage @@ -98,7 +114,7 @@ public StorageItemViewModel(SourceStorageItemsRepository sourceStorageItemsRepos public StorageItemViewModel() { } - public StorageItemViewModel(IImageSource item, string token, SourceStorageItemsRepository sourceStorageItemsRepository, FolderListingSettings folderListingSettings, BookmarkManager bookmarkManager) + public StorageItemViewModel(IImageSource item, StorageItemToken token, SourceStorageItemsRepository sourceStorageItemsRepository, FolderListingSettings folderListingSettings, BookmarkManager bookmarkManager) : this(sourceStorageItemsRepository, folderListingSettings, bookmarkManager) { Item = item; @@ -134,9 +150,7 @@ public void ClearImage() public void StopImageLoading() { - _cts?.Cancel(); - _cts?.Dispose(); - _cts = new CancellationTokenSource(); + // do nothing. } private bool _isAppearingRequestButLoadingCancelled; @@ -144,16 +158,18 @@ public void StopImageLoading() bool _isInitialized = false; public async void Initialize() { - if (Item == null) { return; } - if (_isInitialized) { return; } - - if (Type == StorageItemTypes.Image && !_folderListingSettings.IsImageFileThumbnailEnabled) { return; } - if (Type == StorageItemTypes.Archive && !_folderListingSettings.IsArchiveFileThumbnailEnabled) { return; } - if (Type == StorageItemTypes.Folder && !_folderListingSettings.IsFolderThumbnailEnabled) { return; } - var ct = _cts.Token; try { + using var _ = await _imageLoadingLock.LockAsync(ct); + + if (Item == null) { return; } + if (_isInitialized) { return; } + + if (Type == StorageItemTypes.Image && !_folderListingSettings.IsImageFileThumbnailEnabled) { return; } + if (Type == StorageItemTypes.Archive && !_folderListingSettings.IsArchiveFileThumbnailEnabled) { return; } + if (Type == StorageItemTypes.Folder && !_folderListingSettings.IsFolderThumbnailEnabled) { return; } + if (ct.IsCancellationRequested) { _isAppearingRequestButLoadingCancelled = true; @@ -162,24 +178,18 @@ public async void Initialize() ct.ThrowIfCancellationRequested(); - await _loadinLock.WaitAsync(ct); - - try + _isAppearingRequestButLoadingCancelled = false; + using (var stream = await Task.Run(async () => await Item.GetThumbnailImageStreamAsync(ct))) { - ct.ThrowIfCancellationRequested(); - - _isAppearingRequestButLoadingCancelled = false; - Image = await Item.GenerateThumbnailBitmapImageAsync(ct); + if (stream is null || stream.Size == 0) { return; } - await Task.Delay(10); - } - finally - { - _loadinLock.Release(); + var bitmapImage = new BitmapImage(); + bitmapImage.AutoPlay = false; + //bitmapImage.DecodePixelHeight = Models.Domain.FolderItemListing.ListingImageConstants.LargeFileThumbnailImageHeight; + await bitmapImage.SetSourceAsync(stream).AsTask(ct); + Image = bitmapImage; } - Debug.WriteLine("Thumbnail Load: " + Name); - _isInitialized = true; } catch (OperationCanceledException) @@ -211,8 +221,16 @@ public void RestoreThumbnailLoadingTask() } } + public void ThumbnailChanged() + { + Image = null; + _isInitialized = false; + } + public void Dispose() { + Item.TryDispose(); + Image = null; _cts?.Cancel(); _cts?.Dispose(); } diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PrimaryWindowCoreLayoutViewModel.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PrimaryWindowCoreLayoutViewModel.cs index a9614b31..589efeee 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PrimaryWindowCoreLayoutViewModel.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/PrimaryWindowCoreLayoutViewModel.cs @@ -1,4 +1,5 @@ -using Prism.Commands; +using Microsoft.Toolkit.Mvvm.Messaging; +using Prism.Commands; using Prism.Events; using Prism.Mvvm; using Prism.Navigation; @@ -38,6 +39,7 @@ public sealed class PrimaryWindowCoreLayoutViewModel : BindableBase public INavigationService NavigationService => _navigationServiceLazy.Value; private readonly Lazy _navigationServiceLazy; private readonly IScheduler _scheduler; + private readonly IMessenger _messenger; private readonly PathReferenceCountManager _PathReferenceCountManager; private readonly FolderContainerTypeManager _folderContainerTypeManager; private readonly StorageItemSearchManager _storageItemSearchManager; @@ -50,6 +52,7 @@ public PrimaryWindowCoreLayoutViewModel( [Dependency("PrimaryWindowNavigationService")] Lazy navigationServiceLazy, IEventAggregator eventAggregator, IScheduler scheduler, + IMessenger messenger, ApplicationSettings applicationSettings, RestoreNavigationManager restoreNavigationManager, SourceStorageItemsRepository sourceStorageItemsRepository, @@ -69,6 +72,7 @@ OpenPageCommand openPageCommand _navigationServiceLazy = navigationServiceLazy; EventAggregator = eventAggregator; _scheduler = scheduler; + _messenger = messenger; ApplicationSettings = applicationSettings; RestoreNavigationManager = restoreNavigationManager; SourceStorageItemsRepository = sourceStorageItemsRepository; @@ -175,7 +179,8 @@ async void ExecuteSuggestChosenCommand(StorageItemSearchEntry entry) if (storageItem is StorageFolder itemFolder) { - if (await _folderContainerTypeManager.GetFolderContainerTypeWithCacheAsync(itemFolder) == FolderContainerType.OnlyImages) + var containerType = await _messenger.WorkWithBusyWallAsync(async ct => await _folderContainerTypeManager.GetFolderContainerTypeWithCacheAsync(itemFolder, ct), CancellationToken.None); + if (containerType == FolderContainerType.OnlyImages) { await NavigationService.NavigateAsync(nameof(Presentation.Views.ImageViewerPage), parameters, new SuppressNavigationTransitionInfo()); return; diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/SettingsPageViewModel.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/SettingsPageViewModel.cs index ab498096..4067006c 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/SettingsPageViewModel.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/SettingsPageViewModel.cs @@ -1,4 +1,5 @@ using I18NPortable; +using Microsoft.Toolkit.Mvvm.Messaging; using Prism.Commands; using Prism.Events; using Prism.Mvvm; @@ -33,6 +34,7 @@ namespace TsubameViewer.Presentation.ViewModels public sealed class SettingsPageViewModel : ViewModelBase, IDisposable { private readonly IEventAggregator _eventAggregator; + private readonly IMessenger _messenger; private readonly ApplicationSettings _applicationSettings; private readonly FolderListingSettings _folderListingSettings; private readonly SourceStorageItemsRepository _sourceStorageItemsRepository; @@ -45,6 +47,7 @@ public sealed class SettingsPageViewModel : ViewModelBase, IDisposable public SettingsPageViewModel( IEventAggregator eventAggregator, + IMessenger messenger, ApplicationSettings applicationSettings, FolderListingSettings folderListingSettings, SourceStorageItemsRepository sourceStorageItemsRepository, @@ -53,6 +56,7 @@ ThumbnailManager thumbnailManager ) { _eventAggregator = eventAggregator; + _messenger = messenger; _applicationSettings = applicationSettings; _folderListingSettings = folderListingSettings; _sourceStorageItemsRepository = sourceStorageItemsRepository; diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/Sorting/TitleDigitCompletionComparer.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/Sorting/TitleDigitCompletionComparer.cs new file mode 100644 index 00000000..d2854c3c --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/Sorting/TitleDigitCompletionComparer.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using TsubameViewer.Models.Domain.ImageViewer; +using TsubameViewer.Presentation.ViewModels.PageNavigation; + +namespace TsubameViewer.Presentation.ViewModels.Sorting +{ + public sealed class TitleDigitCompletionComparer : IComparer, IComparer + { + public static readonly TitleDigitCompletionComparer Default = new TitleDigitCompletionComparer(); + private TitleDigitCompletionComparer() { } + + public static int ComparePath(string x, string y) + { + var xDictPath = Path.GetDirectoryName(x); + var yDictPath = Path.GetDirectoryName(y); + + if (xDictPath != yDictPath) + { + return String.CompareOrdinal(x, y); + } + + static bool TryGetPageNumber(string name, out int pageNumber) + { + int keta = 1; + int number = 0; + foreach (var i in name.Reverse().SkipWhile(c => !char.IsDigit(c)).TakeWhile(c => char.IsDigit(c)).Select(x => x - '0')) + { + number += i * keta; + keta *= 10; + } + + pageNumber = number; + return number > 0; + } + + var xName = Path.GetFileNameWithoutExtension(x); + if (!TryGetPageNumber(xName, out int xPageNumber)) { return String.CompareOrdinal(x, y); } + + var yName = Path.GetFileNameWithoutExtension(y); + if (!TryGetPageNumber(yName, out int yPageNumber)) { return String.CompareOrdinal(x, y); } + + return xPageNumber - yPageNumber; + } + + + public int Compare(string x, string y) + { + return TitleDigitCompletionComparer.ComparePath(x, y); + } + + public int Compare(object x, object y) + { + return TitleDigitCompletionComparer.ComparePath(x as string, y as string); + } + + } + + public sealed class ImageSourceTitleDigitCompletionComparer : IComparer + { + public static readonly ImageSourceTitleDigitCompletionComparer Default = new ImageSourceTitleDigitCompletionComparer(); + private ImageSourceTitleDigitCompletionComparer() { } + public int Compare(IImageSource x, IImageSource y) + { + return TitleDigitCompletionComparer.ComparePath(x.Path, y.Path); + } + } +} diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/SourceFolders.Commands/ChangeStorageItemThumbnailImageCommand.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/SourceFolders.Commands/ChangeStorageItemThumbnailImageCommand.cs new file mode 100644 index 00000000..ac10eecb --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/SourceFolders.Commands/ChangeStorageItemThumbnailImageCommand.cs @@ -0,0 +1,81 @@ +using Prism.Commands; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using TsubameViewer.Models.Domain.FolderItemListing; +using TsubameViewer.Models.Domain.ImageViewer; +using TsubameViewer.Models.Domain.ImageViewer.ImageSource; +using TsubameViewer.Models.Domain.SourceFolders; +using TsubameViewer.Presentation.ViewModels.PageNavigation; + +namespace TsubameViewer.Presentation.ViewModels.SourceFolders.Commands +{ + public sealed class ChangeStorageItemThumbnailImageCommand : DelegateCommandBase + { + private readonly ThumbnailManager _thumbnailManager; + private readonly SourceStorageItemsRepository _sourceStorageItemsRepository; + + public ChangeStorageItemThumbnailImageCommand( + ThumbnailManager thumbnailManager, + SourceStorageItemsRepository sourceStorageItemsRepository + ) + { + _thumbnailManager = thumbnailManager; + _sourceStorageItemsRepository = sourceStorageItemsRepository; + } + + protected override bool CanExecute(object parameter) + { + return parameter is StorageItemViewModel; + } + + protected override async void Execute(object parameter) + { + if (parameter is StorageItemViewModel item) + { + using (var stream = await item.Item.GetThumbnailImageStreamAsync()) + { + if (item.Item is ArchiveEntryImageSource archiveEntry) + { + var parentDirectoryArchiveEntry = archiveEntry.GetParentDirectoryEntry(); + if (parentDirectoryArchiveEntry == null) + { + await _thumbnailManager.SetThumbnailAsync(archiveEntry.StorageItem, stream, default); + } + else + { + await _thumbnailManager.SetArchiveEntryThumbnailAsync(archiveEntry.StorageItem, parentDirectoryArchiveEntry, stream, default); + } + } + else if (item.Item is PdfPageImageSource pdf) + { + await _thumbnailManager.SetThumbnailAsync(pdf.StorageItem, stream, default); + } + else if (item.Item is StorageItemImageSource folderItem) + { + var folder = await _sourceStorageItemsRepository.GetStorageItemFromPath(item.Token.TokenString, Path.GetDirectoryName(folderItem.Path)); + if (folder == null) { throw new InvalidOperationException(); } + await _thumbnailManager.SetThumbnailAsync(folder, stream, default); + } + else if (item.Item is ArchiveDirectoryImageSource archiveDirectoryItem) + { + var parentDirectoryArchiveEntry = archiveDirectoryItem.GetParentDirectoryEntry(); + if (parentDirectoryArchiveEntry == null) + { + await _thumbnailManager.SetThumbnailAsync(archiveDirectoryItem.StorageItem, stream, default); + } + else + { + await _thumbnailManager.SetArchiveEntryThumbnailAsync(archiveDirectoryItem.StorageItem, parentDirectoryArchiveEntry, stream, default); + } + } + else + { + throw new NotSupportedException(); + } + } + } + } + } +} diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/SourceStorageItemsPageViewModel.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/SourceStorageItemsPageViewModel.cs index 2bbfd5c5..d1f15ca5 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/SourceStorageItemsPageViewModel.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.ViewModels/SourceStorageItemsPageViewModel.cs @@ -26,6 +26,7 @@ using TsubameViewer.Presentation.Services.UWP; using Uno.Extensions; using TsubameViewer.Models.Domain; +using TsubameViewer.Models.Domain.RestoreNavigation; #if WINDOWS_UWP using Windows.Storage.AccessCache; #endif @@ -44,6 +45,7 @@ public sealed class SourceStorageItemsPageViewModel : ViewModelBase, IDisposable private readonly PathReferenceCountManager _PathReferenceCountManager; private readonly FolderListingSettings _folderListingSettings; private readonly SourceStorageItemsRepository _sourceStorageItemsRepository; + private readonly FolderLastIntractItemManager _folderLastIntractItemManager; private readonly RecentlyAccessManager _recentlyAccessManager; private readonly IEventAggregator _eventAggregator; @@ -71,6 +73,7 @@ public SourceStorageItemsPageViewModel( ThumbnailManager thumbnailManager, PathReferenceCountManager PathReferenceCountManager, SourceStorageItemsRepository sourceStorageItemsRepository, + FolderLastIntractItemManager folderLastIntractItemManager, RecentlyAccessManager recentlyAccessManager, SecondaryTileManager secondaryTileManager, SourceChoiceCommand sourceChoiceCommand, @@ -90,6 +93,7 @@ SecondaryTileRemoveCommand secondaryTileRemoveCommand OpenFolderItemSecondaryCommand = openFolderItemSecondaryCommand; SourceChoiceCommand = sourceChoiceCommand; _sourceStorageItemsRepository = sourceStorageItemsRepository; + _folderLastIntractItemManager = folderLastIntractItemManager; _recentlyAccessManager = recentlyAccessManager; SecondaryTileManager = secondaryTileManager; _eventAggregator = eventAggregator; @@ -122,13 +126,13 @@ SecondaryTileRemoveCommand secondaryTileRemoveCommand .Subscribe(args => { - var existInFolders = Folders.FirstOrDefault(x => x.Token == args.Token); + var existInFolders = Folders.Skip(1).FirstOrDefault(x => x.Token.TokenString == args.Token); if (existInFolders != null) { Folders.Remove(existInFolders); } - var existInFiles = RecentlyItems.FirstOrDefault(x => x.Token == args.Token); + var existInFiles = RecentlyItems.FirstOrDefault(x => x.Token.TokenString == args.Token); if (existInFiles != null) { RecentlyItems.Remove(existInFiles); @@ -138,14 +142,14 @@ SecondaryTileRemoveCommand secondaryTileRemoveCommand if (storageItemImageSource.ItemTypes == Models.Domain.StorageItemTypes.Folder) { // 追加用ボタンの次に配置するための 1 - Folders.Insert(1, new StorageItemViewModel(storageItemImageSource, args.Token, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager)); + Folders.Insert(1, new StorageItemViewModel(storageItemImageSource, new StorageItemToken(args.StorageItem.Path, args.Token), _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager)); } else if (storageItemImageSource.ItemTypes == Models.Domain.StorageItemTypes.Image || storageItemImageSource.ItemTypes == Models.Domain.StorageItemTypes.Archive || storageItemImageSource.ItemTypes == Models.Domain.StorageItemTypes.EBook ) { - RecentlyItems.Insert(0, new StorageItemViewModel(storageItemImageSource, args.Token, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager)); + RecentlyItems.Insert(0, new StorageItemViewModel(storageItemImageSource, new StorageItemToken(args.StorageItem.Path, args.Token), _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager)); } }) .AddTo(_disposables); @@ -153,13 +157,13 @@ SecondaryTileRemoveCommand secondaryTileRemoveCommand _eventAggregator.GetEvent() .Subscribe(args => { - var existInFolders = Folders.FirstOrDefault(x => x.Token == args.Token); + var existInFolders = Folders.Skip(1).FirstOrDefault(x => x.Token.TokenString == args.Token); if (existInFolders != null) { Folders.Remove(existInFolders); } - var existInFiles = RecentlyItems.Where(x => x.Token == args.Token).ToList(); + var existInFiles = RecentlyItems.Where(x => x.Token.TokenString == args.Token).ToList(); foreach (var item in existInFiles) { RecentlyItems.Remove(item); @@ -183,7 +187,7 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) var storageItemImageSource = new StorageItemImageSource(item.item, _thumbnailManager); if (storageItemImageSource.ItemTypes == Models.Domain.StorageItemTypes.Folder) { - Folders.Add(new StorageItemViewModel(storageItemImageSource, item.token, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager)); + Folders.Add(new StorageItemViewModel(storageItemImageSource, new StorageItemToken(item.item.Path, item.token), _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager)); } else { @@ -194,15 +198,21 @@ public override async Task OnNavigatedToAsync(INavigationParameters parameters) else { Folders.ForEach(x => x.UpdateLastReadPosition()); + + var lastIntaractItemPath = _folderLastIntractItemManager.GetLastIntractItemName(nameof(SourceStorageItemsPageViewModel)); + Folders.Where(x => x.Name == lastIntaractItemPath).ForEach(x => + { + x.ThumbnailChanged(); + x.Initialize(); + }); } async Task ToStorageItemViewModel(RecentlyAccessManager.RecentlyAccessEntry entry) { - //_currentFolderItem = var token = _PathReferenceCountManager.GetToken(entry.Path); var storageItem = await _sourceStorageItemsRepository.GetStorageItemFromPath(token, entry.Path); var storageItemImageSource = new StorageItemImageSource(storageItem, _thumbnailManager); - return new StorageItemViewModel(storageItemImageSource, token, _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); + return new StorageItemViewModel(storageItemImageSource, new StorageItemToken(storageItem.Path, token), _sourceStorageItemsRepository, _folderListingSettings, _bookmarkManager); } var recentlyAccessItems = _recentlyAccessManager.GetItemsSortWithRecently(10); @@ -262,6 +272,11 @@ public override void OnNavigatedFrom(INavigationParameters parameters) { _navigationDisposables?.Dispose(); + if (parameters.TryGetValue(PageNavigationConstants.Path, out string path)) + { + _folderLastIntractItemManager.SetLastIntractItemName(nameof(SourceStorageItemsPageViewModel), Uri.UnescapeDataString(path)); + } + base.OnNavigatedFrom(parameters); } @@ -269,14 +284,6 @@ public void Dispose() { ((IDisposable)_disposables).Dispose(); } - - - private DelegateCommand _DeleteStoredFolderCommand; - public DelegateCommand DeleteStoredFolderCommand => - _DeleteStoredFolderCommand ??= new DelegateCommand(async (itemVM) => - { - _sourceStorageItemsRepository.RemoveFolder(itemVM.Token); - }); } public sealed class SourceItemsGroup diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/Behaviors/ListViewContainerChangeTriggerBehavior.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/Behaviors/ListViewContainerChangeTriggerBehavior.cs index 24e3c9de..ba31548f 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/Behaviors/ListViewContainerChangeTriggerBehavior.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/Behaviors/ListViewContainerChangeTriggerBehavior.cs @@ -34,26 +34,16 @@ protected override void OnAttached() if (AssociatedObject != null) { AssociatedObject.ChoosingItemContainer += AssociatedObject_ChoosingItemContainer; - AssociatedObject.ContainerContentChanging += AssociatedObject_ContainerContentChanging; } base.OnAttached(); } - private void AssociatedObject_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) - { - if (args.Item is ViewModels.PageNavigation.StorageItemViewModel itemVM) - { - System.Diagnostics.Debug.WriteLine($"phase: {args.Phase}, {itemVM.Name}"); - } - } - protected override void OnDetaching() { if (AssociatedObject != null) { AssociatedObject.ChoosingItemContainer -= AssociatedObject_ChoosingItemContainer; - AssociatedObject.ContainerContentChanging -= AssociatedObject_ContainerContentChanging; } _map.Clear(); base.OnDetaching(); diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/Flyouts/StorageItemMenuFlyout.xaml b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/Flyouts/StorageItemMenuFlyout.xaml new file mode 100644 index 00000000..2ccec3b3 --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/Flyouts/StorageItemMenuFlyout.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/Flyouts/StorageItemMenuFlyout.xaml.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/Flyouts/StorageItemMenuFlyout.xaml.cs new file mode 100644 index 00000000..b7b74c2f --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/Flyouts/StorageItemMenuFlyout.xaml.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using TsubameViewer.Models.Domain.ImageViewer.ImageSource; +using TsubameViewer.Presentation.ViewModels.PageNavigation; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; +using Prism.Ioc; +using TsubameViewer.Presentation.ViewModels.PageNavigation.Commands; +using TsubameViewer.Presentation.Services.UWP; +using TsubameViewer.Presentation.Views.Helpers; +using TsubameViewer.Presentation.ViewModels.SourceFolders.Commands; +using Windows.Storage; + +// ユーザー コントロールの項目テンプレートについては、https://go.microsoft.com/fwlink/?LinkId=234236 を参照してください + +namespace TsubameViewer.Presentation.Views.Flyouts +{ + public sealed partial class StorageItemMenuFlyout : MenuFlyout + { + public StorageItemMenuFlyout() + { + this.InitializeComponent(); + + var container = App.Current.Container; + OpenListupItem.Command = container.Resolve(); + SetThumbnailImageMenuItem.Command = container.Resolve(); + AddSecondaryTile.Command = container.Resolve(); + RemoveSecondaryTile.Command = container.Resolve(); + OpenWithExplorerItem.Command = container.Resolve(); + OpenWithExternalAppMenuItem.Command = container.Resolve(); + RemoveSourceStorageItem.Command = container.Resolve(); + _secondaryTileManager = container.Resolve(); + } + + private SecondaryTileManager _secondaryTileManager; + + private void FolderAndArchiveMenuFlyout_Opened(object sender, object e) + { + var flyout = sender as FlyoutBase; + + StorageItemViewModel itemVM = flyout.Target.DataContext as StorageItemViewModel; + if (itemVM == null && flyout.Target is Control content) + { + itemVM = (content as ContentControl)?.Content as StorageItemViewModel; + } + + if (itemVM == null) + { + flyout.Hide(); + return; + } + + if (itemVM.Item is StorageItemImageSource or ArchiveEntryImageSource or PdfPageImageSource) + { + NoActionDescMenuItem.Visibility = Visibility.Collapsed; + + bool isSourceStorageItem = (itemVM.Token.RootItemPath == itemVM.Path); + + OpenListupItem.CommandParameter = itemVM; + OpenListupItem.Visibility = (itemVM.Type == Models.Domain.StorageItemTypes.Archive || itemVM.Type == Models.Domain.StorageItemTypes.Folder).TrueToVisible(); + + SetThumbnailImageMenuItem.CommandParameter = itemVM; + SetThumbnailImageMenuItem.Visibility = (isSourceStorageItem is false && itemVM.Type is Models.Domain.StorageItemTypes.Image or Models.Domain.StorageItemTypes.Folder or Models.Domain.StorageItemTypes.Archive).TrueToVisible(); + + FolderAndArchiveMenuSeparator1.Visibility = OpenListupItem.Visibility; + + if (itemVM.Path is not null) + { + bool isExistTile = _secondaryTileManager.ExistTile(itemVM.Path); + AddSecondaryTile.CommandParameter = itemVM; + AddSecondaryTile.Visibility = isExistTile.FalseToVisible(); + + RemoveSecondaryTile.CommandParameter = itemVM; + RemoveSecondaryTile.Visibility = isExistTile.TrueToVisible(); + } + else + { + AddSecondaryTile.Visibility = Visibility.Collapsed; + RemoveSecondaryTile.Visibility = Visibility.Collapsed; + } + + FolderAndArchiveMenuSeparator2.Visibility = Visibility.Visible; + + OpenWithExplorerItem.CommandParameter = itemVM; + OpenWithExplorerItem.Visibility = (itemVM.Item is StorageItemImageSource).TrueToVisible(); + + OpenWithExternalAppMenuItem.CommandParameter = itemVM; + OpenWithExternalAppMenuItem.Visibility = (itemVM.Item is StorageItemImageSource item && item.StorageItem is StorageFile).TrueToVisible(); + + RemoveSourceStorageItem.CommandParameter = itemVM; + SourceManageSeparetor.Visibility = isSourceStorageItem.TrueToVisible(); + SourceManageSubItem.Visibility = isSourceStorageItem.TrueToVisible(); + } + else if (itemVM.Item is ArchiveDirectoryImageSource) + { + NoActionDescMenuItem.Visibility = Visibility.Collapsed; + + bool isSourceStorageItem = (itemVM.Token.RootItemPath == itemVM.Path); + + OpenListupItem.CommandParameter = itemVM; + OpenListupItem.Visibility = Visibility.Visible; + + SetThumbnailImageMenuItem.CommandParameter = itemVM; + SetThumbnailImageMenuItem.Visibility = Visibility.Visible; + + FolderAndArchiveMenuSeparator1.Visibility = OpenListupItem.Visibility; + + AddSecondaryTile.Visibility = Visibility.Collapsed; + RemoveSecondaryTile.Visibility = Visibility.Collapsed; + + FolderAndArchiveMenuSeparator2.Visibility = Visibility.Collapsed; + + OpenWithExplorerItem.CommandParameter = itemVM; + OpenWithExplorerItem.Visibility = Visibility.Collapsed; + + OpenWithExternalAppMenuItem.CommandParameter = itemVM; + OpenWithExternalAppMenuItem.Visibility = Visibility.Collapsed; + + RemoveSourceStorageItem.CommandParameter = itemVM; + SourceManageSeparetor.Visibility = Visibility.Collapsed; + SourceManageSubItem.Visibility = Visibility.Collapsed; + } + else + { + NoActionDescMenuItem.Visibility = Visibility.Visible; + + OpenListupItem.Visibility = Visibility.Collapsed; + SetThumbnailImageMenuItem.Visibility = Visibility.Collapsed; + AddSecondaryTile.Visibility = Visibility.Collapsed; + RemoveSecondaryTile.Visibility = Visibility.Collapsed; + OpenWithExplorerItem.Visibility = Visibility.Collapsed; + OpenWithExternalAppMenuItem.Visibility = Visibility.Collapsed; + FolderAndArchiveMenuSeparator1.Visibility = Visibility.Collapsed; + FolderAndArchiveMenuSeparator2.Visibility = Visibility.Collapsed; + SourceManageSeparetor.Visibility = Visibility.Collapsed; + SourceManageSubItem.Visibility = Visibility.Collapsed; + } + } + } +} diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/FolderListup/FolderListupItemTemplate.xaml b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/FolderListup/FolderListupItemTemplate.xaml index 8df46e48..dcfc003d 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/FolderListup/FolderListupItemTemplate.xaml +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/FolderListup/FolderListupItemTemplate.xaml @@ -43,7 +43,7 @@ Style="{StaticResource FolderItemImageStyle}" /> - + - + @@ -113,6 +113,24 @@ + + + + + + + + + + + + + + + @@ -144,6 +162,7 @@ Folder="{StaticResource FolderTemplate}" AddNewFolder="{StaticResource AddNewFolderTemplate}" Archive="{StaticResource ArchiveFileTemplate}" + ArchiveFolder="{StaticResource ArchiveFolderTemplate}" Image="{StaticResource ImageFileTemplate}" EBook="{StaticResource EBookFileTemplate}" > diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/FolderListupPage.xaml b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/FolderListupPage.xaml index 060ef6be..b7f7c625 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/FolderListupPage.xaml +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/FolderListupPage.xaml @@ -11,104 +11,180 @@ xmlns:core="using:Microsoft.Xaml.Interactions.Core" xmlns:myBehaior="using:TsubameViewer.Presentation.Views.Behaviors" xmlns:wst="using:WindowsStateTriggers" - xmlns:models ="using:TsubameViewer.Models.Domain.FolderItemListing" + xmlns:folderModels ="using:TsubameViewer.Models.Domain.FolderItemListing" xmlns:winUI="using:Microsoft.UI.Xaml.Controls" xmlns:uiNavigation="using:TsubameViewer.Presentation.Views.UINavigation" xmlns:uwpUIExtensions="using:Microsoft.Toolkit.Uwp.UI.Extensions" xmlns:vm="using:TsubameViewer.Presentation.ViewModels.PageNavigation" - xmlns:i18nExt="using:I18NPortable.Xaml.Extensions" + xmlns:i18nExt="using:I18NPortable.Xaml.Extensions" xmlns:models="using:TsubameViewer.Models.Domain" mc:Ignorable="d" Background="{ThemeResource ApplicationContentBackgroundBrush}" > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + + + + - - - - - + + + + + : + + + + + + + UpdateTimeDescThenTitleAsc + + + + + UpdateTimeDecending + + + + + UpdateTimeAscending + + + + + TitleAscending + + + + + TitleDecending + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -116,16 +192,33 @@ + + + + + + + + + + + + + + + + - + + @@ -156,6 +249,7 @@ + @@ -164,6 +258,7 @@ + @@ -172,6 +267,7 @@ + @@ -180,6 +276,7 @@ + @@ -188,6 +285,7 @@ + diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/FolderListupPage.xaml.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/FolderListupPage.xaml.cs index 295f007d..54d17ea1 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/FolderListupPage.xaml.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/FolderListupPage.xaml.cs @@ -119,74 +119,11 @@ private void FoldersAdaptiveGridView_ContextRequested(UIElement sender, ContextR flyout.ShowAt(args.OriginalSource as FrameworkElement); } - private void FolderAndArchiveMenuFlyout_Opened(object sender, object e) + public void DeselectItem() { - var flyout = sender as FlyoutBase; - var pageVM = DataContext as FolderListupPageViewModel; - - StorageItemViewModel itemVM = flyout.Target.DataContext as StorageItemViewModel; - if (itemVM == null && flyout.Target is Control content) - { - itemVM = (content as ContentControl)?.Content as StorageItemViewModel; - } - - if (itemVM == null) - { - flyout.Hide(); - return; - } - - if (itemVM.Item is StorageItemImageSource == false) - { - NoActionDescMenuItem.Visibility = Visibility.Visible; - - OpenListupItem.Visibility = Visibility.Collapsed; - AddSecondaryTile.Visibility = Visibility.Collapsed; - RemoveSecondaryTile.Visibility = Visibility.Collapsed; - OpenWithExplorerItem.Visibility = Visibility.Collapsed; - FolderAndArchiveMenuSeparator1.Visibility = Visibility.Collapsed; - FolderAndArchiveMenuSeparator2.Visibility = Visibility.Collapsed; - } - else - { - OpenListupItem.CommandParameter = itemVM; - OpenListupItem.Command = pageVM.OpenFolderItemSecondaryCommand; - OpenListupItem.Visibility = (itemVM.Type == Models.Domain.StorageItemTypes.Archive || itemVM.Type == Models.Domain.StorageItemTypes.Folder) - ? Visibility.Visible - : Visibility.Collapsed - ; - FolderAndArchiveMenuSeparator1.Visibility = OpenListupItem.Visibility; - - AddSecondaryTile.CommandParameter = itemVM; - AddSecondaryTile.Command = pageVM.SecondaryTileAddCommand; - AddSecondaryTile.Visibility = !pageVM.SecondaryTileManager.ExistTile(itemVM.Path) - ? Visibility.Visible - : Visibility.Collapsed - ; - - RemoveSecondaryTile.CommandParameter = itemVM; - RemoveSecondaryTile.Command = pageVM.SecondaryTileRemoveCommand; - RemoveSecondaryTile.Visibility = pageVM.SecondaryTileManager.ExistTile(itemVM.Path) - ? Visibility.Visible - : Visibility.Collapsed - ; - - FolderAndArchiveMenuSeparator2.Visibility = Visibility.Visible; - - OpenWithExplorerItem.CommandParameter = itemVM; - OpenWithExplorerItem.Command = pageVM.OpenWithExplorerCommand; - OpenWithExplorerItem.Visibility = Visibility.Visible; - ; - - NoActionDescMenuItem.Visibility = Visibility.Collapsed; - } + FoldersAdaptiveGridView.DeselectAll(); } - - - - - public async void BringIntoViewLastIntractItem() { var pageVM = (DataContext as FolderListupPageViewModel); @@ -197,6 +134,8 @@ public async void BringIntoViewLastIntractItem() { FoldersAdaptiveGridView.ScrollIntoView(lastIntaractItem, ScrollIntoViewAlignment.Leading); + // 並び替えを伴う場合にスクロール位置がズレる問題を回避するためDelayを入れてる + await Task.Delay(100); { DependencyObject item; do diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/Helpers/CodeBehindExtensions.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/Helpers/CodeBehindExtensions.cs new file mode 100644 index 00000000..35fdb1cc --- /dev/null +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/Helpers/CodeBehindExtensions.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Windows.UI.Xaml; + +namespace TsubameViewer.Presentation.Views.Helpers +{ + public static class CodeBehindExtensions + { + public static Visibility TrueToVisible(this bool b) => b ? Visibility.Visible : Visibility.Collapsed; + public static Visibility FalseToVisible(this bool b) => TrueToVisible(!b); + } +} diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/ImageListupPage.xaml b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/ImageListupPage.xaml index 6dd2be29..abbbe791 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/ImageListupPage.xaml +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/ImageListupPage.xaml @@ -36,24 +36,11 @@ - - - - - - - - - - - - - - + @@ -70,7 +57,7 @@ - + @@ -79,7 +66,7 @@ - + @@ -88,7 +75,7 @@ - + @@ -100,7 +87,7 @@ - + - + @@ -133,7 +120,7 @@ - + @@ -141,20 +128,24 @@ Height="7" Width="7" HorizontalAlignment="Left" VerticalAlignment="Top" + Margin="4 0 0 0" /> - + + @@ -174,9 +165,15 @@ - + + + + + + + + @@ -328,7 +362,7 @@ FocusVisualSecondaryBrush="{ThemeResource SystemAccentColor}" > - + @@ -375,22 +409,13 @@ - - - - - - - - - - - + + - + diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/ImageListupPage.xaml.cs b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/ImageListupPage.xaml.cs index 508cd89d..58904163 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/ImageListupPage.xaml.cs +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/ImageListupPage.xaml.cs @@ -9,6 +9,8 @@ using TsubameViewer.Models.Domain.ImageViewer.ImageSource; using TsubameViewer.Presentation.ViewModels; using TsubameViewer.Presentation.ViewModels.PageNavigation; +using TsubameViewer.Presentation.Views.Helpers; +using Uno.Threading; using Windows.Foundation; using Windows.Foundation.Collections; using Windows.UI.Xaml; @@ -129,8 +131,6 @@ private void FileItemsRepeater_Large_ElementPrepared(ItemsRepeater sender, Items #endregion - - private void FileItemsRepeater_ElementPrepared(ItemsRepeater sender, ItemsRepeaterElementPreparedEventArgs args) { if (args.Element is FrameworkElement fe @@ -161,6 +161,13 @@ private void Image_PointerEntered(object sender, PointerRoutedEventArgs e) var item = sender as FrameworkElement; item.Scale(1.020f, 1.020f, centerX: (float)item.ActualWidth * 0.5f, centerY: (float)item.ActualHeight * 0.5f, duration: 50) .Start(); + if (item.DataContext is StorageItemViewModel itemVM) + { + if (itemVM.Image?.IsAnimatedBitmap ?? false) + { + itemVM.Image.Play(); + } + } } private void Image_PointerExited(object sender, PointerRoutedEventArgs e) @@ -168,6 +175,13 @@ private void Image_PointerExited(object sender, PointerRoutedEventArgs e) var item = sender as FrameworkElement; item.Scale(1.0f, 1.0f, centerX: (float)item.ActualWidth * 0.5f, centerY: (float)item.ActualHeight * 0.5f, duration: 50) .Start(); + if (item.DataContext is StorageItemViewModel itemVM) + { + if (itemVM.Image?.IsAnimatedBitmap ?? false) + { + itemVM.Image.Stop(); + } + } } @@ -181,67 +195,5 @@ private void FoldersAdaptiveGridView_ContextRequested(UIElement sender, ContextR flyout.ShowAt(args.OriginalSource as FrameworkElement); } - private void FolderAndArchiveMenuFlyout_Opened(object sender, object e) - { - var flyout = sender as FlyoutBase; - var pageVM = DataContext as ImageListupPageViewModel; - - StorageItemViewModel itemVM = flyout.Target.DataContext as StorageItemViewModel; - if (itemVM == null && flyout.Target is Control content) - { - itemVM = (content as ContentControl)?.Content as StorageItemViewModel; - } - - if (itemVM == null) - { - flyout.Hide(); - return; - } - - if (itemVM.Item is StorageItemImageSource == false) - { - NoActionDescMenuItem.Visibility = Visibility.Visible; - - OpenListupItem.Visibility = Visibility.Collapsed; - AddSecondaryTile.Visibility = Visibility.Collapsed; - RemoveSecondaryTile.Visibility = Visibility.Collapsed; - OpenWithExplorerItem.Visibility = Visibility.Collapsed; - FolderAndArchiveMenuSeparator1.Visibility = Visibility.Collapsed; - FolderAndArchiveMenuSeparator2.Visibility = Visibility.Collapsed; - } - else - { - OpenListupItem.CommandParameter = itemVM; - OpenListupItem.Command = pageVM.OpenFolderListupCommand; - OpenListupItem.Visibility = (itemVM.Type == Models.Domain.StorageItemTypes.Archive || itemVM.Type == Models.Domain.StorageItemTypes.Folder) - ? Visibility.Visible - : Visibility.Collapsed - ; - FolderAndArchiveMenuSeparator1.Visibility = OpenListupItem.Visibility; - - AddSecondaryTile.CommandParameter = itemVM; - AddSecondaryTile.Command = pageVM.SecondaryTileAddCommand; - AddSecondaryTile.Visibility = !pageVM.SecondaryTileManager.ExistTile(itemVM.Path) - ? Visibility.Visible - : Visibility.Collapsed - ; - - RemoveSecondaryTile.CommandParameter = itemVM; - RemoveSecondaryTile.Command = pageVM.SecondaryTileRemoveCommand; - RemoveSecondaryTile.Visibility = pageVM.SecondaryTileManager.ExistTile(itemVM.Path) - ? Visibility.Visible - : Visibility.Collapsed - ; - - FolderAndArchiveMenuSeparator2.Visibility = Visibility.Visible; - - OpenWithExplorerItem.CommandParameter = itemVM; - OpenWithExplorerItem.Command = pageVM.OpenWithExplorerCommand; - OpenWithExplorerItem.Visibility = Visibility.Visible; - ; - - NoActionDescMenuItem.Visibility = Visibility.Collapsed; - } - } } } diff --git a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/ImageViewerPage.xaml b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/ImageViewerPage.xaml index e9655a33..1a134b97 100644 --- a/TsubameViewer/TsubameViewer.Shared/Presentation.Views/ImageViewerPage.xaml +++ b/TsubameViewer/TsubameViewer.Shared/Presentation.Views/ImageViewerPage.xaml @@ -14,16 +14,21 @@ xmlns:uwpUIExtensions="using:Microsoft.Toolkit.Uwp.UI.Extensions" xmlns:uiNavigation="using:TsubameViewer.Presentation.Views.UINavigation" xmlns:domainModels="using:TsubameViewer.Models.Domain" - xmlns:myStateTriggers="using:TsubameViewer.Presentation.Views.StateTrigger" + xmlns:myStateTriggers="using:TsubameViewer.Presentation.Views.StateTrigger" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls" + xmlns:i18nExt="using:I18NPortable.Xaml.Extensions" + xmlns:models="using:TsubameViewer.Models.Domain.FolderItemListing" mc:Ignorable="d" Background="{ThemeResource ApplicationContentBackgroundBrush}" > + @@ -31,6 +36,7 @@ + @@ -49,17 +55,17 @@ - + + > @@ -69,18 +75,19 @@ + + Padding="0" + > @@ -88,10 +95,59 @@ -