Compare commits

..

1 Commits

Author SHA1 Message Date
Zhe Fang
7bca1d1205 Merge pull request #5 from jayfunc/dev
Add dock mode, improve glow effect, fix bugs ...
2025-06-17 21:50:22 -04:00
215 changed files with 6471 additions and 346418 deletions

15
.github/FUNDING.yml vendored
View File

@@ -1,15 +0,0 @@
# These are supported funding model platforms
github: [jayfunc]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: founchoo
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

0
.github/workflows/dotnet-desktop.yml vendored Normal file
View File

View File

@@ -1,18 +0,0 @@
name: 'issue-translator'
on:
issue_comment:
types: [created]
issues:
types: [opened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: usthe/issues-translate-action@v2.7
with:
IS_MODIFY_TITLE: false
# not require, default false, . Decide whether to modify the issue title
# if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot.
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿
# not require. Customize the translation robot prefix message.

View File

@@ -1,27 +0,0 @@
name: Notify Telegram on GitHub Release
on:
release:
types: [published]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send release info to Telegram
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
RELEASE_URL: ${{ github.event.release.html_url }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_NAME: ${{ github.event.release.name }}
RELEASE_BODY: ${{ github.event.release.body }}
run: |
TEXT="🚀 *New Release:* ${RELEASE_TAG} - ${RELEASE_NAME}%0A"
TEXT+="📝 *Description:*%0A${RELEASE_BODY}%0A"
TEXT+="🔗 [View on GitHub](${RELEASE_URL})"
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
-d chat_id="${CHAT_ID}" \
-d text="${TEXT}" \
-d parse_mode=Markdown

View File

@@ -1,22 +0,0 @@
name: Notify Discord on GitHub Release
on:
release:
types: [published]
jobs:
github-releases-to-discord:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: GitHub Releases to Discord
uses: SethCohen/github-releases-to-discord@v1
with:
webhook_url: ${{ secrets.WEBHOOK_URL }}
color: "2105893"
username: "GitHub"
avatar_url: "https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png"
content: "||@everyone||"
footer_title: "Changelog"
reduce_headings: true

View File

@@ -142,8 +142,8 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4654" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
</ItemGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />
</Project>

View File

@@ -5,62 +5,50 @@
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
xmlns:uap18="http://schemas.microsoft.com/appx/manifest/uap/windows10/18"
IgnorableNamespaces="uap rescap uap18">
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.0.39.0" />
<Identity
Name="37412.BetterLyrics"
Publisher="CN=Zhe"
Version="1.0.3.0" />
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>BetterLyrics</DisplayName>
<PublisherDisplayName>founchoo</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
</Properties>
<Properties>
<DisplayName>BetterLyrics</DisplayName>
<PublisherDisplayName>founchoo</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Resources>
<Resource Language="en-US"/>
<Resource Language="zh-CN"/>
<Resource Language="zh-TW"/>
<Resource Language="ja-JP"/>
<Resource Language="ko-KR"/>
</Resources>
<Resources>
<Resource Language="en-US"/>
<Resource Language="zh-CN"/>
<Resource Language="zh-TW"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="BetterLyrics"
Description="BetterLyrics.WinUI3 (Package)"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/>
<uap:SplashScreen Image="Images\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<uap5:Extension
Category="windows.startupTask">
<uap5:StartupTask
TaskId="AutoStartup"
Enabled="false"
DisplayName="BetterLyrics" />
</uap5:Extension>
</Extensions>
</Application>
</Applications>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="BetterLyrics"
Description="BetterLyrics.WinUI3 (Package)"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/>
<uap:SplashScreen Image="Images\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -12,7 +12,6 @@
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsExpander/SettingsExpander.xaml" />
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/Segmented/Segmented.xaml" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
@@ -46,18 +45,13 @@
<converter:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converter:MatchedLocalFilesPathToVisibilityConverter x:Key="MatchedLocalFilesPathToVisibilityConverter" />
<converter:IntToCornerRadius x:Key="IntToCornerRadius" />
<converter:CornerRadiusToDoubleConverter x:Key="CornerRadiusToDoubleConverter" />
<converter:LyricsSearchProviderToDisplayNameConverter x:Key="LyricsSearchProviderToDisplayNameConverter" />
<converter:TranslationSearchProviderToDisplayNameConverter x:Key="TranslationSearchProviderToDisplayNameConverter" />
<converter:AlbumArtSearchProviderToDisplayNameConverter x:Key="AlbumArtSearchProviderToDisplayNameConverter" />
<converter:SecondsToFormattedTimeConverter x:Key="SecondsToFormattedTimeConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<x:Double x:Key="SettingsCardSpacing">4</x:Double>
<!-- Style -->
<!-- Style (inc. the correct spacing) of a section header -->
<Style
x:Key="SettingsSectionHeaderTextBlockStyle"
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
@@ -67,16 +61,14 @@
</Style.Setters>
</Style>
<Style x:Key="TitleBarButtonStyle" TargetType="Button">
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="16,9,16,11" />
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="16,0" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostButtonStyle" TargetType="Button">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Padding" Value="8" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
@@ -85,221 +77,18 @@
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="16,9,16,11" />
<Setter Property="Padding" Value="16,0" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="GhostToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="CornerRadius" Value="4" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="8" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style x:Key="CardGridStyle" TargetType="Grid">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
</Style>
<Style x:Key="GhostSliderStyle" TargetType="Slider">
<Setter Property="Background" Value="{ThemeResource ControlStrokeColorOnAccentDefaultBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource SliderBorderThemeThickness}" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="ManipulationMode" Value="None" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-7,0,-7,0" />
<Setter Property="IsFocusEngagementEnabled" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Slider">
<Grid Margin="{TemplateBinding Padding}">
<Grid.Resources>
<Style x:Key="SliderThumbStyle" TargetType="Thumb">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="{ThemeResource TextFillColorPrimaryBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="0,1,1,0" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter
x:Name="HeaderContentPresenter"
Grid.Row="0"
Margin="{ThemeResource SliderTopHeaderMargin}"
x:DeferLoadStrategy="Lazy"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontWeight="{ThemeResource SliderHeaderThemeFontWeight}"
Foreground="{ThemeResource SliderHeaderForeground}"
TextWrapping="Wrap"
Visibility="Collapsed" />
<Grid
x:Name="SliderContainer"
Grid.Row="1"
Background="{ThemeResource SliderContainerBackground}"
Control.IsTemplateFocusTarget="True">
<Grid x:Name="HorizontalTemplate" MinHeight="{ThemeResource SliderHorizontalHeight}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="{ThemeResource SliderPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{ThemeResource SliderPostContentMargin}" />
</Grid.RowDefinitions>
<Rectangle
x:Name="HorizontalTrackRect"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="HorizontalDecreaseRect"
Grid.Row="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="TopTickBar"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,0,4"
VerticalAlignment="Bottom"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="HorizontalInlineTickBar"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="2"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="BottomTickBar"
Grid.Row="2"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,4,0,0"
VerticalAlignment="Top"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="HorizontalThumb"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
Width="2"
Height="2"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-14,-6,-14,-6"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
<Grid
x:Name="VerticalTemplate"
MinWidth="{ThemeResource SliderVerticalWidth}"
Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{ThemeResource SliderPreContentMargin}" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="{ThemeResource SliderPostContentMargin}" />
</Grid.ColumnDefinitions>
<Rectangle
x:Name="VerticalTrackRect"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{TemplateBinding Background}" />
<Rectangle
x:Name="VerticalDecreaseRect"
Grid.Row="2"
Grid.Column="1"
Fill="{TemplateBinding Foreground}" />
<TickBar
x:Name="LeftTickBar"
Grid.RowSpan="3"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,4,0"
HorizontalAlignment="Right"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="VerticalInlineTickBar"
Grid.RowSpan="3"
Grid.Column="1"
Width="{ThemeResource SliderTrackThemeHeight}"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar
x:Name="RightTickBar"
Grid.RowSpan="3"
Grid.Column="2"
Width="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="4,0,0,0"
HorizontalAlignment="Left"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<Thumb
x:Name="VerticalThumb"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Width="24"
Height="8"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-6,-14,-6,-14"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ListViewStretchedItemContainerStyle" TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" />
</Style>
<StaticResource x:Key="ToggleButtonBackgroundChecked" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPointerOver" ResourceKey="TextFillColorPrimaryBrush" />
<StaticResource x:Key="ToggleButtonBackgroundCheckedPressed" ResourceKey="TextFillColorPrimaryBrush" />
<!-- Dimensions -->
<!-- Fonts -->
<FontFamily x:Key="IconFontFamily">ms-appx:///Assets/Segoe Fluent Icons.ttf#Segoe Fluent Icons</FontFamily>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,8 +1,10 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using System.Text;
using BetterInAppLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.Rendering;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Playback;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
@@ -10,35 +12,33 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.ApplicationModel.Resources;
using Serilog;
using ShadowViewer.Controls;
using System;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
using WinUIEx;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace BetterLyrics.WinUI3
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : Application
{
private readonly ILogger<App> _logger;
public static new App Current => (App)Application.Current;
public static DispatcherQueue? DispatcherQueue { get; private set; }
public static DispatcherQueueTimer? DispatcherQueueTimer { get; private set; }
public static ResourceLoader? ResourceLoader { get; private set; }
public NotificationPanel? LyricsWindowNotificationPanel { get; set; }
public NotificationPanel? SettingsWindowNotificationPanel { get; set; }
private static Mutex? _instanceMutex;
public static DispatcherQueue? DispatcherQueue { get; private set; }
public static DispatcherQueueTimer? DispatcherQueueTimer { get; private set; }
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
@@ -47,49 +47,20 @@ namespace BetterLyrics.WinUI3
DispatcherQueueTimer = DispatcherQueue.CreateTimer();
ResourceLoader = new ResourceLoader();
EnsureSingleInstance();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
PathHelper.EnsureDirectories();
Helper.AppInfo.EnsureDirectories();
ConfigureServices();
_logger = Ioc.Default.GetRequiredService<ILogger<App>>();
_logger = Ioc.Default.GetService<ILogger<App>>()!;
UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
private void EnsureSingleInstance()
{
bool createdNew;
_instanceMutex = new Mutex(true, MetadataHelper.AppName, out createdNew);
if (!createdNew)
{
User32.MessageBox(HWND.NULL, ResourceLoader!.GetString("TryRunMultipleInstance"), null, User32.MB_FLAGS.MB_APPLMODAL);
Environment.Exit(0);
}
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
WindowHelper.OpenWindow<LyricsWindow>();
var lyricsWindow = WindowHelper.GetWindowByWindowType<LyricsWindow>();
if (lyricsWindow != null)
{
lyricsWindow.ViewModel.InitLockHotKey();
lyricsWindow.AutoSelectLyricsMode();
}
}
private static void ConfigureServices()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Is(Serilog.Events.LogEventLevel.Verbose)
.WriteTo.File(PathHelper.LogFilePattern, rollingInterval: RollingInterval.Day)
.MinimumLevel.Debug()
.WriteTo.File(Helper.AppInfo.LogFilePattern, rollingInterval: RollingInterval.Day)
.CreateLogger();
// Register services
@@ -100,44 +71,36 @@ namespace BetterLyrics.WinUI3
loggingBuilder.ClearProviders();
loggingBuilder.AddSerilog();
})
// Services
// Services (Singleton)
.AddSingleton<ISettingsService, SettingsService>()
.AddSingleton<IDatabaseService, DatabaseService>()
.AddSingleton<IPlaybackService, PlaybackService>()
.AddSingleton<IAlbumArtSearchService, AlbumArtSearchService>()
.AddSingleton<ILyricsSearchService, LyricsSearchService>()
.AddSingleton<ILibWatcherService, LibWatcherService>()
.AddSingleton<ITranslateService, TranslateService>()
// ViewModels
.AddSingleton<LyricsWindowViewModel>()
.AddSingleton<SettingsWindowViewModel>()
.AddSingleton<SystemTrayViewModel>()
.AddSingleton<SettingsPageViewModel>()
// ViewModels (Singleton)
.AddSingleton<HostWindowViewModel>()
.AddSingleton<SettingsViewModel>()
.AddSingleton<LyricsPageViewModel>()
.AddSingleton<MusicGalleryViewModel>()
.AddSingleton<LyricsRendererViewModel>()
.AddSingleton<LyricsSettingsControlViewModel>()
.BuildServiceProvider()
);
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
private void App_UnhandledException(
object sender,
Microsoft.UI.Xaml.UnhandledExceptionEventArgs e
)
{
_logger.LogError(e.Exception, "App_UnhandledException");
e.Handled = true;
}
private void CurrentDomain_FirstChanceException(object? sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e)
/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
_logger.LogError(e.Exception, "CurrentDomain_FirstChanceException");
}
private void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e)
{
_logger.LogError(e.ExceptionObject.ToString(), "CurrentDomain_UnhandledException");
}
private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
_logger.LogError(e.Exception, "TaskScheduler_UnobservedTaskException");
WindowHelper.OpenLyricsWindow();
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,170 +1,93 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<RootNamespace>BetterLyrics.WinUI3</RootNamespace>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
</PropertyGroup>
<ItemGroup>
<Compile Remove="ViewModels\Lyrics\**" />
<Content Remove="ViewModels\Lyrics\**" />
<EmbeddedResource Remove="ViewModels\Lyrics\**" />
<None Remove="ViewModels\Lyrics\**" />
<Page Remove="ViewModels\Lyrics\**" />
<PRIResource Remove="ViewModels\Lyrics\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\Segoe Fluent Icons.ttf" />
<None Remove="Assets\Wiki82.profile.xml" />
<None Remove="Controls\SystemTray.xaml" />
<None Remove="Views\MusicGalleryPage.xaml" />
<None Remove="Views\MusicGalleryWindow.xaml" />
<None Remove="Views\SettingsWindow.xaml" />
</ItemGroup>
<ItemGroup>
<Content Include="Logo.ico" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="3v.EvtSource" Version="2.0.0" />
<PackageReference Include="ColorThief.ImageSharp" Version="1.0.0" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250703-build.2173" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.Shimmer" Version="0.1.250703-build.2173" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.MetadataControl" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.5" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.0" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.7" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4654" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
<PackageReference Include="NTextCat" Version="0.3.65" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.3-dev-02320" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="ShadowViewer.Controls.Notification" Version="1.2.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="System.Drawing.Common" Version="9.0.7" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.7" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="TinyPinyin.Net" Version="1.0.2" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="Vanara.PInvoke.CoreAudio" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.DwmApi" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.Gdi32" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.Shell32" Version="4.1.6" />
<PackageReference Include="Vanara.PInvoke.User32" Version="4.1.6" />
<PackageReference Include="WinUIEx" Version="2.6.0" />
<PackageReference Include="z440.atl.core" Version="7.2.0" />
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\DesktopLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--Disable Trimming for Specific Packages-->
<ItemGroup>
<TrimmerRootAssembly Include="TagLibSharp" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\Discord.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EmptyBox.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\EmptyState.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Logo.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Logo.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\QQ.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Segoe Fluent Icons.ttf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Telegram.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\Wiki82.profile.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Views\MusicGalleryWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\MusicGalleryPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Views\SettingsWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\SystemTray.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
</PropertyGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants)</DefineConstants>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Logo.ico</ApplicationIcon>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ShouldCreateLogs>True</ShouldCreateLogs>
<AdvancedSettingsExpanded>True</AdvancedSettingsExpanded>
<UpdateAssemblyVersion>False</UpdateAssemblyVersion>
<UpdateAssemblyFileVersion>False</UpdateAssemblyFileVersion>
<UpdateAssemblyInfoVersion>False</UpdateAssemblyInfoVersion>
<UpdatePackageVersion>True</UpdatePackageVersion>
<AssemblyInfoVersionType>SettingsVersion</AssemblyInfoVersionType>
<InheritWinAppVersionFrom>AssemblyVersion</InheritWinAppVersionFrom>
<PackageVersionSettings>AssemblyVersion.None.None</PackageVersionSettings>
<Version>2025.6.0</Version>
<AssemblyVersion>2025.6.18.0110</AssemblyVersion>
<FileVersion>2025.6.18.0110</FileVersion>
</PropertyGroup>
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<RootNamespace>BetterLyrics.WinUI3</RootNamespace>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<Content Include="Logo.ico" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250513-build.2126" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Drawing.Common" Version="9.0.6" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.6" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="WinUIEx" Version="2.5.1" />
<PackageReference Include="z440.atl.core" Version="6.25.0" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\AI - 甜度爆表.mp3">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\InAppLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Rendering\DesktopLyricsRenderer.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Folder Include="Controls\" />
<Folder Include="ViewModels\Lyrics\" />
</ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
</PropertyGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants)</DefineConstants>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Logo.ico</ApplicationIcon>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ShouldCreateLogs>True</ShouldCreateLogs>
<AdvancedSettingsExpanded>True</AdvancedSettingsExpanded>
<UpdateAssemblyVersion>False</UpdateAssemblyVersion>
<UpdateAssemblyFileVersion>False</UpdateAssemblyFileVersion>
<UpdateAssemblyInfoVersion>False</UpdateAssemblyInfoVersion>
<UpdatePackageVersion>True</UpdatePackageVersion>
<AssemblyInfoVersionType>SettingsVersion</AssemblyInfoVersionType>
<InheritWinAppVersionFrom>AssemblyVersion</InheritWinAppVersionFrom>
<PackageVersionSettings>AssemblyVersion.None.None</PackageVersionSettings>
<Version>2025.6.0</Version>
<AssemblyVersion>2025.6.18.0110</AssemblyVersion>
<FileVersion>2025.6.18.0110</FileVersion>
</PropertyGroup>
</Project>

