Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ namespace Files.App.Actions
[GeneratedRichCommand]
internal sealed partial class DecompressArchiveAction : BaseDecompressArchiveAction
{
private readonly IUserSettingsService UserSettingsService = Ioc.Default.GetRequiredService<IUserSettingsService>();

public override string Label
=> Strings.ExtractFiles.GetLocalizedResource();

Expand Down Expand Up @@ -81,6 +83,9 @@ public override async Task ExecuteAsync(object? parameter = null)
BaseStorageFolder destinationFolder = decompressArchiveViewModel.DestinationFolder;
string destinationFolderPath = decompressArchiveViewModel.DestinationFolderPath;

// Save extraction location for future use
SaveExtractionLocation(destinationFolderPath);

if (destinationFolder is null)
{
BaseStorageFolder parentFolder = await StorageHelpers.ToStorageItem<BaseStorageFolder>(Path.GetDirectoryName(archive.Path) ?? string.Empty);
Expand Down Expand Up @@ -119,5 +124,17 @@ protected override bool CanDecompressSelectedItems()

return null;
}

private void SaveExtractionLocation(string path)
{
var previousArchiveExtractionLocations = UserSettingsService.GeneralSettingsService.PreviousArchiveExtractionLocations?.ToList() ?? [];
previousArchiveExtractionLocations.Remove(path);
previousArchiveExtractionLocations.Insert(0, path);

if (previousArchiveExtractionLocations.Count > 10)
UserSettingsService.GeneralSettingsService.PreviousArchiveExtractionLocations = previousArchiveExtractionLocations.RemoveFrom(11);
else
UserSettingsService.GeneralSettingsService.PreviousArchiveExtractionLocations = previousArchiveExtractionLocations;
}
}
}
5 changes: 5 additions & 0 deletions src/Files.App/Data/Contracts/IGeneralSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public interface IGeneralSettingsService : IBaseSettingsService, INotifyProperty
/// </summary>
List<string> PreviousSearchQueriesList { get; set; }

/// <summary>
/// Stores list of paths where archives have previously been extracted.
/// </summary>
List<string> PreviousArchiveExtractionLocations { get; set; }

/// <summary>
/// Gets or sets a value indicating which date and time format to use.
/// </summary>
Expand Down
8 changes: 5 additions & 3 deletions src/Files.App/Dialogs/DecompressArchiveDialog.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
CornerRadius="{StaticResource OverlayCornerRadius}"
DefaultButton="Primary"
HighContrastAdjustment="None"
IsPrimaryButtonEnabled="{x:Bind ViewModel.IsDestinationPathValid, Mode=OneWay}"
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
PrimaryButtonText="{helpers:ResourceString Name=Extract}"
RequestedTheme="{x:Bind RootAppElement.RequestedTheme, Mode=OneWay}"
Expand Down Expand Up @@ -44,13 +45,14 @@
Text="{helpers:ResourceString Name=ExtractToPath}" />

<!-- Path Box -->
<TextBox
<AutoSuggestBox
x:Name="DestinationFolderPath"
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Stretch"
IsReadOnly="True"
Text="{x:Bind ViewModel.DestinationFolderPath, Mode=OneWay}" />
ItemsSource="{x:Bind ViewModel.PreviousExtractionLocations, Mode=OneWay}"
Text="{x:Bind ViewModel.DestinationFolderPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TextChanged="DestinationFolderPath_TextChanged" />

<Button
x:Name="SelectDestination"
Expand Down
8 changes: 8 additions & 0 deletions src/Files.App/Dialogs/DecompressArchiveDialog.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,13 @@ private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialo
if (ViewModel.IsArchiveEncrypted)
ViewModel.PrimaryButtonClickCommand.Execute(new DisposableArray(Encoding.UTF8.GetBytes(Password.Password)));
}

private void DestinationFolderPath_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
{
ViewModel.UpdateSuggestions(sender.Text);
}
}
}
}
6 changes: 6 additions & 0 deletions src/Files.App/Services/Settings/GeneralSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ public List<string> PreviousSearchQueriesList
set => Set(value);
}

public List<string> PreviousArchiveExtractionLocations
{
get => Get<List<string>>(null);
set => Set(value);
}

