Skip to content

Commit 4bbd56d

Browse files
authored
Feature: Added support for editing album covers (#14485)
1 parent 54f58fc commit 4bbd56d

File tree

8 files changed

+185
-10
lines changed

8 files changed

+185
-10
lines changed

src/Files.App/Data/Models/SelectedItemsPropertiesViewModel.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
using Files.App.ViewModels.Properties;
55
using Files.Shared.Helpers;
66
using System.Windows.Input;
7+
using TagLib;
8+
using Windows.Storage;
9+
using Windows.Storage.FileProperties;
10+
using Windows.Storage.Pickers;
711

812
namespace Files.App.Data.Models
913
{
@@ -480,6 +484,13 @@ public ICommand FormatDriveCommand
480484
get => formatDriveCommand;
481485
set => SetProperty(ref formatDriveCommand, value);
482486
}
487+
488+
private ICommand editAlbumCoverCommand;
489+
public ICommand EditAlbumCoverCommand
490+
{
491+
get => editAlbumCoverCommand;
492+
set => SetProperty(ref editAlbumCoverCommand, value);
493+
}
483494

484495
private bool itemAttributesVisibility = true;
485496
public bool ItemAttributesVisibility
@@ -511,6 +522,7 @@ public bool IsItemSelected
511522

512523
public SelectedItemsPropertiesViewModel()
513524
{
525+
514526
}
515527

516528
private bool isSelectedItemImage = false;
@@ -691,5 +703,26 @@ public bool IsUnblockFileSelected
691703
get => isUnblockFileSelected;
692704
set => SetProperty(ref isUnblockFileSelected, value);
693705
}
706+
707+
private bool isAblumCoverModified;
708+
public bool IsAblumCoverModified
709+
{
710+
get => isAblumCoverModified;
711+
set => SetProperty(ref isAblumCoverModified, value);
712+
}
713+
714+
private bool isEditAlbumCoverVisible;
715+
public bool IsEditAlbumCoverVisible
716+
{
717+
get => isEditAlbumCoverVisible;
718+
set => SetProperty(ref isEditAlbumCoverVisible, value);
719+
}
720+
721+
private Picture modifiedAlbumCover;
722+
public Picture ModifiedAlbumCover
723+
{
724+
get => modifiedAlbumCover;
725+
set => SetProperty(ref modifiedAlbumCover, value);
726+
}
694727
}
695728
}

src/Files.App/Files.App.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
9494
<PackageReference Include="CommunityToolkit.WinUI.UI.Behaviors" Version="7.1.2" />
9595
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
96+
<PackageReference Include="TagLibSharp" Version="2.3.0" />
9697
<PackageReference Include="Tulpep.ActiveDirectoryObjectPicker" Version="3.0.11" />
9798
<PackageReference Include="Vanara.PInvoke.DwmApi" Version="3.4.17" />
9899
<PackageReference Include="Vanara.Windows.Extensions" Version="3.4.17" />
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using TagLib;
2+
3+
namespace Files.App.Helpers
4+
{
5+
internal class MediaFileHelper
6+
{
7+
/// <summary>
8+
/// Changes the album cover for a given <paramref name="filePath"/>.
9+
/// </summary>
10+
/// <param name="filePath">The file path to change the album cover.</param>
11+
/// <param name="albumCover">The album cover to use.</param>
12+
public static bool ChangeAlbumCover(string filePath, Picture albumCover)
13+
{
14+
try
15+
{
16+
File mediaFile = File.Create(filePath);
17+
IPicture[] pictures = [albumCover];
18+
mediaFile.Tag.Pictures = pictures;
19+
mediaFile.Save();
20+
mediaFile.Dispose();
21+
22+
return true;
23+
}
24+
catch (Exception)
25+
{
26+
return false;
27+
}
28+
29+
}
30+
}
31+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3686,4 +3686,7 @@
36863686
<data name="NewWindowDescription" xml:space="preserve">
36873687
<value>Open new window</value>
36883688
</data>
3689+
<data name="ChangeAlbumCover" xml:space="preserve">
3690+
<value>Change album cover</value>
3691+
</data>
36893692
</root>

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

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

4+
using Files.Shared.Helpers;
45
using Microsoft.UI.Xaml;
56
using Microsoft.UI.Xaml.Controls;
67
using Microsoft.UI.Xaml.Navigation;
8+
using TagLib;
79
using Windows.Storage;
10+
using Windows.Storage.FileProperties;
11+
using Windows.Storage.Pickers;
812

913
namespace Files.App.ViewModels.Properties
1014
{
@@ -53,7 +57,13 @@ protected override void OnNavigatedTo(NavigationEventArgs e)
5357
{
5458
// Selection only contains files
5559
if (items.All(item => item.PrimaryItemAttribute == StorageItemTypes.File || item.IsArchive))
60+
{
5661
BaseProperties = new CombinedFileProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, items, AppInstance);
62+
63+
ViewModel.IsEditAlbumCoverVisible =
64+
items.All(item => FileExtensionHelpers.IsVideoFile(item.FileExtension)) ||
65+
items.All(item => FileExtensionHelpers.IsAudioFile(item.FileExtension));
66+
}
5767
// Selection includes folders
5868
else
5969
BaseProperties = new CombinedProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, items, AppInstance);
@@ -69,6 +79,28 @@ protected override void OnNavigatedTo(NavigationEventArgs e)
6979
BaseProperties = new FolderProperties(ViewModel, np.CancellationTokenSource, DispatcherQueue, item, AppInstance);
7080
}
7181

