Skip to content

Commit

Permalink
Feature: Added support for displaying and editing metadata of multipl…
Browse files Browse the repository at this point in the history
…e files (#12476)
  • Loading branch information
hishitetsu authored May 30, 2023
1 parent f9e8f75 commit 4785b53
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 83 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

using Files.Backend.Enums;
using Files.Backend.Helpers;
using Microsoft.UI.Xaml;
using Windows.Storage;

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

if (!compatibilityItemEnabled)
PropertiesNavigationViewItems.Remove(compatibilityItem);

if (!onlyFiles)
PropertiesNavigationViewItems.Remove(detailsItem);

PropertiesNavigationViewItems.Remove(libraryItem);
PropertiesNavigationViewItems.Remove(shortcutItem);
PropertiesNavigationViewItems.Remove(detailsItem);
PropertiesNavigationViewItems.Remove(securityItem);
PropertiesNavigationViewItems.Remove(customizationItem);
PropertiesNavigationViewItems.Remove(hashesItem);
Expand Down
44 changes: 44 additions & 0 deletions src/Files.App/Helpers/LocationHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright(c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

using System.Text.Json;
using Windows.Devices.Geolocation;
using Windows.Services.Maps;
using Windows.Storage;

namespace Files.App.Helpers
{
public static class LocationHelpers
{
public static async Task<string> GetAddressFromCoordinatesAsync(double? Lat, double? Lon)
{
if (!Lat.HasValue || !Lon.HasValue)
return null;

if (string.IsNullOrEmpty(MapService.ServiceToken))
{
try
{
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(new Uri(@"ms-appx:///Resources/BingMapsKey.txt"));
var lines = await FileIO.ReadTextAsync(file);
using var obj = JsonDocument.Parse(lines);
MapService.ServiceToken = obj.RootElement.GetProperty("key").GetString();
}
catch (Exception)
{
return null;
}
}

BasicGeoposition location = new BasicGeoposition();
location.Latitude = Lat.Value;
location.Longitude = Lon.Value;
Geopoint pointToReverseGeocode = new Geopoint(location);

// Reverse geocode the specified geographic location.

var result = await MapLocationFinder.FindLocationsAtAsync(pointToReverseGeocode);
return result?.Locations?.FirstOrDefault()?.DisplayName;
}
}
}
4 changes: 4 additions & 0 deletions src/Files.App/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -3319,4 +3319,8 @@
<data name="Preview" xml:space="preserve">
<value>Preview</value>
</data>
<data name="MultipleValues" xml:space="preserve">
<value>(multiple values)</value>
<comment>Text indicating that multiple selected files have different metadata values.</comment>
</data>
</root>
3 changes: 1 addition & 2 deletions src/Files.App/ViewModels/Previews/BasePreviewModel.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

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

list.Find(x => x.ID is "address").Value = await FileProperties.GetAddressFromCoordinatesAsync(
list.Find(x => x.ID is "address").Value = await LocationHelpers.GetAddressFromCoordinatesAsync(
(double?)list.Find(x => x.Property is "System.GPS.LatitudeDecimal").Value,
(double?)list.Find(x => x.Property is "System.GPS.LongitudeDecimal").Value
);
Expand Down
18 changes: 11 additions & 7 deletions src/Files.App/ViewModels/Properties/BasePropertiesPage.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
using Files.App.Data.Items;
using Files.App.Data.Parameters;
using Files.App.Filesystem;
// Copyright(c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Storage;

namespace Files.App.ViewModels.Properties
Expand Down Expand Up @@ -37,7 +34,14 @@ protected override void OnNavigatedTo(NavigationEventArgs e)
BaseProperties = new DriveProperties(ViewModel, drive, AppInstance);
// Storage objects (multi-selected)
else if (np.Parameter is List<ListedItem> items)
BaseProperties = new CombinedProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, items, AppInstance);
{
// Selection only contains files
if (items.All(item => item.PrimaryItemAttribute == StorageItemTypes.File || item.IsArchive))
BaseProperties = new CombinedFileProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, items, AppInstance);
// Selection includes folders
else
BaseProperties = new CombinedProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, items, AppInstance);
}
// A storage object
else if (np.Parameter is ListedItem item)
{
Expand Down
170 changes: 170 additions & 0 deletions src/Files.App/ViewModels/Properties/Items/CombinedFileProperties.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright(c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

using Files.App.Filesystem.StorageItems;
using Microsoft.UI.Dispatching;

namespace Files.App.ViewModels.Properties
{
internal class CombinedFileProperties : CombinedProperties, IFileProperties
{
public CombinedFileProperties(
SelectedItemsPropertiesViewModel viewModel,
CancellationTokenSource tokenSource,
DispatcherQueue coreDispatcher,
List<ListedItem> listedItems,
IShellPage instance)
: base(viewModel, tokenSource, coreDispatcher, listedItems, instance) { }

public async Task GetSystemFilePropertiesAsync()
{
var queries = await Task.WhenAll(List.AsParallel().Select(async item => {
BaseStorageFile file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath));
if (file is null)
{
// Could not access file, can't show any other property
return null;
}

var list = await FileProperty.RetrieveAndInitializePropertiesAsync(file);

list.Find(x => x.ID == "address").Value =
await LocationHelpers.GetAddressFromCoordinatesAsync((double?)list.Find(
x => x.Property == "System.GPS.LatitudeDecimal").Value,
(double?)list.Find(x => x.Property == "System.GPS.LongitudeDecimal").Value);

// Find Encoding Bitrate property and convert it to kbps
var encodingBitrate = list.Find(x => x.Property == "System.Audio.EncodingBitrate");
if (encodingBitrate?.Value is not null)
{
var sizes = new string[] { "Bps", "KBps", "MBps", "GBps" };
var order = (int)Math.Floor(Math.Log((uint)encodingBitrate.Value, 1024));
var readableSpeed = (uint)encodingBitrate.Value / Math.Pow(1024, order);
encodingBitrate.Value = $"{readableSpeed:0.##} {sizes[order]}";
}

return list
.Where(fileProp => !(fileProp.Value is null && fileProp.IsReadOnly))
.GroupBy(fileProp => fileProp.SectionResource)
.Select(group => new FilePropertySection(group) { Key = group.Key })
.Where(section => !section.All(fileProp => fileProp.Value is null));
}));

if (queries.Any(query => query is null))
return;

// Display only the sections that all files have
var keys = queries.Select(query => query!.Select(section => section.Key)).Aggregate((x, y) => x.Intersect(y));
var sections = queries[0]!.Where(section => keys.Contains(section.Key)).OrderBy(group => group.Priority).ToArray();

foreach (var group in sections)
{
var props = queries.SelectMany(query => query!.First(section => section.Key == group.Key));
foreach (FileProperty prop in group)
{
if (props.Where(x => x.Property == prop.Property).Any(x => !Equals(x.Value, prop.Value)))
{
// Has multiple values
prop.Value = null;
prop.PlaceholderText = "MultipleValues".GetLocalizedResource();
}
}
}

ViewModel.PropertySections = new ObservableCollection<FilePropertySection>(sections);
}

public async Task SyncPropertyChangesAsync()
{
var files = new List<BaseStorageFile>();
foreach (var item in List)
{
BaseStorageFile file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath));

// Couldn't access the file to save properties
if (file is null)
return;

files.Add(file);
}

var failedProperties = "";

foreach (var group in ViewModel.PropertySections)
{
foreach (FileProperty prop in group)
{
if (!prop.IsReadOnly && prop.Modified)
{
var newDict = new Dictionary<string, object>();
newDict.Add(prop.Property, prop.Value);

foreach (var file in files)
{
try
{
if (file.Properties is not null)
{
await file.Properties.SavePropertiesAsync(newDict);
}
}
catch
{
failedProperties += $"{file.Name}: {prop.Name}\n";
}
}
}
}
}

if (!string.IsNullOrWhiteSpace(failedProperties))
{
throw new Exception($"The following properties failed to save: {failedProperties}");
}
}

public async Task ClearPropertiesAsync()
{
var failedProperties = new List<string>();
var files = new List<BaseStorageFile>();
foreach (var item in List)
{
BaseStorageFile file = await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFileFromPathAsync(item.ItemPath));

if (file is null)
return;

files.Add(file);
}

foreach (var group in ViewModel.PropertySections)
{
foreach (FileProperty prop in group)
{
if (!prop.IsReadOnly)
{
var newDict = new Dictionary<string, object>();
newDict.Add(prop.Property, null);

foreach (var file in files)
{
try
{
if (file.Properties is not null)
{
await file.Properties.SavePropertiesAsync(newDict);
}
}
catch
{
failedProperties.Add(prop.Name);
}
}
}
}
}

_ = GetSystemFilePropertiesAsync();
}
}
}
Loading

0 comments on commit 4785b53

Please sign in to comment.