View File

@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="BetterLyrics.WinUI3.Controls.SystemTray"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:BetterLyrics.WinUI3.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tb="using:H.NotifyIcon"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<tb:TaskbarIcon
x:Name="TrayIcon"
x:FieldModifier="public"
ContextMenuMode="SecondWindow"
DoubleClickCommand="{x:Bind ViewModel.OpenLyricsWindowCommand}"
IconSource="ms-appx:///Assets/Logo.ico"
LeftClickCommand="{x:Bind ViewModel.OpenLyricsWindowCommand}"
NoLeftClickDelay="True"
ToolTipText="{x:Bind ViewModel.ToolTipText, Mode=OneWay}">
<tb:TaskbarIcon.ContextFlyout>
<MenuFlyout
AreOpenCloseAnimationsEnabled="True"
LightDismissOverlayMode="On"
ShowMode="TransientWithDismissOnPointerMoveAway">
<MenuFlyoutItem x:Uid="SystemTrayMusicGallery" Command="{x:Bind ViewModel.OpenMusicGalleryCommand}" />
<MenuFlyoutItem x:Uid="SystemTraySettings" Command="{x:Bind ViewModel.OpenSettingsCommand}" />
<MenuFlyoutItem x:Uid="SystemTrayResetWindowPosition" Command="{x:Bind ViewModel.ResetWindowPositionCommand}" />
<MenuFlyoutItem x:Uid="SystemTrayRestart" Command="{x:Bind ViewModel.RestartAppCommand}" />
<MenuFlyoutItem x:Uid="SystemTrayExit" Command="{x:Bind ViewModel.ExitAppCommand}" />
<MenuFlyoutItem
x:Uid="SystemTrayUnlock"
Command="{x:Bind ViewModel.UnlockWindowCommand}"
Visibility="{x:Bind ViewModel.IsLyricsWindowLocked, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
</MenuFlyout>
</tb:TaskbarIcon.ContextFlyout>
</tb:TaskbarIcon>
</UserControl>

View File

@@ -1,17 +0,0 @@
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.Controls
{
public sealed partial class SystemTray : UserControl
{
public SystemTrayViewModel ViewModel => (SystemTrayViewModel)DataContext;
public SystemTray()
{
InitializeComponent();
DataContext = Ioc.Default.GetService<SystemTrayViewModel>();
}
}
}

View File

@@ -1,33 +0,0 @@
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Converter
{
public partial class AlbumArtSearchProviderToDisplayNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is AlbumArtSearchProvider provider)
{
return provider switch
{
AlbumArtSearchProvider.Local => App.ResourceLoader!.GetString("AlbumArtSearchLocalProvider"),
AlbumArtSearchProvider.SMTC => App.ResourceLoader!.GetString("AlbumArtSearchSMTCProvider"),
AlbumArtSearchProvider.iTunes => "iTunes",
_ => throw new Exception($"Unknown AlbumArtSearchProvider: {provider}"),
};
}
throw new ArgumentException("Value must be of type AlbumArtSearchProvider", nameof(value));
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,13 +1,15 @@
// 2025/6/23 by Zhe Fang
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace BetterLyrics.WinUI3.Converter
{
public partial class ColorToBrushConverter : IValueConverter
public class ColorToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{

View File

@@ -1,22 +0,0 @@
using System;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public partial class CornerRadiusToDoubleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is Microsoft.UI.Xaml.CornerRadius cornerRadius)
{
return (double)cornerRadius.TopLeft;
}
return .0;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,11 +1,14 @@
// 2025/6/23 by Zhe Fang
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public partial class EnumToIntConverter : IValueConverter
internal class EnumToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{

View File

@@ -1,11 +1,14 @@
// 2025/6/23 by Zhe Fang
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.ViewModels;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public partial class IntToCornerRadius : IValueConverter
public class IntToCornerRadius : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{

View File

@@ -1,37 +0,0 @@
// 2025/6/23 by Zhe Fang
using System;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public partial class LyricsSearchProviderToDisplayNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is LyricsSearchProvider provider)
{
return provider switch
{
LyricsSearchProvider.LrcLib => "LrcLib",
LyricsSearchProvider.QQ => "QQ",
LyricsSearchProvider.Netease => "Netease",
LyricsSearchProvider.Kugou => "Kugou",
LyricsSearchProvider.AmllTtmlDb => "amll-ttml-db",
LyricsSearchProvider.LocalLrcFile => App.ResourceLoader!.GetString("LyricsSearchProviderLocalLrcFile"),
LyricsSearchProvider.LocalMusicFile => App.ResourceLoader!.GetString("LyricsSearchProviderLocalMusicFile"),
LyricsSearchProvider.LocalEslrcFile => App.ResourceLoader!.GetString("LyricsSearchProviderEslrcFile"),
LyricsSearchProvider.LocalTtmlFile => App.ResourceLoader!.GetString("LyricsSearchProviderTtmlFile"),
_ => "N/A",
};
}
return "N/A";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,12 +1,10 @@
// 2025/6/23 by Zhe Fang
using System;
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public partial class MatchedLocalFilesPathToVisibilityConverter : IValueConverter
public class MatchedLocalFilesPathToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{

View File

@@ -1,30 +0,0 @@
using Microsoft.UI.Xaml.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Converter
{
public partial class SecondsToFormattedTimeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is double seconds)
{
return TimeSpan.FromSeconds(seconds).ToString(@"mm\:ss");
}
else if (value is int secondsInt)
{
return TimeSpan.FromSeconds(secondsInt).ToString(@"mm\:ss");
}
return value?.ToString() ?? "";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,38 +0,0 @@
// 2025/6/23 by Zhe Fang
using System;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Xaml.Data;
namespace BetterLyrics.WinUI3.Converter
{
public partial class TranslationSearchProviderToDisplayNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is TranslationSearchProvider provider)
{
return provider switch
{
TranslationSearchProvider.LrcLib => "LrcLib",
TranslationSearchProvider.QQ => "QQ",
TranslationSearchProvider.Netease => "Netease",
TranslationSearchProvider.Kugou => "Kugou",
TranslationSearchProvider.AmllTtmlDb => "amll-ttml-db",
TranslationSearchProvider.LocalLrcFile => App.ResourceLoader!.GetString("LyricsSearchProviderLocalLrcFile"),
TranslationSearchProvider.LocalMusicFile => App.ResourceLoader!.GetString("LyricsSearchProviderLocalMusicFile"),
TranslationSearchProvider.LocalEslrcFile => App.ResourceLoader!.GetString("LyricsSearchProviderEslrcFile"),
TranslationSearchProvider.LocalTtmlFile => App.ResourceLoader!.GetString("LyricsSearchProviderTtmlFile"),
TranslationSearchProvider.LibreTranslate => "LibreTranslate",
_ => "N/A",
};
}
return "N/A";
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,4 +1,8 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
@@ -6,6 +10,5 @@ namespace BetterLyrics.WinUI3.Enums
{
StandardMode,
DockMode,
DesktopMode,
}
}

View File

@@ -1,4 +1,8 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{

View File

@@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum DockPlacement
{
Top,
Bottom
}
public static class DockPlacementExtensions
{
public static WindowPixelSampleMode ToWindowPixelSampleMode(this DockPlacement placement)
{
return placement switch
{
DockPlacement.Top => WindowPixelSampleMode.BelowWindow,
DockPlacement.Bottom => WindowPixelSampleMode.AboveWindow,
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null)
};
}
}
}

View File

@@ -1,20 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum EasingType
{
Linear,
SmoothStep,
EaseInOutSine,
EaseInOutQuad,
EaseInOutCubic,
EaseInOutQuart,
EaseInOutQuint,
EaseInOutExpo,
EaseInOutCirc,
EaseInOutBack,
EaseInOutElastic,
EaseInOutBounce,
}
}

View File

@@ -1,6 +1,4 @@
// 2025/6/23 by Zhe Fang
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -14,7 +12,5 @@ namespace BetterLyrics.WinUI3.Enums
English,
SimplifiedChinese,
TraditionalChinese,
Japanese,
Korean,
}
}

View File

@@ -1,11 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum LineRenderingType
{
CurrentChar,
LineStartToCurrentChar,
CurrentLine
}
}

View File

@@ -1,10 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum LocalSearchTargetProps
{
LyricsOnly,
LyricsAndAlbumArt,
}
}

View File

@@ -6,10 +6,10 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum CommonSongProperty
public enum LyricsAlignmentType
{
Title,
Album,
Artist
Left,
Center,
Right,
}
}

View File

@@ -1,11 +1,10 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsDisplayType
{
AlbumArtOnly,
LyricsOnly,
SplitView,
PlaceholderOnly,
}
}

View File

@@ -1,11 +1,14 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsFontColorType
{
AdaptiveColored,
AdaptiveGrayed,
Custom,
Default,
Dominant,
}
}

View File

@@ -1,6 +1,8 @@
// 2025/6/23 by Zhe Fang
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Text;
using Windows.UI.Text;
@@ -38,7 +40,11 @@ namespace BetterLyrics.WinUI3.Enums
LyricsFontWeight.ExtraBold => FontWeights.ExtraBold,
LyricsFontWeight.Black => FontWeights.Black,
LyricsFontWeight.ExtraBlack => FontWeights.ExtraBlack,
LyricsFontWeight _ => throw new ArgumentOutOfRangeException(nameof(weight), weight, null),
LyricsFontWeight _ => throw new ArgumentOutOfRangeException(
nameof(weight),
weight,
null
),
};
}
}

View File

@@ -1,71 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsFormat
{
Lrc,
Eslrc,
Ttml,
Qrc,
Krc,
NotSpecified,
}
public static class LyricsFormatExtensions
{
public static LyricsFormat? DetectFormat(this string content)
{
if (string.IsNullOrWhiteSpace(content))
return null;
// TTML: 检查 <tt ... xmlns="http://www.w3.org/ns/ttml"
if (System.Text.RegularExpressions.Regex.IsMatch(
content,
@"<tt\b[^>]*\bxmlns\s*=\s*[""']http://www\.w3\.org/ns/ttml[""']",
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
return LyricsFormat.Ttml;
}
// KRC: 检测主内容格式 [start,duration]<offset,duration,0>字...
else if (System.Text.RegularExpressions.Regex.IsMatch(
content,
@"^\[\d+,\d+\](<\d+,\d+,0>.+)+",
System.Text.RegularExpressions.RegexOptions.Multiline))
{
return LyricsFormat.Krc;
}
// QRC: 检测主内容格式 [start,duration]字(offset,duration)
else if (System.Text.RegularExpressions.Regex.IsMatch(
content,
@"^\[\d+,\d+\].*?\(\d+,\d+\)",
System.Text.RegularExpressions.RegexOptions.Multiline))
{
return LyricsFormat.Qrc;
}
// 标准LRC和增强型LRC
else if (System.Text.RegularExpressions.Regex.IsMatch(content, @"\[\d{1,2}:\d{2}") ||
System.Text.RegularExpressions.Regex.IsMatch(content, @"<\d{1,2}:\d{2}\.\d{2,3}>"))
{
return LyricsFormat.Lrc;
}
else
{
return null;
}
}
public static string ToFileExtension(this LyricsFormat format)
{
return format switch
{
LyricsFormat.Lrc => ".lrc",
LyricsFormat.Qrc => ".qrc",
LyricsFormat.Krc => ".krc",
LyricsFormat.Eslrc => ".eslrc",
LyricsFormat.Ttml => ".ttml",
_ => ".*",
};
}
}
}

View File

@@ -6,10 +6,10 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum AlbumArtSearchProvider
public enum LyricsGlowEffectScope
{
Local,
SMTC,
iTunes,
WholeLyrics,
CurrentLine,
CurrentChar,
}
}

View File

@@ -0,0 +1,8 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsHighlightType
{
LineByLine,
CharByChar,
}
}

View File

@@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsLayoutOrientation
{
Horizontal,
Vertical,
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsPlayingState
{
/// <summary>
/// Not played yet, will be playing in the future
/// </summary>
NotPlayed,
/// <summary>
/// Playing
/// </summary>
Playing,
/// <summary>
/// Has already played
/// </summary>
Played,
}
}

View File

@@ -1,82 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Helper;
namespace BetterLyrics.WinUI3.Enums
{
public enum LyricsSearchProvider
{
QQ,
Kugou,
Netease,
LrcLib,
AmllTtmlDb,
LocalMusicFile,
LocalLrcFile,
LocalEslrcFile,
LocalTtmlFile,
}
public static class LyricsSearchProviderExtensions
{
public static string GetCacheDirectory(this LyricsSearchProvider provider)
{
return provider switch
{
LyricsSearchProvider.LrcLib => PathHelper.LrcLibLyricsCacheDirectory,
LyricsSearchProvider.QQ => PathHelper.QQLyricsCacheDirectory,
LyricsSearchProvider.Netease => PathHelper.NeteaseLyricsCacheDirectory,
LyricsSearchProvider.Kugou => PathHelper.KugouLyricsCacheDirectory,
LyricsSearchProvider.AmllTtmlDb => PathHelper.AmllTtmlDbLyricsCacheDirectory,
_ => throw new System.ArgumentOutOfRangeException(nameof(provider)),
};
}
public static LyricsFormat GetLyricsFormat(this LyricsSearchProvider provider)
{
return provider switch
{
LyricsSearchProvider.LrcLib => LyricsFormat.Lrc,
LyricsSearchProvider.QQ => LyricsFormat.Qrc,
LyricsSearchProvider.Kugou => LyricsFormat.Krc,
LyricsSearchProvider.Netease => LyricsFormat.Lrc,
LyricsSearchProvider.AmllTtmlDb => LyricsFormat.Ttml,
LyricsSearchProvider.LocalLrcFile => LyricsFormat.Lrc,
LyricsSearchProvider.LocalEslrcFile => LyricsFormat.Eslrc,
LyricsSearchProvider.LocalTtmlFile => LyricsFormat.Ttml,
_ => LyricsFormat.NotSpecified,
};
}
public static bool IsLocal(this LyricsSearchProvider provider)
{
return provider
is LyricsSearchProvider.LocalMusicFile
or LyricsSearchProvider.LocalLrcFile
or LyricsSearchProvider.LocalEslrcFile
or LyricsSearchProvider.LocalTtmlFile;
}
public static bool IsRemote(this LyricsSearchProvider provider)
{
return !provider.IsLocal();
}
public static TranslationSearchProvider? ToTranslationSearchProvider(this LyricsSearchProvider? provider)
{
return provider switch
{
LyricsSearchProvider.LrcLib => TranslationSearchProvider.LrcLib,
LyricsSearchProvider.QQ => TranslationSearchProvider.QQ,
LyricsSearchProvider.Kugou => TranslationSearchProvider.Kugou,
LyricsSearchProvider.Netease => TranslationSearchProvider.Netease,
LyricsSearchProvider.AmllTtmlDb => TranslationSearchProvider.AmllTtmlDb,
LyricsSearchProvider.LocalMusicFile => TranslationSearchProvider.LocalMusicFile,
LyricsSearchProvider.LocalLrcFile => TranslationSearchProvider.LocalLrcFile,
LyricsSearchProvider.LocalEslrcFile => TranslationSearchProvider.LocalEslrcFile,
LyricsSearchProvider.LocalTtmlFile => TranslationSearchProvider.LocalTtmlFile,
_ => null,
};
}
}
}

View File

@@ -6,9 +6,9 @@ using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum LineMaskType
public enum LyricsType
{
Glow,
Highlight,
InAppLyrics,
DesktopLyrics,
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum PlaybackOrder
{
RepeatAll,
RepeatOne,
Shuffle,
}
}

