Skip to content

Commit 5ddc007

Browse files
authored
Feature: Added previous locations dropdown to the extract archive dialog (#17783)
1 parent d459f87 commit 5ddc007

File tree

7 files changed

+130
-26
lines changed

7 files changed

+130
-26
lines changed

src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ namespace Files.App.Actions
1414
[GeneratedRichCommand]
1515
internal sealed partial class DecompressArchiveAction : BaseDecompressArchiveAction
1616
{
17+
private readonly IUserSettingsService UserSettingsService = Ioc.Default.GetRequiredService<IUserSettingsService>();
18+
1719
public override string Label
1820
=> Strings.ExtractFiles.GetLocalizedResource();
1921

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

86+
// Save extraction location for future use
87+
SaveExtractionLocation(destinationFolderPath);
88+
8489
if (destinationFolder is null)
8590
{
8691
BaseStorageFolder parentFolder = await StorageHelpers.ToStorageItem<BaseStorageFolder>(Path.GetDirectoryName(archive.Path) ?? string.Empty);
@@ -119,5 +124,17 @@ protected override bool CanDecompressSelectedItems()
119124

120125
return null;
121126
}
127+
128+
private void SaveExtractionLocation(string path)
129+
{
130+
var previousArchiveExtractionLocations = UserSettingsService.GeneralSettingsService.PreviousArchiveExtractionLocations?.ToList() ?? [];
131+
previousArchiveExtractionLocations.Remove(path);
132+
previousArchiveExtractionLocations.Insert(0, path);
133+
134+
if (previousArchiveExtractionLocations.Count > 10)
135+
UserSettingsService.GeneralSettingsService.PreviousArchiveExtractionLocations = previousArchiveExtractionLocations.RemoveFrom(11);
136+
else
137+
UserSettingsService.GeneralSettingsService.PreviousArchiveExtractionLocations = previousArchiveExtractionLocations;
138+
}
122139
}
123140
}

src/Files.App/Data/Contracts/IGeneralSettingsService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ public interface IGeneralSettingsService : IBaseSettingsService, INotifyProperty
5555
/// </summary>
5656
List<string> PreviousSearchQueriesList { get; set; }
5757

58+
/// <summary>
59+
/// Stores list of paths where archives have previously been extracted.
60+
/// </summary>
61+
List<string> PreviousArchiveExtractionLocations { get; set; }
62+
5863
/// <summary>
5964
/// Gets or sets a value indicating which date and time format to use.
6065
/// </summary>

src/Files.App/Dialogs/DecompressArchiveDialog.xaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
CornerRadius="{StaticResource OverlayCornerRadius}"
1313
DefaultButton="Primary"
1414
HighContrastAdjustment="None"
15+
IsPrimaryButtonEnabled="{x:Bind ViewModel.IsDestinationPathValid, Mode=OneWay}"
1516
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
1617
PrimaryButtonText="{helpers:ResourceString Name=Extract}"
1718
RequestedTheme="{x:Bind RootAppElement.RequestedTheme, Mode=OneWay}"
@@ -44,13 +45,14 @@
4445
Text="{helpers:ResourceString Name=ExtractToPath}" />
4546

4647
<!-- Path Box -->
47-
<TextBox
48+
<AutoSuggestBox
4849
x:Name="DestinationFolderPath"
4950
Grid.Row="1"
5051
Grid.Column="0"
5152
HorizontalAlignment="Stretch"
52-
IsReadOnly="True"
53-
Text="{x:Bind ViewModel.DestinationFolderPath, Mode=OneWay}" />
53+
ItemsSource="{x:Bind ViewModel.PreviousExtractionLocations, Mode=OneWay}"
54+
Text="{x:Bind ViewModel.DestinationFolderPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
55+
TextChanged="DestinationFolderPath_TextChanged" />
5456

5557
<Button
5658
x:Name="SelectDestination"

src/Files.App/Dialogs/DecompressArchiveDialog.xaml.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,13 @@ private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialo
2828
if (ViewModel.IsArchiveEncrypted)
2929
ViewModel.PrimaryButtonClickCommand.Execute(new DisposableArray(Encoding.UTF8.GetBytes(Password.Password)));
3030
}
31+
32+
private void DestinationFolderPath_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
33+
{
34+
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
35+
{
36+
ViewModel.UpdateSuggestions(sender.Text);
37+
}
38+
}
3139
}
3240
}

src/Files.App/Services/Settings/GeneralSettingsService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ public List<string> PreviousSearchQueriesList
7171
set => Set(value);
7272
}
7373

74+
public List<string> PreviousArchiveExtractionLocations
75+
{
76+
get => Get<List<string>>(null);
77+
set => Set(value);
78+
}
79+
7480
public DateTimeFormats DateTimeFormat
7581
{
7682
get => Get(DateTimeFormats.Application);

src/Files.App/Services/Settings/UserSettingsService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public override object ExportSettings()
7070
export.Remove(nameof(GeneralSettingsService.LastCrashedTabList));
7171
export.Remove(nameof(GeneralSettingsService.PathHistoryList));
7272
export.Remove(nameof(GeneralSettingsService.PreviousSearchQueriesList));
73+
export.Remove(nameof(GeneralSettingsService.PreviousArchiveExtractionLocations));
7374

7475
return JsonSettingsSerializer.SerializeToJson(export);
7576
}

src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs

