Skip to content

Commit 4785b53

Browse files
authored
Feature: Added support for displaying and editing metadata of multiple files (#12476)
1 parent f9e8f75 commit 4785b53

File tree

11 files changed

+295
-83
lines changed

11 files changed

+295
-83
lines changed

src/Files.App/Data/Factories/PropertiesNavigationViewItemFactory.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// Copyright (c) 2023 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4-
using Files.Backend.Enums;
54
using Files.Backend.Helpers;
65
using Microsoft.UI.Xaml;
6+
using Windows.Storage;
77

88
namespace Files.App.Data.Factories
99
{
@@ -75,13 +75,16 @@ public static ObservableCollection<NavigationViewItemButtonStyleItem> Initialize
7575
{
7676
var commonFileExt = listedItems.Select(x => x.FileExtension).Distinct().Count() == 1 ? listedItems.First().FileExtension : null;
7777
var compatibilityItemEnabled = listedItems.All(listedItem => FileExtensionHelpers.IsExecutableFile(listedItem is ShortcutItem sht ? sht.TargetPath : commonFileExt, true));
78+
var onlyFiles = listedItems.All(listedItem => listedItem.PrimaryItemAttribute == StorageItemTypes.File || listedItem.IsArchive);
7879

7980
if (!compatibilityItemEnabled)
8081
PropertiesNavigationViewItems.Remove(compatibilityItem);
8182

83+
if (!onlyFiles)
84+
PropertiesNavigationViewItems.Remove(detailsItem);
85+
8286
PropertiesNavigationViewItems.Remove(libraryItem);
8387
PropertiesNavigationViewItems.Remove(shortcutItem);
84-
PropertiesNavigationViewItems.Remove(detailsItem);
8588
PropertiesNavigationViewItems.Remove(securityItem);
8689
PropertiesNavigationViewItems.Remove(customizationItem);
8790
PropertiesNavigationViewItems.Remove(hashesItem);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright(c) 2023 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using System.Text.Json;
5+
using Windows.Devices.Geolocation;
6+
using Windows.Services.Maps;
7+
using Windows.Storage;
8+
9+
namespace Files.App.Helpers
10+
{
11+
public static class LocationHelpers
12+
{
13+
public static async Task<string> GetAddressFromCoordinatesAsync(double? Lat, double? Lon)
14+
{
15+
if (!Lat.HasValue || !Lon.HasValue)
16+
return null;
17+
18+
if (string.IsNullOrEmpty(MapService.ServiceToken))
19+
{
20+
try
21+
{
22+
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(new Uri(@"ms-appx:///Resources/BingMapsKey.txt"));
23+
var lines = await FileIO.ReadTextAsync(file);
24+
using var obj = JsonDocument.Parse(lines);
25+
MapService.ServiceToken = obj.RootElement.GetProperty("key").GetString();
26+
}
27+
catch (Exception)
28+
{
29+
return null;
30+
}
31+
}
32+
33+
BasicGeoposition location = new BasicGeoposition();
34+
location.Latitude = Lat.Value;
35+
location.Longitude = Lon.Value;
36+
Geopoint pointToReverseGeocode = new Geopoint(location);
37+
38+
// Reverse geocode the specified geographic location.
39+
40+
var result = await MapLocationFinder.FindLocationsAtAsync(pointToReverseGeocode);
41+
return result?.Locations?.FirstOrDefault()?.DisplayName;
42+
}
43+
}
44+
}

src/Files.App/Strings/en-US/Resources.resw

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3319,4 +3319,8 @@
33193319
<data name="Preview" xml:space="preserve">
33203320
<value>Preview</value>
33213321
</data>
3322+
<data name="MultipleValues" xml:space="preserve">
3323+
<value>(multiple values)</value>
3324+
<comment>Text indicating that multiple selected files have different metadata values.</comment>
3325+
</data>
33223326
</root>

src/Files.App/ViewModels/Previews/BasePreviewModel.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) 2023 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4-
using CommunityToolkit.WinUI;
54
using Files.App.Filesystem.StorageItems;
65
using Files.App.ViewModels.Properties;
76
using Microsoft.UI.Xaml;
@@ -120,7 +119,7 @@ private async Task<List<FileProperty>> GetSystemFilePropertiesAsync()
120119
var list = await FileProperty.RetrieveAndInitializePropertiesAsync(Item.ItemFile,
121120
Constants.ResourceFilePaths.PreviewPaneDetailsPropertiesJsonPath);
122121

123-
list.Find(x => x.ID is "address").Value = await FileProperties.GetAddressFromCoordinatesAsync(
122+
list.Find(x => x.ID is "address").Value = await LocationHelpers.GetAddressFromCoordinatesAsync(
124123
(double?)list.Find(x => x.Property is "System.GPS.LatitudeDecimal").Value,
125124
(double?)list.Find(x => x.Property is "System.GPS.LongitudeDecimal").Value
126125
);

src/Files.App/ViewModels/Properties/BasePropertiesPage.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
using Files.App.Data.Items;
2-
using Files.App.Data.Parameters;
3-
using Files.App.Filesystem;
1+
// Copyright(c) 2023 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
44
using Microsoft.UI.Xaml;
55
using Microsoft.UI.Xaml.Controls;
66
using Microsoft.UI.Xaml.Navigation;
7-
using System;
8-
using System.Collections.Generic;
9-
using System.Threading.Tasks;
107
using Windows.Storage;
118

129
namespace Files.App.ViewModels.Properties
@@ -37,7 +34,14 @@ protected override void OnNavigatedTo(NavigationEventArgs e)
3734
BaseProperties = new DriveProperties(ViewModel, drive, AppInstance);
3835
// Storage objects (multi-selected)
3936
else if (np.Parameter is List<ListedItem> items)
40-
BaseProperties = new CombinedProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, items, AppInstance);
37+
{
38+
// Selection only contains files
39+
if (items.All(item => item.PrimaryItemAttribute == StorageItemTypes.File || item.IsArchive))
40+
BaseProperties = new CombinedFileProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, items, AppInstance);
41+
// Selection includes folders
42+
else
43+
BaseProperties = new CombinedProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, items, AppInstance);
44+
}
4145
// A storage object
4246
else if (np.Parameter is ListedItem item)
4347
{
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Copyright(c) 2023 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using Files.App.Filesystem.StorageItems;
5+
using Microsoft.UI.Dispatching;
6+
7+
namespace Files.App.ViewModels.Properties
8+
{
9+
internal class CombinedFileProperties : CombinedProperties, IFileProperties
10+
{
11+
public CombinedFileProperties(
12+
SelectedItemsPropertiesViewModel viewModel,
13+
CancellationTokenSource tokenSource,
14+
DispatcherQueue coreDispatcher,
15+
List<ListedItem> listedItems,
16+
IShellPage instance)
17+
: base(viewModel, tokenSource, coreDispatcher, listedItems, instance) { }
18+
19+
public async Task GetSystemFilePropertiesAsync()
20+
{
21+
var queries = await Task.WhenAll(List.AsParallel().Select(async item => {
22+
BaseStorageFile file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath));
23+
if (file is null)
24+
{
25+
// Could not access file, can't show any other property
26+
return null;
27+
}
28+
29+
var list = await FileProperty.RetrieveAndInitializePropertiesAsync(file);
30+
31+
list.Find(x => x.ID == "address").Value =
32+
await LocationHelpers.GetAddressFromCoordinatesAsync((double?)list.Find(
33+
x => x.Property == "System.GPS.LatitudeDecimal").Value,
34+
(double?)list.Find(x => x.Property == "System.GPS.LongitudeDecimal").Value);
35+
36+
// Find Encoding Bitrate property and convert it to kbps
37+
var encodingBitrate = list.Find(x => x.Property == "System.Audio.EncodingBitrate");
38+
if (encodingBitrate?.Value is not null)
39+
{
40+
var sizes = new string[] { "Bps", "KBps", "MBps", "GBps" };
41+
var order = (int)Math.Floor(Math.Log((uint)encodingBitrate.Value, 1024));
42+
var readableSpeed = (uint)encodingBitrate.Value / Math.Pow(1024, order);
43+
encodingBitrate.Value = $"{readableSpeed:0.##} {sizes[order]}";
44+
}
45+
46+
return list
47+
.Where(fileProp => !(fileProp.Value is null && fileProp.IsReadOnly))
48+
.GroupBy(fileProp => fileProp.SectionResource)
49+
.Select(group => new FilePropertySection(group) { Key = group.Key })
50+
.Where(section => !section.All(fileProp => fileProp.Value is null));
51+
}));
52+
53+
if (queries.Any(query => query is null))
54+
return;
55+
56+
// Display only the sections that all files have
57+
var keys = queries.Select(query => query!.Select(section => section.Key)).Aggregate((x, y) => x.Intersect(y));
58+
var sections = queries[0]!.Where(section => keys.Contains(section.Key)).OrderBy(group => group.Priority).ToArray();
59+
60+
foreach (var group in sections)
61+
{
62+
var props = queries.SelectMany(query => query!.First(section => section.Key == group.Key));
63+
foreach (FileProperty prop in group)
64+
{
65+
if (props.Where(x => x.Property == prop.Property).Any(x => !Equals(x.Value, prop.Value)))
66+
{
67+
// Has multiple values
68+
prop.Value = null;
69+
prop.PlaceholderText = "MultipleValues".GetLocalizedResource();
70+
}
71+
}
72+
}
73+
74+
ViewModel.PropertySections = new ObservableCollection<FilePropertySection>(sections);
75+
}
76+
77+
public async Task SyncPropertyChangesAsync()
78+
{
79+
var files = new List<BaseStorageFile>();
80+
foreach (var item in List)
81+
{
82+
BaseStorageFile file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath));
83+
84+
// Couldn't access the file to save properties
85+
if (file is null)
86+
return;
87+
88+
files.Add(file);
89+
}
90+
91+
var failedProperties = "";
92+
93+
foreach (var group in ViewModel.PropertySections)
94+
{
95+
foreach (FileProperty prop in group)
96+
{
97+
if (!prop.IsReadOnly && prop.Modified)
98+
{
99+
var newDict = new Dictionary<string, object>();
100+
newDict.Add(prop.Property, prop.Value);
101+
102+
foreach (var file in files)
103+
{
104+
try
105+
{
106+
if (file.Properties is not null)
107+
{
108+
await file.Properties.SavePropertiesAsync(newDict);
109+
}
110+
}
111+
catch
112+
{
113+
failedProperties += $"{file.Name}: {prop.Name}\n";
114+
}
115+
}
116+
}
117+
}
118+
}
119+
120+
if (!string.IsNullOrWhiteSpace(failedProperties))
121+
{
122+
throw new Exception($"The following properties failed to save: {failedProperties}");
123+
}
124+
}
125+
126+
public async Task ClearPropertiesAsync()
127+
{
128+
var failedProperties = new List<string>();
129+
var files = new List<BaseStorageFile>();
130+
foreach (var item in List)
131+
{
132+
BaseStorageFile file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath));
133+
134+
if (file is null)
135+
return;
136+
137+
files.Add(file);
138+
}
139+
140+
foreach (var group in ViewModel.PropertySections)
141+
{
142+
foreach (FileProperty prop in group)
143+
{
144+
if (!prop.IsReadOnly)
145+
{
146+
var newDict = new Dictionary<string, object>();
147+
newDict.Add(prop.Property, null);
148+
149+
foreach (var file in files)
150+
{
151+
try
152+
{
153+
if (file.Properties is not null)
154+
{
155+
await file.Properties.SavePropertiesAsync(newDict);
156+
}
157+
}
158+
catch
159+
{
160+
failedProperties.Add(prop.Name);
161+
}
162+
}
163+
}
164+
}
165+
}
166+
167+
_ = GetSystemFilePropertiesAsync();
168+
}
169+
}
170+
}

0 commit comments

Comments
 (0)