View File

@@ -1,28 +0,0 @@
// 2025/6/23 by Zhe Fang
using Microsoft.Graphics.Canvas.Text;
using System;
namespace BetterLyrics.WinUI3.Enums
{
public enum TextAlignmentType
{
Left,
Center,
Right,
}
public static class LyricsAlignmentTypeExtensions
{
public static CanvasHorizontalAlignment ToCanvasHorizontalAlignment(this TextAlignmentType alignmentType)
{
return alignmentType switch
{
TextAlignmentType.Left => CanvasHorizontalAlignment.Left,
TextAlignmentType.Center => CanvasHorizontalAlignment.Center,
TextAlignmentType.Right => CanvasHorizontalAlignment.Right,
_ => throw new ArgumentOutOfRangeException(nameof(alignmentType), alignmentType, null),
};
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum TitleBarType
{
Compact,
Extended,
}
public static class TitleBarTypeExtensions
{
public static double GetHeight(this TitleBarType titleBarType)
{
return titleBarType switch
{
TitleBarType.Compact => 32.0,
TitleBarType.Extended => 48.0,
_ => throw new ArgumentOutOfRangeException(
nameof(titleBarType),
titleBarType,
null
),
};
}
}
}

View File

@@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Enums
{
public enum TranslationSearchProvider
{
QQ,
Kugou,
Netease,
LrcLib,
AmllTtmlDb,
LocalMusicFile,
LocalLrcFile,
LocalEslrcFile,
LocalTtmlFile,
LibreTranslate,
}
}

View File

@@ -1,10 +0,0 @@
namespace BetterLyrics.WinUI3.Enums
{
public enum WindowPixelSampleMode
{
BelowWindow,
AboveWindow,
WindowArea,
WindowEdge,
}
}

View File

@@ -1,17 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.Events
{
public class AlbumArtChangedEventArgs(SoftwareBitmap? albumArtSwBitmap, Color? albumArtLightAccentColor, Color? albumArtDarkAccentColor) : EventArgs
{
public SoftwareBitmap? AlbumArtSwBitmap { get; set; } = albumArtSwBitmap;
public Color? AlbumArtLightAccentColor { get; set; } = albumArtLightAccentColor;
public Color? AlbumArtDarkAccentColor { get; set; } = albumArtDarkAccentColor;
}
}

View File

@@ -1,6 +1,8 @@
// 2025/6/23 by Zhe Fang
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Events
{

View File

@@ -1,18 +0,0 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Events
{
public class LibChangedEventArgs(string folder, string filePath, WatcherChangeTypes changeType) : EventArgs
{
public WatcherChangeTypes ChangeType { get; } = changeType;
public string FilePath { get; } = filePath;
public string Folder { get; } = folder;
}
}

View File

@@ -1,14 +0,0 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Events
{
public class MediaSourceProvidersInfoEventArgs(List<MediaSourceProviderInfo> sessionIds):EventArgs
{
public List<MediaSourceProviderInfo> MediaSourceProviersInfo { get; set; } = sessionIds;
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Events
{
public class PositionChangedEventArgs(TimeSpan position) : EventArgs()
{
public TimeSpan Position { get; set; } = position;
}
}

View File

@@ -1,6 +1,8 @@
// 2025/6/23 by Zhe Fang
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Models;
namespace BetterLyrics.WinUI3.Events

View File

@@ -1,12 +0,0 @@
// 2025/6/23 by Zhe Fang
using System;
namespace BetterLyrics.WinUI3.Events
{
public class TimelineChangedEventArgs(TimeSpan position, TimeSpan end) : EventArgs()
{
public TimeSpan Position { get; set; } = position;
public TimeSpan End { get; set; } = end;
}
}

View File

@@ -0,0 +1,13 @@
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Animation;
namespace BetterLyrics.WinUI3.Helper
{
public static class AnimationHelper
{
public const int StackedNotificationsShowingDuration = 3900;
public const int StoryboardDefaultDuration = 200;
public const int DebounceDefaultDuration = 200;
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
using System;
using System.IO;
using Windows.ApplicationModel;
using Windows.Storage;
public static class AppInfo
{
// App Metadata
public const string AppName = "BetterLyrics";
public const string AppDisplayName = "Better Lyrics";
public const string AppAuthor = "Zhe Fang";
public const string GithubUrl = "https://github.com/jayfunc/BetterLyrics";
public static string AppVersion
{
get
{
var version = Package.Current.Id.Version;
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
}
// Environment Info
public static bool IsDebug =>
#if DEBUG
true;
#else
false;
#endif
// Base Folders
private static string LocalFolder => ApplicationData.Current.LocalFolder.Path;
public static string CacheFolder => ApplicationData.Current.LocalCacheFolder.Path;
public static string AssetsFolder => Path.Combine(Package.Current.InstalledPath, "Assets");
// Data Files
private static string DatabaseFileName => "database.db";
public static string DatabasePath => Path.Combine(LocalFolder, DatabaseFileName);
public static string LogDirectory => Path.Combine(CacheFolder, "logs");
public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt");
private static string TestMusicFileName => "AI - 甜度爆表.mp3";
public static string TestMusicPath => Path.Combine(AssetsFolder, TestMusicFileName);
public static void EnsureDirectories()
{
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LocalFolder);
}
}
}

View File

@@ -1,19 +0,0 @@
using Microsoft.UI.Windowing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public static class AppWindowHelper
{
public static void SetIcons(this AppWindow appWindow)
{
appWindow.SetIcon(PathHelper.LogoPath);
appWindow.SetTaskbarIcon(PathHelper.LogoPath);
appWindow.SetTitleBarIcon(PathHelper.LogoPath);
}
}
}

View File

@@ -1,10 +1,5 @@
using ATL;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using System;
using System.Collections;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -13,39 +8,11 @@ namespace BetterLyrics.WinUI3.Helper
{
public static class CollectionHelper
{
public static ObservableCollection<GroupInfoList> GetGroupedBy<T>(
this IEnumerable<T> items,
Func<T, object> groupKeySelector,
Func<object, object>? orderSelector = null)
public static T? SafeGet<T>(this IList<T> list, int index)
{
var query = from item in items
group item by groupKeySelector(item) into g
orderby g.Key
select new GroupInfoList(g.Cast<object>(), orderSelector) { Key = g.Key };
return new ObservableCollection<GroupInfoList>(query);
if (list == null || index < 0 || index >= list.Count)
return default;
return list[index];
}
public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> items)
{
if (collection == null) return;
if (items == null) return;
foreach (var item in items)
{
collection.Add(item);
}
}
public static void InsertRange<T>(this IList<T> list, int index, IEnumerable<T> items)
{
if (list == null) return;
if (items == null) return;
if (index < 0 || index > list.Count) return;
foreach (var item in items)
{
list.Insert(index++, item);
}
}
}
}

View File

@@ -1,58 +1,24 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using System;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using Vanara.PInvoke;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI;
using Color = Windows.UI.Color;
namespace BetterLyrics.WinUI3.Helper
{
public static class ColorHelper
{
public static ElementTheme GetElementThemeFromBackgroundColor(Color backgroundColor)
public static Windows.UI.Color ToWindowsUIColor(this System.Drawing.Color color)
{
// 计算亮度YIQ公式
double yiq =
((backgroundColor.R * 299) + (backgroundColor.G * 587) + (backgroundColor.B * 114))
/ 1000.0;
return yiq >= 128 ? ElementTheme.Light : ElementTheme.Dark;
return Windows.UI.Color.FromArgb(color.A, color.R, color.G, color.B);
}
public static Color GetForegroundColor(Color background)
{
// 转为 HSL
var hsl = CommunityToolkit.WinUI.Helpers.ColorHelper.ToHsl(background);
double h = hsl.H;
double s = hsl.S;
double l = hsl.L;
// 目标亮度与背景错开,但不极端
double targetL;
if (l >= 0.7)
targetL = 0.35; // 背景很亮,前景适中偏暗
else if (l <= 0.3)
targetL = 0.75; // 背景很暗,前景适中偏亮
else
targetL = l > 0.5 ? l - 0.35 : l + 0.35; // 其余情况适度错开
// 保持色相,适当提升饱和度
double targetS = Math.Min(1.0, s + 0.2);
// 转回 Color
var fg = CommunityToolkit.WinUI.Helpers.ColorHelper.FromHsl(h, targetS, targetL);
// 保持不透明
return Color.FromArgb(255, fg.R, fg.G, fg.B);
}
public static Color GetInterpolatedColor(float progress, Color startColor, Color targetColor)
public static Color GetInterpolatedColor(
float progress,
Color startColor,
Color targetColor
)
{
byte Lerp(byte a, byte b) => (byte)(a + (progress * (b - a)));
return Color.FromArgb(
@@ -62,190 +28,5 @@ namespace BetterLyrics.WinUI3.Helper
Lerp(startColor.B, targetColor.B)
);
}
public static Color ToColor(this int argb)
{
byte a = (byte)(argb >> 24);
byte r = (byte)(argb >> 16);
byte g = (byte)(argb >> 8);
byte b = (byte)argb;
// 还原非预乘分量
if (a == 0)
return Color.FromArgb(0, 0, 0, 0);
// 预乘解码
// 这里 a+1 是编码时的分母
int ap1 = a + 1;
r = (byte)Math.Min(255, (r * 255 + (ap1 / 2)) / ap1);
g = (byte)Math.Min(255, (g * 255 + (ap1 / 2)) / ap1);
b = (byte)Math.Min(255, (b * 255 + (ap1 / 2)) / ap1);
return Color.FromArgb(a, r, g, b);
}
public static Color ToColor(this System.Drawing.Color color)
{
return Color.FromArgb(color.A, color.R, color.G, color.B);
}
public static Color WithAlpha(this Color color, byte alpha)
{
return Color.FromArgb(alpha, color.R, color.G, color.B);
}
public static Color WithBrightness(this Color color, double brightness)
{
// 确保亮度因子在合理范围内
brightness = Math.Max(0, Math.Min(1, brightness));
var hsl = CommunityToolkit.WinUI.Helpers.ColorHelper.ToHsl(color);
double h = hsl.H;
double s = hsl.S;
return CommunityToolkit.WinUI.Helpers.ColorHelper.FromHsl(h, s, brightness);
}
public static System.Drawing.Color GetAccentColor(IntPtr myHwnd, string monitorDeviceName, WindowPixelSampleMode mode)
{
if (!User32.GetWindowRect(myHwnd, out RECT myRect)) return System.Drawing.Color.Transparent;
var monitorInfo = MonitorHelper.GetMonitorInfoExFromDeviceName(monitorDeviceName);
int screenWidth = monitorInfo.rcMonitor.Width;
switch (mode)
{
case WindowPixelSampleMode.BelowWindow:
{
int sampleHeight = 1;
int sampleY = myRect.Bottom + 1;
return GetAverageColorFromScreenRegion(myRect.Left, sampleY, screenWidth, sampleHeight);
}
case WindowPixelSampleMode.AboveWindow:
{
int sampleHeight = 1;
int sampleY = myRect.Top - 1;
return GetAverageColorFromScreenRegion(myRect.Left, sampleY, screenWidth, sampleHeight);
}
case WindowPixelSampleMode.WindowArea:
{
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return System.Drawing.Color.Transparent;
// 采集窗口区域的平均色
return GetAverageColorFromScreenRegion(myRect.Left, myRect.Top, width, height);
}
case WindowPixelSampleMode.WindowEdge:
{
int width = myRect.Right - myRect.Left;
int height = myRect.Bottom - myRect.Top;
if (width <= 0 || height <= 0)
return System.Drawing.Color.Transparent;
var edgeThickness = new Thickness(36, 0, 36, 0);
List<System.Drawing.Color> edgeColors = [];
// Top edge
if (edgeThickness.Top > 0 && edgeThickness.Top < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top,
width,
(int)edgeThickness.Top
)
);
// Bottom edge
if (edgeThickness.Bottom > 0 && edgeThickness.Bottom < height)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Bottom - (int)edgeThickness.Bottom,
width,
(int)edgeThickness.Bottom
)
);
// Left edge
if (edgeThickness.Left > 0 && edgeThickness.Left < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Left,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Left,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
// Right edge
if (edgeThickness.Right > 0 && edgeThickness.Right < width)
edgeColors.Add(
GetAverageColorFromScreenRegion(
myRect.Right - (int)edgeThickness.Right,
myRect.Top + (int)edgeThickness.Top,
(int)edgeThickness.Right,
height - (int)edgeThickness.Top - (int)edgeThickness.Bottom
)
);
// 合并四边平均色
if (edgeColors.Count == 0)
return System.Drawing.Color.Transparent;
long r = 0,
g = 0,
b = 0;
foreach (var c in edgeColors)
{
r += c.R;
g += c.G;
b += c.B;
}
return System.Drawing.Color.FromArgb(
255,
(int)(r / edgeColors.Count),
(int)(g / edgeColors.Count),
(int)(b / edgeColors.Count)
);
}
default:
return System.Drawing.Color.Transparent;
}
}
private static System.Drawing.Color GetAverageColorFromScreenRegion(int x, int y, int width, int height)
{
using Bitmap bmp = new(width, height, PixelFormat.Format32bppArgb);
using Graphics gDest = Graphics.FromImage(bmp);
IntPtr hdcDest = gDest.GetHdc();
IntPtr hdcSrc = (nint)User32.GetDC(IntPtr.Zero); // Entire screen
Gdi32.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, Gdi32.RasterOperationMode.SRCCOPY);
gDest.ReleaseHdc(hdcDest);
User32.ReleaseDC(IntPtr.Zero, hdcSrc);
return ComputeAverageColor(bmp);
}
private static System.Drawing.Color ComputeAverageColor(Bitmap bmp)
{
long r = 0, g = 0, b = 0;
int count = 0;
for (int y = 0; y < bmp.Height; y++)
{
for (int x = 0; x < bmp.Width; x++)
{
System.Drawing.Color pixel = bmp.GetPixel(x, y);
r += pixel.R;
g += pixel.G;
b += pixel.B;
count++;
}
}
if (count == 0) return System.Drawing.Color.Transparent;
return System.Drawing.Color.FromArgb((int)(r / count), (int)(g / count), (int)(b / count));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,122 +0,0 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using Vanara.PInvoke;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
public static class DesktopModeHelper
{
private static readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private static readonly Dictionary<IntPtr, bool> _originalTopmostStates = [];
private static readonly Dictionary<IntPtr, (double X, double Y, double Width, double Height)> _originalWindowBounds = [];
private static readonly Dictionary<IntPtr, WindowStyle> _originalWindowStyles = [];
private delegate nint WndProcDelegate(nint hWnd, uint msg, nint wParam, nint lParam);
public static void Disable(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
// <20>ָ<EFBFBD>TopMost״̬
if (_originalTopmostStates.TryGetValue(hwnd, out var wasTopMost))
{
window.SetIsAlwaysOnTop(wasTopMost);
_originalTopmostStates.Remove(hwnd);
}
// <20>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD><EFBFBD>λ<EFBFBD>úʹ<C3BA>С
if (_originalWindowBounds.TryGetValue(hwnd, out var bounds))
{
window.AppWindow.MoveAndResize(
new Windows.Graphics.RectInt32(
(int)bounds.X,
(int)bounds.Y,
(int)bounds.Width,
(int)bounds.Height
)
);
_originalWindowBounds.Remove(hwnd);
}
window.SetIsShownInSwitchers(true);
}
public static void Enable(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
// <20><>¼ԭʼ<D4AD><CABC><EFBFBD><EFBFBD>λ<EFBFBD>úʹ<C3BA>С
if (!_originalWindowBounds.ContainsKey(hwnd))
{
_originalWindowBounds[hwnd] = (
window.AppWindow.Position.X,
window.AppWindow.Position.Y,
window.AppWindow.Size.Width,
window.AppWindow.Size.Height
);
}
// <20>Ӵ洢<D3B4><E6B4A2><EFBFBD><EFBFBD>ȡĿ<C8A1><C4BF><EFBFBD><EFBFBD><EFBFBD>ߺ<EFBFBD>λ<EFBFBD><CEBB>
int targetWidth = _settingsService.DesktopWindowWidth;
int targetHeight = _settingsService.DesktopWindowHeight;
int targetX = _settingsService.DesktopWindowLeft;
int targetY = _settingsService.DesktopWindowTop;
// <20><><EFBFBD>ô<EFBFBD><C3B4>ڴ<EFBFBD>С<EFBFBD><D0A1>λ<EFBFBD><CEBB>
window.AppWindow.MoveAndResize(
new Windows.Graphics.RectInt32(
targetX,
targetY,
targetWidth,
targetHeight
)
);
// <20><><EFBFBD><EFBFBD>ԭTopMost״̬
if (!_originalTopmostStates.ContainsKey(hwnd))
_originalTopmostStates[hwnd] = window.GetIsAlwaysOnTop();
// <20><><EFBFBD>ô<EFBFBD><C3B4><EFBFBD><EFBFBD>ö<EFBFBD>
window.SetIsAlwaysOnTop(true);
window.SetIsShownInSwitchers(false);
}
public static void SetClickThrough(Window window, bool enable)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
int exStyle = User32.GetWindowLong(hwnd, User32.WindowLongFlags.GWL_EXSTYLE);
if (enable)
{
// <20><><EFBFBD><EFBFBD>ԭ<EFBFBD><D4AD>ʽ
if (!_originalWindowStyles.ContainsKey(hwnd))
_originalWindowStyles[hwnd] = window.GetWindowStyle();
window.ToggleWindowStyle(true, WindowStyle.Popup | WindowStyle.Visible);
User32.SetWindowLong(hwnd, User32.WindowLongFlags.GWL_EXSTYLE, exStyle | (int)User32.WindowStylesEx.WS_EX_TRANSPARENT | (int)User32.WindowStylesEx.WS_EX_LAYERED);
}
else
{
User32.SetWindowLong(hwnd, User32.WindowLongFlags.GWL_EXSTYLE, exStyle & ~(int)User32.WindowStylesEx.WS_EX_TRANSPARENT);
// <20>ָ<EFBFBD><D6B8><EFBFBD>ʽ
if (_originalWindowStyles.TryGetValue(hwnd, out var style))
{
window.SetWindowStyle(style);
_originalWindowStyles.Remove(hwnd);
}
}
}
}
}

View File

@@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
public static class DockHelper
{
private static readonly HashSet<IntPtr> _registered = [];
private static readonly Dictionary<IntPtr, RECT> _originalPositions = [];
private static readonly Dictionary<IntPtr, WindowStyle> _originalWindowStyle = [];
public static void Disable(Window window)
{
window.SetIsShownInSwitchers(true);
window.ExtendsContentIntoTitleBar = true;
IntPtr hwnd = WindowNative.GetWindowHandle(window);
window.SetWindowStyle(_originalWindowStyle[hwnd]);
_originalWindowStyle.Remove(hwnd);
if (_originalPositions.TryGetValue(hwnd, out var rect))
{
SetWindowPos(
hwnd,
IntPtr.Zero,
rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_SHOWWINDOW
);
_originalPositions.Remove(hwnd);
}
window.SetIsAlwaysOnTop(false);
UnregisterAppBar(hwnd);
}
public static void Enable(Window window, int appBarHeight)
{
window.SetIsShownInSwitchers(false);
window.ExtendsContentIntoTitleBar = false;
IntPtr hwnd = WindowNative.GetWindowHandle(window);
if (!_originalWindowStyle.ContainsKey(hwnd))
{
_originalWindowStyle[hwnd] = window.GetWindowStyle();
}
window.SetWindowStyle(WindowStyle.Popup | WindowStyle.Visible);
if (!_originalPositions.ContainsKey(hwnd))
{
if (GetWindowRect(hwnd, out var rect))
{
_originalPositions[hwnd] = rect;
}
}
RegisterAppBar(hwnd, appBarHeight);
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
SetWindowPos(
hwnd,
IntPtr.Zero,
0,
0,
screenWidth,
appBarHeight,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_SHOWWINDOW
);
window.SetIsAlwaysOnTop(true);
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
#region AppBar registration
private const uint ABM_NEW = 0x00000000;
private const uint ABM_REMOVE = 0x00000001;
private const uint ABM_SETPOS = 0x00000003;
private const int ABE_TOP = 1;
[StructLayout(LayoutKind.Sequential)]
private struct APPBARDATA
{
public int cbSize;
public IntPtr hWnd;
public uint uCallbackMessage;
public uint uEdge;
public RECT rc;
public int lParam;
}
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int left,
top,
right,
bottom;
}
[DllImport("shell32.dll", SetLastError = true)]
private static extern uint SHAppBarMessage(uint dwMessage, ref APPBARDATA pData);
private static void RegisterAppBar(IntPtr hwnd, int height)
{
if (_registered.Contains(hwnd))
return;
APPBARDATA abd = new()
{
cbSize = Marshal.SizeOf<APPBARDATA>(),
hWnd = hwnd,
uEdge = ABE_TOP,
rc = new RECT
{
left = 0,
top = 0,
right = GetSystemMetrics(SM_CXSCREEN),
bottom = height,
},
};
SHAppBarMessage(ABM_NEW, ref abd);
SHAppBarMessage(ABM_SETPOS, ref abd);
_registered.Add(hwnd);
}
private static void UnregisterAppBar(IntPtr hwnd)
{
if (!_registered.Contains(hwnd))
return;
APPBARDATA abd = new() { cbSize = Marshal.SizeOf<APPBARDATA>(), hWnd = hwnd };
SHAppBarMessage(ABM_REMOVE, ref abd);
_registered.Remove(hwnd);
}
#endregion
#region Win32 Helper and Constants
private const int SWP_NOACTIVATE = 0x0010;
private const int SWP_NOOWNERZORDER = 0x0200;
private const int SWP_SHOWWINDOW = 0x0040;
private const int SM_CXSCREEN = 0;
private const int SM_CYSCREEN = 0;
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(int nIndex);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool SetWindowPos(
IntPtr hWnd,
IntPtr hWndInsertAfter,
int X,
int Y,
int cx,
int cy,
uint uFlags
);
#endregion
}
}

View File

@@ -1,200 +0,0 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Vanara.PInvoke;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
public static class DockModeHelper
{
private static readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private static readonly HashSet<IntPtr> _registered = [];
private static readonly Dictionary<IntPtr, RECT> _originalPositions = [];
private static readonly Dictionary<IntPtr, WindowStyle> _originalWindowStyle = [];
public static void Disable(Window window)
{
IntPtr hwnd = WindowNative.GetWindowHandle(window);
if (!_registered.Contains(hwnd)) return;
window.SetIsShownInSwitchers(true);
window.ExtendsContentIntoTitleBar = true;
window.SetIsAlwaysOnTop(false);
UnregisterAppBar(hwnd);
window.SetWindowStyle(_originalWindowStyle[hwnd]);
_originalWindowStyle.Remove(hwnd);
window.ExtendsContentIntoTitleBar = true;
if (_originalPositions.TryGetValue(hwnd, out var rect))
{
User32.SetWindowPos(
hwnd,
IntPtr.Zero,
rect.Left,
rect.Top,
rect.Right - rect.Left,
rect.Bottom - rect.Top,
User32.SetWindowPosFlags.SWP_SHOWWINDOW
);
_originalPositions.Remove(hwnd);
}
}
public static void Enable(Window window, string monitorDeviceName, int appBarHeight, DockPlacement dockPlacement)
{
window.SetIsShownInSwitchers(false);
window.SetIsAlwaysOnTop(true);
IntPtr hwnd = WindowNative.GetWindowHandle(window);
if (!_originalWindowStyle.ContainsKey(hwnd))
{
_originalWindowStyle[hwnd] = window.GetWindowStyle();
}
if (!_originalPositions.ContainsKey(hwnd))
{
if (User32.GetWindowRect(hwnd, out var rect))
{
_originalPositions[hwnd] = rect;
}
}
RegisterAppBar(hwnd, monitorDeviceName, appBarHeight, dockPlacement);
var monitorInfo = MonitorHelper.GetMonitorInfoExFromDeviceName(_settingsService.DockMonitorDeviceName);
int screenWidth = monitorInfo.rcMonitor.Width;
int screenHeight = monitorInfo.rcMonitor.Bottom - monitorInfo.rcMonitor.Top;
int y = dockPlacement == DockPlacement.Top ? monitorInfo.rcMonitor.Top : monitorInfo.rcMonitor.Bottom - appBarHeight;
User32.SetWindowPos(
hwnd,
IntPtr.Zero,
monitorInfo.rcMonitor.Left,
y,
screenWidth,
appBarHeight,
User32.SetWindowPosFlags.SWP_HIDEWINDOW
);
window.ExtendsContentIntoTitleBar = false;
window.ToggleWindowStyle(true, WindowStyle.Popup);
window.Show();
}
private static void RegisterAppBar(IntPtr hwnd, string monitorDeviceName, int height, DockPlacement dockPlacement)
{
if (_registered.Contains(hwnd)) return;
var uEdge = dockPlacement == DockPlacement.Top ? Shell32.ABE.ABE_TOP : Shell32.ABE.ABE_BOTTOM;
var monitorInfo = MonitorHelper.GetMonitorInfoExFromDeviceName(monitorDeviceName);
int top = dockPlacement == DockPlacement.Top ? monitorInfo.rcMonitor.Top : monitorInfo.rcMonitor.Bottom - height;
int bottom = dockPlacement == DockPlacement.Top ? monitorInfo.rcMonitor.Top + height : monitorInfo.rcMonitor.Bottom;
Shell32.APPBARDATA abd = new()
{
cbSize = (uint)Marshal.SizeOf<Shell32.APPBARDATA>(),
hWnd = hwnd,
uEdge = uEdge,
rc = new RECT
{
Left = monitorInfo.rcMonitor.Left,
Top = top,
Right = monitorInfo.rcMonitor.Right,
Bottom = bottom,
},
};
Shell32.SHAppBarMessage(Shell32.ABM.ABM_NEW, ref abd);
Shell32.SHAppBarMessage(Shell32.ABM.ABM_QUERYPOS, ref abd);
Shell32.SHAppBarMessage(Shell32.ABM.ABM_SETPOS, ref abd);
_registered.Add(hwnd);
}
private static void UnregisterAppBar(IntPtr hwnd)
{
if (!_registered.Contains(hwnd))
return;
Shell32.APPBARDATA abd = new()
{
cbSize = (uint)Marshal.SizeOf<Shell32.APPBARDATA>(),
hWnd = hwnd
};
Shell32.SHAppBarMessage(Shell32.ABM.ABM_REMOVE, ref abd);
_registered.Remove(hwnd);
}
public static void UpdateAppBarHeight(IntPtr hwnd, string monitorDeviceName, int newHeight, DockPlacement dockPlacement)
{
App.DispatcherQueueTimer?.Debounce(() =>
{
if (!_registered.Contains(hwnd))
return;
var uEdge = dockPlacement == DockPlacement.Top ? Shell32.ABE.ABE_TOP : Shell32.ABE.ABE_BOTTOM;
var monitorInfo = MonitorHelper.GetMonitorInfoExFromDeviceName(monitorDeviceName);
int screenWidth = monitorInfo.rcMonitor.Width;
int top = dockPlacement == DockPlacement.Top ? monitorInfo.rcMonitor.Top : monitorInfo.rcMonitor.Bottom - newHeight;
int bottom = dockPlacement == DockPlacement.Top ? monitorInfo.rcMonitor.Top + newHeight : monitorInfo.rcMonitor.Bottom;
Shell32.APPBARDATA abd = new()
{
cbSize = (uint)Marshal.SizeOf<Shell32.APPBARDATA>(),
hWnd = hwnd,
uEdge = uEdge,
rc = new RECT
{
Left = monitorInfo.rcMonitor.Left,
Top = top,
Right = monitorInfo.rcMonitor.Right,
Bottom = bottom,
},
};
Shell32.SHAppBarMessage(Shell32.ABM.ABM_QUERYPOS, ref abd);
Shell32.SHAppBarMessage(Shell32.ABM.ABM_SETPOS, ref abd);
// 同步窗口实际高度和位置
int y = dockPlacement == DockPlacement.Top ? monitorInfo.rcMonitor.Top : monitorInfo.rcMonitor.Bottom - newHeight;
int repeatCount = 2;
while (repeatCount > 0)
{
repeatCount--;
User32.SetWindowPos(
hwnd,
IntPtr.Zero,
monitorInfo.rcMonitor.Left,
y,
screenWidth,
newHeight,
newHeight == 0 ? User32.SetWindowPosFlags.SWP_HIDEWINDOW : User32.SetWindowPosFlags.SWP_SHOWWINDOW
);
}
}, TimeSpan.FromMilliseconds(100));
}
}
}

