From fc3d2c6bac73149db0652f0c7e01d343bc8c20d2 Mon Sep 17 00:00:00 2001 From: unknown <1463567152@qq.com> Date: Mon, 18 May 2026 18:27:13 +0800 Subject: [PATCH 1/9] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=89=93=E5=BC=80zip?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E9=80=89=E6=8B=A9=E7=BC=96=E7=A0=81=E3=80=82?= =?UTF-8?q?=E7=9B=AE=E5=89=8D=E9=97=AE=E9=A2=98=EF=BC=9A=E4=B8=8D=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=96=87=E4=BB=B6=E5=A4=B9=EF=BC=8C=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E8=A2=AB=E5=B1=95=E5=B9=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Files.App.Launcher.vcxproj | 4 +- .../Data/Models/CurrentInstanceViewModel.cs | 14 +++ .../UserControls/NavigationToolbar.xaml | 11 ++ .../Storage/StorageItems/ZipStorageFolder.cs | 108 ++++++++++++++++++ .../NavigationToolbarViewModel.cs | 95 +++++++++++++++ 5 files changed, 230 insertions(+), 2 deletions(-) diff --git a/src/Files.App.Launcher/Files.App.Launcher.vcxproj b/src/Files.App.Launcher/Files.App.Launcher.vcxproj index 9fc73ecd9115..fec07fc042ad 100644 --- a/src/Files.App.Launcher/Files.App.Launcher.vcxproj +++ b/src/Files.App.Launcher/Files.App.Launcher.vcxproj @@ -89,7 +89,7 @@ - _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + _DEBUG;_CONSOLE;%(PreprocessorDefinitions);_SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS MultiThreadedDebug @@ -103,7 +103,7 @@ true true - NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + NDEBUG;_CONSOLE;%(PreprocessorDefinitions);_SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS MultiThreaded diff --git a/src/Files.App/Data/Models/CurrentInstanceViewModel.cs b/src/Files.App/Data/Models/CurrentInstanceViewModel.cs index ffc6a977f291..f77555489655 100644 --- a/src/Files.App/Data/Models/CurrentInstanceViewModel.cs +++ b/src/Files.App/Data/Models/CurrentInstanceViewModel.cs @@ -146,6 +146,20 @@ public bool IsPageTypeLibrary } } + private string? zipEncodingName; + public string? ZipEncodingName + { + get => zipEncodingName; + set => SetProperty(ref zipEncodingName, value); + } + + private bool isZipEncodingUndetermined; + public bool IsZipEncodingUndetermined + { + get => isZipEncodingUndetermined; + set => SetProperty(ref isZipEncodingUndetermined, value); + } + public bool CanCopyPathInPage { get => !isPageTypeMtpDevice && !isPageTypeRecycleBin && isPageTypeNotHome && !isPageTypeSearchResults && !IsPageTypeReleaseNotes && !IsPageTypeSettings; diff --git a/src/Files.App/UserControls/NavigationToolbar.xaml b/src/Files.App/UserControls/NavigationToolbar.xaml index 3ec89274a576..3c67ee053f43 100644 --- a/src/Files.App/UserControls/NavigationToolbar.xaml +++ b/src/Files.App/UserControls/NavigationToolbar.xaml @@ -395,6 +395,17 @@ Orientation="Horizontal" Spacing="4"> + + + + /// Gets or sets the encoding to use when browsing ZIP files. + /// When set, SharpZipLib is used instead of SevenZipSharp. + /// + internal static Encoding? CurrentEncoding { get; set; } + public override string Path { get; } public override string Name { get; } public override string DisplayName => Name; @@ -170,6 +178,9 @@ public override IAsyncOperation GetBasicPropertiesAsync() public override IAsyncOperation GetItemAsync(string name) { + if (CurrentEncoding is not null) + return GetItemWithEncodingAsync(name); + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using SevenZipExtractor zipFile = await OpenZipFileAsync(); @@ -200,6 +211,44 @@ public override IAsyncOperation GetItemAsync(string name) }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } + private IAsyncOperation GetItemWithEncodingAsync(string name) + { + return AsyncInfo.Run((cancellationToken) => + { + return Task.Run(() => + { + using var zipFile = new ZipFile(containerPath, StringCodec.FromEncoding(CurrentEncoding!)); + + if (!string.IsNullOrEmpty(Credentials.Password)) + zipFile.Password = Credentials.Password; + + var targetPath = System.IO.Path.Combine(Path, name); + + foreach (ZipEntry entry in zipFile) + { + var entryPath = System.IO.Path.Combine(System.IO.Path.GetFullPath(containerPath), entry.Name); + if (entryPath == targetPath) + { + if (entry.IsDirectory) + { + var folder = new ZipStorageFolder(targetPath, containerPath, backingFile); + ((IPasswordProtectedItem)folder).CopyFrom(this); + return folder; + } + else + { + var file = new ZipStorageFile(targetPath, containerPath, backingFile); + ((IPasswordProtectedItem)file).CopyFrom(this); + return file; + } + } + } + + return null; + }); + }); + } + public override IAsyncOperation TryGetItemAsync(string name) { return AsyncInfo.Run(async (cancellationToken) => @@ -216,6 +265,9 @@ public override IAsyncOperation TryGetItemAsync(string name) } public override IAsyncOperation> GetItemsAsync() { + if (CurrentEncoding is not null) + return GetItemsWithEncodingAsync(); + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap>(async () => { using SevenZipExtractor zipFile = await OpenZipFileAsync(); @@ -255,6 +307,62 @@ public override IAsyncOperation> GetItemsAsync() return items; }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } + + private IAsyncOperation> GetItemsWithEncodingAsync() + { + return AsyncInfo.Run((cancellationToken) => + { + return Task.Run>(() => + { + using var zipFile = new ZipFile(containerPath, StringCodec.FromEncoding(CurrentEncoding!)); + + if (!string.IsNullOrEmpty(Credentials.Password)) + zipFile.Password = Credentials.Password; + + var items = new List(); + var dirPaths = new Dictionary(StringComparer.OrdinalIgnoreCase); + var addedDirs = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (ZipEntry entry in zipFile) + { + string winPath = System.IO.Path.Combine(System.IO.Path.GetFullPath(containerPath), entry.Name); + if (!winPath.StartsWith(Path.WithEnding("\\"), StringComparison.Ordinal)) + continue; + + var relativePath = winPath.Substring(Path.Length).Trim('\\'); + var parts = relativePath.Split('\\', StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length == 0) + continue; + + if (parts.Length > 1 || entry.IsDirectory) + { + dirPaths[parts[0]] = entry.IsDirectory; + } + + if (parts.Length == 1 && !entry.IsDirectory) + { + var file = new ZipStorageFile(winPath, containerPath, backingFile); + ((IPasswordProtectedItem)file).CopyFrom(this); + items.Add(file); + } + } + + foreach (var (dirName, _) in dirPaths) + { + var itemPath = System.IO.Path.Combine(Path, dirName); + if (addedDirs.Add(itemPath)) + { + var folder = new ZipStorageFolder(itemPath, containerPath, backingFile); + ((IPasswordProtectedItem)folder).CopyFrom(this); + items.Add(folder); + } + } + + return items; + }); + }); + } public override IAsyncOperation> GetItemsAsync(uint startIndex, uint maxItemsToRetrieve) => AsyncInfo.Run>(async (cancellationToken) => (await GetItemsAsync()).Skip((int)startIndex).Take((int)maxItemsToRetrieve).ToList() diff --git a/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs b/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs index 901f23a0f0c1..fad5450e577b 100644 --- a/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs @@ -3,6 +3,9 @@ using CommunityToolkit.WinUI; using Files.App.Controls; +using Files.App.Data.Contracts; +using Files.App.Data.Items; +using Files.App.Utils.Storage; using Files.App.ViewModels.Settings; using Files.Shared.Helpers; using Microsoft.Extensions.Logging; @@ -11,6 +14,7 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using System.IO; +using System.Text; using System.Windows.Input; using Windows.ApplicationModel.DataTransfer; @@ -35,6 +39,7 @@ public sealed partial class NavigationToolbarViewModel : ObservableObject, IAddr private readonly ICommandManager Commands = Ioc.Default.GetRequiredService(); private readonly IContentPageContext ContentPageContext = Ioc.Default.GetRequiredService(); private readonly StatusCenterViewModel OngoingTasksViewModel = Ioc.Default.GetRequiredService(); + private readonly IStorageArchiveService StorageArchiveService = Ioc.Default.GetRequiredService(); // Fields @@ -180,6 +185,26 @@ public int UpdateProgress private bool _CanRefresh; public bool CanRefresh { get => _CanRefresh; set => SetProperty(ref _CanRefresh, value); } + private bool _IsZipEncodingSelectorVisible; + public bool IsZipEncodingSelectorVisible + { + get => _IsZipEncodingSelectorVisible; + set => SetProperty(ref _IsZipEncodingSelectorVisible, value); + } + + public EncodingItem[] ZipEncodingOptions { get; } = EncodingItem.Defaults; + + private EncodingItem? _SelectedZipEncoding; + public EncodingItem? SelectedZipEncoding + { + get => _SelectedZipEncoding; + set + { + if (SetProperty(ref _SelectedZipEncoding, value) && value is not null) + _ = OnZipEncodingChangedAsync(value); + } + } + private string _PathControlDisplayText; [Obsolete("Superseded by Omnibar.")] public string PathControlDisplayText { get => _PathControlDisplayText; set => SetProperty(ref _PathControlDisplayText, value); } @@ -253,6 +278,10 @@ private void InstanceViewModel_PropertyChanged(object? sender, PropertyChangedEv OmnibarSearchModeSuggestionItems.Clear(); OmnibarSearchModeText = string.Empty; } + else if (e.PropertyName is nameof(CurrentInstanceViewModel.IsPageTypeZipFolder)) + { + _ = UpdateZipEncodingStateAsync(); + } } private List? _SelectedItems; @@ -339,6 +368,72 @@ public NavigationToolbarViewModel() // Methods + private async Task UpdateZipEncodingStateAsync() + { + if (InstanceViewModel is null) + return; + + if (!InstanceViewModel.IsPageTypeZipFolder) + { + IsZipEncodingSelectorVisible = false; + ZipStorageFolder.CurrentEncoding = null; + return; + } + + var workingDir = ContentPageContext.ShellPage?.ShellViewModel.WorkingDirectory; + if (string.IsNullOrEmpty(workingDir) || !ZipStorageFolder.IsZipPath(workingDir)) + return; + + try + { + var isUndetermined = await StorageArchiveService.IsEncodingUndeterminedAsync(workingDir); + InstanceViewModel.IsZipEncodingUndetermined = isUndetermined; + + if (!isUndetermined) + { + IsZipEncodingSelectorVisible = false; + return; + } + + // Auto-detect encoding + var detected = await StorageArchiveService.DetectEncodingAsync(workingDir); + if (detected is not null) + { + InstanceViewModel.ZipEncodingName = detected.WebName; + SelectedZipEncoding = ZipEncodingOptions.FirstOrDefault(e => + e.Encoding?.WebName.Equals(detected.WebName, StringComparison.OrdinalIgnoreCase) == true); + } + else + { + InstanceViewModel.ZipEncodingName = null; + SelectedZipEncoding = ZipEncodingOptions.FirstOrDefault(e => e.Encoding is null); + } + + IsZipEncodingSelectorVisible = true; + } + catch (Exception ex) + { + App.Logger.LogError(ex, "Error checking zip encoding."); + IsZipEncodingSelectorVisible = false; + } + } + + private async Task OnZipEncodingChangedAsync(EncodingItem encodingItem) + { + if (ContentPageContext.ShellPage is null) + return; + + var workingDir = ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory; + if (string.IsNullOrEmpty(workingDir)) + return; + + // Update the static encoding on ZipStorageFolder + ZipStorageFolder.CurrentEncoding = encodingItem.Encoding; + + // Refresh the folder to re-read with new encoding + ContentPageContext.ShellPage.ShellViewModel.RefreshItems(null); + } + private void UpdateService_OnPropertyChanged(object? sender, PropertyChangedEventArgs e) { IsUpdateAvailable = UpdateService.IsUpdateAvailable; From d31bcafb1ba675f024cb8d0c8502fc8e16b1d272 Mon Sep 17 00:00:00 2001 From: unknown <1463567152@qq.com> Date: Fri, 29 May 2026 21:39:35 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E6=AD=A3=E5=B8=B8=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Storage/StorageItems/ZipStorageFolder.cs | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs index 0839366070db..3b4c2e1b4769 100644 --- a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs @@ -223,11 +223,15 @@ private IAsyncOperation GetItemWithEncodingAsync(string name) zipFile.Password = Credentials.Password; var targetPath = System.IO.Path.Combine(Path, name); + var normalizedTarget = targetPath.TrimEnd('\\', '/'); + bool foundChild = false; foreach (ZipEntry entry in zipFile) { - var entryPath = System.IO.Path.Combine(System.IO.Path.GetFullPath(containerPath), entry.Name); - if (entryPath == targetPath) + var entryPath = System.IO.Path.Combine(System.IO.Path.GetFullPath(containerPath), entry.Name.Replace("/", "\\")); + var normalizedEntry = entryPath.TrimEnd('\\', '/'); + + if (normalizedEntry == normalizedTarget) { if (entry.IsDirectory) { @@ -242,6 +246,17 @@ private IAsyncOperation GetItemWithEncodingAsync(string name) return file; } } + + if (!foundChild && normalizedEntry.StartsWith(normalizedTarget + "\\", StringComparison.OrdinalIgnoreCase)) + foundChild = true; + } + + // No exact match found; check if target is an implicit directory + if (foundChild) + { + var folder = new ZipStorageFolder(targetPath, containerPath, backingFile); + ((IPasswordProtectedItem)folder).CopyFrom(this); + return folder; } return null; @@ -321,44 +336,36 @@ private IAsyncOperation> GetItemsWithEncodingAsync() var items = new List(); var dirPaths = new Dictionary(StringComparer.OrdinalIgnoreCase); - var addedDirs = new HashSet(StringComparer.OrdinalIgnoreCase); + var dirPrefix = Path.WithEnding("\\"); foreach (ZipEntry entry in zipFile) { - string winPath = System.IO.Path.Combine(System.IO.Path.GetFullPath(containerPath), entry.Name); - if (!winPath.StartsWith(Path.WithEnding("\\"), StringComparison.Ordinal)) + string winPath = System.IO.Path.Combine(System.IO.Path.GetFullPath(containerPath), entry.Name.Replace("/", "\\")); + if (!winPath.StartsWith(dirPrefix, StringComparison.Ordinal)) continue; - var relativePath = winPath.Substring(Path.Length).Trim('\\'); - var parts = relativePath.Split('\\', StringSplitOptions.RemoveEmptyEntries); - - if (parts.Length == 0) + var split = winPath.Substring(Path.Length).Split('\\', StringSplitOptions.RemoveEmptyEntries); + if (split.Length <= 0) continue; - if (parts.Length > 1 || entry.IsDirectory) + if (entry.IsDirectory || split.Length > 1) // Not all folders have a ZipEntry { - dirPaths[parts[0]] = entry.IsDirectory; + var itemPath = System.IO.Path.Combine(Path, split[0]); + if (!items.Any(x => x.Path == itemPath)) + { + var folder = new ZipStorageFolder(itemPath, containerPath, backingFile); + ((IPasswordProtectedItem)folder).CopyFrom(this); + items.Add(folder); + } } - - if (parts.Length == 1 && !entry.IsDirectory) + else { var file = new ZipStorageFile(winPath, containerPath, backingFile); ((IPasswordProtectedItem)file).CopyFrom(this); items.Add(file); } + } - - foreach (var (dirName, _) in dirPaths) - { - var itemPath = System.IO.Path.Combine(Path, dirName); - if (addedDirs.Add(itemPath)) - { - var folder = new ZipStorageFolder(itemPath, containerPath, backingFile); - ((IPasswordProtectedItem)folder).CopyFrom(this); - items.Add(folder); - } - } - return items; }); }); From 722fb3c7ac9695400fde304fba74e1be390bbaf7 Mon Sep 17 00:00:00 2001 From: unknown <1463567152@qq.com> Date: Sat, 30 May 2026 06:51:48 +0800 Subject: [PATCH 3/9] =?UTF-8?q?=E5=8F=AF=E5=A4=8D=E5=88=B6=E3=80=81?= =?UTF-8?q?=E6=89=93=E5=BC=80=E6=96=87=E4=BB=B6=EF=BC=8C=E4=BB=8D=E7=84=B6?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E6=89=93=E5=BC=80=E5=AD=90=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=A4=B9=E7=9A=84=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Storage/StorageItems/ZipStorageFile.cs | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) diff --git a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs index dff6b9dc7d93..58be04266bc9 100644 --- a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Files.Shared.Helpers; +using ICSharpCode.SharpZipLib.Zip; using SevenZip; using System.IO; using System.Runtime.InteropServices.WindowsRuntime; @@ -95,6 +96,9 @@ public static IAsyncOperation FromPathAsync(string path) public override IAsyncOperation OpenAsync(FileAccessMode accessMode) { + if (ZipStorageFolder.CurrentEncoding is not null && Path != containerPath) + return OpenWithEncodingAsync(accessMode); + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { bool rw = accessMode is FileAccessMode.ReadWrite; @@ -136,11 +140,53 @@ public override IAsyncOperation OpenAsync(FileAccessMode ac throw new NotSupportedException("Can't open zip file as RW"); }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } + private IAsyncOperation OpenWithEncodingAsync(FileAccessMode accessMode) + { + return AsyncInfo.Run((cancellationToken) => + { + return Task.Run(() => + { + bool rw = accessMode is FileAccessMode.ReadWrite; + if (rw) + throw new NotSupportedException("Can't open zip file as RW"); + + using var zipFile = new ZipFile(containerPath, StringCodec.FromEncoding(ZipStorageFolder.CurrentEncoding!)); + + if (!string.IsNullOrEmpty(Credentials.Password)) + zipFile.Password = Credentials.Password; + + var targetName = GetEntryRelativePath(); + + foreach (ZipEntry entry in zipFile) + { + if (!entry.IsFile) + continue; + + if (string.Equals(entry.Name.Replace('\\', '/'), targetName, StringComparison.OrdinalIgnoreCase)) + { + var ms = new MemoryStream(); + using (var zipStream = zipFile.GetInputStream(entry)) + { + zipStream.CopyTo(ms); + } + ms.Position = 0; + return new NonSeekableRandomAccessStreamForRead(ms, (ulong)entry.Size); + } + } + + return null; + }); + }); + } + public override IAsyncOperation OpenAsync(FileAccessMode accessMode, StorageOpenOptions options) => OpenAsync(accessMode); public override IAsyncOperation OpenReadAsync() { + if (ZipStorageFolder.CurrentEncoding is not null && Path != containerPath) + return OpenReadWithEncodingAsync(); + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { if (Path == containerPath) @@ -178,8 +224,47 @@ public override IAsyncOperation OpenReadAsyn }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } + private IAsyncOperation OpenReadWithEncodingAsync() + { + return AsyncInfo.Run((cancellationToken) => + { + return Task.Run(() => + { + using var zipFile = new ZipFile(containerPath, StringCodec.FromEncoding(ZipStorageFolder.CurrentEncoding!)); + + if (!string.IsNullOrEmpty(Credentials.Password)) + zipFile.Password = Credentials.Password; + + var targetName = GetEntryRelativePath(); + + foreach (ZipEntry entry in zipFile) + { + if (!entry.IsFile) + continue; + + if (string.Equals(entry.Name.Replace('\\', '/'), targetName, StringComparison.OrdinalIgnoreCase)) + { + var ms = new MemoryStream(); + using (var zipStream = zipFile.GetInputStream(entry)) + { + zipStream.CopyTo(ms); + } + ms.Position = 0; + var nsStream = new NonSeekableRandomAccessStreamForRead(ms, (ulong)entry.Size); + return new StreamWithContentType(nsStream); + } + } + + return null; + }); + }); + } + public override IAsyncOperation OpenSequentialReadAsync() { + if (ZipStorageFolder.CurrentEncoding is not null && Path != containerPath) + return OpenSequentialReadWithEncodingAsync(); + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { if (Path == containerPath) @@ -215,6 +300,41 @@ public override IAsyncOperation OpenSequentialReadAsync() }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } + private IAsyncOperation OpenSequentialReadWithEncodingAsync() + { + return AsyncInfo.Run((cancellationToken) => + { + return Task.Run(() => + { + using var zipFile = new ZipFile(containerPath, StringCodec.FromEncoding(ZipStorageFolder.CurrentEncoding!)); + + if (!string.IsNullOrEmpty(Credentials.Password)) + zipFile.Password = Credentials.Password; + + var targetName = GetEntryRelativePath(); + + foreach (ZipEntry entry in zipFile) + { + if (!entry.IsFile) + continue; + + if (string.Equals(entry.Name.Replace('\\', '/'), targetName, StringComparison.OrdinalIgnoreCase)) + { + var ms = new MemoryStream(); + using (var zipStream = zipFile.GetInputStream(entry)) + { + zipStream.CopyTo(ms); + } + ms.Position = 0; + return new NonSeekableRandomAccessStreamForRead(ms, (ulong)entry.Size); + } + } + + return null; + }); + }); + } + public override IAsyncOperation OpenTransactedWriteAsync() => throw new NotSupportedException(); public override IAsyncOperation OpenTransactedWriteAsync(StorageOpenOptions options) @@ -226,6 +346,9 @@ public override IAsyncOperation CopyAsync(IStorageFolder destin => CopyAsync(destinationFolder, desiredNewName, NameCollisionOption.FailIfExists); public override IAsyncOperation CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) { + if (ZipStorageFolder.CurrentEncoding is not null && Path != containerPath) + return CopyWithEncodingAsync(destinationFolder, desiredNewName, option); + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.Wrap(async () => { using SevenZipExtractor zipFile = await OpenZipFileAsync(); @@ -264,8 +387,57 @@ await SafetyExtensions.WrapAsync(() => zipFile.ExtractFileAsync(entry.Index, out } }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } + + private IAsyncOperation CopyWithEncodingAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) + { + return AsyncInfo.Run(async (cancellationToken) => + { + using var zipFile = new ZipFile(containerPath, StringCodec.FromEncoding(ZipStorageFolder.CurrentEncoding!)); + + if (!string.IsNullOrEmpty(Credentials.Password)) + zipFile.Password = Credentials.Password; + + var targetName = GetEntryRelativePath(); + + foreach (ZipEntry entry in zipFile) + { + if (!entry.IsFile) + continue; + + if (string.Equals(entry.Name.Replace('\\', '/'), targetName, StringComparison.OrdinalIgnoreCase)) + { + var ms = new MemoryStream(); + using (var zipStream = zipFile.GetInputStream(entry)) + { + zipStream.CopyTo(ms); + } + ms.Position = 0; + + var destFolder = destinationFolder.AsBaseStorageFolder(); + if (destFolder is ICreateFileWithStream cwsf) + { + using var inStream = new NonSeekableRandomAccessStreamForRead(ms, (ulong)entry.Size); + return await cwsf.CreateFileAsync(inStream.AsStreamForRead(), desiredNewName, option.Convert()); + } + else + { + var destFile = await destFolder.CreateFileAsync(desiredNewName, option.Convert()); + await using var outStream = await destFile.OpenStreamForWriteAsync(); + ms.Position = 0; + ms.CopyTo(outStream); + return destFile; + } + } + } + + return null; + }); + } public override IAsyncAction CopyAndReplaceAsync(IStorageFile fileToReplace) { + if (ZipStorageFolder.CurrentEncoding is not null && Path != containerPath) + return CopyAndReplaceWithEncodingAsync(fileToReplace); + return AsyncInfo.Run((cancellationToken) => SafetyExtensions.WrapAsync(async () => { using SevenZipExtractor zipFile = await OpenZipFileAsync(); @@ -288,6 +460,36 @@ public override IAsyncAction CopyAndReplaceAsync(IStorageFile fileToReplace) }, ((IPasswordProtectedItem)this).RetryWithCredentialsAsync)); } + private IAsyncAction CopyAndReplaceWithEncodingAsync(IStorageFile fileToReplace) + { + return AsyncInfo.Run(async (cancellationToken) => + { + using var zipFile = new ZipFile(containerPath, StringCodec.FromEncoding(ZipStorageFolder.CurrentEncoding!)); + + if (!string.IsNullOrEmpty(Credentials.Password)) + zipFile.Password = Credentials.Password; + + var targetName = GetEntryRelativePath(); + + foreach (ZipEntry entry in zipFile) + { + if (!entry.IsFile) + continue; + + if (string.Equals(entry.Name.Replace('\\', '/'), targetName, StringComparison.OrdinalIgnoreCase)) + { + using var hDestFile = fileToReplace.CreateSafeFileHandle(FileAccess.ReadWrite); + await using (var outStream = new FileStream(hDestFile, FileAccess.Write)) + using (var zipStream = zipFile.GetInputStream(entry)) + { + zipStream.CopyTo(outStream); + } + return; + } + } + }); + } + public override IAsyncAction MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException(); public override IAsyncAction MoveAsync(IStorageFolder destinationFolder, string desiredNewName) @@ -399,6 +601,12 @@ public override IAsyncOperation GetThumbnailAsync(Thumbnai public override IAsyncOperation GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options) => Task.FromResult(null).AsAsyncOperation(); + private string GetEntryRelativePath() + { + var relative = Path.Substring(containerPath.Length).Trim('\\', '/'); + return relative.Replace('\\', '/'); + } + private static bool CheckAccess(string path) { try From acaaa1a793ab19dc2f7214ea438f585d7ca3141f Mon Sep 17 00:00:00 2001 From: unknown <1463567152@qq.com> Date: Mon, 1 Jun 2026 18:20:42 +0800 Subject: [PATCH 4/9] =?UTF-8?q?=E7=BC=96=E7=A0=81=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E6=A1=86=E7=A7=BB=E5=8A=A8=E5=88=B0=E5=8F=B3=E4=B8=8B=E8=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/DirectoryPropertiesViewModel.cs | 131 ++++++++++++++++++ .../UserControls/NavigationToolbar.xaml | 11 -- src/Files.App/UserControls/StatusBar.xaml | 17 ++- .../NavigationToolbarViewModel.cs | 94 ------------- 4 files changed, 147 insertions(+), 106 deletions(-) diff --git a/src/Files.App/Data/Models/DirectoryPropertiesViewModel.cs b/src/Files.App/Data/Models/DirectoryPropertiesViewModel.cs index 1109998f2b6a..4b0f8f4122c8 100644 --- a/src/Files.App/Data/Models/DirectoryPropertiesViewModel.cs +++ b/src/Files.App/Data/Models/DirectoryPropertiesViewModel.cs @@ -1,6 +1,11 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using Files.App.Data.Items; +using Files.App.Utils.Storage; +using Microsoft.Extensions.Logging; +using System.ComponentModel; +using System.Text; using System.Windows.Input; namespace Files.App.ViewModels.UserControls @@ -9,6 +14,8 @@ public sealed partial class StatusBarViewModel : ObservableObject { private IContentPageContext ContentPageContext { get; } = Ioc.Default.GetRequiredService(); private IDevToolsSettingsService DevToolsSettingsService = Ioc.Default.GetRequiredService(); + private readonly IStorageArchiveService StorageArchiveService = Ioc.Default.GetRequiredService(); + private CurrentInstanceViewModel? InstanceViewModel => ContentPageContext.ShellPage?.InstanceViewModel; // The first branch will always be the active one. public const int ACTIVE_BRANCH_INDEX = 0; @@ -81,6 +88,26 @@ public string ExtendedStatusInfo set => SetProperty(ref _ExtendedStatusInfo, value); } + private bool _IsZipEncodingSelectorVisible; + public bool IsZipEncodingSelectorVisible + { + get => _IsZipEncodingSelectorVisible; + set => SetProperty(ref _IsZipEncodingSelectorVisible, value); + } + + public EncodingItem[] ZipEncodingOptions { get; } = EncodingItem.Defaults; + + private EncodingItem? _SelectedZipEncoding; + public EncodingItem? SelectedZipEncoding + { + get => _SelectedZipEncoding; + set + { + if (SetProperty(ref _SelectedZipEncoding, value) && value is not null) + _ = OnZipEncodingChangedAsync(value); + } + } + public bool ShowOpenInIDEButton { get @@ -112,6 +139,24 @@ public StatusBarViewModel() break; } }; + + SubscribeToShellPage(); + ContentPageContext.PropertyChanged += OnContentPageContextPropertyChanged; + } + + private void OnContentPageContextPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ContentPageContext.ShellPage)) + { + UnsubscribeFromInstanceViewModel(); + SubscribeToShellPage(); + } + } + + private void SubscribeToShellPage() + { + SubscribeToInstanceViewModel(); + _ = UpdateZipEncodingStateAsync(); } public void UpdateGitInfo(bool isGitRepository, string? repositoryPath, BranchItem? head) @@ -164,5 +209,91 @@ public Task ExecuteDeleteBranch(string? branchName) { return GitHelpers.DeleteBranchAsync(_gitRepositoryPath, GitBranchDisplayName, branchName); } + + public async Task UpdateZipEncodingStateAsync() + { + var instanceVM = InstanceViewModel; + if (instanceVM is null) + return; + + if (!instanceVM.IsPageTypeZipFolder) + { + IsZipEncodingSelectorVisible = false; + ZipStorageFolder.CurrentEncoding = null; + return; + } + + var workingDir = ContentPageContext.ShellPage?.ShellViewModel.WorkingDirectory; + if (string.IsNullOrEmpty(workingDir) || !ZipStorageFolder.IsZipPath(workingDir)) + return; + + try + { + var isUndetermined = await StorageArchiveService.IsEncodingUndeterminedAsync(workingDir); + instanceVM.IsZipEncodingUndetermined = isUndetermined; + + if (!isUndetermined) + { + IsZipEncodingSelectorVisible = false; + return; + } + + var detected = await StorageArchiveService.DetectEncodingAsync(workingDir); + if (detected is not null) + { + instanceVM.ZipEncodingName = detected.WebName; + SelectedZipEncoding = ZipEncodingOptions.FirstOrDefault(e => + e.Encoding?.WebName.Equals(detected.WebName, StringComparison.OrdinalIgnoreCase) == true); + } + else + { + instanceVM.ZipEncodingName = null; + SelectedZipEncoding = ZipEncodingOptions.FirstOrDefault(e => e.Encoding is null); + } + + IsZipEncodingSelectorVisible = true; + } + catch (Exception ex) + { + App.Logger.LogError(ex, "Error checking zip encoding."); + IsZipEncodingSelectorVisible = false; + } + } + + private async Task OnZipEncodingChangedAsync(EncodingItem encodingItem) + { + if (ContentPageContext.ShellPage is null) + return; + + var workingDir = ContentPageContext.ShellPage.ShellViewModel.WorkingDirectory; + if (string.IsNullOrEmpty(workingDir)) + return; + + ZipStorageFolder.CurrentEncoding = encodingItem.Encoding; + + ContentPageContext.ShellPage.ShellViewModel.RefreshItems(null); + } + + internal void SubscribeToInstanceViewModel() + { + var instanceVM = InstanceViewModel; + if (instanceVM is not null) + instanceVM.PropertyChanged += OnInstanceViewModelPropertyChanged; + } + + internal void UnsubscribeFromInstanceViewModel() + { + var instanceVM = InstanceViewModel; + if (instanceVM is not null) + instanceVM.PropertyChanged -= OnInstanceViewModelPropertyChanged; + } + + private void OnInstanceViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(CurrentInstanceViewModel.IsPageTypeZipFolder)) + { + _ = UpdateZipEncodingStateAsync(); + } + } } } diff --git a/src/Files.App/UserControls/NavigationToolbar.xaml b/src/Files.App/UserControls/NavigationToolbar.xaml index 3c67ee053f43..3ec89274a576 100644 --- a/src/Files.App/UserControls/NavigationToolbar.xaml +++ b/src/Files.App/UserControls/NavigationToolbar.xaml @@ -395,17 +395,6 @@ Orientation="Horizontal" Spacing="4"> - - - + @@ -193,10 +194,24 @@ Background="{ThemeResource DividerStrokeColorDefaultBrush}" /> + + +