diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1ff0c42
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9491a2f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,363 @@
+## 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/master/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/
+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
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.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_*
+.*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 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
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# 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
\ No newline at end of file
diff --git a/AkiraVoid.WordBook.sln b/AkiraVoid.WordBook.sln
new file mode 100644
index 0000000..bf47cf5
--- /dev/null
+++ b/AkiraVoid.WordBook.sln
@@ -0,0 +1,59 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.4.33403.182
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AkiraVoid.WordBook", "AkiraVoid.WordBook\AkiraVoid.WordBook.csproj", "{294907F2-3A74-4D03-B84D-8AE36CAAA311}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "解决方案项", "解决方案项", "{5884847E-7EC4-4A24-9A56-2F6190D2B21A}"
+ ProjectSection(SolutionItems) = preProject
+ .gitattributes = .gitattributes
+ .gitignore = .gitignore
+ AkiraVoid.WordBook.sln.DotSettings = AkiraVoid.WordBook.sln.DotSettings
+ CHANGELOG = CHANGELOG
+ LICENSE = LICENSE
+ README.md = README.md
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|ARM64 = Debug|ARM64
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|ARM64 = Release|ARM64
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Debug|Any CPU.Build.0 = Debug|x64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Debug|ARM64.Build.0 = Debug|ARM64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Debug|ARM64.Deploy.0 = Debug|ARM64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Debug|x64.ActiveCfg = Debug|x64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Debug|x64.Build.0 = Debug|x64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Debug|x64.Deploy.0 = Debug|x64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Debug|x86.ActiveCfg = Debug|x86
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Debug|x86.Build.0 = Debug|x86
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Debug|x86.Deploy.0 = Debug|x86
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Release|Any CPU.ActiveCfg = Release|x64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Release|Any CPU.Build.0 = Release|x64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Release|ARM64.ActiveCfg = Release|ARM64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Release|ARM64.Build.0 = Release|ARM64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Release|ARM64.Deploy.0 = Release|ARM64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Release|x64.ActiveCfg = Release|x64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Release|x64.Build.0 = Release|x64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Release|x64.Deploy.0 = Release|x64
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Release|x86.ActiveCfg = Release|x86
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Release|x86.Build.0 = Release|x86
+ {294907F2-3A74-4D03-B84D-8AE36CAAA311}.Release|x86.Deploy.0 = Release|x86
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {E6869E46-AA64-49E8-9CC6-5A3B8F23CAA0}
+ EndGlobalSection
+EndGlobal
diff --git a/AkiraVoid.WordBook.sln.DotSettings b/AkiraVoid.WordBook.sln.DotSettings
new file mode 100644
index 0000000..4473c42
--- /dev/null
+++ b/AkiraVoid.WordBook.sln.DotSettings
@@ -0,0 +1,21 @@
+
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
\ No newline at end of file
diff --git a/AkiraVoid.WordBook/.editorconfig b/AkiraVoid.WordBook/.editorconfig
new file mode 100644
index 0000000..e69de29
diff --git a/AkiraVoid.WordBook/AkiraVoid.WordBook.csproj b/AkiraVoid.WordBook/AkiraVoid.WordBook.csproj
new file mode 100644
index 0000000..ea813d2
--- /dev/null
+++ b/AkiraVoid.WordBook/AkiraVoid.WordBook.csproj
@@ -0,0 +1,119 @@
+
+
+ WinExe
+ net6.0-windows10.0.19041.0
+ 10.0.17763.0
+ AkiraVoid.WordBook
+ app.manifest
+ x86;x64;ARM64
+ win10-x86;win10-x64;win10-arm64
+ win10-$(Platform).pubxml
+ true
+ true
+ None
+ AkiraVoid WordBook
+ AkiraVoid
+ © 2023-2024 AkiraVoid.
+ https://word-book.akiravoid.com
+ WordBook
+ AkiraVoid.WordBook
+ AkiraVoid.WordBook
+ Assets\favicon.ico
+ square-light.png
+ 2b4bdbd5-3249-498e-a133-1b4528a7fc7b
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+
+
+
+ True
+ \
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+
diff --git a/AkiraVoid.WordBook/App.xaml b/AkiraVoid.WordBook/App.xaml
new file mode 100644
index 0000000..76a6835
--- /dev/null
+++ b/AkiraVoid.WordBook/App.xaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AkiraVoid WordBook
+ 0,48,0,0
+ Transparent
+ Transparent
+ 12
+
+ 12
+
+
+
+
+
\ No newline at end of file
diff --git a/AkiraVoid.WordBook/App.xaml.cs b/AkiraVoid.WordBook/App.xaml.cs
new file mode 100644
index 0000000..19ab5ee
--- /dev/null
+++ b/AkiraVoid.WordBook/App.xaml.cs
@@ -0,0 +1,68 @@
+// Copyright (c) Microsoft Corporation and Contributors.
+// Licensed under the MIT License.
+
+using System.Linq;
+using AkiraVoid.WordBook.Pages;
+using AkiraVoid.WordBook.Utilities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.UI.Xaml;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace AkiraVoid.WordBook
+{
+ ///
+ /// Provides application-specific behavior to supplement the default Application class.
+ ///
+ public partial class App : Application
+ {
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ this.InitializeComponent();
+
+ // 初始化数据库。
+ Global.WordBank.Database.Migrate();
+
+ // 从数据库中获取单词和词性并存储在内存中。
+ Global.WordList = new(
+ Global.WordBank.Words.OrderBy(w => w.AddedAt)
+ .Include(w => w.Explanations)
+ .ThenInclude(e => e.PartOfSpeech)
+ .ToList());
+ Global.PartsOfSpeech = Global.WordBank.PartsOfSpeech.OrderBy(p => p.DisplayName).ToList();
+ }
+
+ ///
+ /// Invoked when the application is launched.
+ ///
+ /// Details about the launch request and process.
+ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
+ {
+ base.OnLaunched(args);
+
+ // 创建主窗口。
+ Global.UIHelper ??= new();
+ Global.UIHelper.CreateWindow(
+ "MainWindow",
+ (window) =>
+ {
+ var resources = Resources;
+
+ // 初始化窗口。
+ window.Title = resources["AppTitleText"] as string ?? "AkiraVoid WordBook";
+ window.Content = new RootPage();
+ window.ExtendsContentIntoTitleBar = true;
+ });
+
+ // 激活主窗口并设置云母主题。
+ var window = Global.UIHelper.GetWindow("MainWindow");
+ window.Activate();
+ Global.UIHelper.TrySetMicaBackdrop(window);
+ }
+ }
+}
\ No newline at end of file
diff --git a/AkiraVoid.WordBook/Assets/Logo/large-dark-2x.png b/AkiraVoid.WordBook/Assets/Logo/large-dark-2x.png
new file mode 100644
index 0000000..879b404
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/large-dark-2x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/large-dark-3x.png b/AkiraVoid.WordBook/Assets/Logo/large-dark-3x.png
new file mode 100644
index 0000000..08680fc
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/large-dark-3x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/large-dark-4x.png b/AkiraVoid.WordBook/Assets/Logo/large-dark-4x.png
new file mode 100644
index 0000000..6798582
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/large-dark-4x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/large-dark.png b/AkiraVoid.WordBook/Assets/Logo/large-dark.png
new file mode 100644
index 0000000..fc3951a
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/large-dark.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/large-dark.svg b/AkiraVoid.WordBook/Assets/Logo/large-dark.svg
new file mode 100644
index 0000000..b5a928a
--- /dev/null
+++ b/AkiraVoid.WordBook/Assets/Logo/large-dark.svg
@@ -0,0 +1,4 @@
+
diff --git a/AkiraVoid.WordBook/Assets/Logo/large-light-2x.png b/AkiraVoid.WordBook/Assets/Logo/large-light-2x.png
new file mode 100644
index 0000000..0cf1054
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/large-light-2x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/large-light-3x.png b/AkiraVoid.WordBook/Assets/Logo/large-light-3x.png
new file mode 100644
index 0000000..86cb4a3
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/large-light-3x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/large-light-4x.png b/AkiraVoid.WordBook/Assets/Logo/large-light-4x.png
new file mode 100644
index 0000000..c13e24a
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/large-light-4x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/large-light.png b/AkiraVoid.WordBook/Assets/Logo/large-light.png
new file mode 100644
index 0000000..553dc96
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/large-light.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/large-light.svg b/AkiraVoid.WordBook/Assets/Logo/large-light.svg
new file mode 100644
index 0000000..de6f81c
--- /dev/null
+++ b/AkiraVoid.WordBook/Assets/Logo/large-light.svg
@@ -0,0 +1,4 @@
+
diff --git a/AkiraVoid.WordBook/Assets/Logo/medium-dark-2x.png b/AkiraVoid.WordBook/Assets/Logo/medium-dark-2x.png
new file mode 100644
index 0000000..eed92d2
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/medium-dark-2x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/medium-dark-3x.png b/AkiraVoid.WordBook/Assets/Logo/medium-dark-3x.png
new file mode 100644
index 0000000..e3b251d
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/medium-dark-3x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/medium-dark-4x.png b/AkiraVoid.WordBook/Assets/Logo/medium-dark-4x.png
new file mode 100644
index 0000000..e2fe0be
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/medium-dark-4x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/medium-dark.png b/AkiraVoid.WordBook/Assets/Logo/medium-dark.png
new file mode 100644
index 0000000..f07dd5b
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/medium-dark.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/medium-dark.svg b/AkiraVoid.WordBook/Assets/Logo/medium-dark.svg
new file mode 100644
index 0000000..dd5d367
--- /dev/null
+++ b/AkiraVoid.WordBook/Assets/Logo/medium-dark.svg
@@ -0,0 +1,3 @@
+
diff --git a/AkiraVoid.WordBook/Assets/Logo/medium-light-2x.png b/AkiraVoid.WordBook/Assets/Logo/medium-light-2x.png
new file mode 100644
index 0000000..4bbf1c6
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/medium-light-2x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/medium-light-3x.png b/AkiraVoid.WordBook/Assets/Logo/medium-light-3x.png
new file mode 100644
index 0000000..40ba3a0
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/medium-light-3x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/medium-light-4x.png b/AkiraVoid.WordBook/Assets/Logo/medium-light-4x.png
new file mode 100644
index 0000000..1ab4dff
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/medium-light-4x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/medium-light.png b/AkiraVoid.WordBook/Assets/Logo/medium-light.png
new file mode 100644
index 0000000..b907e5d
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/medium-light.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/medium-light.svg b/AkiraVoid.WordBook/Assets/Logo/medium-light.svg
new file mode 100644
index 0000000..febce43
--- /dev/null
+++ b/AkiraVoid.WordBook/Assets/Logo/medium-light.svg
@@ -0,0 +1,3 @@
+
diff --git a/AkiraVoid.WordBook/Assets/Logo/mini-dark-2x.png b/AkiraVoid.WordBook/Assets/Logo/mini-dark-2x.png
new file mode 100644
index 0000000..3329ffc
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/mini-dark-2x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/mini-dark-3x.png b/AkiraVoid.WordBook/Assets/Logo/mini-dark-3x.png
new file mode 100644
index 0000000..79fb671
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/mini-dark-3x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/mini-dark-4x.png b/AkiraVoid.WordBook/Assets/Logo/mini-dark-4x.png
new file mode 100644
index 0000000..04ede7e
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/mini-dark-4x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/mini-dark.png b/AkiraVoid.WordBook/Assets/Logo/mini-dark.png
new file mode 100644
index 0000000..05646bd
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/mini-dark.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/mini-dark.svg b/AkiraVoid.WordBook/Assets/Logo/mini-dark.svg
new file mode 100644
index 0000000..ffb9345
--- /dev/null
+++ b/AkiraVoid.WordBook/Assets/Logo/mini-dark.svg
@@ -0,0 +1,3 @@
+
diff --git a/AkiraVoid.WordBook/Assets/Logo/mini-light-2x.png b/AkiraVoid.WordBook/Assets/Logo/mini-light-2x.png
new file mode 100644
index 0000000..b31fbee
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/mini-light-2x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/mini-light-3x.png b/AkiraVoid.WordBook/Assets/Logo/mini-light-3x.png
new file mode 100644
index 0000000..418a997
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/mini-light-3x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/mini-light-4x.png b/AkiraVoid.WordBook/Assets/Logo/mini-light-4x.png
new file mode 100644
index 0000000..80bc2f1
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/mini-light-4x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/mini-light.png b/AkiraVoid.WordBook/Assets/Logo/mini-light.png
new file mode 100644
index 0000000..8049a11
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/mini-light.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/mini-light.svg b/AkiraVoid.WordBook/Assets/Logo/mini-light.svg
new file mode 100644
index 0000000..aa7b4ea
--- /dev/null
+++ b/AkiraVoid.WordBook/Assets/Logo/mini-light.svg
@@ -0,0 +1,3 @@
+
diff --git a/AkiraVoid.WordBook/Assets/Logo/square-dark-2x.png b/AkiraVoid.WordBook/Assets/Logo/square-dark-2x.png
new file mode 100644
index 0000000..7980408
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/square-dark-2x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/square-dark-3x.png b/AkiraVoid.WordBook/Assets/Logo/square-dark-3x.png
new file mode 100644
index 0000000..1c9e1cf
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/square-dark-3x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/square-dark-4x.png b/AkiraVoid.WordBook/Assets/Logo/square-dark-4x.png
new file mode 100644
index 0000000..900fe28
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/square-dark-4x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/square-dark.png b/AkiraVoid.WordBook/Assets/Logo/square-dark.png
new file mode 100644
index 0000000..597e908
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/square-dark.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/square-dark.svg b/AkiraVoid.WordBook/Assets/Logo/square-dark.svg
new file mode 100644
index 0000000..1a2b3d6
--- /dev/null
+++ b/AkiraVoid.WordBook/Assets/Logo/square-dark.svg
@@ -0,0 +1,3 @@
+
diff --git a/AkiraVoid.WordBook/Assets/Logo/square-light-2x.png b/AkiraVoid.WordBook/Assets/Logo/square-light-2x.png
new file mode 100644
index 0000000..7520a93
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/square-light-2x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/square-light-3x.png b/AkiraVoid.WordBook/Assets/Logo/square-light-3x.png
new file mode 100644
index 0000000..252a25b
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/square-light-3x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/square-light-4x.png b/AkiraVoid.WordBook/Assets/Logo/square-light-4x.png
new file mode 100644
index 0000000..51a76aa
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/square-light-4x.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/square-light.png b/AkiraVoid.WordBook/Assets/Logo/square-light.png
new file mode 100644
index 0000000..38d1dd6
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/Logo/square-light.png differ
diff --git a/AkiraVoid.WordBook/Assets/Logo/square-light.svg b/AkiraVoid.WordBook/Assets/Logo/square-light.svg
new file mode 100644
index 0000000..c3bf9a6
--- /dev/null
+++ b/AkiraVoid.WordBook/Assets/Logo/square-light.svg
@@ -0,0 +1,3 @@
+
diff --git a/AkiraVoid.WordBook/Assets/favicon.ico b/AkiraVoid.WordBook/Assets/favicon.ico
new file mode 100644
index 0000000..3744d17
Binary files /dev/null and b/AkiraVoid.WordBook/Assets/favicon.ico differ
diff --git a/AkiraVoid.WordBook/Controls/ExplanationEditor.xaml b/AkiraVoid.WordBook/Controls/ExplanationEditor.xaml
new file mode 100644
index 0000000..857aae2
--- /dev/null
+++ b/AkiraVoid.WordBook/Controls/ExplanationEditor.xaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+ Auto
+ *
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AkiraVoid.WordBook/Controls/ExplanationEditor.xaml.cs b/AkiraVoid.WordBook/Controls/ExplanationEditor.xaml.cs
new file mode 100644
index 0000000..e36dc21
--- /dev/null
+++ b/AkiraVoid.WordBook/Controls/ExplanationEditor.xaml.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Microsoft Corporation and Contributors.
+// Licensed under the MIT License.
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using System;
+using AkiraVoid.WordBook.Models;
+using AkiraVoid.WordBook.ViewModels;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace AkiraVoid.WordBook.Controls
+{
+ public sealed partial class ExplanationEditor : UserControl
+ {
+ public event EventHandler ActionButtonClick;
+ public event EventHandler AddButtonClick;
+ public event EventHandler RemoveButtonClick;
+ public event EventHandler Edit;
+
+ public ExplanationEditor()
+ {
+ this.InitializeComponent();
+ }
+
+ public WordExplanation Explanation
+ {
+ get => (WordExplanation)GetValue(ExplanationProperty);
+ set
+ {
+ value.PropertyChanged += OnPropertyChanged;
+ OnEdit(new(Explanation) { EditedProperty = "this" });
+ SetValue(ExplanationProperty, value);
+ }
+ }
+
+ private void OnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) =>
+ OnEdit(new(Explanation) { EditedProperty = e.PropertyName });
+
+
+ public static readonly DependencyProperty ExplanationProperty = DependencyProperty.Register(
+ nameof(Explanation),
+ typeof(WordExplanation),
+ typeof(ExplanationEditor),
+ new(null));
+
+
+ private void OnAddButtonClicked(object sender, RoutedEventArgs args)
+ {
+ ActionButtonClick?.Invoke(this, new(args) { ActionButton = AddButton, Explanation = Explanation });
+ AddButtonClick?.Invoke(this, new(args) { ActionButton = AddButton, Explanation = Explanation });
+ }
+
+ private void OnRemoveButtonClicked(object sender, RoutedEventArgs args)
+ {
+ ActionButtonClick?.Invoke(this, new(args) { ActionButton = RemoveButton, Explanation = Explanation });
+ RemoveButtonClick?.Invoke(this, new(args) { ActionButton = AddButton, Explanation = Explanation });
+ }
+
+ private void OnEdit(ExplanationEditorEditedEventArgs args)
+ {
+ Edit?.Invoke(this, args);
+ }
+ }
+}
\ No newline at end of file
diff --git a/AkiraVoid.WordBook/Controls/SettingGroup.cs b/AkiraVoid.WordBook/Controls/SettingGroup.cs
new file mode 100644
index 0000000..5aabb64
--- /dev/null
+++ b/AkiraVoid.WordBook/Controls/SettingGroup.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation and Contributors.
+// Licensed under the MIT License.
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Markup;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace AkiraVoid.WordBook.Controls
+{
+ [ContentProperty(Name = "Content")]
+ public sealed class SettingGroup : Control
+ {
+ public SettingGroup()
+ {
+ this.DefaultStyleKey = typeof(SettingGroup);
+ }
+
+ public object Content
+ {
+ get => GetValue(ContentProperty);
+ set => SetValue(ContentProperty, value);
+ }
+
+ public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(
+ nameof(Content),
+ typeof(object),
+ typeof(SettingGroup),
+ new(null));
+
+ public string Header
+ {
+ get => (string)GetValue(HeaderProperty);
+ set => SetValue(HeaderProperty, value);
+ }
+
+ public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
+ nameof(Header),
+ typeof(string),
+ typeof(SettingGroup),
+ new(string.Empty));
+ }
+}
\ No newline at end of file
diff --git a/AkiraVoid.WordBook/Controls/SpellChecker.xaml b/AkiraVoid.WordBook/Controls/SpellChecker.xaml
new file mode 100644
index 0000000..01669c1
--- /dev/null
+++ b/AkiraVoid.WordBook/Controls/SpellChecker.xaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AkiraVoid.WordBook/Controls/SpellChecker.xaml.cs b/AkiraVoid.WordBook/Controls/SpellChecker.xaml.cs
new file mode 100644
index 0000000..497fbd9
--- /dev/null
+++ b/AkiraVoid.WordBook/Controls/SpellChecker.xaml.cs
@@ -0,0 +1,175 @@
+// Copyright (c) Microsoft Corporation and Contributors.
+// Licensed under the MIT License.
+
+using AkiraVoid.WordBook.Enums;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using System;
+using AkiraVoid.WordBook.ViewModels;
+using AkiraVoid.WordBook.Models;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace AkiraVoid.WordBook.Controls
+{
+ public sealed partial class SpellChecker : UserControl
+ {
+ public SpellChecker()
+ {
+ this.InitializeComponent();
+ StateChanged += OnStateChanged;
+ Loaded += (_, _) =>
+ {
+ SetDescription();
+ VisualStateManager.GoToState(this, State.ToString() + "State", false);
+ };
+ }
+
+ public InputValidationState State
+ {
+ get => (InputValidationState)GetValue(StateProperty);
+ set => SetValue(StateProperty, value);
+ }
+
+ public static readonly DependencyProperty StateProperty = DependencyProperty.Register(
+ nameof(State),
+ typeof(InputValidationState),
+ typeof(SpellChecker),
+ new(InputValidationState.Unvalidated, StateChangedCallback));
+
+ public object ErrorMessage
+ {
+ get => GetValue(ErrorMessageProperty);
+ set => SetValue(ErrorMessageProperty, value);
+ }
+
+ public static readonly DependencyProperty ErrorMessageProperty = DependencyProperty.Register(
+ nameof(ErrorMessage),
+ typeof(object),
+ typeof(SpellChecker),
+ new(null));
+
+ public object PassedMessage
+ {
+ get => GetValue(PassedMessageProperty);
+ set => SetValue(PassedMessageProperty, value);
+ }
+
+ public static readonly DependencyProperty PassedMessageProperty = DependencyProperty.Register(
+ nameof(PassedMessage),
+ typeof(object),
+ typeof(SpellChecker),
+ new(null));
+
+ public object UnvalidatedMessage
+ {
+ get => GetValue(UnvalidatedMessageProperty);
+ set => SetValue(UnvalidatedMessageProperty, value);
+ }
+
+ public static readonly DependencyProperty UnvalidatedMessageProperty = DependencyProperty.Register(
+ nameof(UnvalidatedMessage),
+ typeof(object),
+ typeof(SpellChecker),
+ new(null));
+
+ public Word Word
+ {
+ get => (Word)GetValue(WordProperty);
+ set => SetValue(WordProperty, value);
+ }
+
+ public static readonly DependencyProperty WordProperty = DependencyProperty.Register(
+ nameof(Word),
+ typeof(Word),
+ typeof(SpellChecker),
+ new(null));
+
+ public event EventHandler Validate;
+ public event EventHandler Validated;
+ public event EventHandler Passed;
+ public event EventHandler Error;
+ public event EventHandler StateChanged;
+
+ private object _description;
+
+ private object Description
+ {
+ get => _description;
+ set
+ {
+ _description = value;
+ DescriptionPresenter.Content = Description;
+ }
+ }
+
+ public Func GetContent { get; set; }
+
+ private void OnValidate()
+ {
+ Validate?.Invoke(this, new() { State = State });
+ }
+
+ private void OnValidated()
+ {
+ Validated?.Invoke(this, new() { State = State });
+ }
+
+ private void OnPassed()
+ {
+ Passed?.Invoke(this, new() { State = State });
+ }
+
+ private void OnError()
+ {
+ Error?.Invoke(this, new() { State = State });
+ }
+
+ private void OnStateChanged(object sender, ValidationEventArgs args)
+ {
+ SetDescription();
+ VisualStateManager.GoToState(this, State.ToString() + "State", false);
+ }
+
+ private void SetDescription()
+ {
+ Description = State switch
+ {
+ InputValidationState.Error => ErrorMessage,
+ InputValidationState.Passed => PassedMessage,
+ InputValidationState.Unvalidated => UnvalidatedMessage,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ private static void StateChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs args)
+ {
+ var control = (SpellChecker)d;
+ control.StateChanged?.Invoke(control, new() { State = control.State });
+ }
+
+ public void TriggerValidation()
+ {
+ OnValidate();
+ var isPassed = Spell.Text.Equals(Word?.Spell, StringComparison.InvariantCultureIgnoreCase);
+ if (isPassed)
+ {
+ State = InputValidationState.Passed;
+ OnPassed();
+ }
+ else
+ {
+ State = InputValidationState.Error;
+ OnError();
+ }
+
+ OnValidated();
+ }
+
+ public void ClearInput()
+ {
+ Spell.Text = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AkiraVoid.WordBook/Controls/TeacherPicker.xaml b/AkiraVoid.WordBook/Controls/TeacherPicker.xaml
new file mode 100644
index 0000000..189290f
--- /dev/null
+++ b/AkiraVoid.WordBook/Controls/TeacherPicker.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AkiraVoid.WordBook/Controls/TeacherPicker.xaml.cs b/AkiraVoid.WordBook/Controls/TeacherPicker.xaml.cs
new file mode 100644
index 0000000..450ed06
--- /dev/null
+++ b/AkiraVoid.WordBook/Controls/TeacherPicker.xaml.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft Corporation and Contributors.
+// Licensed under the MIT License.
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using System.Linq;
+using AkiraVoid.WordBook.Enums;
+using AkiraVoid.WordBook.Utilities;
+using AkiraVoid.WordBook.ViewModels;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace AkiraVoid.WordBook.Controls
+{
+ public sealed partial class TeacherPicker : UserControl
+ {
+ public TeacherPicker()
+ {
+ this.InitializeComponent();
+ }
+
+ private WordLanguage _teacherLanguage;
+
+ public WordLanguage TeacherLanguage
+ {
+ get => _teacherLanguage;
+ set
+ {
+ _teacherLanguage = value;
+ var teachers = Teachers.GetTeachers(value);
+ Picker.ItemsSource = teachers;
+ SelectedTeacher = teachers.FirstOrDefault(t => t.Equals(Teachers.GetConfiguredTeacher(value)));
+ }
+ }
+
+ private Teacher _selectedTeacher;
+
+ public Teacher SelectedTeacher
+ {
+ get => _selectedTeacher;
+ private set
+ {
+ _selectedTeacher = value;
+ Picker.SelectedItem = value;
+ }
+ }
+
+ public event SelectionChangedEventHandler SelectionChanged;
+
+ private void OnPickerSelectionChanged(object sender, SelectionChangedEventArgs args)
+ {
+ SelectedTeacher = (sender as ComboBox)?.SelectedItem as Teacher;
+ Teachers.SetTeacher(TeacherLanguage, SelectedTeacher);
+ OnSelectionChanged(args);
+ }
+
+ private void OnSelectionChanged(SelectionChangedEventArgs args)
+ {
+ SelectionChanged?.Invoke(this, args);
+ }
+
+ private void OnTestPlay(object sender, RoutedEventArgs e)
+ {
+#pragma warning disable CS4014 // 由于此调用不会等待,因此在调用完成前将继续执行当前方法
+ Teachers.SpeakAsync(
+ TeacherLanguage == WordLanguage.English ? "Test, test, can you hear me?" : "テスト、テスト。聞こえますか。",
+ TeacherLanguage);
+#pragma warning restore CS4014 // 由于此调用不会等待,因此在调用完成前将继续执行当前方法
+ }
+ }
+}
\ No newline at end of file
diff --git a/AkiraVoid.WordBook/Controls/UsableRadioButtons.xaml b/AkiraVoid.WordBook/Controls/UsableRadioButtons.xaml
new file mode 100644
index 0000000..4cfc973
--- /dev/null
+++ b/AkiraVoid.WordBook/Controls/UsableRadioButtons.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AkiraVoid.WordBook/Controls/UsableRadioButtons.xaml.cs b/AkiraVoid.WordBook/Controls/UsableRadioButtons.xaml.cs
new file mode 100644
index 0000000..dac81f2
--- /dev/null
+++ b/AkiraVoid.WordBook/Controls/UsableRadioButtons.xaml.cs
@@ -0,0 +1,181 @@
+// Copyright (c) Microsoft Corporation and Contributors.
+// Licensed under the MIT License.
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Data;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using AkiraVoid.WordBook.ViewModels;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace AkiraVoid.WordBook.Controls
+{
+ public sealed partial class UsableRadioButtons : UserControl
+ {
+ public UsableRadioButtons()
+ {
+ this.InitializeComponent();
+ Loaded += (_, _) =>
+ {
+ if (ItemTemplate == null)
+ {
+ ItemTemplate = DefaultTemplate;
+ }
+ };
+ }
+
+ public Orientation Orientation
+ {
+ get => (Orientation)GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
+ nameof(Orientation),
+ typeof(Orientation),
+ typeof(UsableRadioButtons),
+ new(Orientation.Vertical));
+
+ public object ItemsSource
+ {
+ get => GetValue(ItemsSourceProperty);
+ set
+ {
+ SetValue(ItemsSourceProperty, value);
+ _radioButtons.Clear();
+ if (value is IEnumerable values)
+ {
+ foreach (var o in values)
+ {
+ _radioButtons.Add(
+ new(o)
+ {
+ DisplayContent = ItemConverter is null
+ ? o.ToString()
+ : ItemConverter.Convert(
+ o,
+ o.GetType(),
+ null,
+ null)
+ .ToString()
+ });
+ }
+ }
+ else
+ {
+ _radioButtons.Add(
+ new(value)
+ {
+ DisplayContent = ItemConverter is null
+ ? value.ToString()
+ : ItemConverter.Convert(
+ value,
+ value.GetType(),
+ null,
+ null)
+ .ToString()
+ });
+ }
+ }
+ }
+
+ public IValueConverter ItemConverter
+ {
+ get => (IValueConverter)GetValue(ItemConverterProperty);
+ set => SetValue(ItemConverterProperty, value);
+ }
+
+ public static readonly DependencyProperty ItemConverterProperty = DependencyProperty.Register(
+ nameof(ItemConverter),
+ typeof(IValueConverter),
+ typeof(UsableRadioButtons),
+ new(null));
+
+ public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
+ nameof(ItemsSource),
+ typeof(object),
+ typeof(UsableRadioButtons),
+ new(null));
+
+ public object ItemTemplate
+ {
+ get => GetValue(ItemTemplateProperty);
+ set => SetValue(ItemTemplateProperty, value ?? DefaultTemplate);
+ }
+
+ public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register(
+ nameof(ItemTemplate),
+ typeof(object),
+ typeof(UsableRadioButtons),
+ new(null));
+
+ public object SelectedItem
+ {
+ get => GetValue(SelectedItemProperty);
+ set => SelectItemBySourceItem(value, true);
+ }
+
+ public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register(
+ nameof(SelectedItem),
+ typeof(object),
+ typeof(UsableRadioButtons),
+ new(null));
+
+ public object Header
+ {
+ get => GetValue(HeaderProperty);
+ set => SetValue(HeaderProperty, value);
+ }
+
+ public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
+ nameof(Header),
+ typeof(object),
+ typeof(UsableRadioButtons),
+ new(null));
+
+ public event SelectionChangedEventHandler SelectionChanged;
+
+ private readonly ObservableCollection _radioButtons = new();
+
+ private void OnSelect(object sender, RoutedEventArgs e)
+ {
+ var radioButton = (RadioButton)sender;
+ if (radioButton != null)
+ {
+ SelectItemBySourceItem(radioButton.Tag);
+ }
+ }
+
+ private void OnSelectionChanged(SelectionChangedEventArgs e)
+ {
+ SelectionChanged?.Invoke(this, e);
+ }
+
+ private void SelectItemBySourceItem(object sourceItem, bool checkItem = false)
+ {
+ var previousSelected = _radioButtons.FirstOrDefault(rb => rb.IsChecked && rb.OriginalItem != sourceItem);
+ if (previousSelected != null)
+ {
+ previousSelected.IsChecked = false;
+ }
+
+ if (checkItem)
+ {
+ var nextSelection = _radioButtons.FirstOrDefault(rb => rb.OriginalItem.Equals(sourceItem));
+ if (nextSelection != null)
+ {
+ nextSelection.IsChecked = true;
+ }
+ }
+
+ var previousItem = SelectedItem;
+ SetValue(SelectedItemProperty, sourceItem);
+ OnSelectionChanged(new(new List