View File

@@ -1,6 +1,4 @@
// 2025/6/23 by Zhe Fang
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -10,103 +8,43 @@ namespace BetterLyrics.WinUI3.Helper
{
public class EasingHelper
{
public static float EaseInOutSine(float t)
{
return -(MathF.Cos(MathF.PI * t) - 1f) / 2f;
}
/// <summary>
/// No easing
/// </summary>
public static float Linear(float t) => t;
/// <summary>
/// Accelerating from 0
/// </summary>
public static float EaseInQuad(float t) => t * t;
/// <summary>
/// Decelerating to 0
/// </summary>
public static float EaseOutQuad(float t) => t * (2 - t);
/// <summary>
/// Acceleration until halfway then deceleration
/// </summary>
public static float EaseInOutQuad(float t)
{
return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
public static float EaseInOutCubic(float t)
{
return t < 0.5f ? 4 * t * t * t : 1 - MathF.Pow(-2 * t + 2, 3) / 2;
}
public static float EaseInOutQuart(float t)
{
return t < 0.5f ? 8 * t * t * t * t : 1 - MathF.Pow(-2 * t + 2, 4) / 2;
}
public static float EaseInOutQuint(float t)
{
return t < 0.5f ? 16 * t * t * t * t * t : 1 - MathF.Pow(-2 * t + 2, 5) / 2;
}
public static float EaseInOutExpo(float t)
{
return t == 0
? 0
: t == 1
? 1
: t < 0.5 ? MathF.Pow(2, 20 * t - 10) / 2
: (2 - MathF.Pow(2, -20 * t + 10)) / 2;
}
public static float EaseInOutCirc(float t)
{
return t < 0.5f
? (1 - MathF.Sqrt(1 - MathF.Pow(2 * t, 2))) / 2
: (MathF.Sqrt(1 - MathF.Pow(-2 * t + 2, 2)) + 1) / 2;
}
public static float EaseInOutBack(float t)
{
float c1 = 1.70158f;
float c2 = c1 * 1.525f;
return t < 0.5
? (MathF.Pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
: (MathF.Pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
}
public static float EaseInOutElastic(float t)
{
if (t == 0 || t == 1) return t;
float p = 0.3f;
float s = p / 4;
return t < 0.5f
? -(MathF.Pow(2, 20 * t - 10) * MathF.Sin((20 * t - 11.125f) * (2 * MathF.PI) / p)) / 2
: (MathF.Pow(2, -20 * t + 10) * MathF.Sin((20 * t - 11.125f) * (2 * MathF.PI) / p)) / 2 + 1;
}
private static float EaseOutBounce(float t)
{
if (t < 4 / 11f)
{
return (121 * t * t) / 16f;
}
else if (t < 8 / 11f)
{
return (363 / 40f * t * t) - (99 / 10f * t) + 17 / 5f;
}
else if (t < 9 / 10f)
{
return (4356 / 361f * t * t) - (35442 / 1805f * t) + 16061 / 1805f;
}
else
{
return (54 / 5f * t * t) - (513 / 25f * t) + 268 / 25f;
}
}
public static float EaseInOutBounce(float t)
{
if (t < 0.5f)
{
return (1 - EaseOutBounce(1 - 2 * t)) / 2;
}
else
{
return (1 + EaseOutBounce(2 * t - 1)) / 2;
}
}
/// <summary>
/// Smoother transition than linear
/// </summary>
public static float SmoothStep(float t)
{
return t * t * (3f - 2f * t);
return t * t * (3 - 2 * t);
}
public static float Linear(float t) => t;
/// <summary>
/// Even smoother transition with continuous first and second derivatives
/// </summary>
public static float SmootherStep(float t)
{
return t * t * t * (t * (6 * t - 15) + 10);
}
}
}

View File

@@ -1,82 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using System;
using System.IO;
using System.Text;
using Ude;
namespace BetterLyrics.WinUI3.Helper
{
public class FileHelper
{
public static Encoding GetEncoding(string filename)
{
var bytes = File.ReadAllBytes(filename);
var cdet = new CharsetDetector();
cdet.Feed(bytes, 0, bytes.Length);
cdet.DataEnd();
var encoding = cdet.Charset;
if (encoding == null)
{
return Encoding.UTF8;
}
return Encoding.GetEncoding(encoding);
}
public static string SanitizeFileName(string fileName, char replacement = '_')
{
var invalidChars = Path.GetInvalidFileNameChars();
var sb = new StringBuilder(fileName.Length);
foreach (var c in fileName)
{
sb.Append(Array.IndexOf(invalidChars, c) >= 0 ? replacement : c);
}
return sb.ToString();
}
public static string? ReadLyricsCache(string title, string artist, LyricsFormat format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {title}{format.ToFileExtension()}"));
if (File.Exists(cacheFilePath))
{
return File.ReadAllText(cacheFilePath);
}
return null;
}
public static byte[]? ReadAlbumArtCache(string album, string artist, string format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}"));
if (File.Exists(cacheFilePath))
{
return File.ReadAllBytes(cacheFilePath);
}
return null;
}
public static void WriteLyricsCache(string title, string artist, string lyrics, LyricsFormat format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {title}{format.ToFileExtension()}"));
File.WriteAllText(cacheFilePath, lyrics);
}
public static void WriteAlbumArtCache(string album, string artist, byte[] img, string format, string cacheFolderPath)
{
var cacheFilePath = Path.Combine(cacheFolderPath, SanitizeFileName($"{artist} - {album}{format}"));
File.WriteAllBytes(cacheFilePath, img);
}
public static bool IsSwitchableNormalizedMatch(string fileName, string q1, string q2)
{
var normFileName = StringHelper.Normalize(fileName.Normalize());
var normQ1 = StringHelper.Normalize(q1);
var normQ2 = StringHelper.Normalize(q2);
// 常见两种顺序
return normFileName == normQ1 + normQ2
|| normFileName == normQ2 + normQ1;
}
}
}

View File

@@ -1,20 +0,0 @@
using BetterLyrics.WinUI3.Services;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Graphics.Canvas.Text;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public static class FontHelper
{
private static readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
public static string[] SystemFontFamilies => CanvasTextFormat.GetSystemFontFamilies();
public static string GetUserPreferredFontFamily() => SystemFontFamilies.ElementAtOrDefault(_settingsService.SelectedFontFamilyIndex) ?? "Segoe UI";
}
}

View File

@@ -1,111 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Vanara.PInvoke;
using Windows.System;
namespace BetterLyrics.WinUI3.Helper
{
public class ForegroundWindowWatcher
{
private readonly User32.WinEventProc _winEventDelegate;
private readonly List<User32.HWINEVENTHOOK> _hooks = new();
private HWND _currentForeground = HWND.NULL;
private readonly IntPtr _selfHwnd;
private readonly ThrottleHelper _winEventProcThrottle = new(TimeSpan.FromSeconds(1));
public delegate void WindowChangedHandler(HWND hwnd);
private readonly WindowChangedHandler _onWindowChanged;
private readonly DispatcherTimer _timer;
public ForegroundWindowWatcher(IntPtr selfHwnd, WindowChangedHandler onWindowChanged)
{
_selfHwnd = selfHwnd;
_onWindowChanged = onWindowChanged;
_winEventDelegate = new User32.WinEventProc(WinEventProc);
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += Timer_Tick;
}
public void Start()
{
// Hook: foreground changes and minimize end
_hooks.Add(
User32.SetWinEventHook(
User32.EventConstants.EVENT_SYSTEM_FOREGROUND,
User32.EventConstants.EVENT_SYSTEM_MINIMIZEEND,
HINSTANCE.NULL,
_winEventDelegate,
0,
0,
User32.WINEVENT.WINEVENT_OUTOFCONTEXT
)
);
// Hook: window move/resize (location change)
_hooks.Add(
User32.SetWinEventHook(
User32.EventConstants.EVENT_OBJECT_LOCATIONCHANGE,
User32.EventConstants.EVENT_OBJECT_LOCATIONCHANGE,
HINSTANCE.NULL,
_winEventDelegate,
0,
0,
User32.WINEVENT.WINEVENT_OUTOFCONTEXT
)
);
_timer.Start();
}
public void Stop()
{
foreach (var hook in _hooks)
User32.UnhookWinEvent(hook);
_hooks.Clear();
_timer.Stop();
}
private void Timer_Tick(object? sender, object e)
{
if (_currentForeground != HWND.NULL)
{
_onWindowChanged?.Invoke(_currentForeground);
}
}
private void WinEventProc(
User32.HWINEVENTHOOK hWinEventHook,
uint eventType,
HWND hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime
)
{
if (hwnd == IntPtr.Zero)
return;
if (eventType == User32.EventConstants.EVENT_SYSTEM_FOREGROUND)
{
_currentForeground = hwnd;
_onWindowChanged?.Invoke(hwnd);
}
else if ((eventType == User32.EventConstants.EVENT_OBJECT_LOCATIONCHANGE || eventType == User32.EventConstants.EVENT_SYSTEM_MINIMIZEEND) && hwnd == _currentForeground)
{
_onWindowChanged?.Invoke(hwnd);
}
}
}
}

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
namespace BetterLyrics.WinUI3.Helper
{
public class ForegroundWindowWatcherHelper
{
private readonly WinEventDelegate _winEventDelegate;
private readonly List<IntPtr> _hooks = new();
private IntPtr _currentForeground = IntPtr.Zero;
private readonly IntPtr _selfHwnd;
private readonly DispatcherTimer _pollingTimer;
private DateTime _lastEventTime = DateTime.MinValue;
private const int ThrottleIntervalMs = 100;
public delegate void WindowChangedHandler(IntPtr hwnd);
private readonly WindowChangedHandler _onWindowChanged;
private const uint EVENT_SYSTEM_FOREGROUND = 0x0003;
private const uint EVENT_SYSTEM_MINIMIZEEND = 0x0017;
private const uint EVENT_OBJECT_LOCATIONCHANGE = 0x800B;
private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
public ForegroundWindowWatcherHelper(IntPtr selfHwnd, WindowChangedHandler onWindowChanged)
{
_selfHwnd = selfHwnd;
_onWindowChanged = onWindowChanged;
_winEventDelegate = new WinEventDelegate(WinEventProc);
_pollingTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
_pollingTimer.Tick += (_, _) =>
{
if (_currentForeground != IntPtr.Zero && _currentForeground != _selfHwnd)
_onWindowChanged?.Invoke(_currentForeground);
};
}
public void Start()
{
// Hook: foreground changes and minimize end
_hooks.Add(
SetWinEventHook(
EVENT_SYSTEM_FOREGROUND,
EVENT_SYSTEM_MINIMIZEEND,
IntPtr.Zero,
_winEventDelegate,
0,
0,
WINEVENT_OUTOFCONTEXT
)
);
// Hook: window move/resize (location change)
_hooks.Add(
SetWinEventHook(
EVENT_OBJECT_LOCATIONCHANGE,
EVENT_OBJECT_LOCATIONCHANGE,
IntPtr.Zero,
_winEventDelegate,
0,
0,
WINEVENT_OUTOFCONTEXT
)
);
_pollingTimer.Start();
}
public void Stop()
{
foreach (var hook in _hooks)
UnhookWinEvent(hook);
_hooks.Clear();
_pollingTimer.Stop();
}
private void WinEventProc(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime
)
{
if (hwnd == IntPtr.Zero || hwnd == _selfHwnd)
return;
var now = DateTime.Now;
if ((now - _lastEventTime).TotalMilliseconds < ThrottleIntervalMs)
return;
_lastEventTime = now;
if (eventType == EVENT_SYSTEM_FOREGROUND)
{
_currentForeground = hwnd;
_onWindowChanged?.Invoke(hwnd);
}
else if (
(eventType == EVENT_OBJECT_LOCATIONCHANGE || eventType == EVENT_SYSTEM_MINIMIZEEND)
&& hwnd == _currentForeground
)
{
_onWindowChanged?.Invoke(hwnd);
}
}
#region WinAPI
private delegate void WinEventDelegate(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime
);
[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(
uint eventMin,
uint eventMax,
IntPtr hmodWinEventProc,
WinEventDelegate lpfnWinEventProc,
uint idProcess,
uint idThread,
uint dwFlags
);
[DllImport("user32.dll")]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
#endregion
}
}

View File

@@ -1,46 +0,0 @@
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Vanara.PInvoke;
using Windows.System;
using WinRT.Interop;
namespace BetterLyrics.WinUI3.Helper
{
public class GlobalHotKeyHelper
{
private static Dictionary<int, Action> _hotKeyActions = [];
private static int _nextId = 0;
public static void RegisterHotKey(Window window, User32.HotKeyModifiers modifiers, uint key, Action action)
{
HWND hwnd = WindowNative.GetWindowHandle(window);
int id = _nextId++;
User32.RegisterHotKey(hwnd, id, modifiers, key);
_hotKeyActions[id] = action;
}
public static void UnregisterAllHotKeys(Window window)
{
HWND hwnd = WindowNative.GetWindowHandle(window);
foreach (var id in _hotKeyActions.Keys.ToList())
{
User32.UnregisterHotKey(hwnd, id);
_hotKeyActions.Remove(id);
}
}
public static bool TryInvokeAction(int id)
{
if (_hotKeyActions.TryGetValue(id, out var action))
{
action?.Invoke();
return true;
}
return false;
}
}
}

View File

@@ -1,22 +1,13 @@
// 2025/6/23 by Zhe Fang
using CommunityToolkit.WinUI.Helpers;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media.Imaging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI;
@@ -24,181 +15,61 @@ namespace BetterLyrics.WinUI3.Helper
{
public class ImageHelper
{
private static readonly ColorThief _colorThief = new();
public const int AccentColorCount = 3;
public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(
byte[] imageBytes
)
{
if (imageBytes == null || imageBytes.Length == 0)
return null;
InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(imageBytes.AsBuffer());
return stream;
}
public static async Task<BitmapImage> GetBitmapImageFromBytesAsync(byte[] imageBytes)
{
var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(imageBytes.AsBuffer());
stream.Seek(0);
var bitmapImage = new BitmapImage();
await bitmapImage.SetSourceAsync(stream);
return bitmapImage;
}
public static async Task<InMemoryRandomAccessStream> ByteArrayToStream(byte[] bytes)
{
using var stream = new InMemoryRandomAccessStream();
var stream = new InMemoryRandomAccessStream();
await stream.WriteAsync(bytes.AsBuffer());
stream.Seek(0);
return stream;
}
public static RandomAccessStreamReference ByteArrayToRandomAccessStreamReference(byte[] bytes)
{
using var stream = new InMemoryRandomAccessStream();
using var writer = new DataWriter(stream);
writer.WriteBytes(bytes);
writer.StoreAsync().GetAwaiter().GetResult();
writer.FlushAsync().GetAwaiter().GetResult();
writer.DetachStream();
return RandomAccessStreamReference.CreateFromStream(stream);
}
public static async Task<byte[]> CreateTextPlaceholderBytesAsync(int width, int height)
{
using var device = CanvasDevice.GetSharedDevice();
using var renderTarget = new CanvasRenderTarget(device, width, height, 96);
// 随机生成渐变色
Windows.UI.Color RandomColor()
{
var rand = new Random(Guid.NewGuid().GetHashCode());
double h = rand.NextDouble() * 360;
double s = 0.35 + rand.NextDouble() * 0.3; // 0.35~0.65,适中饱和度
double l = 0.5 + rand.NextDouble() * 0.3; // 0.5~0.8,明亮
return CommunityToolkit.WinUI.Helpers.ColorHelper.FromHsl(h, s, l);
}
Windows.UI.Color color1 = RandomColor();
Windows.UI.Color color2 = RandomColor();
using (var ds = renderTarget.CreateDrawingSession())
{
// 绘制线性渐变背景
using var gradientBrush = new Microsoft.Graphics.Canvas.Brushes.CanvasLinearGradientBrush(ds, color1, color2)
{
StartPoint = new Vector2(0, 0),
EndPoint = new Vector2(width, height)
};
ds.FillRectangle(0, 0, width, height, gradientBrush);
}
// 保存为 PNG 并转为 byte[]
using var stream = new InMemoryRandomAccessStream();
await renderTarget.SaveAsync(stream, CanvasBitmapFileFormat.Png);
var buffer = new byte[stream.Size];
using (var reader = new DataReader(stream.GetInputStreamAt(0)))
{
await reader.LoadAsync((uint)stream.Size);
reader.ReadBytes(buffer);
}
return buffer;
}
public static List<Windows.UI.Color> GetAccentColorsFromByte(byte[] bytes, int count, bool? isDark = null)
{
using var image = Image.Load<Rgba32>(bytes);
var colorThief = new ColorThief.ImageSharp.ColorThief();
var mainColor = colorThief.GetColor(image, 10, false);
var palette = colorThief.GetPalette(image, 255, 10, false);
var topColors = palette
.OrderByDescending(x => x.Population)
.Where(x => x.IsDark == (isDark ?? mainColor.IsDark))
.Select(x => Windows.UI.Color.FromArgb(x.Color.A, x.Color.R, x.Color.G, x.Color.B))
.Take(count)
.ToList();
return topColors;
}
//public static async Task<BitmapImage> GetBitmapImageFromBytesAsync(byte[] imageBytes)
//{
// var stream = new InMemoryRandomAccessStream();
// await stream.WriteAsync(imageBytes.AsBuffer());
// stream.Seek(0);
// var bitmapImage = new BitmapImage();
// await bitmapImage.SetSourceAsync(stream);
// return bitmapImage;
//}
//public static async Task<BitmapDecoder> GetDecoderFromByte(byte[] bytes) =>
// await BitmapDecoder.CreateAsync(await ByteArrayToStream(bytes));
//public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(byte[] imageBytes)
//{
// if (imageBytes == null || imageBytes.Length == 0)
// return null;
// InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
// await stream.WriteAsync(imageBytes.AsBuffer());
// return stream;
//}
public static async Task<byte[]> ToByteArrayAsync(IRandomAccessStreamReference streamRef)
{
using IRandomAccessStream stream = await streamRef.OpenReadAsync();
using var reader = new DataReader(stream);
await reader.LoadAsync((uint)stream.Size);
byte[] buffer = new byte[stream.Size];
reader.ReadBytes(buffer);
return buffer;
using var memoryStream = new MemoryStream();
await stream.AsStreamForRead().CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
public static float GetAverageLuminance(CanvasBitmap bitmap)
{
var pixels = bitmap.GetPixelBytes();
double sum = 0;
for (int i = 0; i < pixels.Length; i += 4)
{
// BGRA
byte b = pixels[i];
byte g = pixels[i + 1];
byte r = pixels[i + 2];
// 忽略A
double y = 0.299 * r + 0.587 * g + 0.114 * b;
sum += y / 255.0;
}
return (float)(sum / (pixels.Length / 4));
}
public static async Task<List<Color>> GetAccentColorsFromByte(byte[] bytes) =>
[
.. (
await _colorThief.GetPalette(await GetDecoderFromByte(bytes), AccentColorCount)
).Select(color =>
Color.FromArgb(color.Color.A, color.Color.R, color.Color.G, color.Color.B)
),
];
public static byte[] MakeSquareWithThemeColor(byte[] imageBytes)
{
using var image = Image.Load<Rgba32>(imageBytes);
if (image.Width == image.Height)
{
// 已经是正方形,直接返回
return imageBytes;
}
int size = Math.Max(image.Width, image.Height);
var themeColor = Rgba32.ParseHex(GetAccentColorsFromByte(imageBytes, 1).FirstOrDefault().ToHex());
// 新建正方形画布
using var square = new Image<Rgba32>(size, size, themeColor);
// 计算居中位置
int offsetX = (size - image.Width) / 2;
int offsetY = (size - image.Height) / 2;
// 绘制原图到正方形画布
square.Mutate(ctx => ctx.DrawImage(image, new Point(offsetX, offsetY), 1f));
// 保存为 PNG 字节流
using var ms = new MemoryStream();
square.Save(ms, new PngEncoder());
return ms.ToArray();
}
public static byte[] Resize(byte[] imageBytes, int size)
{
using Image image = Image.Load(imageBytes);
var factor = Math.Max(size / image.Width, size / image.Height);
if (factor <= 1)
{
return imageBytes;
}
int width = image.Width * factor;
int height = image.Height * factor;
image.Mutate(x => x.Resize(width, height, KnownResamplers.Welch));
using var ms = new MemoryStream();
image.Save(ms, new PngEncoder());
return ms.ToArray();
}
public static async Task<BitmapDecoder> GetDecoderFromByte(byte[] bytes) =>
await BitmapDecoder.CreateAsync(await ByteArrayToStream(bytes));
}
}

View File

@@ -1,128 +0,0 @@
using BetterLyrics.WinUI3.Helper;
using CommunityToolkit.Mvvm.DependencyInjection;
using Lyricify.Lyrics.Helpers.General;
using NTextCat;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using TinyPinyin;
using Windows.Globalization;
namespace BetterLyrics.WinUI3.Services
{
public class LanguageHelper
{
private static readonly RankedLanguageIdentifierFactory _factory = new();
private static readonly RankedLanguageIdentifier _identifier;
private static readonly ISettingsService _settingsService = Ioc.Default.GetRequiredService<ISettingsService>();
public static List<Models.LanguageInfo> SupportedTargetLanguages =>
[
new Models.LanguageInfo("ar", "العربية"),
new Models.LanguageInfo("az", "Azərbaycan dili"),
new Models.LanguageInfo("zh-Hans", "简体中文"),
new Models.LanguageInfo("zh-Hant", "繁體中文"),
new Models.LanguageInfo("cs", "Čeština"),
new Models.LanguageInfo("da", "Dansk"),
new Models.LanguageInfo("nl", "Nederlands"),
new Models.LanguageInfo("en", "English"),
new Models.LanguageInfo("eo", "Esperanto"),
new Models.LanguageInfo("fi", "Suomi"),
new Models.LanguageInfo("fr", "Français"),
new Models.LanguageInfo("de", "Deutsch"),
new Models.LanguageInfo("el", "Ελληνικά"),
new Models.LanguageInfo("he", "עברית"),
new Models.LanguageInfo("hi", "हिन्दी"),
new Models.LanguageInfo("hu", "Magyar"),
new Models.LanguageInfo("id", "Bahasa Indonesia"),
new Models.LanguageInfo("ga", "Gaeilge"),
new Models.LanguageInfo("it", "Italiano"),
new Models.LanguageInfo("ja", "日本語"),
new Models.LanguageInfo("ko", "한국어"),
new Models.LanguageInfo("fa", "فارسی"),
new Models.LanguageInfo("pl", "Polski"),
new Models.LanguageInfo("pt", "Português"),
new Models.LanguageInfo("ru", "Русский"),
new Models.LanguageInfo("sk", "Slovenčina"),
new Models.LanguageInfo("es", "Español"),
new Models.LanguageInfo("sv", "Svenska"),
new Models.LanguageInfo("tr", "Türkçe"),
new Models.LanguageInfo("uk", "Українська"),
new Models.LanguageInfo("vi", "Tiếng Việt"),
];
static LanguageHelper()
{
_identifier = _factory.Load(PathHelper.LanguageProfilePath);
}
public static string? DetectLanguageCode(string? text)
{
if (text == null) return null;
var guessList = _identifier.Identify(text);
string? code = guessList?.FirstOrDefault()?.Item1.Iso639_2T;
code = code switch
{
"simple" => "en",
"zh_classical" => "zh-Hant",
"zh_yue" => "zh-Hant",
"zh" => "zh-Hans",
_ => code
};
return code;
}
public static bool IsCJK(string text)
{
return DetectLanguageCode(text)?.Substring(0, 2) switch
{
"zh" or "ja" or "ko" => true,
_ => false
};
}
public static string ConvertToCountryCode(string? languageCode)
{
if (languageCode == null) return "us";
return languageCode switch
{
"zh" => "cn",
"zh-Hans" => "cn",
"zh-Hant" => "tw",
"ja" => "jp",
"ko" => "kr",
_ => "us"
};
}
public static string GetUserTargetLanguageCode()
{
return SupportedTargetLanguages[_settingsService.SelectedTargetLanguageIndex].Code;
}
public static int GetDefaultTargetLanguageIndex()
{
int found = SupportedTargetLanguages.FindIndex(x => ApplicationLanguages.Languages.FirstOrDefault()?.Contains(x.Code) == true);
if (found == -1) found = 7; // 默认使用英语
return found;
}
public static string GetOrderChar(string text)
{
if (string.IsNullOrWhiteSpace(text)) return "#";
char c = text.ElementAtOrDefault(0);
if (char.IsLetter(c) && c < 128)
return char.ToUpper(c).ToString();
if (PinyinHelper.IsChinese(c))
{
return PinyinHelper.GetPinyinInitials($"{c}");
}
return "#";
}
}
}

View File

@@ -1,43 +0,0 @@
using Nito.AsyncEx;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public class LatestOnlyTaskRunner
{
private readonly AsyncLock _mutex = new();
private CancellationTokenSource _cts;
public async Task RunAsync(Func<CancellationToken, Task> action)
{
CancellationTokenSource oldCts;
// 使用 AsyncLock 保证线程安全
using (await _mutex.LockAsync())
{
// 取消旧的
oldCts = _cts;
_cts = new CancellationTokenSource();
}
oldCts?.Cancel();
oldCts?.Dispose();
CancellationToken token = _cts.Token;
try
{
await action(token);
}
catch (OperationCanceledException)
{
// 可以选择忽略取消异常
}
}
}
}

View File

@@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.System;
namespace BetterLyrics.WinUI3.Helper
{
public class LauncherHelper
{
public static async Task SelectAndShowFile(string filePath)
{
var file = await StorageFile.GetFileFromPathAsync(filePath);
var folder = await file.GetParentAsync();
var folderOptions = new FolderLauncherOptions();
folderOptions.ItemsToSelect.Add(file);
await Launcher.LaunchFolderAsync(folder, folderOptions);
}
}
}

View File

@@ -1,390 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using Lyricify.Lyrics.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Windows.Globalization.Fonts;
using LyricsData = BetterLyrics.WinUI3.Models.LyricsData;
namespace BetterLyrics.WinUI3.Helper
{
public class LyricsParser
{
private List<LyricsData> _lyricsDataArr = [];
public List<LyricsData> Parse(string? raw, int? durationMs)
{
durationMs ??= (int)TimeSpan.FromMinutes(99).TotalMilliseconds;
_lyricsDataArr = [];
if (raw == null)
{
_lyricsDataArr.Add(LyricsData.GetNotfoundPlaceholder(durationMs.Value));
}
else
{
switch (raw.DetectFormat())
{
case LyricsFormat.Lrc:
case LyricsFormat.Eslrc:
ParseLrc(raw);
break;
case LyricsFormat.Qrc:
ParseQQNeteaseKugou(Lyricify.Lyrics.Parsers.QrcParser.Parse(raw).Lines);
break;
case LyricsFormat.Krc:
ParseQQNeteaseKugou(Lyricify.Lyrics.Parsers.KrcParser.Parse(raw).Lines);
break;
case LyricsFormat.Ttml:
ParseTtml(raw);
break;
default:
break;
}
}
_lyricsDataArr.Add(new LyricsData()); // 为机翻预留
return _lyricsDataArr;
}
private void ParseLrc(string raw)
{
var lines = raw.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries);
var lrcLines =
new List<(int time, string text, List<(int time, string text)> syllables)>();
// 支持 [mm:ss.xx]字、<mm:ss.xx>字,毫秒两位或三位
var syllableRegex = new Regex(
@"(\[|\<)(\d{2}):(\d{2})\.(\d{2,3})(\]|\>)([^\[\]\<\>]*)"
);
foreach (var line in lines)
{
var matches = syllableRegex.Matches(line);
var syllables = new List<(int, string)>();
for (int i = 0; i < matches.Count; i++)
{
var m = matches[i];
int min = int.Parse(m.Groups[2].Value);
int sec = int.Parse(m.Groups[3].Value);
int ms = int.Parse(m.Groups[4].Value.PadRight(3, '0'));
int totalMs = min * 60_000 + sec * 1000 + ms;
string text = m.Groups[6].Value;
syllables.Add((totalMs, text));
}
if (syllables.Count > 0)
{
lrcLines.Add(
(
syllables[0].Item1,
string.Concat(syllables.Select(s => s.Item2)),
syllables
)
);
}
else
{
// 普通LRC行
var bracketRegex = new Regex(@"\[(\d{2}):(\d{2})\.(\d{2,3})\]");
var bracketMatches = bracketRegex.Matches(line);
string content = line;
int? lineStartTime = null;
if (bracketMatches.Count > 0)
{
var m = bracketMatches[0];
int min = int.Parse(m.Groups[1].Value);
int sec = int.Parse(m.Groups[2].Value);
int ms = int.Parse(m.Groups[3].Value.PadRight(3, '0'));
lineStartTime = min * 60_000 + sec * 1000 + ms;
content = bracketRegex.Replace(line, "");
lrcLines.Add((lineStartTime.Value, content, new List<(int, string)>()));
}
}
}
// 按时间分组
var grouped = lrcLines.GroupBy(l => l.time).OrderBy(g => g.Key).ToList();
int languageCount = 0;
if (grouped != null && grouped.Count > 0)
{
// 计算最大语言数量
languageCount = grouped.Max(g => g.Count());
}
// 初始化每种语言的歌词列表
_lyricsDataArr.Clear();
for (int i = 0; i < languageCount; i++) _lyricsDataArr.Add(new LyricsData());
// 遍历每个时间分组
if (grouped != null)
{
foreach (var group in grouped)
{
var linesInGroup = group.ToList();
for (int langIdx = 0; langIdx < languageCount; langIdx++)
{
// 只添加有对应行的语言,否则跳过
if (langIdx < linesInGroup.Count)
{
var (start, text, syllables) = linesInGroup[langIdx];
var line = new LyricsLine
{
StartMs = start,
OriginalText = text,
LyricsChars = [],
};
if (syllables != null && syllables.Count > 0)
{
int currentIndex = 0;
for (int j = 0; j < syllables.Count; j++)
{
var (charStart, charText) = syllables[j];
int startIndex = currentIndex;
line.LyricsChars.Add(
new LyricsChar
{
StartMs = charStart,
Text = charText ?? "",
StartIndex = startIndex,
}
);
currentIndex += charText?.Length ?? 0;
}
}
_lyricsDataArr[langIdx].LyricsLines.Add(line);
}
// 没有翻译行则不补原文,直接跳过
}
}
}
}
private void ParseTtml(string raw)
{
try
{
List<LyricsLine> originalLines = [];
List<LyricsLine> translationLines = [];
var xdoc = XDocument.Parse(raw, LoadOptions.PreserveWhitespace);
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
if (body == null) return;
var ps = body.Descendants().Where(e => e.Name.LocalName == "p");
foreach (var p in ps)
{
// 句级时间
string? pBegin = p.Attribute("begin")?.Value;
string? pEnd = p.Attribute("end")?.Value;
int pStartMs = ParseTtmlTime(pBegin);
int pEndMs = ParseTtmlTime(pEnd);
// 只获取一级span且排除ttm:role="x-bg"的span
var spans = p.Elements()
.Where(s => s.Name.LocalName == "span" &&
s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-bg")
.ToList();
// 原文和翻译分离
var originalTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-translation")
.ToList();
var translationTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == "x-translation")
.ToList();
// 处理原文span后的空白
for (int i = 0; i < originalTextSpans.Count; i++)
{
var span = originalTextSpans[i];
var nextNode = span.NodesAfterSelf().FirstOrDefault();
if (nextNode is XText textNode)
{
span.Value += textNode.Value;
}
}
// 拼接空白字符后的原文
string originalText = string.Concat(originalTextSpans.Select(s => s.Value));
var originalCharTimings = new List<LyricsChar>();
int originalStartIndex = 0;
foreach (var span in originalTextSpans)
{
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
originalCharTimings.Add(new LyricsChar
{
StartMs = sStartMs,
EndMs = sEndMs,
StartIndex = originalStartIndex,
Text = span.Value
});
originalStartIndex += span.Value.Length;
}
if (originalTextSpans.Count == 0)
originalText = p.Value;
originalLines.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
OriginalText = originalText,
LyricsChars = originalCharTimings,
});
// 翻译
string translationText = string.Concat(translationTextSpans.Select(s => s.Value));
var translationCharTimings = new List<LyricsChar>();
int translationStartIndex = 0;
foreach (var span in translationTextSpans)
{
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
translationCharTimings.Add(new LyricsChar
{
StartMs = sStartMs,
EndMs = sEndMs,
StartIndex = translationStartIndex,
Text = span.Value
});
translationStartIndex += span.Value.Length;
}
if (translationTextSpans.Count > 0)
{
translationLines.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = pEndMs,
OriginalText = translationText,
LyricsChars = translationCharTimings,
});
}
}
_lyricsDataArr.Add(new LyricsData(originalLines));
if (translationLines.Count > 0)
_lyricsDataArr.Add(new LyricsData(translationLines));
}
catch
{
// 解析失败,忽略
}
}
private static int ParseTtmlTime(string? t)
{
if (string.IsNullOrWhiteSpace(t))
return 0;
t = t.Trim();
// 支持 "1.000s"
if (t.EndsWith("s"))
{
if (
double.TryParse(
t.TrimEnd('s'),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double seconds
)
)
return (int)(seconds * 1000);
}
else
{
var parts = t.Split(':');
if (parts.Length == 3)
{
// hh:mm:ss.xxx
int h = int.Parse(parts[0]);
int m = int.Parse(parts[1]);
double s = double.Parse(
parts[2],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((h * 3600 + m * 60 + s) * 1000);
}
else if (parts.Length == 2)
{
// mm:ss.xxx
int m = int.Parse(parts[0]);
double s = double.Parse(
parts[1],
System.Globalization.CultureInfo.InvariantCulture
);
return (int)((m * 60 + s) * 1000);
}
else if (parts.Length == 1)
{
// ss.xxx
if (
double.TryParse(
parts[0],
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double s
)
)
return (int)(s * 1000);
}
}
return 0;
}
private void ParseQQNeteaseKugou(List<ILineInfo>? lines)
{
lines = lines?.Where(x => x.Text != string.Empty).ToList();
List<LyricsLine> lyricsLines = [];
if (lines != null && lines.Count > 0)
{
lyricsLines = [];
for (int lineIndex = 0; lineIndex < lines.Count; lineIndex++)
{
var lineRead = lines[lineIndex];
var lineWrite = new LyricsLine
{
StartMs = lineRead.StartTime ?? 0,
EndMs = lineRead.EndTime ?? 0,
OriginalText = lineRead.Text,
LyricsChars = [],
};
var syllables = (lineRead as SyllableLineInfo)?.Syllables;
if (syllables != null)
{
int startIndex = 0;
for (
int syllableIndex = 0;
syllableIndex < syllables.Count;
syllableIndex++
)
{
var syllable = syllables[syllableIndex];
var charTiming = new LyricsChar
{
StartMs = syllable.StartTime,
EndMs = syllable.EndTime,
Text = syllable.Text,
StartIndex = startIndex,
};
lineWrite.LyricsChars.Add(charTiming);
startIndex += syllable.Text.Length;
}
}
lyricsLines.Add(lineWrite);
}
}
_lyricsDataArr.Add(new LyricsData(lyricsLines));
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public class MathHelper
{
public static List<int> GetAllFactors(int n)
{
var result = new SortedSet<int>();
for (int i = 1; i <= Math.Sqrt(n); i++)
{
if (n % i == 0)
{
result.Add(i);
result.Add(n / i);
}
}
return [.. result];
}
}
}

View File

@@ -1,48 +0,0 @@
// 2025/6/23 by Zhe Fang
namespace BetterLyrics.WinUI3.Helper
{
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Storage;
using Windows.Storage.FileProperties;
public static class MetadataHelper
{
public const string AppAuthor = "Zhe Fang";
public const string AppDisplayName = "Better Lyrics";
public const string AppName = "BetterLyrics";
public static string AppVersion
{
get
{
var version = Package.Current.Id.Version;
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
}
public const string GithubUrl = "https://github.com/jayfunc/BetterLyrics";
public const string QQGroupUrl = "https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info";
public const string DiscordUrl = "https://discord.gg/5yAQPnyCKv";
public const string TelegramUrl = "https://t.me/+svhSLZ7awPsxNGY1";
public static async Task<DateTime> GetBuildDate()
{
var assembly = Assembly.GetExecutingAssembly();
var filePath = assembly.Location;
if (!File.Exists(filePath))
return DateTime.MinValue;
StorageFile file = await StorageFile.GetFileFromPathAsync(filePath);
// 获取文件基本属性
BasicProperties props = await file.GetBasicPropertiesAsync();
// 返回修改日期
return props.DateModified.DateTime;
}
}
}

View File

@@ -1,64 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
namespace BetterLyrics.WinUI3.Helper
{
public static class MonitorHelper
{
public static IEnumerable<string> GetAllMonitorDeviceNames()
{
var deviceNames = new List<string>();
User32.EnumDisplayMonitors(IntPtr.Zero, null, (hMonitor, hdcMonitor, lprcMonitor, dwData) =>
{
User32.MONITORINFOEX monitorInfoEx = new() { cbSize = (uint)Marshal.SizeOf<User32.MONITORINFOEX>() };
if (User32.GetMonitorInfo(hMonitor, ref monitorInfoEx))
{
deviceNames.Add(monitorInfoEx.szDevice);
}
return true; // 继续枚举
}, IntPtr.Zero);
return deviceNames;
}
public static User32.MONITORINFOEX GetMonitorInfoExFromDeviceName(string deviceName)
{
User32.MONITORINFOEX? result = null;
User32.EnumDisplayMonitors(IntPtr.Zero, null, (hMonitor, hdcMonitor, lprcMonitor, dwData) =>
{
User32.MONITORINFOEX monitorInfoEx = new() { cbSize = (uint)Marshal.SizeOf<User32.MONITORINFOEX>() };
if (User32.GetMonitorInfo(hMonitor, ref monitorInfoEx))
{
if (string.Equals(monitorInfoEx.szDevice, deviceName, StringComparison.OrdinalIgnoreCase))
{
result = monitorInfoEx;
return false; // 找到后停止枚举
}
}
return true; // 继续枚举
}, IntPtr.Zero);
return result ?? GetPrimaryMonitorInfoEx();
}
public static User32.MONITORINFOEX GetPrimaryMonitorInfoEx()
{
// (0,0) 总是在主屏
var ptZero = new POINT(0, 0);
HMONITOR hMonitor = User32.MonitorFromPoint(ptZero, User32.MonitorFlags.MONITOR_DEFAULTTOPRIMARY);
User32.MONITORINFOEX monitorInfoEx = new() { cbSize = (uint)Marshal.SizeOf<User32.MONITORINFOEX>() };
User32.GetMonitorInfo(hMonitor, ref monitorInfoEx);
return monitorInfoEx;
}
public static string GetPrimaryMonitorDeviceName()
{
var primaryMonitorInfo = GetPrimaryMonitorInfoEx();
return primaryMonitorInfo.szDevice;
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
internal class NativeHelper
{
public const int ERROR_SUCCESS = 0;
public const int ERROR_INSUFFICIENT_BUFFER = 122;
public const int APPMODEL_ERROR_NO_PACKAGE = 15700;
[DllImport("api-ms-win-appmodel-runtime-l1-1-1", SetLastError = true)]
[return: MarshalAs(UnmanagedType.U4)]
internal static extern uint GetCurrentPackageId(ref int pBufferLength, out byte pBuffer);
public static bool IsAppPackaged
{
get
{
int bufferSize = 0;
byte byteBuffer = 0;
uint lastError = NativeHelper.GetCurrentPackageId(ref bufferSize, out byteBuffer);
bool isPackaged = true;
if (lastError == NativeHelper.APPMODEL_ERROR_NO_PACKAGE)
{
isPackaged = false;
}
return isPackaged;
}
}
}
}

View File

@@ -1,26 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public class NetHelper
{
public static async Task<bool> CheckConnectivity(string url)
{
try
{
using var client = new System.Net.Http.HttpClient();
// Try to reach a reliable endpoint
var res = await client.GetAsync(url);
return res.IsSuccessStatusCode;
}
catch
{
return false; // If any exception occurs, assume no connectivity
}
}
}
}

View File

@@ -1,331 +0,0 @@
using System;
using System.Buffers;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Windows.Storage;
using static BetterLyrics.WinUI3.Helper.NoiseOverlayHelper.BitmapFileCreator;
namespace BetterLyrics.WinUI3.Helper
{
internal static class NoiseOverlayHelper
{
const string NoiseOverlayFileName = "noise_overlay.bmp";
static readonly string NoiseOverlayFilePath = Path.Combine(ApplicationData.Current.LocalFolder.Path, "Assets", NoiseOverlayFileName);
/// <summary>
/// 生成 BGRA 格式的灰阶噪声像素数据
/// </summary>
public static byte[] GenerateNoiseBitmapBGRA(int width, int height)
{
var random = new Random();
var pixelData = new byte[width * height * 4];
for (int i = 0; i < width * height; i++)
{
byte gray = (byte)random.Next(0, 256);
pixelData[i * 4 + 0] = gray; // B
pixelData[i * 4 + 1] = gray; // G
pixelData[i * 4 + 2] = gray; // R
pixelData[i * 4 + 3] = 255; // A
}
return pixelData;
}
/// <summary>
/// 生成单色灰阶随机噪声
/// </summary>
/// <param name="outputPath">输出文件路径</param>
/// <param name="width">图片宽度</param>
/// <param name="height">图片高度</param>
public static BitmapFile GenerateNoiseBitmap(int width, int height)
{
const uint NumOfGrayscale = 16;
uint bitCount = NextPowerOfTwo((uint)Math.Round(Math.Sqrt(NumOfGrayscale)));
var palette = BitmapFileCreator.CreateGrayscalePalette(16);
var pixelData = GenerateRandomNoise(width, height, bitCount);
var fileHeader = BitmapFileCreator.CreateFileHeader(palette, pixelData);
var infoHeader = BitmapFileCreator.CreateInfoHeader(width, height, bitCount);
return new BitmapFile(fileHeader, infoHeader, palette, pixelData);
}
/// <summary>
/// 读取噪声图片的位头信息
/// </summary>
public static BitmapFileCreator.WINBMPINFOHEADER ReadBitmapInfoHeaders(string? FilePath)
{
var _filePath = FilePath ?? NoiseOverlayFilePath;
using var fs = new FileStream(_filePath, FileMode.Open, FileAccess.Read);
using var br = new BinaryReader(fs);
// 跳过文件头
fs.Seek(Marshal.SizeOf<BitmapFileCreator.BITMAPFILEHEADER>(), SeekOrigin.Begin);
// 读取信息头
byte[] infoHeaderBytes = br.ReadBytes(Marshal.SizeOf<BitmapFileCreator.WINBMPINFOHEADER>());
return MemoryMarshal.Read<BitmapFileCreator.WINBMPINFOHEADER>(infoHeaderBytes);
}
public static BitmapFileCreator.WINBMPINFOHEADER ReadBitmapInfoHeaders(byte[] FileBytes)
{
// 跳过文件头
var offset = Marshal.SizeOf<BitmapFileCreator.BITMAPFILEHEADER>();
// 读取信息头
var infoHeaderLength = Marshal.SizeOf<BitmapFileCreator.WINBMPINFOHEADER>();
Span<byte> infoHeaderBytes = new(FileBytes, offset, infoHeaderLength);
return MemoryMarshal.Read<BitmapFileCreator.WINBMPINFOHEADER>(infoHeaderBytes);
}
/// <summary>
/// safe 的写入 struct 到流
/// </summary>
/// <typeparam name="T">值 strcut</typeparam>
/// <param name="stream">要写入的字节流</param>
/// <param name="structure">要被写入的结构</param>
public static void WriteStruct<T>(Stream stream, in T structure) where T : struct
{
int size = Unsafe.SizeOf<T>();
byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
MemoryMarshal.Write(buffer, in structure);
stream.Write(buffer, 0, size);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
/// <summary>
/// 随机填充位图内容
/// </summary>
/// <param name="width">填充宽度</param>
/// <param name="height">填充高度</param>
/// <param name="bitCount">单个调色盘索引所占比特位数</param>
/// <returns>字节数据</returns>
private static byte[] GenerateRandomNoise(int width, int height, uint bitCount)
{
// 创建位图行字节数4K 对齐
int rowSize = ((width * (int)bitCount + 31) >> 5) << 2;
// 创建随机位图数据
Random rnd = new();
return Enumerable.Range(0, rowSize * height)
.Select(i => (byte)rnd.Next(0x00, 0xFF))
.ToArray();
}
private static uint NextPowerOfTwo(uint value)
{
value--;
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return value + 1;
}
public static class BitmapFileCreator
{
/// <summary>
/// 创建BMP文件头
/// </summary>
/// <param name="palette">调色盘数据</param>
/// <param name="pixelData">像素数据</param>
/// <returns>文件头结构</returns>
public static BITMAPFILEHEADER CreateFileHeader(byte[] palette, byte[] pixelData)
{
return new BITMAPFILEHEADER
{
bfType = 0x4D42,
bfSize = (uint)(Marshal.SizeOf<BITMAPFILEHEADER>() +
Marshal.SizeOf<WINBMPINFOHEADER>() +
palette.Length +
pixelData.Length),
bfReserved1 = 0,
bfReserved2 = 0,
bfOffBits = (uint)(Marshal.SizeOf<BITMAPFILEHEADER>() +
Marshal.SizeOf<WINBMPINFOHEADER>() +
palette.Length)
};
}
/// <summary>
/// 将指定值填充到为最接近的2的幂数
/// </summary>
/// <summary>
/// 生成灰阶调色盘
/// </summary>
/// <param name="colors">灰阶数量</param>
/// <returns>调色盘byte数组</returns>
public static byte[] CreateGrayscalePalette(int colors)
{
return Enumerable.Range(0, colors)
.SelectMany(i => Enumerable.Repeat((byte)(i * 0x10), 4))
.ToArray();
}
/// <summary>
/// 创建BMP信息头
/// </summary>
/// <param name="width">宽度</param>
/// <param name="height">高度</param>
/// <param name="bitCount">单个像素(调色盘索引)位数</param>
/// <returns>BMP信息头结构</returns>
public static WINBMPINFOHEADER CreateInfoHeader(int width, int height, uint bitCount)
{
return new WINBMPINFOHEADER
{
biSize = (uint)Marshal.SizeOf<WINBMPINFOHEADER>(),
biWidth = (uint)width,
biHeight = (uint)height,
biPlanes = 1,
biBitCount = (ushort)bitCount,
biCompression = 0,
biSizeImage = 0,
biXPelsPerMeter = 0,
biYPelsPerMeter = 0,
biClrUsed = 0,
biClrImportant = 0
};
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
/// <summary>
/// BMP 位图文件头
/// </summary>
public struct BITMAPFILEHEADER
{
/// <summary>
/// 文件类型标识,用于指定文件格式
/// </summary>
public ushort bfType;
/// <summary>
/// 文件大小,以字节为单位
/// </summary>
public uint bfSize;
/// <summary>
/// 保留字段,未使用
/// </summary>
public ushort bfReserved1;
/// <summary>
/// 保留字段,未使用
/// </summary>
public ushort bfReserved2;
/// <summary>
/// 像素数据的起始位置,以字节为单位,从文件头开始计算
/// </summary>
public uint bfOffBits;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
/// <summary>
/// BMP 位图信息头
/// </summary>
public struct WINBMPINFOHEADER
{
/// <summary>
/// 指定此结构体的字节大小。
/// </summary>
public uint biSize;
/// <summary>
/// 以像素为单位的位图宽度。
/// </summary>
public uint biWidth;
/// <summary>
/// 以像素为单位的位图高度。
/// </summary>
public uint biHeight;
/// <summary>
/// 目标设备的平面数通常为1。
/// </summary>
public ushort biPlanes;
/// <summary>
/// 每个像素的位数表示颜色深度如1、4、8、16、24、32等。
/// </summary>
public ushort biBitCount;
/// <summary>
/// 压缩类型0表示不压缩。
/// </summary>
public uint biCompression;
/// <summary>
/// 位图图像数据的大小以字节为单位若图像未压缩该值可设为0。
/// </summary>
public uint biSizeImage;
/// <summary>
/// 每米X轴方向的像素数水平分辨率通常设为0。
/// </summary>
public uint biXPelsPerMeter;
/// <summary>
/// 每米Y轴方向的像素数垂直分辨率通常设为0。
/// </summary>
public uint biYPelsPerMeter;
/// <summary>
/// 实际使用的颜色索引数若为0则使用位图中实际出现的颜色数。
/// </summary>
public uint biClrUsed;
/// <summary>
/// 重要颜色索引数0表示所有颜色都重要。
/// </summary>
public uint biClrImportant;
}
}
public class BitmapFile(BitmapFileCreator.BITMAPFILEHEADER fileHeader, BitmapFileCreator.WINBMPINFOHEADER infoHeader, byte[] palette, byte[] pixelData)
{
public BITMAPFILEHEADER FileHeader = fileHeader;
public WINBMPINFOHEADER InfoHeader = infoHeader;
public byte[] Palette = palette;
public byte[] PixelData = pixelData;
/// <summary>
/// 转换为byte[]
/// </summary>
/// <param name="bf"></param>
public static explicit operator byte[](BitmapFile bf)
{
var result = new byte[bf.FileHeader.bfSize];
var ms = new MemoryStream(result);
bf.WriteToStream(ms);
return result;
}
public byte[] ToByteArray() => (byte[])this;
public Task<byte[]> ToByteArrayAsync() { return Task.FromResult(ToByteArray());}
/// <summary>
/// 写入此结构到流中
/// </summary>
public void WriteToStream(Stream stream)
{
NoiseOverlayHelper.WriteStruct(stream, FileHeader);
NoiseOverlayHelper.WriteStruct(stream, InfoHeader);
stream.Write(Palette);
stream.Write(PixelData);
stream.Flush();
}
}
}
}

View File

@@ -1,50 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public static class ObjectHelper
{
// Credit/Copyright to https://gist.github.com/tcartwright/dab50ebaff7c59f05013de0fb349cabd
public static bool IsDisposed(this IDisposable obj)
{
/*
TIM C: This hacky code is because MSFT does not provide a standard way to interrogate if an object is disposed or not.
I wrote this based upon streams, but it should work for many other types of MSFT objects (maybe).
*/
if (obj == null) { return true; }
var objType = obj.GetType();
//var foo = new System.IO.BufferedStream();
// the _disposed pattern should catch a lot of msft objects.... hopefully
var isDisposedField = objType.GetField("_disposed", BindingFlags.NonPublic | BindingFlags.Instance) ??
objType.GetField("disposed", BindingFlags.NonPublic | BindingFlags.Instance);
if (isDisposedField != null) { return Convert.ToBoolean(isDisposedField.GetValue(obj)); }
isDisposedField = objType.GetField("_isOpen", BindingFlags.NonPublic | BindingFlags.Instance);
if (isDisposedField != null) { return !Convert.ToBoolean(isDisposedField.GetValue(obj)); }
// System.IO.FileStream
var strategyField = objType.GetField("_strategy", BindingFlags.NonPublic | BindingFlags.Instance);
if (strategyField != null)
{
var strategy = strategyField.GetValue(obj);
var isClosedField = strategy.GetType().GetProperty("IsClosed", BindingFlags.NonPublic | BindingFlags.Instance);
if (isClosedField != null) { return Convert.ToBoolean(isClosedField.GetValue(strategy)); }
}
// other streams that use this pattern to determine if they are disposed
if (obj is Stream stream) { return !stream.CanRead && !stream.CanWrite; }
return false;
}
}
}

View File

@@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Storage;
namespace BetterLyrics.WinUI3.Helper
{
public class PathHelper
{
private static string LocalFolder => ApplicationData.Current.LocalFolder.Path;
public static string CacheFolder => ApplicationData.Current.LocalCacheFolder.Path;
public static string AssetsFolder => Path.Combine(Package.Current.InstalledPath, "Assets");
//public static string LanguageProfilePath => Path.Combine(AssetsFolder, "Core14.profile.xml");
public static string LanguageProfilePath => Path.Combine(AssetsFolder, "Wiki82.profile.xml");
public static string LogoPath => Path.Combine(AssetsFolder, "Logo.ico");
public static string LogDirectory => Path.Combine(CacheFolder, "logs");
public static string LogFilePattern => Path.Combine(LogDirectory, "log-.txt");
public static string LyricsCacheDirectory => Path.Combine(CacheFolder, "lyrics");
public static string LrcLibLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "lrclib");
public static string NeteaseLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "netease");
public static string QQLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "qq");
public static string KugouLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "kugou");
public static string AmllTtmlDbLyricsCacheDirectory => Path.Combine(LyricsCacheDirectory, "amll-ttml-db");
public static string AmllTtmlDbIndexPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-index.json");
public static string AmllTtmlDbLastUpdatedPath => Path.Combine(LyricsCacheDirectory, "amll-ttml-db-last-updated.txt");
public static string TranslationCacheDirectory => Path.Combine(CacheFolder, "translations");
public static string QQTranslationCacheDirectory => Path.Combine(TranslationCacheDirectory, "qq");
public static string NeteaseTranslationCacheDirectory => Path.Combine(TranslationCacheDirectory, "netease");
public static string AlbumArtCacheDirectory => Path.Combine(CacheFolder, "album-art");
public static string iTunesAlbumArtCacheDirectory => Path.Combine(AlbumArtCacheDirectory, "itunes");
public static void EnsureDirectories()
{
Directory.CreateDirectory(LogDirectory);
Directory.CreateDirectory(LrcLibLyricsCacheDirectory);
Directory.CreateDirectory(QQLyricsCacheDirectory);
Directory.CreateDirectory(KugouLyricsCacheDirectory);
Directory.CreateDirectory(NeteaseLyricsCacheDirectory);
Directory.CreateDirectory(AmllTtmlDbLyricsCacheDirectory);
Directory.CreateDirectory(QQTranslationCacheDirectory);
Directory.CreateDirectory(NeteaseTranslationCacheDirectory);
Directory.CreateDirectory(iTunesAlbumArtCacheDirectory);
}
}
}

View File

@@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper
{
public static class StringHelper
{
// 去除空格、括号、下划线、横杠、点、大小写等
public static string Normalize(string s) =>
new string(s
.Where(c => char.IsLetterOrDigit(c))
.ToArray())
.ToLowerInvariant();
public static string NewLine = "\n";
}
}

View File

@@ -1,6 +1,4 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Enums;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml.Media;

View File

@@ -1,74 +0,0 @@
using Microsoft.UI.Dispatching;
using System;
using Vanara.Extensions;
using Vanara.PInvoke;
using static Vanara.PInvoke.CoreAudio;
namespace BetterLyrics.WinUI3.Helper
{
public static class SystemVolumeHelper
{
private readonly static IMMDeviceEnumerator _deviceEnumerator = new();
private static IAudioEndpointVolume? _endpointVolume = null;
private static VolumeCallbackImpl? _callbackImpl;
private static int _masterVolume = 0;
private static DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
public static event Action<int>? VolumeChanged;
static SystemVolumeHelper()
{
var device = _deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia);
if (device != null)
{
device.Activate(typeof(IAudioEndpointVolume).GUID, 0, null, out var obj);
if (obj is IAudioEndpointVolume endpointVolume)
{
_endpointVolume = endpointVolume;
_callbackImpl = new VolumeCallbackImpl();
_endpointVolume.RegisterControlChangeNotify(_callbackImpl);
}
}
}
/// <summary>
/// 获取当前系统主音量0~100
/// </summary>
public static int GetMasterVolume()
{
if (_endpointVolume != null)
{
float level = _endpointVolume.GetMasterVolumeLevelScalar();
_masterVolume = (int)(level * 100);
}
return _masterVolume;
}
/// <summary>
/// 设置当前系统主音量0~100
/// </summary>
public static void SetMasterVolume(int volume)
{
if (_masterVolume == volume) return;
_masterVolume = volume;
_endpointVolume?.SetMasterVolumeLevelScalar(_masterVolume / 100f, Guid.Empty);
}
// 内部回调实现
private class VolumeCallbackImpl : IAudioEndpointVolumeCallback
{
HRESULT IAudioEndpointVolumeCallback.OnNotify(nint pNotify)
{
var data = pNotify.ToStructure<AUDIO_VOLUME_NOTIFICATION_DATA>();
_masterVolume = (int)(data.fMasterVolume * 100);
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
VolumeChanged?.Invoke(_masterVolume);
});
return HRESULT.S_OK;
}
}
}
}

