基本完成查看图片页

This commit is contained in:
LanZhan-Harmony
2026-03-24 22:19:24 +08:00
parent 07a430524d
commit 2b95abaee0
14 changed files with 272 additions and 99 deletions

View File

@@ -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">
<Window.SystemBackdrop>
@@ -12,6 +13,7 @@
<Grid>
<Grid.Resources>
<Thickness x:Key="AppBarButtonContentViewboxCollapsedMargin">0,12,0,6</Thickness>
<SolidColorBrush x:Key="AppBarButtonForegroundPointerOver" Color="{ThemeResource SystemAccentColor}"/>
<SolidColorBrush x:Key="AppBarButtonForegroundPressed"
Opacity="0.5"
@@ -23,7 +25,6 @@
<ScrollViewer x:Name="Scroll"
HorizontalScrollBarVisibility="Auto"
Tapped="Scroll_Tapped"
VerticalScrollBarVisibility="Auto"
ZoomMode="Enabled">
<Image x:Name="Image"
@@ -37,56 +38,43 @@
Orientation="Horizontal">
<Border Background="#CC000000" CornerRadius="{ThemeResource OverlayCornerRadius}">
<StackPanel Orientation="Horizontal">
<AppBarButton Width="40" Height="40"
<AppBarButton x:Uid="ImageViewer_Save"
Width="40" Height="40"
Click="SaveButton_Click" Foreground="White"
ToolTipService.ToolTip="保存">
<AppBarButton.Icon>
<SymbolIcon Margin="0,-4,0,4" Symbol="Save"/>
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton Width="40" Height="40"
Icon="{ui:FontIcon Glyph=&#xE74E;}"/>
<AppBarButton x:Uid="ImageViewer_Copy"
Width="40" Height="40"
Click="CopyButton_Click" Foreground="White"
ToolTipService.ToolTip="复制">
<AppBarButton.Icon>
<SymbolIcon Margin="0,-4,0,4" Symbol="Copy"/>
</AppBarButton.Icon>
</AppBarButton>
Icon="{ui:FontIcon Glyph=&#xE8C8;}"/>
</StackPanel>
</Border>
<Border Margin="12,0,0,0"
Background="#CC000000"
CornerRadius="{ThemeResource OverlayCornerRadius}">
<StackPanel Orientation="Horizontal">
<AppBarButton Width="40" Height="40"
<AppBarButton x:Uid="ImageViewer_OriginalSize"
Width="40" Height="40"
Margin="0,0,0,0"
Click="OriginButton_Click" Foreground="White"
ToolTipService.ToolTip="原始大小">
<AppBarButton.Icon>
<SymbolIcon Margin="0,-4,0,4" Symbol="Pictures"/>
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton Width="40" Height="40"
Icon="{ui:FontIcon Glyph=&#xE8B9;}"/>
<AppBarButton x:Uid="ImageViewer_ZoomOut"
Width="40" Height="40"
Click="ZoomOutButton_Click" Foreground="White"
ToolTipService.ToolTip="缩小">
<AppBarButton.Icon>
<SymbolIcon Margin="0,-4,0,4" Symbol="ZoomOut"/>
</AppBarButton.Icon>
Icon="{ui:FontIcon Glyph=&#xE71F;}">
<AppBarButton.KeyboardAccelerators>
<KeyboardAccelerator Key="Down" Modifiers="None"/>
<KeyboardAccelerator Key="Down"/>
</AppBarButton.KeyboardAccelerators>
</AppBarButton>
<TextBlock Margin="8,0" VerticalAlignment="Center"
<TextBlock Margin="8,-4,8,0" VerticalAlignment="Center"
FontSize="16" Foreground="White"
Text="{x:Bind GetZoomText(Scroll.ZoomFactor), Mode=OneWay}"/>
<AppBarButton Width="40" Height="40"
<AppBarButton x:Uid="ImageViewer_ZoomIn"
Width="40" Height="40"
Click="ZoomInButton_Click" Foreground="White"
ToolTipService.ToolTip="放大">
Icon="{ui:FontIcon Glyph=&#xE8A3;}">
<AppBarButton.KeyboardAccelerators>
<KeyboardAccelerator Key="Up" Modifiers="None"/>
<KeyboardAccelerator Key="Up"/>
</AppBarButton.KeyboardAccelerators>
<AppBarButton.Icon>
<SymbolIcon Margin="0,-4,0,4" Symbol="ZoomIn"/>
</AppBarButton.Icon>
</AppBarButton>
</StackPanel>
</Border>

