commit f24e8b5fcd52bbae1d9fc872368b1588c37fa095 Author: Zhe Fang Date: Tue Jun 3 17:48:19 2025 -0400 Initial commit after cleanup diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..586e647 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package).wapproj b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package).wapproj new file mode 100644 index 0000000..fcbc5b7 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package).wapproj @@ -0,0 +1,147 @@ + + + + 15.0 + + + + Debug + x86 + + + Release + x86 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ + BetterLyrics.WinUI3\ + + + + 6576cd19-ef92-4099-b37d-e2d8ebdb6bf5 + 10.0.26100.0 + 10.0.17763.0 + net8.0-windows$(TargetPlatformVersion);$(AssetTargetFallback) + zh-CN + false + ..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj + False + SHA256 + True + True + x86|x64|arm64 + True + 0 + + + Always + en-US + + + Always + en-US + + + Always + en-US + + + Always + en-US + + + Always + en-US + + + Always + en-US + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + Properties\PublishProfiles\win-$(Platform).pubxml + + + + + + + + \ No newline at end of file diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-100.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-100.png new file mode 100644 index 0000000..e1b6be5 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-100.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-125.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-125.png new file mode 100644 index 0000000..05bc112 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-125.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-150.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-150.png new file mode 100644 index 0000000..54df62a Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-150.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-200.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-200.png new file mode 100644 index 0000000..6c9a64c Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-200.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-400.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-400.png new file mode 100644 index 0000000..b4c2066 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LargeTile.scale-400.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LockScreenLogo.scale-200.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LockScreenLogo.scale-200.png new file mode 100644 index 0000000..7440f0d Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/LockScreenLogo.scale-200.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-100.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-100.png new file mode 100644 index 0000000..850e2ce Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-100.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-125.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-125.png new file mode 100644 index 0000000..a2b9275 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-125.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-150.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-150.png new file mode 100644 index 0000000..54239b6 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-150.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-200.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-200.png new file mode 100644 index 0000000..f133fde Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-200.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-400.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-400.png new file mode 100644 index 0000000..fad8443 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SmallTile.scale-400.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-100.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-100.png new file mode 100644 index 0000000..89b91b5 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-100.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-125.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-125.png new file mode 100644 index 0000000..bccc826 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-125.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-150.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-150.png new file mode 100644 index 0000000..8181bad Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-150.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-200.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-200.png new file mode 100644 index 0000000..7c7775e Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-200.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-400.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-400.png new file mode 100644 index 0000000..dbbd13c Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/SplashScreen.scale-400.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-100.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-100.png new file mode 100644 index 0000000..14412b6 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-100.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-125.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-125.png new file mode 100644 index 0000000..136724d Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-125.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-150.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-150.png new file mode 100644 index 0000000..f8b68d1 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-150.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-200.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-200.png new file mode 100644 index 0000000..549d0c0 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-200.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-400.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-400.png new file mode 100644 index 0000000..0873dcf Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square150x150Logo.scale-400.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-16.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-16.png new file mode 100644 index 0000000..8ad2ce0 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-24.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-24.png new file mode 100644 index 0000000..ab2cde5 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-256.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-256.png new file mode 100644 index 0000000..07e582d Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-32.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-32.png new file mode 100644 index 0000000..c35493f Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-48.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-48.png new file mode 100644 index 0000000..8b93016 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-unplated_targetsize-16.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 0000000..8ad2ce0 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-unplated_targetsize-256.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 0000000..07e582d Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-unplated_targetsize-32.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 0000000..c35493f Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-unplated_targetsize-48.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 0000000..8b93016 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-100.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-100.png new file mode 100644 index 0000000..be18505 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-100.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-125.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-125.png new file mode 100644 index 0000000..17877b3 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-125.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-150.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-150.png new file mode 100644 index 0000000..c618dc0 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-150.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-200.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-200.png new file mode 100644 index 0000000..d454412 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-200.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-400.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-400.png new file mode 100644 index 0000000..6da4ac7 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.scale-400.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-16.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-16.png new file mode 100644 index 0000000..7295774 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-16.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-24.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-24.png new file mode 100644 index 0000000..304436e Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-24.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-24_altform-unplated.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000..ab2cde5 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-256.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-256.png new file mode 100644 index 0000000..89f1ff4 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-256.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-32.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-32.png new file mode 100644 index 0000000..0b5da90 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-32.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-48.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-48.png new file mode 100644 index 0000000..c83974b Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Square44x44Logo.targetsize-48.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.backup.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.backup.png new file mode 100644 index 0000000..a4586f2 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.backup.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-100.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-100.png new file mode 100644 index 0000000..20b45be Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-100.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-125.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-125.png new file mode 100644 index 0000000..7a44552 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-125.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-150.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-150.png new file mode 100644 index 0000000..a63f751 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-150.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-200.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-200.png new file mode 100644 index 0000000..cb97225 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-200.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-400.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-400.png new file mode 100644 index 0000000..ca57e91 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/StoreLogo.scale-400.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-100.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-100.png new file mode 100644 index 0000000..6f75e83 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-100.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-125.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-125.png new file mode 100644 index 0000000..2fb9e02 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-125.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-150.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-150.png new file mode 100644 index 0000000..ee4c81f Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-150.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-200.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000..89b91b5 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-200.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-400.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-400.png new file mode 100644 index 0000000..7c7775e Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Images/Wide310x150Logo.scale-400.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest new file mode 100644 index 0000000..ee9c2f6 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/Package.appxmanifest @@ -0,0 +1,51 @@ + + + + + + + + + + BetterLyrics + founchoo + Images\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml new file mode 100644 index 0000000..651e4b9 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + #80000000 + + + #80FFFFFF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4 + + + + + + + diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml.cs new file mode 100644 index 0000000..b00ee70 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml.cs @@ -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 { + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + 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(); + + /// + /// 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(). + /// + public App() { + this.InitializeComponent(); + } + + /// + /// Invoked when the application is launched. + /// + /// Details about the launch request and process. + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) { + + // Register services + Ioc.Default.ConfigureServices( + new ServiceCollection() + .AddSingleton(DispatcherQueue.GetForCurrentThread()) + // Services + .AddSingleton() + .AddSingleton() + // ViewModels + .AddSingleton() + .AddSingleton() + .BuildServiceProvider()); + + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // Activate the window + MainWindow = new MainWindow(); + MainWindow!.Navigate(typeof(MainPage)); + MainWindow.Activate(); + } + + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Icon.png b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Icon.png new file mode 100644 index 0000000..c8f71a8 Binary files /dev/null and b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Icon.png differ diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj new file mode 100644 index 0000000..5b27302 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj @@ -0,0 +1,48 @@ + + + WinExe + net8.0-windows10.0.26100.0 + 10.0.17763.0 + BetterLyrics.WinUI3 + app.manifest + x86;x64;ARM64 + win-x86;win-x64;win-arm64 + true + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + True + False + True + 10.0.19041.0 + + \ No newline at end of file diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/ColorToBrushConverter.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/ColorToBrushConverter.cs new file mode 100644 index 0000000..52150e4 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/ColorToBrushConverter.cs @@ -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(); + } + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/ThemeTypeToElementThemeConverter.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/ThemeTypeToElementThemeConverter.cs new file mode 100644 index 0000000..86aaf70 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Converter/ThemeTypeToElementThemeConverter.cs @@ -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; + } + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/AnimationHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/AnimationHelper.cs new file mode 100644 index 0000000..e407c98 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/AnimationHelper.cs @@ -0,0 +1,44 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media.Animation; +using System; + +namespace BetterLyrics.WinUI3.Helper { + + /// + /// Edited based on: https://stackoverflow.com/a/25236507/11048731 + /// + 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)); + } + } + } + } + +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/CollectionHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/CollectionHelper.cs new file mode 100644 index 0000000..2633064 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/CollectionHelper.cs @@ -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(this IList list, int index) { + if (list == null || index < 0 || index >= list.Count) + return default; + return list[index]; + } + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ColorHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ColorHelper.cs new file mode 100644 index 0000000..5c8747e --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ColorHelper.cs @@ -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); + } + + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ColorThief.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ColorThief.cs new file mode 100644 index 0000000..61139a8 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ColorThief.cs @@ -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 +{ + /// + /// Color map + /// + internal class CMap + { + private readonly List vboxes = new List(); + private List palette; + + public void Push(VBox box) + { + palette = null; + vboxes.Add(box); + } + + public List 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; + } + } + + /// + /// Defines a color in RGB space. + /// + public struct Color + { + /// + /// Get or Set the Alpha component value for sRGB. + /// + public byte A; + + /// + /// Get or Set the Blue component value for sRGB. + /// + public byte B; + + /// + /// Get or Set the Green component value for sRGB. + /// + public byte G; + + /// + /// Get or Set the Red component value for sRGB. + /// + public byte R; + + /// + /// Get HSL color. + /// + /// + 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(); + } + } + + /// + /// Defines a color in Hue/Saturation/Lightness (HSL) space. + /// + public struct HslColor + { + /// + /// The Alpha/opacity in 0..1 range. + /// + public double A; + + /// + /// The Hue in 0..360 range. + /// + public double H; + + /// + /// The Lightness in 0..1 range. + /// + public double L; + + /// + /// The Saturation in 0..1 range. + /// + 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; + } + + /// + /// Gets the histo. + /// + /// The pixels. + /// Histo (1-d array, giving the number of pixels in each quantized region of color space), or null on error. + private static int[] GetHisto(IEnumerable 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 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 partialsum, IList 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 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); + } + + /// + /// Inner function to do the iteration. + /// + /// The lh. + /// The comparator. + /// The target. + /// The histo. + /// vbox1 not defined; shouldn't happen! + private static void Iter(List lh, IComparer comparator, int target, IList 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 }; + + // 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)); + } + } + + /// + /// 3D color space box. + /// + 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 + { + 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 + { + 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; + } + + /// + /// Use the median cut algorithm to cluster similar colors and return the base color from the largest cluster. + /// + /// The source image. + /// + /// 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. + /// + /// if set to true [ignore white]. + /// + public async Task 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; + } + + /// + /// Use the median cut algorithm to cluster similar colors. + /// + /// The source image. + /// The color count. + /// + /// 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. + /// + /// if set to true [ignore white]. + /// + /// true + public async Task> 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(); + } + + private async Task GetIntFromPixel(BitmapDecoder decoder) + { + var pixelsData = await decoder.GetPixelDataAsync(); + var pixels = pixelsData.DetachPixelData(); + return pixels; + } + + private async Task 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); + } + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/DatabaseHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/DatabaseHelper.cs new file mode 100644 index 0000000..7bad089 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/DatabaseHelper.cs @@ -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(); // Create table if it doesn't exist + return _database; + } + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/EasingHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/EasingHelper.cs new file mode 100644 index 0000000..82e2adb --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/EasingHelper.cs @@ -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 { + + /// + /// No easing + /// + public static float Linear(float t) => t; + + /// + /// Accelerating from 0 + /// + public static float EaseInQuad(float t) => t * t; + + /// + /// Decelerating to 0 + /// + public static float EaseOutQuad(float t) => t * (2 - t); + + /// + /// Acceleration until halfway then deceleration + /// + public static float EaseInOutQuad(float t) { + return t < 0.5f + ? 2 * t * t + : -1 + (4 - 2 * t) * t; + } + + /// + /// Smoother transition than linear + /// + public static float SmoothStep(float t) { + return t * t * (3 - 2 * t); + } + + /// + /// Even smoother transition with continuous first and second derivatives + /// + public static float SmootherStep(float t) { + return t * t * t * (t * (6 * t - 15) + 10); + } + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ImageHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ImageHelper.cs new file mode 100644 index 0000000..7a1c7ba --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ImageHelper.cs @@ -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 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; + } + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/MathHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/MathHelper.cs new file mode 100644 index 0000000..d6b271b --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/MathHelper.cs @@ -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 GetAllFactors(int n) { + var result = new SortedSet(); + + for (int i = 1; i <= Math.Sqrt(n); i++) { + if (n % i == 0) { + result.Add(i); + result.Add(n / i); + } + } + + return [.. result]; + } + + } +} \ No newline at end of file diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/SystemBackdropHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/SystemBackdropHelper.cs new file mode 100644 index 0000000..8b49b41 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/SystemBackdropHelper.cs @@ -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, + }; + } + + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/VisualHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/VisualHelper.cs new file mode 100644 index 0000000..e87aabf --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/VisualHelper.cs @@ -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 { + + /// + /// Source: https://stackoverflow.com/a/61626933/11048731 + /// + /// + /// + /// + public static List FindVisualChildren(DependencyObject depObj) where T : DependencyObject { + List list = new List(); + 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 childItems = FindVisualChildren(child); + if (childItems != null && childItems.Count() > 0) { + foreach (var item in childItems) { + list.Add(item); + } + } + } + } + return list; + } + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/MainWindow.xaml b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/MainWindow.xaml new file mode 100644 index 0000000..f1577de --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/MainWindow.xaml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/MainWindow.xaml.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/MainWindow.xaml.cs new file mode 100644 index 0000000..db5dd00 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/MainWindow.xaml.cs @@ -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 { + /// + /// An empty window that can be used on its own or navigated to within a Frame. + /// + 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(); + + RootGrid.RequestedTheme = (ElementTheme)_settingsService.Theme; + SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop((BackdropType)_settingsService.BackdropType); + + WeakReferenceMessenger.Default.Register(this, (r, m) => { + RootGrid.RequestedTheme = m.Value; + }); + + WeakReferenceMessenger.Default.Register(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(); + + } + + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Messages/SystemBackdropChangedMessage.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Messages/SystemBackdropChangedMessage.cs new file mode 100644 index 0000000..86e3da9 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Messages/SystemBackdropChangedMessage.cs @@ -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 { + public SystemBackdropChangedMessage(BackdropType value) : base(value) { + } + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Messages/ThemeChangedMessage.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Messages/ThemeChangedMessage.cs new file mode 100644 index 0000000..d573786 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Messages/ThemeChangedMessage.cs @@ -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 { + public ThemeChangedMessage(ElementTheme value) : base(value) { + } + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/Language.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/Language.cs new file mode 100644 index 0000000..1602945 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/Language.cs @@ -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, + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsAlignmentType.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsAlignmentType.cs new file mode 100644 index 0000000..3cedb29 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsAlignmentType.cs @@ -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, + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsLine.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsLine.cs new file mode 100644 index 0000000..5e3858c --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsLine.cs @@ -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 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; } + + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsPlayingState.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsPlayingState.cs new file mode 100644 index 0000000..b2cf4c6 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsPlayingState.cs @@ -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 { + /// + /// Not played yet, will be playing in the future + /// + NotPlayed, + /// + /// Playing + /// + Playing, + /// + /// Has already played + /// + Played + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/MetadataIndex.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/MetadataIndex.cs new file mode 100644 index 0000000..8e31171 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/MetadataIndex.cs @@ -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; } + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/MusicFolder.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/MusicFolder.cs new file mode 100644 index 0000000..53af245 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/MusicFolder.cs @@ -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; + } + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Database/DatabaseService.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Database/DatabaseService.cs new file mode 100644 index 0000000..93cb845 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Database/DatabaseService.cs @@ -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(); + } + + public async Task RebuildMusicMetadataIndexDatabaseAsync(IList musicFolders) { + await Task.Run(() => { + _connection.DeleteAll(); + 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() + .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; + } + } + } + } + + + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsDefaultValues.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsDefaultValues.cs new file mode 100644 index 0000000..80cc253 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsDefaultValues.cs @@ -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; + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsKey.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsKey.cs new file mode 100644 index 0000000..139d1ae --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsKey.cs @@ -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"; + } + +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsService.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsService.cs new file mode 100644 index 0000000..34dd146 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/Settings/SettingsService.cs @@ -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 _musicLibraries; + + public ObservableCollection MusicLibraries { + get { + if (_musicLibraries == null) { + var list = JsonConvert.DeserializeObject>( + Get(SettingsKeys.MusicLibraries, SettingsDefaultValues.MusicLibraries) + ); + + _musicLibraries = new ObservableCollection(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(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(string key, T value, [CallerMemberName] string propertyName = null) { + _localSettings.Values[key] = value; + OnPropertyChanged(propertyName); + } + + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw new file mode 100644 index 0000000..51d29da --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/en-US/Resources.resw @@ -0,0 +1,342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Local music libraries + + + Add folders storing music or lyrics to build lyrics index database + + + Open in file explorer + + + Remove from app + + + You are safe to remove the following items + + + Original files and folders in this path will not be deleted when removing it from this app + + + Add a folder + + + Theme + + + Language + + + Follow system + + + Light + + + Dark + + + 简体中文 + + + 繁體中文 + + + English + + + This is an open source app + + + Open in new window + + + See source code on GitHub + + + Version + + + None + + + Mica + + + Mica Alt + + + Desktop Acrylic + + + Acrylic Base + + + Acrylic Thin + + + Transparent + + + Backdrop + + + Default + + + Restart app to apply change + + + The path cannot be found on your computer + + + The folder has been added. Please do not add it again. + + + Overlay album art background + + + Dynamic album art background + + + Album art background opacity + + + Settings + + + BetterLyrics + + + Lyrics style + + + Alignment + + + Center + + + Left + + + Right + + + Album art background blur amount + + + Blur amount + + + Adjusting this value will also increase the background blur intensity of the album image. + + + Current value: + + + Significantly higher GPU usage when blur is enabled (> 0) + + + Enabling this feature will slightly increase GPU utilization + + + Top and bottom edge opacity + + + Line spacing + + + x line height + + + Font size + + + Show lyrics only + + + Lyrics effect + + + Album background + + + About + + + Lyrics library + + + App appearance + + + Glow effect + + + Dynamic glow effect + + + Rebuild lyrics index database + + + Rebuild + + + Add + + + Rebuilding the database, please wait... + + + Let's get started now + + + Welcome to BetterLyrics + + + The top area is the title bar + + + Hover on the top area to show it + + + Setup lyrics database now + + + Hover on the bottom left corner and then click to open settings page + + + Hover on the bottom area to show it + + + The bottom area is the command area + + + Toggle "show lyrics only" here + + + When lyrics exist, you can switch them in the lower right corner + + + No music playing now + + \ No newline at end of file diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-CN/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-CN/Resources.resw new file mode 100644 index 0000000..3872a6b --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-CN/Resources.resw @@ -0,0 +1,342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 本地音乐媒体库 + + + 添加存放音乐或歌词的文件夹以构建歌词索引数据库 + + + 在文件资源管理器中打开 + + + 从应用中移除 + + + 您可以安全删除以下项目 + + + 路径中的原始文件和文件夹不会被删除 + + + 添加文件夹 + + + 主题 + + + 语言 + + + 跟随系统 + + + 浅色 + + + 深色 + + + 简体中文 + + + 繁體中文 + + + English + + + 此应用已开源 + + + 在新窗口中打开 + + + 在 GitHub 上查看源代码 + + + 版本号 + + + + + + 云母 + + + 云母(替代样式) + + + 亚克力(桌面) + + + 亚克力(基础) + + + 亚克力(薄层) + + + 透明 + + + 背景材质 + + + 默认 + + + 重启应用以应用更改 + + + 无法在您的计算机中找到该路径 + + + 已添加过该文件夹,请勿重复添加 + + + 叠加专辑图片背景 + + + 动态专辑图片背景 + + + 专辑图片背景不透明度 + + + 设置 + + + BetterLyrics + + + 歌词样式 + + + 对齐方式 + + + 居中 + + + 靠左 + + + 靠右 + + + 专辑图片背景模糊度 + + + 模糊度 + + + 调整该数值将同步提高专辑图片背景模糊强度 + + + 当前值: + + + 启用模糊(> 0)时将显著提升 GPU 占用率 + + + 启用该功能将略微提升 GPU 占用率 + + + 上下边缘不透明度 + + + 行间距 + + + 倍行高 + + + 字体大小 + + + 仅展示歌词 + + + 歌词效果 + + + 专辑背景 + + + 关于 + + + 歌词库 + + + 应用外观 + + + 辉光效果 + + + 动态辉光效果 + + + 重构歌词索引数据库 + + + 重构 + + + 添加 + + + 重构数据库中,请稍候... + + + 来看看怎么使用这款应用吧 + + + 欢迎使用 BetterLyrics + + + 顶部区域是标题栏 + + + 悬停在顶部区域以显示 + + + 现在就来初始化歌词数据库吧 + + + 悬停在左下角后单击以进入设置页面 + + + 悬停在底部区域以显示 + + + 底部区域是命令栏 + + + 在此处切换“仅展示歌词” + + + 当歌词存在时可在右下角进行切换 + + + 当前没有正在播放的音乐 + + \ No newline at end of file diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-TW/Resources.resw b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-TW/Resources.resw new file mode 100644 index 0000000..a4cd688 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Strings/zh-TW/Resources.resw @@ -0,0 +1,342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 本地音樂媒體庫 + + + 新增存放音樂或歌詞的資料夾以建立歌詞索引資料庫 + + + 在檔案總管中開啟 + + + 從應用程式中移除 + + + 您可以安全刪除以下項目 + + + 路徑中的原始檔案和資料夾不會被刪除 + + + 新增資料夾 + + + 主題 + + + 語言 + + + 跟隨系統 + + + 淺色 + + + 深色 + + + 简体中文 + + + 繁體中文 + + + English + + + 此應用程式已開源 + + + 在新視窗中開啟 + + + 在 GitHub 上查看原始碼 + + + 版本號 + + + + + + 雲母 + + + 雲母(替代樣式) + + + 亞克力(桌面) + + + 亞克力(基礎) + + + 亞克力(薄層) + + + 透明 + + + 背景材質 + + + 預設 + + + 重啟應用程式以應用更改 + + + 無法在您的電腦中找到該路徑 + + + 已新增過該資料夾,請勿重複新增 + + + 疊加專輯圖片背景 + + + 動態專輯圖片背景 + + + 專輯圖片背景不透明度 + + + 設定 + + + BetterLyrics + + + 歌詞樣式 + + + 對齊方式 + + + 居中 + + + 靠左 + + + 靠右 + + + 專輯圖片背景模糊度 + + + 模糊度 + + + 調整此數值將同步提升專輯圖片背景模糊強度 + + + 目前值: + + + 啟用模糊(> 0)時將顯著提升 GPU 佔用率 + + + 啟用此功能將略微提升 GPU 佔用率 + + + 上下邊緣不透明度 + + + 行間距 + + + 倍行高 + + + 字體大小 + + + 僅展示歌詞 + + + 歌詞效果 + + + 專輯背景 + + + 關於 + + + 歌詞庫 + + + 應用外觀 + + + 輝光效果 + + + 動態輝光效果 + + + 重構歌詞索引資料庫 + + + 重構 + + + 添加 + + + 重構資料庫中,請稍候... + + + 來看看怎麼使用這款應用程式吧 + + + 歡迎使用 BetterLyrics + + + 頂部區域是標題欄 + + + 懸停在頂部區域以顯示 + + + 現在就來初始化歌詞資料庫吧 + + + 懸停在左下角後點擊以進入設定頁面 + + + 懸停在底部區域以顯示 + + + 底部區域是命令列 + + + 在此切換“僅展示歌詞” + + + 當歌詞存在時可在右下角進行切換 + + + 目前沒有正在播放的音樂 + + \ No newline at end of file diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MainViewModel.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..4d8074a --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/MainViewModel.cs @@ -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 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 GetLyrics(Track? track) { + List 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, 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); + + } + + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/SettingsViewModel.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..c0f69e2 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/SettingsViewModel.cs @@ -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; + } + } + + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml new file mode 100644 index 0000000..17510da --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml @@ -0,0 +1,517 @@ + + + + + + 0,16,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml.cs new file mode 100644 index 0000000..3b9f1a6 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml.cs @@ -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 { + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class MainPage : Page { + + public MainViewModel ViewModel => (MainViewModel)DataContext; + private SettingsService _settingsService; + + private List _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()!; + + if (_settingsService.IsFirstRun) { + WelcomeTeachingTip.IsOpen = true; + } + + _settingsService.PropertyChanged += SettingsService_PropertyChanged; + + DataContext = Ioc.Default.GetService(); + + 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); + } + + /// + /// Note: Non-UI thread + /// + /// + /// + 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); + } + + /// + /// Note: this func is invoked by non-UI thread + /// + /// + /// + 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 GetVisibleLyricsLineIndexBoundaries() { + //Debug.WriteLine($"{_startVisibleLineIndex} {_endVisibleLineIndex}"); + return new Tuple(_startVisibleLineIndex, _endVisibleLineIndex); + } + + private Tuple GetMaxLyricsLineIndexBoundaries() { + if (_lyricsLines.Count == 0) { + return new Tuple(-1, -1); + } + + return new Tuple(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; + } + } +} diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/SettingsPage.xaml b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/SettingsPage.xaml new file mode 100644 index 0000000..d072322 --- /dev/null +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/SettingsPage.xaml @@ -0,0 +1,368 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +