Initial commit after cleanup

This commit is contained in:
Zhe Fang
2025-06-03 17:48:19 -04:00
commit f24e8b5fcd
100 changed files with 6095 additions and 0 deletions

63
.gitattributes vendored Normal file
View File

@@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

407
.gitignore vendored Normal file
View File

@@ -0,0 +1,407 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
[Aa][Rr][Mm]64[Ee][Cc]/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.idb
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# AWS SAM Build and Temporary Artifacts folder
.aws-sam
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml

View File

@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '15.0'">
<VisualStudioVersion>15.0</VisualStudioVersion>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x86">
<Configuration>Debug</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x86">
<Configuration>Release</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup>
<WapProjPath Condition="'$(WapProjPath)'==''">$(MSBuildExtensionsPath)\Microsoft\DesktopBridge\</WapProjPath>
<PathToXAMLWinRTImplementations>BetterLyrics.WinUI3\</PathToXAMLWinRTImplementations>
</PropertyGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.props" />
<PropertyGroup>
<ProjectGuid>6576cd19-ef92-4099-b37d-e2d8ebdb6bf5</ProjectGuid>
<TargetPlatformVersion>10.0.26100.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<AssetTargetFallback>net8.0-windows$(TargetPlatformVersion);$(AssetTargetFallback)</AssetTargetFallback>
<DefaultLanguage>zh-CN</DefaultLanguage>
<AppxPackageSigningEnabled>false</AppxPackageSigningEnabled>
<EntryPointProjectUniqueName>..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj</EntryPointProjectUniqueName>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundlePlatforms>x86|x64|arm64</AppxBundlePlatforms>
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
<SubType>Designer</SubType>
</AppxManifest>
</ItemGroup>
<ItemGroup>
<Content Include="Images\LargeTile.scale-100.png" />
<Content Include="Images\LargeTile.scale-125.png" />
<Content Include="Images\LargeTile.scale-150.png" />
<Content Include="Images\LargeTile.scale-200.png" />
<Content Include="Images\LargeTile.scale-400.png" />
<Content Include="Images\SmallTile.scale-100.png" />
<Content Include="Images\SmallTile.scale-125.png" />
<Content Include="Images\SmallTile.scale-150.png" />
<Content Include="Images\SmallTile.scale-200.png" />
<Content Include="Images\SmallTile.scale-400.png" />
<Content Include="Images\SplashScreen.scale-100.png" />
<Content Include="Images\SplashScreen.scale-125.png" />
<Content Include="Images\SplashScreen.scale-150.png" />
<Content Include="Images\SplashScreen.scale-200.png" />
<Content Include="Images\LockScreenLogo.scale-200.png" />
<Content Include="Images\SplashScreen.scale-400.png" />
<Content Include="Images\Square150x150Logo.scale-100.png" />
<Content Include="Images\Square150x150Logo.scale-125.png" />
<Content Include="Images\Square150x150Logo.scale-150.png" />
<Content Include="Images\Square150x150Logo.scale-200.png" />
<Content Include="Images\Square150x150Logo.scale-400.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-16.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-24.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-256.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-32.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-48.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-16.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-256.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-32.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-48.png" />
<Content Include="Images\Square44x44Logo.scale-100.png" />
<Content Include="Images\Square44x44Logo.scale-125.png" />
<Content Include="Images\Square44x44Logo.scale-150.png" />
<Content Include="Images\Square44x44Logo.scale-200.png" />
<Content Include="Images\Square44x44Logo.scale-400.png" />
<Content Include="Images\Square44x44Logo.targetsize-16.png" />
<Content Include="Images\Square44x44Logo.targetsize-24.png" />
<Content Include="Images\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Images\Square44x44Logo.targetsize-256.png" />
<Content Include="Images\Square44x44Logo.targetsize-32.png" />
<Content Include="Images\Square44x44Logo.targetsize-48.png" />
<Content Include="Images\StoreLogo.scale-100.png" />
<Content Include="Images\StoreLogo.scale-125.png" />
<Content Include="Images\StoreLogo.scale-150.png" />
<Content Include="Images\StoreLogo.scale-200.png" />
<Content Include="Images\StoreLogo.scale-400.png" />
<Content Include="Images\Wide310x150Logo.scale-100.png" />
<Content Include="Images\Wide310x150Logo.scale-125.png" />
<Content Include="Images\Wide310x150Logo.scale-150.png" />
<Content Include="Images\Wide310x150Logo.scale-200.png" />
<Content Include="Images\Wide310x150Logo.scale-400.png" />
<None Include="Package.StoreAssociation.xml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj">
<SkipGetTargetFrameworkProperties>True</SkipGetTargetFrameworkProperties>
<PublishProfile>Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfile>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
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"
IgnorableNamespaces="uap rescap">
<Identity
Name="37412.BetterLyrics"
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
Version="1.0.0.0" />
<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>
<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="x-generate"/>
</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>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8" ?>
<Application
x:Class="BetterLyrics.WinUI3.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converter="using:BetterLyrics.WinUI3.Converter"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:local="using:BetterLyrics.WinUI3">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsExpander/SettingsExpander.xaml" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Theme -->
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<Color x:Key="SemiTransparentSystemBaseHighColor">#80000000</Color>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<Color x:Key="SemiTransparentSystemBaseHighColor">#80FFFFFF</Color>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<!-- Brush -->
<LinearGradientBrush x:Key="BaseHighEdgeHorizontalFadeBrush" StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Offset="0" Color="Transparent" />
<GradientStop Offset="0.02" Color="{ThemeResource SystemBaseHighColor}" />
<GradientStop Offset="0.98" Color="{ThemeResource SystemBaseHighColor}" />
<GradientStop Offset="1" Color="Transparent" />
</LinearGradientBrush>
<LinearGradientBrush x:Key="BaseHighEdgeVerticalFadeBrush" StartPoint="0.5,0" EndPoint="0.5,1">
<GradientStop Offset="0" Color="Transparent" />
<GradientStop Offset="0.15" Color="{ThemeResource SystemBaseHighColor}" />
<GradientStop Offset="0.85" Color="{ThemeResource SystemBaseHighColor}" />
<GradientStop Offset="1" Color="Transparent" />
</LinearGradientBrush>
<!-- Animation -->
<ExponentialEase x:Key="EaseInOut" EasingMode="EaseInOut" />
<ExponentialEase x:Key="EaseOut" EasingMode="EaseOut" />
<ExponentialEase x:Key="EaseIn" EasingMode="EaseIn" />
<!-- Converter -->
<converter:ThemeTypeToElementThemeConverter x:Key="ThemeTypeToElementThemeConverter" />
<converter:ColorToBrushConverter x:Key="ColorToBrushConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
<x:Double x:Key="SettingsCardSpacing">4</x:Double>
<!-- Style (inc. the correct spacing) of a section header -->
<Style
x:Key="SettingsSectionHeaderTextBlockStyle"
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
TargetType="TextBlock">
<Style.Setters>
<Setter Property="Margin" Value="1,30,0,6" />
</Style.Setters>
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,63 @@
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.ApplicationModel.Resources;
using System.Text;
// 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 {
public static App Current => (App)Application.Current;
public MainWindow? MainWindow { get; private set; }
public MainWindow? SettingsWindow { get; set; }
public static ResourceLoader ResourceLoader = new();
public static DispatcherQueue DispatcherQueue => DispatcherQueue.GetForCurrentThread();
/// <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();
}
/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) {
// Register services
Ioc.Default.ConfigureServices(
new ServiceCollection()
.AddSingleton(DispatcherQueue.GetForCurrentThread())
// Services
.AddSingleton<SettingsService>()
.AddSingleton<DatabaseService>()
// ViewModels
.AddSingleton<MainViewModel>()
.AddSingleton<SettingsViewModel>()
.BuildServiceProvider());
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// Activate the window
MainWindow = new MainWindow();
MainWindow!.Navigate(typeof(MainPage));
MainWindow.Activate();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

View File

@@ -0,0 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>BetterLyrics.WinUI3</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
</PropertyGroup>
<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.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.Media" Version="8.2.250402" />
<PackageReference Include="DevWinUI" Version="8.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<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.250513003" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.5" />
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
<PackageReference Include="z440.atl.core" Version="6.24.0" />
</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>
</Project>

View File

@@ -0,0 +1,23 @@
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI;
namespace BetterLyrics.WinUI3.Converter {
public class ColorToBrushConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, string language) {
if (value is Color color) {
return new SolidColorBrush(color);
}
return new SolidColorBrush();
}
public object ConvertBack(object value, Type targetType, object parameter, string language) {
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.UI.Xaml;
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 {
internal class ThemeTypeToElementThemeConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, string language) {
if (value is int themeType) {
return (ElementTheme)themeType;
}
return ElementTheme.Default;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) {
return 0;
}
}
}

View File