View File

@@ -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<ImageViewerWindow>();
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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -423,7 +423,8 @@ public sealed partial class MainWindow : WindowEx, IRecipient<LogMessage>
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<LogMessage>
try
{
Data.MusicPlayer.Dispose();
Data.DesktopLyricWindow?.Dispose();
UnregisterGlobalHotKeys();
RootGrid.RemoveHandler(UIElement.KeyDownEvent, new KeyEventHandler(OnGlobalKeyDown));

View File

@@ -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<ImageViewerWindow>? ImageViewerWindows { get; set; }
#endregion
#region ViewModels

View File

@@ -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();
}

View File

@@ -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);
}
}
}

View File

@@ -999,4 +999,25 @@
<data name="Settings_Playlist" xml:space="preserve">
<value>Playlist</value>
</data>
<data name="Lyric_ShowCover.Text" xml:space="preserve">
<value>Show cover</value>
</data>
<data name="ImageViewerTitle" xml:space="preserve">
<value>Untamed Image Viewer</value>
</data>
<data name="ImageViewer_ZoomIn.ToolTipService.ToolTip" xml:space="preserve">
<value>Zoom in (↑ / Ctrl+Mouse wheel up)</value>
</data>
<data name="ImageViewer_OriginalSize.ToolTipService.ToolTip" xml:space="preserve">
<value>Original size</value>
</data>
<data name="ImageViewer_Copy.ToolTipService.ToolTip" xml:space="preserve">
<value>Copy to clipboard</value>
</data>
<data name="ImageViewer_Save.ToolTipService.ToolTip" xml:space="preserve">
<value>Save</value>
</data>
<data name="ImageViewer_ZoomOut.ToolTipService.ToolTip" xml:space="preserve">
<value>Zoom out (↑ / Ctrl+Mouse wheel down)</value>
</data>
</root>

View File

@@ -999,4 +999,25 @@
<data name="Settings_Playlist" xml:space="preserve">
<value>播放列表</value>
</data>
<data name="Lyric_ShowCover.Text" xml:space="preserve">
<value>显示封面</value>
</data>
<data name="ImageViewerTitle" xml:space="preserve">
<value>无羁 图片查看器</value>
</data>
<data name="ImageViewer_ZoomIn.ToolTipService.ToolTip" xml:space="preserve">
<value>放大 (↑ / Ctrl+鼠标滚轮向上)</value>
</data>
<data name="ImageViewer_OriginalSize.ToolTipService.ToolTip" xml:space="preserve">
<value>原始大小</value>
</data>
<data name="ImageViewer_Copy.ToolTipService.ToolTip" xml:space="preserve">
<value>复制到剪贴板</value>
</data>
<data name="ImageViewer_Save.ToolTipService.ToolTip" xml:space="preserve">
<value>保存</value>
</data>
<data name="ImageViewer_ZoomOut.ToolTipService.ToolTip" xml:space="preserve">
<value>缩小 (↓ / Ctrl+鼠标滚轮向下)</value>
</data>
</root>

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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">
<Grid x:Name="Draggable" VerticalAlignment="Center">

View File

@@ -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;
}
}

View File

@@ -238,6 +238,10 @@
Click="{x:Bind ViewModel.ShowArtistButton_Click}"
Icon="{ui:FontIcon FontFamily={StaticResource UntamedFontFamily},
Glyph=&#xE77B;}"/>
<MenuFlyoutItem x:Uid="Lyric_ShowCover"
Click="{x:Bind ViewModel.ShowCoverButton_Click}"
Icon="{ui:FontIcon Glyph=&#xE91B;}"
IsEnabled="{x:Bind ViewModel.IsShowCoverEnabled, Mode=OneWay}"/>
<MenuFlyoutItem x:Uid="Songs_Properties"
Click="PropertiesButton_Click"
Icon="{ui:FontIcon FontFamily={StaticResource UntamedFontFamily},