82+
ViewModel.EditAlbumCoverCommand = new RelayCommand(async () =>
83+
{
84+
FileOpenPicker filePicker = new FileOpenPicker();
85+
filePicker.FileTypeFilter.Add(".jpg");
86+
filePicker.FileTypeFilter.Add(".jpeg");
87+
filePicker.FileTypeFilter.Add(".bmp");
88+
filePicker.FileTypeFilter.Add(".png");
89+
90+
var parentWindowId = np.Window.AppWindow.Id;
91+
var handle = Microsoft.UI.Win32Interop.GetWindowFromWindowId(parentWindowId);
92+
WinRT.Interop.InitializeWithWindow.Initialize(filePicker, handle);
93+
94+
StorageFile file = await filePicker.PickSingleFileAsync();
95+
96+
if (file is not null)
97+
{
98+
ViewModel.IsAblumCoverModified = true;
99+
ViewModel.ModifiedAlbumCover = new Picture(file.Path);
100+
ViewModel.IconData = await FileThumbnailHelper.LoadIconFromPathAsync(file.Path, 80, ThumbnailMode.DocumentsView, ThumbnailOptions.ResizeThumbnail, false);
101+
}
102+
});
103+
72104
base.OnNavigatedTo(e);
73105
}
74106

src/Files.App/ViewModels/Properties/Items/FileProperties.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public override void GetBaseProperties()
4545
ViewModel.CustomIconSource = Item.CustomIconSource;
4646
ViewModel.LoadFileIcon = Item.LoadFileIcon;
4747
ViewModel.IsDownloadedFile = NativeFileOperationsHelper.ReadStringFromFile($"{Item.ItemPath}:Zone.Identifier") is not null;
48+
ViewModel.IsEditAlbumCoverVisible =
49+
FileExtensionHelpers.IsVideoFile(Item.FileExtension) ||
50+
FileExtensionHelpers.IsAudioFile(Item.FileExtension);
4851

4952
if (!Item.IsShortcut)
5053
return;

src/Files.App/Views/Properties/GeneralPage.xaml

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
x:Class="Files.App.Views.Properties.GeneralPage"
44
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
55
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
6+
xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
7+
xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
68
xmlns:controls="using:Files.App.UserControls.Settings"
79
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
810
xmlns:helpers="using:Files.App.Helpers"
@@ -64,19 +66,70 @@
6466
</Grid.RowDefinitions>
6567