@@ -0,0 +1,44 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media.Animation;
using System;
namespace BetterLyrics.WinUI3.Helper {
/// <summary>
/// Edited based on: https://stackoverflow.com/a/25236507/11048731
/// </summary>
public class AnimationHelper : DependencyObject {
public static int GetAnimationDuration(DependencyObject obj) {
return (int)obj.GetValue(AnimationDurationProperty);
}
public static void SetAnimationDuration(DependencyObject obj, int value) {
obj.SetValue(AnimationDurationProperty, value);
}
// Using a DependencyProperty as the backing store for AnimationDuration.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty AnimationDurationProperty =
DependencyProperty.RegisterAttached("AnimationDuration", typeof(int),
typeof(AnimationHelper), new PropertyMetadata(0,
OnAnimationDurationChanged));
private static void OnAnimationDurationChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
FrameworkElement element = d as FrameworkElement;
var ms = (int)e.NewValue;
if (ms < 0) return;
var key = "LyricsLineCharGradientInTextBlock";
foreach (var timeline in (element.Resources[key] as Storyboard).Children) {
foreach (var keyFrame in (timeline as DoubleAnimationUsingKeyFrames).KeyFrames) {
(keyFrame as LinearDoubleKeyFrame).KeyTime =
KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(ms));
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper {
public static class CollectionHelper {
public static T? SafeGet<T>(this IList<T> list, int index) {
if (list == null || index < 0 || index >= list.Count)
return default;
return list[index];
}
}
}

View File

@@ -0,0 +1,12 @@
namespace BetterLyrics.WinUI3.Helper {
public class ColorHelper {
public static Windows.UI.Color LerpColor(Windows.UI.Color a, Windows.UI.Color b, double t) {
byte A = (byte)(a.A + (b.A - a.A) * t);
byte R = (byte)(a.R + (b.R - a.R) * t);
byte G = (byte)(a.G + (b.G - a.G) * t);
byte B = (byte)(a.B + (b.B - a.B) * t);
return Windows.UI.Color.FromArgb(A, R, G, B);
}
}
}

View File

@@ -0,0 +1,928 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
namespace BetterLyrics.WinUI3.Helper
{
/// <summary>
/// Color map
/// </summary>
internal class CMap
{
private readonly List<VBox> vboxes = new List<VBox>();
private List<QuantizedColor> palette;
public void Push(VBox box)
{
palette = null;
vboxes.Add(box);
}
public List<QuantizedColor> GeneratePalette()
{
if (palette == null)
{
palette = (from vBox in vboxes
let rgb = vBox.Avg(false)
let color = FromRgb(rgb[0], rgb[1], rgb[2])
select new QuantizedColor(color, vBox.Count(false))).ToList();
}
return palette;
}
public int Size()
{
return vboxes.Count;
}
public int[] Map(int[] color)
{
foreach (var vbox in vboxes.Where(vbox => vbox.Contains(color)))
{
return vbox.Avg(false);
}
return Nearest(color);
}
public int[] Nearest(int[] color)
{
var d1 = double.MaxValue;
int[] pColor = null;
foreach (var t in vboxes)
{
var vbColor = t.Avg(false);
var d2 = Math.Sqrt(Math.Pow(color[0] - vbColor[0], 2)
+ Math.Pow(color[1] - vbColor[1], 2)
+ Math.Pow(color[2] - vbColor[2], 2));
if (d2 < d1)
{
d1 = d2;
pColor = vbColor;
}
}
return pColor;
}
public VBox FindColor(double targetLuma, double minLuma, double maxLuma, double targetSaturation, double minSaturation, double maxSaturation)
{
VBox max = null;
double maxValue = 0;
var highestPopulation = vboxes.Select(p => p.Count(false)).Max();
foreach (var swatch in vboxes)
{
var avg = swatch.Avg(false);
var hsl = FromRgb(avg[0], avg[1], avg[2]).ToHsl();
var sat = hsl.S;
var luma = hsl.L;
if (sat >= minSaturation && sat <= maxSaturation &&
luma >= minLuma && luma <= maxLuma)
{
var thisValue = Mmcq.CreateComparisonValue(sat, targetSaturation, luma, targetLuma,
swatch.Count(false), highestPopulation);
if (max == null || thisValue > maxValue)
{
max = swatch;
maxValue = thisValue;
}
}
}
return max;
}
public Color FromRgb(int red, int green, int blue)
{
var color = new Color
{
A = 255,
R = (byte)red,
G = (byte)green,
B = (byte)blue
};
return color;
}
}
/// <summary>
/// Defines a color in RGB space.
/// </summary>
public struct Color
{
/// <summary>
/// Get or Set the Alpha component value for sRGB.
/// </summary>
public byte A;
/// <summary>
/// Get or Set the Blue component value for sRGB.
/// </summary>
public byte B;
/// <summary>
/// Get or Set the Green component value for sRGB.
/// </summary>
public byte G;
/// <summary>
/// Get or Set the Red component value for sRGB.
/// </summary>
public byte R;
/// <summary>
/// Get HSL color.
/// </summary>
/// <returns></returns>
public HslColor ToHsl()
{
const double toDouble = 1.0 / 255;
var r = toDouble * R;
var g = toDouble * G;
var b = toDouble * B;
var max = Math.Max(Math.Max(r, g), b);
var min = Math.Min(Math.Min(r, g), b);
var chroma = max - min;
double h1;
// ReSharper disable CompareOfFloatsByEqualityOperator
if (chroma == 0)
{
h1 = 0;
}
else if (max == r)
{
h1 = (g - b) / chroma % 6;
}
else if (max == g)
{
h1 = 2 + (b - r) / chroma;
}
else //if (max == b)
{
h1 = 4 + (r - g) / chroma;
}
var lightness = 0.5 * (max - min);
var saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs(2 * lightness - 1));
HslColor ret;
ret.H = 60 * h1;
ret.S = saturation;
ret.L = lightness;
ret.A = toDouble * A;
return ret;
// ReSharper restore CompareOfFloatsByEqualityOperator
}
public string ToHexString()
{
return "#" + R.ToString("X2") + G.ToString("X2") + B.ToString("X2");
}
public string ToHexAlphaString()
{
return "#" + A.ToString("X2") + R.ToString("X2") + G.ToString("X2") + B.ToString("X2");
}
public override string ToString()
{
if (A == 255)
{
return ToHexString();
}
return ToHexAlphaString();
}
}
/// <summary>
/// Defines a color in Hue/Saturation/Lightness (HSL) space.
/// </summary>
public struct HslColor
{
/// <summary>
/// The Alpha/opacity in 0..1 range.
/// </summary>
public double A;
/// <summary>
/// The Hue in 0..360 range.
/// </summary>
public double H;
/// <summary>
/// The Lightness in 0..1 range.
/// </summary>
public double L;
/// <summary>
/// The Saturation in 0..1 range.
/// </summary>
public double S;
}
internal static class Mmcq
{
public const int Sigbits = 5;
public const int Rshift = 8 - Sigbits;
public const int Mult = 1 << Rshift;
public const int Histosize = 1 << (3 * Sigbits);
public const int VboxLength = 1 << Sigbits;
public const double FractByPopulation = 0.75;
public const int MaxIterations = 1000;
public const double WeightSaturation = 3d;
public const double WeightLuma = 6d;
public const double WeightPopulation = 1d;
private static readonly VBoxComparer ComparatorProduct = new VBoxComparer();
private static readonly VBoxCountComparer ComparatorCount = new VBoxCountComparer();
public static int GetColorIndex(int r, int g, int b)
{
return (r << (2 * Sigbits)) + (g << Sigbits) + b;
}
/// <summary>
/// Gets the histo.
/// </summary>
/// <param name="pixels">The pixels.</param>
/// <returns>Histo (1-d array, giving the number of pixels in each quantized region of color space), or null on error.</returns>
private static int[] GetHisto(IEnumerable<byte[]> pixels)
{
var histo = new int[Histosize];
foreach (var pixel in pixels)
{
var rval = pixel[0] >> Rshift;
var gval = pixel[1] >> Rshift;
var bval = pixel[2] >> Rshift;
var index = GetColorIndex(rval, gval, bval);
histo[index]++;
}
return histo;
}
private static VBox VboxFromPixels(IList<byte[]> pixels, int[] histo)
{
int rmin = 1000000, rmax = 0;
int gmin = 1000000, gmax = 0;
int bmin = 1000000, bmax = 0;
// find min/max
var numPixels = pixels.Count;
for (var i = 0; i < numPixels; i++)
{
var pixel = pixels[i];
var rval = pixel[0] >> Rshift;
var gval = pixel[1] >> Rshift;
var bval = pixel[2] >> Rshift;
if (rval < rmin)
{
rmin = rval;
}
else if (rval > rmax)
{
rmax = rval;
}
if (gval < gmin)
{
gmin = gval;
}
else if (gval > gmax)
{
gmax = gval;
}
if (bval < bmin)
{
bmin = bval;
}
else if (bval > bmax)
{
bmax = bval;
}
}
return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo);
}
private static VBox[] DoCut(char color, VBox vbox, IList<int> partialsum, IList<int> lookaheadsum, int total)
{
int vboxDim1;
int vboxDim2;
switch (color)
{
case 'r':
vboxDim1 = vbox.R1;
vboxDim2 = vbox.R2;
break;
case 'g':
vboxDim1 = vbox.G1;
vboxDim2 = vbox.G2;
break;
default:
vboxDim1 = vbox.B1;
vboxDim2 = vbox.B2;
break;
}
for (var i = vboxDim1; i <= vboxDim2; i++)
{
if (partialsum[i] > total / 2)
{
var vbox1 = vbox.Clone();
var vbox2 = vbox.Clone();
var left = i - vboxDim1;
var right = vboxDim2 - i;
var d2 = left <= right
? Math.Min(vboxDim2 - 1, Math.Abs(i + right / 2))
: Math.Max(vboxDim1, Math.Abs(Convert.ToInt32(i - 1 - left / 2.0)));
// avoid 0-count boxes
while (d2 < 0 || partialsum[d2] <= 0)
{
d2++;
}
var count2 = lookaheadsum[d2];
while (count2 == 0 && d2 > 0 && partialsum[d2 - 1] > 0)
{
count2 = lookaheadsum[--d2];
}
// set dimensions
switch (color)
{
case 'r':
vbox1.R2 = d2;
vbox2.R1 = d2 + 1;
break;
case 'g':
vbox1.G2 = d2;
vbox2.G1 = d2 + 1;
break;
default:
vbox1.B2 = d2;
vbox2.B1 = d2 + 1;
break;
}
return new[] { vbox1, vbox2 };
}
}
throw new Exception("VBox can't be cut");
}
private static VBox[] MedianCutApply(IList<int> histo, VBox vbox)
{
if (vbox.Count(false) == 0)
{
return null;
}
if (vbox.Count(false) == 1)
{
return new[] { vbox.Clone(), null };
}
// only one pixel, no split
var rw = vbox.R2 - vbox.R1 + 1;
var gw = vbox.G2 - vbox.G1 + 1;
var bw = vbox.B2 - vbox.B1 + 1;
var maxw = Math.Max(Math.Max(rw, gw), bw);
// Find the partial sum arrays along the selected axis.
var total = 0;
var partialsum = new int[VboxLength];
// -1 = not set / 0 = 0
for (var l = 0; l < partialsum.Length; l++)
{
partialsum[l] = -1;
}
// -1 = not set / 0 = 0
var lookaheadsum = new int[VboxLength];
for (var l = 0; l < lookaheadsum.Length; l++)
{
lookaheadsum[l] = -1;
}
int i, j, k, sum, index;
if (maxw == rw)
{
for (i = vbox.R1; i <= vbox.R2; i++)
{
sum = 0;
for (j = vbox.G1; j <= vbox.G2; j++)
{
for (k = vbox.B1; k <= vbox.B2; k++)
{
index = GetColorIndex(i, j, k);
sum += histo[index];
}
}
total += sum;
partialsum[i] = total;
}
}
else if (maxw == gw)
{
for (i = vbox.G1; i <= vbox.G2; i++)
{
sum = 0;
for (j = vbox.R1; j <= vbox.R2; j++)
{
for (k = vbox.B1; k <= vbox.B2; k++)
{
index = GetColorIndex(j, i, k);
sum += histo[index];
}
}
total += sum;
partialsum[i] = total;
}
}
else /* maxw == bw */
{
for (i = vbox.B1; i <= vbox.B2; i++)
{
sum = 0;
for (j = vbox.R1; j <= vbox.R2; j++)
{
for (k = vbox.G1; k <= vbox.G2; k++)
{
index = GetColorIndex(j, k, i);
sum += histo[index];
}
}
total += sum;
partialsum[i] = total;
}
}
for (i = 0; i < VboxLength; i++)
{
if (partialsum[i] != -1)
{
lookaheadsum[i] = total - partialsum[i];
}
}
// determine the cut planes
return maxw == rw ? DoCut('r', vbox, partialsum, lookaheadsum, total) : maxw == gw
? DoCut('g', vbox, partialsum, lookaheadsum, total) : DoCut('b', vbox, partialsum, lookaheadsum, total);
}
/// <summary>
/// Inner function to do the iteration.
/// </summary>
/// <param name="lh">The lh.</param>
/// <param name="comparator">The comparator.</param>
/// <param name="target">The target.</param>
/// <param name="histo">The histo.</param>
/// <exception cref="System.Exception">vbox1 not defined; shouldn't happen!</exception>
private static void Iter(List<VBox> lh, IComparer<VBox> comparator, int target, IList<int> histo)
{
var ncolors = 1;
var niters = 0;
while (niters < MaxIterations)
{
var vbox = lh[lh.Count - 1];
if (vbox.Count(false) == 0)
{
lh.Sort(comparator);
niters++;
continue;
}
lh.RemoveAt(lh.Count - 1);
// do the cut
var vboxes = MedianCutApply(histo, vbox);
var vbox1 = vboxes[0];
var vbox2 = vboxes[1];
if (vbox1 == null)
{
throw new Exception(
"vbox1 not defined; shouldn't happen!");
}
lh.Add(vbox1);
if (vbox2 != null)
{
lh.Add(vbox2);
ncolors++;
}
lh.Sort(comparator);
if (ncolors >= target)
{
return;
}
if (niters++ > MaxIterations)
{
return;
}
}
}
public static CMap Quantize(byte[][] pixels, int maxcolors)
{
// short-circuit
if (pixels.Length == 0 || maxcolors < 2 || maxcolors > 256)
{
return null;
}
var histo = GetHisto(pixels);
// get the beginning vbox from the colors
var vbox = VboxFromPixels(pixels, histo);
var pq = new List<VBox> { vbox };
// Round up to have the same behaviour as in JavaScript
var target = (int)Math.Ceiling(FractByPopulation * maxcolors);
// first set of colors, sorted by population
Iter(pq, ComparatorCount, target, histo);
// Re-sort by the product of pixel occupancy times the size in color
// space.
pq.Sort(ComparatorProduct);
// next set - generate the median cuts using the (npix * vol) sorting.
Iter(pq, ComparatorProduct, maxcolors - pq.Count, histo);
// Reverse to put the highest elements first into the color map
pq.Reverse();
// calculate the actual colors
var cmap = new CMap();
foreach (var vb in pq)
{
cmap.Push(vb);
}
return cmap;
}
public static double CreateComparisonValue(double saturation, double targetSaturation, double luma, double targetLuma, int population, int highestPopulation)
{
return WeightedMean(InvertDiff(saturation, targetSaturation), WeightSaturation,
InvertDiff(luma, targetLuma), WeightLuma,
population / (double)highestPopulation, WeightPopulation);
}
private static double WeightedMean(params double[] values)
{
double sum = 0;
double sumWeight = 0;
for (var i = 0; i < values.Length; i += 2)
{
var value = values[i];
var weight = values[i + 1];
sum += value * weight;
sumWeight += weight;
}
return sum / sumWeight;
}
private static double InvertDiff(double value, double targetValue)
{
return 1 - Math.Abs(value - targetValue);
}
}
public class QuantizedColor
{
public QuantizedColor(Color color, int population)
{
Color = color;
Population = population;
IsDark = CalculateYiqLuma(color) < 128;
}
public Color Color { get; private set; }
public int Population { get; private set; }
public bool IsDark { get; private set; }
public int CalculateYiqLuma(Color color)
{
return Convert.ToInt32(Math.Round((299 * color.R + 587 * color.G + 114 * color.B) / 1000f));
}
}
/// <summary>
/// 3D color space box.
/// </summary>
internal class VBox
{
private readonly int[] histo;
private int[] avg;
public int B1;
public int B2;
private int? count;
public int G1;
public int G2;
public int R1;
public int R2;
private int? volume;
public VBox(int r1, int r2, int g1, int g2, int b1, int b2, int[] histo)
{
R1 = r1;
R2 = r2;
G1 = g1;
G2 = g2;
B1 = b1;
B2 = b2;
this.histo = histo;
}
public int Volume(bool force)
{
if (volume == null || force)
{
volume = (R2 - R1 + 1) * (G2 - G1 + 1) * (B2 - B1 + 1);
}
return volume.Value;
}
public int Count(bool force)
{
if (count == null || force)
{
var npix = 0;
int i;
for (i = R1; i <= R2; i++)
{
int j;
for (j = G1; j <= G2; j++)
{
int k;
for (k = B1; k <= B2; k++)
{
var index = Mmcq.GetColorIndex(i, j, k);
npix += histo[index];
}
}
}
count = npix;
}
return count.Value;
}
public VBox Clone()
{
return new VBox(R1, R2, G1, G2, B1, B2, histo);
}
public int[] Avg(bool force)
{
if (avg == null || force)
{
var ntot = 0;
var rsum = 0;
var gsum = 0;
var bsum = 0;
int i;
for (i = R1; i <= R2; i++)
{
int j;
for (j = G1; j <= G2; j++)
{
int k;
for (k = B1; k <= B2; k++)
{
var histoindex = Mmcq.GetColorIndex(i, j, k);
var hval = histo[histoindex];
ntot += hval;
rsum += Convert.ToInt32((hval * (i + 0.5) * Mmcq.Mult));
gsum += Convert.ToInt32((hval * (j + 0.5) * Mmcq.Mult));
bsum += Convert.ToInt32((hval * (k + 0.5) * Mmcq.Mult));
}
}
}
if (ntot > 0)
{
avg = new[]
{
Math.Abs(rsum / ntot), Math.Abs(gsum / ntot),
Math.Abs(bsum / ntot)
};
}
else
{
avg = new[]
{
Math.Abs(Mmcq.Mult * (R1 + R2 + 1) / 2),
Math.Abs(Mmcq.Mult * (G1 + G2 + 1) / 2),
Math.Abs(Mmcq.Mult * (B1 + B2 + 1) / 2)
};
}
}
return avg;
}
public bool Contains(int[] pixel)
{
var rval = pixel[0] >> Mmcq.Rshift;
var gval = pixel[1] >> Mmcq.Rshift;
var bval = pixel[2] >> Mmcq.Rshift;
return rval >= R1 && rval <= R2 && gval >= G1 && gval <= G2 && bval >= B1 && bval <= B2;
}
}
internal class VBoxCountComparer : IComparer<VBox>
{
public int Compare(VBox x, VBox y)
{
var a = x.Count(false);
var b = y.Count(false);
return a < b ? -1 : (a > b ? 1 : 0);
}
}
internal class VBoxComparer : IComparer<VBox>
{
public int Compare(VBox x, VBox y)
{
var aCount = x.Count(false);
var bCount = y.Count(false);
var aVolume = x.Volume(false);
var bVolume = y.Volume(false);
// Otherwise sort by products
var a = aCount * aVolume;
var b = bCount * bVolume;
return a < b ? -1 : (a > b ? 1 : 0);
}
}
public class ColorThief
{
public const int DefaultColorCount = 5;
public const int DefaultQuality = 10;
public const bool DefaultIgnoreWhite = true;
public const int ColorDepth = 4;
private CMap GetColorMap(byte[][] pixelArray, int colorCount)
{
// Send array to quantize function which clusters values using median
// cut algorithm
if (colorCount > 0)
{
--colorCount;
}
var cmap = Mmcq.Quantize(pixelArray, colorCount);
return cmap;
}
private byte[][] ConvertPixels(byte[] pixels, int pixelCount, int quality, bool ignoreWhite)
{
var expectedDataLength = pixelCount * ColorDepth;
if (expectedDataLength != pixels.Length)
{
throw new ArgumentException("(expectedDataLength = "
+ expectedDataLength + ") != (pixels.length = "
+ pixels.Length + ")");
}
// Store the RGB values in an array format suitable for quantize
// function
// numRegardedPixels must be rounded up to avoid an
// ArrayIndexOutOfBoundsException if all pixels are good.
var numRegardedPixels = (pixelCount + quality - 1) / quality;
var numUsedPixels = 0;
var pixelArray = new byte[numRegardedPixels][];
for (var i = 0; i < pixelCount; i += quality)
{
var offset = i * ColorDepth;
var b = pixels[offset];
var g = pixels[offset + 1];
var r = pixels[offset + 2];
var a = pixels[offset + 3];
// If pixel is mostly opaque and not white
if (a >= 125 && !(ignoreWhite && r > 250 && g > 250 && b > 250))
{
pixelArray[numUsedPixels] = new[] { r, g, b };
numUsedPixels++;
}
}
// Remove unused pixels from the array
var copy = new byte[numUsedPixels][];
Array.Copy(pixelArray, copy, numUsedPixels);
return copy;
}
/// <summary>
/// Use the median cut algorithm to cluster similar colors and return the base color from the largest cluster.
/// </summary>
/// <param name="sourceImage">The source image.</param>
/// <param name="quality">
/// 1 is the highest quality settings. 10 is the default. There is
/// a trade-off between quality and speed. The bigger the number,
/// the faster a color will be returned but the greater the
/// likelihood that it will not be the visually most dominant color.
/// </param>
/// <param name="ignoreWhite">if set to <c>true</c> [ignore white].</param>
/// <returns></returns>
public async Task<QuantizedColor> GetColor(BitmapDecoder sourceImage, int quality = DefaultQuality, bool ignoreWhite = DefaultIgnoreWhite)
{
var palette = await GetPalette(sourceImage, 3, quality, ignoreWhite);
var dominantColor = new QuantizedColor(new Color
{
A = Convert.ToByte(palette.Average(a => a.Color.A)),
R = Convert.ToByte(palette.Average(a => a.Color.R)),
G = Convert.ToByte(palette.Average(a => a.Color.G)),
B = Convert.ToByte(palette.Average(a => a.Color.B))
}, Convert.ToInt32(palette.Average(a => a.Population)));
return dominantColor;
}
/// <summary>
/// Use the median cut algorithm to cluster similar colors.
/// </summary>
/// <param name="sourceImage">The source image.</param>
/// <param name="colorCount">The color count.</param>
/// <param name="quality">
/// 1 is the highest quality settings. 10 is the default. There is
/// a trade-off between quality and speed. The bigger the number,
/// the faster a color will be returned but the greater the
/// likelihood that it will not be the visually most dominant color.
/// </param>
/// <param name="ignoreWhite">if set to <c>true</c> [ignore white].</param>
/// <returns></returns>
/// <code>true</code>
public async Task<List<QuantizedColor>> GetPalette(BitmapDecoder sourceImage, int colorCount = DefaultColorCount, int quality = DefaultQuality, bool ignoreWhite = DefaultIgnoreWhite)
{
var pixelArray = await GetPixelsFast(sourceImage, quality, ignoreWhite);
var cmap = GetColorMap(pixelArray, colorCount);
if (cmap != null)
{
var colors = cmap.GeneratePalette();
return colors;
}
return new List<QuantizedColor>();
}
private async Task<byte[]> GetIntFromPixel(BitmapDecoder decoder)
{
var pixelsData = await decoder.GetPixelDataAsync();
var pixels = pixelsData.DetachPixelData();
return pixels;
}
private async Task<byte[][]> GetPixelsFast(BitmapDecoder sourceImage, int quality, bool ignoreWhite)
{
if (quality < 1)
{
quality = DefaultQuality;
}
var pixels = await GetIntFromPixel(sourceImage);
var pixelCount = sourceImage.PixelWidth * sourceImage.PixelHeight;
return ConvertPixels(pixels, Convert.ToInt32(pixelCount), quality, ignoreWhite);
}
}
}

View File

@@ -0,0 +1,21 @@
using BetterLyrics.WinUI3.Models;
using SQLite;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper {
public class DatabaseHelper {
private static SQLiteConnection _database;
public static SQLiteConnection InitializeDatabase() {
string dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MusicMetadataIndex.db");
_database = new SQLiteConnection(dbPath);
_database.CreateTable<MetadataIndex>(); // Create table if it doesn't exist
return _database;
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper {
public class EasingHelper {
/// <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;
}
/// <summary>
/// Smoother transition than linear
/// </summary>
public static float SmoothStep(float t) {
return t * t * (3 - 2 * 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

@@ -0,0 +1,26 @@
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
namespace BetterLyrics.WinUI3.Helper
{
public class ImageHelper
{
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());
stream.Seek(0);
return stream;
}
}
}

View File

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,23 @@
using DevWinUI;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml.Media;
namespace BetterLyrics.WinUI3.Helper
{
public class SystemBackdropHelper
{
public static SystemBackdrop? CreateSystemBackdrop(BackdropType backdropType) {
return backdropType switch {
BackdropType.None => null,
BackdropType.Mica => new MicaSystemBackdrop(MicaKind.Base),
BackdropType.MicaAlt => new MicaSystemBackdrop(MicaKind.BaseAlt),
BackdropType.DesktopAcrylic => new DesktopAcrylicBackdrop(),
BackdropType.AcrylicThin => new AcrylicSystemBackdrop(DesktopAcrylicKind.Thin),
BackdropType.AcrylicBase => new AcrylicSystemBackdrop(DesktopAcrylicKind.Base),
BackdropType.Transparent => new TransparentBackdrop(),
_ => null,
};
}
}
}

View File

@@ -0,0 +1,38 @@
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Helper {
public class VisualHelper {
/// <summary>
/// Source: https://stackoverflow.com/a/61626933/11048731
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="depObj"></param>
/// <returns></returns>
public static List<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject {
List<T> list = new List<T>();
if (depObj != null) {
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++) {
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T) {
list.Add((T)child);
}
List<T> childItems = FindVisualChildren<T>(child);
if (childItems != null && childItems.Count() > 0) {
foreach (var item in childItems) {
list.Add(item);
}
}
}
}
return list;
}
}
}

View File

@@ -0,0 +1,191 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="BetterLyrics.WinUI3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:media="using:CommunityToolkit.WinUI.Media"
SizeChanged="Window_SizeChanged"
mc:Ignorable="d">
<Grid x:Name="RootGrid">
<Frame
x:Name="RootFrame"
Navigated="RootFrame_Navigated"
NavigationFailed="RootFrame_NavigationFailed" />
<Grid
x:Name="TopCommandGrid"
Padding="2,0"
VerticalAlignment="Top"
Background="Transparent"
Opacity="0">
<Grid.Resources>
<Storyboard x:Name="TopCommandGridFadeInStoryboard">
<DoubleAnimation
Storyboard.TargetName="TopCommandGrid"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Name="TopCommandGridFadeOutStoryboard">
<DoubleAnimation
Storyboard.TargetName="TopCommandGrid"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</Grid.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:EventTriggerBehavior EventName="PointerEntered">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TopCommandGridFadeInStoryboard}" />
</interactivity:EventTriggerBehavior>
<interactivity:EventTriggerBehavior EventName="PointerExited">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TopCommandGridFadeOutStoryboard}" />
</interactivity:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<!-- Always On Top -->
<AppBarButton
x:Name="AOTButton"
Click="AOTButton_Click"
LabelPosition="Collapsed">
<Grid>
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE840;" />
<FontIcon
x:Name="PinnedFontIcon"
FontFamily="Segoe Fluent Icons"
Glyph="&#xE841;"
Opacity="0">
<FontIcon.Resources>
<Storyboard x:Key="ShowPinnedFontIconStoryboard">
<DoubleAnimation
Storyboard.TargetName="PinnedFontIcon"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.3" />
</Storyboard>
<Storyboard x:Key="HidePinnedFontIconStoryboard">
<DoubleAnimation
Storyboard.TargetName="PinnedFontIcon"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.3" />
</Storyboard>
</FontIcon.Resources>
</FontIcon>
</Grid>
</AppBarButton>
<TextBlock
x:Name="AppTitleTextBlock"
Margin="0,-4,0,0"
VerticalAlignment="Center"
Text="{x:Bind Title}" />
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<!-- Window Mini -->
<AppBarButton
x:Name="MiniButton"
Click="MiniButton_Click"
LabelPosition="Collapsed"
Visibility="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xEE49;" />
</AppBarButton>
<!-- Window Unmini -->
<AppBarButton
x:Name="UnminiButton"
Click="UnminiButton_Click"
LabelPosition="Collapsed"
Visibility="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xEE47;" />
</AppBarButton>
<!-- Window Minimise -->
<AppBarButton
x:Name="MinimiseButton"
Click="MinimiseButton_Click"
LabelPosition="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE921;" />
</AppBarButton>
<!-- Window Maximise -->
<AppBarButton
x:Name="MaximiseButton"
Click="MaximiseButton_Click"
LabelPosition="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE922;" />
</AppBarButton>
<!-- Window Restore -->
<AppBarButton
x:Name="RestoreButton"
Click="RestoreButton_Click"
LabelPosition="Collapsed"
Visibility="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE923;" />
</AppBarButton>
<!-- Window Close -->
<AppBarButton
x:Name="CloseButton"
Click="CloseButton_Click"
LabelPosition="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE8BB;" />
</AppBarButton>
</StackPanel>
</Grid>
<InfoBar
x:Name="HostInfoBar"
Margin="18"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
IsClosable="False"
Opacity="0">
<InfoBar.RenderTransform>
<TranslateTransform x:Name="HostInfoBarTransform" Y="20" />
</InfoBar.RenderTransform>
<InfoBar.Resources>
<Storyboard x:Key="InfoBarShowAndHideStoryboard">
<!-- Opacity -->
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="HostInfoBar" Storyboard.TargetProperty="Opacity">
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:3.6" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:3.9" Value="0" />
</DoubleAnimationUsingKeyFrames>
<!-- Y -->
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="HostInfoBarTransform" Storyboard.TargetProperty="Y">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="20" />
<EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:3.6" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:3.9" Value="20" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</InfoBar.Resources>
<interactivity:Interaction.Behaviors>
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
<interactivity:DataTriggerBehavior
Binding="{Binding ElementName=HostInfoBar, Path=IsOpen, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource InfoBarShowAndHideStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</InfoBar>
</Grid>
</Window>

View File

@@ -0,0 +1,152 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.ViewModels;
using BetterLyrics.WinUI3.Views;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.Behaviors;
using DevWinUI;
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using Microsoft.UI.Xaml.Navigation;
using WinRT;
using WinRT.Interop;
// 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>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : Window {
private readonly OverlappedPresenter _presenter;
private SettingsService _settingsService;
public static StackedNotificationsBehavior? StackedNotificationsBehavior { get; private set; }
public MainWindow() {
this.InitializeComponent();
_settingsService = Ioc.Default.GetService<SettingsService>();
RootGrid.RequestedTheme = (ElementTheme)_settingsService.Theme;
SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop((BackdropType)_settingsService.BackdropType);
WeakReferenceMessenger.Default.Register<ThemeChangedMessage>(this, (r, m) => {
RootGrid.RequestedTheme = m.Value;
});
WeakReferenceMessenger.Default.Register<SystemBackdropChangedMessage>(this, (r, m) => {
SystemBackdrop = null;
SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(m.Value);
});
// AppWindow.SetIcon("white_round.ico");
StackedNotificationsBehavior = NotificationQueue;
_presenter = (OverlappedPresenter)AppWindow.Presenter;
ExtendsContentIntoTitleBar = true;
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
SetTitleBar(TopCommandGrid);
}
public void Navigate(Type type) {
RootFrame.Navigate(type);
}
private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e) {
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
private void BackButton_Click(object sender, RoutedEventArgs e) {
if (RootFrame.CanGoBack) {
RootFrame.GoBack();
}
}
private void CloseButton_Click(object sender, RoutedEventArgs e) {
if (RootFrame.CurrentSourcePageType == typeof(MainPage)) {
App.Current.Exit();
} else if (RootFrame.CurrentSourcePageType == typeof(SettingsPage)) {
App.Current.SettingsWindow!.AppWindow.Hide();
}
}
private void MaximiseButton_Click(object sender, RoutedEventArgs e) {
_presenter.Maximize();
//MaximiseButton.Visibility = Visibility.Collapsed;
//RestoreButton.Visibility = Visibility.Visible;
}
private void MinimiseButton_Click(object sender, RoutedEventArgs e) {
_presenter.Minimize();
}
private void RestoreButton_Click(object sender, RoutedEventArgs e) {
_presenter.Restore();
//MaximiseButton.Visibility = Visibility.Visible;
//RestoreButton.Visibility = Visibility.Collapsed;
}
private void Window_SizeChanged(object sender, WindowSizeChangedEventArgs args) {
if (_presenter.State == OverlappedPresenterState.Maximized) {
MaximiseButton.Visibility = Visibility.Collapsed;
RestoreButton.Visibility = Visibility.Visible;
} else if (_presenter.State == OverlappedPresenterState.Restored) {
MaximiseButton.Visibility = Visibility.Visible;
RestoreButton.Visibility = Visibility.Collapsed;
}
}
private void MiniButton_Click(object sender, RoutedEventArgs e) {
AppWindow.Resize(new Windows.Graphics.SizeInt32(144, 48));
MiniButton.Visibility = Visibility.Collapsed;
UnminiButton.Visibility = Visibility.Visible;
MinimiseButton.Visibility = Visibility.Collapsed;
MaximiseButton.Visibility = Visibility.Collapsed;
RestoreButton.Visibility = Visibility.Collapsed;
CloseButton.Visibility = Visibility.Collapsed;
}
private void UnminiButton_Click(object sender, RoutedEventArgs e) {
AppWindow.Resize(new Windows.Graphics.SizeInt32(800, 600));
MiniButton.Visibility = Visibility.Visible;
UnminiButton.Visibility = Visibility.Collapsed;
MinimiseButton.Visibility = Visibility.Visible;
MaximiseButton.Visibility = Visibility.Visible;
RestoreButton.Visibility = Visibility.Collapsed;
CloseButton.Visibility = Visibility.Visible;
}
private void RootFrame_Navigated(object sender, NavigationEventArgs e) {
AppWindow.Title = Title = App.ResourceLoader.GetString($"{e.SourcePageType.Name}Title");
}
private void AOTButton_Click(object sender, RoutedEventArgs e) {
_presenter.IsAlwaysOnTop = !_presenter.IsAlwaysOnTop;
string prefix;
if (_presenter.IsAlwaysOnTop) {
prefix = "Show";
} else {
prefix = "Hide";
}
(PinnedFontIcon.Resources[$"{prefix}PinnedFontIconStoryboard"] as Storyboard)!.Begin();
}
}
}