View File

@@ -1,37 +0,0 @@
using System;
namespace BetterLyrics.WinUI3.Helper
{
public class ThrottleHelper
{
private DateTime _lastTriggerTime = DateTime.MinValue;
private readonly TimeSpan _interval;
public ThrottleHelper(TimeSpan interval)
{
_interval = interval;
}
/// <summary>
/// 判断是否可以触发(距离上次触发已超过设定间隔),如果可以则更新时间戳并返回 true否则返回 false。
/// </summary>
public bool CanTrigger()
{
var now = DateTime.Now;
if ((now - _lastTriggerTime) >= _interval)
{
_lastTriggerTime = now;
return true;
}
return false;
}
/// <summary>
/// 重置触发时间
/// </summary>
public void Reset()
{
_lastTriggerTime = DateTime.MinValue;
}
}
}

View File

@@ -1,180 +0,0 @@
// 2025/6/23 by Zhe Fang
using System;
using System.Diagnostics;
using BetterLyrics.WinUI3.Enums;
namespace BetterLyrics.WinUI3.Helper
{
public class ValueTransition<T>
where T : struct
{
private T _currentValue;
private float _durationSeconds;
private EasingType? _easingType;
private Func<T, T, float, T> _interpolator;
private bool _isTransitioning;
private float _progress;
private T _startValue;
private T _targetValue;
public float DurationSeconds => _durationSeconds;
public bool IsTransitioning => _isTransitioning;
public T Value => _currentValue;
public T TargetValue => _targetValue;
public ValueTransition(T initialValue, float durationSeconds, Func<T, T, float, T>? interpolator = null, EasingType? easingType = null)
{
_currentValue = initialValue;
_startValue = initialValue;
_targetValue = initialValue;
_durationSeconds = durationSeconds;
_progress = 1f;
_isTransitioning = false;
if (interpolator != null)
{
_interpolator = interpolator;
_easingType = null;
}
else if (easingType.HasValue)
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
}
else
{
_easingType = EasingType.Linear;
_interpolator = GetInterpolatorByEasingType(_easingType.Value);
}
}
public void SetDuration(float seconds)
{
if (seconds <= 0)
throw new ArgumentOutOfRangeException(nameof(seconds), "Duration must be positive.");
_durationSeconds = seconds;
}
private void JumpTo(T value)
{
_currentValue = value;
_startValue = value;
_targetValue = value;
_progress = 1f;
_isTransitioning = false;
}
public void Reset(T value)
{
_currentValue = value;
_startValue = value;
_targetValue = value;
_progress = 0f;
_isTransitioning = false;
}
public void StartTransition(T targetValue, bool jumpTo = false)
{
if (jumpTo)
{
JumpTo(targetValue);
return;
}
if (!targetValue.Equals(_currentValue))
{
_startValue = _currentValue;
_targetValue = targetValue;
_progress = 0f;
_isTransitioning = true;
}
}
public static bool Equals(double x, double y, double tolerance)
{
var diff = Math.Abs(x - y);
return diff <= tolerance || diff <= Math.Max(Math.Abs(x), Math.Abs(y)) * tolerance;
}
public void Update(TimeSpan elapsedTime)
{
if (!_isTransitioning) return;
_progress += (float)elapsedTime.TotalSeconds / _durationSeconds;
if (_progress >= 1f)
{
_progress = 1f;
_currentValue = _targetValue;
_isTransitioning = false;
}
else
{
_currentValue = _interpolator(_startValue, _targetValue, _progress);
}
}
private Func<T, T, float, T> GetInterpolatorByEasingType(EasingType type)
{
if (typeof(T) == typeof(float))
{
return (start, end, progress) =>
{
float s = (float)(object)start;
float e = (float)(object)end;
float t = progress;
switch (type)
{
case EasingType.EaseInOutSine:
t = EasingHelper.EaseInOutSine(t);
break;
case EasingType.EaseInOutQuad:
t = EasingHelper.EaseInOutQuad(t);
break;
case EasingType.EaseInOutCubic:
t = EasingHelper.EaseInOutCubic(t);
break;
case EasingType.EaseInOutQuart:
t = EasingHelper.EaseInOutQuart(t);
break;
case EasingType.EaseInOutQuint:
t = EasingHelper.EaseInOutQuint(t);
break;
case EasingType.EaseInOutExpo:
t = EasingHelper.EaseInOutExpo(t);
break;
case EasingType.EaseInOutCirc:
t = EasingHelper.EaseInOutCirc(t);
break;
case EasingType.EaseInOutBack:
t = EasingHelper.EaseInOutBack(t);
break;
case EasingType.EaseInOutElastic:
t = EasingHelper.EaseInOutElastic(t);
break;
case EasingType.EaseInOutBounce:
t = EasingHelper.EaseInOutBounce(t);
break;
case EasingType.SmoothStep:
t = EasingHelper.SmoothStep(t);
break;
case EasingType.Linear:
t = EasingHelper.Linear(t);
break;
default:
break;
}
return (T)(object)(s + (e - s) * t);
};
}
throw new NotSupportedException($"Easing type {type} is not supported for type {typeof(T)}.");
}
public void SetEasingType(EasingType easingType)
{
_easingType = easingType;
_interpolator = GetInterpolatorByEasingType(easingType);
}
}
}

