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}" />
+
+
+