Initial commit after cleanup
63
.gitattributes
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
||||
407
.gitignore
vendored
Normal file
@@ -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
|
||||
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition="'$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' < '15.0'">
|
||||
<VisualStudioVersion>15.0</VisualStudioVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x86">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x86</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x86">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x86</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<WapProjPath Condition="'$(WapProjPath)'==''">$(MSBuildExtensionsPath)\Microsoft\DesktopBridge\</WapProjPath>
|
||||
<PathToXAMLWinRTImplementations>BetterLyrics.WinUI3\</PathToXAMLWinRTImplementations>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.props" />
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>6576cd19-ef92-4099-b37d-e2d8ebdb6bf5</ProjectGuid>
|
||||
<TargetPlatformVersion>10.0.26100.0</TargetPlatformVersion>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<AssetTargetFallback>net8.0-windows$(TargetPlatformVersion);$(AssetTargetFallback)</AssetTargetFallback>
|
||||
<DefaultLanguage>zh-CN</DefaultLanguage>
|
||||
<AppxPackageSigningEnabled>false</AppxPackageSigningEnabled>
|
||||
<EntryPointProjectUniqueName>..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj</EntryPointProjectUniqueName>
|
||||
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
|
||||
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
|
||||
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
|
||||
<GenerateTestArtifacts>True</GenerateTestArtifacts>
|
||||
<AppxBundlePlatforms>x86|x64|arm64</AppxBundlePlatforms>
|
||||
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
|
||||
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'">
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<AppxBundle>Always</AppxBundle>
|
||||
<DefaultLanguage>en-US</DefaultLanguage>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AppxManifest Include="Package.appxmanifest">
|
||||
<SubType>Designer</SubType>
|
||||
</AppxManifest>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Images\LargeTile.scale-100.png" />
|
||||
<Content Include="Images\LargeTile.scale-125.png" />
|
||||
<Content Include="Images\LargeTile.scale-150.png" />
|
||||
<Content Include="Images\LargeTile.scale-200.png" />
|
||||
<Content Include="Images\LargeTile.scale-400.png" />
|
||||
<Content Include="Images\SmallTile.scale-100.png" />
|
||||
<Content Include="Images\SmallTile.scale-125.png" />
|
||||
<Content Include="Images\SmallTile.scale-150.png" />
|
||||
<Content Include="Images\SmallTile.scale-200.png" />
|
||||
<Content Include="Images\SmallTile.scale-400.png" />
|
||||
<Content Include="Images\SplashScreen.scale-100.png" />
|
||||
<Content Include="Images\SplashScreen.scale-125.png" />
|
||||
<Content Include="Images\SplashScreen.scale-150.png" />
|
||||
<Content Include="Images\SplashScreen.scale-200.png" />
|
||||
<Content Include="Images\LockScreenLogo.scale-200.png" />
|
||||
<Content Include="Images\SplashScreen.scale-400.png" />
|
||||
<Content Include="Images\Square150x150Logo.scale-100.png" />
|
||||
<Content Include="Images\Square150x150Logo.scale-125.png" />
|
||||
<Content Include="Images\Square150x150Logo.scale-150.png" />
|
||||
<Content Include="Images\Square150x150Logo.scale-200.png" />
|
||||
<Content Include="Images\Square150x150Logo.scale-400.png" />
|
||||
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-16.png" />
|
||||
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-24.png" />
|
||||
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-256.png" />
|
||||
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-32.png" />
|
||||
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-48.png" />
|
||||
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-16.png" />
|
||||
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-256.png" />
|
||||
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-32.png" />
|
||||
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-48.png" />
|
||||
<Content Include="Images\Square44x44Logo.scale-100.png" />
|
||||
<Content Include="Images\Square44x44Logo.scale-125.png" />
|
||||
<Content Include="Images\Square44x44Logo.scale-150.png" />
|
||||
<Content Include="Images\Square44x44Logo.scale-200.png" />
|
||||
<Content Include="Images\Square44x44Logo.scale-400.png" />
|
||||
<Content Include="Images\Square44x44Logo.targetsize-16.png" />
|
||||
<Content Include="Images\Square44x44Logo.targetsize-24.png" />
|
||||
<Content Include="Images\Square44x44Logo.targetsize-24_altform-unplated.png" />
|
||||
<Content Include="Images\Square44x44Logo.targetsize-256.png" />
|
||||
<Content Include="Images\Square44x44Logo.targetsize-32.png" />
|
||||
<Content Include="Images\Square44x44Logo.targetsize-48.png" />
|
||||
<Content Include="Images\StoreLogo.scale-100.png" />
|
||||
<Content Include="Images\StoreLogo.scale-125.png" />
|
||||
<Content Include="Images\StoreLogo.scale-150.png" />
|
||||
<Content Include="Images\StoreLogo.scale-200.png" />
|
||||
<Content Include="Images\StoreLogo.scale-400.png" />
|
||||
<Content Include="Images\Wide310x150Logo.scale-100.png" />
|
||||
<Content Include="Images\Wide310x150Logo.scale-125.png" />
|
||||
<Content Include="Images\Wide310x150Logo.scale-150.png" />
|
||||
<Content Include="Images\Wide310x150Logo.scale-200.png" />
|
||||
<Content Include="Images\Wide310x150Logo.scale-400.png" />
|
||||
<None Include="Package.StoreAssociation.xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj">
|
||||
<SkipGetTargetFrameworkProperties>True</SkipGetTargetFrameworkProperties>
|
||||
<PublishProfile>Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfile>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />
|
||||
</Project>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 432 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 379 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 379 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 531 B |
|
After Width: | Height: | Size: 703 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 456 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity
|
||||
Name="37412.BetterLyrics"
|
||||
Publisher="CN=E1428B0E-DC1D-4EA4-ACB1-4556569D5BA9"
|
||||
Version="1.0.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="ca4a4830-fc19-40d9-b823-53e2bff3d816" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>BetterLyrics</DisplayName>
|
||||
<PublisherDisplayName>founchoo</PublisherDisplayName>
|
||||
<Logo>Images\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="BetterLyrics"
|
||||
Description="BetterLyrics.WinUI3 (Package)"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/>
|
||||
<uap:SplashScreen Image="Images\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
66
BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml
Normal file
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Application
|
||||
x:Class="BetterLyrics.WinUI3.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converter="using:BetterLyrics.WinUI3.Converter"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:local="using:BetterLyrics.WinUI3">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsExpander/SettingsExpander.xaml" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<!-- Theme -->
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<Color x:Key="SemiTransparentSystemBaseHighColor">#80000000</Color>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<Color x:Key="SemiTransparentSystemBaseHighColor">#80FFFFFF</Color>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<!-- Brush -->
|
||||
<LinearGradientBrush x:Key="BaseHighEdgeHorizontalFadeBrush" StartPoint="0,0.5" EndPoint="1,0.5">
|
||||
<GradientStop Offset="0" Color="Transparent" />
|
||||
<GradientStop Offset="0.02" Color="{ThemeResource SystemBaseHighColor}" />
|
||||
<GradientStop Offset="0.98" Color="{ThemeResource SystemBaseHighColor}" />
|
||||
<GradientStop Offset="1" Color="Transparent" />
|
||||
</LinearGradientBrush>
|
||||
<LinearGradientBrush x:Key="BaseHighEdgeVerticalFadeBrush" StartPoint="0.5,0" EndPoint="0.5,1">
|
||||
<GradientStop Offset="0" Color="Transparent" />
|
||||
<GradientStop Offset="0.15" Color="{ThemeResource SystemBaseHighColor}" />
|
||||
<GradientStop Offset="0.85" Color="{ThemeResource SystemBaseHighColor}" />
|
||||
<GradientStop Offset="1" Color="Transparent" />
|
||||
</LinearGradientBrush>
|
||||
|
||||
<!-- Animation -->
|
||||
<ExponentialEase x:Key="EaseInOut" EasingMode="EaseInOut" />
|
||||
<ExponentialEase x:Key="EaseOut" EasingMode="EaseOut" />
|
||||
<ExponentialEase x:Key="EaseIn" EasingMode="EaseIn" />
|
||||
|
||||
<!-- Converter -->
|
||||
<converter:ThemeTypeToElementThemeConverter x:Key="ThemeTypeToElementThemeConverter" />
|
||||
<converter:ColorToBrushConverter x:Key="ColorToBrushConverter" />
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<converters:BoolNegationConverter x:Key="BoolNegationConverter" />
|
||||
|
||||
<x:Double x:Key="SettingsCardSpacing">4</x:Double>
|
||||
|
||||
<!-- Style (inc. the correct spacing) of a section header -->
|
||||
<Style
|
||||
x:Key="SettingsSectionHeaderTextBlockStyle"
|
||||
BasedOn="{StaticResource BodyStrongTextBlockStyle}"
|
||||
TargetType="TextBlock">
|
||||
<Style.Setters>
|
||||
<Setter Property="Margin" Value="1,30,0,6" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
63
BetterLyrics.WinUI3/BetterLyrics.WinUI3/App.xaml.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using BetterLyrics.WinUI3.Services.Database;
|
||||
using BetterLyrics.WinUI3.Services.Settings;
|
||||
using BetterLyrics.WinUI3.ViewModels;
|
||||
using BetterLyrics.WinUI3.Views;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Windows.ApplicationModel.Resources;
|
||||
using System.Text;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace BetterLyrics.WinUI3 {
|
||||
/// <summary>
|
||||
/// Provides application-specific behavior to supplement the default Application class.
|
||||
/// </summary>
|
||||
public partial class App : Application {
|
||||
public static App Current => (App)Application.Current;
|
||||
public MainWindow? MainWindow { get; private set; }
|
||||
public MainWindow? SettingsWindow { get; set; }
|
||||
|
||||
public static ResourceLoader ResourceLoader = new();
|
||||
|
||||
public static DispatcherQueue DispatcherQueue => DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the singleton application object. This is the first line of authored code
|
||||
/// executed, and as such is the logical equivalent of main() or WinMain().
|
||||
/// </summary>
|
||||
public App() {
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the application is launched.
|
||||
/// </summary>
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) {
|
||||
|
||||
// Register services
|
||||
Ioc.Default.ConfigureServices(
|
||||
new ServiceCollection()
|
||||
.AddSingleton(DispatcherQueue.GetForCurrentThread())
|
||||
// Services
|
||||
.AddSingleton<SettingsService>()
|
||||
.AddSingleton<DatabaseService>()
|
||||
// ViewModels
|
||||
.AddSingleton<MainViewModel>()
|
||||
.AddSingleton<SettingsViewModel>()
|
||||
.BuildServiceProvider());
|
||||
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
||||
// Activate the window
|
||||
MainWindow = new MainWindow();
|
||||
MainWindow!.Navigate(typeof(MainPage));
|
||||
MainWindow.Activate();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
BIN
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Assets/Icon.png
Normal file
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1,48 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>BetterLyrics.WinUI3</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Platforms>x86;x64;ARM64</Platforms>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250513-build.2126" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
|
||||
<PackageReference Include="DevWinUI" Version="8.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.5" />
|
||||
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
|
||||
<PackageReference Include="z440.atl.core" Version="6.24.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Publish Properties -->
|
||||
<PropertyGroup>
|
||||
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
|
||||
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
||||
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
|
||||
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
|
||||
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using System;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper {
|
||||
|
||||
/// <summary>
|
||||
/// Edited based on: https://stackoverflow.com/a/25236507/11048731
|
||||
/// </summary>
|
||||
public class AnimationHelper : DependencyObject {
|
||||
public static int GetAnimationDuration(DependencyObject obj) {
|
||||
return (int)obj.GetValue(AnimationDurationProperty);
|
||||
}
|
||||
|
||||
public static void SetAnimationDuration(DependencyObject obj, int value) {
|
||||
obj.SetValue(AnimationDurationProperty, value);
|
||||
}
|
||||
|
||||
// Using a DependencyProperty as the backing store for AnimationDuration.
|
||||
// This enables animation, styling, binding, etc...
|
||||
public static readonly DependencyProperty AnimationDurationProperty =
|
||||
DependencyProperty.RegisterAttached("AnimationDuration", typeof(int),
|
||||
typeof(AnimationHelper), new PropertyMetadata(0,
|
||||
OnAnimationDurationChanged));
|
||||
|
||||
private static void OnAnimationDurationChanged(DependencyObject d,
|
||||
DependencyPropertyChangedEventArgs e) {
|
||||
FrameworkElement element = d as FrameworkElement;
|
||||
|
||||
var ms = (int)e.NewValue;
|
||||
|
||||
if (ms < 0) return;
|
||||
|
||||
var key = "LyricsLineCharGradientInTextBlock";
|
||||
foreach (var timeline in (element.Resources[key] as Storyboard).Children) {
|
||||
foreach (var keyFrame in (timeline as DoubleAnimationUsingKeyFrames).KeyFrames) {
|
||||
(keyFrame as LinearDoubleKeyFrame).KeyTime =
|
||||
KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(ms));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper {
|
||||
public static class CollectionHelper {
|
||||
public static T? SafeGet<T>(this IList<T> list, int index) {
|
||||
if (list == null || index < 0 || index >= list.Count)
|
||||
return default;
|
||||
return list[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
928
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ColorThief.cs
Normal file
@@ -0,0 +1,928 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Graphics.Imaging;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
/// <summary>
|
||||
/// Color map
|
||||
/// </summary>
|
||||
internal class CMap
|
||||
{
|
||||
private readonly List<VBox> vboxes = new List<VBox>();
|
||||
private List<QuantizedColor> palette;
|
||||
|
||||
public void Push(VBox box)
|
||||
{
|
||||
palette = null;
|
||||
vboxes.Add(box);
|
||||
}
|
||||
|
||||
public List<QuantizedColor> GeneratePalette()
|
||||
{
|
||||
if (palette == null)
|
||||
{
|
||||
palette = (from vBox in vboxes
|
||||
let rgb = vBox.Avg(false)
|
||||
let color = FromRgb(rgb[0], rgb[1], rgb[2])
|
||||
select new QuantizedColor(color, vBox.Count(false))).ToList();
|
||||
}
|
||||
|
||||
return palette;
|
||||
}
|
||||
|
||||
public int Size()
|
||||
{
|
||||
return vboxes.Count;
|
||||
}
|
||||
|
||||
public int[] Map(int[] color)
|
||||
{
|
||||
foreach (var vbox in vboxes.Where(vbox => vbox.Contains(color)))
|
||||
{
|
||||
return vbox.Avg(false);
|
||||
}
|
||||
return Nearest(color);
|
||||
}
|
||||
|
||||
public int[] Nearest(int[] color)
|
||||
{
|
||||
var d1 = double.MaxValue;
|
||||
int[] pColor = null;
|
||||
|
||||
foreach (var t in vboxes)
|
||||
{
|
||||
var vbColor = t.Avg(false);
|
||||
var d2 = Math.Sqrt(Math.Pow(color[0] - vbColor[0], 2)
|
||||
+ Math.Pow(color[1] - vbColor[1], 2)
|
||||
+ Math.Pow(color[2] - vbColor[2], 2));
|
||||
if (d2 < d1)
|
||||
{
|
||||
d1 = d2;
|
||||
pColor = vbColor;
|
||||
}
|
||||
}
|
||||
return pColor;
|
||||
}
|
||||
|
||||
public VBox FindColor(double targetLuma, double minLuma, double maxLuma, double targetSaturation, double minSaturation, double maxSaturation)
|
||||
{
|
||||
VBox max = null;
|
||||
double maxValue = 0;
|
||||
var highestPopulation = vboxes.Select(p => p.Count(false)).Max();
|
||||
|
||||
foreach (var swatch in vboxes)
|
||||
{
|
||||
var avg = swatch.Avg(false);
|
||||
var hsl = FromRgb(avg[0], avg[1], avg[2]).ToHsl();
|
||||
var sat = hsl.S;
|
||||
var luma = hsl.L;
|
||||
|
||||
if (sat >= minSaturation && sat <= maxSaturation &&
|
||||
luma >= minLuma && luma <= maxLuma)
|
||||
{
|
||||
var thisValue = Mmcq.CreateComparisonValue(sat, targetSaturation, luma, targetLuma,
|
||||
swatch.Count(false), highestPopulation);
|
||||
|
||||
if (max == null || thisValue > maxValue)
|
||||
{
|
||||
max = swatch;
|
||||
maxValue = thisValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
public Color FromRgb(int red, int green, int blue)
|
||||
{
|
||||
var color = new Color
|
||||
{
|
||||
A = 255,
|
||||
R = (byte)red,
|
||||
G = (byte)green,
|
||||
B = (byte)blue
|
||||
};
|
||||
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a color in RGB space.
|
||||
/// </summary>
|
||||
public struct Color
|
||||
{
|
||||
/// <summary>
|
||||
/// Get or Set the Alpha component value for sRGB.
|
||||
/// </summary>
|
||||
public byte A;
|
||||
|
||||
/// <summary>
|
||||
/// Get or Set the Blue component value for sRGB.
|
||||
/// </summary>
|
||||
public byte B;
|
||||
|
||||
/// <summary>
|
||||
/// Get or Set the Green component value for sRGB.
|
||||
/// </summary>
|
||||
public byte G;
|
||||
|
||||
/// <summary>
|
||||
/// Get or Set the Red component value for sRGB.
|
||||
/// </summary>
|
||||
public byte R;
|
||||
|
||||
/// <summary>
|
||||
/// Get HSL color.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public HslColor ToHsl()
|
||||
{
|
||||
const double toDouble = 1.0 / 255;
|
||||
var r = toDouble * R;
|
||||
var g = toDouble * G;
|
||||
var b = toDouble * B;
|
||||
var max = Math.Max(Math.Max(r, g), b);
|
||||
var min = Math.Min(Math.Min(r, g), b);
|
||||
var chroma = max - min;
|
||||
double h1;
|
||||
|
||||
// ReSharper disable CompareOfFloatsByEqualityOperator
|
||||
if (chroma == 0)
|
||||
{
|
||||
h1 = 0;
|
||||
}
|
||||
else if (max == r)
|
||||
{
|
||||
h1 = (g - b) / chroma % 6;
|
||||
}
|
||||
else if (max == g)
|
||||
{
|
||||
h1 = 2 + (b - r) / chroma;
|
||||
}
|
||||
else //if (max == b)
|
||||
{
|
||||
h1 = 4 + (r - g) / chroma;
|
||||
}
|
||||
|
||||
var lightness = 0.5 * (max - min);
|
||||
var saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs(2 * lightness - 1));
|
||||
HslColor ret;
|
||||
ret.H = 60 * h1;
|
||||
ret.S = saturation;
|
||||
ret.L = lightness;
|
||||
ret.A = toDouble * A;
|
||||
return ret;
|
||||
// ReSharper restore CompareOfFloatsByEqualityOperator
|
||||
}
|
||||
|
||||
public string ToHexString()
|
||||
{
|
||||
return "#" + R.ToString("X2") + G.ToString("X2") + B.ToString("X2");
|
||||
}
|
||||
|
||||
public string ToHexAlphaString()
|
||||
{
|
||||
return "#" + A.ToString("X2") + R.ToString("X2") + G.ToString("X2") + B.ToString("X2");
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (A == 255)
|
||||
{
|
||||
return ToHexString();
|
||||
}
|
||||
|
||||
return ToHexAlphaString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a color in Hue/Saturation/Lightness (HSL) space.
|
||||
/// </summary>
|
||||
public struct HslColor
|
||||
{
|
||||
/// <summary>
|
||||
/// The Alpha/opacity in 0..1 range.
|
||||
/// </summary>
|
||||
public double A;
|
||||
|
||||
/// <summary>
|
||||
/// The Hue in 0..360 range.
|
||||
/// </summary>
|
||||
public double H;
|
||||
|
||||
/// <summary>
|
||||
/// The Lightness in 0..1 range.
|
||||
/// </summary>
|
||||
public double L;
|
||||
|
||||
/// <summary>
|
||||
/// The Saturation in 0..1 range.
|
||||
/// </summary>
|
||||
public double S;
|
||||
}
|
||||
|
||||
internal static class Mmcq
|
||||
{
|
||||
public const int Sigbits = 5;
|
||||
public const int Rshift = 8 - Sigbits;
|
||||
public const int Mult = 1 << Rshift;
|
||||
public const int Histosize = 1 << (3 * Sigbits);
|
||||
public const int VboxLength = 1 << Sigbits;
|
||||
public const double FractByPopulation = 0.75;
|
||||
public const int MaxIterations = 1000;
|
||||
public const double WeightSaturation = 3d;
|
||||
public const double WeightLuma = 6d;
|
||||
public const double WeightPopulation = 1d;
|
||||
private static readonly VBoxComparer ComparatorProduct = new VBoxComparer();
|
||||
private static readonly VBoxCountComparer ComparatorCount = new VBoxCountComparer();
|
||||
|
||||
public static int GetColorIndex(int r, int g, int b)
|
||||
{
|
||||
return (r << (2 * Sigbits)) + (g << Sigbits) + b;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the histo.
|
||||
/// </summary>
|
||||
/// <param name="pixels">The pixels.</param>
|
||||
/// <returns>Histo (1-d array, giving the number of pixels in each quantized region of color space), or null on error.</returns>
|
||||
private static int[] GetHisto(IEnumerable<byte[]> pixels)
|
||||
{
|
||||
var histo = new int[Histosize];
|
||||
|
||||
foreach (var pixel in pixels)
|
||||
{
|
||||
var rval = pixel[0] >> Rshift;
|
||||
var gval = pixel[1] >> Rshift;
|
||||
var bval = pixel[2] >> Rshift;
|
||||
var index = GetColorIndex(rval, gval, bval);
|
||||
histo[index]++;
|
||||
}
|
||||
return histo;
|
||||
}
|
||||
|
||||
private static VBox VboxFromPixels(IList<byte[]> pixels, int[] histo)
|
||||
{
|
||||
int rmin = 1000000, rmax = 0;
|
||||
int gmin = 1000000, gmax = 0;
|
||||
int bmin = 1000000, bmax = 0;
|
||||
|
||||
// find min/max
|
||||
var numPixels = pixels.Count;
|
||||
for (var i = 0; i < numPixels; i++)
|
||||
{
|
||||
var pixel = pixels[i];
|
||||
var rval = pixel[0] >> Rshift;
|
||||
var gval = pixel[1] >> Rshift;
|
||||
var bval = pixel[2] >> Rshift;
|
||||
|
||||
if (rval < rmin)
|
||||
{
|
||||
rmin = rval;
|
||||
}
|
||||
else if (rval > rmax)
|
||||
{
|
||||
rmax = rval;
|
||||
}
|
||||
|
||||
if (gval < gmin)
|
||||
{
|
||||
gmin = gval;
|
||||
}
|
||||
else if (gval > gmax)
|
||||
{
|
||||
gmax = gval;
|
||||
}
|
||||
|
||||
if (bval < bmin)
|
||||
{
|
||||
bmin = bval;
|
||||
}
|
||||
else if (bval > bmax)
|
||||
{
|
||||
bmax = bval;
|
||||
}
|
||||
}
|
||||
|
||||
return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo);
|
||||
}
|
||||
|
||||
private static VBox[] DoCut(char color, VBox vbox, IList<int> partialsum, IList<int> lookaheadsum, int total)
|
||||
{
|
||||
int vboxDim1;
|
||||
int vboxDim2;
|
||||
|
||||
switch (color)
|
||||
{
|
||||
case 'r':
|
||||
vboxDim1 = vbox.R1;
|
||||
vboxDim2 = vbox.R2;
|
||||
break;
|
||||
case 'g':
|
||||
vboxDim1 = vbox.G1;
|
||||
vboxDim2 = vbox.G2;
|
||||
break;
|
||||
default:
|
||||
vboxDim1 = vbox.B1;
|
||||
vboxDim2 = vbox.B2;
|
||||
break;
|
||||
}
|
||||
|
||||
for (var i = vboxDim1; i <= vboxDim2; i++)
|
||||
{
|
||||
if (partialsum[i] > total / 2)
|
||||
{
|
||||
var vbox1 = vbox.Clone();
|
||||
var vbox2 = vbox.Clone();
|
||||
|
||||
var left = i - vboxDim1;
|
||||
var right = vboxDim2 - i;
|
||||
|
||||
var d2 = left <= right
|
||||
? Math.Min(vboxDim2 - 1, Math.Abs(i + right / 2))
|
||||
: Math.Max(vboxDim1, Math.Abs(Convert.ToInt32(i - 1 - left / 2.0)));
|
||||
|
||||
// avoid 0-count boxes
|
||||
while (d2 < 0 || partialsum[d2] <= 0)
|
||||
{
|
||||
d2++;
|
||||
}
|
||||
var count2 = lookaheadsum[d2];
|
||||
while (count2 == 0 && d2 > 0 && partialsum[d2 - 1] > 0)
|
||||
{
|
||||
count2 = lookaheadsum[--d2];
|
||||
}
|
||||
|
||||
// set dimensions
|
||||
switch (color)
|
||||
{
|
||||
case 'r':
|
||||
vbox1.R2 = d2;
|
||||
vbox2.R1 = d2 + 1;
|
||||
break;
|
||||
case 'g':
|
||||
vbox1.G2 = d2;
|
||||
vbox2.G1 = d2 + 1;
|
||||
break;
|
||||
default:
|
||||
vbox1.B2 = d2;
|
||||
vbox2.B1 = d2 + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
return new[] { vbox1, vbox2 };
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception("VBox can't be cut");
|
||||
}
|
||||
|
||||
private static VBox[] MedianCutApply(IList<int> histo, VBox vbox)
|
||||
{
|
||||
if (vbox.Count(false) == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (vbox.Count(false) == 1)
|
||||
{
|
||||
return new[] { vbox.Clone(), null };
|
||||
}
|
||||
|
||||
// only one pixel, no split
|
||||
|
||||
var rw = vbox.R2 - vbox.R1 + 1;
|
||||
var gw = vbox.G2 - vbox.G1 + 1;
|
||||
var bw = vbox.B2 - vbox.B1 + 1;
|
||||
var maxw = Math.Max(Math.Max(rw, gw), bw);
|
||||
|
||||
// Find the partial sum arrays along the selected axis.
|
||||
var total = 0;
|
||||
var partialsum = new int[VboxLength];
|
||||
// -1 = not set / 0 = 0
|
||||
for (var l = 0; l < partialsum.Length; l++)
|
||||
{
|
||||
partialsum[l] = -1;
|
||||
}
|
||||
|
||||
// -1 = not set / 0 = 0
|
||||
var lookaheadsum = new int[VboxLength];
|
||||
for (var l = 0; l < lookaheadsum.Length; l++)
|
||||
{
|
||||
lookaheadsum[l] = -1;
|
||||
}
|
||||
|
||||
int i, j, k, sum, index;
|
||||
|
||||
if (maxw == rw)
|
||||
{
|
||||
for (i = vbox.R1; i <= vbox.R2; i++)
|
||||
{
|
||||
sum = 0;
|
||||
for (j = vbox.G1; j <= vbox.G2; j++)
|
||||
{
|
||||
for (k = vbox.B1; k <= vbox.B2; k++)
|
||||
{
|
||||
index = GetColorIndex(i, j, k);
|
||||
sum += histo[index];
|
||||
}
|
||||
}
|
||||
total += sum;
|
||||
partialsum[i] = total;
|
||||
}
|
||||
}
|
||||
else if (maxw == gw)
|
||||
{
|
||||
for (i = vbox.G1; i <= vbox.G2; i++)
|
||||
{
|
||||
sum = 0;
|
||||
for (j = vbox.R1; j <= vbox.R2; j++)
|
||||
{
|
||||
for (k = vbox.B1; k <= vbox.B2; k++)
|
||||
{
|
||||
index = GetColorIndex(j, i, k);
|
||||
sum += histo[index];
|
||||
}
|
||||
}
|
||||
total += sum;
|
||||
partialsum[i] = total;
|
||||
}
|
||||
}
|
||||
else /* maxw == bw */
|
||||
{
|
||||
for (i = vbox.B1; i <= vbox.B2; i++)
|
||||
{
|
||||
sum = 0;
|
||||
for (j = vbox.R1; j <= vbox.R2; j++)
|
||||
{
|
||||
for (k = vbox.G1; k <= vbox.G2; k++)
|
||||
{
|
||||
index = GetColorIndex(j, k, i);
|
||||
sum += histo[index];
|
||||
}
|
||||
}
|
||||
total += sum;
|
||||
partialsum[i] = total;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < VboxLength; i++)
|
||||
{
|
||||
if (partialsum[i] != -1)
|
||||
{
|
||||
lookaheadsum[i] = total - partialsum[i];
|
||||
}
|
||||
}
|
||||
|
||||
// determine the cut planes
|
||||
return maxw == rw ? DoCut('r', vbox, partialsum, lookaheadsum, total) : maxw == gw
|
||||
? DoCut('g', vbox, partialsum, lookaheadsum, total) : DoCut('b', vbox, partialsum, lookaheadsum, total);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inner function to do the iteration.
|
||||
/// </summary>
|
||||
/// <param name="lh">The lh.</param>
|
||||
/// <param name="comparator">The comparator.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="histo">The histo.</param>
|
||||
/// <exception cref="System.Exception">vbox1 not defined; shouldn't happen!</exception>
|
||||
private static void Iter(List<VBox> lh, IComparer<VBox> comparator, int target, IList<int> histo)
|
||||
{
|
||||
var ncolors = 1;
|
||||
var niters = 0;
|
||||
|
||||
while (niters < MaxIterations)
|
||||
{
|
||||
var vbox = lh[lh.Count - 1];
|
||||
if (vbox.Count(false) == 0)
|
||||
{
|
||||
lh.Sort(comparator);
|
||||
niters++;
|
||||
continue;
|
||||
}
|
||||
|
||||
lh.RemoveAt(lh.Count - 1);
|
||||
|
||||
// do the cut
|
||||
var vboxes = MedianCutApply(histo, vbox);
|
||||
var vbox1 = vboxes[0];
|
||||
var vbox2 = vboxes[1];
|
||||
|
||||
if (vbox1 == null)
|
||||
{
|
||||
throw new Exception(
|
||||
"vbox1 not defined; shouldn't happen!");
|
||||
}
|
||||
|
||||
lh.Add(vbox1);
|
||||
if (vbox2 != null)
|
||||
{
|
||||
lh.Add(vbox2);
|
||||
ncolors++;
|
||||
}
|
||||
lh.Sort(comparator);
|
||||
|
||||
if (ncolors >= target)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (niters++ > MaxIterations)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static CMap Quantize(byte[][] pixels, int maxcolors)
|
||||
{
|
||||
// short-circuit
|
||||
if (pixels.Length == 0 || maxcolors < 2 || maxcolors > 256)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var histo = GetHisto(pixels);
|
||||
|
||||
// get the beginning vbox from the colors
|
||||
var vbox = VboxFromPixels(pixels, histo);
|
||||
var pq = new List<VBox> { vbox };
|
||||
|
||||
// Round up to have the same behaviour as in JavaScript
|
||||
var target = (int)Math.Ceiling(FractByPopulation * maxcolors);
|
||||
|
||||
// first set of colors, sorted by population
|
||||
Iter(pq, ComparatorCount, target, histo);
|
||||
|
||||
// Re-sort by the product of pixel occupancy times the size in color
|
||||
// space.
|
||||
pq.Sort(ComparatorProduct);
|
||||
|
||||
// next set - generate the median cuts using the (npix * vol) sorting.
|
||||
Iter(pq, ComparatorProduct, maxcolors - pq.Count, histo);
|
||||
|
||||
// Reverse to put the highest elements first into the color map
|
||||
pq.Reverse();
|
||||
|
||||
// calculate the actual colors
|
||||
var cmap = new CMap();
|
||||
foreach (var vb in pq)
|
||||
{
|
||||
cmap.Push(vb);
|
||||
}
|
||||
|
||||
return cmap;
|
||||
}
|
||||
|
||||
public static double CreateComparisonValue(double saturation, double targetSaturation, double luma, double targetLuma, int population, int highestPopulation)
|
||||
{
|
||||
return WeightedMean(InvertDiff(saturation, targetSaturation), WeightSaturation,
|
||||
InvertDiff(luma, targetLuma), WeightLuma,
|
||||
population / (double)highestPopulation, WeightPopulation);
|
||||
}
|
||||
|
||||
private static double WeightedMean(params double[] values)
|
||||
{
|
||||
double sum = 0;
|
||||
double sumWeight = 0;
|
||||
|
||||
for (var i = 0; i < values.Length; i += 2)
|
||||
{
|
||||
var value = values[i];
|
||||
var weight = values[i + 1];
|
||||
|
||||
sum += value * weight;
|
||||
sumWeight += weight;
|
||||
}
|
||||
|
||||
return sum / sumWeight;
|
||||
}
|
||||
|
||||
private static double InvertDiff(double value, double targetValue)
|
||||
{
|
||||
return 1 - Math.Abs(value - targetValue);
|
||||
}
|
||||
}
|
||||
|
||||
public class QuantizedColor
|
||||
{
|
||||
public QuantizedColor(Color color, int population)
|
||||
{
|
||||
Color = color;
|
||||
Population = population;
|
||||
IsDark = CalculateYiqLuma(color) < 128;
|
||||
}
|
||||
|
||||
public Color Color { get; private set; }
|
||||
public int Population { get; private set; }
|
||||
public bool IsDark { get; private set; }
|
||||
|
||||
public int CalculateYiqLuma(Color color)
|
||||
{
|
||||
return Convert.ToInt32(Math.Round((299 * color.R + 587 * color.G + 114 * color.B) / 1000f));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 3D color space box.
|
||||
/// </summary>
|
||||
internal class VBox
|
||||
{
|
||||
private readonly int[] histo;
|
||||
private int[] avg;
|
||||
public int B1;
|
||||
public int B2;
|
||||
private int? count;
|
||||
public int G1;
|
||||
public int G2;
|
||||
public int R1;
|
||||
public int R2;
|
||||
private int? volume;
|
||||
|
||||
public VBox(int r1, int r2, int g1, int g2, int b1, int b2, int[] histo)
|
||||
{
|
||||
R1 = r1;
|
||||
R2 = r2;
|
||||
G1 = g1;
|
||||
G2 = g2;
|
||||
B1 = b1;
|
||||
B2 = b2;
|
||||
|
||||
this.histo = histo;
|
||||
}
|
||||
|
||||
public int Volume(bool force)
|
||||
{
|
||||
if (volume == null || force)
|
||||
{
|
||||
volume = (R2 - R1 + 1) * (G2 - G1 + 1) * (B2 - B1 + 1);
|
||||
}
|
||||
|
||||
return volume.Value;
|
||||
}
|
||||
|
||||
public int Count(bool force)
|
||||
{
|
||||
if (count == null || force)
|
||||
{
|
||||
var npix = 0;
|
||||
int i;
|
||||
|
||||
for (i = R1; i <= R2; i++)
|
||||
{
|
||||
int j;
|
||||
for (j = G1; j <= G2; j++)
|
||||
{
|
||||
int k;
|
||||
for (k = B1; k <= B2; k++)
|
||||
{
|
||||
var index = Mmcq.GetColorIndex(i, j, k);
|
||||
npix += histo[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
count = npix;
|
||||
}
|
||||
|
||||
return count.Value;
|
||||
}
|
||||
|
||||
public VBox Clone()
|
||||
{
|
||||
return new VBox(R1, R2, G1, G2, B1, B2, histo);
|
||||
}
|
||||
|
||||
public int[] Avg(bool force)
|
||||
{
|
||||
if (avg == null || force)
|
||||
{
|
||||
var ntot = 0;
|
||||
|
||||
var rsum = 0;
|
||||
var gsum = 0;
|
||||
var bsum = 0;
|
||||
|
||||
int i;
|
||||
|
||||
for (i = R1; i <= R2; i++)
|
||||
{
|
||||
int j;
|
||||
for (j = G1; j <= G2; j++)
|
||||
{
|
||||
int k;
|
||||
for (k = B1; k <= B2; k++)
|
||||
{
|
||||
var histoindex = Mmcq.GetColorIndex(i, j, k);
|
||||
var hval = histo[histoindex];
|
||||
ntot += hval;
|
||||
rsum += Convert.ToInt32((hval * (i + 0.5) * Mmcq.Mult));
|
||||
gsum += Convert.ToInt32((hval * (j + 0.5) * Mmcq.Mult));
|
||||
bsum += Convert.ToInt32((hval * (k + 0.5) * Mmcq.Mult));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ntot > 0)
|
||||
{
|
||||
avg = new[]
|
||||
{
|
||||
Math.Abs(rsum / ntot), Math.Abs(gsum / ntot),
|
||||
Math.Abs(bsum / ntot)
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
avg = new[]
|
||||
{
|
||||
Math.Abs(Mmcq.Mult * (R1 + R2 + 1) / 2),
|
||||
Math.Abs(Mmcq.Mult * (G1 + G2 + 1) / 2),
|
||||
Math.Abs(Mmcq.Mult * (B1 + B2 + 1) / 2)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return avg;
|
||||
}
|
||||
|
||||
public bool Contains(int[] pixel)
|
||||
{
|
||||
var rval = pixel[0] >> Mmcq.Rshift;
|
||||
var gval = pixel[1] >> Mmcq.Rshift;
|
||||
var bval = pixel[2] >> Mmcq.Rshift;
|
||||
|
||||
return rval >= R1 && rval <= R2 && gval >= G1 && gval <= G2 && bval >= B1 && bval <= B2;
|
||||
}
|
||||
}
|
||||
|
||||
internal class VBoxCountComparer : IComparer<VBox>
|
||||
{
|
||||
public int Compare(VBox x, VBox y)
|
||||
{
|
||||
var a = x.Count(false);
|
||||
var b = y.Count(false);
|
||||
return a < b ? -1 : (a > b ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
internal class VBoxComparer : IComparer<VBox>
|
||||
{
|
||||
public int Compare(VBox x, VBox y)
|
||||
{
|
||||
var aCount = x.Count(false);
|
||||
var bCount = y.Count(false);
|
||||
var aVolume = x.Volume(false);
|
||||
var bVolume = y.Volume(false);
|
||||
|
||||
// Otherwise sort by products
|
||||
var a = aCount * aVolume;
|
||||
var b = bCount * bVolume;
|
||||
return a < b ? -1 : (a > b ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
public class ColorThief
|
||||
{
|
||||
public const int DefaultColorCount = 5;
|
||||
public const int DefaultQuality = 10;
|
||||
public const bool DefaultIgnoreWhite = true;
|
||||
public const int ColorDepth = 4;
|
||||
|
||||
private CMap GetColorMap(byte[][] pixelArray, int colorCount)
|
||||
{
|
||||
// Send array to quantize function which clusters values using median
|
||||
// cut algorithm
|
||||
|
||||
if (colorCount > 0)
|
||||
{
|
||||
--colorCount;
|
||||
}
|
||||
|
||||
var cmap = Mmcq.Quantize(pixelArray, colorCount);
|
||||
return cmap;
|
||||
}
|
||||
|
||||
private byte[][] ConvertPixels(byte[] pixels, int pixelCount, int quality, bool ignoreWhite)
|
||||
{
|
||||
|
||||
|
||||
var expectedDataLength = pixelCount * ColorDepth;
|
||||
if (expectedDataLength != pixels.Length)
|
||||
{
|
||||
throw new ArgumentException("(expectedDataLength = "
|
||||
+ expectedDataLength + ") != (pixels.length = "
|
||||
+ pixels.Length + ")");
|
||||
}
|
||||
|
||||
// Store the RGB values in an array format suitable for quantize
|
||||
// function
|
||||
|
||||
// numRegardedPixels must be rounded up to avoid an
|
||||
// ArrayIndexOutOfBoundsException if all pixels are good.
|
||||
|
||||
var numRegardedPixels = (pixelCount + quality - 1) / quality;
|
||||
|
||||
var numUsedPixels = 0;
|
||||
var pixelArray = new byte[numRegardedPixels][];
|
||||
|
||||
for (var i = 0; i < pixelCount; i += quality)
|
||||
{
|
||||
var offset = i * ColorDepth;
|
||||
var b = pixels[offset];
|
||||
var g = pixels[offset + 1];
|
||||
var r = pixels[offset + 2];
|
||||
var a = pixels[offset + 3];
|
||||
|
||||
// If pixel is mostly opaque and not white
|
||||
if (a >= 125 && !(ignoreWhite && r > 250 && g > 250 && b > 250))
|
||||
{
|
||||
pixelArray[numUsedPixels] = new[] { r, g, b };
|
||||
numUsedPixels++;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove unused pixels from the array
|
||||
var copy = new byte[numUsedPixels][];
|
||||
Array.Copy(pixelArray, copy, numUsedPixels);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use the median cut algorithm to cluster similar colors and return the base color from the largest cluster.
|
||||
/// </summary>
|
||||
/// <param name="sourceImage">The source image.</param>
|
||||
/// <param name="quality">
|
||||
/// 1 is the highest quality settings. 10 is the default. There is
|
||||
/// a trade-off between quality and speed. The bigger the number,
|
||||
/// the faster a color will be returned but the greater the
|
||||
/// likelihood that it will not be the visually most dominant color.
|
||||
/// </param>
|
||||
/// <param name="ignoreWhite">if set to <c>true</c> [ignore white].</param>
|
||||
/// <returns></returns>
|
||||
public async Task<QuantizedColor> GetColor(BitmapDecoder sourceImage, int quality = DefaultQuality, bool ignoreWhite = DefaultIgnoreWhite)
|
||||
{
|
||||
var palette = await GetPalette(sourceImage, 3, quality, ignoreWhite);
|
||||
|
||||
var dominantColor = new QuantizedColor(new Color
|
||||
{
|
||||
A = Convert.ToByte(palette.Average(a => a.Color.A)),
|
||||
R = Convert.ToByte(palette.Average(a => a.Color.R)),
|
||||
G = Convert.ToByte(palette.Average(a => a.Color.G)),
|
||||
B = Convert.ToByte(palette.Average(a => a.Color.B))
|
||||
}, Convert.ToInt32(palette.Average(a => a.Population)));
|
||||
|
||||
return dominantColor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use the median cut algorithm to cluster similar colors.
|
||||
/// </summary>
|
||||
/// <param name="sourceImage">The source image.</param>
|
||||
/// <param name="colorCount">The color count.</param>
|
||||
/// <param name="quality">
|
||||
/// 1 is the highest quality settings. 10 is the default. There is
|
||||
/// a trade-off between quality and speed. The bigger the number,
|
||||
/// the faster a color will be returned but the greater the
|
||||
/// likelihood that it will not be the visually most dominant color.
|
||||
/// </param>
|
||||
/// <param name="ignoreWhite">if set to <c>true</c> [ignore white].</param>
|
||||
/// <returns></returns>
|
||||
/// <code>true</code>
|
||||
public async Task<List<QuantizedColor>> GetPalette(BitmapDecoder sourceImage, int colorCount = DefaultColorCount, int quality = DefaultQuality, bool ignoreWhite = DefaultIgnoreWhite)
|
||||
{
|
||||
var pixelArray = await GetPixelsFast(sourceImage, quality, ignoreWhite);
|
||||
var cmap = GetColorMap(pixelArray, colorCount);
|
||||
if (cmap != null)
|
||||
{
|
||||
var colors = cmap.GeneratePalette();
|
||||
return colors;
|
||||
}
|
||||
return new List<QuantizedColor>();
|
||||
}
|
||||
|
||||
private async Task<byte[]> GetIntFromPixel(BitmapDecoder decoder)
|
||||
{
|
||||
var pixelsData = await decoder.GetPixelDataAsync();
|
||||
var pixels = pixelsData.DetachPixelData();
|
||||
return pixels;
|
||||
}
|
||||
|
||||
private async Task<byte[][]> GetPixelsFast(BitmapDecoder sourceImage, int quality, bool ignoreWhite)
|
||||
{
|
||||
if (quality < 1)
|
||||
{
|
||||
quality = DefaultQuality;
|
||||
}
|
||||
|
||||
var pixels = await GetIntFromPixel(sourceImage);
|
||||
var pixelCount = sourceImage.PixelWidth * sourceImage.PixelHeight;
|
||||
|
||||
return ConvertPixels(pixels, Convert.ToInt32(pixelCount), quality, ignoreWhite);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using SQLite;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper {
|
||||
public class DatabaseHelper {
|
||||
private static SQLiteConnection _database;
|
||||
|
||||
public static SQLiteConnection InitializeDatabase() {
|
||||
string dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MusicMetadataIndex.db");
|
||||
_database = new SQLiteConnection(dbPath);
|
||||
_database.CreateTable<MetadataIndex>(); // Create table if it doesn't exist
|
||||
return _database;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper {
|
||||
public class EasingHelper {
|
||||
|
||||
/// <summary>
|
||||
/// No easing
|
||||
/// </summary>
|
||||
public static float Linear(float t) => t;
|
||||
|
||||
/// <summary>
|
||||
/// Accelerating from 0
|
||||
/// </summary>
|
||||
public static float EaseInQuad(float t) => t * t;
|
||||
|
||||
/// <summary>
|
||||
/// Decelerating to 0
|
||||
/// </summary>
|
||||
public static float EaseOutQuad(float t) => t * (2 - t);
|
||||
|
||||
/// <summary>
|
||||
/// Acceleration until halfway then deceleration
|
||||
/// </summary>
|
||||
public static float EaseInOutQuad(float t) {
|
||||
return t < 0.5f
|
||||
? 2 * t * t
|
||||
: -1 + (4 - 2 * t) * t;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Smoother transition than linear
|
||||
/// </summary>
|
||||
public static float SmoothStep(float t) {
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Even smoother transition with continuous first and second derivatives
|
||||
/// </summary>
|
||||
public static float SmootherStep(float t) {
|
||||
return t * t * t * (t * (6 * t - 15) + 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using System;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper
|
||||
{
|
||||
public class ImageHelper
|
||||
{
|
||||
public static async Task<InMemoryRandomAccessStream> GetStreamFromBytesAsync(byte[] imageBytes)
|
||||
{
|
||||
if (imageBytes == null || imageBytes.Length == 0)
|
||||
return null;
|
||||
|
||||
InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream();
|
||||
await stream.WriteAsync(imageBytes.AsBuffer());
|
||||
stream.Seek(0);
|
||||
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/MathHelper.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper {
|
||||
public class MathHelper {
|
||||
public static List<int> GetAllFactors(int n) {
|
||||
var result = new SortedSet<int>();
|
||||
|
||||
for (int i = 1; i <= Math.Sqrt(n); i++) {
|
||||
if (n % i == 0) {
|
||||
result.Add(i);
|
||||
result.Add(n / i);
|
||||
}
|
||||
}
|
||||
|
||||
return [.. result];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Helper {
|
||||
public class VisualHelper {
|
||||
|
||||
/// <summary>
|
||||
/// Source: https://stackoverflow.com/a/61626933/11048731
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="depObj"></param>
|
||||
/// <returns></returns>
|
||||
public static List<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject {
|
||||
List<T> list = new List<T>();
|
||||
if (depObj != null) {
|
||||
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++) {
|
||||
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
|
||||
if (child != null && child is T) {
|
||||
list.Add((T)child);
|
||||
}
|
||||
|
||||
List<T> childItems = FindVisualChildren<T>(child);
|
||||
if (childItems != null && childItems.Count() > 0) {
|
||||
foreach (var item in childItems) {
|
||||
list.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
191
BetterLyrics.WinUI3/BetterLyrics.WinUI3/MainWindow.xaml
Normal file
@@ -0,0 +1,191 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Window
|
||||
x:Class="BetterLyrics.WinUI3.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:local="using:BetterLyrics.WinUI3"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:media="using:CommunityToolkit.WinUI.Media"
|
||||
SizeChanged="Window_SizeChanged"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid x:Name="RootGrid">
|
||||
|
||||
<Frame
|
||||
x:Name="RootFrame"
|
||||
Navigated="RootFrame_Navigated"
|
||||
NavigationFailed="RootFrame_NavigationFailed" />
|
||||
|
||||
<Grid
|
||||
x:Name="TopCommandGrid"
|
||||
Padding="2,0"
|
||||
VerticalAlignment="Top"
|
||||
Background="Transparent"
|
||||
Opacity="0">
|
||||
|
||||
<Grid.Resources>
|
||||
|
||||
<Storyboard x:Name="TopCommandGridFadeInStoryboard">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="TopCommandGrid"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
<Storyboard x:Name="TopCommandGridFadeOutStoryboard">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="TopCommandGrid"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="0"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
|
||||
</Grid.Resources>
|
||||
|
||||
<interactivity:Interaction.Behaviors>
|
||||
|
||||
<interactivity:EventTriggerBehavior EventName="PointerEntered">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TopCommandGridFadeInStoryboard}" />
|
||||
</interactivity:EventTriggerBehavior>
|
||||
<interactivity:EventTriggerBehavior EventName="PointerExited">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TopCommandGridFadeOutStoryboard}" />
|
||||
</interactivity:EventTriggerBehavior>
|
||||
|
||||
</interactivity:Interaction.Behaviors>
|
||||
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
||||
<!-- Always On Top -->
|
||||
<AppBarButton
|
||||
x:Name="AOTButton"
|
||||
Click="AOTButton_Click"
|
||||
LabelPosition="Collapsed">
|
||||
<Grid>
|
||||
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="" />
|
||||
<FontIcon
|
||||
x:Name="PinnedFontIcon"
|
||||
FontFamily="Segoe Fluent Icons"
|
||||
Glyph=""
|
||||
Opacity="0">
|
||||
<FontIcon.Resources>
|
||||
<Storyboard x:Key="ShowPinnedFontIconStoryboard">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="PinnedFontIcon"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1"
|
||||
Duration="0:0:0.3" />
|
||||
</Storyboard>
|
||||
<Storyboard x:Key="HidePinnedFontIconStoryboard">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="PinnedFontIcon"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="0"
|
||||
Duration="0:0:0.3" />
|
||||
</Storyboard>
|
||||
</FontIcon.Resources>
|
||||
</FontIcon>
|
||||
</Grid>
|
||||
</AppBarButton>
|
||||
|
||||
<TextBlock
|
||||
x:Name="AppTitleTextBlock"
|
||||
Margin="0,-4,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Title}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||
<!-- Window Mini -->
|
||||
<AppBarButton
|
||||
x:Name="MiniButton"
|
||||
Click="MiniButton_Click"
|
||||
LabelPosition="Collapsed"
|
||||
Visibility="Collapsed">
|
||||
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="" />
|
||||
</AppBarButton>
|
||||
<!-- Window Unmini -->
|
||||
<AppBarButton
|
||||
x:Name="UnminiButton"
|
||||
Click="UnminiButton_Click"
|
||||
LabelPosition="Collapsed"
|
||||
Visibility="Collapsed">
|
||||
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="" />
|
||||
</AppBarButton>
|
||||
<!-- Window Minimise -->
|
||||
<AppBarButton
|
||||
x:Name="MinimiseButton"
|
||||
Click="MinimiseButton_Click"
|
||||
LabelPosition="Collapsed">
|
||||
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="" />
|
||||
</AppBarButton>
|
||||
<!-- Window Maximise -->
|
||||
<AppBarButton
|
||||
x:Name="MaximiseButton"
|
||||
Click="MaximiseButton_Click"
|
||||
LabelPosition="Collapsed">
|
||||
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="" />
|
||||
</AppBarButton>
|
||||
<!-- Window Restore -->
|
||||
<AppBarButton
|
||||
x:Name="RestoreButton"
|
||||
Click="RestoreButton_Click"
|
||||
LabelPosition="Collapsed"
|
||||
Visibility="Collapsed">
|
||||
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="" />
|
||||
</AppBarButton>
|
||||
<!-- Window Close -->
|
||||
<AppBarButton
|
||||
x:Name="CloseButton"
|
||||
Click="CloseButton_Click"
|
||||
LabelPosition="Collapsed">
|
||||
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="" />
|
||||
</AppBarButton>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
<InfoBar
|
||||
x:Name="HostInfoBar"
|
||||
Margin="18"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
IsClosable="False"
|
||||
Opacity="0">
|
||||
<InfoBar.RenderTransform>
|
||||
<TranslateTransform x:Name="HostInfoBarTransform" Y="20" />
|
||||
</InfoBar.RenderTransform>
|
||||
<InfoBar.Resources>
|
||||
<Storyboard x:Key="InfoBarShowAndHideStoryboard">
|
||||
<!-- Opacity -->
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="HostInfoBar" Storyboard.TargetProperty="Opacity">
|
||||
<DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="1" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:3.6" Value="1" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:3.9" Value="0" />
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
|
||||
<!-- Y -->
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="HostInfoBarTransform" Storyboard.TargetProperty="Y">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="20" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="0" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:3.6" Value="0" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:3.9" Value="20" />
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</InfoBar.Resources>
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{Binding ElementName=HostInfoBar, Path=IsOpen, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource InfoBarShowAndHideStoryboard}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
</interactivity:Interaction.Behaviors>
|
||||
</InfoBar>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
152
BetterLyrics.WinUI3/BetterLyrics.WinUI3/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Messages;
|
||||
using BetterLyrics.WinUI3.Services.Settings;
|
||||
using BetterLyrics.WinUI3.ViewModels;
|
||||
using BetterLyrics.WinUI3.Views;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI.Behaviors;
|
||||
using DevWinUI;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using WinRT;
|
||||
using WinRT.Interop;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace BetterLyrics.WinUI3 {
|
||||
/// <summary>
|
||||
/// An empty window that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class MainWindow : Window {
|
||||
|
||||
private readonly OverlappedPresenter _presenter;
|
||||
|
||||
private SettingsService _settingsService;
|
||||
|
||||
public static StackedNotificationsBehavior? StackedNotificationsBehavior { get; private set; }
|
||||
|
||||
public MainWindow() {
|
||||
this.InitializeComponent();
|
||||
|
||||
_settingsService = Ioc.Default.GetService<SettingsService>();
|
||||
|
||||
RootGrid.RequestedTheme = (ElementTheme)_settingsService.Theme;
|
||||
SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop((BackdropType)_settingsService.BackdropType);
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ThemeChangedMessage>(this, (r, m) => {
|
||||
RootGrid.RequestedTheme = m.Value;
|
||||
});
|
||||
|
||||
WeakReferenceMessenger.Default.Register<SystemBackdropChangedMessage>(this, (r, m) => {
|
||||
SystemBackdrop = null;
|
||||
SystemBackdrop = SystemBackdropHelper.CreateSystemBackdrop(m.Value);
|
||||
});
|
||||
|
||||
// AppWindow.SetIcon("white_round.ico");
|
||||
StackedNotificationsBehavior = NotificationQueue;
|
||||
|
||||
_presenter = (OverlappedPresenter)AppWindow.Presenter;
|
||||
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
||||
SetTitleBar(TopCommandGrid);
|
||||
}
|
||||
|
||||
public void Navigate(Type type) {
|
||||
RootFrame.Navigate(type);
|
||||
}
|
||||
|
||||
private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e) {
|
||||
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
|
||||
}
|
||||
|
||||
private void BackButton_Click(object sender, RoutedEventArgs e) {
|
||||
if (RootFrame.CanGoBack) {
|
||||
RootFrame.GoBack();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseButton_Click(object sender, RoutedEventArgs e) {
|
||||
if (RootFrame.CurrentSourcePageType == typeof(MainPage)) {
|
||||
App.Current.Exit();
|
||||
} else if (RootFrame.CurrentSourcePageType == typeof(SettingsPage)) {
|
||||
App.Current.SettingsWindow!.AppWindow.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void MaximiseButton_Click(object sender, RoutedEventArgs e) {
|
||||
_presenter.Maximize();
|
||||
//MaximiseButton.Visibility = Visibility.Collapsed;
|
||||
//RestoreButton.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void MinimiseButton_Click(object sender, RoutedEventArgs e) {
|
||||
_presenter.Minimize();
|
||||
}
|
||||
|
||||
private void RestoreButton_Click(object sender, RoutedEventArgs e) {
|
||||
_presenter.Restore();
|
||||
//MaximiseButton.Visibility = Visibility.Visible;
|
||||
//RestoreButton.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void Window_SizeChanged(object sender, WindowSizeChangedEventArgs args) {
|
||||
if (_presenter.State == OverlappedPresenterState.Maximized) {
|
||||
MaximiseButton.Visibility = Visibility.Collapsed;
|
||||
RestoreButton.Visibility = Visibility.Visible;
|
||||
} else if (_presenter.State == OverlappedPresenterState.Restored) {
|
||||
MaximiseButton.Visibility = Visibility.Visible;
|
||||
RestoreButton.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void MiniButton_Click(object sender, RoutedEventArgs e) {
|
||||
AppWindow.Resize(new Windows.Graphics.SizeInt32(144, 48));
|
||||
MiniButton.Visibility = Visibility.Collapsed;
|
||||
UnminiButton.Visibility = Visibility.Visible;
|
||||
MinimiseButton.Visibility = Visibility.Collapsed;
|
||||
MaximiseButton.Visibility = Visibility.Collapsed;
|
||||
RestoreButton.Visibility = Visibility.Collapsed;
|
||||
CloseButton.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void UnminiButton_Click(object sender, RoutedEventArgs e) {
|
||||
AppWindow.Resize(new Windows.Graphics.SizeInt32(800, 600));
|
||||
MiniButton.Visibility = Visibility.Visible;
|
||||
UnminiButton.Visibility = Visibility.Collapsed;
|
||||
MinimiseButton.Visibility = Visibility.Visible;
|
||||
MaximiseButton.Visibility = Visibility.Visible;
|
||||
RestoreButton.Visibility = Visibility.Collapsed;
|
||||
CloseButton.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void RootFrame_Navigated(object sender, NavigationEventArgs e) {
|
||||
AppWindow.Title = Title = App.ResourceLoader.GetString($"{e.SourcePageType.Name}Title");
|
||||
}
|
||||
|
||||
private void AOTButton_Click(object sender, RoutedEventArgs e) {
|
||||
_presenter.IsAlwaysOnTop = !_presenter.IsAlwaysOnTop;
|
||||
string prefix;
|
||||
if (_presenter.IsAlwaysOnTop) {
|
||||
prefix = "Show";
|
||||
} else {
|
||||
prefix = "Hide";
|
||||
}
|
||||
(PinnedFontIcon.Resources[$"{prefix}PinnedFontIconStoryboard"] as Storyboard)!.Begin();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using DevWinUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Messages {
|
||||
public class SystemBackdropChangedMessage : ValueChangedMessage<BackdropType> {
|
||||
public SystemBackdropChangedMessage(BackdropType value) : base(value) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
using Microsoft.UI.Xaml;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Messages {
|
||||
public class ThemeChangedMessage : ValueChangedMessage<ElementTheme> {
|
||||
public ThemeChangedMessage(ElementTheme value) : base(value) {
|
||||
}
|
||||
}
|
||||
}
|
||||
14
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/Language.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models {
|
||||
public enum Language {
|
||||
FollowSystem,
|
||||
English,
|
||||
SimplifiedChinese,
|
||||
TraditionalChinese,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
42
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Models/LyricsLine.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.Graphics.Canvas.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models {
|
||||
public class LyricsLine {
|
||||
|
||||
public List<string> Texts { get; set; } = [];
|
||||
|
||||
public int LanguageIndex { get; set; } = 0;
|
||||
|
||||
public string Text => Texts[LanguageIndex];
|
||||
|
||||
public int StartPlayingTimestampMs { get; set; }
|
||||
public int EndPlayingTimestampMs { get; set; }
|
||||
|
||||
public LyricsPlayingState PlayingState { get; set; }
|
||||
|
||||
public int DurationMs => EndPlayingTimestampMs - StartPlayingTimestampMs;
|
||||
|
||||
public float EnteringProgress { get; set; }
|
||||
|
||||
public float ExitingProgress { get; set; }
|
||||
|
||||
public float PlayingProgress { get; set; }
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
public Vector2 CenterPosition { get; set; }
|
||||
|
||||
public float Scale { get; set; }
|
||||
|
||||
public float Opacity { get; set; }
|
||||
|
||||
public CanvasTextLayout TextLayout { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Models {
|
||||
public enum LyricsPlayingState {
|
||||
/// <summary>
|
||||
/// Not played yet, will be playing in the future
|
||||
/// </summary>
|
||||
NotPlayed,
|
||||
/// <summary>
|
||||
/// Playing
|
||||
/// </summary>
|
||||
Playing,
|
||||
/// <summary>
|
||||
/// Has already played
|
||||
/// </summary>
|
||||
Played
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using ATL;
|
||||
using BetterLyrics.WinUI3.Messages;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.Settings;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.UI.Xaml;
|
||||
using SQLite;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Ude;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.Database {
|
||||
public class DatabaseService {
|
||||
|
||||
private readonly SQLiteConnection _connection;
|
||||
private readonly CharsetDetector _charsetDetector = new();
|
||||
|
||||
public DatabaseService() {
|
||||
string dbPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"MusicMetadataIndex.db"
|
||||
);
|
||||
_connection = new SQLiteConnection(dbPath);
|
||||
_connection.CreateTable<MetadataIndex>();
|
||||
}
|
||||
|
||||
public async Task RebuildMusicMetadataIndexDatabaseAsync(IList<MusicFolder> musicFolders) {
|
||||
await Task.Run(() => {
|
||||
_connection.DeleteAll<MetadataIndex>();
|
||||
foreach (var localMusicFolder in musicFolders) {
|
||||
if (localMusicFolder.IsValid) {
|
||||
foreach (var file in Directory.GetFiles(localMusicFolder.Path)) {
|
||||
var fileExtension = Path.GetExtension(file);
|
||||
var track = new Track(file);
|
||||
_connection.Insert(new MetadataIndex {
|
||||
Path = file,
|
||||
Title = track.Title,
|
||||
Artist = track.Artist,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Track? GetMusicMetadata(string? title, string? artist) {
|
||||
var founds = _connection.Table<MetadataIndex>()
|
||||
.Where(m => m.Title == title && m.Artist == artist).ToList();
|
||||
if (founds == null || founds.Count == 0) {
|
||||
return null;
|
||||
} else {
|
||||
var first = new Track(founds[0].Path);
|
||||
if (founds.Count == 1) {
|
||||
return first;
|
||||
} else {
|
||||
if (first.Lyrics.Exists()) {
|
||||
return first;
|
||||
} else {
|
||||
foreach (var found in founds) {
|
||||
if (found.Path.EndsWith(".lrc")) {
|
||||
using (FileStream fs = File.OpenRead(found.Path)) {
|
||||
_charsetDetector.Feed(fs);
|
||||
_charsetDetector.DataEnd();
|
||||
}
|
||||
|
||||
string content;
|
||||
if (_charsetDetector.Charset != null) {
|
||||
Encoding encoding = Encoding.GetEncoding(_charsetDetector.Charset);
|
||||
content = File.ReadAllText(found.Path, encoding);
|
||||
} else {
|
||||
content = File.ReadAllText(found.Path, Encoding.UTF8);
|
||||
}
|
||||
first.Lyrics.ParseLRC(content);
|
||||
|
||||
return first;
|
||||
}
|
||||
}
|
||||
return first;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using ATL;
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Messages;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.Database;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using DevWinUI;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Globalization;
|
||||
using Windows.Storage;
|
||||
using Windows.System;
|
||||
|
||||
namespace BetterLyrics.WinUI3.Services.Settings {
|
||||
public partial class SettingsService : ObservableObject {
|
||||
|
||||
public bool IsFirstRun {
|
||||
get => Get(SettingsKeys.IsFirstRun, SettingsDefaultValues.IsFirstRun);
|
||||
set => Set(SettingsKeys.IsFirstRun, value);
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isRebuildingLyricsIndexDatabase;
|
||||
|
||||
// Theme
|
||||
public int Theme {
|
||||
get => Get(SettingsKeys.ThemeType, SettingsDefaultValues.ThemeType);
|
||||
set {
|
||||
Set(SettingsKeys.ThemeType, value);
|
||||
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage((ElementTheme)value));
|
||||
}
|
||||
}
|
||||
|
||||
// Music
|
||||
private ObservableCollection<MusicFolder> _musicLibraries;
|
||||
|
||||
public ObservableCollection<MusicFolder> MusicLibraries {
|
||||
get {
|
||||
if (_musicLibraries == null) {
|
||||
var list = JsonConvert.DeserializeObject<List<MusicFolder>>(
|
||||
Get(SettingsKeys.MusicLibraries, SettingsDefaultValues.MusicLibraries)
|
||||
);
|
||||
|
||||
_musicLibraries = new ObservableCollection<MusicFolder>(list);
|
||||
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
|
||||
}
|
||||
|
||||
return _musicLibraries;
|
||||
}
|
||||
set {
|
||||
if (_musicLibraries != null) {
|
||||
_musicLibraries.CollectionChanged -= (_, _) => SaveMusicLibraries();
|
||||
}
|
||||
|
||||
_musicLibraries = value;
|
||||
_musicLibraries.CollectionChanged += (_, _) => SaveMusicLibraries();
|
||||
SaveMusicLibraries();
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveMusicLibraries() {
|
||||
Set(SettingsKeys.MusicLibraries, JsonConvert.SerializeObject(MusicLibraries.ToList()));
|
||||
}
|
||||
|
||||
|
||||
// Language
|
||||
public int Language {
|
||||
get => Get(SettingsKeys.Language, SettingsDefaultValues.Language);
|
||||
set {
|
||||
Set(SettingsKeys.Language, value);
|
||||
switch ((Models.Language)Language) {
|
||||
case Models.Language.FollowSystem:
|
||||
ApplicationLanguages.PrimaryLanguageOverride = "";
|
||||
break;
|
||||
case Models.Language.English:
|
||||
ApplicationLanguages.PrimaryLanguageOverride = "en-US";
|
||||
break;
|
||||
case Models.Language.SimplifiedChinese:
|
||||
ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
|
||||
break;
|
||||
case Models.Language.TraditionalChinese:
|
||||
ApplicationLanguages.PrimaryLanguageOverride = "zh-TW";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backdrop
|
||||
public int BackdropType {
|
||||
get => Get(SettingsKeys.BackdropType, SettingsDefaultValues.BackdropType);
|
||||
set {
|
||||
Set(SettingsKeys.BackdropType, value);
|
||||
WeakReferenceMessenger.Default.Send(new SystemBackdropChangedMessage((BackdropType)value));
|
||||
}
|
||||
}
|
||||
public bool IsCoverOverlayEnabled {
|
||||
get => Get(SettingsKeys.IsCoverOverlayEnabled, SettingsDefaultValues.IsCoverOverlayEnabled);
|
||||
set => Set(SettingsKeys.IsCoverOverlayEnabled, value);
|
||||
}
|
||||
public bool IsDynamicCoverOverlay {
|
||||
get => Get(SettingsKeys.IsDynamicCoverOverlay, SettingsDefaultValues.IsDynamicCoverOverlay);
|
||||
set => Set(SettingsKeys.IsDynamicCoverOverlay, value);
|
||||
}
|
||||
public int CoverOverlayOpacity {
|
||||
get => Get(SettingsKeys.CoverOverlayOpacity, SettingsDefaultValues.CoverOverlayOpacity);
|
||||
set => Set(SettingsKeys.CoverOverlayOpacity, value);
|
||||
}
|
||||
public int CoverOverlayBlurAmount {
|
||||
get => Get(SettingsKeys.CoverOverlayBlurAmount, SettingsDefaultValues.CoverOverlayBlurAmount);
|
||||
set => Set(SettingsKeys.CoverOverlayBlurAmount, value);
|
||||
}
|
||||
|
||||
// Lyrics
|
||||
public int LyricsAlignmentType {
|
||||
get => Get(SettingsKeys.LyricsAlignmentType, SettingsDefaultValues.LyricsAlignmentType);
|
||||
set => Set(SettingsKeys.LyricsAlignmentType, value);
|
||||
}
|
||||
public int LyricsBlurAmount {
|
||||
get => Get(SettingsKeys.LyricsBlurAmount, SettingsDefaultValues.LyricsBlurAmount);
|
||||
set => Set(SettingsKeys.LyricsBlurAmount, value);
|
||||
}
|
||||
public int LyricsVerticalEdgeOpacity {
|
||||
get => Get(SettingsKeys.LyricsVerticalEdgeOpacity, SettingsDefaultValues.LyricsVerticalEdgeOpacity);
|
||||
set => Set(SettingsKeys.LyricsVerticalEdgeOpacity, value);
|
||||
}
|
||||
public float LyricsLineSpacingFactor {
|
||||
get => Get(SettingsKeys.LyricsLineSpacingFactor, SettingsDefaultValues.LyricsLineSpacingFactor);
|
||||
set => Set(SettingsKeys.LyricsLineSpacingFactor, value);
|
||||
}
|
||||
public int LyricsFontSize {
|
||||
get => Get(SettingsKeys.LyricsFontSize, SettingsDefaultValues.LyricsFontSize);
|
||||
set => Set(SettingsKeys.LyricsFontSize, value);
|
||||
}
|
||||
public bool IsLyricsGlowEffectEnabled {
|
||||
get => Get(SettingsKeys.IsLyricsGlowEffectEnabled, SettingsDefaultValues.IsLyricsGlowEffectEnabled);
|
||||
set => Set(SettingsKeys.IsLyricsGlowEffectEnabled, value);
|
||||
}
|
||||
public bool IsLyricsDynamicGlowEffectEnabled {
|
||||
get => Get(SettingsKeys.IsLyricsDynamicGlowEffectEnabled, SettingsDefaultValues.IsLyricsDynamicGlowEffectEnabled);
|
||||
set => Set(SettingsKeys.IsLyricsDynamicGlowEffectEnabled, value);
|
||||
}
|
||||
|
||||
|
||||
private readonly ApplicationDataContainer _localSettings;
|
||||
private readonly DatabaseService _databaseService;
|
||||
|
||||
public SettingsService(DatabaseService databaseService) {
|
||||
_localSettings = ApplicationData.Current.LocalSettings;
|
||||
_databaseService = databaseService;
|
||||
}
|
||||
|
||||
private T Get<T>(string key, T defaultValue = default) {
|
||||
if (_localSettings.Values.TryGetValue(key, out object value)) {
|
||||
return (T)Convert.ChangeType(value, typeof(T));
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private void Set<T>(string key, T value, [CallerMemberName] string propertyName = null) {
|
||||
_localSettings.Values[key] = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>Local music libraries</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
|
||||
<value>Add folders storing music or lyrics to build lyrics index database</value>
|
||||
</data>
|
||||
<data name="SettingsPageOpenPath.Content" xml:space="preserve">
|
||||
<value>Open in file explorer</value>
|
||||
</data>
|
||||
<data name="SettingsPageRemovePath.Content" xml:space="preserve">
|
||||
<value>Remove from app</value>
|
||||
</data>
|
||||
<data name="SettingsPageRemoveInfo.Title" xml:space="preserve">
|
||||
<value>You are safe to remove the following items</value>
|
||||
</data>
|
||||
<data name="SettingsPageRemoveInfo.Message" xml:space="preserve">
|
||||
<value>Original files and folders in this path will not be deleted when removing it from this app</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
|
||||
<value>Add a folder</value>
|
||||
</data>
|
||||
<data name="SettingsPageTheme.Header" xml:space="preserve">
|
||||
<value>Theme</value>
|
||||
</data>
|
||||
<data name="SettingsPageLanguage.Header" xml:space="preserve">
|
||||
<value>Language</value>
|
||||
</data>
|
||||
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
|
||||
<value>Follow system</value>
|
||||
</data>
|
||||
<data name="SettingsPageLight.Content" xml:space="preserve">
|
||||
<value>Light</value>
|
||||
</data>
|
||||
<data name="SettingsPageDark.Content" xml:space="preserve">
|
||||
<value>Dark</value>
|
||||
</data>
|
||||
<data name="SettingsPageSC.Content" xml:space="preserve">
|
||||
<value>简体中文</value>
|
||||
</data>
|
||||
<data name="SettingsPageTC.Content" xml:space="preserve">
|
||||
<value>繁體中文</value>
|
||||
</data>
|
||||
<data name="SettingsPageEN.Content" xml:space="preserve">
|
||||
<value>English</value>
|
||||
</data>
|
||||
<data name="SettingsPageGitHub.Header" xml:space="preserve">
|
||||
<value>This is an open source app</value>
|
||||
</data>
|
||||
<data name="SettingsPageGitHub.ActionIconToolTip" xml:space="preserve">
|
||||
<value>Open in new window</value>
|
||||
</data>
|
||||
<data name="SettingsPageGitHub.Description" xml:space="preserve">
|
||||
<value>See source code on GitHub</value>
|
||||
</data>
|
||||
<data name="SettingsPageVersion.Text" xml:space="preserve">
|
||||
<value>Version</value>
|
||||
</data>
|
||||
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
|
||||
<value>None</value>
|
||||
</data>
|
||||
<data name="SettingsPageMica.Content" xml:space="preserve">
|
||||
<value>Mica</value>
|
||||
</data>
|
||||
<data name="SettingsPageMicaAlt.Content" xml:space="preserve">
|
||||
<value>Mica Alt</value>
|
||||
</data>
|
||||
<data name="SettingsPageDesktopAcrylic.Content" xml:space="preserve">
|
||||
<value>Desktop Acrylic</value>
|
||||
</data>
|
||||
<data name="SettingsPageAcrylicBase.Content" xml:space="preserve">
|
||||
<value>Acrylic Base</value>
|
||||
</data>
|
||||
<data name="SettingsPageAcrylicThin.Content" xml:space="preserve">
|
||||
<value>Acrylic Thin</value>
|
||||
</data>
|
||||
<data name="SettingsPageTransparent.Content" xml:space="preserve">
|
||||
<value>Transparent</value>
|
||||
</data>
|
||||
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
|
||||
<value>Backdrop</value>
|
||||
</data>
|
||||
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
|
||||
<value>Default</value>
|
||||
</data>
|
||||
<data name="SettingsPageRestart.Content" xml:space="preserve">
|
||||
<value>Restart app to apply change</value>
|
||||
</data>
|
||||
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
|
||||
<value>The path cannot be found on your computer</value>
|
||||
</data>
|
||||
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
|
||||
<value>The folder has been added. Please do not add it again.</value>
|
||||
</data>
|
||||
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
|
||||
<value>Overlay album art background</value>
|
||||
</data>
|
||||
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
|
||||
<value>Dynamic album art background</value>
|
||||
</data>
|
||||
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
|
||||
<value>Album art background opacity</value>
|
||||
</data>
|
||||
<data name="SettingsPageTitle" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="MainPageTitle" xml:space="preserve">
|
||||
<value>BetterLyrics</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
|
||||
<value>Lyrics style</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
|
||||
<value>Alignment</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsCenter.Content" xml:space="preserve">
|
||||
<value>Center</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
|
||||
<value>Left</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
|
||||
<value>Right</value>
|
||||
</data>
|
||||
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
|
||||
<value>Album art background blur amount</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
|
||||
<value>Blur amount</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsBlurAmountSideEffect.Text" xml:space="preserve">
|
||||
<value>Adjusting this value will also increase the background blur intensity of the album image.</value>
|
||||
</data>
|
||||
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
|
||||
<value>Current value: </value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
|
||||
<value>Significantly higher GPU usage when blur is enabled (> 0)</value>
|
||||
</data>
|
||||
<data name="SettingsPageCoverOverlayGPUUsage.Text" xml:space="preserve">
|
||||
<value>Enabling this feature will slightly increase GPU utilization</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
|
||||
<value>Top and bottom edge opacity</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
|
||||
<value>Line spacing</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
|
||||
<value>x line height</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
|
||||
<value>Font size</value>
|
||||
</data>
|
||||
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Show lyrics only</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
|
||||
<value>Lyrics effect</value>
|
||||
</data>
|
||||
<data name="SettingsPageAlbum.Text" xml:space="preserve">
|
||||
<value>Album background</value>
|
||||
</data>
|
||||
<data name="SettingsPageAbout.Text" xml:space="preserve">
|
||||
<value>About</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsLib.Text" xml:space="preserve">
|
||||
<value>Lyrics library</value>
|
||||
</data>
|
||||
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
|
||||
<value>App appearance</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
|
||||
<value>Glow effect</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsDynamicGlowEffect.Header" xml:space="preserve">
|
||||
<value>Dynamic glow effect</value>
|
||||
</data>
|
||||
<data name="SettingsPageRebuildDatabase.Header" xml:space="preserve">
|
||||
<value>Rebuild lyrics index database</value>
|
||||
</data>
|
||||
<data name="SettingsPageRebuildDatabaseButton.Content" xml:space="preserve">
|
||||
<value>Rebuild</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
|
||||
<value>Add</value>
|
||||
</data>
|
||||
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
|
||||
<value>Rebuilding the database, please wait...</value>
|
||||
</data>
|
||||
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>Let's get started now</value>
|
||||
</data>
|
||||
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
|
||||
<value>Welcome to BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MainPageTitleBarTeachingTip.Title" xml:space="preserve">
|
||||
<value>The top area is the title bar</value>
|
||||
</data>
|
||||
<data name="MainPageTitleBarTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>Hover on the top area to show it</value>
|
||||
</data>
|
||||
<data name="MainPageInitDatabaseTeachingTip.Title" xml:space="preserve">
|
||||
<value>Setup lyrics database now</value>
|
||||
</data>
|
||||
<data name="MainPageInitDatabaseTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>Hover on the bottom left corner and then click to open settings page</value>
|
||||
</data>
|
||||
<data name="MainPageBottomCommandTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>Hover on the bottom area to show it</value>
|
||||
</data>
|
||||
<data name="MainPageBottomCommandTeachingTip.Title" xml:space="preserve">
|
||||
<value>The bottom area is the command area</value>
|
||||
</data>
|
||||
<data name="MainPageLyricsOnlyTeachingTip.Title" xml:space="preserve">
|
||||
<value>Toggle "show lyrics only" here</value>
|
||||
</data>
|
||||
<data name="MainPageLyricsOnlyTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>When lyrics exist, you can switch them in the lower right corner</value>
|
||||
</data>
|
||||
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
|
||||
<value>No music playing now</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,342 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>本地音乐媒体库</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
|
||||
<value>添加存放音乐或歌词的文件夹以构建歌词索引数据库</value>
|
||||
</data>
|
||||
<data name="SettingsPageOpenPath.Content" xml:space="preserve">
|
||||
<value>在文件资源管理器中打开</value>
|
||||
</data>
|
||||
<data name="SettingsPageRemovePath.Content" xml:space="preserve">
|
||||
<value>从应用中移除</value>
|
||||
</data>
|
||||
<data name="SettingsPageRemoveInfo.Title" xml:space="preserve">
|
||||
<value>您可以安全删除以下项目</value>
|
||||
</data>
|
||||
<data name="SettingsPageRemoveInfo.Message" xml:space="preserve">
|
||||
<value>路径中的原始文件和文件夹不会被删除</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
|
||||
<value>添加文件夹</value>
|
||||
</data>
|
||||
<data name="SettingsPageTheme.Header" xml:space="preserve">
|
||||
<value>主题</value>
|
||||
</data>
|
||||
<data name="SettingsPageLanguage.Header" xml:space="preserve">
|
||||
<value>语言</value>
|
||||
</data>
|
||||
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
|
||||
<value>跟随系统</value>
|
||||
</data>
|
||||
<data name="SettingsPageLight.Content" xml:space="preserve">
|
||||
<value>浅色</value>
|
||||
</data>
|
||||
<data name="SettingsPageDark.Content" xml:space="preserve">
|
||||
<value>深色</value>
|
||||
</data>
|
||||
<data name="SettingsPageSC.Content" xml:space="preserve">
|
||||
<value>简体中文</value>
|
||||
</data>
|
||||
<data name="SettingsPageTC.Content" xml:space="preserve">
|
||||
<value>繁體中文</value>
|
||||
</data>
|
||||
<data name="SettingsPageEN.Content" xml:space="preserve">
|
||||
<value>English</value>
|
||||
</data>
|
||||
<data name="SettingsPageGitHub.Header" xml:space="preserve">
|
||||
<value>此应用已开源</value>
|
||||
</data>
|
||||
<data name="SettingsPageGitHub.ActionIconToolTip" xml:space="preserve">
|
||||
<value>在新窗口中打开</value>
|
||||
</data>
|
||||
<data name="SettingsPageGitHub.Description" xml:space="preserve">
|
||||
<value>在 GitHub 上查看源代码</value>
|
||||
</data>
|
||||
<data name="SettingsPageVersion.Text" xml:space="preserve">
|
||||
<value>版本号</value>
|
||||
</data>
|
||||
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
|
||||
<value>无</value>
|
||||
</data>
|
||||
<data name="SettingsPageMica.Content" xml:space="preserve">
|
||||
<value>云母</value>
|
||||
</data>
|
||||
<data name="SettingsPageMicaAlt.Content" xml:space="preserve">
|
||||
<value>云母(替代样式)</value>
|
||||
</data>
|
||||
<data name="SettingsPageDesktopAcrylic.Content" xml:space="preserve">
|
||||
<value>亚克力(桌面)</value>
|
||||
</data>
|
||||
<data name="SettingsPageAcrylicBase.Content" xml:space="preserve">
|
||||
<value>亚克力(基础)</value>
|
||||
</data>
|
||||
<data name="SettingsPageAcrylicThin.Content" xml:space="preserve">
|
||||
<value>亚克力(薄层)</value>
|
||||
</data>
|
||||
<data name="SettingsPageTransparent.Content" xml:space="preserve">
|
||||
<value>透明</value>
|
||||
</data>
|
||||
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
|
||||
<value>背景材质</value>
|
||||
</data>
|
||||
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
|
||||
<value>默认</value>
|
||||
</data>
|
||||
<data name="SettingsPageRestart.Content" xml:space="preserve">
|
||||
<value>重启应用以应用更改</value>
|
||||
</data>
|
||||
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
|
||||
<value>无法在您的计算机中找到该路径</value>
|
||||
</data>
|
||||
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
|
||||
<value>已添加过该文件夹,请勿重复添加</value>
|
||||
</data>
|
||||
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
|
||||
<value>叠加专辑图片背景</value>
|
||||
</data>
|
||||
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
|
||||
<value>动态专辑图片背景</value>
|
||||
</data>
|
||||
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
|
||||
<value>专辑图片背景不透明度</value>
|
||||
</data>
|
||||
<data name="SettingsPageTitle" xml:space="preserve">
|
||||
<value>设置</value>
|
||||
</data>
|
||||
<data name="MainPageTitle" xml:space="preserve">
|
||||
<value>BetterLyrics</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
|
||||
<value>歌词样式</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
|
||||
<value>对齐方式</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsCenter.Content" xml:space="preserve">
|
||||
<value>居中</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
|
||||
<value>靠左</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
|
||||
<value>靠右</value>
|
||||
</data>
|
||||
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
|
||||
<value>专辑图片背景模糊度</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
|
||||
<value>模糊度</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsBlurAmountSideEffect.Text" xml:space="preserve">
|
||||
<value>调整该数值将同步提高专辑图片背景模糊强度</value>
|
||||
</data>
|
||||
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
|
||||
<value>当前值:</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
|
||||
<value>启用模糊(> 0)时将显著提升 GPU 占用率</value>
|
||||
</data>
|
||||
<data name="SettingsPageCoverOverlayGPUUsage.Text" xml:space="preserve">
|
||||
<value>启用该功能将略微提升 GPU 占用率</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
|
||||
<value>上下边缘不透明度</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
|
||||
<value>行间距</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
|
||||
<value> 倍行高</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
|
||||
<value>字体大小</value>
|
||||
</data>
|
||||
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>仅展示歌词</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
|
||||
<value>歌词效果</value>
|
||||
</data>
|
||||
<data name="SettingsPageAlbum.Text" xml:space="preserve">
|
||||
<value>专辑背景</value>
|
||||
</data>
|
||||
<data name="SettingsPageAbout.Text" xml:space="preserve">
|
||||
<value>关于</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsLib.Text" xml:space="preserve">
|
||||
<value>歌词库</value>
|
||||
</data>
|
||||
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
|
||||
<value>应用外观</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
|
||||
<value>辉光效果</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsDynamicGlowEffect.Header" xml:space="preserve">
|
||||
<value>动态辉光效果</value>
|
||||
</data>
|
||||
<data name="SettingsPageRebuildDatabase.Header" xml:space="preserve">
|
||||
<value>重构歌词索引数据库</value>
|
||||
</data>
|
||||
<data name="SettingsPageRebuildDatabaseButton.Content" xml:space="preserve">
|
||||
<value>重构</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
|
||||
<value>添加</value>
|
||||
</data>
|
||||
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
|
||||
<value>重构数据库中,请稍候...</value>
|
||||
</data>
|
||||
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>来看看怎么使用这款应用吧</value>
|
||||
</data>
|
||||
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
|
||||
<value>欢迎使用 BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MainPageTitleBarTeachingTip.Title" xml:space="preserve">
|
||||
<value>顶部区域是标题栏</value>
|
||||
</data>
|
||||
<data name="MainPageTitleBarTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>悬停在顶部区域以显示</value>
|
||||
</data>
|
||||
<data name="MainPageInitDatabaseTeachingTip.Title" xml:space="preserve">
|
||||
<value>现在就来初始化歌词数据库吧</value>
|
||||
</data>
|
||||
<data name="MainPageInitDatabaseTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>悬停在左下角后单击以进入设置页面</value>
|
||||
</data>
|
||||
<data name="MainPageBottomCommandTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>悬停在底部区域以显示</value>
|
||||
</data>
|
||||
<data name="MainPageBottomCommandTeachingTip.Title" xml:space="preserve">
|
||||
<value>底部区域是命令栏</value>
|
||||
</data>
|
||||
<data name="MainPageLyricsOnlyTeachingTip.Title" xml:space="preserve">
|
||||
<value>在此处切换“仅展示歌词”</value>
|
||||
</data>
|
||||
<data name="MainPageLyricsOnlyTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>当歌词存在时可在右下角进行切换</value>
|
||||
</data>
|
||||
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
|
||||
<value>当前没有正在播放的音乐</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,342 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="SettingsPageMusicLib.Header" xml:space="preserve">
|
||||
<value>本地音樂媒體庫</value>
|
||||
</data>
|
||||
<data name="SettingsPageMusicLib.Description" xml:space="preserve">
|
||||
<value>新增存放音樂或歌詞的資料夾以建立歌詞索引資料庫</value>
|
||||
</data>
|
||||
<data name="SettingsPageOpenPath.Content" xml:space="preserve">
|
||||
<value>在檔案總管中開啟</value>
|
||||
</data>
|
||||
<data name="SettingsPageRemovePath.Content" xml:space="preserve">
|
||||
<value>從應用程式中移除</value>
|
||||
</data>
|
||||
<data name="SettingsPageRemoveInfo.Title" xml:space="preserve">
|
||||
<value>您可以安全刪除以下項目</value>
|
||||
</data>
|
||||
<data name="SettingsPageRemoveInfo.Message" xml:space="preserve">
|
||||
<value>路徑中的原始檔案和資料夾不會被刪除</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolder.Header" xml:space="preserve">
|
||||
<value>新增資料夾</value>
|
||||
</data>
|
||||
<data name="SettingsPageTheme.Header" xml:space="preserve">
|
||||
<value>主題</value>
|
||||
</data>
|
||||
<data name="SettingsPageLanguage.Header" xml:space="preserve">
|
||||
<value>語言</value>
|
||||
</data>
|
||||
<data name="SettingsPageFollowSystem.Content" xml:space="preserve">
|
||||
<value>跟隨系統</value>
|
||||
</data>
|
||||
<data name="SettingsPageLight.Content" xml:space="preserve">
|
||||
<value>淺色</value>
|
||||
</data>
|
||||
<data name="SettingsPageDark.Content" xml:space="preserve">
|
||||
<value>深色</value>
|
||||
</data>
|
||||
<data name="SettingsPageSC.Content" xml:space="preserve">
|
||||
<value>简体中文</value>
|
||||
</data>
|
||||
<data name="SettingsPageTC.Content" xml:space="preserve">
|
||||
<value>繁體中文</value>
|
||||
</data>
|
||||
<data name="SettingsPageEN.Content" xml:space="preserve">
|
||||
<value>English</value>
|
||||
</data>
|
||||
<data name="SettingsPageGitHub.Header" xml:space="preserve">
|
||||
<value>此應用程式已開源</value>
|
||||
</data>
|
||||
<data name="SettingsPageGitHub.ActionIconToolTip" xml:space="preserve">
|
||||
<value>在新視窗中開啟</value>
|
||||
</data>
|
||||
<data name="SettingsPageGitHub.Description" xml:space="preserve">
|
||||
<value>在 GitHub 上查看原始碼</value>
|
||||
</data>
|
||||
<data name="SettingsPageVersion.Text" xml:space="preserve">
|
||||
<value>版本號</value>
|
||||
</data>
|
||||
<data name="SettingsPageNoBackdrop.Content" xml:space="preserve">
|
||||
<value>無</value>
|
||||
</data>
|
||||
<data name="SettingsPageMica.Content" xml:space="preserve">
|
||||
<value>雲母</value>
|
||||
</data>
|
||||
<data name="SettingsPageMicaAlt.Content" xml:space="preserve">
|
||||
<value>雲母(替代樣式)</value>
|
||||
</data>
|
||||
<data name="SettingsPageDesktopAcrylic.Content" xml:space="preserve">
|
||||
<value>亞克力(桌面)</value>
|
||||
</data>
|
||||
<data name="SettingsPageAcrylicBase.Content" xml:space="preserve">
|
||||
<value>亞克力(基礎)</value>
|
||||
</data>
|
||||
<data name="SettingsPageAcrylicThin.Content" xml:space="preserve">
|
||||
<value>亞克力(薄層)</value>
|
||||
</data>
|
||||
<data name="SettingsPageTransparent.Content" xml:space="preserve">
|
||||
<value>透明</value>
|
||||
</data>
|
||||
<data name="SettingsPageBackdrop.Header" xml:space="preserve">
|
||||
<value>背景材質</value>
|
||||
</data>
|
||||
<data name="SettingsPageSystemLanguage.Content" xml:space="preserve">
|
||||
<value>預設</value>
|
||||
</data>
|
||||
<data name="SettingsPageRestart.Content" xml:space="preserve">
|
||||
<value>重啟應用程式以應用更改</value>
|
||||
</data>
|
||||
<data name="SettingsPagePathNotFound.Text" xml:space="preserve">
|
||||
<value>無法在您的電腦中找到該路徑</value>
|
||||
</data>
|
||||
<data name="SettingsPagePathExistedInfo" xml:space="preserve">
|
||||
<value>已新增過該資料夾,請勿重複新增</value>
|
||||
</data>
|
||||
<data name="SettingsPageCoverOverlay.Header" xml:space="preserve">
|
||||
<value>疊加專輯圖片背景</value>
|
||||
</data>
|
||||
<data name="SettingsPageDynamicCoverOverlay.Header" xml:space="preserve">
|
||||
<value>動態專輯圖片背景</value>
|
||||
</data>
|
||||
<data name="SettingsPageCoverOverlayOpacity.Header" xml:space="preserve">
|
||||
<value>專輯圖片背景不透明度</value>
|
||||
</data>
|
||||
<data name="SettingsPageTitle" xml:space="preserve">
|
||||
<value>設定</value>
|
||||
</data>
|
||||
<data name="MainPageTitle" xml:space="preserve">
|
||||
<value>BetterLyrics</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsStyle.Text" xml:space="preserve">
|
||||
<value>歌詞樣式</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsAlignment.Header" xml:space="preserve">
|
||||
<value>對齊方式</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsCenter.Content" xml:space="preserve">
|
||||
<value>居中</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsLeft.Content" xml:space="preserve">
|
||||
<value>靠左</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsRight.Content" xml:space="preserve">
|
||||
<value>靠右</value>
|
||||
</data>
|
||||
<data name="SettingsPageCoverOverlayBlurAmount.Header" xml:space="preserve">
|
||||
<value>專輯圖片背景模糊度</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsBlurAmount.Header" xml:space="preserve">
|
||||
<value>模糊度</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsBlurAmountSideEffect.Text" xml:space="preserve">
|
||||
<value>調整此數值將同步提升專輯圖片背景模糊強度</value>
|
||||
</data>
|
||||
<data name="SettingsPageSliderPrefix.Text" xml:space="preserve">
|
||||
<value>目前值:</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsBlurHighGPUUsage.Text" xml:space="preserve">
|
||||
<value>啟用模糊(> 0)時將顯著提升 GPU 佔用率</value>
|
||||
</data>
|
||||
<data name="SettingsPageCoverOverlayGPUUsage.Text" xml:space="preserve">
|
||||
<value>啟用此功能將略微提升 GPU 佔用率</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsVerticalEdgeOpacity.Header" xml:space="preserve">
|
||||
<value>上下邊緣不透明度</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsLineSpacingFactor.Header" xml:space="preserve">
|
||||
<value>行間距</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsLineSpacingFactorUnit.Text" xml:space="preserve">
|
||||
<value> 倍行高</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsFontSize.Header" xml:space="preserve">
|
||||
<value>字體大小</value>
|
||||
</data>
|
||||
<data name="MainWindowLyricsOnly.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>僅展示歌詞</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsEffect.Text" xml:space="preserve">
|
||||
<value>歌詞效果</value>
|
||||
</data>
|
||||
<data name="SettingsPageAlbum.Text" xml:space="preserve">
|
||||
<value>專輯背景</value>
|
||||
</data>
|
||||
<data name="SettingsPageAbout.Text" xml:space="preserve">
|
||||
<value>關於</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsLib.Text" xml:space="preserve">
|
||||
<value>歌詞庫</value>
|
||||
</data>
|
||||
<data name="SettingsPageAppAppearance.Text" xml:space="preserve">
|
||||
<value>應用外觀</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsGlowEffect.Header" xml:space="preserve">
|
||||
<value>輝光效果</value>
|
||||
</data>
|
||||
<data name="SettingsPageLyricsDynamicGlowEffect.Header" xml:space="preserve">
|
||||
<value>動態輝光效果</value>
|
||||
</data>
|
||||
<data name="SettingsPageRebuildDatabase.Header" xml:space="preserve">
|
||||
<value>重構歌詞索引資料庫</value>
|
||||
</data>
|
||||
<data name="SettingsPageRebuildDatabaseButton.Content" xml:space="preserve">
|
||||
<value>重構</value>
|
||||
</data>
|
||||
<data name="SettingsPageAddFolderButton.Content" xml:space="preserve">
|
||||
<value>添加</value>
|
||||
</data>
|
||||
<data name="SettingsPageRebuildDatabaseDesc.Text" xml:space="preserve">
|
||||
<value>重構資料庫中,請稍候...</value>
|
||||
</data>
|
||||
<data name="MainPageWelcomeTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>來看看怎麼使用這款應用程式吧</value>
|
||||
</data>
|
||||
<data name="MainPageWelcomeTeachingTip.Title" xml:space="preserve">
|
||||
<value>歡迎使用 BetterLyrics</value>
|
||||
</data>
|
||||
<data name="MainPageTitleBarTeachingTip.Title" xml:space="preserve">
|
||||
<value>頂部區域是標題欄</value>
|
||||
</data>
|
||||
<data name="MainPageTitleBarTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>懸停在頂部區域以顯示</value>
|
||||
</data>
|
||||
<data name="MainPageInitDatabaseTeachingTip.Title" xml:space="preserve">
|
||||
<value>現在就來初始化歌詞資料庫吧</value>
|
||||
</data>
|
||||
<data name="MainPageInitDatabaseTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>懸停在左下角後點擊以進入設定頁面</value>
|
||||
</data>
|
||||
<data name="MainPageBottomCommandTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>懸停在底部區域以顯示</value>
|
||||
</data>
|
||||
<data name="MainPageBottomCommandTeachingTip.Title" xml:space="preserve">
|
||||
<value>底部區域是命令列</value>
|
||||
</data>
|
||||
<data name="MainPageLyricsOnlyTeachingTip.Title" xml:space="preserve">
|
||||
<value>在此切換“僅展示歌詞”</value>
|
||||
</data>
|
||||
<data name="MainPageLyricsOnlyTeachingTip.Subtitle" xml:space="preserve">
|
||||
<value>當歌詞存在時可在右下角進行切換</value>
|
||||
</data>
|
||||
<data name="MainPageNoMusicPlaying.Text" xml:space="preserve">
|
||||
<value>目前沒有正在播放的音樂</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,154 @@
|
||||
using ATL;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.Database;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Media.Control;
|
||||
using Windows.Storage.Streams;
|
||||
using Windows.UI;
|
||||
using static ATL.LyricsInfo;
|
||||
|
||||
namespace BetterLyrics.WinUI3.ViewModels {
|
||||
public partial class MainViewModel : ObservableObject {
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isAnyMusicSessionExisted = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _title;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _artist;
|
||||
|
||||
public List<Color> CoverImageDominantColors { get; set; } =
|
||||
[Colors.Transparent, Colors.Transparent, Colors.Transparent];
|
||||
|
||||
[ObservableProperty]
|
||||
private BitmapImage? _coverImage;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _aboutToUpdateUI;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isSmallScreenMode;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _showLyricsOnly = false;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _lyricsExisted = false;
|
||||
|
||||
private Helper.ColorThief _colorThief = new();
|
||||
|
||||
private readonly DatabaseService _databaseService;
|
||||
|
||||
public MainViewModel(DatabaseService databaseService) {
|
||||
_databaseService = databaseService;
|
||||
}
|
||||
|
||||
public List<LyricsLine> GetLyrics(Track? track) {
|
||||
List<LyricsLine> result = [];
|
||||
var lyricsPhrases = track?.Lyrics.SynchronizedLyrics;
|
||||
|
||||
if (lyricsPhrases?.Count > 0) {
|
||||
if (lyricsPhrases[0].TimestampMs > 0) {
|
||||
var placeholder = new LyricsPhrase(0, " ");
|
||||
lyricsPhrases.Insert(0, placeholder);
|
||||
lyricsPhrases.Insert(0, placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
LyricsLine? lyricsLine = null;
|
||||
|
||||
for (int i = 0; i < lyricsPhrases?.Count; i++) {
|
||||
var lyricsPhrase = lyricsPhrases[i];
|
||||
int startTimestampMs = lyricsPhrase.TimestampMs;
|
||||
int endTimestampMs;
|
||||
|
||||
if (i + 1 < lyricsPhrases.Count) {
|
||||
endTimestampMs = lyricsPhrases[i + 1].TimestampMs;
|
||||
} else {
|
||||
endTimestampMs = (int)track.DurationMs;
|
||||
}
|
||||
|
||||
lyricsLine ??= new LyricsLine {
|
||||
StartPlayingTimestampMs = startTimestampMs,
|
||||
};
|
||||
|
||||
lyricsLine.Texts.Add(lyricsPhrase.Text);
|
||||
|
||||
if (endTimestampMs == startTimestampMs) {
|
||||
continue;
|
||||
} else {
|
||||
lyricsLine.EndPlayingTimestampMs = endTimestampMs;
|
||||
result.Add(lyricsLine);
|
||||
lyricsLine = null;
|
||||
}
|
||||
|
||||
}
|
||||
LyricsExisted = result.Count != 0;
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
public async Task<(List<LyricsLine>, CanvasBitmap?)> SetSongInfoAsync(GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps, ICanvasAnimatedControl control) {
|
||||
|
||||
CanvasBitmap? canvasBitmap = null;
|
||||
|
||||
Title = mediaProps?.Title;
|
||||
Artist = mediaProps?.Artist;
|
||||
|
||||
IRandomAccessStream? stream = null;
|
||||
|
||||
var track = _databaseService.GetMusicMetadata(Title, Artist);
|
||||
|
||||
if (mediaProps?.Thumbnail is IRandomAccessStreamReference reference) {
|
||||
stream = await reference.OpenReadAsync();
|
||||
} else {
|
||||
if (track?.EmbeddedPictures.Count > 0) {
|
||||
var bytes = track.EmbeddedPictures[0].PictureData;
|
||||
if (bytes != null) {
|
||||
stream = await Helper.ImageHelper.GetStreamFromBytesAsync(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set cover image and dominant colors
|
||||
if (stream == null) {
|
||||
CoverImage = null;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
CoverImageDominantColors[i] = Colors.Transparent;
|
||||
}
|
||||
} else {
|
||||
canvasBitmap = await CanvasBitmap.LoadAsync(control, stream);
|
||||
stream.Seek(0);
|
||||
|
||||
CoverImage = new BitmapImage();
|
||||
await CoverImage.SetSourceAsync(stream);
|
||||
stream.Seek(0);
|
||||
|
||||
var decoder = await BitmapDecoder.CreateAsync(stream);
|
||||
var quantizedColors = await _colorThief.GetPalette(decoder, 3);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
Helper.QuantizedColor quantizedColor = quantizedColors[i];
|
||||
CoverImageDominantColors[i] = Color.FromArgb(
|
||||
quantizedColor.Color.A, quantizedColor.Color.R, quantizedColor.Color.G, quantizedColor.Color.B);
|
||||
}
|
||||
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
return (GetLyrics(track), canvasBitmap);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
517
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml
Normal file
@@ -0,0 +1,517 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="BetterLyrics.WinUI3.Views.MainPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:labs="using:CommunityToolkit.Labs.WinUI.MarqueeTextRns"
|
||||
xmlns:local="using:BetterLyrics.WinUI3.Views"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
NavigationCacheMode="Required"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid x:Name="RootGrid" SizeChanged="RootGrid_SizeChanged">
|
||||
<Grid.Resources>
|
||||
<Thickness x:Key="TeachingTipDescriptionMargin">0,16,0,0</Thickness>
|
||||
</Grid.Resources>
|
||||
<Grid
|
||||
x:Name="TopPlaceholder"
|
||||
Height="36"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top" />
|
||||
|
||||
<Grid x:Name="MainGrid">
|
||||
|
||||
<!-- Lyrics area -->
|
||||
<Grid x:Name="LyricsGrid">
|
||||
|
||||
<canvas:CanvasAnimatedControl
|
||||
x:Name="LyricsCanvas"
|
||||
Draw="LyricsCanvas_Draw"
|
||||
Loaded="LyricsCanvas_Loaded"
|
||||
Update="LyricsCanvas_Update">
|
||||
|
||||
<canvas:CanvasAnimatedControl.Resources>
|
||||
<Storyboard x:Key="LyricsCanvasFadeInStoryboard">
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LyricsCanvas" Storyboard.TargetProperty="Opacity">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<Storyboard x:Key="LyricsCanvasFadeOutStoryboard">
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="LyricsCanvas" Storyboard.TargetProperty="Opacity">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</canvas:CanvasAnimatedControl.Resources>
|
||||
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource LyricsCanvasFadeOutStoryboard}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource LyricsCanvasFadeInStoryboard}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
</interactivity:Interaction.Behaviors>
|
||||
|
||||
</canvas:CanvasAnimatedControl>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Song info area -->
|
||||
<Grid x:Name="SongInfoGrid" Margin="36">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition x:Name="SongInfoColumnDefinition" Width="*" />
|
||||
<ColumnDefinition x:Name="SpacerColumnDefinition" Width="36" />
|
||||
<ColumnDefinition x:Name="LyricsAreaColumnDefinition" Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid.Resources>
|
||||
<Storyboard x:Key="HideSongInfoGrid">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="SongInfoGrid"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="1"
|
||||
To="0"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
<Storyboard x:Key="ShowSongInfoGrid">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="SongInfoGrid"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="0"
|
||||
To="1"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
</Grid.Resources>
|
||||
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.ShowLyricsOnly, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource HideSongInfoGrid}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.ShowLyricsOnly, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ShowSongInfoGrid}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
</interactivity:Interaction.Behaviors>
|
||||
|
||||
<StackPanel
|
||||
x:Name="SongInfoStackPanel"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
|
||||
<StackPanel.Resources>
|
||||
|
||||
<!-- Animation for song info -->
|
||||
<Storyboard x:Name="SongInfoStackPanelFadeInStoryboard">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="SongInfoStackPanel"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
<Storyboard x:Name="SongInfoStackPanelFadeOutStoryboard" BeginTime="0:0:0.2">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="SongInfoStackPanel"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="0"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
|
||||
</StackPanel.Resources>
|
||||
|
||||
<interactivity:Interaction.Behaviors>
|
||||
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.IsSmallScreenMode, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource SongInfoStackPanelFadeOutStoryboard}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.IsSmallScreenMode, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource SongInfoStackPanelFadeInStoryboard}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
|
||||
</interactivity:Interaction.Behaviors>
|
||||
|
||||
<!-- Cover area -->
|
||||
<Grid
|
||||
x:Name="CoverGrid"
|
||||
MaxWidth="300"
|
||||
MaxHeight="300"
|
||||
CornerRadius="24">
|
||||
<Grid.Resources>
|
||||
|
||||
<!-- Animation for cover grid -->
|
||||
<Storyboard x:Key="CoverGridShowStoryboard">
|
||||
<DoubleAnimation
|
||||
EnableDependentAnimation="True"
|
||||
Storyboard.TargetName="CoverGrid"
|
||||
Storyboard.TargetProperty="Height"
|
||||
To="300"
|
||||
Duration="0:0:0.2">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseInOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<DoubleAnimation
|
||||
BeginTime="0:0:0.2"
|
||||
Storyboard.TargetName="CoverGrid"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1"
|
||||
Duration="0:0:0.2">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseInOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
<Storyboard x:Key="CoverGridHideStoryboard">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="CoverGrid"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="0"
|
||||
Duration="0:0:0.2">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseInOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<DoubleAnimation
|
||||
BeginTime="0:0:0.2"
|
||||
EnableDependentAnimation="True"
|
||||
Storyboard.TargetName="CoverGrid"
|
||||
Storyboard.TargetProperty="Height"
|
||||
To="0"
|
||||
Duration="0:0:0.2">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseInOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
|
||||
</Grid.Resources>
|
||||
|
||||
<Image x:Name="CoverImage" Source="{x:Bind ViewModel.CoverImage, Mode=OneWay}">
|
||||
<Image.Resources>
|
||||
<Storyboard x:Key="CoverIamgeFadeInStoryboard">
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<Storyboard x:Key="CoverIamgeFadeOutStoryboard">
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="CoverImage" Storyboard.TargetProperty="Opacity">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</Image.Resources>
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeOutStoryboard}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource CoverIamgeFadeInStoryboard}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
</interactivity:Interaction.Behaviors>
|
||||
</Image>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- Song title -->
|
||||
<controls:OpacityMaskView x:Name="TitleOpacityMaskView" HorizontalAlignment="Center">
|
||||
<controls:OpacityMaskView.OpacityMask>
|
||||
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
|
||||
</controls:OpacityMaskView.OpacityMask>
|
||||
|
||||
<controls:OpacityMaskView.Resources>
|
||||
<Storyboard x:Key="TitleOpacityMaskViewFadeInStoryboard">
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<Storyboard x:Key="TitleOpacityMaskViewFadeOutStoryboard">
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="TitleOpacityMaskView" Storyboard.TargetProperty="Opacity">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</controls:OpacityMaskView.Resources>
|
||||
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeOutStoryboard}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource TitleOpacityMaskViewFadeInStoryboard}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
</interactivity:Interaction.Behaviors>
|
||||
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Margin="0,12,0,0"
|
||||
FontSize="{StaticResource TitleTextBlockFontSize}"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind ViewModel.Title, Mode=OneWay}" />
|
||||
</controls:OpacityMaskView>
|
||||
|
||||
<!-- Song artist -->
|
||||
<controls:OpacityMaskView x:Name="ArtistOpacityMaskView" HorizontalAlignment="Center">
|
||||
<controls:OpacityMaskView.OpacityMask>
|
||||
<Rectangle Fill="{StaticResource BaseHighEdgeHorizontalFadeBrush}" />
|
||||
</controls:OpacityMaskView.OpacityMask>
|
||||
<controls:OpacityMaskView.Resources>
|
||||
<Storyboard x:Key="ArtistOpacityMaskViewFadeInStoryboard">
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<Storyboard x:Key="ArtistOpacityMaskViewFadeOutStoryboard">
|
||||
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArtistOpacityMaskView" Storyboard.TargetProperty="Opacity">
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
|
||||
<EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="0" />
|
||||
</DoubleAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</controls:OpacityMaskView.Resources>
|
||||
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeOutStoryboard}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.AboutToUpdateUI, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ArtistOpacityMaskViewFadeInStoryboard}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
</interactivity:Interaction.Behaviors>
|
||||
|
||||
<TextBlock
|
||||
FontSize="{StaticResource SubtitleTextBlockFontSize}"
|
||||
FontWeight="SemiBold"
|
||||
Opacity="0.5"
|
||||
Text="{x:Bind ViewModel.Artist, Mode=OneWay}" />
|
||||
</controls:OpacityMaskView>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
x:Name="MainPageNoMusicPlayingTextBlock"
|
||||
x:Uid="MainPageNoMusicPlaying"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource TitleTextBlockStyle}">
|
||||
<TextBlock.Resources>
|
||||
<Storyboard x:Key="ShowMainPageNoMusicPlayingTextBlockStoryboard">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="MainPageNoMusicPlayingTextBlock"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
<Storyboard x:Key="HideMainPageNoMusicPlayingTextBlockStoryboard">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="MainPageNoMusicPlayingTextBlock"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="0"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
</TextBlock.Resources>
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.IsAnyMusicSessionExisted, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource HideMainPageNoMusicPlayingTextBlockStoryboard}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.IsAnyMusicSessionExisted, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource ShowMainPageNoMusicPlayingTextBlockStoryboard}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
</interactivity:Interaction.Behaviors>
|
||||
</TextBlock>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
x:Name="BottomCommandGrid"
|
||||
Padding="2,0"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="Transparent"
|
||||
Opacity="0">
|
||||
|
||||
<Grid.Resources>
|
||||
|
||||
<Storyboard x:Name="BottomCommandGridFadeInStoryboard">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="BottomCommandGrid"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
<Storyboard x:Name="BottomCommandGridFadeOutStoryboard" BeginTime="0:0:0.2">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="BottomCommandGrid"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="0"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
|
||||
</Grid.Resources>
|
||||
|
||||
<interactivity:Interaction.Behaviors>
|
||||
|
||||
<interactivity:EventTriggerBehavior EventName="PointerEntered">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource BottomCommandGridFadeInStoryboard}" />
|
||||
</interactivity:EventTriggerBehavior>
|
||||
<interactivity:EventTriggerBehavior EventName="PointerExited">
|
||||
<interactivity:ControlStoryboardAction Storyboard="{StaticResource BottomCommandGridFadeOutStoryboard}" />
|
||||
</interactivity:EventTriggerBehavior>
|
||||
|
||||
</interactivity:Interaction.Behaviors>
|
||||
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
||||
<AppBarButton
|
||||
x:Name="SettingsButton"
|
||||
Click="SettingsButton_Click"
|
||||
LabelPosition="Collapsed">
|
||||
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="" />
|
||||
</AppBarButton>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||
<AppBarToggleButton
|
||||
x:Name="LyricsOnlyButton"
|
||||
x:Uid="MainWindowLyricsOnly"
|
||||
IsChecked="{x:Bind ViewModel.ShowLyricsOnly, Mode=TwoWay}"
|
||||
LabelPosition="Collapsed"
|
||||
Visibility="{x:Bind ViewModel.LyricsExisted, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
|
||||
<FontIcon FontFamily="Segoe Fluent Icons" Glyph="" />
|
||||
</AppBarToggleButton>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
<TeachingTip
|
||||
x:Name="WelcomeTeachingTip"
|
||||
x:Uid="MainPageWelcomeTeachingTip"
|
||||
Closed="WelcomeTeachingTip_Closed"
|
||||
IsOpen="False"
|
||||
Target="{x:Bind RootGrid}">
|
||||
<TeachingTip.Content>
|
||||
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="1/5" />
|
||||
</TeachingTip.Content>
|
||||
</TeachingTip>
|
||||
|
||||
<TeachingTip
|
||||
x:Name="TopCommandTeachingTip"
|
||||
x:Uid="MainPageTitleBarTeachingTip"
|
||||
Closed="TopCommandTeachingTip_Closed"
|
||||
IsOpen="False"
|
||||
Target="{x:Bind TopPlaceholder}">
|
||||
<TeachingTip.Content>
|
||||
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="2/5" />
|
||||
</TeachingTip.Content>
|
||||
</TeachingTip>
|
||||
|
||||
<TeachingTip
|
||||
x:Name="BottomCommandTeachingTip"
|
||||
x:Uid="MainPageBottomCommandTeachingTip"
|
||||
Closed="BottomCommandTeachingTip_Closed"
|
||||
IsOpen="False"
|
||||
Target="{x:Bind BottomCommandGrid}">
|
||||
<TeachingTip.Content>
|
||||
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="3/5" />
|
||||
</TeachingTip.Content>
|
||||
</TeachingTip>
|
||||
|
||||
<TeachingTip
|
||||
x:Name="LyricsOnlyTeachingTip"
|
||||
x:Uid="MainPageLyricsOnlyTeachingTip"
|
||||
Closed="LyricsOnlyTeachingTip_Closed"
|
||||
IsOpen="False"
|
||||
Target="{x:Bind LyricsOnlyButton}">
|
||||
<TeachingTip.Content>
|
||||
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="4/5" />
|
||||
</TeachingTip.Content>
|
||||
</TeachingTip>
|
||||
|
||||
<TeachingTip
|
||||
x:Name="InitDatabaseTeachingTip"
|
||||
x:Uid="MainPageInitDatabaseTeachingTip"
|
||||
Closed="InitDatabaseTeachingTip_Closed"
|
||||
IsOpen="False"
|
||||
Target="{x:Bind SettingsButton}">
|
||||
<TeachingTip.Content>
|
||||
<TextBlock Margin="{StaticResource TeachingTipDescriptionMargin}" Text="5/5" />
|
||||
</TeachingTip.Content>
|
||||
</TeachingTip>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup>
|
||||
<!-- Narrow -->
|
||||
<VisualState x:Name="NarrowState">
|
||||
<VisualState.StateTriggers>
|
||||
<AdaptiveTrigger MinWindowWidth="0" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="SongInfoColumnDefinition.Width" Value="0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
|
||||
<!-- Wide -->
|
||||
<VisualState x:Name="WideState">
|
||||
<VisualState.StateTriggers>
|
||||
<AdaptiveTrigger MinWindowWidth="720" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="SongInfoColumnDefinition.Width" Value="*" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</Page>
|
||||
756
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/MainPage.xaml.cs
Normal file
@@ -0,0 +1,756 @@
|
||||
using BetterLyrics.WinUI3.Helper;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using BetterLyrics.WinUI3.Services.Settings;
|
||||
using BetterLyrics.WinUI3.ViewModels;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.Brushes;
|
||||
using Microsoft.Graphics.Canvas.Effects;
|
||||
using Microsoft.Graphics.Canvas.Text;
|
||||
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Text;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation;
|
||||
using Windows.Media.Control;
|
||||
using Color = Windows.UI.Color;
|
||||
using System.Linq;
|
||||
using Microsoft.UI.Windowing;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace BetterLyrics.WinUI3.Views {
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class MainPage : Page {
|
||||
|
||||
public MainViewModel ViewModel => (MainViewModel)DataContext;
|
||||
private SettingsService _settingsService;
|
||||
|
||||
private List<LyricsLine> _lyricsLines = [];
|
||||
|
||||
private CanvasBitmap? _canvasCoverBitmap;
|
||||
|
||||
private float _coverBitmapRotateAngle = 0f;
|
||||
private float _coverScaleFactor = 1;
|
||||
|
||||
private float _coverRotateSpeed = 0.003f;
|
||||
|
||||
private float _lyricsGlowEffectAngle = 0f;
|
||||
private float _lyricsGlowEffectSpeed = 0.01f;
|
||||
|
||||
private float _lyricsGlowEffectMinBlurAmount = 0f;
|
||||
private float _lyricsGlowEffectMaxBlurAmount = 6f;
|
||||
|
||||
private int _animationDurationMs = 200;
|
||||
|
||||
private DispatcherQueueTimer _queueTimer;
|
||||
|
||||
private TimeSpan _currentTime = TimeSpan.Zero;
|
||||
|
||||
private float _defaultOpacity = 0.3f;
|
||||
private float _highlightedOpacity = 1.0f;
|
||||
|
||||
private float _defaultScale = 0.95f;
|
||||
private float _highlightedScale = 1.0f;
|
||||
|
||||
private int _lineEnteringDurationMs = 800;
|
||||
private int _lineExitingDurationMs = 800;
|
||||
private int _lineScrollDurationMs = 800;
|
||||
|
||||
private float _lastTotalYScroll = 0.0f;
|
||||
private float _totalYScroll = 0.0f;
|
||||
|
||||
private double _lyricsAreaWidth = 0.0f;
|
||||
private double _lyricsAreaHeight = 0.0f;
|
||||
|
||||
private double _lyricsCanvasRightMargin = 36;
|
||||
private double _lyricsCanvasLeftMargin = 0;
|
||||
private double _lyricsCanvasMaxTextWidth = 0;
|
||||
|
||||
private int _startVisibleLineIndex = -1;
|
||||
private int _endVisibleLineIndex = -1;
|
||||
|
||||
private bool _forceToScroll = false;
|
||||
private bool _isRelayoutLyricsNeeded = false;
|
||||
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
private GlobalSystemMediaTransportControlsSessionManager? _sessionManager = null;
|
||||
private GlobalSystemMediaTransportControlsSession? _currentSession = null;
|
||||
|
||||
private Color _lyricsColor;
|
||||
|
||||
public MainPage() {
|
||||
this.InitializeComponent();
|
||||
|
||||
SetLyricsColor();
|
||||
|
||||
_settingsService = Ioc.Default.GetService<SettingsService>()!;
|
||||
|
||||
if (_settingsService.IsFirstRun) {
|
||||
WelcomeTeachingTip.IsOpen = true;
|
||||
}
|
||||
|
||||
_settingsService.PropertyChanged += SettingsService_PropertyChanged;
|
||||
|
||||
DataContext = Ioc.Default.GetService<MainViewModel>();
|
||||
|
||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
|
||||
_queueTimer = _dispatcherQueue.CreateTimer();
|
||||
}
|
||||
|
||||
private async Task ForceToScrollToCurrentPlayingLineAsync() {
|
||||
_forceToScroll = true;
|
||||
await Task.Delay(1);
|
||||
_forceToScroll = false;
|
||||
}
|
||||
|
||||
private async void SettingsService_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) {
|
||||
switch (e.PropertyName) {
|
||||
case nameof(_settingsService.LyricsFontSize):
|
||||
case nameof(_settingsService.LyricsLineSpacingFactor):
|
||||
LayoutLyrics();
|
||||
await ForceToScrollToCurrentPlayingLineAsync();
|
||||
break;
|
||||
case nameof(_settingsService.IsRebuildingLyricsIndexDatabase):
|
||||
if (!_settingsService.IsRebuildingLyricsIndexDatabase) {
|
||||
CurrentSession_MediaPropertiesChanged(_currentSession, null);
|
||||
}
|
||||
break;
|
||||
case nameof(_settingsService.Theme):
|
||||
SetLyricsColor();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) {
|
||||
switch (e.PropertyName) {
|
||||
case nameof(ViewModel.ShowLyricsOnly):
|
||||
RootGrid_SizeChanged(null, null);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLyricsColor() {
|
||||
_lyricsColor = ((SolidColorBrush)TitleTextBlock.Foreground).Color;
|
||||
}
|
||||
|
||||
private async void InitMediaManager() {
|
||||
_sessionManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
|
||||
_sessionManager.CurrentSessionChanged += SessionManager_CurrentSessionChanged;
|
||||
_sessionManager.SessionsChanged += SessionManager_SessionsChanged;
|
||||
|
||||
SessionManager_CurrentSessionChanged(_sessionManager, null);
|
||||
}
|
||||
|
||||
private void CurrentSession_TimelinePropertiesChanged(GlobalSystemMediaTransportControlsSession sender, TimelinePropertiesChangedEventArgs args) {
|
||||
if (sender == null) {
|
||||
_currentTime = TimeSpan.Zero;
|
||||
return;
|
||||
}
|
||||
|
||||
_currentTime = sender.GetTimelineProperties().Position;
|
||||
// Debug.WriteLine(_currentTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note: Non-UI thread
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="args"></param>
|
||||
private void CurrentSession_PlaybackInfoChanged(GlobalSystemMediaTransportControlsSession sender, PlaybackInfoChangedEventArgs args) {
|
||||
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () => {
|
||||
if (sender == null) {
|
||||
LyricsCanvas.Paused = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var playbackState = sender.GetPlaybackInfo().PlaybackStatus;
|
||||
Debug.WriteLine(playbackState);
|
||||
|
||||
switch (playbackState) {
|
||||
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed:
|
||||
LyricsCanvas.Paused = true;
|
||||
break;
|
||||
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Opened:
|
||||
LyricsCanvas.Paused = true;
|
||||
break;
|
||||
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Changing:
|
||||
LyricsCanvas.Paused = true;
|
||||
break;
|
||||
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Stopped:
|
||||
LyricsCanvas.Paused = true;
|
||||
break;
|
||||
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing:
|
||||
LyricsCanvas.Paused = false;
|
||||
break;
|
||||
case GlobalSystemMediaTransportControlsSessionPlaybackStatus.Paused:
|
||||
LyricsCanvas.Paused = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void SessionManager_SessionsChanged(GlobalSystemMediaTransportControlsSessionManager sender, SessionsChangedEventArgs args) {
|
||||
Debug.WriteLine("SessionManager_SessionsChanged");
|
||||
}
|
||||
|
||||
private void SessionManager_CurrentSessionChanged(GlobalSystemMediaTransportControlsSessionManager sender, CurrentSessionChangedEventArgs args) {
|
||||
Debug.WriteLine("SessionManager_CurrentSessionChanged");
|
||||
// Unregister events associated with the previous session
|
||||
if (_currentSession != null) {
|
||||
_currentSession.MediaPropertiesChanged -= CurrentSession_MediaPropertiesChanged;
|
||||
_currentSession.PlaybackInfoChanged -= CurrentSession_PlaybackInfoChanged;
|
||||
_currentSession.TimelinePropertiesChanged -= CurrentSession_TimelinePropertiesChanged;
|
||||
}
|
||||
|
||||
// Record and register events for current session
|
||||
_currentSession = sender.GetCurrentSession();
|
||||
|
||||
if (_currentSession != null) {
|
||||
_currentSession.MediaPropertiesChanged += CurrentSession_MediaPropertiesChanged;
|
||||
_currentSession.PlaybackInfoChanged += CurrentSession_PlaybackInfoChanged;
|
||||
_currentSession.TimelinePropertiesChanged += CurrentSession_TimelinePropertiesChanged;
|
||||
}
|
||||
|
||||
CurrentSession_MediaPropertiesChanged(_currentSession, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note: this func is invoked by non-UI thread
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="args"></param>
|
||||
private void CurrentSession_MediaPropertiesChanged(GlobalSystemMediaTransportControlsSession sender, MediaPropertiesChangedEventArgs args) {
|
||||
_queueTimer.Debounce(() => {
|
||||
Debug.WriteLine("CurrentSession_MediaPropertiesChanged");
|
||||
_dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, async () => {
|
||||
|
||||
GlobalSystemMediaTransportControlsSessionMediaProperties? mediaProps = null;
|
||||
|
||||
if (_currentSession != null) {
|
||||
try {
|
||||
mediaProps = await _currentSession.TryGetMediaPropertiesAsync();
|
||||
} catch (Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
ViewModel.IsAnyMusicSessionExisted = _currentSession != null;
|
||||
|
||||
ViewModel.AboutToUpdateUI = true;
|
||||
await Task.Delay(_animationDurationMs);
|
||||
|
||||
(_lyricsLines, _canvasCoverBitmap) = await ViewModel.SetSongInfoAsync(mediaProps, LyricsCanvas);
|
||||
|
||||
// Force to show lyrics and scroll to current line even if the music is not playing
|
||||
LyricsCanvas.Paused = false;
|
||||
await ForceToScrollToCurrentPlayingLineAsync();
|
||||
// Detect and recover the music state
|
||||
CurrentSession_PlaybackInfoChanged(_currentSession, null);
|
||||
CurrentSession_TimelinePropertiesChanged(_currentSession, null);
|
||||
|
||||
ViewModel.AboutToUpdateUI = false;
|
||||
|
||||
if (_lyricsLines.Count == 0) {
|
||||
Grid.SetColumnSpan(SongInfoStackPanel, 3);
|
||||
} else {
|
||||
Grid.SetColumnSpan(SongInfoStackPanel, 1);
|
||||
}
|
||||
|
||||
});
|
||||
}, TimeSpan.FromMilliseconds(200));
|
||||
|
||||
}
|
||||
|
||||
private async void RootGrid_SizeChanged(object sender, SizeChangedEventArgs e) {
|
||||
//_queueTimer.Debounce(async () => {
|
||||
|
||||
_lyricsAreaHeight = LyricsGrid.ActualHeight;
|
||||
_lyricsAreaWidth = LyricsGrid.ActualWidth;
|
||||
|
||||
if (SongInfoColumnDefinition.ActualWidth == 0 || ViewModel.ShowLyricsOnly) {
|
||||
_lyricsCanvasLeftMargin = 36;
|
||||
} else {
|
||||
_lyricsCanvasLeftMargin = 36 + SongInfoColumnDefinition.ActualWidth + 36;
|
||||
}
|
||||
|
||||
_lyricsCanvasMaxTextWidth = _lyricsAreaWidth - _lyricsCanvasLeftMargin - _lyricsCanvasRightMargin;
|
||||
|
||||
LayoutLyrics();
|
||||
await ForceToScrollToCurrentPlayingLineAsync();
|
||||
|
||||
//}, TimeSpan.FromMilliseconds(50));
|
||||
}
|
||||
|
||||
// Comsumes GPU related resources
|
||||
private void LyricsCanvas_Draw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args) {
|
||||
using var ds = args.DrawingSession;
|
||||
|
||||
var r = _lyricsColor.R;
|
||||
var g = _lyricsColor.G;
|
||||
var b = _lyricsColor.B;
|
||||
|
||||
// Draw (dynamic) cover image as the very first layer
|
||||
if (_settingsService.IsCoverOverlayEnabled && _canvasCoverBitmap != null) {
|
||||
DrawCoverImage(sender, ds, _settingsService.IsDynamicCoverOverlay);
|
||||
}
|
||||
|
||||
// Lyrics only layer
|
||||
using var lyrics = new CanvasCommandList(sender);
|
||||
using (var lyricsDs = lyrics.CreateDrawingSession()) {
|
||||
DrawLyrics(sender, lyricsDs, r, g, b);
|
||||
}
|
||||
|
||||
using var glowedLyrics = new CanvasCommandList(sender);
|
||||
using (var glowedLyricsDs = glowedLyrics.CreateDrawingSession()) {
|
||||
if (_settingsService.IsLyricsGlowEffectEnabled) {
|
||||
glowedLyricsDs.DrawImage(new GaussianBlurEffect {
|
||||
Source = lyrics,
|
||||
BlurAmount = MathF.Sin(_lyricsGlowEffectAngle) * (_lyricsGlowEffectMaxBlurAmount - _lyricsGlowEffectMinBlurAmount) / 2f +
|
||||
(_lyricsGlowEffectMaxBlurAmount + _lyricsGlowEffectMinBlurAmount) / 2f,
|
||||
BorderMode = EffectBorderMode.Soft,
|
||||
Optimization = EffectOptimization.Quality,
|
||||
});
|
||||
}
|
||||
glowedLyricsDs.DrawImage(lyrics);
|
||||
}
|
||||
|
||||
// Mock gradient blurred lyrics layer
|
||||
using var combinedBlurredLyrics = new CanvasCommandList(sender);
|
||||
using var combinedBlurredLyricsDs = combinedBlurredLyrics.CreateDrawingSession();
|
||||
if (_settingsService.LyricsBlurAmount == 0) {
|
||||
combinedBlurredLyricsDs.DrawImage(glowedLyrics);
|
||||
} else {
|
||||
double step = 0.05;
|
||||
double overlapFactor = 0;
|
||||
for (double i = 0; i <= 0.5 - step; i += step) {
|
||||
using var blurredLyrics = new GaussianBlurEffect {
|
||||
Source = glowedLyrics,
|
||||
BlurAmount = (float)(_settingsService.LyricsBlurAmount * (1 - i / (0.5 - step))),
|
||||
Optimization = EffectOptimization.Quality,
|
||||
BorderMode = EffectBorderMode.Soft,
|
||||
};
|
||||
using var topCropped = new CropEffect {
|
||||
Source = blurredLyrics,
|
||||
SourceRectangle = new Rect(0, sender.Size.Height * i, sender.Size.Width, sender.Size.Height * step * (1 + overlapFactor))
|
||||
};
|
||||
using var bottomCropped = new CropEffect {
|
||||
Source = blurredLyrics,
|
||||
SourceRectangle = new Rect(0, sender.Size.Height * (1 - i - step * (1 + overlapFactor)), sender.Size.Width, sender.Size.Height * step * (1 + overlapFactor))
|
||||
};
|
||||
combinedBlurredLyricsDs.DrawImage(topCropped);
|
||||
combinedBlurredLyricsDs.DrawImage(bottomCropped);
|
||||
}
|
||||
}
|
||||
|
||||
// Masked mock gradient blurred lyrics layer
|
||||
using var maskedCombinedBlurredLyrics = new CanvasCommandList(sender);
|
||||
using (var maskedCombinedBlurredLyricsDs = maskedCombinedBlurredLyrics.CreateDrawingSession()) {
|
||||
if (_settingsService.LyricsVerticalEdgeOpacity == 100) {
|
||||
maskedCombinedBlurredLyricsDs.DrawImage(combinedBlurredLyrics);
|
||||
} else {
|
||||
using var mask = new CanvasCommandList(sender);
|
||||
using (var maskDs = mask.CreateDrawingSession()) {
|
||||
DrawGradientOpacityMask(sender, maskDs, r, g, b);
|
||||
}
|
||||
maskedCombinedBlurredLyricsDs.DrawImage(new AlphaMaskEffect {
|
||||
Source = combinedBlurredLyrics,
|
||||
AlphaMask = mask
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the final composed layer
|
||||
ds.DrawImage(maskedCombinedBlurredLyrics);
|
||||
|
||||
}
|
||||
|
||||
private void DrawLyrics(ICanvasAnimatedControl control, CanvasDrawingSession ds, byte r, byte g, byte b) {
|
||||
|
||||
var (displayStartLineIndex, displayEndLineIndex) = GetVisibleLyricsLineIndexBoundaries();
|
||||
|
||||
for (int i = displayStartLineIndex; _lyricsLines.Count > 0 && i >= 0 && i < _lyricsLines.Count && i <= displayEndLineIndex; i++) {
|
||||
|
||||
var line = _lyricsLines[i];
|
||||
|
||||
if (line.TextLayout == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
float progressPerChar = 1f / line.Text.Length;
|
||||
|
||||
var position = line.Position;
|
||||
|
||||
float centerX = position.X;
|
||||
float centerY = position.Y + (float)line.TextLayout.LayoutBounds.Height / 2;
|
||||
|
||||
switch ((LyricsAlignmentType)_settingsService.LyricsAlignmentType) {
|
||||
case LyricsAlignmentType.Left:
|
||||
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Left;
|
||||
break;
|
||||
case LyricsAlignmentType.Center:
|
||||
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Center;
|
||||
centerX += (float)_lyricsCanvasMaxTextWidth / 2;
|
||||
break;
|
||||
case LyricsAlignmentType.Right:
|
||||
line.TextLayout.HorizontalAlignment = CanvasHorizontalAlignment.Right;
|
||||
centerX += (float)_lyricsCanvasMaxTextWidth;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
int startIndex = 0;
|
||||
|
||||
// Set brush
|
||||
for (int j = 0; j < line.TextLayout.LineCount; j++) {
|
||||
|
||||
int count = line.TextLayout.LineMetrics[j].CharacterCount;
|
||||
var regions = line.TextLayout.GetCharacterRegions(startIndex, count);
|
||||
float subLinePlayingProgress = Math.Clamp((line.PlayingProgress * line.Text.Length - startIndex) / count, 0, 1);
|
||||
|
||||
using var horizontalFillBrush = new CanvasLinearGradientBrush(control, [
|
||||
new() { Position = 0, Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b) },
|
||||
new() { Position = subLinePlayingProgress * (1 + progressPerChar) - progressPerChar, Color = Color.FromArgb((byte)(255 * line.Opacity), r, g, b) },
|
||||
new() { Position = subLinePlayingProgress * (1 + progressPerChar), Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b) },
|
||||
new() { Position = 1.5f, Color = Color.FromArgb((byte)(255 * _defaultOpacity), r, g, b) },
|
||||
]) {
|
||||
StartPoint = new Vector2((float)(regions[0].LayoutBounds.Left + position.X), 0),
|
||||
EndPoint = new Vector2((float)(regions[^1].LayoutBounds.Right + position.X), 0)
|
||||
};
|
||||
|
||||
line.TextLayout.SetBrush(startIndex, count, horizontalFillBrush);
|
||||
startIndex += count;
|
||||
|
||||
}
|
||||
|
||||
// Scale
|
||||
ds.Transform = Matrix3x2.CreateScale(line.Scale, new Vector2(centerX, centerY)) * Matrix3x2.CreateTranslation(0, _totalYScroll);
|
||||
//Debug.WriteLine(_totalYScroll);
|
||||
|
||||
ds.DrawTextLayout(line.TextLayout, position, Colors.Transparent);
|
||||
|
||||
// Reset scale
|
||||
ds.Transform = Matrix3x2.Identity;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void DrawCoverImage(ICanvasAnimatedControl control, CanvasDrawingSession ds, bool dynamic) {
|
||||
if (_settingsService.IsCoverOverlayEnabled && _canvasCoverBitmap != null) {
|
||||
|
||||
ds.Transform = Matrix3x2.CreateRotation(_coverBitmapRotateAngle, control.Size.ToVector2() * 0.5f);
|
||||
|
||||
using var coverOverlayEffect = new OpacityEffect {
|
||||
Opacity = _settingsService.CoverOverlayOpacity / 100f,
|
||||
Source = new GaussianBlurEffect {
|
||||
BlurAmount = _settingsService.CoverOverlayBlurAmount,
|
||||
Source = new ScaleEffect {
|
||||
InterpolationMode = CanvasImageInterpolation.HighQualityCubic,
|
||||
BorderMode = EffectBorderMode.Hard,
|
||||
Scale = new Vector2(_coverScaleFactor),
|
||||
Source = _canvasCoverBitmap,
|
||||
}
|
||||
}
|
||||
};
|
||||
ds.DrawImage(
|
||||
coverOverlayEffect,
|
||||
(float)control.Size.Width / 2 - _canvasCoverBitmap.SizeInPixels.Width * _coverScaleFactor / 2,
|
||||
(float)control.Size.Height / 2 - _canvasCoverBitmap.SizeInPixels.Height * _coverScaleFactor / 2);
|
||||
ds.Transform = Matrix3x2.Identity;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawGradientOpacityMask(ICanvasAnimatedControl control, CanvasDrawingSession ds, byte r, byte g, byte b) {
|
||||
byte verticalEdgeAlpha = (byte)(255 * _settingsService.LyricsVerticalEdgeOpacity / 100f);
|
||||
using var maskBrush = new CanvasLinearGradientBrush(control, [
|
||||
new() { Position = 0, Color = Color.FromArgb(verticalEdgeAlpha, r, g, b)},
|
||||
new() { Position = 0.5f, Color = Color.FromArgb(255, r, g, b)},
|
||||
new() { Position = 1, Color = Color.FromArgb(verticalEdgeAlpha, r, g, b)},
|
||||
]) {
|
||||
StartPoint = new Vector2(0, 0),
|
||||
EndPoint = new Vector2(0, (float)control.Size.Height)
|
||||
};
|
||||
ds.FillRectangle(new Rect(0, 0, control.Size.Width, control.Size.Height), maskBrush);
|
||||
}
|
||||
|
||||
|
||||
// Comsumes CPU related resources
|
||||
private void LyricsCanvas_Update(ICanvasAnimatedControl sender, CanvasAnimatedUpdateEventArgs args) {
|
||||
_currentTime += args.Timing.ElapsedTime;
|
||||
|
||||
if (_settingsService.IsDynamicCoverOverlay) {
|
||||
_coverBitmapRotateAngle += _coverRotateSpeed;
|
||||
_coverBitmapRotateAngle %= MathF.PI * 2;
|
||||
}
|
||||
if (_settingsService.IsLyricsDynamicGlowEffectEnabled) {
|
||||
_lyricsGlowEffectAngle += _lyricsGlowEffectSpeed;
|
||||
_lyricsGlowEffectAngle %= MathF.PI * 2;
|
||||
}
|
||||
|
||||
if (_settingsService.IsCoverOverlayEnabled && _canvasCoverBitmap != null) {
|
||||
|
||||
var diagonal = Math.Sqrt(Math.Pow(_lyricsAreaWidth, 2) + Math.Pow(_lyricsAreaHeight, 2));
|
||||
|
||||
_coverScaleFactor = (float)diagonal / Math.Min(
|
||||
_canvasCoverBitmap.SizeInPixels.Width,
|
||||
_canvasCoverBitmap.SizeInPixels.Height);
|
||||
|
||||
}
|
||||
|
||||
if (_lyricsLines.LastOrDefault()?.TextLayout == null || _isRelayoutLyricsNeeded) {
|
||||
LayoutLyrics();
|
||||
}
|
||||
|
||||
int currentPlayingLineIndex = GetCurrentPlayingLineIndex();
|
||||
UpdateScaleAndOpacity(currentPlayingLineIndex);
|
||||
UpdatePosition(currentPlayingLineIndex);
|
||||
}
|
||||
|
||||
private int GetCurrentPlayingLineIndex() {
|
||||
for (int i = 0; i < _lyricsLines.Count; i++) {
|
||||
|
||||
var line = _lyricsLines[i];
|
||||
if (line.EndPlayingTimestampMs < _currentTime.TotalMilliseconds) {
|
||||
continue;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
|
||||
}
|
||||
|
||||
private Tuple<int, int> GetVisibleLyricsLineIndexBoundaries() {
|
||||
//Debug.WriteLine($"{_startVisibleLineIndex} {_endVisibleLineIndex}");
|
||||
return new Tuple<int, int>(_startVisibleLineIndex, _endVisibleLineIndex);
|
||||
}
|
||||
|
||||
private Tuple<int, int> GetMaxLyricsLineIndexBoundaries() {
|
||||
if (_lyricsLines.Count == 0) {
|
||||
return new Tuple<int, int>(-1, -1);
|
||||
}
|
||||
|
||||
return new Tuple<int, int>(0, _lyricsLines.Count - 1);
|
||||
}
|
||||
|
||||
private void UpdateScaleAndOpacity(int currentPlayingLineIndex) {
|
||||
|
||||
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
|
||||
|
||||
for (int i = startLineIndex; _lyricsLines.Count > 0 && i <= endLineIndex; i++) {
|
||||
|
||||
var line = _lyricsLines[i];
|
||||
|
||||
bool linePlaying = i == currentPlayingLineIndex;
|
||||
|
||||
var lineEnteringDurationMs = Math.Min(line.DurationMs, _lineEnteringDurationMs);
|
||||
var lineExitingDurationMs = _lineExitingDurationMs;
|
||||
if (i + 1 <= endLineIndex) {
|
||||
lineExitingDurationMs = Math.Min(_lyricsLines[i + 1].DurationMs, lineExitingDurationMs);
|
||||
}
|
||||
|
||||
float lineEnteringProgress = 0.0f;
|
||||
float lineExitingProgress = 0.0f;
|
||||
|
||||
bool lineEntering = false;
|
||||
bool lineExiting = false;
|
||||
|
||||
float scale = _defaultScale;
|
||||
float opacity = _defaultOpacity;
|
||||
|
||||
float playProgress = 0;
|
||||
|
||||
if (linePlaying) {
|
||||
line.PlayingState = LyricsPlayingState.Playing;
|
||||
|
||||
scale = _highlightedScale;
|
||||
opacity = _highlightedOpacity;
|
||||
|
||||
playProgress = ((float)_currentTime.TotalMilliseconds - line.StartPlayingTimestampMs) / line.DurationMs;
|
||||
|
||||
var durationFromStartMs = _currentTime.TotalMilliseconds - line.StartPlayingTimestampMs;
|
||||
lineEntering = durationFromStartMs <= lineEnteringDurationMs;
|
||||
if (lineEntering) {
|
||||
lineEnteringProgress = (float)durationFromStartMs / lineEnteringDurationMs;
|
||||
scale = _defaultScale + (_highlightedScale - _defaultScale) * (float)lineEnteringProgress;
|
||||
opacity = _defaultOpacity + (_highlightedOpacity - _defaultOpacity) * (float)lineEnteringProgress;
|
||||
}
|
||||
|
||||
} else {
|
||||
if (i < currentPlayingLineIndex) {
|
||||
line.PlayingState = LyricsPlayingState.Played;
|
||||
playProgress = 1;
|
||||
|
||||
var durationToEndMs = _currentTime.TotalMilliseconds - line.EndPlayingTimestampMs;
|
||||
lineExiting = durationToEndMs <= lineExitingDurationMs;
|
||||
if (lineExiting) {
|
||||
|
||||
lineExitingProgress = (float)durationToEndMs / lineExitingDurationMs;
|
||||
scale = _highlightedScale - (_highlightedScale - _defaultScale) * (float)lineExitingProgress;
|
||||
opacity = _highlightedOpacity - (_highlightedOpacity - _defaultOpacity) * (float)lineExitingProgress;
|
||||
}
|
||||
|
||||
} else {
|
||||
line.PlayingState = LyricsPlayingState.NotPlayed;
|
||||
}
|
||||
}
|
||||
|
||||
line.EnteringProgress = lineEnteringProgress;
|
||||
line.ExitingProgress = lineExitingProgress;
|
||||
|
||||
line.Scale = scale;
|
||||
line.Opacity = opacity;
|
||||
|
||||
line.PlayingProgress = playProgress;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void LayoutLyrics() {
|
||||
|
||||
using CanvasTextFormat textFormat = new() {
|
||||
FontSize = _settingsService.LyricsFontSize,
|
||||
HorizontalAlignment = CanvasHorizontalAlignment.Left,
|
||||
VerticalAlignment = CanvasVerticalAlignment.Top,
|
||||
FontWeight = FontWeights.Bold,
|
||||
//FontFamily = "Segoe UI Mono",
|
||||
};
|
||||
float y = (float)_lyricsAreaHeight / 2;
|
||||
|
||||
// Init Positions
|
||||
for (int i = 0; i < _lyricsLines.Count; i++) {
|
||||
|
||||
var line = _lyricsLines[i];
|
||||
|
||||
// Calculate layout bounds
|
||||
line.TextLayout = new CanvasTextLayout(LyricsCanvas.Device, line.Text, textFormat, (float)_lyricsCanvasMaxTextWidth, (float)_lyricsAreaHeight);
|
||||
line.Position = new Vector2((float)_lyricsCanvasLeftMargin, y);
|
||||
|
||||
y += (float)line.TextLayout.LayoutBounds.Height / line.TextLayout.LineCount * (line.TextLayout.LineCount + _settingsService.LyricsLineSpacingFactor);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void UpdatePosition(int currentPlayingLineIndex) {
|
||||
|
||||
if (currentPlayingLineIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var (startLineIndex, endLineIndex) = GetMaxLyricsLineIndexBoundaries();
|
||||
|
||||
if (startLineIndex < 0 || endLineIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set _scrollOffsetY
|
||||
LyricsLine? currentPlayingLine = _lyricsLines?[currentPlayingLineIndex];
|
||||
|
||||
if (currentPlayingLine == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentPlayingLine.TextLayout == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lineScrollingProgress =
|
||||
(_currentTime.TotalMilliseconds - currentPlayingLine.StartPlayingTimestampMs) /
|
||||
Math.Min(_lineScrollDurationMs, currentPlayingLine.DurationMs);
|
||||
|
||||
var targetYScrollOffset = (float)(-currentPlayingLine.Position.Y + _lyricsLines![0].Position.Y - currentPlayingLine.TextLayout.LayoutBounds.Height / 2 - _lastTotalYScroll);
|
||||
|
||||
var yScrollOffset = targetYScrollOffset * EasingHelper.SmootherStep((float)Math.Min(1, lineScrollingProgress));
|
||||
|
||||
bool isScrollingNow = lineScrollingProgress <= 1;
|
||||
|
||||
if (isScrollingNow) {
|
||||
_totalYScroll = _lastTotalYScroll + yScrollOffset;
|
||||
} else {
|
||||
if (_forceToScroll && Math.Abs(targetYScrollOffset) >= 1) {
|
||||
_totalYScroll = _lastTotalYScroll + targetYScrollOffset;
|
||||
}
|
||||
_lastTotalYScroll = _totalYScroll;
|
||||
}
|
||||
|
||||
_startVisibleLineIndex = _endVisibleLineIndex = -1;
|
||||
|
||||
// Update Positions
|
||||
for (int i = startLineIndex; i >= 0 && i <= endLineIndex; i++) {
|
||||
|
||||
var line = _lyricsLines[i];
|
||||
|
||||
if (_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height >= 0) {
|
||||
if (_startVisibleLineIndex == -1) {
|
||||
_startVisibleLineIndex = i;
|
||||
}
|
||||
}
|
||||
if (_totalYScroll + line.Position.Y + line.TextLayout.LayoutBounds.Height >= _lyricsAreaHeight) {
|
||||
if (_endVisibleLineIndex == -1) {
|
||||
_endVisibleLineIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_startVisibleLineIndex != -1 && _endVisibleLineIndex == -1) {
|
||||
_endVisibleLineIndex = endLineIndex;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void LyricsCanvas_Loaded(object sender, RoutedEventArgs e) {
|
||||
InitMediaManager();
|
||||
}
|
||||
|
||||
private void SettingsButton_Click(object sender, RoutedEventArgs e) {
|
||||
if (App.Current.SettingsWindow == null) {
|
||||
App.Current.SettingsWindow = new MainWindow();
|
||||
App.Current.SettingsWindow!.Navigate(typeof(SettingsPage));
|
||||
}
|
||||
App.Current.SettingsWindow.AppWindow.Show();
|
||||
}
|
||||
|
||||
private void WelcomeTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args) {
|
||||
TopCommandTeachingTip.IsOpen = true;
|
||||
}
|
||||
|
||||
private void TopCommandTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args) {
|
||||
BottomCommandTeachingTip.IsOpen = true;
|
||||
}
|
||||
|
||||
private void BottomCommandTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args) {
|
||||
LyricsOnlyTeachingTip.IsOpen = true;
|
||||
}
|
||||
|
||||
private void LyricsOnlyTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args) {
|
||||
InitDatabaseTeachingTip.IsOpen = true;
|
||||
}
|
||||
|
||||
private void InitDatabaseTeachingTip_Closed(TeachingTip sender, TeachingTipClosedEventArgs args) {
|
||||
_settingsService.IsFirstRun = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
368
BetterLyrics.WinUI3/BetterLyrics.WinUI3/Views/SettingsPage.xaml
Normal file
@@ -0,0 +1,368 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="BetterLyrics.WinUI3.Views.SettingsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:local="using:BetterLyrics.WinUI3.Views"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:vm="using:BetterLyrics.WinUI3.ViewModels"
|
||||
x:Name="RootPage"
|
||||
NavigationCacheMode="Required"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid x:Name="RootGrid">
|
||||
|
||||
<controls:OpacityMaskView HorizontalContentAlignment="Stretch">
|
||||
<controls:OpacityMaskView.OpacityMask>
|
||||
<Rectangle Fill="{StaticResource BaseHighEdgeVerticalFadeBrush}" />
|
||||
</controls:OpacityMaskView.OpacityMask>
|
||||
<ScrollViewer>
|
||||
<Grid Margin="36,72,36,72">
|
||||
<StackPanel Spacing="{StaticResource SettingsCardSpacing}">
|
||||
|
||||
<TextBlock x:Uid="SettingsPageLyricsLib" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsExpander
|
||||
x:Uid="SettingsPageMusicLib"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True"
|
||||
ItemsSource="{x:Bind ViewModel.SettingsService.MusicLibraries, Mode=OneWay}">
|
||||
<controls:SettingsExpander.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<controls:SettingsCard Header="{Binding Path, Mode=OneWay}">
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{Binding IsValid, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<interactivity:ChangePropertyAction PropertyName="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{Binding IsValid, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False">
|
||||
<interactivity:ChangePropertyAction PropertyName="Background" Value="{ThemeResource SystemFillColorCriticalBackgroundBrush}" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
</interactivity:Interaction.Behaviors>
|
||||
<controls:SettingsCard.Description>
|
||||
<TextBlock x:Uid="SettingsPagePathNotFound" Foreground="{ThemeResource SystemFillColorCriticalBrush}">
|
||||
<interactivity:Interaction.Behaviors>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{Binding IsValid, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="True">
|
||||
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{Binding IsValid, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="False">
|
||||
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
</interactivity:Interaction.Behaviors>
|
||||
</TextBlock>
|
||||
</controls:SettingsCard.Description>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<HyperlinkButton
|
||||
x:Uid="SettingsPageOpenPath"
|
||||
Command="{Binding DataContext.OpenFolderInFileExplorerCommand, ElementName=RootPage}"
|
||||
CommandParameter="{Binding}"
|
||||
IsEnabled="{Binding IsValid, Mode=OneWay}" />
|
||||
<HyperlinkButton
|
||||
x:Uid="SettingsPageRemovePath"
|
||||
Command="{Binding DataContext.RemoveFolderCommand, ElementName=RootPage}"
|
||||
CommandParameter="{Binding}"
|
||||
IsEnabled="{Binding DataContext.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay, ElementName=RootPage}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
</DataTemplate>
|
||||
</controls:SettingsExpander.ItemTemplate>
|
||||
<controls:SettingsExpander.ItemsHeader>
|
||||
<InfoBar
|
||||
x:Uid="SettingsPageRemoveInfo"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Severity="Success">
|
||||
|
||||
<interactivity:Interaction.Behaviors>
|
||||
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.SettingsService.MusicLibraries.Count, Mode=OneWay}"
|
||||
ComparisonCondition="Equal"
|
||||
Value="0">
|
||||
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Collapsed" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
<interactivity:DataTriggerBehavior
|
||||
Binding="{x:Bind ViewModel.SettingsService.MusicLibraries.Count, Mode=OneWay}"
|
||||
ComparisonCondition="NotEqual"
|
||||
Value="0">
|
||||
<interactivity:ChangePropertyAction PropertyName="Visibility" Value="Visible" />
|
||||
</interactivity:DataTriggerBehavior>
|
||||
|
||||
</interactivity:Interaction.Behaviors>
|
||||
|
||||
</InfoBar>
|
||||
</controls:SettingsExpander.ItemsHeader>
|
||||
<controls:SettingsExpander.ItemsFooter>
|
||||
<controls:SettingsCard
|
||||
x:Uid="SettingsPageAddFolder"
|
||||
IsEnabled="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}"
|
||||
Style="{StaticResource DefaultSettingsExpanderItemStyle}">
|
||||
<Button x:Uid="SettingsPageAddFolderButton" Command="{x:Bind ViewModel.AddFolderCommand}" />
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.ItemsFooter>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<controls:SettingsCard
|
||||
x:Uid="SettingsPageRebuildDatabase"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolNegationConverter}, Mode=OneWay}">
|
||||
<controls:SettingsCard.Description>
|
||||
<TextBlock x:Uid="SettingsPageRebuildDatabaseDesc" Visibility="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
|
||||
</controls:SettingsCard.Description>
|
||||
<StackPanel>
|
||||
<Button x:Uid="SettingsPageRebuildDatabaseButton" Command="{x:Bind ViewModel.RebuildLyricsIndexDatabaseCommand}" />
|
||||
<ProgressBar
|
||||
IsIndeterminate="True"
|
||||
ShowError="False"
|
||||
ShowPaused="False"
|
||||
Visibility="{x:Bind ViewModel.SettingsService.IsRebuildingLyricsIndexDatabase, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<TextBlock x:Uid="SettingsPageAppAppearance" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="SettingsPageTheme" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox x:Name="ThemeComboBox" SelectedIndex="{x:Bind ViewModel.SettingsService.Theme, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="SettingsPageFollowSystem" />
|
||||
<ComboBoxItem x:Uid="SettingsPageLight" />
|
||||
<ComboBoxItem x:Uid="SettingsPageDark" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="SettingsPageBackdrop" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox x:Name="BackdropComboBox" SelectedIndex="{x:Bind ViewModel.SettingsService.BackdropType, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="SettingsPageNoBackdrop" />
|
||||
<ComboBoxItem x:Uid="SettingsPageMica" />
|
||||
<ComboBoxItem x:Uid="SettingsPageMicaAlt" />
|
||||
<ComboBoxItem x:Uid="SettingsPageDesktopAcrylic" />
|
||||
<ComboBoxItem x:Uid="SettingsPageAcrylicThin" />
|
||||
<ComboBoxItem x:Uid="SettingsPageAcrylicBase" />
|
||||
<ComboBoxItem x:Uid="SettingsPageTransparent" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsExpander
|
||||
x:Uid="SettingsPageLanguage"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<ComboBox SelectedIndex="{x:Bind ViewModel.SettingsService.Language, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="SettingsPageSystemLanguage" />
|
||||
<ComboBoxItem x:Uid="SettingsPageEN" />
|
||||
<ComboBoxItem x:Uid="SettingsPageSC" />
|
||||
<ComboBoxItem x:Uid="SettingsPageTC" />
|
||||
</ComboBox>
|
||||
<controls:SettingsExpander.Items>
|
||||
<controls:SettingsCard>
|
||||
<Button x:Uid="SettingsPageRestart" Command="{x:Bind ViewModel.RestartAppCommand}" />
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<TextBlock x:Uid="SettingsPageAlbum" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsExpander
|
||||
x:Uid="SettingsPageCoverOverlay"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<controls:SettingsExpander.Description>
|
||||
<StackPanel>
|
||||
<TextBlock x:Uid="SettingsPageCoverOverlayGPUUsage" />
|
||||
</StackPanel>
|
||||
</controls:SettingsExpander.Description>
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=TwoWay}" />
|
||||
<controls:SettingsExpander.Items>
|
||||
|
||||
<controls:SettingsCard x:Uid="SettingsPageDynamicCoverOverlay" IsEnabled="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=OneWay}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsDynamicCoverOverlay, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="SettingsPageCoverOverlayOpacity" IsEnabled="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.CoverOverlayOpacity, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Margin="0,0,14,0"
|
||||
VerticalAlignment="Center"
|
||||
Text=" %" />
|
||||
<Slider
|
||||
Maximum="100"
|
||||
Minimum="1"
|
||||
SnapsTo="Ticks"
|
||||
StepFrequency="1"
|
||||
TickFrequency="1"
|
||||
TickPlacement="Outside"
|
||||
Value="{x:Bind ViewModel.SettingsService.CoverOverlayOpacity, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="SettingsPageCoverOverlayBlurAmount" IsEnabled="{x:Bind ViewModel.SettingsService.IsCoverOverlayEnabled, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
|
||||
<TextBlock
|
||||
Margin="0,0,14,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind ViewModel.SettingsService.CoverOverlayBlurAmount, Mode=OneWay}" />
|
||||
<Slider
|
||||
Maximum="200"
|
||||
Minimum="50"
|
||||
SnapsTo="Ticks"
|
||||
StepFrequency="10"
|
||||
TickFrequency="10"
|
||||
TickPlacement="Outside"
|
||||
Value="{x:Bind ViewModel.SettingsService.CoverOverlayBlurAmount, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<TextBlock x:Uid="SettingsPageLyricsStyle" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="SettingsPageLyricsAlignment" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox SelectedIndex="{x:Bind ViewModel.SettingsService.LyricsAlignmentType, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="SettingsPageLyricsLeft" />
|
||||
<ComboBoxItem x:Uid="SettingsPageLyricsCenter" />
|
||||
<ComboBoxItem x:Uid="SettingsPageLyricsRight" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="SettingsPageLyricsFontSize" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
|
||||
<TextBlock
|
||||
Margin="0,0,14,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind ViewModel.SettingsService.LyricsFontSize, Mode=OneWay}" />
|
||||
<Slider
|
||||
Maximum="48"
|
||||
Minimum="12"
|
||||
SnapsTo="Ticks"
|
||||
StepFrequency="2"
|
||||
TickFrequency="2"
|
||||
TickPlacement="Outside"
|
||||
Value="{x:Bind ViewModel.SettingsService.LyricsFontSize, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="SettingsPageLyricsLineSpacingFactor" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.LyricsLineSpacingFactor, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
x:Uid="SettingsPageLyricsLineSpacingFactorUnit"
|
||||
Margin="0,0,14,0"
|
||||
VerticalAlignment="Center" />
|
||||
<Slider
|
||||
Maximum="2"
|
||||
Minimum="0"
|
||||
SnapsTo="Ticks"
|
||||
StepFrequency="0.1"
|
||||
TickFrequency="0.1"
|
||||
TickPlacement="Outside"
|
||||
Value="{x:Bind ViewModel.SettingsService.LyricsLineSpacingFactor, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<TextBlock x:Uid="SettingsPageLyricsEffect" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="SettingsPageLyricsVerticalEdgeOpacity" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{x:Bind ViewModel.SettingsService.LyricsVerticalEdgeOpacity, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Margin="0,0,14,0"
|
||||
VerticalAlignment="Center"
|
||||
Text=" %" />
|
||||
<Slider
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
SnapsTo="Ticks"
|
||||
StepFrequency="1"
|
||||
TickFrequency="1"
|
||||
TickPlacement="Outside"
|
||||
Value="{x:Bind ViewModel.SettingsService.LyricsVerticalEdgeOpacity, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="SettingsPageLyricsBlurAmount" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:SettingsCard.Description>
|
||||
<StackPanel>
|
||||
<TextBlock x:Uid="SettingsPageLyricsBlurHighGPUUsage" Foreground="{ThemeResource SystemFillColorCautionBrush}" />
|
||||
<TextBlock x:Uid="SettingsPageLyricsBlurAmountSideEffect" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard.Description>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock x:Uid="SettingsPageSliderPrefix" VerticalAlignment="Center" />
|
||||
<TextBlock
|
||||
Margin="0,0,14,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind ViewModel.SettingsService.LyricsBlurAmount, Mode=OneWay}" />
|
||||
<Slider
|
||||
Maximum="10"
|
||||
Minimum="0"
|
||||
SnapsTo="Ticks"
|
||||
StepFrequency="1"
|
||||
TickFrequency="1"
|
||||
TickPlacement="Outside"
|
||||
Value="{x:Bind ViewModel.SettingsService.LyricsBlurAmount, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsExpander
|
||||
x:Uid="SettingsPageLyricsGlowEffect"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="True">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsLyricsGlowEffectEnabled, Mode=TwoWay}" />
|
||||
<controls:SettingsExpander.Items>
|
||||
|
||||
<controls:SettingsCard x:Uid="SettingsPageLyricsDynamicGlowEffect" IsEnabled="{x:Bind ViewModel.SettingsService.IsLyricsGlowEffectEnabled, Mode=OneWay}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.SettingsService.IsLyricsDynamicGlowEffectEnabled, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<TextBlock x:Uid="SettingsPageAbout" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard Header="BetterLyrics" HeaderIcon="{ui:BitmapIcon Source=ms-appx:///Assets/Icon.png}">
|
||||
<controls:SettingsCard.Description>
|
||||
<RichTextBlock>
|
||||
<Paragraph>
|
||||
<Run x:Uid="SettingsPageVersion" />
|
||||
<Run Text="{x:Bind ViewModel.Version, Mode=OneWay}" />
|
||||
</Paragraph>
|
||||
</RichTextBlock>
|
||||
</controls:SettingsCard.Description>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard
|
||||
x:Uid="SettingsPageGitHub"
|
||||
ActionIcon="{ui:FontIcon Glyph=}"
|
||||
Command="{x:Bind ViewModel.LaunchProjectGitHubPageCommand}"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsClickEnabled="True" />
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</controls:OpacityMaskView>
|
||||
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using ABI.System;
|
||||
using Windows.System;
|
||||
using BetterLyrics.WinUI3.ViewModels;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
using System.Diagnostics;
|
||||
using WinRT.Interop;
|
||||
using Windows.Storage.Pickers;
|
||||
using BetterLyrics.WinUI3.Models;
|
||||
using Microsoft.Windows.ApplicationModel.Resources;
|
||||
using BetterLyrics.WinUI3.Services.Settings;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace BetterLyrics.WinUI3.Views {
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class SettingsPage : Page {
|
||||
|
||||
public SettingsViewModel ViewModel => (SettingsViewModel)DataContext;
|
||||
|
||||
public SettingsPage() {
|
||||
this.InitializeComponent();
|
||||
DataContext = Ioc.Default.GetService<SettingsViewModel>();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
19
BetterLyrics.WinUI3/BetterLyrics.WinUI3/app.manifest
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="BetterLyrics.WinUI3.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
|
||||
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
|
||||
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
57
BetterLyrics.sln
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.13.36105.23 d17.13
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "BetterLyrics.WinUI3 (Package)", "BetterLyrics.WinUI3\BetterLyrics.WinUI3 (Package)\BetterLyrics.WinUI3 (Package).wapproj", "{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.WinUI3", "BetterLyrics.WinUI3\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj", "{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|ARM64 = Release|ARM64
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|ARM64.Deploy.0 = Debug|ARM64
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x64.Build.0 = Debug|x64
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x64.Deploy.0 = Debug|x64
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x86.Build.0 = Debug|x86
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Debug|x86.Deploy.0 = Debug|x86
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|ARM64.Deploy.0 = Release|ARM64
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x64.ActiveCfg = Release|x64
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x64.Build.0 = Release|x64
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x64.Deploy.0 = Release|x64
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x86.ActiveCfg = Release|x86
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x86.Build.0 = Release|x86
|
||||
{6576CD19-EF92-4099-B37D-E2D8EBDB6BF5}.Release|x86.Deploy.0 = Release|x86
|
||||
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|x64.Build.0 = Debug|x64
|
||||
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|x86.ActiveCfg = Debug|x86
|
||||
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Debug|x86.Build.0 = Debug|x86
|
||||
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x64.ActiveCfg = Release|x64
|
||||
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x64.Build.0 = Release|x64
|
||||
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x86.ActiveCfg = Release|x86
|
||||
{6D26909A-9EE5-4D26-9E81-686BDE36A9D3}.Release|x86.Build.0 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {0FD73F15-ED69-4DC2-9FD3-A4F03AF172A4}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
21
LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) [year] [fullname]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
22
PrivacyPolicy.md
Normal file
@@ -0,0 +1,22 @@
|
||||
Privacy Policy – BetterLyrics
|
||||
|
||||
Effective Date: June 3, 2025
|
||||
|
||||
Thank you for using BetterLyrics.
|
||||
|
||||
BetterLyrics is a local lyrics viewer application built with WinUI 3. We respect your privacy and are committed to protecting it. This Privacy Policy explains how we handle your data.
|
||||
|
||||
1. No Personal Data Collection
|
||||
BetterLyrics does not collect, store, or transmit any personal data. All lyrics, preferences, and settings are stored locally on your device. We do not access or send your information to any server.
|
||||
|
||||
2. Internet Usage
|
||||
BetterLyrics does not require an internet connection for its core functionality. The app does not make any network requests or communicate with external services unless you manually choose to view external resources (such as a linked demo video).
|
||||
|
||||
3. Third-Party Libraries
|
||||
BetterLyrics uses third-party open-source libraries (e.g., Win2D, CommunityToolkit, DevWinUI). These libraries are used locally and do not perform any background tracking or data transmission.
|
||||
|
||||
4. Changes to This Policy
|
||||
If our privacy practices change in the future (for example, if online lyrics fetching is added), we will update this policy accordingly and notify users through app updates or store listing.
|
||||
|
||||
5. Contact
|
||||
If you have any questions about this privacy policy, please contact us at: founchoo@outlook.com
|
||||
101
README.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# BetterLyrics
|
||||
|
||||
Local song lyrics presentation app built with WinUI3
|
||||
|
||||
## Highlighted features
|
||||
- Dynamic blur album art as background
|
||||
- Smooth lyrics fade in/out, zoom in/out effects
|
||||
- Smooth user interface change from song to song
|
||||
- Gradient Karaoke effect on every single character
|
||||
|
||||
> Note: Lyrics related effects and functions are built with [CanvasAnimatedControl](https://learn.microsoft.com/en-us/windows/apps/develop/win2d/quick-start#animate-your-app-with-canvasanimatedcontrol) instead of list of `TextBlock`, which ensures a smooth and accurate animation and more customized style.
|
||||
|
||||
Coding in progress...
|
||||
|
||||
## Customize in your way
|
||||
|
||||
We provide more than one setting item to better align with your preference
|
||||
|
||||
- Theme
|
||||
- Follow system
|
||||
- Light
|
||||
- Dark
|
||||
|
||||
- Backdrop
|
||||
- None
|
||||
- Mica
|
||||
- Mica alt
|
||||
- Acrylic desktop
|
||||
- Acrylic thin
|
||||
- Acrylic base
|
||||
- Transparent
|
||||
|
||||
- Album art as background
|
||||
- Dynamic
|
||||
- Opacity
|
||||
- Blur amount
|
||||
|
||||
- Lyrics
|
||||
- Alignment
|
||||
- Font size
|
||||
- Line spacing
|
||||
- Opacity on the edge
|
||||
- Blur amount
|
||||
- Glow effect
|
||||
|
||||
- Language
|
||||
- English
|
||||
- Simplified Chinese
|
||||
- Traditional Chinese
|
||||
|
||||
## Inspired by
|
||||
- [BetterNCM](https://github.com/std-microblock/BetterNCM)
|
||||
- [Lyricify-App](https://github.com/WXRIW/Lyricify-App)
|
||||
- [椒盐音乐 Salt Player](https://moriafly.com/program/salt-player)
|
||||
|
||||
## Demonstration video
|
||||
|
||||
See our latest introduction video「BetterLyrics 阶段性开发成果展示」on [Bilibili](https://b23.tv/QjKkYmL)
|
||||
|
||||
## Screenshots (outdated)
|
||||
|
||||
### Settings
|
||||
|
||||

|
||||
|
||||
### Light music mode
|
||||
|
||||
Will be activated automatically when lyrics are not detected/found
|
||||
|
||||

|
||||
|
||||
### General music mode
|
||||

|
||||
|
||||
### Real-time gif
|
||||

|
||||
|
||||
## Many thanks to
|
||||
- [Audio Tools Library (ATL) for .NET](https://github.com/Zeugma440/atldotnet)
|
||||
- [DevWinUI](https://github.com/ghost1372/DevWinUI)
|
||||
- [Stackoverflow - How to animate Margin property in WPF](https://stackoverflow.com/a/21542882/11048731)
|
||||
- [TagLib#](https://github.com/mono/taglib-sharp)
|
||||
- [Bilibili -【WinUI3】SystemBackdropController:定义云母、亚克力效果](https://www.bilibili.com/video/BV1PY4FevEkS)
|
||||
- [cnblogs - .NET App 与Windows系统媒体控制(SMTC)交互](https://www.cnblogs.com/TwilightLemon/p/18279496)
|
||||
- [Win2D 中的游戏循环:CanvasAnimatedControl](https://www.cnblogs.com/walterlv/p/10236395.html)
|
||||
- [r2d2rigo/Win2D-Samples](https://github.com/r2d2rigo/Win2D-Samples/blob/master/IrisBlurWin2D/IrisBlurWin2D/MainPage.xaml.cs)
|
||||
|
||||
## Third-party libraries that this app uses
|
||||
- CommunityToolkit.Labs.WinUI.MarqueeText
|
||||
- CommunityToolkit.Labs.WinUI.OpacityMaskView
|
||||
- CommunityToolkit.Mvvm
|
||||
- CommunityToolkit.WinUI.Controls.Primitives
|
||||
- CommunityToolkit.WinUI.Extensions
|
||||
- DevWinUI
|
||||
- DevWinUI.Controls
|
||||
- Microsoft.Extensions.DependencyInjection
|
||||
- Microsoft.Graphics.Win2D
|
||||
- Microsoft.Windows.SDK.BuildTools
|
||||
- Microsoft.WindowsAppSDK
|
||||
- Microsoft.Xaml.Behaviors.WinUI.Managed
|
||||
- z440.atl.core
|
||||