6668
<!-- File Icon -->
67-
<uc:FileIcon
68-
x:Name="Icon"
69+
<Grid
6970
Grid.Row="0"
7071
Grid.Column="0"
71-
Width="40"
72-
Height="40"
72+
Width="56"
73+
Height="56"
7374
Margin="{StaticResource PropertyNameMargin}"
7475
HorizontalAlignment="Left"
7576
VerticalAlignment="Center"
76-
AutomationProperties.AccessibilityView="Raw"
77-
FileIconImageData="{x:Bind ViewModel.IconData, Mode=OneWay}"
78-
ItemSize="40"
79-
ViewModel="{x:Bind ViewModel}" />
77+
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
78+
BorderThickness="1"
79+
CornerRadius="4">
80+
81+
<uc:FileIcon
82+
x:Name="Icon"
83+
Width="48"
84+
Height="48"
85+
HorizontalAlignment="Center"
86+
VerticalAlignment="Center"
87+
AutomationProperties.AccessibilityView="Raw"
88+
FileIconImageData="{x:Bind ViewModel.IconData, Mode=OneWay}"
89+
ItemSize="48"
90+
ViewModel="{x:Bind ViewModel}" />
91+
92+
<!-- Edit Album Cover Button -->
93+
<Button
94+
x:Name="EditAlbumCoverButton"
95+
Grid.Row="0"
96+
Grid.Column="0"
97+
Width="56"
98+
Height="56"
99+
Padding="0"
100+
HorizontalAlignment="Center"
101+
VerticalAlignment="Center"
102+
x:Load="{x:Bind ViewModel.IsEditAlbumCoverVisible, Mode=OneWay}"
103+
AutomationProperties.Name="{helpers:ResourceString Name=ChangeAlbumCover}"
104+
Background="Transparent"
105+
BorderThickness="0"
106+
Command="{x:Bind ViewModel.EditAlbumCoverCommand, Mode=OneWay}"
107+
Opacity="0"
108+
ToolTipService.ToolTip="{helpers:ResourceString Name=ChangeAlbumCover}">
109+
<Button.Resources>
110+
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="Transparent" />
111+
</Button.Resources>
112+
<Border
113+
Width="32"
114+
Height="32"
115+
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}"
116+
CornerRadius="30">
117+
<FontIcon
118+
FontSize="12"
119+
Foreground="{ThemeResource TextFillColorPrimary}"
120+
Glyph="&#xE70F;" />
121+
</Border>
122+
123+
<Interactivity:Interaction.Behaviors>
124+
<Core:EventTriggerBehavior EventName="PointerEntered">
125+
<Core:ChangePropertyAction PropertyName="Opacity" Value="1.0" />
126+
</Core:EventTriggerBehavior>
127+
<Core:EventTriggerBehavior EventName="PointerExited">
128+
<Core:ChangePropertyAction PropertyName="Opacity" Value="0" />
129+
</Core:EventTriggerBehavior>
130+
</Interactivity:Interaction.Behaviors>
131+
</Button>
132+
</Grid>
80133

81134
<!-- Name & Folder/File Count -->
82135
<StackPanel

src/Files.App/Views/Properties/GeneralPage.xaml.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
// Licensed under the MIT License. See the LICENSE.
33

44
using Files.App.ViewModels.Properties;
5+
using Microsoft.UI.Dispatching;
56
using Microsoft.UI.Xaml;
67
using Microsoft.UI.Xaml.Input;
78
using System.IO;
89
using System.Text.RegularExpressions;
910
using Windows.Storage;
10-
using Microsoft.UI.Dispatching;
1111

1212
namespace Files.App.Views.Properties
1313
{
@@ -16,7 +16,6 @@ public sealed partial class GeneralPage : BasePropertiesPage
1616
private readonly Regex letterRegex = new(@"\s*\(\w:\)$");
1717

1818
private readonly DispatcherQueueTimer _updateDateDisplayTimer;
19-
2019
public GeneralPage()
2120
{
2221
InitializeComponent();
@@ -135,6 +134,16 @@ async Task<bool> SaveCombinedAsync(IList<ListedItem> fileOrFolders)
135134
await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() =>
136135
UIFilesystemHelpers.SetHiddenAttributeItem(fileOrFolder, ViewModel.IsHidden, itemMM)
137136
);
137+
138+
if (ViewModel.IsAblumCoverModified)
139+
{
140+
MediaFileHelper.ChangeAlbumCover(fileOrFolder.ItemPath, ViewModel.ModifiedAlbumCover);
141+
142+
await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() =>
143+
{
144+
AppInstance?.FilesystemViewModel?.RefreshItems(null);
145+
});
146+
}
138147
}
139148
}
140149
return true;
@@ -154,6 +163,16 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() =>
154163
if (ViewModel.IsUnblockFileSelected)
155164
NativeFileOperationsHelper.DeleteFileFromApp($"{item.ItemPath}:Zone.Identifier");
156165

166+
if (ViewModel.IsAblumCoverModified)
167+
{
168+
MediaFileHelper.ChangeAlbumCover(item.ItemPath, ViewModel.ModifiedAlbumCover);
169+
170+
await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() =>
171+
{
172+
AppInstance?.FilesystemViewModel?.RefreshItems(null);
173+
});
174+
}
175+
157176
if (!GetNewName(out var newName))
158177
return true;
159178

0 commit comments

Comments
 (0)