From 182c7b1692a552c55ad420c61b9e3379d9922df6 Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Fri, 18 Oct 2024 02:52:44 +0700 Subject: [PATCH] add share track button --- MusicX.Shared/Player/PlaylistTrack.cs | 1 + MusicX/Controls/TrackControl.xaml | 8 ++ MusicX/Controls/TrackControl.xaml.cs | 7 ++ MusicX/Helpers/EmbeddedFileStreamReference.cs | 66 ++++++++++++ MusicX/Helpers/IDataTransferManagerInterop.cs | 25 +++++ MusicX/MusicX.csproj | 2 + MusicX/RootWindow.xaml.cs | 4 + MusicX/Services/DownloaderService.cs | 75 +++++++------- MusicX/Services/ShareService.cs | 95 ++++++++++++++++++ MusicX/StoreLogo.scale-30.png | Bin 0 -> 747 bytes MusicX/ViewModels/DownloaderViewModel.cs | 2 +- MusicX/Views/StartingWindow.xaml.cs | 1 + 12 files changed, 251 insertions(+), 35 deletions(-) create mode 100644 MusicX/Helpers/EmbeddedFileStreamReference.cs create mode 100644 MusicX/Helpers/IDataTransferManagerInterop.cs create mode 100644 MusicX/Services/ShareService.cs create mode 100644 MusicX/StoreLogo.scale-30.png diff --git a/MusicX.Shared/Player/PlaylistTrack.cs b/MusicX.Shared/Player/PlaylistTrack.cs index bbbd4bef..6dc95007 100644 --- a/MusicX.Shared/Player/PlaylistTrack.cs +++ b/MusicX.Shared/Player/PlaylistTrack.cs @@ -41,6 +41,7 @@ public sealed record BoomTrackData(string Url, bool IsLiked, bool IsExplicit, Ti public record IdInfo(long Id, long OwnerId, string AccessKey) { public string ToOwnerIdString() => $"{OwnerId}_{Id}"; + public override string ToString() => $"{OwnerId}_{Id}_{AccessKey}"; } [ProtoContract(ImplicitFields = ImplicitFields.AllPublic, SkipConstructor = true)] diff --git a/MusicX/Controls/TrackControl.xaml b/MusicX/Controls/TrackControl.xaml index 65ce2d7a..8f317a16 100644 --- a/MusicX/Controls/TrackControl.xaml +++ b/MusicX/Controls/TrackControl.xaml @@ -29,6 +29,14 @@ Margin="10,0,0,0" /> + + + + + (); + + shareService.ShareTrack(Audio.ToTrack()); + } } } diff --git a/MusicX/Helpers/EmbeddedFileStreamReference.cs b/MusicX/Helpers/EmbeddedFileStreamReference.cs new file mode 100644 index 00000000..fab2a9b5 --- /dev/null +++ b/MusicX/Helpers/EmbeddedFileStreamReference.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.Storage.Streams; + +namespace MusicX.Helpers; + +public class EmbeddedFileStreamReference(string resourceName, string contentType) : IRandomAccessStreamReference +{ + public IAsyncOperation OpenReadAsync() => + OpenReadAsyncInternal().AsAsyncOperation(); + + private async Task OpenReadAsyncInternal() + { + var assembly = typeof(EmbeddedFileStreamReference).Assembly; + await using var manifestResourceStream = assembly.GetManifestResourceStream(resourceName); + + if (manifestResourceStream == null) + throw new FileNotFoundException("Resource not found", resourceName); + + var stream = new InMemoryRandomAccessStream(); + + await using var writeStream = stream.AsStreamForWrite(); + await manifestResourceStream.CopyToAsync(writeStream); + + return new InMemoryRandomAccessStreamWithContentType(stream, contentType); + } + +} + +public sealed class InMemoryRandomAccessStreamWithContentType( + InMemoryRandomAccessStream randomAccessStream, + string contentType) : IRandomAccessStreamWithContentType +{ + public void Dispose() + { + randomAccessStream.Dispose(); + } + + public IAsyncOperationWithProgress ReadAsync(IBuffer buffer, uint count, InputStreamOptions options) => + randomAccessStream.ReadAsync(buffer, count, options); + + public IAsyncOperationWithProgress WriteAsync(IBuffer buffer) => + randomAccessStream.WriteAsync(buffer); + + public IAsyncOperation FlushAsync() => randomAccessStream.FlushAsync(); + + public IInputStream GetInputStreamAt(ulong position) => randomAccessStream.GetInputStreamAt(position); + + public IOutputStream GetOutputStreamAt(ulong position) => randomAccessStream.GetOutputStreamAt(position); + + public void Seek(ulong position) => randomAccessStream.Seek(position); + + public IRandomAccessStream CloneStream() => randomAccessStream.CloneStream(); + + public bool CanRead => randomAccessStream.CanRead; + public bool CanWrite => randomAccessStream.CanWrite; + public ulong Position => randomAccessStream.Position; + public ulong Size + { + get => randomAccessStream.Size; + set => randomAccessStream.Size = value; + } + public string ContentType => contentType; +} \ No newline at end of file diff --git a/MusicX/Helpers/IDataTransferManagerInterop.cs b/MusicX/Helpers/IDataTransferManagerInterop.cs new file mode 100644 index 00000000..248c9264 --- /dev/null +++ b/MusicX/Helpers/IDataTransferManagerInterop.cs @@ -0,0 +1,25 @@ +using System; +using System.Runtime.InteropServices; +using Windows.ApplicationModel.DataTransfer; +using WinRT; + +namespace MusicX.Helpers; + +public static class DataTransferManagerInterop +{ + private static IDataTransferManagerInterop InteropInstance => DataTransferManager.As(); + + public static DataTransferManager GetForWindow(nint appWindow) => DataTransferManager.FromAbi(InteropInstance + .GetForWindow(appWindow, new("A5CAEE9B-8708-49D1-8D36-67D25A8DA00C") /* Guid of IDataTransferManager */)); + + public static void ShowForWindow(nint appWindow) => InteropInstance + .ShowShareUIForWindow(appWindow); + + [ComImport, Guid("3A3DCD6C-3EAB-43DC-BCDE-45671CE800C8")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IDataTransferManagerInterop + { + nint GetForWindow([In] nint appWindow, [In] ref Guid riid); + void ShowShareUIForWindow(nint appWindow); + } +} \ No newline at end of file diff --git a/MusicX/MusicX.csproj b/MusicX/MusicX.csproj index efa9bd07..e5ddbb1d 100644 --- a/MusicX/MusicX.csproj +++ b/MusicX/MusicX.csproj @@ -67,6 +67,8 @@ + + diff --git a/MusicX/RootWindow.xaml.cs b/MusicX/RootWindow.xaml.cs index 9ba894c6..b0df7518 100644 --- a/MusicX/RootWindow.xaml.cs +++ b/MusicX/RootWindow.xaml.cs @@ -326,6 +326,10 @@ private async void Window_Loaded(object sender, RoutedEventArgs e) } this.WindowState = WindowState.Normal; + + var shareService = StaticService.Container.GetRequiredService(); + + shareService.AssignWindow(new(this)); } catch (Exception ex) { diff --git a/MusicX/Services/DownloaderService.cs b/MusicX/Services/DownloaderService.cs index c643920f..acf4c579 100644 --- a/MusicX/Services/DownloaderService.cs +++ b/MusicX/Services/DownloaderService.cs @@ -50,44 +50,12 @@ public string GetDownloadDirectoryAsync() return Directory.CreateDirectory(directory).FullName; } - public async Task DownloadAudioAsync(PlaylistTrack audio, IProgress<(TimeSpan Position, TimeSpan Duration)>? progress = null, CancellationToken cancellationToken = default) + public async Task DownloadAudioAsync(PlaylistTrack audio, IProgress<(TimeSpan Position, TimeSpan Duration)>? progress = null, FileInfo? destinationFile = null, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(audio.Data.Url)) return; - var fileName = $"{audio.GetArtistsString()} - {audio.Title}"; - fileName = ReplaceSymbols(fileName) + ".mp3"; - - string fileDownloadPath; - var musicFolder = GetDownloadDirectoryAsync(); - - if (audio.Data is DownloaderData data) - { - var name = ReplaceSymbols(data.PlaylistName); - - var playlistDirPath = Path.Combine(musicFolder, name); - - if (!Directory.Exists(playlistDirPath)) - { - Directory.CreateDirectory(playlistDirPath); - } - - fileDownloadPath = Path.Combine(playlistDirPath, fileName); - } - else - { - fileDownloadPath = Path.Combine(musicFolder, fileName); - } - - var i = 0; - while (File.Exists(fileDownloadPath)) - { - fileDownloadPath = fileDownloadPath.Replace(".mp3", string.Empty); - var value = $"({i})"; - if (fileDownloadPath.EndsWith(value)) - fileDownloadPath = fileDownloadPath[..^value.Length]; - fileDownloadPath += $"({++i}).mp3"; - } + var fileDownloadPath = destinationFile?.FullName ?? ResolveFileDownloadPath(audio); if (audio.Data is BoomTrackData) { @@ -144,6 +112,45 @@ await _mediaTranscoder.PrepareMediaStreamSourceTranscodeAsync(streamSource, await AddMetadataAsync(audio, fileDownloadPath, cancellationToken); } + private string ResolveFileDownloadPath(PlaylistTrack audio) + { + var fileName = $"{audio.GetArtistsString()} - {audio.Title}"; + fileName = ReplaceSymbols(fileName) + ".mp3"; + + string fileDownloadPath; + var musicFolder = GetDownloadDirectoryAsync(); + + if (audio.Data is DownloaderData data) + { + var name = ReplaceSymbols(data.PlaylistName); + + var playlistDirPath = Path.Combine(musicFolder, name); + + if (!Directory.Exists(playlistDirPath)) + { + Directory.CreateDirectory(playlistDirPath); + } + + fileDownloadPath = Path.Combine(playlistDirPath, fileName); + } + else + { + fileDownloadPath = Path.Combine(musicFolder, fileName); + } + + var i = 0; + while (File.Exists(fileDownloadPath)) + { + fileDownloadPath = fileDownloadPath.Replace(".mp3", string.Empty); + var value = $"({i})"; + if (fileDownloadPath.EndsWith(value)) + fileDownloadPath = fileDownloadPath[..^value.Length]; + fileDownloadPath += $"({++i}).mp3"; + } + + return fileDownloadPath; + } + private string ReplaceSymbols(string fileName) { return string.Join("_", fileName.Split(Path.GetInvalidFileNameChars())).Replace('.', '_'); diff --git a/MusicX/Services/ShareService.cs b/MusicX/Services/ShareService.cs new file mode 100644 index 00000000..ccfd8151 --- /dev/null +++ b/MusicX/Services/ShareService.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Interop; +using Windows.ApplicationModel.DataTransfer; +using Windows.Storage; +using Windows.Storage.Streams; +using MusicX.Helpers; +using MusicX.Services.Player.Playlists; +using MusicX.Shared.Player; +using NLog; +using Wpf.Ui; +using Wpf.Ui.Extensions; + +namespace MusicX.Services; + +public class ShareService(Logger logger, DownloaderService downloaderService, ISnackbarService snackbarService) +{ + private (PlaylistTrack track, FileInfo file)? _pendingTrack; + private WindowInteropHelper? _window; + + public void AssignWindow(WindowInteropHelper window) + { + _window = window; + + var manager = DataTransferManagerInterop.GetForWindow(window.Handle); + + manager.DataRequested += TransferManagerOnDataRequested; + } + + private async void TransferManagerOnDataRequested(DataTransferManager sender, DataRequestedEventArgs args) + { + var deferral = args.Request.GetDeferral(); + + try + { + var data = args.Request.Data; + + SetApplicationDetails(data); + + if (_pendingTrack is not null) + { + var (track, file) = _pendingTrack.Value; + await SetTrackAsync(data, track, file); + } + else + throw new InvalidOperationException("No pending data to share"); + } + catch (Exception e) + { + logger.Error(e, "Failed to respond to a share request"); + + args.Request.FailWithDisplayText("Упс! Что-то пошло не так"); + } + finally + { + deferral.Complete(); + } + } + + private async Task SetTrackAsync(DataPackage data, PlaylistTrack track, FileInfo file) + { + data.Properties.Title = $"{track.GetArtistsString()} - {track.Title}"; + if (track.AlbumId?.CoverUrl is not null) + data.Properties.Thumbnail = RandomAccessStreamReference.CreateFromUri(new Uri(track.AlbumId.CoverUrl)); + + // todo url-only option + // if (track.Data is VkTrackData trackData) + // data.SetWebLink(new Uri($"https://vk.com/audio{trackData.Info}")); + + data.SetStorageItems([await StorageFile.GetFileFromPathAsync(file.FullName)]); + } + + private static void SetApplicationDetails(DataPackage data) + { + data.Properties.ApplicationName = "MusicX Player"; + data.Properties.Square30x30Logo = new EmbeddedFileStreamReference("MusicX.StoreLogo.scale-30.png", "image/png"); + } + + public async void ShareTrack(PlaylistTrack track) + { + snackbarService.Show("Подождите...", "Мы готовим трек для отправки", TimeSpan.FromSeconds(5)); + + var file = new FileInfo(Path.Join(Directory.CreateTempSubdirectory("MusicX").FullName, $"{track.GetArtistsString()} - {track.Title}.mp3")); + + // todo fix ffmpeg blocking thread on start + await Task.Run(() => downloaderService.DownloadAudioAsync(track, destinationFile: file)); + + _pendingTrack = (track, file); + + if (_window is not null) + DataTransferManagerInterop.ShowForWindow(_window.Handle); + } +} \ No newline at end of file diff --git a/MusicX/StoreLogo.scale-30.png b/MusicX/StoreLogo.scale-30.png new file mode 100644 index 0000000000000000000000000000000000000000..2a5660d1e519e0999fdee5eb297b0fb1d4eb4397 GIT binary patch literal 747 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0)R3Kmh3g5tr0hkgVF4}u>+#dfs?MOvsR zMevYspqPUo{z1+Dndf^myJE6Xk<@h*@YNM5|Ry>EHp+PapCo^+7i#SF{~v@y2ax%JiqHCoDy78AVY5CwkW#+yvJyR=Eu8~A#_8Dxq< z&)Umw0G9)kFeeWSlGp4tn@x_GT%vpZ%Sx>It z{6xyX%W=si*6{kpT^|-69V;)ltAf@)Po^pP#Y`MGW%Z~Je}BC)(mc5u@{4Js69pUH zM*S4sx6H<;ccfh85~G$D(49LD*366^#C{oH%u<^&ooldE@E|5(JaH;*)IqvNntyT! z$rdM4$D0M|xPmr_q+v2figjj|h0bgu<>}>}YznZLd$M(@4=uGxTC0)NF;dfDt^=65 z>>=qmb_pp8Zig;y^VrU#(zd~=)&xtb#IVE~=Sc0Rn$bX7?iVwKd>;Uc~7eN3!rS2Q-U#fLri d9&(TY_z&d2NH-eixuF06002ovPDHLkV1iubWEcPd literal 0 HcmV?d00001 diff --git a/MusicX/ViewModels/DownloaderViewModel.cs b/MusicX/ViewModels/DownloaderViewModel.cs index 41cf5082..2d1ad925 100644 --- a/MusicX/ViewModels/DownloaderViewModel.cs +++ b/MusicX/ViewModels/DownloaderViewModel.cs @@ -190,7 +190,7 @@ private async Task DownloaderTask(CancellationToken token) try { CurrentDownloadingAudio = audio; - await downloaderService.DownloadAudioAsync(audio, progress, token); + await downloaderService.DownloadAudioAsync(audio, progress, cancellationToken: token); DownloadProgress = 0; } catch (Exception e) when (e is TypeInitializationException or COMException) diff --git a/MusicX/Views/StartingWindow.xaml.cs b/MusicX/Views/StartingWindow.xaml.cs index 366c765b..2e1682f4 100644 --- a/MusicX/Views/StartingWindow.xaml.cs +++ b/MusicX/Views/StartingWindow.xaml.cs @@ -132,6 +132,7 @@ await Task.Run(async () => collection.AddSingleton(s => new BackendConnectionService(s.GetRequiredService(), StaticService.Version)); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(); var container = StaticService.Container = collection.BuildServiceProvider();