public DateTimeFormats DateTimeFormat
{
get => Get(DateTimeFormats.Application);
Expand Down
1 change: 1 addition & 0 deletions src/Files.App/Services/Settings/UserSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public override object ExportSettings()
export.Remove(nameof(GeneralSettingsService.LastCrashedTabList));
export.Remove(nameof(GeneralSettingsService.PathHistoryList));
export.Remove(nameof(GeneralSettingsService.PreviousSearchQueriesList));
export.Remove(nameof(GeneralSettingsService.PreviousArchiveExtractionLocations));

return JsonSettingsSerializer.SerializeToJson(export);
}
Expand Down
111 changes: 88 additions & 23 deletions src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,57 @@ namespace Files.App.ViewModels.Dialogs
{
public sealed partial class DecompressArchiveDialogViewModel : ObservableObject
{
// Services
private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService<ICommonDialogService>();
private readonly IUserSettingsService UserSettingsService = Ioc.Default.GetRequiredService<IUserSettingsService>();

// Fields
private readonly IStorageFile archive;

// Properties
public BaseStorageFolder DestinationFolder { get; private set; }

private string destinationFolderPath;
public string DestinationFolderPath
{
get => destinationFolderPath;
private set => SetProperty(ref destinationFolderPath, value);
set
{
if (SetProperty(ref destinationFolderPath, value))
{
OnPropertyChanged(nameof(IsDestinationPathValid));
}
}
}

public bool IsDestinationPathValid
{
get
{
try
{
if (string.IsNullOrWhiteSpace(DestinationFolderPath))
return false;

string parentDir = Path.GetDirectoryName(DestinationFolderPath);
string finalSegment = Path.GetFileName(DestinationFolderPath);

// Check parent directory exists
if (string.IsNullOrWhiteSpace(parentDir) || !Directory.Exists(parentDir))
return false;

// Check for invalid characters (IsValidForFilename already does this)
if (!FilesystemHelpers.IsValidForFilename(finalSegment))
return false;

return true;
}
catch
{
// Catch any exception to prevent crashes
return false;
}
}
}

private bool openDestinationFolderOnCompletion;
Expand Down Expand Up @@ -65,31 +105,17 @@ public bool ShowPathSelection
public DisposableArray? Password { get; private set; }

public EncodingItem[] EncodingOptions { get; set; } = EncodingItem.Defaults;
public EncodingItem SelectedEncoding { get; set; }
void RefreshEncodingOptions()
{
if (detectedEncoding != null)
{
EncodingOptions = EncodingItem.Defaults
.Prepend(new EncodingItem(
detectedEncoding,
string.Format(Strings.EncodingDetected.GetLocalizedResource(), detectedEncoding.EncodingName)
))
.ToArray();
}
else
{
EncodingOptions = EncodingItem.Defaults;
}
SelectedEncoding = EncodingOptions.FirstOrDefault();
}

public EncodingItem SelectedEncoding { get; set; }

public ObservableCollection<string> PreviousExtractionLocations { get; } = [];

// Commands
public IRelayCommand PrimaryButtonClickCommand { get; private set; }

public ICommand SelectDestinationCommand { get; private set; }
public ICommand QuerySubmittedCommand { get; private set; }

// Constructor
public DecompressArchiveDialogViewModel(IStorageFile archive)
{
this.archive = archive;
Expand All @@ -101,6 +127,12 @@ public DecompressArchiveDialogViewModel(IStorageFile archive)
PrimaryButtonClickCommand = new RelayCommand<DisposableArray>(password => Password = password);
}

// Private Methods
private string DefaultDestinationFolderPath()
{
return Path.Combine(Path.GetDirectoryName(archive.Path), Path.GetFileNameWithoutExtension(archive.Path));
}

private async Task SelectDestinationAsync()
{
bool result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath);
Expand All @@ -111,9 +143,42 @@ private async Task SelectDestinationAsync()
DestinationFolderPath = (DestinationFolder is not null) ? DestinationFolder.Path : DefaultDestinationFolderPath();
}

private string DefaultDestinationFolderPath()
private void RefreshEncodingOptions()
{
return Path.Combine(Path.GetDirectoryName(archive.Path), Path.GetFileNameWithoutExtension(archive.Path));
if (detectedEncoding != null)
{
EncodingOptions = EncodingItem.Defaults
.Prepend(new EncodingItem(
detectedEncoding,
string.Format(Strings.EncodingDetected.GetLocalizedResource(), detectedEncoding.EncodingName)
))
.ToArray();
}
else
{
EncodingOptions = EncodingItem.Defaults;
}
SelectedEncoding = EncodingOptions.FirstOrDefault();
}

// Public Methods
public void UpdateSuggestions(string query)
{
var allItems = UserSettingsService.GeneralSettingsService.PreviousArchiveExtractionLocations;
if (allItems is null)
return;

var filtered = allItems
.Where(item => item.StartsWith(query, StringComparison.OrdinalIgnoreCase))
.ToList();

// Only update if results changed to prevent flickering
if (!filtered.SequenceEqual(PreviousExtractionLocations))
{
PreviousExtractionLocations.Clear();
foreach (var item in filtered)
PreviousExtractionLocations.Add(item);
}
}
}
}
}
Loading