View File

@@ -0,0 +1,14 @@
using CommunityToolkit.Mvvm.Messaging.Messages;
using DevWinUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Messages {
public class SystemBackdropChangedMessage : ValueChangedMessage<BackdropType> {
public SystemBackdropChangedMessage(BackdropType value) : base(value) {
}
}
}

View File

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

View File

@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models {
public enum Language {
FollowSystem,
English,
SimplifiedChinese,
TraditionalChinese,
}
}

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.Models {
public enum LyricsAlignmentType {
Left,
Center,
Right,
}
}

View File

@@ -0,0 +1,42 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Graphics.Canvas.Text;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Numerics;
using Windows.Foundation;
namespace BetterLyrics.WinUI3.Models {
public class LyricsLine {
public List<string> Texts { get; set; } = [];
public int LanguageIndex { get; set; } = 0;
public string Text => Texts[LanguageIndex];
public int StartPlayingTimestampMs { get; set; }
public int EndPlayingTimestampMs { get; set; }
public LyricsPlayingState PlayingState { get; set; }
public int DurationMs => EndPlayingTimestampMs - StartPlayingTimestampMs;
public float EnteringProgress { get; set; }
public float ExitingProgress { get; set; }
public float PlayingProgress { get; set; }
public Vector2 Position { get; set; }
public Vector2 CenterPosition { get; set; }
public float Scale { get; set; }
public float Opacity { get; set; }
public CanvasTextLayout TextLayout { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models {
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

@@ -0,0 +1,15 @@
using SQLite;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models {
public class MetadataIndex {
[PrimaryKey]
public string Path { get; set; }
public string Title { get; set; }
public string Artist { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Models {
public partial class MusicFolder : ObservableObject {
[ObservableProperty]
private string _path;
public bool IsValid => Directory.Exists(Path);
public MusicFolder(string path) {
Path = path;
}
}
}

View File

@@ -0,0 +1,89 @@
using ATL;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml;
using SQLite;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Ude;
namespace BetterLyrics.WinUI3.Services.Database {
public class DatabaseService {
private readonly SQLiteConnection _connection;
private readonly CharsetDetector _charsetDetector = new();
public DatabaseService() {
string dbPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MusicMetadataIndex.db"
);
_connection = new SQLiteConnection(dbPath);
_connection.CreateTable<MetadataIndex>();
}
public async Task RebuildMusicMetadataIndexDatabaseAsync(IList<MusicFolder> musicFolders) {
await Task.Run(() => {
_connection.DeleteAll<MetadataIndex>();
foreach (var localMusicFolder in musicFolders) {
if (localMusicFolder.IsValid) {
foreach (var file in Directory.GetFiles(localMusicFolder.Path)) {
var fileExtension = Path.GetExtension(file);
var track = new Track(file);
_connection.Insert(new MetadataIndex {
Path = file,
Title = track.Title,
Artist = track.Artist,
});
}
}
}
});
}
public Track? GetMusicMetadata(string? title, string? artist) {
var founds = _connection.Table<MetadataIndex>()
.Where(m => m.Title == title && m.Artist == artist).ToList();
if (founds == null || founds.Count == 0) {
return null;
} else {
var first = new Track(founds[0].Path);
if (founds.Count == 1) {
return first;
} else {
if (first.Lyrics.Exists()) {
return first;
} else {
foreach (var found in founds) {
if (found.Path.EndsWith(".lrc")) {
using (FileStream fs = File.OpenRead(found.Path)) {
_charsetDetector.Feed(fs);
_charsetDetector.DataEnd();
}
string content;
if (_charsetDetector.Charset != null) {
Encoding encoding = Encoding.GetEncoding(_charsetDetector.Charset);
content = File.ReadAllText(found.Path, encoding);
} else {
content = File.ReadAllText(found.Path, Encoding.UTF8);
}
first.Lyrics.ParseLRC(content);
return first;
}
}
return first;
}
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
using BetterLyrics.WinUI3.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.Settings {
public static class SettingsDefaultValues {
public const bool IsFirstRun = true;
// Theme
public const int ThemeType = 0; // Follow system
// Language
public const int Language = 0; // Default
// Music
public const string MusicLibraries = "[]";
// Backdrop
public const int BackdropType = 5; // Acrylic Base
public const bool IsCoverOverlayEnabled = true;
public const bool IsDynamicCoverOverlay = true;
public const int CoverOverlayOpacity = 100; // 1.0
public const int CoverOverlayBlurAmount = 200;
// Lyrics
public const int LyricsAlignmentType = 1; // Center
public const int LyricsBlurAmount = 0;
public const int LyricsVerticalEdgeOpacity = 0; // 0.0
public const float LyricsLineSpacingFactor = 0.5f;
public const int LyricsFontSize = 28;
public const bool IsLyricsGlowEffectEnabled = false;
public const bool IsLyricsDynamicGlowEffectEnabled = false;
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services.Settings {
public static class SettingsKeys {
public const string IsFirstRun = "IsFirstRun";
// Theme
public const string ThemeType = "ThemeType";
// Language
public const string Language = "Language";
// Music
public const string MusicLibraries = "MusicLibraries";
// Backdrop
public const string BackdropType = "BackdropType";
public const string IsCoverOverlayEnabled = "IsCoverOverlayEnabled";
public const string IsDynamicCoverOverlay = "IsDynamicCoverOverlay";
public const string CoverOverlayOpacity = "CoverOverlayOpacity";
public const string CoverOverlayBlurAmount = "CoverOverlayBlurAmount";
// Lyrics
public const string LyricsAlignmentType = "LyricsAlignmentType";
public const string LyricsBlurAmount = "LyricsBlurAmount";
public const string LyricsVerticalEdgeOpacity = "LyricsVerticalEdgeOpacity";
public const string LyricsLineSpacingFactor = "LyricsLineSpacingFactor";
public const string LyricsFontSize = "LyricsFontSize";
public const string IsLyricsGlowEffectEnabled = "IsLyricsGlowEffectEnabled";
public const string IsLyricsDynamicGlowEffectEnabled = "IsLyricsDynamicGlowEffectEnabled";
}
}

View File

@@ -0,0 +1,183 @@
using ATL;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Messages;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using DevWinUI;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Media;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Common;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Windows.Globalization;
using Windows.Storage;
using Windows.System;
namespace BetterLyrics.WinUI3.Services.Settings {
public partial class SettingsService : ObservableObject {
public bool IsFirstRun {
get => Get(SettingsKeys.IsFirstRun, SettingsDefaultValues.IsFirstRun);
set => Set(SettingsKeys.IsFirstRun, value);
}
[ObservableProperty]
private bool _isRebuildingLyricsIndexDatabase;
// Theme
public int Theme {
get => Get(SettingsKeys.ThemeType, SettingsDefaultValues.ThemeType);
set {
Set(SettingsKeys.ThemeType, value);
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage((ElementTheme)value));
}
}
// Music
private ObservableCollection<MusicFolder> _musicLibraries;
public ObservableCollection<MusicFolder> MusicLibraries {
get {
if (_musicLibraries == null) {
var list = JsonConvert.DeserializeObject<List<MusicFolder>>(
Get(SettingsKeys.MusicLibraries, SettingsDefaultValues.MusicLibraries)
);
_musicLibraries = new ObservableCollection<MusicFolder>(list);
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
}
return _musicLibraries;
}
set {
if (_musicLibraries != null) {
_musicLibraries.CollectionChanged -= (_, _) => SaveMusicLibraries();
}
_musicLibraries = value;
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
SaveMusicLibraries();
OnPropertyChanged();
}
}
private void SaveMusicLibraries() {
Set(SettingsKeys.MusicLibraries, JsonConvert.SerializeObject(MusicLibraries.ToList()));
}
// Language
public int Language {
get => Get(SettingsKeys.Language, SettingsDefaultValues.Language);
set {
Set(SettingsKeys.Language, value);
switch ((Models.Language)Language) {
case Models.Language.FollowSystem:
ApplicationLanguages.PrimaryLanguageOverride = "";
break;
case Models.Language.English:
ApplicationLanguages.PrimaryLanguageOverride = "en-US";
break;
case Models.Language.SimplifiedChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
break;
case Models.Language.TraditionalChinese:
ApplicationLanguages.PrimaryLanguageOverride = "zh-TW";
break;
default:
break;
}
}
}
// Backdrop
public int BackdropType {
get => Get(SettingsKeys.BackdropType, SettingsDefaultValues.BackdropType);
set {
Set(SettingsKeys.BackdropType, value);
WeakReferenceMessenger.Default.Send(new SystemBackdropChangedMessage((BackdropType)value));
}
}
public bool IsCoverOverlayEnabled {
get => Get(SettingsKeys.IsCoverOverlayEnabled, SettingsDefaultValues.IsCoverOverlayEnabled);
set => Set(SettingsKeys.IsCoverOverlayEnabled, value);
}
public bool IsDynamicCoverOverlay {
get => Get(SettingsKeys.IsDynamicCoverOverlay, SettingsDefaultValues.IsDynamicCoverOverlay);
set => Set(SettingsKeys.IsDynamicCoverOverlay, value);
}
public int CoverOverlayOpacity {
get => Get(SettingsKeys.CoverOverlayOpacity, SettingsDefaultValues.CoverOverlayOpacity);
set => Set(SettingsKeys.CoverOverlayOpacity, value);
}
public int CoverOverlayBlurAmount {
get => Get(SettingsKeys.CoverOverlayBlurAmount, SettingsDefaultValues.CoverOverlayBlurAmount);
set => Set(SettingsKeys.CoverOverlayBlurAmount, value);
}
// Lyrics
public int LyricsAlignmentType {
get => Get(SettingsKeys.LyricsAlignmentType, SettingsDefaultValues.LyricsAlignmentType);
set => Set(SettingsKeys.LyricsAlignmentType, value);
}
public int LyricsBlurAmount {
get => Get(SettingsKeys.LyricsBlurAmount, SettingsDefaultValues.LyricsBlurAmount);
set => Set(SettingsKeys.LyricsBlurAmount, value);
}
public int LyricsVerticalEdgeOpacity {
get => Get(SettingsKeys.LyricsVerticalEdgeOpacity, SettingsDefaultValues.LyricsVerticalEdgeOpacity);
set => Set(SettingsKeys.LyricsVerticalEdgeOpacity, value);
}
public float LyricsLineSpacingFactor {
get => Get(SettingsKeys.LyricsLineSpacingFactor, SettingsDefaultValues.LyricsLineSpacingFactor);
set => Set(SettingsKeys.LyricsLineSpacingFactor, value);
}
public int LyricsFontSize {
get => Get(SettingsKeys.LyricsFontSize, SettingsDefaultValues.LyricsFontSize);
set => Set(SettingsKeys.LyricsFontSize, value);
}
public bool IsLyricsGlowEffectEnabled {
get => Get(SettingsKeys.IsLyricsGlowEffectEnabled, SettingsDefaultValues.IsLyricsGlowEffectEnabled);
set => Set(SettingsKeys.IsLyricsGlowEffectEnabled, value);
}
public bool IsLyricsDynamicGlowEffectEnabled {
get => Get(SettingsKeys.IsLyricsDynamicGlowEffectEnabled, SettingsDefaultValues.IsLyricsDynamicGlowEffectEnabled);
set => Set(SettingsKeys.IsLyricsDynamicGlowEffectEnabled, value);
}
private readonly ApplicationDataContainer _localSettings;
private readonly DatabaseService _databaseService;
public SettingsService(DatabaseService databaseService) {
_localSettings = ApplicationData.Current.LocalSettings;
_databaseService = databaseService;
}
private T Get<T>(string key, T defaultValue = default) {
if (_localSettings.Values.TryGetValue(key, out object value)) {
return (T)Convert.ChangeType(value, typeof(T));
}
return defaultValue;
}
private void Set<T>(string key, T value, [CallerMemberName] string propertyName = null) {
_localSettings.Values[key] = value;
OnPropertyChanged(propertyName);
}
}
}

View File

@@ -0,0 +1,342 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>Local music libraries</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>Add folders storing music or lyrics to build lyrics index database</value>
</data>
<data name="SettingsPageOpenPath.Content" xml:space="preserve">
<value>Open in file explorer</value>
</data>
<data name="SettingsPageRemovePath.Content" xml:space="preserve">
<value>Remove from app</value>
</data>
<data name="SettingsPageRemoveInfo.Title" xml:space="preserve">
<value>You are safe to remove the following items</value>
</data>
<data name="SettingsPageRemoveInfo.Message" xml:space="preserve">
<value>Original files and folders in this path will not be deleted when removing it from this app</value>
</data>
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
<value>Add a folder</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>Theme</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>Language</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>Follow system</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>Light</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>Dark</value>
</data>
<data name="SettingsPageSC.Content" xml:space="preserve">
<value>简体中文</value>
</data>
<data name="SettingsPageTC.Content" xml:space="preserve">
<value>繁體中文</value>
</data>
<data name="SettingsPageEN.Content" xml:space="preserve">
<value>English</value>
</data>
<data name="SettingsPageGitHub.Header" xml:space="preserve">
<value>This is an open source app</value>
</data>
<data name="SettingsPageGitHub.ActionIconToolTip" xml:space="preserve">
<value>Open in new window</value>
</data>
<data name="SettingsPageGitHub.Description" xml:space="preserve">
<value>See source code on GitHub</value>
</data>
<data name="SettingsPageVersion.Text" xml:space="preserve">
<value>Version</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>None</value>
</data>
<data name="SettingsPageMica.Content" xml:space="preserve">
<value>Mica</value>
</data>
<data name="SettingsPageMicaAlt.Content" xml:space="preserve">
<value>Mica Alt</value>
</data>
<data name="SettingsPageDesktopAcrylic.Content" xml:space="preserve">
<value>Desktop Acrylic</value>
</data>
<data name="SettingsPageAcrylicBase.Content" xml:space="preserve">
<value>Acrylic Base</value>
</data>
<data name="SettingsPageAcrylicThin.Content" xml:space="preserve">
<value>Acrylic Thin</value>
</data>
<data name="SettingsPageTransparent.Content" xml:space="preserve">
<value>Transparent</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>Backdrop</value>
</data>
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
<value>Default</value>
</data>
<data name="SettingsPageRestart.Content" xml:space="preserve">
<value>Restart app to apply change</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>The path cannot be found on your computer</value>
</data>
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
<value>The folder has been added. Please do not add it again.</value>
</data>
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
<value>Overlay album art background</value>
</data>
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
<value>Dynamic album art background</value>
</data>
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
<value>Album art background opacity</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>Settings</value>
</data>
<data name="MainPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>Lyrics style</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>Alignment</value>
</data>
<data name="SettingsPageLyricsCenter.Content" xml:space="preserve">
<value>Center</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>Left</value>
</data>
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
<value>Right</value>
</data>
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
<value>Album art background blur amount</value>
</data>
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
<value>Blur amount</value>
</data>
<data name="SettingsPageLyricsBlurAmountSideEffect.Text" xml:space="preserve">
<value>Adjusting this value will also increase the background blur intensity of the album image.</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>Current value: </value>
</data>
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>Significantly higher GPU usage when blur is enabled (&gt; 0)</value>
</data>
<data name="SettingsPageCoverOverlayGPUUsage.Text" xml:space="preserve">
<value>Enabling this feature will slightly increase GPU utilization</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>Top and bottom edge opacity</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>Line spacing</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value>x line height</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>Font size</value>
</data>
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>Show lyrics only</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>Lyrics effect</value>
</data>
<data name="SettingsPageAlbum.Text" xml:space="preserve">
<value>Album background</value>
</data>
<data name="SettingsPageAbout.Text" xml:space="preserve">
<value>About</value>
</data>
<data name="SettingsPageLyricsLib.Text" xml:space="preserve">
<value>Lyrics library</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>App appearance</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>Glow effect</value>
</data>
<data name="SettingsPageLyricsDynamicGlowEffect.Header" xml:space="preserve">
<value>Dynamic glow effect</value>
</data>
<data name="SettingsPageRebuildDatabase.Header" xml:space="preserve">
<value>Rebuild lyrics index database</value>
</data>
<data name="SettingsPageRebuildDatabaseButton.Content" xml:space="preserve">
<value>Rebuild</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>Add</value>
</data>
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
<value>Rebuilding the database, please wait...</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>Let's get started now</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>Welcome to BetterLyrics</value>
</data>
<data name="MainPageTitleBarTeachingTip.Title" xml:space="preserve">
<value>The top area is the title bar</value>
</data>
<data name="MainPageTitleBarTeachingTip.Subtitle" xml:space="preserve">
<value>Hover on the top area to show it</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Title" xml:space="preserve">
<value>Setup lyrics database now</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Subtitle" xml:space="preserve">
<value>Hover on the bottom left corner and then click to open settings page</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Subtitle" xml:space="preserve">
<value>Hover on the bottom area to show it</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Title" xml:space="preserve">
<value>The bottom area is the command area</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Title" xml:space="preserve">
<value>Toggle "show lyrics only" here</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Subtitle" xml:space="preserve">
<value>When lyrics exist, you can switch them in the lower right corner</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>No music playing now</value>
</data>
</root>

View File

@@ -0,0 +1,342 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>本地音乐媒体库</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>添加存放音乐或歌词的文件夹以构建歌词索引数据库</value>
</data>
<data name="SettingsPageOpenPath.Content" xml:space="preserve">
<value>在文件资源管理器中打开</value>
</data>
<data name="SettingsPageRemovePath.Content" xml:space="preserve">
<value>从应用中移除</value>
</data>
<data name="SettingsPageRemoveInfo.Title" xml:space="preserve">
<value>您可以安全删除以下项目</value>
</data>
<data name="SettingsPageRemoveInfo.Message" xml:space="preserve">
<value>路径中的原始文件和文件夹不会被删除</value>
</data>
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
<value>添加文件夹</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>主题</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>语言</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>跟随系统</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>浅色</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>深色</value>
</data>
<data name="SettingsPageSC.Content" xml:space="preserve">
<value>简体中文</value>
</data>
<data name="SettingsPageTC.Content" xml:space="preserve">
<value>繁體中文</value>
</data>
<data name="SettingsPageEN.Content" xml:space="preserve">
<value>English</value>
</data>
<data name="SettingsPageGitHub.Header" xml:space="preserve">
<value>此应用已开源</value>
</data>
<data name="SettingsPageGitHub.ActionIconToolTip" xml:space="preserve">
<value>在新窗口中打开</value>
</data>
<data name="SettingsPageGitHub.Description" xml:space="preserve">
<value>在 GitHub 上查看源代码</value>
</data>
<data name="SettingsPageVersion.Text" xml:space="preserve">
<value>版本号</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>无</value>
</data>
<data name="SettingsPageMica.Content" xml:space="preserve">
<value>云母</value>
</data>
<data name="SettingsPageMicaAlt.Content" xml:space="preserve">
<value>云母(替代样式)</value>
</data>
<data name="SettingsPageDesktopAcrylic.Content" xml:space="preserve">
<value>亚克力(桌面)</value>
</data>
<data name="SettingsPageAcrylicBase.Content" xml:space="preserve">
<value>亚克力(基础)</value>
</data>
<data name="SettingsPageAcrylicThin.Content" xml:space="preserve">
<value>亚克力(薄层)</value>
</data>
<data name="SettingsPageTransparent.Content" xml:space="preserve">
<value>透明</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>背景材质</value>
</data>
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
<value>默认</value>
</data>
<data name="SettingsPageRestart.Content" xml:space="preserve">
<value>重启应用以应用更改</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>无法在您的计算机中找到该路径</value>
</data>
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
<value>已添加过该文件夹,请勿重复添加</value>
</data>
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
<value>叠加专辑图片背景</value>
</data>
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
<value>动态专辑图片背景</value>
</data>
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
<value>专辑图片背景不透明度</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>设置</value>
</data>
<data name="MainPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>歌词样式</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>对齐方式</value>
</data>
<data name="SettingsPageLyricsCenter.Content" xml:space="preserve">
<value>居中</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>靠左</value>
</data>
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
<value>靠右</value>
</data>
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
<value>专辑图片背景模糊度</value>
</data>
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
<value>模糊度</value>
</data>
<data name="SettingsPageLyricsBlurAmountSideEffect.Text" xml:space="preserve">
<value>调整该数值将同步提高专辑图片背景模糊强度</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>当前值:</value>
</data>
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>启用模糊(&gt; 0时将显著提升 GPU 占用率</value>
</data>
<data name="SettingsPageCoverOverlayGPUUsage.Text" xml:space="preserve">
<value>启用该功能将略微提升 GPU 占用率</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>上下边缘不透明度</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>行间距</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value> 倍行高</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>字体大小</value>
</data>
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>仅展示歌词</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌词效果</value>
</data>
<data name="SettingsPageAlbum.Text" xml:space="preserve">
<value>专辑背景</value>
</data>
<data name="SettingsPageAbout.Text" xml:space="preserve">
<value>关于</value>
</data>
<data name="SettingsPageLyricsLib.Text" xml:space="preserve">
<value>歌词库</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>应用外观</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>辉光效果</value>
</data>
<data name="SettingsPageLyricsDynamicGlowEffect.Header" xml:space="preserve">
<value>动态辉光效果</value>
</data>
<data name="SettingsPageRebuildDatabase.Header" xml:space="preserve">
<value>重构歌词索引数据库</value>
</data>
<data name="SettingsPageRebuildDatabaseButton.Content" xml:space="preserve">
<value>重构</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>添加</value>
</data>
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
<value>重构数据库中,请稍候...</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>来看看怎么使用这款应用吧</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>欢迎使用 BetterLyrics</value>
</data>
<data name="MainPageTitleBarTeachingTip.Title" xml:space="preserve">
<value>顶部区域是标题栏</value>
</data>
<data name="MainPageTitleBarTeachingTip.Subtitle" xml:space="preserve">
<value>悬停在顶部区域以显示</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Title" xml:space="preserve">
<value>现在就来初始化歌词数据库吧</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Subtitle" xml:space="preserve">
<value>悬停在左下角后单击以进入设置页面</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Subtitle" xml:space="preserve">
<value>悬停在底部区域以显示</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Title" xml:space="preserve">
<value>底部区域是命令栏</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Title" xml:space="preserve">
<value>在此处切换“仅展示歌词”</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Subtitle" xml:space="preserve">
<value>当歌词存在时可在右下角进行切换</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>当前没有正在播放的音乐</value>
</data>
</root>

View File

@@ -0,0 +1,342 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
<value>本地音樂媒體庫</value>
</data>
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
<value>新增存放音樂或歌詞的資料夾以建立歌詞索引資料庫</value>
</data>
<data name="SettingsPageOpenPath.Content" xml:space="preserve">
<value>在檔案總管中開啟</value>
</data>
<data name="SettingsPageRemovePath.Content" xml:space="preserve">
<value>從應用程式中移除</value>
</data>
<data name="SettingsPageRemoveInfo.Title" xml:space="preserve">
<value>您可以安全刪除以下項目</value>
</data>
<data name="SettingsPageRemoveInfo.Message" xml:space="preserve">
<value>路徑中的原始檔案和資料夾不會被刪除</value>
</data>
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
<value>新增資料夾</value>
</data>
<data name="SettingsPageTheme.Header" xml:space="preserve">
<value>主題</value>
</data>
<data name="SettingsPageLanguage.Header" xml:space="preserve">
<value>語言</value>
</data>
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
<value>跟隨系統</value>
</data>
<data name="SettingsPageLight.Content" xml:space="preserve">
<value>淺色</value>
</data>
<data name="SettingsPageDark.Content" xml:space="preserve">
<value>深色</value>
</data>
<data name="SettingsPageSC.Content" xml:space="preserve">
<value>简体中文</value>
</data>
<data name="SettingsPageTC.Content" xml:space="preserve">
<value>繁體中文</value>
</data>
<data name="SettingsPageEN.Content" xml:space="preserve">
<value>English</value>
</data>
<data name="SettingsPageGitHub.Header" xml:space="preserve">
<value>此應用程式已開源</value>
</data>
<data name="SettingsPageGitHub.ActionIconToolTip" xml:space="preserve">
<value>在新視窗中開啟</value>
</data>
<data name="SettingsPageGitHub.Description" xml:space="preserve">
<value>在 GitHub 上查看原始碼</value>
</data>
<data name="SettingsPageVersion.Text" xml:space="preserve">
<value>版本號</value>
</data>
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
<value>無</value>
</data>
<data name="SettingsPageMica.Content" xml:space="preserve">
<value>雲母</value>
</data>
<data name="SettingsPageMicaAlt.Content" xml:space="preserve">
<value>雲母(替代樣式)</value>
</data>
<data name="SettingsPageDesktopAcrylic.Content" xml:space="preserve">
<value>亞克力(桌面)</value>
</data>
<data name="SettingsPageAcrylicBase.Content" xml:space="preserve">
<value>亞克力(基礎)</value>
</data>
<data name="SettingsPageAcrylicThin.Content" xml:space="preserve">
<value>亞克力(薄層)</value>
</data>
<data name="SettingsPageTransparent.Content" xml:space="preserve">
<value>透明</value>
</data>
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
<value>背景材質</value>
</data>
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
<value>預設</value>
</data>
<data name="SettingsPageRestart.Content" xml:space="preserve">
<value>重啟應用程式以應用更改</value>
</data>
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
<value>無法在您的電腦中找到該路徑</value>
</data>
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
<value>已新增過該資料夾,請勿重複新增</value>
</data>
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
<value>疊加專輯圖片背景</value>
</data>
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
<value>動態專輯圖片背景</value>
</data>
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
<value>專輯圖片背景不透明度</value>
</data>
<data name="SettingsPageTitle" xml:space="preserve">
<value>設定</value>
</data>
<data name="MainPageTitle" xml:space="preserve">
<value>BetterLyrics</value>
</data>
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
<value>歌詞樣式</value>
</data>
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
<value>對齊方式</value>
</data>
<data name="SettingsPageLyricsCenter.Content" xml:space="preserve">
<value>居中</value>
</data>
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
<value>靠左</value>
</data>
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
<value>靠右</value>
</data>
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
<value>專輯圖片背景模糊度</value>
</data>
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
<value>模糊度</value>
</data>
<data name="SettingsPageLyricsBlurAmountSideEffect.Text" xml:space="preserve">
<value>調整此數值將同步提升專輯圖片背景模糊強度</value>
</data>
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
<value>目前值:</value>
</data>
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
<value>啟用模糊(&gt; 0時將顯著提升 GPU 佔用率</value>
</data>
<data name="SettingsPageCoverOverlayGPUUsage.Text" xml:space="preserve">
<value>啟用此功能將略微提升 GPU 佔用率</value>
</data>
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
<value>上下邊緣不透明度</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
<value>行間距</value>
</data>
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
<value> 倍行高</value>
</data>
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
<value>字體大小</value>
</data>
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
<value>僅展示歌詞</value>
</data>
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
<value>歌詞效果</value>
</data>
<data name="SettingsPageAlbum.Text" xml:space="preserve">
<value>專輯背景</value>
</data>
<data name="SettingsPageAbout.Text" xml:space="preserve">
<value>關於</value>
</data>
<data name="SettingsPageLyricsLib.Text" xml:space="preserve">
<value>歌詞庫</value>
</data>
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
<value>應用外觀</value>
</data>
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
<value>輝光效果</value>
</data>
<data name="SettingsPageLyricsDynamicGlowEffect.Header" xml:space="preserve">
<value>動態輝光效果</value>
</data>
<data name="SettingsPageRebuildDatabase.Header" xml:space="preserve">
<value>重構歌詞索引資料庫</value>
</data>
<data name="SettingsPageRebuildDatabaseButton.Content" xml:space="preserve">
<value>重構</value>
</data>
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
<value>添加</value>
</data>
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
<value>重構資料庫中,請稍候...</value>
</data>
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
<value>來看看怎麼使用這款應用程式吧</value>
</data>
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
<value>歡迎使用 BetterLyrics</value>
</data>
<data name="MainPageTitleBarTeachingTip.Title" xml:space="preserve">
<value>頂部區域是標題欄</value>
</data>
<data name="MainPageTitleBarTeachingTip.Subtitle" xml:space="preserve">
<value>懸停在頂部區域以顯示</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Title" xml:space="preserve">
<value>現在就來初始化歌詞資料庫吧</value>
</data>
<data name="MainPageInitDatabaseTeachingTip.Subtitle" xml:space="preserve">
<value>懸停在左下角後點擊以進入設定頁面</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Subtitle" xml:space="preserve">
<value>懸停在底部區域以顯示</value>
</data>
<data name="MainPageBottomCommandTeachingTip.Title" xml:space="preserve">
<value>底部區域是命令列</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Title" xml:space="preserve">
<value>在此切換“僅展示歌詞”</value>
</data>
<data name="MainPageLyricsOnlyTeachingTip.Subtitle" xml:space="preserve">
<value>當歌詞存在時可在右下角進行切換</value>
</data>
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
<value>目前沒有正在播放的音樂</value>
</data>
</root>

View File

@@ -0,0 +1,154 @@
using ATL;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Media.Control;
using Windows.Storage.Streams;
using Windows.UI;
using static ATL.LyricsInfo;
namespace BetterLyrics.WinUI3.ViewModels {
public partial class MainViewModel : ObservableObject {
[ObservableProperty]
private bool _isAnyMusicSessionExisted = false;
[ObservableProperty]
private string? _title;
[ObservableProperty]
private string? _artist;
public List<Color> CoverImageDominantColors { get; set; } =
[Colors.Transparent, Colors.Transparent, Colors.Transparent];
[ObservableProperty]
private BitmapImage? _coverImage;
[ObservableProperty]
private bool _aboutToUpdateUI;
[ObservableProperty]
private bool _isSmallScreenMode;
[ObservableProperty]
private bool _showLyricsOnly = false;
[ObservableProperty]
private bool _lyricsExisted = false;
private Helper.ColorThief _colorThief = new();
private readonly DatabaseService _databaseService;
public MainViewModel(DatabaseService databaseService) {
_databaseService = databaseService;
}
public List<LyricsLine> GetLyrics(Track? track) {
List<LyricsLine> result = [];
var lyricsPhrases = track?.Lyrics.SynchronizedLyrics;
if (lyricsPhrases?.Count > 0) {
if (lyricsPhrases[0].TimestampMs > 0) {
var placeholder = new LyricsPhrase(0, " ");
lyricsPhrases.Insert(0, placeholder);
lyricsPhrases.Insert(0, placeholder);
}
}
LyricsLine? lyricsLine = null;
for (int i = 0; i < lyricsPhrases?.Count; i++) {
var lyricsPhrase = lyricsPhrases[i];
int startTimestampMs = lyricsPhrase.TimestampMs;
int endTimestampMs;
if (i + 1 < lyricsPhrases.Count) {
endTimestampMs = lyricsPhrases[i + 1].TimestampMs;
} else {
endTimestampMs = (int)track.DurationMs;
}
lyricsLine ??= new LyricsLine {
StartPlayingTimestampMs = startTimestampMs,
};
lyricsLine.Texts.Add(lyricsPhrase.Text);
if (endTimestampMs == startTimestampMs) {
continue;
} else {
lyricsLine.EndPlayingTimestampMs = endTimestampMs;
result.Add(lyricsLine);
lyricsLine = null;
}
}
LyricsExisted = result.Count != 0;
return result;
}
public async Task<(List<LyricsLine>, CanvasBitmap?)> SetSongInfoAsync(GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps, ICanvasAnimatedControl control) {
CanvasBitmap? canvasBitmap = null;
Title = mediaProps?.Title;
Artist = mediaProps?.Artist;
IRandomAccessStream? stream = null;
var track = _databaseService.GetMusicMetadata(Title, Artist);
if (mediaProps?.Thumbnail is IRandomAccessStreamReference reference) {
stream = await reference.OpenReadAsync();
} else {
if (track?.EmbeddedPictures.Count > 0) {
var bytes = track.EmbeddedPictures[0].PictureData;
if (bytes != null) {
stream = await Helper.ImageHelper.GetStreamFromBytesAsync(bytes);
}
}
}
// Set cover image and dominant colors
if (stream == null) {
CoverImage = null;
for (int i = 0; i < 3; i++) {
CoverImageDominantColors[i] = Colors.Transparent;
}
} else {
canvasBitmap = await CanvasBitmap.LoadAsync(control, stream);
stream.Seek(0);
CoverImage = new BitmapImage();
await CoverImage.SetSourceAsync(stream);
stream.Seek(0);
var decoder = await BitmapDecoder.CreateAsync(stream);
var quantizedColors = await _colorThief.GetPalette(decoder, 3);
for (int i = 0; i < 3; i++) {
Helper.QuantizedColor quantizedColor = quantizedColors[i];
CoverImageDominantColors[i] = Color.FromArgb(
quantizedColor.Color.A, quantizedColor.Color.R, quantizedColor.Color.G, quantizedColor.Color.B);
}
stream.Dispose();
}
return (GetLyrics(track), canvasBitmap);
}
}
}

View File

@@ -0,0 +1,108 @@
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Database;
using BetterLyrics.WinUI3.Services.Settings;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Xaml;
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Core;
using Windows.Storage.Pickers;
using Windows.System;
using WinRT.Interop;
namespace BetterLyrics.WinUI3.ViewModels {
public partial class SettingsViewModel : ObservableObject {
private readonly DatabaseService _databaseService;
[ObservableProperty]
private SettingsService _settingsService;
[ObservableProperty]
private string _version;
public SettingsViewModel(DatabaseService databaseService, SettingsService settingsService) {
_databaseService = databaseService;
_settingsService = settingsService;
var version = Package.Current.Id.Version;
Version = $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
[RelayCommand]
private async Task RebuildLyricsIndexDatabaseAsync() {
SettingsService.IsRebuildingLyricsIndexDatabase = true;
await _databaseService.RebuildMusicMetadataIndexDatabaseAsync(SettingsService.MusicLibraries);
SettingsService.IsRebuildingLyricsIndexDatabase = false;
}
[RelayCommand]
private async Task RemoveFolderAsync(MusicFolder musicFolder) {
SettingsService.MusicLibraries.Remove(musicFolder);
await RebuildLyricsIndexDatabaseAsync();
}
[RelayCommand]
private async Task AddFolderAsync() {
var picker = new FolderPicker();
picker.FileTypeFilter.Add("*");
var hwnd = WindowNative.GetWindowHandle(App.Current.MainWindow);
InitializeWithWindow.Initialize(picker, hwnd);
var folder = await picker.PickSingleFolderAsync();
if (folder != null) {
string path = folder.Path;
bool existed = SettingsService.MusicLibraries.Count((x) => x.Path == path) > 0;
if (existed) {
MainWindow.StackedNotificationsBehavior?.Show(App.ResourceLoader.GetString("SettingsPagePathExistedInfo"), 3900);
} else {
SettingsService.MusicLibraries.Add(new MusicFolder(path));
await RebuildLyricsIndexDatabaseAsync();
}
}
}
[RelayCommand]
private async Task LaunchProjectGitHubPageAsync() {
await Launcher.LaunchUriAsync(new Uri("https://github.com/jayfunc/BetterLyrics"));
}
[RelayCommand]
private void OpenFolderInFileExplorer(MusicFolder musicFolder) {
Process.Start(new ProcessStartInfo {
FileName = "explorer.exe",
Arguments = musicFolder.Path,
UseShellExecute = true
});
}
[RelayCommand]
private void RestartApp() {
// The restart will be executed immediately.
AppRestartFailureReason failureReason =
Microsoft.Windows.AppLifecycle.AppInstance.Restart("");
// 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;
}
}
}
}

View File

@@ -0,0 +1,517 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="BetterLyrics.WinUI3.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:labs="using:CommunityToolkit.Labs.WinUI.MarqueeTextRns"
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
NavigationCacheMode="Required"
mc:Ignorable="d">
<Grid x:Name="RootGrid" SizeChanged="RootGrid_SizeChanged">
<Grid.Resources>
<Thickness x:Key="TeachingTipDescriptionMargin">0,16,0,0</Thickness>
</Grid.Resources>
<Grid
x:Name="TopPlaceholder"
Height="36"
HorizontalAlignment="Stretch"
VerticalAlignment="Top" />
<Grid x:Name="MainGrid">
<!-- Lyrics area -->
<Grid x:Name="LyricsGrid">
<canvas:CanvasAnimatedControl
x:Name="LyricsCanvas"
Draw="LyricsCanvas_Draw"
Loaded="LyricsCanvas_Loaded"
Update="LyricsCanvas_Update">
<canvas:CanvasAnimatedControl.Resources>
<Storyboard x:Key="LyricsCanvasFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LyricsCanvas" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="LyricsCanvasFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LyricsCanvas" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</canvas:CanvasAnimatedControl.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource LyricsCanvasFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource LyricsCanvasFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</canvas:CanvasAnimatedControl>
</Grid>
<!-- Song info area -->
<Grid x:Name="SongInfoGrid" Margin="36">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="SongInfoColumnDefinition" Width="*" />
<ColumnDefinition x:Name="SpacerColumnDefinition" Width="36" />
<ColumnDefinition x:Name="LyricsAreaColumnDefinition" Width="*" />
</Grid.ColumnDefinitions>
<Grid.Resources>
<Storyboard x:Key="HideSongInfoGrid">
<DoubleAnimation
Storyboard.TargetName="SongInfoGrid"
Storyboard.TargetProperty="Opacity"
From="1"
To="0"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="ShowSongInfoGrid">
<DoubleAnimation
Storyboard.TargetName="SongInfoGrid"
Storyboard.TargetProperty="Opacity"
From="0"
To="1"
Duration="0:0:0.2" />
</Storyboard>
</Grid.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.ShowLyricsOnly, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource HideSongInfoGrid}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.ShowLyricsOnly, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ShowSongInfoGrid}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel
x:Name="SongInfoStackPanel"
Grid.Column="0"
Grid.ColumnSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<StackPanel.Resources>
<!-- Animation for song info -->
<Storyboard x:Name="SongInfoStackPanelFadeInStoryboard">
<DoubleAnimation
Storyboard.TargetName="SongInfoStackPanel"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Name="SongInfoStackPanelFadeOutStoryboard" BeginTime="0:0:0.2">
<DoubleAnimation
Storyboard.TargetName="SongInfoStackPanel"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</StackPanel.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSmallScreenMode, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource SongInfoStackPanelFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsSmallScreenMode, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource SongInfoStackPanelFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<!-- Cover area -->
<Grid
x:Name="CoverGrid"
MaxWidth="300"
MaxHeight="300"
CornerRadius="24">
<Grid.Resources>
<!-- Animation for cover grid -->
<Storyboard x:Key="CoverGridShowStoryboard">
<DoubleAnimation
EnableDependentAnimation="True"
Storyboard.TargetName="CoverGrid"
Storyboard.TargetProperty="Height"
To="300"
Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation
BeginTime="0:0:0.2"
Storyboard.TargetName="CoverGrid"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<Storyboard x:Key="CoverGridHideStoryboard">
<DoubleAnimation
Storyboard.TargetName="CoverGrid"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation
BeginTime="0:0:0.2"
EnableDependentAnimation="True"
Storyboard.TargetName="CoverGrid"
Storyboard.TargetProperty="Height"
To="0"
Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseInOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</Grid.Resources>
<Image x:Name="CoverImage" Source="{x:Bind ViewModel.CoverImage, Mode=OneWay}">
<Image.Resources>
<Storyboard x:Key="CoverIamgeFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="CoverIamgeFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Image.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</Image>
</Grid>
<!-- Song title -->
<controls:OpacityMaskView x:Name="TitleOpacityMaskView" HorizontalAlignment="Center">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<controls:OpacityMaskView.Resources>
<Storyboard x:Key="TitleOpacityMaskViewFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="TitleOpacityMaskViewFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</controls:OpacityMaskView.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<TextBlock
x:Name="TitleTextBlock"
Margin="0,12,0,0"
FontSize="{StaticResource TitleTextBlockFontSize}"
FontWeight="SemiBold"
Text="{x:Bind ViewModel.Title, Mode=OneWay}" />
</controls:OpacityMaskView>
<!-- Song artist -->
<controls:OpacityMaskView x:Name="ArtistOpacityMaskView" HorizontalAlignment="Center">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<controls:OpacityMaskView.Resources>
<Storyboard x:Key="ArtistOpacityMaskViewFadeInStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="ArtistOpacityMaskViewFadeOutStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</controls:OpacityMaskView.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeOutStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeInStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<TextBlock
FontSize="{StaticResource SubtitleTextBlockFontSize}"
FontWeight="SemiBold"
Opacity="0.5"
Text="{x:Bind ViewModel.Artist, Mode=OneWay}" />
</controls:OpacityMaskView>
</StackPanel>
<TextBlock
x:Name="MainPageNoMusicPlayingTextBlock"
x:Uid="MainPageNoMusicPlaying"
Grid.Column="0"
Grid.ColumnSpan="3"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}">
<TextBlock.Resources>
<Storyboard x:Key="ShowMainPageNoMusicPlayingTextBlockStoryboard">
<DoubleAnimation
Storyboard.TargetName="MainPageNoMusicPlayingTextBlock"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="HideMainPageNoMusicPlayingTextBlockStoryboard">
<DoubleAnimation
Storyboard.TargetName="MainPageNoMusicPlayingTextBlock"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</TextBlock.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsAnyMusicSessionExisted, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource HideMainPageNoMusicPlayingTextBlockStoryboard}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.IsAnyMusicSessionExisted, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ShowMainPageNoMusicPlayingTextBlockStoryboard}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</TextBlock>
</Grid>
</Grid>
<Grid
x:Name="BottomCommandGrid"
Padding="2,0"
VerticalAlignment="Bottom"
Background="Transparent"
Opacity="0">
<Grid.Resources>
<Storyboard x:Name="BottomCommandGridFadeInStoryboard">
<DoubleAnimation
Storyboard.TargetName="BottomCommandGrid"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Name="BottomCommandGridFadeOutStoryboard" BeginTime="0:0:0.2">
<DoubleAnimation
Storyboard.TargetName="BottomCommandGrid"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</Grid.Resources>
<interactivity:Interaction.Behaviors>
<interactivity:EventTriggerBehavior EventName="PointerEntered">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource BottomCommandGridFadeInStoryboard}" />
</interactivity:EventTriggerBehavior>
<interactivity:EventTriggerBehavior EventName="PointerExited">
<interactivity:ControlStoryboardAction Storyboard="{StaticResource BottomCommandGridFadeOutStoryboard}" />
</interactivity:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
<AppBarButton
x:Name="SettingsButton"
Click="SettingsButton_Click"
LabelPosition="Collapsed">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xE713;" />
</AppBarButton>
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<AppBarToggleButton
x:Name="LyricsOnlyButton"
x:Uid="MainWindowLyricsOnly"
IsChecked="{x:Bind ViewModel.ShowLyricsOnly, Mode=TwoWay}"
LabelPosition="Collapsed"
Visibility="{x:Bind ViewModel.LyricsExisted, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="&#xF15F;" />
</AppBarToggleButton>
</StackPanel>
</Grid>
<TeachingTip
x:Name="WelcomeTeachingTip"
x:Uid="MainPageWelcomeTeachingTip"
Closed="WelcomeTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind RootGrid}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="1/5" />
</TeachingTip.Content>
</TeachingTip>
<TeachingTip
x:Name="TopCommandTeachingTip"
x:Uid="MainPageTitleBarTeachingTip"
Closed="TopCommandTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind TopPlaceholder}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="2/5" />
</TeachingTip.Content>
</TeachingTip>
<TeachingTip
x:Name="BottomCommandTeachingTip"
x:Uid="MainPageBottomCommandTeachingTip"
Closed="BottomCommandTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind BottomCommandGrid}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="3/5" />
</TeachingTip.Content>
</TeachingTip>
<TeachingTip
x:Name="LyricsOnlyTeachingTip"
x:Uid="MainPageLyricsOnlyTeachingTip"
Closed="LyricsOnlyTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind LyricsOnlyButton}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="4/5" />
</TeachingTip.Content>
</TeachingTip>
<TeachingTip
x:Name="InitDatabaseTeachingTip"
x:Uid="MainPageInitDatabaseTeachingTip"
Closed="InitDatabaseTeachingTip_Closed"
IsOpen="False"
Target="{x:Bind SettingsButton}">
<TeachingTip.Content>
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="5/5" />
</TeachingTip.Content>
</TeachingTip>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<!-- Narrow -->
<VisualState x:Name="NarrowState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="SongInfoColumnDefinition.Width" Value="0" />
</VisualState.Setters>
</VisualState>
<!-- Wide -->
<VisualState x:Name="WideState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="720" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="SongInfoColumnDefinition.Width" Value="*" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</Page>

View File

@@ -0,0 +1,756 @@
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services.Settings;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.WinUI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Media.Control;
using Color = Windows.UI.Color;
using System.Linq;
using Microsoft.UI.Windowing;
// 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.Views {
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page {
public MainViewModel ViewModel => (MainViewModel)DataContext;
private SettingsService _settingsService;
private List<LyricsLine> _lyricsLines = [];
private CanvasBitmap? _canvasCoverBitmap;
private float _coverBitmapRotateAngle = 0f;
private float _coverScaleFactor = 1;
private float _coverRotateSpeed = 0.003f;
private float _lyricsGlowEffectAngle = 0f;
private float _lyricsGlowEffectSpeed = 0.01f;
private float _lyricsGlowEffectMinBlurAmount = 0f;
private float _lyricsGlowEffectMaxBlurAmount = 6f;
private int _animationDurationMs = 200;
private DispatcherQueueTimer _queueTimer;
private TimeSpan _currentTime = TimeSpan.Zero;
private float _defaultOpacity = 0.3f;
private float _highlightedOpacity = 1.0f;
private float _defaultScale = 0.95f;
private float _highlightedScale = 1.0f;
private int _lineEnteringDurationMs = 800;
private int _lineExitingDurationMs = 800;
private int _lineScrollDurationMs = 800;
private float _lastTotalYScroll = 0.0f;
private float _totalYScroll = 0.0f;
private double _lyricsAreaWidth = 0.0f;
private double _lyricsAreaHeight = 0.0f;
private double _lyricsCanvasRightMargin = 36;
private double _lyricsCanvasLeftMargin = 0;
private double _lyricsCanvasMaxTextWidth = 0;
private int _startVisibleLineIndex = -1;
private int _endVisibleLineIndex = -1;
private bool _forceToScroll = false;
private bool _isRelayoutLyricsNeeded = false;
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private GlobalSystemMediaTransportControlsSessionManager? _sessionManager = null;
private GlobalSystemMediaTransportControlsSession? _currentSession = null;
private Color _lyricsColor;
public MainPage() {
this.InitializeComponent();
SetLyricsColor();
_settingsService = Ioc.Default.GetService<SettingsService>()!;
if (_settingsService.IsFirstRun) {
WelcomeTeachingTip.IsOpen = true;
}
_settingsService.PropertyChanged += SettingsService_PropertyChanged;
DataContext = Ioc.Default.GetService<MainViewModel>();
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
_queueTimer = _dispatcherQueue.CreateTimer();
}
private async Task ForceToScrollToCurrentPlayingLineAsync() {
_forceToScroll = true;
await Task.Delay(1);
_forceToScroll = false;
}
private async void SettingsService_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) {
switch (e.PropertyName) {
case nameof(_settingsService.LyricsFontSize):
case nameof(_settingsService.LyricsLineSpacingFactor):
LayoutLyrics();
await ForceToScrollToCurrentPlayingLineAsync();
break;
case nameof(_settingsService.IsRebuildingLyricsIndexDatabase):
if (!_settingsService.IsRebuildingLyricsIndexDatabase) {
CurrentSession_MediaPropertiesChanged(_currentSession, null);
}
break;
case nameof(_settingsService.Theme):
SetLyricsColor();
break;
default:
break;
}
}
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) {
switch (e.PropertyName) {
case nameof(ViewModel.ShowLyricsOnly):
RootGrid_SizeChanged(null, null);
break;
default:
break;
}
}
private void SetLyricsColor() {
_lyricsColor = ((SolidColorBrush)TitleTextBlock.Foreground).Color;
}
private async void InitMediaManager() {
_sessionManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
_sessionManager.CurrentSessionChanged += SessionManager_CurrentSessionChanged;
_sessionManager.SessionsChanged += SessionManager_SessionsChanged;
SessionManager_CurrentSessionChanged(_sessionManager, null);
}
private void CurrentSession_TimelinePropertiesChanged(GlobalSystemMediaTransportControlsSession sender, TimelinePropertiesChangedEventArgs args) {
if (sender == null) {
_currentTime = TimeSpan.Zero;
return;
}
_currentTime = sender.GetTimelineProperties().Position;
// Debug.WriteLine(_currentTime);
}
/// <summary>
/// Note: Non-UI thread
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void CurrentSession_PlaybackInfoChanged(GlobalSystemMediaTransportControlsSession sender, PlaybackInfoChangedEventArgs args) {
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () => {
if (sender == null) {
LyricsCanvas.Paused = true;
return;
}
var playbackState = sender.GetPlaybackInfo().PlaybackStatus;
Debug.WriteLine(playbackState);
switch (playbackState) {
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed:
LyricsCanvas.Paused = true;
break;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Opened:
LyricsCanvas.Paused = true;
break;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Changing:
LyricsCanvas.Paused = true;
break;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Stopped:
LyricsCanvas.Paused = true;
break;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing:
LyricsCanvas.Paused = false;
break;
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Paused:
LyricsCanvas.Paused = true;
break;
default:
break;
}
});
}
private void SessionManager_SessionsChanged(GlobalSystemMediaTransportControlsSessionManager sender, SessionsChangedEventArgs args) {
Debug.WriteLine("SessionManager_SessionsChanged");
}
private void SessionManager_CurrentSessionChanged(GlobalSystemMediaTransportControlsSessionManager sender, CurrentSessionChangedEventArgs args) {
Debug.WriteLine("SessionManager_CurrentSessionChanged");
// Unregister events associated with the previous session
if (_currentSession != null) {
_currentSession.MediaPropertiesChanged -= CurrentSession_MediaPropertiesChanged;
_currentSession.PlaybackInfoChanged -= CurrentSession_PlaybackInfoChanged;
_currentSession.TimelinePropertiesChanged -= CurrentSession_TimelinePropertiesChanged;
}
// Record and register events for current session
_currentSession = sender.GetCurrentSession();
if (_currentSession != null) {
_currentSession.MediaPropertiesChanged += CurrentSession_MediaPropertiesChanged;
_currentSession.PlaybackInfoChanged += CurrentSession_PlaybackInfoChanged;
_currentSession.TimelinePropertiesChanged += CurrentSession_TimelinePropertiesChanged;
}
CurrentSession_MediaPropertiesChanged(_currentSession, null);
}
/// <summary>
/// Note: this func is invoked by non-UI thread
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void CurrentSession_MediaPropertiesChanged(GlobalSystemMediaTransportControlsSession sender, MediaPropertiesChangedEventArgs args) {
_queueTimer.Debounce(() => {
Debug.WriteLine("CurrentSession_MediaPropertiesChanged");
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, async () => {
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps = null;
if (_currentSession != null) {
try {
mediaProps = await _currentSession.TryGetMediaPropertiesAsync();
} catch (Exception) {
}
}
ViewModel.IsAnyMusicSessionExisted = _currentSession != null;
ViewModel.AboutToUpdateUI = true;
await Task.Delay(_animationDurationMs);
(_lyricsLines, _canvasCoverBitmap) = await ViewModel.SetSongInfoAsync(mediaProps, LyricsCanvas);
// Force to show lyrics and scroll to current line even if the music is not playing
LyricsCanvas.Paused = false;
await ForceToScrollToCurrentPlayingLineAsync();
// Detect and recover the music state
CurrentSession_PlaybackInfoChanged(_currentSession, null);
CurrentSession_TimelinePropertiesChanged(_currentSession, null);
ViewModel.AboutToUpdateUI = false;
if (_lyricsLines.Count == 0) {
Grid.SetColumnSpan(SongInfoStackPanel, 3);
} else {
Grid.SetColumnSpan(SongInfoStackPanel, 1);
}
});
}, TimeSpan.FromMilliseconds(200));
}
private async void RootGrid_SizeChanged(object sender, SizeChangedEventArgs e) {
//_queueTimer.Debounce(async () => {
_lyricsAreaHeight = LyricsGrid.ActualHeight;
_lyricsAreaWidth = LyricsGrid.ActualWidth;
if (SongInfoColumnDefinition.ActualWidth == 0 || ViewModel.ShowLyricsOnly) {
_lyricsCanvasLeftMargin = 36;
} else {
_lyricsCanvasLeftMargin = 36 + SongInfoColumnDefinition.ActualWidth + 36;
}
_lyricsCanvasMaxTextWidth = _lyricsAreaWidth - _lyricsCanvasLeftMargin - _lyricsCanvasRightMargin;
LayoutLyrics();
await ForceToScrollToCurrentPlayingLineAsync();
//}, TimeSpan.FromMilliseconds(50));
}
// Comsumes GPU related resources
private void LyricsCanvas_Draw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args) {
using var ds = args.DrawingSession;
var r = _lyricsColor.R;
var g = _lyricsColor.G;
var b = _lyricsColor.B;
// Draw (dynamic) cover image as the very first layer
if (_settingsService.IsCoverOverlayEnabled && _canvasCoverBitmap != null) {
DrawCoverImage(sender, ds, _settingsService.IsDynamicCoverOverlay);
}
// Lyrics only layer
using var lyrics = new CanvasCommandList(sender);
using (var lyricsDs = lyrics.CreateDrawingSession()) {
DrawLyrics(sender, lyricsDs, r, g, b);
}
using var glowedLyrics = new CanvasCommandList(sender);
using (var glowedLyricsDs = glowedLyrics.CreateDrawingSession()) {
if (_settingsService.IsLyricsGlowEffectEnabled) {
glowedLyricsDs.DrawImage(new GaussianBlurEffect {
Source = lyrics,
BlurAmount = MathF.Sin(_lyricsGlowEffectAngle) * (_lyricsGlowEffectMaxBlurAmount - _lyricsGlowEffectMinBlurAmount) / 2f +
(_lyricsGlowEffectMaxBlurAmount + _lyricsGlowEffectMinBlurAmount) / 2f,
BorderMode = EffectBorderMode.Soft,
Optimization = EffectOptimization.Quality,
});
}
glowedLyricsDs.DrawImage(lyrics);
}
// Mock gradient blurred lyrics layer
using var combinedBlurredLyrics = new CanvasCommandList(sender);
using var combinedBlurredLyricsDs = combinedBlurredLyrics.CreateDrawingSession();
if (_settingsService.LyricsBlurAmount == 0) {
combinedBlurredLyricsDs.DrawImage(glowedLyrics);
} else {
double step = 0.05;
double overlapFactor = 0;
for (double i = 0; i <= 0.5 - step; i += step) {
using var blurredLyrics = new GaussianBlurEffect {
Source = glowedLyrics,
BlurAmount = (float)(_settingsService.LyricsBlurAmount * (1 - i / (0.5 - step))),
Optimization = EffectOptimization.Quality,
BorderMode = EffectBorderMode.Soft,
};
using var topCropped = new CropEffect {
Source = blurredLyrics,
SourceRectangle = new Rect(0, sender.Size.Height * i, sender.Size.Width, sender.Size.Height * step * (1 + overlapFactor))
};
using var bottomCropped = new CropEffect {
Source = blurredLyrics,
SourceRectangle = new Rect(0, sender.Size.Height * (1 - i - step * (1 + overlapFactor)), sender.Size.Width, sender.Size.Height * step * (1 + overlapFactor))
};
combinedBlurredLyricsDs.DrawImage(topCropped);
combinedBlurredLyricsDs.DrawImage(bottomCropped);
}
}
// Masked mock gradient blurred lyrics layer
using var maskedCombinedBlurredLyrics = new CanvasCommandList(sender);
using (var maskedCombinedBlurredLyricsDs = maskedCombinedBlurredLyrics.CreateDrawingSession()) {
if (_settingsService.LyricsVerticalEdgeOpacity == 100) {
maskedCombinedBlurredLyricsDs.DrawImage(combinedBlurredLyrics);
} else {
using var mask = new CanvasCommandList(sender);
using (var maskDs = mask.CreateDrawingSession()) {
DrawGradientOpacityMask(sender, maskDs, r, g, b);
}
maskedCombinedBlurredLyricsDs.DrawImage(new AlphaMaskEffect {
Source = combinedBlurredLyrics,
AlphaMask = mask
});
}
}
// Draw the final composed layer
ds.DrawImage(maskedCombinedBlurredLyrics);
}
private void DrawLyrics(ICanvasAnimatedControl control, CanvasDrawingSession ds, byte r, byte g, byte b) {
var (displayStartLineIndex, displayEndLineIndex) = GetVisibleLyricsLineIndexBoundaries();
for (int i = displayStartLineIndex; _lyricsLines.Count > 0 && i >= 0 && i < _lyricsLines.Count && i <= displayEndLineIndex; i++) {
var line = _lyricsLines[i];
if (line.TextLayout == null) {
return;
}
float progressPerChar = 1f / line.Text.Length;
var position = line.Position;
float centerX = position.X;
float centerY = position.Y + (float)line.TextLayout.LayoutBounds.Height / 2;
switch ((LyricsAlignmentType)_settingsService.LyricsAlignmentType) {
case LyricsAlignmentType.Left:
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
break;
case LyricsAlignmentType.Center:
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Center;
centerX += (float)_lyricsCanvasMaxTextWidth / 2;
break;
case LyricsAlignmentType.Right:
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Right;
centerX += (float)_lyricsCanvasMaxTextWidth;
break;
default:
break;
}
int startIndex = 0;
// Set brush
for (int j = 0; j < line.TextLayout.LineCount; j++) {
int count = line.TextLayout.LineMetrics[j].CharacterCount;
var regions = line.TextLayout.GetCharacterRegions(startIndex, count);
float subLinePlayingProgress = Math.Clamp((line.PlayingProgress * line.Text.Length - startIndex) / count, 0, 1);
using var horizontalFillBrush = new CanvasLinearGradientBrush(control, [
new() { Position = 0, Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b) },
new() { Position = subLinePlayingProgress * (1 + progressPerChar) - progressPerChar, Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b) },
new() { Position = subLinePlayingProgress * (1 + progressPerChar), Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b) },
new() { Position = 1.5f, Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b) },
]) {
StartPoint = new Vector2((float)(regions[0].LayoutBounds.Left + position.X), 0),
EndPoint = new Vector2((float)(regions[^1].LayoutBounds.Right + position.X), 0)
};
line.TextLayout.SetBrush(startIndex, count, horizontalFillBrush);
startIndex += count;
}
// Scale
ds.Transform = Matrix3x2.CreateScale(line.Scale, new Vector2(centerX, centerY)) * Matrix3x2.CreateTranslation(0, _totalYScroll);
//Debug.WriteLine(_totalYScroll);
ds.DrawTextLayout(line.TextLayout, position, Colors.Transparent);
// Reset scale
ds.Transform = Matrix3x2.Identity;
}
}
private void DrawCoverImage(ICanvasAnimatedControl control, CanvasDrawingSession ds, bool dynamic) {
if (_settingsService.IsCoverOverlayEnabled && _canvasCoverBitmap != null) {
ds.Transform = Matrix3x2.CreateRotation(_coverBitmapRotateAngle, control.Size.ToVector2() * 0.5f);
using var coverOverlayEffect = new OpacityEffect {
Opacity = _settingsService.CoverOverlayOpacity / 100f,
Source = new GaussianBlurEffect {
BlurAmount = _settingsService.CoverOverlayBlurAmount,
Source = new ScaleEffect {
InterpolationMode = CanvasImageInterpolation.HighQualityCubic,
BorderMode = EffectBorderMode.Hard,
Scale = new Vector2(_coverScaleFactor),
Source = _canvasCoverBitmap,
}
}
};
ds.DrawImage(
coverOverlayEffect,
(float)control.Size.Width / 2 - _canvasCoverBitmap.SizeInPixels.Width * _coverScaleFactor / 2,
(float)control.Size.Height / 2 - _canvasCoverBitmap.SizeInPixels.Height * _coverScaleFactor / 2);
ds.Transform = Matrix3x2.Identity;
}
}
private void DrawGradientOpacityMask(ICanvasAnimatedControl control, CanvasDrawingSession ds, byte r, byte g, byte b) {
byte verticalEdgeAlpha = (byte)(255 * _settingsService.LyricsVerticalEdgeOpacity / 100f);
using var maskBrush = new CanvasLinearGradientBrush(control, [
new() { Position = 0, Color = Color.FromArgb(verticalEdgeAlpha, r, g, b)},
new() { Position = 0.5f, Color = Color.FromArgb(255, r, g, b)},
new() { Position = 1, Color = Color.FromArgb(verticalEdgeAlpha, r, g, b)},
]) {
StartPoint = new Vector2(0, 0),
EndPoint = new Vector2(0, (float)control.Size.Height)
};
ds.FillRectangle(new Rect(0, 0, control.Size.Width, control.Size.Height), maskBrush);
}
// Comsumes CPU related resources
private void LyricsCanvas_Update(ICanvasAnimatedControl sender, CanvasAnimatedUpdateEventArgs args) {
_currentTime += args.Timing.ElapsedTime;
if (_settingsService.IsDynamicCoverOverlay) {
_coverBitmapRotateAngle += _coverRotateSpeed;
_coverBitmapRotateAngle %= MathF.PI * 2;
}
if (_settingsService.IsLyricsDynamicGlowEffectEnabled) {
_lyricsGlowEffectAngle += _lyricsGlowEffectSpeed;
_lyricsGlowEffectAngle %= MathF.PI * 2;
}
if (_settingsService.IsCoverOverlayEnabled && _canvasCoverBitmap != null) {
var diagonal = Math.Sqrt(Math.Pow(_lyricsAreaWidth, 2) + Math.Pow(_lyricsAreaHeight, 2));
_coverScaleFactor = (float)diagonal / Math.Min(
_canvasCoverBitmap.SizeInPixels.Width,
_canvasCoverBitmap.SizeInPixels.Height);
}
if (_lyricsLines.LastOrDefault()?.TextLayout == null || _isRelayoutLyricsNeeded) {
LayoutLyrics();
}
int currentPlayingLineIndex = GetCurrentPlayingLineIndex();
UpdateScaleAndOpacity(currentPlayingLineIndex);
UpdatePosition(currentPlayingLineIndex);
}
private int GetCurrentPlayingLineIndex() {
for (int i = 0; i < _lyricsLines.Count; i++) {
var line = _lyricsLines[i];
if (line.EndPlayingTimestampMs < _currentTime.TotalMilliseconds) {
continue;
}
return i;
}
return -1;
}
private Tuple<int, int> GetVisibleLyricsLineIndexBoundaries() {
//Debug.WriteLine($"{_startVisibleLineIndex} {_endVisibleLineIndex}");
return new Tuple<int, int>(_startVisibleLineIndex, _endVisibleLineIndex);
}
private Tuple<int, int> GetMaxLyricsLineIndexBoundaries() {
if (_lyricsLines.Count == 0) {
return new Tuple<int, int>(-1, -1);
}
return new Tuple<int, int>(0, _lyricsLines.Count - 1);
}
private void UpdateScaleAndOpacity(int currentPlayingLineIndex) {
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
for (int i = startLineIndex; _lyricsLines.Count > 0 && i <= endLineIndex; i++) {
var line = _lyricsLines[i];
bool linePlaying = i == currentPlayingLineIndex;
var lineEnteringDurationMs = Math.Min(line.DurationMs, _lineEnteringDurationMs);
var lineExitingDurationMs = _lineExitingDurationMs;
if (i + 1 <= endLineIndex) {
lineExitingDurationMs = Math.Min(_lyricsLines[i + 1].DurationMs, lineExitingDurationMs);
}
float lineEnteringProgress = 0.0f;
float lineExitingProgress = 0.0f;
bool lineEntering = false;
bool lineExiting = false;
float scale = _defaultScale;
float opacity = _defaultOpacity;
float playProgress = 0;
if (linePlaying) {
line.PlayingState = LyricsPlayingState.Playing;
scale = _highlightedScale;
opacity = _highlightedOpacity;
playProgress = ((float)_currentTime.TotalMilliseconds - line.StartPlayingTimestampMs) / line.DurationMs;
var durationFromStartMs = _currentTime.TotalMilliseconds - line.StartPlayingTimestampMs;
lineEntering = durationFromStartMs <= lineEnteringDurationMs;
if (lineEntering) {
lineEnteringProgress = (float)durationFromStartMs / lineEnteringDurationMs;
scale = _defaultScale + (_highlightedScale - _defaultScale) * (float)lineEnteringProgress;
opacity = _defaultOpacity + (_highlightedOpacity - _defaultOpacity) * (float)lineEnteringProgress;
}
} else {
if (i < currentPlayingLineIndex) {
line.PlayingState = LyricsPlayingState.Played;
playProgress = 1;
var durationToEndMs = _currentTime.TotalMilliseconds - line.EndPlayingTimestampMs;
lineExiting = durationToEndMs <= lineExitingDurationMs;
if (lineExiting) {
lineExitingProgress = (float)durationToEndMs / lineExitingDurationMs;
scale = _highlightedScale - (_highlightedScale - _defaultScale) * (float)lineExitingProgress;
opacity = _highlightedOpacity - (_highlightedOpacity - _defaultOpacity) * (float)lineExitingProgress;
}
} else {
line.PlayingState = LyricsPlayingState.NotPlayed;
}
}
line.EnteringProgress = lineEnteringProgress;
line.ExitingProgress = lineExitingProgress;
line.Scale = scale;
line.Opacity = opacity;
line.PlayingProgress = playProgress;
}
}
private void LayoutLyrics() {
using CanvasTextFormat textFormat = new() {
FontSize = _settingsService.LyricsFontSize,
HorizontalAlignment = CanvasHorizontalAlignment.Left,
VerticalAlignment = CanvasVerticalAlignment.Top,
FontWeight = FontWeights.Bold,
//FontFamily = "Segoe UI Mono",
};
float y = (float)_lyricsAreaHeight / 2;
// Init Positions
for (int i = 0; i < _lyricsLines.Count; i++) {
var line = _lyricsLines[i];
// Calculate layout bounds
line.TextLayout = new CanvasTextLayout(LyricsCanvas.Device, line.Text, textFormat, (float)_lyricsCanvasMaxTextWidth, (float)_lyricsAreaHeight);
line.Position = new Vector2((float)_lyricsCanvasLeftMargin, y);
y += (float)line.TextLayout.LayoutBounds.Height / line.TextLayout.LineCount * (line.TextLayout.LineCount + _settingsService.LyricsLineSpacingFactor);
}
}
private void UpdatePosition(int currentPlayingLineIndex) {
if (currentPlayingLineIndex < 0) {
return;
}
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
if (startLineIndex < 0 || endLineIndex < 0) {
return;
}
// Set _scrollOffsetY
LyricsLine? currentPlayingLine = _lyricsLines?[currentPlayingLineIndex];
if (currentPlayingLine == null) {
return;
}
if (currentPlayingLine.TextLayout == null) {
return;
}
var lineScrollingProgress =
(_currentTime.TotalMilliseconds - currentPlayingLine.StartPlayingTimestampMs) /
Math.Min(_lineScrollDurationMs, currentPlayingLine.DurationMs);
var targetYScrollOffset = (float)(-currentPlayingLine.Position.Y + _lyricsLines![0].Position.Y - currentPlayingLine.TextLayout.LayoutBounds.Height / 2 - _lastTotalYScroll);
var yScrollOffset = targetYScrollOffset * EasingHelper.SmootherStep((float)Math.Min(1, lineScrollingProgress));
bool isScrollingNow = lineScrollingProgress <= 1;
if (isScrollingNow) {
_totalYScroll = _lastTotalYScroll + yScrollOffset;
} else {
if (_forceToScroll && Math.Abs(targetYScrollOffset) >= 1) {
_totalYScroll = _lastTotalYScroll + targetYScrollOffset;
}
_lastTotalYScroll = _totalYScroll;
}
_startVisibleLineIndex = _endVisibleLineIndex = -1;
// Update Positions
for (int i = startLineIndex; i >= 0 && i <= endLineIndex; i++) {
var line = _lyricsLines[i];
if (_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height >= 0) {
if (_startVisibleLineIndex == -1) {
_startVisibleLineIndex = i;
}
}
if (_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height >= _lyricsAreaHeight) {
if (_endVisibleLineIndex == -1) {
_endVisibleLineIndex = i;
}
}
}
if (_startVisibleLineIndex != -1 && _endVisibleLineIndex == -1) {
_endVisibleLineIndex = endLineIndex;
}
}
private void LyricsCanvas_Loaded(object sender, RoutedEventArgs e) {
InitMediaManager();
}
private void SettingsButton_Click(object sender, RoutedEventArgs e) {
if (App.Current.SettingsWindow == null) {
App.Current.SettingsWindow = new MainWindow();
App.Current.SettingsWindow!.Navigate(typeof(SettingsPage));
}
App.Current.SettingsWindow.AppWindow.Show();
}
private void WelcomeTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args) {
TopCommandTeachingTip.IsOpen = true;
}
private void TopCommandTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args) {
BottomCommandTeachingTip.IsOpen = true;
}
private void BottomCommandTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args) {
LyricsOnlyTeachingTip.IsOpen = true;
}
private void LyricsOnlyTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args) {
InitDatabaseTeachingTip.IsOpen = true;
}
private void InitDatabaseTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args) {
_settingsService.IsFirstRun = false;
}
}
}