Lines changed: 88 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,57 @@ namespace Files.App.ViewModels.Dialogs
1010
{
1111
public sealed partial class DecompressArchiveDialogViewModel : ObservableObject
1212
{
13+
// Services
1314
private ICommonDialogService CommonDialogService { get; } = Ioc.Default.GetRequiredService<ICommonDialogService>();
15+
private readonly IUserSettingsService UserSettingsService = Ioc.Default.GetRequiredService<IUserSettingsService>();
1416

17+
// Fields
1518
private readonly IStorageFile archive;
1619

20+
// Properties
1721
public BaseStorageFolder DestinationFolder { get; private set; }
1822

1923
private string destinationFolderPath;
2024
public string DestinationFolderPath
2125
{
2226
get => destinationFolderPath;
23-
private set => SetProperty(ref destinationFolderPath, value);
27+
set
28+
{
29+
if (SetProperty(ref destinationFolderPath, value))
30+
{
31+
OnPropertyChanged(nameof(IsDestinationPathValid));
32+
}
33+
}
34+
}
35+
36+
public bool IsDestinationPathValid
37+
{
38+
get
39+
{
40+
try
41+
{
42+
if (string.IsNullOrWhiteSpace(DestinationFolderPath))
43+
return false;
44+
45+
string parentDir = Path.GetDirectoryName(DestinationFolderPath);
46+
string finalSegment = Path.GetFileName(DestinationFolderPath);
47+
48+
// Check parent directory exists
49+
if (string.IsNullOrWhiteSpace(parentDir) || !Directory.Exists(parentDir))
50+
return false;
51+
52+
// Check for invalid characters (IsValidForFilename already does this)
53+
if (!FilesystemHelpers.IsValidForFilename(finalSegment))
54+
return false;
55+
56+
return true;
57+
}
58+
catch
59+
{
60+
// Catch any exception to prevent crashes
61+
return false;
62+
}
63+
}
2464
}
2565

2666
private bool openDestinationFolderOnCompletion;
@@ -65,31 +105,17 @@ public bool ShowPathSelection
65105
public DisposableArray? Password { get; private set; }
66106

67107
public EncodingItem[] EncodingOptions { get; set; } = EncodingItem.Defaults;
68-
public EncodingItem SelectedEncoding { get; set; }
69-
void RefreshEncodingOptions()
70-
{
71-
if (detectedEncoding != null)
72-
{
73-
EncodingOptions = EncodingItem.Defaults
74-
.Prepend(new EncodingItem(
75-
detectedEncoding,
76-
string.Format(Strings.EncodingDetected.GetLocalizedResource(), detectedEncoding.EncodingName)
77-
))
78-
.ToArray();
79-
}
80-
else
81-
{
82-
EncodingOptions = EncodingItem.Defaults;
83-
}
84-
SelectedEncoding = EncodingOptions.FirstOrDefault();
85-
}
86108

109+
public EncodingItem SelectedEncoding { get; set; }
87110

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

113+
// Commands
89114
public IRelayCommand PrimaryButtonClickCommand { get; private set; }
90-
91115
public ICommand SelectDestinationCommand { get; private set; }
116+
public ICommand QuerySubmittedCommand { get; private set; }
92117

118+
// Constructor
93119
public DecompressArchiveDialogViewModel(IStorageFile archive)
94120
{
95121
this.archive = archive;
@@ -101,6 +127,12 @@ public DecompressArchiveDialogViewModel(IStorageFile archive)
101127
PrimaryButtonClickCommand = new RelayCommand<DisposableArray>(password => Password = password);
102128
}
103129

130+
// Private Methods
131+
private string DefaultDestinationFolderPath()
132+
{
133+
return Path.Combine(Path.GetDirectoryName(archive.Path), Path.GetFileNameWithoutExtension(archive.Path));
134+
}
135+
104136
private async Task SelectDestinationAsync()
105137
{
106138
bool result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath);
@@ -111,9 +143,42 @@ private async Task SelectDestinationAsync()
111143
DestinationFolderPath = (DestinationFolder is not null) ? DestinationFolder.Path : DefaultDestinationFolderPath();
112144
}
113145

114-
private string DefaultDestinationFolderPath()
146+
private void RefreshEncodingOptions()
115147
{
116-
return Path.Combine(Path.GetDirectoryName(archive.Path), Path.GetFileNameWithoutExtension(archive.Path));
148+
if (detectedEncoding != null)
149+
{
150+
EncodingOptions = EncodingItem.Defaults
151+
.Prepend(new EncodingItem(
152+
detectedEncoding,
153+
string.Format(Strings.EncodingDetected.GetLocalizedResource(), detectedEncoding.EncodingName)
154+
))
155+
.ToArray();
156+
}
157+
else
158+
{
159+
EncodingOptions = EncodingItem.Defaults;
160+
}
161+
SelectedEncoding = EncodingOptions.FirstOrDefault();
162+
}
163+
164+
// Public Methods
165+
public void UpdateSuggestions(string query)
166+
{
167+
var allItems = UserSettingsService.GeneralSettingsService.PreviousArchiveExtractionLocations;
168+
if (allItems is null)
169+
return;
170+
171+
var filtered = allItems
172+
.Where(item => item.StartsWith(query, StringComparison.OrdinalIgnoreCase))
173+
.ToList();
174+
175+
// Only update if results changed to prevent flickering
176+
if (!filtered.SequenceEqual(PreviousExtractionLocations))
177+
{
178+
PreviousExtractionLocations.Clear();
179+
foreach (var item in filtered)
180+
PreviousExtractionLocations.Add(item);
181+
}
117182
}
118183
}
119-
}
184+
}

0 commit comments

Comments
 (0)