diff --git a/UntamedMusicPlayer/Controls/ImageViewerWindow.xaml b/UntamedMusicPlayer/Controls/ImageViewerWindow.xaml index 5fbd5c0..fe7fe80 100644 --- a/UntamedMusicPlayer/Controls/ImageViewerWindow.xaml +++ b/UntamedMusicPlayer/Controls/ImageViewerWindow.xaml @@ -4,6 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:UntamedMusicPlayer.Controls" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="using:CommunityToolkit.WinUI" mc:Ignorable="d"> @@ -12,6 +13,7 @@ + 0,12,0,6 - - - - - - + - - - - + Icon="{ui:FontIcon Glyph=}"/> - - - - - - + - - - + Icon="{ui:FontIcon Glyph=}"> - + - - + Icon="{ui:FontIcon Glyph=}"> - + - - - diff --git a/UntamedMusicPlayer/Controls/ImageViewerWindow.xaml.cs b/UntamedMusicPlayer/Controls/ImageViewerWindow.xaml.cs index 75c1f3f..d5f313b 100644 --- a/UntamedMusicPlayer/Controls/ImageViewerWindow.xaml.cs +++ b/UntamedMusicPlayer/Controls/ImageViewerWindow.xaml.cs @@ -1,4 +1,6 @@ using System.Diagnostics; +using System.Runtime.InteropServices.WindowsRuntime; +using Microsoft.Extensions.Logging; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -6,13 +8,18 @@ using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.Windows.Storage.Pickers; using UntamedMusicPlayer.Contracts.Models; using UntamedMusicPlayer.Helpers; +using UntamedMusicPlayer.Models; +using UntamedMusicPlayer.Services; using Windows.ApplicationModel.DataTransfer; using Windows.Storage.Streams; +using ZLogger; namespace UntamedMusicPlayer.Controls; -public sealed partial class ImageViewerWindow : Window +public sealed partial class ImageViewerWindow : Window, IDisposable { + private readonly ILogger _logger = LoggingService.CreateLogger(); + private readonly BitmapImage _image; private readonly IDetailedSongInfoBase _info; @@ -24,29 +31,59 @@ public sealed partial class ImageViewerWindow : Window Path.Combine(AppContext.BaseDirectory, "Assets/AppIcon/Icon.ico") ); AppWindow.SetIcon(Path.Combine(AppContext.BaseDirectory, "Assets/AppIcon/Icon.ico")); - Title = "AppDisplayName".GetLocalized(); + Title = "ImageViewerTitle".GetLocalized(); + ExtendsContentIntoTitleBar = true; + var theme = ThemeSelectorService.IsDarkTheme ? ElementTheme.Dark : ElementTheme.Light; + ((FrameworkElement)Content).RequestedTheme = theme; + TitleBarHelper.UpdateTitleBar( + AppWindow.TitleBar, + ((FrameworkElement)Content).RequestedTheme + ); + _image = info.Cover!; _info = info; - var presenter = (OverlappedPresenter)AppWindow.Presenter; - presenter.Maximize(); + Activate(); } private async void SaveButton_Click(object sender, RoutedEventArgs e) { (sender as Button)!.IsEnabled = false; - var extension = ".jpg"; - var picker = new FileSavePicker(App.MainWindow!.AppWindow.Id) + var picker = new FileSavePicker(AppWindow.Id) { - CommitButtonText = "保存图片", SuggestedFileName = _info.Title, SuggestedStartLocation = PickerLocationId.PicturesLibrary, - FileTypeChoices = { { "EditSongInfoDialog_CoverImage".GetLocalized(), [extension] } }, + FileTypeChoices = + { + { "EditSongInfoDialog_CoverImage".GetLocalized(), [ExtractExtension()] }, + }, }; var file = await picker.PickSaveFileAsync(); if (file is not null) { - await File.WriteAllBytesAsync(file.Path, null); + byte[] data; + try + { + if (_info.IsOnline) + { + using var client = new HttpClient(); + data = await client.GetByteArrayAsync( + ((IDetailedOnlineSongInfo)_info).CoverPath! + ); + } + else + { + data = ((DetailedLocalSongInfo)_info).CoverBuffer!; + } + if (data.Length > 0) + { + await File.WriteAllBytesAsync(file.Path, data); + } + } + catch (Exception ex) + { + _logger.ZLogInformation(ex, $"{_info.Title}封面保存失败"); + } } (sender as Button)!.IsEnabled = true; } @@ -66,14 +103,20 @@ public sealed partial class ImageViewerWindow : Window } else { - var stream = RandomAccessStreamReference.CreateFromStream(null); - package.SetBitmap(stream); + var buffer = ((DetailedLocalSongInfo)_info).CoverBuffer!; + var stream = new InMemoryRandomAccessStream(); + await stream.WriteAsync(buffer.AsBuffer()); + stream.Seek(0); + var streamRef = RandomAccessStreamReference.CreateFromStream(stream); + package.SetBitmap(streamRef); + package.OperationCompleted += (s, _) => stream.Dispose(); } Clipboard.SetContent(package); + Clipboard.Flush(); } catch (Exception ex) { - Debug.WriteLine(ex); + _logger.ZLogInformation(ex, $"复制封面失败"); } } @@ -93,4 +136,72 @@ public sealed partial class ImageViewerWindow : Window } private string GetZoomText(float zoom) => $"{(int)(zoom * 100)}%"; + + private string ExtractExtension() + { + if (_info.IsOnline) + { + var coverPath = ((IDetailedOnlineSongInfo)_info).CoverPath; + if (string.IsNullOrEmpty(coverPath)) + { + return ".jpeg"; + } + var uri = new Uri(coverPath); + var extension = Path.GetExtension(uri.AbsolutePath).ToLower(); + if (string.IsNullOrEmpty(extension)) + { + return ".jpeg"; + } + return extension; + } + + var buffer = ((DetailedLocalSongInfo)_info).CoverBuffer; + if (buffer is not { Length: >= 2 }) + { + return ".jpeg"; + } + + // JPEG (jpg, jpeg, jpe, jfif): FF D8 + if (buffer[0] == 0xFF && buffer[1] == 0xD8) + { + return ".jpeg"; + } + + // BMP (bmp, dip): 42 4D + if (buffer[0] == 0x42 && buffer[1] == 0x4D) + { + return ".bmp"; + } + + if (buffer.Length >= 4) + { + // PNG: 89 50 4E 47 + if (buffer[0] == 0x89 && buffer[1] == 0x50 && buffer[2] == 0x4E && buffer[3] == 0x47) + { + return ".png"; + } + // GIF: 47 49 46 38 ('GIF8') + if (buffer[0] == 0x47 && buffer[1] == 0x49 && buffer[2] == 0x46 && buffer[3] == 0x38) + { + return ".gif"; + } + // TIFF (Little Endian): 49 49 2A 00, (Big Endian): 4D 4D 00 2A + if ( + (buffer[0] == 0x49 && buffer[1] == 0x49 && buffer[2] == 0x2A && buffer[3] == 0x00) + || ( + buffer[0] == 0x4D && buffer[1] == 0x4D && buffer[2] == 0x00 && buffer[3] == 0x2A + ) + ) + { + return ".tiff"; + } + } + + return ".jpeg"; + } + + public void Dispose() + { + Close(); + } } diff --git a/UntamedMusicPlayer/Helpers/TitleBarHelper.cs b/UntamedMusicPlayer/Helpers/TitleBarHelper.cs index d39d81a..465c135 100644 --- a/UntamedMusicPlayer/Helpers/TitleBarHelper.cs +++ b/UntamedMusicPlayer/Helpers/TitleBarHelper.cs @@ -1,4 +1,5 @@ using Microsoft.UI; +using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; using Windows.UI; using Windows.UI.ViewManagement; @@ -13,60 +14,55 @@ public sealed partial class TitleBarHelper private const int WAACTIVE = 0x01; private const int WMACTIVATE = 0x0006; - public static void UpdateTitleBar(ElementTheme theme) + public static void UpdateTitleBar(AppWindowTitleBar titleBar, ElementTheme theme) { - if (App.MainWindow is not null && App.MainWindow.ExtendsContentIntoTitleBar) + if (theme == ElementTheme.Default) { - if (theme == ElementTheme.Default) - { - var uiSettings = new UISettings(); - var background = uiSettings.GetColorValue(UIColorType.Background); - theme = background == Colors.White ? ElementTheme.Light : ElementTheme.Dark; - } + var uiSettings = new UISettings(); + var background = uiSettings.GetColorValue(UIColorType.Background); + theme = background == Colors.White ? ElementTheme.Light : ElementTheme.Dark; + } - var titleBar = App.MainWindow.AppWindow.TitleBar; + titleBar.ButtonForegroundColor = theme switch + { + ElementTheme.Dark => Colors.White, + ElementTheme.Light => Colors.Black, + _ => Colors.Transparent, + }; - titleBar.ButtonForegroundColor = theme switch - { - ElementTheme.Dark => Colors.White, - ElementTheme.Light => Colors.Black, - _ => Colors.Transparent, - }; + titleBar.ButtonHoverForegroundColor = theme switch + { + ElementTheme.Dark => Colors.White, + ElementTheme.Light => Colors.Black, + _ => Colors.Transparent, + }; - titleBar.ButtonHoverForegroundColor = theme switch - { - ElementTheme.Dark => Colors.White, - ElementTheme.Light => Colors.Black, - _ => Colors.Transparent, - }; + titleBar.ButtonHoverBackgroundColor = theme switch + { + ElementTheme.Dark => Color.FromArgb(0x33, 0xFF, 0xFF, 0xFF), + ElementTheme.Light => Color.FromArgb(0x33, 0x00, 0x00, 0x00), + _ => Colors.Transparent, + }; - titleBar.ButtonHoverBackgroundColor = theme switch - { - ElementTheme.Dark => Color.FromArgb(0x33, 0xFF, 0xFF, 0xFF), - ElementTheme.Light => Color.FromArgb(0x33, 0x00, 0x00, 0x00), - _ => Colors.Transparent, - }; + titleBar.ButtonPressedBackgroundColor = theme switch + { + ElementTheme.Dark => Color.FromArgb(0x66, 0xFF, 0xFF, 0xFF), + ElementTheme.Light => Color.FromArgb(0x66, 0x00, 0x00, 0x00), + _ => Colors.Transparent, + }; - titleBar.ButtonPressedBackgroundColor = theme switch - { - ElementTheme.Dark => Color.FromArgb(0x66, 0xFF, 0xFF, 0xFF), - ElementTheme.Light => Color.FromArgb(0x66, 0x00, 0x00, 0x00), - _ => Colors.Transparent, - }; + titleBar.BackgroundColor = Colors.Transparent; - titleBar.BackgroundColor = Colors.Transparent; - - var hwnd = WindowNative.GetWindowHandle(App.MainWindow); - if (hwnd == GetActiveWindow()) - { - SendMessage(hwnd, WMACTIVATE, WAINACTIVE, nint.Zero); - SendMessage(hwnd, WMACTIVATE, WAACTIVE, nint.Zero); - } - else - { - SendMessage(hwnd, WMACTIVATE, WAACTIVE, nint.Zero); - SendMessage(hwnd, WMACTIVATE, WAINACTIVE, nint.Zero); - } + var hwnd = WindowNative.GetWindowHandle(App.MainWindow); + if (hwnd == GetActiveWindow()) + { + SendMessage(hwnd, WMACTIVATE, WAINACTIVE, nint.Zero); + SendMessage(hwnd, WMACTIVATE, WAACTIVE, nint.Zero); + } + else + { + SendMessage(hwnd, WMACTIVATE, WAACTIVE, nint.Zero); + SendMessage(hwnd, WMACTIVATE, WAINACTIVE, nint.Zero); } } } diff --git a/UntamedMusicPlayer/MainWindow.xaml.cs b/UntamedMusicPlayer/MainWindow.xaml.cs index 2474071..e9a037b 100644 --- a/UntamedMusicPlayer/MainWindow.xaml.cs +++ b/UntamedMusicPlayer/MainWindow.xaml.cs @@ -423,7 +423,8 @@ public sealed partial class MainWindow : WindowEx, IRecipient args.Cancel = true; sender.Hide(); // 立即隐藏窗口,提升视觉响应 Data.MusicPlayer.Pause(); // 立即停止音乐播放 - Data.DesktopLyricWindow?.Close(); // 立即关闭桌面歌词 + Data.DesktopLyricWindow?.Dispose(); // 立即关闭桌面歌词 + Data.ImageViewerWindows?.ForEach(w => w.Dispose()); // 立即关闭图片查看器 Settings.NotFirstUsed = true; // 并行执行保存以缩短退出后的存活时间 @@ -459,7 +460,6 @@ public sealed partial class MainWindow : WindowEx, IRecipient try { Data.MusicPlayer.Dispose(); - Data.DesktopLyricWindow?.Dispose(); UnregisterGlobalHotKeys(); RootGrid.RemoveHandler(UIElement.KeyDownEvent, new KeyEventHandler(OnGlobalKeyDown)); diff --git a/UntamedMusicPlayer/Models/Data.cs b/UntamedMusicPlayer/Models/Data.cs index 8b4b2ac..92398b1 100644 --- a/UntamedMusicPlayer/Models/Data.cs +++ b/UntamedMusicPlayer/Models/Data.cs @@ -1,4 +1,5 @@ using UntamedMusicPlayer.Contracts.Models; +using UntamedMusicPlayer.Controls; using UntamedMusicPlayer.Helpers; using UntamedMusicPlayer.LyricRenderer; using UntamedMusicPlayer.Playback; @@ -94,6 +95,7 @@ public static class Data public static LyricPage? LyricPage { get; set; } public static RootPlayBarView? RootPlayBarView { get; set; } public static DesktopLyricWindow? DesktopLyricWindow { get; set; } + public static List? ImageViewerWindows { get; set; } #endregion #region ViewModels diff --git a/UntamedMusicPlayer/Services/MaterialSelectorService.cs b/UntamedMusicPlayer/Services/MaterialSelectorService.cs index 3dff897..78045fd 100644 --- a/UntamedMusicPlayer/Services/MaterialSelectorService.cs +++ b/UntamedMusicPlayer/Services/MaterialSelectorService.cs @@ -328,7 +328,7 @@ public sealed partial class MaterialSelectorService : IMaterialSelectorService StrongReferenceMessenger.Default.Send( new ThemeChangeMessage(ThemeSelectorService.IsDarkTheme) ); - TitleBarHelper.UpdateTitleBar(sender.ActualTheme); + TitleBarHelper.UpdateTitleBar(App.MainWindow!.AppWindow.TitleBar, sender.ActualTheme); SetConfigurationSourceTheme(); ChangeTheme(); } diff --git a/UntamedMusicPlayer/Services/ThemeSelectorService.cs b/UntamedMusicPlayer/Services/ThemeSelectorService.cs index effc435..78b25f5 100644 --- a/UntamedMusicPlayer/Services/ThemeSelectorService.cs +++ b/UntamedMusicPlayer/Services/ThemeSelectorService.cs @@ -32,7 +32,7 @@ public sealed class ThemeSelectorService : IThemeSelectorService if (App.MainWindow!.Content is FrameworkElement rootElement) { rootElement.RequestedTheme = Theme; - TitleBarHelper.UpdateTitleBar(Theme); + TitleBarHelper.UpdateTitleBar(App.MainWindow.AppWindow.TitleBar, Theme); } } } diff --git a/UntamedMusicPlayer/Strings/en-us/Resources.resw b/UntamedMusicPlayer/Strings/en-us/Resources.resw index 0860811..6752b1f 100644 --- a/UntamedMusicPlayer/Strings/en-us/Resources.resw +++ b/UntamedMusicPlayer/Strings/en-us/Resources.resw @@ -999,4 +999,25 @@ Playlist + + Show cover + + + Untamed Image Viewer + + + Zoom in (↑ / Ctrl+Mouse wheel up) + + + Original size + + + Copy to clipboard + + + Save + + + Zoom out (↑ / Ctrl+Mouse wheel down) + \ No newline at end of file diff --git a/UntamedMusicPlayer/Strings/zh-cn/Resources.resw b/UntamedMusicPlayer/Strings/zh-cn/Resources.resw index fd73602..73413c3 100644 --- a/UntamedMusicPlayer/Strings/zh-cn/Resources.resw +++ b/UntamedMusicPlayer/Strings/zh-cn/Resources.resw @@ -999,4 +999,25 @@ 播放列表 + + 显示封面 + + + 无羁 图片查看器 + + + 放大 (↑ / Ctrl+鼠标滚轮向上) + + + 原始大小 + + + 复制到剪贴板 + + + 保存 + + + 缩小 (↓ / Ctrl+鼠标滚轮向下) + \ No newline at end of file diff --git a/UntamedMusicPlayer/ViewModels/LyricViewModel.cs b/UntamedMusicPlayer/ViewModels/LyricViewModel.cs index 7af28af..9e7e41b 100644 --- a/UntamedMusicPlayer/ViewModels/LyricViewModel.cs +++ b/UntamedMusicPlayer/ViewModels/LyricViewModel.cs @@ -1,16 +1,35 @@ +using System.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media.Animation; using UntamedMusicPlayer.Contracts.Models; +using UntamedMusicPlayer.Controls; using UntamedMusicPlayer.LyricRenderer; using UntamedMusicPlayer.Models; +using UntamedMusicPlayer.Playback; using UntamedMusicPlayer.Views; namespace UntamedMusicPlayer.ViewModels; -public sealed class LyricViewModel +public sealed partial class LyricViewModel : ObservableObject, IDisposable { - public LyricViewModel() { } + [ObservableProperty] + public partial bool IsShowCoverEnabled { get; set; } + + public LyricViewModel() + { + IsShowCoverEnabled = Data.PlayState.CurrentSong?.Cover is not null; + Data.PlayState.PropertyChanged += OnStateChanged; + } + + private void OnStateChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(SharedPlaybackState.CurrentSong)) + { + IsShowCoverEnabled = Data.PlayState.CurrentSong?.Cover is not null; + } + } public void ListView_ItemClick(object _, ItemClickEventArgs e) { @@ -108,4 +127,15 @@ public sealed class LyricViewModel } } } + + public async void ShowCoverButton_Click(object _1, RoutedEventArgs _2) + { + Data.ImageViewerWindows ??= []; + Data.ImageViewerWindows.Add(new ImageViewerWindow(Data.PlayState.CurrentSong!)); + } + + public void Dispose() + { + Data.PlayState.PropertyChanged -= OnStateChanged; + } } diff --git a/UntamedMusicPlayer/ViewModels/RootPlayBarViewModel.cs b/UntamedMusicPlayer/ViewModels/RootPlayBarViewModel.cs index 1c5760a..eb5f323 100644 --- a/UntamedMusicPlayer/ViewModels/RootPlayBarViewModel.cs +++ b/UntamedMusicPlayer/ViewModels/RootPlayBarViewModel.cs @@ -153,12 +153,10 @@ public sealed partial class RootPlayBarViewModel : ObservableObject if (!IsDesktopLyricWindowStarted) { Data.DesktopLyricWindow = new DesktopLyricWindow(); - Data.DesktopLyricWindow.Activate(); IsDesktopLyricWindowStarted = true; } else { - Data.DesktopLyricWindow?.Close(); Data.DesktopLyricWindow?.Dispose(); IsDesktopLyricWindowStarted = false; } diff --git a/UntamedMusicPlayer/Views/DesktopLyricWindow.xaml b/UntamedMusicPlayer/Views/DesktopLyricWindow.xaml index d30f83e..de4f4e8 100644 --- a/UntamedMusicPlayer/Views/DesktopLyricWindow.xaml +++ b/UntamedMusicPlayer/Views/DesktopLyricWindow.xaml @@ -6,6 +6,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:model="using:UntamedMusicPlayer.Models" xmlns:winuiex="using:WinUIEx" + Closed="Window_Closed" mc:Ignorable="d"> diff --git a/UntamedMusicPlayer/Views/DesktopLyricWindow.xaml.cs b/UntamedMusicPlayer/Views/DesktopLyricWindow.xaml.cs index b0eb4ea..8e4c559 100644 --- a/UntamedMusicPlayer/Views/DesktopLyricWindow.xaml.cs +++ b/UntamedMusicPlayer/Views/DesktopLyricWindow.xaml.cs @@ -74,7 +74,7 @@ public sealed partial class DesktopLyricWindow : WindowEx, IDisposable this.CenterOnScreen(null, null); this.Move(AppWindow.Position.X, y); - Closed += Window_Closed; + Activate(); } private double GetTextBlockWidth(string? currentLyricContent) @@ -167,6 +167,7 @@ public sealed partial class DesktopLyricWindow : WindowEx, IDisposable public void Dispose() { + Close(); Data.DesktopLyricWindow = null; } } diff --git a/UntamedMusicPlayer/Views/LyricPage.xaml b/UntamedMusicPlayer/Views/LyricPage.xaml index 87b3617..12752d9 100644 --- a/UntamedMusicPlayer/Views/LyricPage.xaml +++ b/UntamedMusicPlayer/Views/LyricPage.xaml @@ -238,6 +238,10 @@ Click="{x:Bind ViewModel.ShowArtistButton_Click}" Icon="{ui:FontIcon FontFamily={StaticResource UntamedFontFamily}, Glyph=}"/> +