View File

@@ -0,0 +1,368 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="BetterLyrics.WinUI3.Views.SettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:BetterLyrics.WinUI3.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:vm="using:BetterLyrics.WinUI3.ViewModels"
x:Name="RootPage"
NavigationCacheMode="Required"
mc:Ignorable="d">
<Grid x:Name="RootGrid">
<controls:OpacityMaskView HorizontalContentAlignment="Stretch">
<controls:OpacityMaskView.OpacityMask>
<Rectangle Fill="{StaticResource BaseHighEdgeVerticalFadeBrush}" />
</controls:OpacityMaskView.OpacityMask>
<ScrollViewer>
<Grid Margin="36,72,36,72">
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock x:Uid="SettingsPageLyricsLib" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsExpander
x:Uid="SettingsPageMusicLib"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}"
IsExpanded="True"
ItemsSource="{x:Bind ViewModel.SettingsService.MusicLibraries, Mode=OneWay}">
<controls:SettingsExpander.ItemTemplate>
<DataTemplate>
<controls:SettingsCard Header="{Binding Path, Mode=OneWay}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{Binding IsValid, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{Binding IsValid, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Background" Value="{ThemeResource SystemFillColorCriticalBackgroundBrush}" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
<controls:SettingsCard.Description>
<TextBlock x:Uid="SettingsPagePathNotFound" Foreground="{ThemeResource SystemFillColorCriticalBrush}">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{Binding IsValid, Mode=OneWay}"
ComparisonCondition="Equal"
Value="True">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{Binding IsValid, Mode=OneWay}"
ComparisonCondition="Equal"
Value="False">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</TextBlock>
</controls:SettingsCard.Description>
<StackPanel Orientation="Horizontal">
<HyperlinkButton
x:Uid="SettingsPageOpenPath"
Command="{Binding DataContext.OpenFolderInFileExplorerCommand, ElementName=RootPage}"
CommandParameter="{Binding}"
IsEnabled="{Binding IsValid, Mode=OneWay}" />
<HyperlinkButton
x:Uid="SettingsPageRemovePath"
Command="{Binding DataContext.RemoveFolderCommand, ElementName=RootPage}"
CommandParameter="{Binding}"
IsEnabled="{Binding DataContext.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay, ElementName=RootPage}" />
</StackPanel>
</controls:SettingsCard>
</DataTemplate>
</controls:SettingsExpander.ItemTemplate>
<controls:SettingsExpander.ItemsHeader>
<InfoBar
x:Uid="SettingsPageRemoveInfo"
BorderThickness="0"
CornerRadius="0"
IsClosable="False"
IsOpen="True"
Severity="Success">
<interactivity:Interaction.Behaviors>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.MusicLibraries.Count, Mode=OneWay}"
ComparisonCondition="Equal"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
</interactivity:DataTriggerBehavior>
<interactivity:DataTriggerBehavior
Binding="{x:Bind ViewModel.SettingsService.MusicLibraries.Count, Mode=OneWay}"
ComparisonCondition="NotEqual"
Value="0">
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
</interactivity:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</InfoBar>
</controls:SettingsExpander.ItemsHeader>
<controls:SettingsExpander.ItemsFooter>
<controls:SettingsCard
x:Uid="SettingsPageAddFolder"
IsEnabled="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}"
Style="{StaticResource DefaultSettingsExpanderItemStyle}">
<Button x:Uid="SettingsPageAddFolderButton" Command="{x:Bind ViewModel.AddFolderCommand}" />
</controls:SettingsCard>
</controls:SettingsExpander.ItemsFooter>
</controls:SettingsExpander>
<controls:SettingsCard
x:Uid="SettingsPageRebuildDatabase"
HeaderIcon="{ui:FontIcon Glyph=&#xE621;}"
IsEnabled="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
<controls:SettingsCard.Description>
<TextBlock x:Uid="SettingsPageRebuildDatabaseDesc" Visibility="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</controls:SettingsCard.Description>
<StackPanel>
<Button x:Uid="SettingsPageRebuildDatabaseButton" Command="{x:Bind ViewModel.RebuildLyricsIndexDatabaseCommand}" />
<ProgressBar
IsIndeterminate="True"
ShowError="False"
ShowPaused="False"
Visibility="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
</StackPanel>
</controls:SettingsCard>
<TextBlock x:Uid="SettingsPageAppAppearance" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageTheme" HeaderIcon="{ui:FontIcon Glyph=&#xE790;}">
<ComboBox x:Name="ThemeComboBox" SelectedIndex="{x:Bind ViewModel.SettingsService.Theme, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPageFollowSystem" />
<ComboBoxItem x:Uid="SettingsPageLight" />
<ComboBoxItem x:Uid="SettingsPageDark" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageBackdrop" HeaderIcon="{ui:FontIcon Glyph=&#xF5EF;}">
<ComboBox x:Name="BackdropComboBox" SelectedIndex="{x:Bind ViewModel.SettingsService.BackdropType, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPageNoBackdrop" />
<ComboBoxItem x:Uid="SettingsPageMica" />
<ComboBoxItem x:Uid="SettingsPageMicaAlt" />
<ComboBoxItem x:Uid="SettingsPageDesktopAcrylic" />
<ComboBoxItem x:Uid="SettingsPageAcrylicThin" />
<ComboBoxItem x:Uid="SettingsPageAcrylicBase" />
<ComboBoxItem x:Uid="SettingsPageTransparent" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsExpander
x:Uid="SettingsPageLanguage"
HeaderIcon="{ui:FontIcon Glyph=&#xF2B7;}"
IsExpanded="True">
<ComboBox SelectedIndex="{x:Bind ViewModel.SettingsService.Language, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPageSystemLanguage" />
<ComboBoxItem x:Uid="SettingsPageEN" />
<ComboBoxItem x:Uid="SettingsPageSC" />
<ComboBoxItem x:Uid="SettingsPageTC" />
</ComboBox>
<controls:SettingsExpander.Items>
<controls:SettingsCard>
<Button x:Uid="SettingsPageRestart" Command="{x:Bind ViewModel.RestartAppCommand}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<TextBlock x:Uid="SettingsPageAlbum" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsExpander
x:Uid="SettingsPageCoverOverlay"
HeaderIcon="{ui:FontIcon Glyph=&#xE93C;}"
IsExpanded="True">
<controls:SettingsExpander.Description>
<StackPanel>
<TextBlock x:Uid="SettingsPageCoverOverlayGPUUsage" />
</StackPanel>
</controls:SettingsExpander.Description>
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageDynamicCoverOverlay" IsEnabled="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsDynamicCoverOverlay, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageCoverOverlayOpacity" IsEnabled="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.CoverOverlayOpacity, Mode=OneWay}" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text=" %" />
<Slider
Maximum="100"
Minimum="1"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.CoverOverlayOpacity, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageCoverOverlayBlurAmount" IsEnabled="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.SettingsService.CoverOverlayBlurAmount, Mode=OneWay}" />
<Slider
Maximum="200"
Minimum="50"
SnapsTo="Ticks"
StepFrequency="10"
TickFrequency="10"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.CoverOverlayBlurAmount, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<TextBlock x:Uid="SettingsPageLyricsStyle" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageLyricsAlignment" HeaderIcon="{ui:FontIcon Glyph=&#xE8E3;}">
<ComboBox SelectedIndex="{x:Bind ViewModel.SettingsService.LyricsAlignmentType, Mode=TwoWay}">
<ComboBoxItem x:Uid="SettingsPageLyricsLeft" />
<ComboBoxItem x:Uid="SettingsPageLyricsCenter" />
<ComboBoxItem x:Uid="SettingsPageLyricsRight" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsFontSize" HeaderIcon="{ui:FontIcon Glyph=&#xE8E9;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.SettingsService.LyricsFontSize, Mode=OneWay}" />
<Slider
Maximum="48"
Minimum="12"
SnapsTo="Ticks"
StepFrequency="2"
TickFrequency="2"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.LyricsFontSize, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsLineSpacingFactor" HeaderIcon="{ui:FontIcon Glyph=&#xF579;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.LyricsLineSpacingFactor, Mode=OneWay}" />
<TextBlock
x:Uid="SettingsPageLyricsLineSpacingFactorUnit"
Margin="0,0,14,0"
VerticalAlignment="Center" />
<Slider
Maximum="2"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="0.1"
TickFrequency="0.1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.LyricsLineSpacingFactor, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<TextBlock x:Uid="SettingsPageLyricsEffect" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard x:Uid="SettingsPageLyricsVerticalEdgeOpacity" HeaderIcon="{ui:FontIcon Glyph=&#xF573;}">
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.LyricsVerticalEdgeOpacity, Mode=OneWay}" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text=" %" />
<Slider
Maximum="100"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.LyricsVerticalEdgeOpacity, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="SettingsPageLyricsBlurAmount" HeaderIcon="{ui:FontIcon Glyph=&#xE727;}">
<controls:SettingsCard.Description>
<StackPanel>
<TextBlock x:Uid="SettingsPageLyricsBlurHighGPUUsage" Foreground="{ThemeResource SystemFillColorCautionBrush}" />
<TextBlock x:Uid="SettingsPageLyricsBlurAmountSideEffect" />
</StackPanel>
</controls:SettingsCard.Description>
<StackPanel Orientation="Horizontal">
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
<TextBlock
Margin="0,0,14,0"
VerticalAlignment="Center"
Text="{x:Bind ViewModel.SettingsService.LyricsBlurAmount, Mode=OneWay}" />
<Slider
Maximum="10"
Minimum="0"
SnapsTo="Ticks"
StepFrequency="1"
TickFrequency="1"
TickPlacement="Outside"
Value="{x:Bind ViewModel.SettingsService.LyricsBlurAmount, Mode=TwoWay}" />
</StackPanel>
</controls:SettingsCard>
<controls:SettingsExpander
x:Uid="SettingsPageLyricsGlowEffect"
HeaderIcon="{ui:FontIcon Glyph=&#xE9A9;}"
IsExpanded="True">
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsLyricsGlowEffectEnabled, Mode=TwoWay}" />
<controls:SettingsExpander.Items>
<controls:SettingsCard x:Uid="SettingsPageLyricsDynamicGlowEffect" IsEnabled="{x:Bind ViewModel.SettingsService.IsLyricsGlowEffectEnabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsLyricsDynamicGlowEffectEnabled, Mode=TwoWay}" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<TextBlock x:Uid="SettingsPageAbout" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<controls:SettingsCard Header="BetterLyrics" HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/Icon.png}">
<controls:SettingsCard.Description>
<RichTextBlock>
<Paragraph>
<Run x:Uid="SettingsPageVersion" />
<Run Text="{x:Bind ViewModel.Version, Mode=OneWay}" />
</Paragraph>
</RichTextBlock>
</controls:SettingsCard.Description>
</controls:SettingsCard>
<controls:SettingsCard
x:Uid="SettingsPageGitHub"
ActionIcon="{ui:FontIcon Glyph=&#xE8A7;}"
Command="{x:Bind ViewModel.LaunchProjectGitHubPageCommand}"
HeaderIcon="{ui:FontIcon Glyph=&#xE943;}"
IsClickEnabled="True" />
</StackPanel>
</Grid>
</ScrollViewer>
</controls:OpacityMaskView>
</Grid>
</Page>

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using ABI.System;
using Windows.System;
using BetterLyrics.WinUI3.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
using System.Diagnostics;
using WinRT.Interop;
using Windows.Storage.Pickers;
using BetterLyrics.WinUI3.Models;
using Microsoft.Windows.ApplicationModel.Resources;
using BetterLyrics.WinUI3.Services.Settings;
// 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.Views {
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class SettingsPage : Page {
public SettingsViewModel ViewModel => (SettingsViewModel)DataContext;
public SettingsPage() {
this.InitializeComponent();
DataContext = Ioc.Default.GetService<SettingsViewModel>();
}
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="BetterLyrics.WinUI3.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

57
BetterLyrics.sln Normal file
View File

@@ -0,0 +1,57 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.36105.23 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "BetterLyrics.WinUI3 (Package)", "BetterLyrics.WinUI3\BetterLyrics.WinUI3 (Package)\BetterLyrics.WinUI3 (Package).wapproj", "{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.WinUI3", "BetterLyrics.WinUI3\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj", "{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|ARM64.ActiveCfg = Debug|ARM64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|ARM64.Build.0 = Debug|ARM64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|ARM64.Deploy.0 = Debug|ARM64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x64.ActiveCfg = Debug|x64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x64.Build.0 = Debug|x64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x64.Deploy.0 = Debug|x64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x86.ActiveCfg = Debug|x86
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x86.Build.0 = Debug|x86
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x86.Deploy.0 = Debug|x86
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|ARM64.ActiveCfg = Release|ARM64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|ARM64.Build.0 = Release|ARM64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|ARM64.Deploy.0 = Release|ARM64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x64.ActiveCfg = Release|x64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x64.Build.0 = Release|x64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x64.Deploy.0 = Release|x64
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x86.ActiveCfg = Release|x86
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x86.Build.0 = Release|x86
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x86.Deploy.0 = Release|x86
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|ARM64.ActiveCfg = Debug|ARM64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|ARM64.Build.0 = Debug|ARM64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|x64.ActiveCfg = Debug|x64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|x64.Build.0 = Debug|x64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|x86.ActiveCfg = Debug|x86
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|x86.Build.0 = Debug|x86
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|ARM64.ActiveCfg = Release|ARM64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|ARM64.Build.0 = Release|ARM64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x64.ActiveCfg = Release|x64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x64.Build.0 = Release|x64
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x86.ActiveCfg = Release|x86
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0FD73F15-ED69-4DC2-9FD3-A4F03AF172A4}
EndGlobalSection
EndGlobal

21
LICENSE.txt Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) [year] [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

22
PrivacyPolicy.md Normal file
View File

@@ -0,0 +1,22 @@
Privacy Policy BetterLyrics
Effective Date: June 3, 2025
Thank you for using BetterLyrics.
BetterLyrics is a local lyrics viewer application built with WinUI 3. We respect your privacy and are committed to protecting it. This Privacy Policy explains how we handle your data.
1. No Personal Data Collection
BetterLyrics does not collect, store, or transmit any personal data. All lyrics, preferences, and settings are stored locally on your device. We do not access or send your information to any server.
2. Internet Usage
BetterLyrics does not require an internet connection for its core functionality. The app does not make any network requests or communicate with external services unless you manually choose to view external resources (such as a linked demo video).
3. Third-Party Libraries
BetterLyrics uses third-party open-source libraries (e.g., Win2D, CommunityToolkit, DevWinUI). These libraries are used locally and do not perform any background tracking or data transmission.
4. Changes to This Policy
If our privacy practices change in the future (for example, if online lyrics fetching is added), we will update this policy accordingly and notify users through app updates or store listing.
5. Contact
If you have any questions about this privacy policy, please contact us at: founchoo@outlook.com

101
README.md Normal file
View File

@@ -0,0 +1,101 @@
# BetterLyrics
Local song lyrics presentation app built with WinUI3
## Highlighted features
- Dynamic blur album art as background
- Smooth lyrics fade in/out, zoom in/out effects
- Smooth user interface change from song to song
- Gradient Karaoke effect on every single character
> Note: Lyrics related effects and functions are built with [CanvasAnimatedControl](https://learn.microsoft.com/en-us/windows/apps/develop/win2d/quick-start#animate-your-app-with-canvasanimatedcontrol) instead of list of `TextBlock`, which ensures a smooth and accurate animation and more customized style.
Coding in progress...
## Customize in your way
We provide more than one setting item to better align with your preference
- Theme
- Follow system
- Light
- Dark
- Backdrop
- None
- Mica
- Mica alt
- Acrylic desktop
- Acrylic thin
- Acrylic base
- Transparent
- Album art as background
- Dynamic
- Opacity
- Blur amount
- Lyrics
- Alignment
- Font size
- Line spacing
- Opacity on the edge
- Blur amount
- Glow effect
- Language
- English
- Simplified Chinese
- Traditional Chinese
## Inspired by
- [BetterNCM](https://github.com/std-microblock/BetterNCM)
- [Lyricify-App](https://github.com/WXRIW/Lyricify-App)
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
## Demonstration video
See our latest introduction video「BetterLyrics 阶段性开发成果展示」on [Bilibili](https://b23.tv/QjKkYmL)
## Screenshots (outdated)
### Settings
![Settings](/Screenshots/settings.png)
### Light music mode
Will be activated automatically when lyrics are not detected/found
![Light music mode](/Screenshots/light-music.png)
### General music mode
![General music mode](/Screenshots/general-music.png)
### Real-time gif
![Real-time gif](/Screenshots/lyrics-animation.gif)
## Many thanks to
- [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet)
- [DevWinUI](https://github.com/ghost1372/DevWinUI)
- [Stackoverflow - How to animate Margin property in WPF](https://stackoverflow.com/a/21542882/11048731)
- [TagLib#](https://github.com/mono/taglib-sharp)
- [Bilibili -【WinUI3】SystemBackdropController定义云母、亚克力效果](https://www.bilibili.com/video/BV1PY4FevEkS)
- [cnblogs - .NET App 与Windows系统媒体控制(SMTC)交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
- [Win2D 中的游戏循环CanvasAnimatedControl](https://www.cnblogs.com/walterlv/p/10236395.html)
- [r2d2rigo/Win2D-Samples](https://github.com/r2d2rigo/Win2D-Samples/blob/master/IrisBlurWin2D/IrisBlurWin2D/MainPage.xaml.cs)
## Third-party libraries that this app uses
- CommunityToolkit.Labs.WinUI.MarqueeText
- CommunityToolkit.Labs.WinUI.OpacityMaskView
- CommunityToolkit.Mvvm
- CommunityToolkit.WinUI.Controls.Primitives
- CommunityToolkit.WinUI.Extensions
- DevWinUI
- DevWinUI.Controls
- Microsoft.Extensions.DependencyInjection
- Microsoft.Graphics.Win2D
- Microsoft.Windows.SDK.BuildTools
- Microsoft.WindowsAppSDK
- Microsoft.Xaml.Behaviors.WinUI.Managed
- z440.atl.core