View File

@@ -0,0 +1,106 @@
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace BetterLyrics.WinUI3.Helper
{
public static class WindowColorHelper
{
public static Color GetDominantColorBelow(IntPtr myHwnd)
{
if (!GetWindowRect(myHwnd, out RECT myRect))
return Color.Transparent;
int screenWidth = GetSystemMetrics(SystemMetric.SM_CXSCREEN);
int sampleHeight = 1;
int sampleY = myRect.Bottom + 1;
return GetAverageColorFromScreenRegion(0, sampleY, screenWidth, sampleHeight);
}
private static Color GetAverageColorFromScreenRegion(int x, int y, int width, int height)
{
using Bitmap bmp = new(width, height, PixelFormat.Format32bppArgb);
using Graphics gDest = Graphics.FromImage(bmp);
IntPtr hdcDest = gDest.GetHdc();
IntPtr hdcSrc = GetDC(IntPtr.Zero); // Entire screen
BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, SRCCOPY);
gDest.ReleaseHdc(hdcDest);
ReleaseDC(IntPtr.Zero, hdcSrc);
return ComputeAverageColor(bmp);
}
private static Color ComputeAverageColor(Bitmap bmp)
{
long r = 0,
g = 0,
b = 0;
int count = 0;
for (int y = 0; y < bmp.Height; y++)
{
for (int x = 0; x < bmp.Width; x++)
{
Color pixel = bmp.GetPixel(x, y);
r += pixel.R;
g += pixel.G;
b += pixel.B;
count++;
}
}
if (count == 0)
return Color.Transparent;
return Color.FromArgb((int)(r / count), (int)(g / count), (int)(b / count));
}
#region Win32 Imports & Structs
private const int SRCCOPY = 0x00CC0020;
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("gdi32.dll")]
private static extern bool BitBlt(
IntPtr hdcDest,
int nXDest,
int nYDest,
int nWidth,
int nHeight,
IntPtr hdcSrc,
int nXSrc,
int nYSrc,
int dwRop
);
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(SystemMetric smIndex);
private enum SystemMetric
{
SM_CXSCREEN = 0,
SM_CYSCREEN = 1,
}
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
#endregion
}
}

View File

@@ -1,106 +1,122 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using System;
using System.Collections.Generic;
using BetterLyrics.WinUI3.Views;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using Windows.ApplicationModel.Core;
using WinRT.Interop;
using WinUIEx;
namespace BetterLyrics.WinUI3.Helper
{
public static class WindowHelper
{
private static List<object> _activeWindows = [];
private static readonly Dictionary<Type, Window> _windowCache = new();
public static void CloseWindow<T>()
public static void HideSystemTitleBar(this Window window)
{
var window = _activeWindows.Find(w => w is T);
if (window is Window w)
window.ExtendsContentIntoTitleBar = true;
window.AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
}
public static void HideSystemTitleBarAndSetCustomTitleBar(
this Window window,
UIElement titleBar
)
{
window.HideSystemTitleBar();
window.SetTitleBar(titleBar);
}
public static void OpenSettingsWindow()
{
OpenOrShowWindow(typeof(SettingsPage));
}
public static void OpenLyricsWindow()
{
OpenOrShowWindow(typeof(LyricsPage));
}
private static void OpenOrShowWindow(Type pageType)
{
if (_windowCache.TryGetValue(pageType, out var window))
{
w.Close();
_activeWindows.Remove(w);
if (window is HostWindow hostWindow)
{
hostWindow.Navigate(pageType);
}
window.TryShow();
}
else
{
var newWindow = new HostWindow();
TrackWindow(newWindow, pageType);
newWindow.Navigate(pageType);
newWindow.Activate();
}
}
public static T? GetWindowByWindowType<T>()
public static void TrackWindow(Window window, Type pageType = null)
{
foreach (var window in _activeWindows)
if (pageType != null)
{
if (window is T castedWindow)
{
return castedWindow;
}
_windowCache[pageType] = window;
}
return default;
}
public static void OpenWindow<T>()
{
var window = _activeWindows.Find(w => w is T);
if (window == null)
{
if (typeof(T) == typeof(LyricsWindow))
{
window = new LyricsWindow();
((LyricsWindow)window).SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(BackdropType.Transparent);
}
else if (typeof(T) == typeof(SettingsWindow))
{
window = new SettingsWindow();
}
else if (typeof(T) == typeof(MusicGalleryWindow))
{
window = new MusicGalleryWindow();
}
else
{
throw new ArgumentException("Unsupported window type", nameof(T));
}
TrackWindow(window);
}
var castedWindow = (Window)window;
castedWindow.Restore();
castedWindow.Activate();
}
public static void RestartApp(string args = "")
{
// The restart will be executed immediately.
AppRestartFailureReason failureReason =
Microsoft.Windows.AppLifecycle.AppInstance.Restart(args);
// If the restart fails, handle it here.
switch (failureReason)
{
case AppRestartFailureReason.RestartPending:
break;
case AppRestartFailureReason.NotInForeground:
break;
case AppRestartFailureReason.InvalidUser:
break;
default: //AppRestartFailureReason.Other
break;
}
}
private static void TrackWindow(object window)
{
if (!_activeWindows.Contains(window))
{
_activeWindows.Add(window);
var castedWindow = (Window)window;
castedWindow.Closed += WindowHelper_Closed;
}
public static Window GetWindowForElement(UIElement element)
{
if (element.XamlRoot != null)
{
foreach (Window window in _activeWindows)
{
if (element.XamlRoot == window.Content.XamlRoot)
{
return window;
}
}
}
return null;
}
// get dpi for an element
static public double GetRasterizationScaleForElement(UIElement element)
{
if (element.XamlRoot != null)
{
foreach (Window window in _activeWindows)
{
if (element.XamlRoot == window.Content.XamlRoot)
{
return element.XamlRoot.RasterizationScale;
}
}
}
return 0.0;
}
public static List<Window> ActiveWindows
{
get { return _activeWindows; }
}
private static List<Window> _activeWindows = new List<Window>();
public static void TryShow(this Window window)
{
if (window is not null)
{
window.Activate();
}
}
private static void WindowHelper_Closed(object sender, WindowEventArgs args)
public static void TryHide(this Window window)
{
if (_activeWindows.Contains(sender))
if (window is not null)
{
_activeWindows.Remove(sender);
window.Hide();
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Models;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace BetterLyrics.WinUI3.Messages
{
public class ShowNotificatonMessage(Notification value)
: ValueChangedMessage<Notification>(value) { }
}

View File

@@ -1,24 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
public partial class AlbumArtSearchProviderInfo : ObservableObject
{
[ObservableProperty]
public partial bool IsEnabled { get; set; }
[ObservableProperty]
public partial AlbumArtSearchProvider Provider { get; set; }
public AlbumArtSearchProviderInfo() { }
public AlbumArtSearchProviderInfo(AlbumArtSearchProvider provider, bool isEnabled)
{
Provider = provider;
IsEnabled = isEnabled;
}
}
}

View File

@@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public partial class GroupInfoList : List<object>
{
public required object Key { get; set; }
public GroupInfoList(IEnumerable<object> items, Func<object, object>? orderSelector = null)
: base(orderSelector != null
? items.OrderBy(orderSelector)
: items)
{
}
public override string ToString()
{
return $"{Key}";
}
}
}

View File

@@ -1,23 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models
{
public partial class LanguageInfo : ObservableObject
{
[ObservableProperty]
public partial string Code { get; set; }
[ObservableProperty]
public partial string Name { get; set; }
public LanguageInfo(string code, string name)
{
Code = code;
Name = name;
}
}
}

View File

@@ -1,23 +0,0 @@
// 2025/6/23 by Zhe Fang
using CommunityToolkit.Mvvm.ComponentModel;
namespace BetterLyrics.WinUI3.Models
{
public partial class LocalMediaFolder : ObservableObject
{
[ObservableProperty]
public partial bool IsEnabled { get; set; }
[ObservableProperty]
public partial string Path { get; set; }
public LocalMediaFolder() { }
public LocalMediaFolder(string path, bool isEnabled)
{
Path = path;
IsEnabled = isEnabled;
}
}
}

View File

@@ -1,14 +0,0 @@
// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Helper;
namespace BetterLyrics.WinUI3.Models
{
public class LyricsChar
{
public int? EndMs { get; set; }
public int StartIndex { get; set; }
public int StartMs { get; set; }
public string Text { get; set; } = string.Empty;
}
}

Some files were not shown because too many files have changed in this diff Show More