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 { previousItem }, new List { SelectedItem })); + } + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Controls/WordListItem.xaml b/AkiraVoid.WordBook/Controls/WordListItem.xaml new file mode 100644 index 0000000..552138d --- /dev/null +++ b/AkiraVoid.WordBook/Controls/WordListItem.xaml @@ -0,0 +1,35 @@ + + + + + + + + * + * + + + + + + + + + + + \ No newline at end of file diff --git a/AkiraVoid.WordBook/Controls/WordListItem.xaml.cs b/AkiraVoid.WordBook/Controls/WordListItem.xaml.cs new file mode 100644 index 0000000..f0bf87c --- /dev/null +++ b/AkiraVoid.WordBook/Controls/WordListItem.xaml.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +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 WordListItem : UserControl + { + public WordListItem() + { + this.InitializeComponent(); + } + + public event EventHandler MemorizedToggle; + public event EventHandler ImportantToggle; + + public Word Word + { + get => (Word)GetValue(WordProperty); + set + { + ChangeButtonIconAndToolTip("IsImportant"); + ChangeButtonIconAndToolTip("HasMemorized"); + value.PropertyChanged += OnWordChange; + SetValue(WordProperty, value); + } + } + + public static readonly DependencyProperty WordProperty = DependencyProperty.Register( + nameof(Word), + typeof(Word), + typeof(WordListItem), + new( + null, + (property, args) => + { + var a = property.GetValue(WordProperty); + })); + + private void OnMemorizedToggled(object sender, RoutedEventArgs args) + { + var e = new WordListRoutedEventArgs(args) { WordId = Word.Id }; + MemorizedToggle?.Invoke(this, e); + } + + private void OnImportantToggled(object sender, RoutedEventArgs args) + { + var e = new WordListRoutedEventArgs(args) { WordId = Word.Id }; + ImportantToggle?.Invoke(this, e); + } + + private void OnWordChange(object sender, PropertyChangedEventArgs args) => + ChangeButtonIconAndToolTip(args.PropertyName); + + private void ChangeButtonIconAndToolTip(string changedProperty) + { + if (Word == null) return; + if (changedProperty != "IsImportant" && changedProperty != "HasMemorized") return; + var glyph = ""; + var toolTipContent = ""; + var button = ImportantToggleButton; + switch (changedProperty) + { + case "IsImportant": + glyph = Word.IsImportant ? "\xE842" : "\xE840"; + toolTipContent = Word.IsImportant ? "标记为一般词汇" : "标记为重要词汇"; + break; + case "HasMemorized": + glyph = Word.HasMemorized ? "\xE735" : "\xE734"; + toolTipContent = Word.HasMemorized ? "标记为未记住的单词" : "标记为已记住的单词"; + button = MemorizedToggleButton; + break; + } + + button.IsChecked = changedProperty == "IsImportant" ? Word.IsImportant : Word.HasMemorized; + + var fontIcon = new FontIcon + { + FontFamily = (Microsoft.UI.Xaml.Media.FontFamily)Resources["SymbolThemeFontFamily"], Glyph = glyph + }; + button.Content = fontIcon; + ToolTip toolTip = new() { Content = toolTipContent }; + ToolTipService.SetToolTip(button, toolTip); + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + ChangeButtonIconAndToolTip("IsImportant"); + ChangeButtonIconAndToolTip("HasMemorized"); + } + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Converters/DebugConverter.cs b/AkiraVoid.WordBook/Converters/DebugConverter.cs new file mode 100644 index 0000000..06fd0fb --- /dev/null +++ b/AkiraVoid.WordBook/Converters/DebugConverter.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.UI.Xaml.Data; + +namespace AkiraVoid.WordBook.Converters; + +/// +/// 该转换器只用于帮助调试 +/// +public class DebugConverter : IValueConverter +{ + /// + public object Convert( + object value, + Type targetType, + object parameter, + string language) + { + return value; + } + + /// + public object ConvertBack( + object value, + Type targetType, + object parameter, + string language) + { + return value; + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Converters/WordRangeStringConverter.cs b/AkiraVoid.WordBook/Converters/WordRangeStringConverter.cs new file mode 100644 index 0000000..c754dc3 --- /dev/null +++ b/AkiraVoid.WordBook/Converters/WordRangeStringConverter.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AkiraVoid.WordBook.Enums; +using Microsoft.UI.Xaml.Data; + +namespace AkiraVoid.WordBook.Converters; + +/// +/// 用于将 WordRange 枚举转换成对应的字符串,或反之。 +/// +public class WordRangeStringConverter : IValueConverter +{ + private readonly Dictionary _rangeMap = new() + { + { DictationWordRange.All, "所有单词" }, + { DictationWordRange.Random, "随机单词" }, + { DictationWordRange.Today, "今天登记的单词" }, + { DictationWordRange.Yesterday, "昨天登记的单词" }, + { DictationWordRange.Important, "标记为重要的单词" } + }; + + /// + public object Convert( + object value, + Type targetType, + object parameter, + string language) + { + return _rangeMap[(DictationWordRange)value]; + } + + /// + public object ConvertBack( + object value, + Type targetType, + object parameter, + string language) + { + return _rangeMap.FirstOrDefault(range => range.Value == (string)value).Key; + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Enums/DictationWordRange.cs b/AkiraVoid.WordBook/Enums/DictationWordRange.cs new file mode 100644 index 0000000..2cb0018 --- /dev/null +++ b/AkiraVoid.WordBook/Enums/DictationWordRange.cs @@ -0,0 +1,32 @@ +namespace AkiraVoid.WordBook.Enums; + +/// +/// 指示要听写的单词范围。 +/// +public enum DictationWordRange +{ + /// + /// 随机选择单词,除了标记为已记住的单词。 + /// + Random, + + /// + /// 选择所有单词,包括标记为已记住的单词。 + /// + All, + + /// + /// 选择今天登记的单词,除了标记为已记住的单词。 + /// + Today, + + /// + /// 选择昨天登记的单词,除了标记为已记住的单词。 + /// + Yesterday, + + /// + /// 选择标记为重要的单词,除了标记为已记住的单词。 + /// + Important +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Enums/InputValidationState.cs b/AkiraVoid.WordBook/Enums/InputValidationState.cs new file mode 100644 index 0000000..0bb009c --- /dev/null +++ b/AkiraVoid.WordBook/Enums/InputValidationState.cs @@ -0,0 +1,22 @@ +namespace AkiraVoid.WordBook.Enums; + +/// +/// 指示输入验证器的验证状态。 +/// +public enum InputValidationState +{ + /// + /// 输入验证检测到错误。 + /// + Error, + + /// + /// 输入验证已通过。 + /// + Passed, + + /// + /// 输入验证还未开始。 + /// + Unvalidated +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Enums/PartOfSpeechName.cs b/AkiraVoid.WordBook/Enums/PartOfSpeechName.cs new file mode 100644 index 0000000..09d4d57 --- /dev/null +++ b/AkiraVoid.WordBook/Enums/PartOfSpeechName.cs @@ -0,0 +1,30 @@ +namespace AkiraVoid.WordBook.Enums; + +/// +/// 指示单词的词性。 +/// +public enum PartOfSpeechName +{ + Noun, + Pronoun, + Adjective, + Adverb, + Verb, + Numeral, + Article, + Preposition, + Conjunction, + Interjection, + Meishi, + Daimeishi, + Suushi, + Doushi, + Keiyoushi, + Keiyoudoushi, + Fukushi, + Rentaishi, + Setsuzokushi, + Kandoushi, + Jodoushi, + Joshi, +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Enums/WordLanguage.cs b/AkiraVoid.WordBook/Enums/WordLanguage.cs new file mode 100644 index 0000000..a1fc47e --- /dev/null +++ b/AkiraVoid.WordBook/Enums/WordLanguage.cs @@ -0,0 +1,10 @@ +namespace AkiraVoid.WordBook.Enums +{ + /// + /// 指示单词的语言。 + /// + public enum WordLanguage + { + English, Japanese + } +} diff --git a/AkiraVoid.WordBook/Helpers/Navigator.cs b/AkiraVoid.WordBook/Helpers/Navigator.cs new file mode 100644 index 0000000..6c1c8f2 --- /dev/null +++ b/AkiraVoid.WordBook/Helpers/Navigator.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Linq; +using AkiraVoid.WordBook.Pages; +using AkiraVoid.WordBook.ViewModels; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Animation; + +namespace AkiraVoid.WordBook.Helpers; + +/// +/// 用于简化导航操作的类型。 +/// +public class Navigator +{ + private readonly Frame _frame; + private readonly NavigationView _navigationView; + private object _selectedItem; + + /// + /// 获取或设置 中选中的选项。 + /// + private object SelectedItem + { + get => _selectedItem; + set + { + _navigationView.SelectedItem = value; + _selectedItem = value; + } + } + + /// + /// 获取已定义的导航。 + /// + public readonly List Navigations = new() + { + new() { PageTag = "WordBook", PageTitle = "生词本", PageType = typeof(WordBookPage) }, + new() { PageTag = "Dictation", PageTitle = "听写", PageType = typeof(DictationPage) }, + new() { PageTag = "WordDetail", PageTitle = "单词详情", PageType = typeof(WordDetailPage) } + }; + + /// + /// 初始化一个 实例。 + /// + /// 定义该实例操作的 。 + /// 定义用于导航的 。 + public Navigator(Frame frame, NavigationView navigationView) + { + _frame = frame; + _navigationView = navigationView; + _navigationView.ItemInvoked += OnNavigationRequested; + SelectedItem = _navigationView.MenuItems[0]; + Navigate(Navigations.Find(nav => nav.PageTag == "WordBook")); + } + + /// + /// 手动导航至指定位置。 + /// + /// 要导航的位置。 + /// 要向目标传递的参数。 + /// 要使用的过渡动画。 + /// + public bool Navigate(Navigation navigation, object parameter = null, NavigationTransitionInfo transition = null) + { + var isNavigated = _frame.Navigate(navigation.PageType, parameter, transition); + if (!isNavigated) + return false; + _navigationView.Header = navigation.PageTitle; + navigation.CallBack?.Invoke(); + + return true; + } + + /// + /// 当 发起导航时自动触发。 + /// + /// 事件发起者。 + /// 收到的参数。 + private void OnNavigationRequested(NavigationView sender, NavigationViewItemInvokedEventArgs args) + { + var pageTag = args.IsSettingsInvoked ? null : args.InvokedItemContainer.Tag as string; + if (pageTag != null) + { + var navigation = Navigations.Find(nav => nav.PageTag == pageTag); + Navigate(navigation, null, args.RecommendedNavigationTransitionInfo); + SelectedItem = + _navigationView.MenuItems.FirstOrDefault(i => (i as NavigationViewItem)?.Tag.ToString() == pageTag); + } + else + { + SelectedItem = _selectedItem; + } + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Helpers/UIHelper.cs b/AkiraVoid.WordBook/Helpers/UIHelper.cs new file mode 100644 index 0000000..ded02a8 --- /dev/null +++ b/AkiraVoid.WordBook/Helpers/UIHelper.cs @@ -0,0 +1,222 @@ +using AkiraVoid.WordBook.Utilities; +using Microsoft.UI.Composition; +using Microsoft.UI.Composition.SystemBackdrops; +using Microsoft.UI.Xaml; +using System; +using System.Collections.Generic; + +namespace AkiraVoid.WordBook.Helpers +{ + // ReSharper disable once InconsistentNaming + /// + /// 提供用于简化部分 UI 操作的方法的类。 + /// + public class UIHelper + { + private WindowsSystemDispatcherQueueHelper _queueHelper; + private MicaController _micaController; + private DesktopAcrylicController _acrylicController; + private SystemBackdropConfiguration _backdropConfiguration; + private readonly Dictionary _windows = new(); + + /// + /// 使对应的窗口标题栏重新渲染。 + /// + /// 该方法通常在应用主题切换时自动调用,以更新右上角 + /// 窗口控制按钮的颜色。 + /// + /// 要操作的窗口。 + public static void TriggerTitleBarRepaint(Window window) + { + var windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(window); + var activeWindow = Win32.GetActiveWindow(); + if (windowHandle == activeWindow) + { + Win32.SendMessage( + windowHandle, + Win32.WM_ACTIVATE, + Win32.WA_INACTIVE, + IntPtr.Zero); + Win32.SendMessage( + windowHandle, + Win32.WM_ACTIVATE, + Win32.WA_ACTIVE, + IntPtr.Zero); + } + else + { + Win32.SendMessage( + windowHandle, + Win32.WM_ACTIVATE, + Win32.WA_ACTIVE, + IntPtr.Zero); + Win32.SendMessage( + windowHandle, + Win32.WM_ACTIVATE, + Win32.WA_INACTIVE, + IntPtr.Zero); + } + } + + /// + /// 尝试将窗口背景设置成云母材质。 + /// + /// 要操作的窗口。 + /// 设置成功返回 ,否则返回 + public bool TrySetMicaBackdrop(Window window) + { + if (MicaController.IsSupported()) + { + _queueHelper = new(); + _queueHelper.EnsureWindowsSystemDispatcherQueueController(); + + _backdropConfiguration = new(); + window.Activated += Window_Activated; + window.Closed += Window_Closed; + var windowContent = (FrameworkElement)window.Content; + windowContent.ActualThemeChanged += Window_ThemeChanged; + + _backdropConfiguration.IsInputActive = true; + SetConfigurationSourceTheme(windowContent); + + _micaController = new(); + + _micaController.AddSystemBackdropTarget(window as ICompositionSupportsSystemBackdrop); + _micaController.SetSystemBackdropConfiguration(_backdropConfiguration); + return true; + } + + return false; + } + + /// + /// 尝试将窗口背景设置成亚克力材质。 + /// + /// 要操作的窗口。 + /// 设置成功返回 ,否则返回 + public bool TrySetAcrylicBackdrop(Window window) + { + if (DesktopAcrylicController.IsSupported()) + { + _queueHelper = new(); + _queueHelper.EnsureWindowsSystemDispatcherQueueController(); + + _backdropConfiguration = new(); + window.Activated += Window_Activated; + window.Closed += Window_Closed; + var windowContent = (FrameworkElement)window.Content; + windowContent.ActualThemeChanged += Window_ThemeChanged; + + _backdropConfiguration.IsInputActive = true; + SetConfigurationSourceTheme(windowContent); + + _acrylicController = new(); + + _acrylicController.AddSystemBackdropTarget(window as ICompositionSupportsSystemBackdrop); + _acrylicController.SetSystemBackdropConfiguration(_backdropConfiguration); + return true; + } + + return false; + } + + private void Window_Activated(object sender, WindowActivatedEventArgs args) + { + _backdropConfiguration.IsInputActive = args.WindowActivationState != WindowActivationState.Deactivated; + } + + private void Window_Closed(object sender, WindowEventArgs args) + { + if (_micaController != null) + { + _micaController.Dispose(); + _micaController = null; + } + + if (_acrylicController != null) + { + _acrylicController.Dispose(); + _acrylicController = null; + } + + (sender as Window)!.Activated -= Window_Activated; + _backdropConfiguration = null; + } + + private void Window_ThemeChanged(FrameworkElement sender, object args) + { + if (_backdropConfiguration != null) + { + SetConfigurationSourceTheme(sender); + } + } + + /// + /// 根据内容的主题设置云母或亚克力材质的主题。 + /// + /// 主题发生变化的内容。 + private void SetConfigurationSourceTheme(FrameworkElement content) + { + _backdropConfiguration.Theme = content.ActualTheme switch + { + ElementTheme.Dark => SystemBackdropTheme.Dark, + ElementTheme.Light => SystemBackdropTheme.Light, + ElementTheme.Default => SystemBackdropTheme.Default, + _ => SystemBackdropTheme.Default, + }; + } + + /// + /// 创建一个 实例,并将其加入到监控中。 + /// + /// 实例的名称。 + /// 创建的实例。 + public Window CreateWindow(string windowName) + { + var window = new Window(); + _windows.Add(windowName, window); + return window; + } + + /// + /// 创建一个 实例,并将其加入到监控中。 + /// + /// 实例的名称。 + /// 创建完成后触发的操作。 + /// 创建的实例。 + public Window CreateWindow(string windowName, Action callback) + { + var window = CreateWindow(windowName); + callback(window); + return window; + } + + /// + /// 根据 实例名称获取对应的实例。 + /// + /// 实例名称。 + /// 对应的实例。 + public Window GetWindow(string windowName) + { + return _windows[windowName]; + } + + /// + /// 尝试根据 实例名称获取对应的实例。 + /// + /// 实例名称。 + /// 用于接收对应实例的变量。 + /// 获取成功返回 ,否则返回 + public bool TryGetWindow(string windowName, out Window window) + { + if (_windows.ContainsKey(windowName)) + { + window = _windows[windowName]; + return true; + } + + window = null; + return false; + } + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Helpers/WindowsSystemDispatcherQueueHelper.cs b/AkiraVoid.WordBook/Helpers/WindowsSystemDispatcherQueueHelper.cs new file mode 100644 index 0000000..f2d382a --- /dev/null +++ b/AkiraVoid.WordBook/Helpers/WindowsSystemDispatcherQueueHelper.cs @@ -0,0 +1,40 @@ +using System.Runtime.InteropServices; + +namespace AkiraVoid.WordBook.Helpers +{ + /// + /// 参考 WinUI 3 Gallery 上的示例。 + /// + internal class WindowsSystemDispatcherQueueHelper + { + [StructLayout(LayoutKind.Sequential)] + struct DispatcherQueueOptions + { + internal int _dwSize; + internal int _threadType; + internal int _apartmentType; + } + + [DllImport("CoreMessaging.dll")] + private static extern int CreateDispatcherQueueController([In] DispatcherQueueOptions options, [In, Out, MarshalAs(UnmanagedType.IUnknown)] ref object dispatcherQueueController); + + object _dispatcherQueueController = null; + + public void EnsureWindowsSystemDispatcherQueueController() + { + if (Windows.System.DispatcherQueue.GetForCurrentThread() != null) + { + return; + } + if (_dispatcherQueueController == null) + { + DispatcherQueueOptions options; + options._dwSize = Marshal.SizeOf(typeof(DispatcherQueueOptions)); + options._threadType = 2; // DQTYPE_THREAD_CURRENT + options._apartmentType = 2; // DQTAT_COM_STA + CreateDispatcherQueueController(options, ref _dispatcherQueueController); + } + } + + } +} diff --git a/AkiraVoid.WordBook/Migrations/20230310064651_WordBank.Designer.cs b/AkiraVoid.WordBook/Migrations/20230310064651_WordBank.Designer.cs new file mode 100644 index 0000000..0092621 --- /dev/null +++ b/AkiraVoid.WordBook/Migrations/20230310064651_WordBank.Designer.cs @@ -0,0 +1,316 @@ +// +using System; +using AkiraVoid.WordBook.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AkiraVoid.WordBook.Migrations +{ + [DbContext(typeof(WordBankContext))] + [Migration("20230310064651_WordBank")] + partial class WordBank + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + modelBuilder.Entity("AkiraVoid.WordBook.Models.PartOfSpeech", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Abbreviation") + .HasColumnType("TEXT") + .HasColumnName("abbreviation"); + + b.Property("DisplayName") + .HasColumnType("TEXT") + .HasColumnName("display_name"); + + b.Property("Name") + .HasColumnType("INTEGER") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("parts_of_speech"); + + b.HasData( + new + { + Id = new Guid("5d46507e-65ac-44d7-987e-ddd9b06a8b38"), + Abbreviation = "n.", + DisplayName = "noun", + Name = 0 + }, + new + { + Id = new Guid("34cc4f49-c92a-453a-aebe-e3917afb806a"), + Abbreviation = "pron.", + DisplayName = "pronoun", + Name = 1 + }, + new + { + Id = new Guid("43b5f7f3-860f-408c-9614-a5ba26d6a18b"), + Abbreviation = "adj.", + DisplayName = "adjective", + Name = 2 + }, + new + { + Id = new Guid("67959c5c-ae7f-40b7-8953-5382852476e4"), + Abbreviation = "adv.", + DisplayName = "adverb", + Name = 3 + }, + new + { + Id = new Guid("634b15d4-3934-4977-ae86-f14ac0307ae3"), + Abbreviation = "v.", + DisplayName = "verb", + Name = 4 + }, + new + { + Id = new Guid("fb9c654e-1e02-49ca-9378-b6dd37cc043a"), + Abbreviation = "num.", + DisplayName = "numeral", + Name = 5 + }, + new + { + Id = new Guid("7f433147-fa2b-408a-be15-835cf1b7aa2f"), + Abbreviation = "art.", + DisplayName = "article", + Name = 6 + }, + new + { + Id = new Guid("9a95aa8d-0089-4a83-a255-52028645fbde"), + Abbreviation = "prep.", + DisplayName = "preposition", + Name = 7 + }, + new + { + Id = new Guid("2254bace-a80c-41b6-9f68-8e6519c3458b"), + Abbreviation = "conj.", + DisplayName = "conjunction", + Name = 8 + }, + new + { + Id = new Guid("b71ab6c6-79d2-429b-bbe0-30336aa27219"), + Abbreviation = "interj.", + DisplayName = "interjection", + Name = 9 + }, + new + { + Id = new Guid("2235054b-f47d-4156-ad20-1b14a4114292"), + Abbreviation = "名", + DisplayName = "名詞", + Name = 10 + }, + new + { + Id = new Guid("9c61aff0-79a6-49e1-ba7c-e7432c6ae604"), + Abbreviation = "代", + DisplayName = "代名詞", + Name = 11 + }, + new + { + Id = new Guid("7513069d-13f5-4b1a-bac4-792330a225c0"), + Abbreviation = "数", + DisplayName = "数詞", + Name = 12 + }, + new + { + Id = new Guid("ad2b60da-cc90-40f3-b672-085552fe16c6"), + Abbreviation = "動", + DisplayName = "動詞", + Name = 13 + }, + new + { + Id = new Guid("f361aa7f-dd59-4a68-9c7a-6a67c8c702e1"), + Abbreviation = "形", + DisplayName = "形容詞", + Name = 14 + }, + new + { + Id = new Guid("5d127d36-9841-4198-b435-44e976ca6d6a"), + Abbreviation = "形動", + DisplayName = "形容動詞", + Name = 15 + }, + new + { + Id = new Guid("49f06470-0b2a-46dc-8b8d-63ae2a036634"), + Abbreviation = "副", + DisplayName = "副詞", + Name = 16 + }, + new + { + Id = new Guid("78753e63-283d-4419-87d9-a159795c8950"), + Abbreviation = "連体", + DisplayName = "連体詞", + Name = 17 + }, + new + { + Id = new Guid("b4f48c14-ddac-4baa-bb24-fdcec7bdb76f"), + Abbreviation = "接続", + DisplayName = "接続詞", + Name = 18 + }, + new + { + Id = new Guid("6f438479-edd2-46ef-b40e-10086f8cb638"), + Abbreviation = "感動", + DisplayName = "感動詞", + Name = 19 + }, + new + { + Id = new Guid("1315f153-1596-4622-bdba-434b239c63b9"), + Abbreviation = "助動", + DisplayName = "助動詞", + Name = 20 + }, + new + { + Id = new Guid("c6360920-2930-4db2-84da-024ca152a4c0"), + Abbreviation = "助", + DisplayName = "助詞", + Name = 21 + }); + }); + + modelBuilder.Entity("AkiraVoid.WordBook.Models.Word", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AddedAt") + .HasColumnType("TEXT") + .HasColumnName("added_at"); + + b.Property("HasMemorized") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("has_memorized"); + + b.Property("IsImportant") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("is_important"); + + b.Property("Language") + .HasColumnType("INTEGER") + .HasColumnName("language"); + + b.Property("LastMemorizedAt") + .HasColumnType("TEXT") + .HasColumnName("last_memorized_at"); + + b.Property("MemorizationTimes") + .HasColumnType("INTEGER") + .HasColumnName("memorization_times"); + + b.Property("Pronunciation") + .HasColumnType("TEXT") + .HasColumnName("Pronunciation"); + + b.Property("Spell") + .HasColumnType("TEXT") + .HasColumnName("spell"); + + b.HasKey("Id"); + + b.ToTable("words"); + }); + + modelBuilder.Entity("AkiraVoid.WordBook.Models.WordExplanation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Explanation") + .HasColumnType("TEXT") + .HasColumnName("explanation"); + + b.Property("PartOfSpeechId") + .HasColumnType("TEXT") + .HasColumnName("part_of_speech_id"); + + b.Property("Translation") + .HasColumnType("TEXT") + .HasColumnName("translation"); + + b.Property("WordId") + .HasColumnType("TEXT") + .HasColumnName("word_id"); + + b.HasKey("Id"); + + b.HasIndex("PartOfSpeechId"); + + b.HasIndex("WordId"); + + b.ToTable("explanations"); + }); + + modelBuilder.Entity("AkiraVoid.WordBook.Models.WordExplanation", b => + { + b.HasOne("AkiraVoid.WordBook.Models.PartOfSpeech", "PartOfSpeech") + .WithMany("Explanations") + .HasForeignKey("PartOfSpeechId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired() + .HasConstraintName("part_of_speech_to_explanations"); + + b.HasOne("AkiraVoid.WordBook.Models.Word", "Word") + .WithMany("Explanations") + .HasForeignKey("WordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("word_to_explanations"); + + b.Navigation("PartOfSpeech"); + + b.Navigation("Word"); + }); + + modelBuilder.Entity("AkiraVoid.WordBook.Models.PartOfSpeech", b => + { + b.Navigation("Explanations"); + }); + + modelBuilder.Entity("AkiraVoid.WordBook.Models.Word", b => + { + b.Navigation("Explanations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AkiraVoid.WordBook/Migrations/20230310064651_WordBank.cs b/AkiraVoid.WordBook/Migrations/20230310064651_WordBank.cs new file mode 100644 index 0000000..51c4b28 --- /dev/null +++ b/AkiraVoid.WordBook/Migrations/20230310064651_WordBank.cs @@ -0,0 +1,128 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace AkiraVoid.WordBook.Migrations +{ + /// + public partial class WordBank : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "parts_of_speech", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + name = table.Column(type: "INTEGER", nullable: false), + display_name = table.Column(type: "TEXT", nullable: true), + abbreviation = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_parts_of_speech", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "words", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + spell = table.Column(type: "TEXT", nullable: true), + language = table.Column(type: "INTEGER", nullable: false), + added_at = table.Column(type: "TEXT", nullable: false), + last_memorized_at = table.Column(type: "TEXT", nullable: false), + memorization_times = table.Column(type: "INTEGER", nullable: false), + Pronunciation = table.Column(type: "TEXT", nullable: true), + has_memorized = table.Column(type: "INTEGER", nullable: false, defaultValue: false), + is_important = table.Column(type: "INTEGER", nullable: false, defaultValue: false) + }, + constraints: table => + { + table.PrimaryKey("PK_words", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "explanations", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + explanation = table.Column(type: "TEXT", nullable: true), + translation = table.Column(type: "TEXT", nullable: true), + word_id = table.Column(type: "TEXT", nullable: false), + part_of_speech_id = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_explanations", x => x.id); + table.ForeignKey( + name: "part_of_speech_to_explanations", + column: x => x.part_of_speech_id, + principalTable: "parts_of_speech", + principalColumn: "id"); + table.ForeignKey( + name: "word_to_explanations", + column: x => x.word_id, + principalTable: "words", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "parts_of_speech", + columns: new[] { "id", "abbreviation", "display_name", "name" }, + values: new object[,] + { + { new Guid("1315f153-1596-4622-bdba-434b239c63b9"), "助動", "助動詞", 20 }, + { new Guid("2235054b-f47d-4156-ad20-1b14a4114292"), "名", "名詞", 10 }, + { new Guid("2254bace-a80c-41b6-9f68-8e6519c3458b"), "conj.", "conjunction", 8 }, + { new Guid("34cc4f49-c92a-453a-aebe-e3917afb806a"), "pron.", "pronoun", 1 }, + { new Guid("43b5f7f3-860f-408c-9614-a5ba26d6a18b"), "adj.", "adjective", 2 }, + { new Guid("49f06470-0b2a-46dc-8b8d-63ae2a036634"), "副", "副詞", 16 }, + { new Guid("5d127d36-9841-4198-b435-44e976ca6d6a"), "形動", "形容動詞", 15 }, + { new Guid("5d46507e-65ac-44d7-987e-ddd9b06a8b38"), "n.", "noun", 0 }, + { new Guid("634b15d4-3934-4977-ae86-f14ac0307ae3"), "v.", "verb", 4 }, + { new Guid("67959c5c-ae7f-40b7-8953-5382852476e4"), "adv.", "adverb", 3 }, + { new Guid("6f438479-edd2-46ef-b40e-10086f8cb638"), "感動", "感動詞", 19 }, + { new Guid("7513069d-13f5-4b1a-bac4-792330a225c0"), "数", "数詞", 12 }, + { new Guid("78753e63-283d-4419-87d9-a159795c8950"), "連体", "連体詞", 17 }, + { new Guid("7f433147-fa2b-408a-be15-835cf1b7aa2f"), "art.", "article", 6 }, + { new Guid("9a95aa8d-0089-4a83-a255-52028645fbde"), "prep.", "preposition", 7 }, + { new Guid("9c61aff0-79a6-49e1-ba7c-e7432c6ae604"), "代", "代名詞", 11 }, + { new Guid("ad2b60da-cc90-40f3-b672-085552fe16c6"), "動", "動詞", 13 }, + { new Guid("b4f48c14-ddac-4baa-bb24-fdcec7bdb76f"), "接続", "接続詞", 18 }, + { new Guid("b71ab6c6-79d2-429b-bbe0-30336aa27219"), "interj.", "interjection", 9 }, + { new Guid("c6360920-2930-4db2-84da-024ca152a4c0"), "助", "助詞", 21 }, + { new Guid("f361aa7f-dd59-4a68-9c7a-6a67c8c702e1"), "形", "形容詞", 14 }, + { new Guid("fb9c654e-1e02-49ca-9378-b6dd37cc043a"), "num.", "numeral", 5 } + }); + + migrationBuilder.CreateIndex( + name: "IX_explanations_part_of_speech_id", + table: "explanations", + column: "part_of_speech_id"); + + migrationBuilder.CreateIndex( + name: "IX_explanations_word_id", + table: "explanations", + column: "word_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "explanations"); + + migrationBuilder.DropTable( + name: "parts_of_speech"); + + migrationBuilder.DropTable( + name: "words"); + } + } +} diff --git a/AkiraVoid.WordBook/Migrations/WordBankContextModelSnapshot.cs b/AkiraVoid.WordBook/Migrations/WordBankContextModelSnapshot.cs new file mode 100644 index 0000000..8f8fcc8 --- /dev/null +++ b/AkiraVoid.WordBook/Migrations/WordBankContextModelSnapshot.cs @@ -0,0 +1,313 @@ +// +using System; +using AkiraVoid.WordBook.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AkiraVoid.WordBook.Migrations +{ + [DbContext(typeof(WordBankContext))] + partial class WordBankContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + modelBuilder.Entity("AkiraVoid.WordBook.Models.PartOfSpeech", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Abbreviation") + .HasColumnType("TEXT") + .HasColumnName("abbreviation"); + + b.Property("DisplayName") + .HasColumnType("TEXT") + .HasColumnName("display_name"); + + b.Property("Name") + .HasColumnType("INTEGER") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("parts_of_speech"); + + b.HasData( + new + { + Id = new Guid("5d46507e-65ac-44d7-987e-ddd9b06a8b38"), + Abbreviation = "n.", + DisplayName = "noun", + Name = 0 + }, + new + { + Id = new Guid("34cc4f49-c92a-453a-aebe-e3917afb806a"), + Abbreviation = "pron.", + DisplayName = "pronoun", + Name = 1 + }, + new + { + Id = new Guid("43b5f7f3-860f-408c-9614-a5ba26d6a18b"), + Abbreviation = "adj.", + DisplayName = "adjective", + Name = 2 + }, + new + { + Id = new Guid("67959c5c-ae7f-40b7-8953-5382852476e4"), + Abbreviation = "adv.", + DisplayName = "adverb", + Name = 3 + }, + new + { + Id = new Guid("634b15d4-3934-4977-ae86-f14ac0307ae3"), + Abbreviation = "v.", + DisplayName = "verb", + Name = 4 + }, + new + { + Id = new Guid("fb9c654e-1e02-49ca-9378-b6dd37cc043a"), + Abbreviation = "num.", + DisplayName = "numeral", + Name = 5 + }, + new + { + Id = new Guid("7f433147-fa2b-408a-be15-835cf1b7aa2f"), + Abbreviation = "art.", + DisplayName = "article", + Name = 6 + }, + new + { + Id = new Guid("9a95aa8d-0089-4a83-a255-52028645fbde"), + Abbreviation = "prep.", + DisplayName = "preposition", + Name = 7 + }, + new + { + Id = new Guid("2254bace-a80c-41b6-9f68-8e6519c3458b"), + Abbreviation = "conj.", + DisplayName = "conjunction", + Name = 8 + }, + new + { + Id = new Guid("b71ab6c6-79d2-429b-bbe0-30336aa27219"), + Abbreviation = "interj.", + DisplayName = "interjection", + Name = 9 + }, + new + { + Id = new Guid("2235054b-f47d-4156-ad20-1b14a4114292"), + Abbreviation = "名", + DisplayName = "名詞", + Name = 10 + }, + new + { + Id = new Guid("9c61aff0-79a6-49e1-ba7c-e7432c6ae604"), + Abbreviation = "代", + DisplayName = "代名詞", + Name = 11 + }, + new + { + Id = new Guid("7513069d-13f5-4b1a-bac4-792330a225c0"), + Abbreviation = "数", + DisplayName = "数詞", + Name = 12 + }, + new + { + Id = new Guid("ad2b60da-cc90-40f3-b672-085552fe16c6"), + Abbreviation = "動", + DisplayName = "動詞", + Name = 13 + }, + new + { + Id = new Guid("f361aa7f-dd59-4a68-9c7a-6a67c8c702e1"), + Abbreviation = "形", + DisplayName = "形容詞", + Name = 14 + }, + new + { + Id = new Guid("5d127d36-9841-4198-b435-44e976ca6d6a"), + Abbreviation = "形動", + DisplayName = "形容動詞", + Name = 15 + }, + new + { + Id = new Guid("49f06470-0b2a-46dc-8b8d-63ae2a036634"), + Abbreviation = "副", + DisplayName = "副詞", + Name = 16 + }, + new + { + Id = new Guid("78753e63-283d-4419-87d9-a159795c8950"), + Abbreviation = "連体", + DisplayName = "連体詞", + Name = 17 + }, + new + { + Id = new Guid("b4f48c14-ddac-4baa-bb24-fdcec7bdb76f"), + Abbreviation = "接続", + DisplayName = "接続詞", + Name = 18 + }, + new + { + Id = new Guid("6f438479-edd2-46ef-b40e-10086f8cb638"), + Abbreviation = "感動", + DisplayName = "感動詞", + Name = 19 + }, + new + { + Id = new Guid("1315f153-1596-4622-bdba-434b239c63b9"), + Abbreviation = "助動", + DisplayName = "助動詞", + Name = 20 + }, + new + { + Id = new Guid("c6360920-2930-4db2-84da-024ca152a4c0"), + Abbreviation = "助", + DisplayName = "助詞", + Name = 21 + }); + }); + + modelBuilder.Entity("AkiraVoid.WordBook.Models.Word", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AddedAt") + .HasColumnType("TEXT") + .HasColumnName("added_at"); + + b.Property("HasMemorized") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("has_memorized"); + + b.Property("IsImportant") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false) + .HasColumnName("is_important"); + + b.Property("Language") + .HasColumnType("INTEGER") + .HasColumnName("language"); + + b.Property("LastMemorizedAt") + .HasColumnType("TEXT") + .HasColumnName("last_memorized_at"); + + b.Property("MemorizationTimes") + .HasColumnType("INTEGER") + .HasColumnName("memorization_times"); + + b.Property("Pronunciation") + .HasColumnType("TEXT") + .HasColumnName("Pronunciation"); + + b.Property("Spell") + .HasColumnType("TEXT") + .HasColumnName("spell"); + + b.HasKey("Id"); + + b.ToTable("words"); + }); + + modelBuilder.Entity("AkiraVoid.WordBook.Models.WordExplanation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Explanation") + .HasColumnType("TEXT") + .HasColumnName("explanation"); + + b.Property("PartOfSpeechId") + .HasColumnType("TEXT") + .HasColumnName("part_of_speech_id"); + + b.Property("Translation") + .HasColumnType("TEXT") + .HasColumnName("translation"); + + b.Property("WordId") + .HasColumnType("TEXT") + .HasColumnName("word_id"); + + b.HasKey("Id"); + + b.HasIndex("PartOfSpeechId"); + + b.HasIndex("WordId"); + + b.ToTable("explanations"); + }); + + modelBuilder.Entity("AkiraVoid.WordBook.Models.WordExplanation", b => + { + b.HasOne("AkiraVoid.WordBook.Models.PartOfSpeech", "PartOfSpeech") + .WithMany("Explanations") + .HasForeignKey("PartOfSpeechId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired() + .HasConstraintName("part_of_speech_to_explanations"); + + b.HasOne("AkiraVoid.WordBook.Models.Word", "Word") + .WithMany("Explanations") + .HasForeignKey("WordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("word_to_explanations"); + + b.Navigation("PartOfSpeech"); + + b.Navigation("Word"); + }); + + modelBuilder.Entity("AkiraVoid.WordBook.Models.PartOfSpeech", b => + { + b.Navigation("Explanations"); + }); + + modelBuilder.Entity("AkiraVoid.WordBook.Models.Word", b => + { + b.Navigation("Explanations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AkiraVoid.WordBook/Models/PartOfSpeech.cs b/AkiraVoid.WordBook/Models/PartOfSpeech.cs new file mode 100644 index 0000000..6fc3011 --- /dev/null +++ b/AkiraVoid.WordBook/Models/PartOfSpeech.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using AkiraVoid.WordBook.Enums; +using AkiraVoid.WordBook.Utilities; + +namespace AkiraVoid.WordBook.Models; + +[Table("parts_of_speech")] +public class PartOfSpeech +{ + [Key] [Column("id")] public Guid Id { get; set; } + [Column("name")] public PartOfSpeechName Name { get; set; } + [Column("display_name")] public string DisplayName { get; set; } + [Column("abbreviation")] public string Abbreviation { get; set; } + public IList Explanations { get; set; } + + /// + /// 检查两个 实例是否相等。 + /// + /// + /// 相等则返回 ,否则返回 + public bool Equals(PartOfSpeech partOfSpeech) + { + return Name == partOfSpeech.Name; + } + + /// + public override string ToString() + { + return DisplayName.Capitalize(); + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Models/Word.cs b/AkiraVoid.WordBook/Models/Word.cs new file mode 100644 index 0000000..b49c9cd --- /dev/null +++ b/AkiraVoid.WordBook/Models/Word.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Runtime.CompilerServices; +using AkiraVoid.WordBook.Enums; + +namespace AkiraVoid.WordBook.Models; + +[Table("words")] +public class Word : INotifyPropertyChanged +{ + [NotMapped] private Guid _id = Guid.NewGuid(); + [NotMapped] private string _spell; + [NotMapped] private WordLanguage _language; + [NotMapped] private DateTime _addedAt = DateTime.Now; + [NotMapped] private DateTime _lastMemorizedAt = default; + [NotMapped] private int _memorizationTimes = 0; + [NotMapped] private string _pronunciation; + [NotMapped] private bool _hasMemorized; + [NotMapped] private bool _isImportant; + [NotMapped] private ObservableCollection _explanations; + + [Key] + [Column("id")] + public Guid Id + { + get => _id; + set => SetField(ref _id, value); + } + + [Column("spell")] + public string Spell + { + get => _spell; + set => SetField(ref _spell, value); + } + + [Column("language")] + public WordLanguage Language + { + get => _language; + set => SetField(ref _language, value); + } + + [Column("added_at")] + public DateTime AddedAt + { + get => _addedAt; + set => SetField(ref _addedAt, value); + } + + [Column("last_memorized_at")] + public DateTime LastMemorizedAt + { + get => _lastMemorizedAt; + set => SetField(ref _lastMemorizedAt, value); + } + + [Column("memorization_times")] + public int MemorizationTimes + { + get => _memorizationTimes; + set => SetField(ref _memorizationTimes, value); + } + + [Column("Pronunciation")] + public string Pronunciation + { + get => _pronunciation; + set => SetField(ref _pronunciation, value); + } + + [Column("has_memorized")] + public bool HasMemorized + { + get => _hasMemorized; + set => SetField(ref _hasMemorized, value); + } + + [Column("is_important")] + public bool IsImportant + { + get => _isImportant; + set => SetField(ref _isImportant, value); + } + + public ObservableCollection Explanations + { + get => _explanations; + set + { + value.CollectionChanged += (_, _) => OnPropertyChanged(nameof(Explanations)); + _explanations = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new(propertyName)); + } + + protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) return false; + field = value; + OnPropertyChanged(propertyName); + return true; + } + + /// + public override string ToString() + { + return Spell; + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Models/WordBankContext.cs b/AkiraVoid.WordBook/Models/WordBankContext.cs new file mode 100644 index 0000000..6ae77e0 --- /dev/null +++ b/AkiraVoid.WordBook/Models/WordBankContext.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.IO; +using AkiraVoid.WordBook.Enums; +using Microsoft.EntityFrameworkCore; + +namespace AkiraVoid.WordBook.Models; + +public class WordBankContext : DbContext +{ + public DbSet Words { get; set; } + public DbSet PartsOfSpeech { get; set; } + public DbSet WordExplanations { get; set; } + + public WordBankContext() + { + } + + public WordBankContext(DbContextOptions options) : base(options) + { + } + + /// + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) + { + var connectionString = $"Data Source={Path.Combine(Environment.CurrentDirectory, "WordBank.db")}"; + optionsBuilder.UseSqlite(connectionString); + + // 开发环境可以通过取消下方注释来查看 EntityFramework Core 的日志输出。 + + //optionsBuilder.LogTo(message => System.Diagnostics.Debug.WriteLine(message)) + // .EnableDetailedErrors() + // .EnableSensitiveDataLogging(); + } + + base.OnConfiguring(optionsBuilder); + } + + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasMany(entity => entity.Explanations) + .WithOne(entity => entity.Word) + .HasForeignKey(entity => entity.WordId) + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("word_to_explanations"); + modelBuilder.Entity().Property(entity => entity.HasMemorized).HasDefaultValue(false); + modelBuilder.Entity().Property(entity => entity.IsImportant).HasDefaultValue(false); + + modelBuilder.Entity() + .HasMany(entity => entity.Explanations) + .WithOne(entity => entity.PartOfSpeech) + .HasForeignKey(entity => entity.PartOfSpeechId) + .OnDelete(DeleteBehavior.NoAction) + .HasConstraintName("part_of_speech_to_explanations"); + modelBuilder.Entity().Property(entity => entity.Id).ValueGeneratedOnAdd(); + SeedPartsOfSpeech(modelBuilder); + + modelBuilder.Entity().Property(entity => entity.Id).ValueGeneratedOnAdd(); + } + + /// + /// 向数据库中插入词性数据。 + /// + /// + private void SeedPartsOfSpeech(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasData( + new List + { + new() + { + Name = PartOfSpeechName.Noun, + DisplayName = "noun", + Abbreviation = "n.", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Pronoun, + DisplayName = "pronoun", + Abbreviation = "pron.", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Adjective, + DisplayName = "adjective", + Abbreviation = "adj.", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Adverb, + DisplayName = "adverb", + Abbreviation = "adv.", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Verb, + DisplayName = "verb", + Abbreviation = "v.", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Numeral, + DisplayName = "numeral", + Abbreviation = "num.", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Article, + DisplayName = "article", + Abbreviation = "art.", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Preposition, + DisplayName = "preposition", + Abbreviation = "prep.", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Conjunction, + DisplayName = "conjunction", + Abbreviation = "conj.", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Interjection, + DisplayName = "interjection", + Abbreviation = "interj.", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Meishi, + DisplayName = "名詞", + Abbreviation = "名", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Daimeishi, + DisplayName = "代名詞", + Abbreviation = "代", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Suushi, + DisplayName = "数詞", + Abbreviation = "数", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Doushi, + DisplayName = "動詞", + Abbreviation = "動", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Keiyoushi, + DisplayName = "形容詞", + Abbreviation = "形", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Keiyoudoushi, + DisplayName = "形容動詞", + Abbreviation = "形動", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Fukushi, + DisplayName = "副詞", + Abbreviation = "副", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Rentaishi, + DisplayName = "連体詞", + Abbreviation = "連体", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Setsuzokushi, + DisplayName = "接続詞", + Abbreviation = "接続", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Kandoushi, + DisplayName = "感動詞", + Abbreviation = "感動", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Jodoushi, + DisplayName = "助動詞", + Abbreviation = "助動", + Id = Guid.NewGuid() + }, + new() + { + Name = PartOfSpeechName.Joshi, + DisplayName = "助詞", + Abbreviation = "助", + Id = Guid.NewGuid() + }, + }); + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Models/WordExplanation.cs b/AkiraVoid.WordBook/Models/WordExplanation.cs new file mode 100644 index 0000000..05f57d1 --- /dev/null +++ b/AkiraVoid.WordBook/Models/WordExplanation.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Runtime.CompilerServices; + +namespace AkiraVoid.WordBook.Models; + +[Table("explanations")] +public class WordExplanation : INotifyPropertyChanged +{ + [NotMapped] private Guid _id; + [NotMapped] private string _explanation; + [NotMapped] private string _translation; + [NotMapped] private Guid _wordId; + [NotMapped] private Guid _partOfSpeechId; + [NotMapped] private Word _word; + [NotMapped] private PartOfSpeech _partOfSpeech; + + /// + /// 检查该实例是否为一个实践意义上的空实例。 + /// + /// 如果是空实例则返回 ,否则返回 + public bool IsEmpty() + { + return string.IsNullOrEmpty(Explanation) || string.IsNullOrEmpty(Translation) || PartOfSpeech is null; + } + + [Key] + [Column("id")] + public Guid Id + { + get => _id; + set => SetField(ref _id, value); + } + + [Column("explanation")] + public string Explanation + { + get => _explanation; + set => SetField(ref _explanation, value); + } + + [Column("translation")] + public string Translation + { + get => _translation; + set => SetField(ref _translation, value); + } + + [Column("word_id")] + public Guid WordId + { + get => _wordId; + set => SetField(ref _wordId, value); + } + + [Column("part_of_speech_id")] + public Guid PartOfSpeechId + { + get => _partOfSpeechId; + set => SetField(ref _partOfSpeechId, value); + } + + public Word Word + { + get => _word; + set => SetField(ref _word, value); + } + + public PartOfSpeech PartOfSpeech + { + get => _partOfSpeech; + set => SetField(ref _partOfSpeech, value); + } + + public WordExplanation(Guid wordId) + { + WordId = wordId; + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new(propertyName)); + } + + protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) return false; + field = value; + OnPropertyChanged(propertyName); + return true; + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Package.appxmanifest b/AkiraVoid.WordBook/Package.appxmanifest new file mode 100644 index 0000000..b8c8cc0 --- /dev/null +++ b/AkiraVoid.WordBook/Package.appxmanifest @@ -0,0 +1,48 @@ + + + + + + + + AkiraVoid WordBook + AkiraVoid Productions + Assets\Logo\squre-light.png + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AkiraVoid.WordBook/Pages/DictationPage.xaml b/AkiraVoid.WordBook/Pages/DictationPage.xaml new file mode 100644 index 0000000..2ff23de --- /dev/null +++ b/AkiraVoid.WordBook/Pages/DictationPage.xaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + * + * + + + 听写选项 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AkiraVoid.WordBook/Pages/DictationPage.xaml.cs b/AkiraVoid.WordBook/Pages/DictationPage.xaml.cs new file mode 100644 index 0000000..5387616 --- /dev/null +++ b/AkiraVoid.WordBook/Pages/DictationPage.xaml.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using Windows.System; +using AkiraVoid.WordBook.Controls; +using AkiraVoid.WordBook.Enums; +using AkiraVoid.WordBook.Models; +using AkiraVoid.WordBook.Utilities; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; + +// 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.Pages +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class DictationPage : Page + { + public DictationPage() + { + this.InitializeComponent(); + } + + private int WordCount + { + get => (int)GetValue(_wordCountProperty); + set => SetValue(_wordCountProperty, value); + } + + private static readonly DependencyProperty _wordCountProperty = DependencyProperty.Register( + nameof(WordCount), + typeof(int), + typeof(DictationPage), + new(2)); + + private readonly ObservableCollection _words = new(); + private readonly ObservableCollection _ranges = new(Enum.GetValues()); + + private int _toPlayIndex; + + private int ToPlayIndex + { + get => _toPlayIndex; + set + { + _toPlayIndex = value; + ReplayButton.IsEnabled = value > 0; + RefreshButton.IsEnabled = value > 0; + NextButton.IsEnabled = value < _words.Count; + Checker.IsEnabled = value > 0; + Checker.State = InputValidationState.Unvalidated; + Checker.ClearInput(); + } + } + + private void WordRangeChanged(object sender, SelectionChangedEventArgs args) => SelectWords(); + + private void SelectWords() + { + var selectedWordRange = (DictationWordRange)WordRangePicker.SelectedItem; + _words.Clear(); + switch (selectedWordRange) + { + case DictationWordRange.Random: + SelectRandomWords(); + break; + case DictationWordRange.All: + SelectAllWords(); + break; + case DictationWordRange.Today: + SelectWordsOfToday(); + break; + case DictationWordRange.Yesterday: + SelectWordsOfYesterday(); + break; + case DictationWordRange.Important: + SelectImportantWords(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + ToPlayIndex = 0; + } + + private void SelectRandomWords() => + _words.AddRange(Global.WordList.Where(w => !w.HasMemorized).ToList().Shuffle().Take(WordCount)); + + private void SelectAllWords() => + _words.AddRange(Global.WordList.Where(w => !w.HasMemorized).ToList().Shuffle()); + + private void SelectWordsOfToday() => + _words.AddRange( + Global.WordList.Where(w => DateTime.Now - w.AddedAt <= TimeSpan.FromDays(1)) + .Where(w => !w.HasMemorized) + .Take(WordCount) + .ToList() + .Shuffle()); + + private void SelectWordsOfYesterday() => + _words.AddRange( + Global.WordList + .Where( + w => DateTime.Now - w.AddedAt <= TimeSpan.FromDays(2) && + DateTime.Now - w.AddedAt > TimeSpan.FromDays(1)) + .Where(w => !w.HasMemorized) + .Take(WordCount) + .ToList() + .Shuffle()); + + private void SelectImportantWords() => + _words.AddRange(Global.WordList.Where(w => w.IsImportant).Take(WordCount).ToList().Shuffle()); + + private async void OnPlayNextAsync(object sender, RoutedEventArgs args) => await PlayNextAsync(); + + private async Task PlayNextAsync() + { + await Teachers.SpeakAsync(_words[ToPlayIndex]); + Checker.Word = _words[ToPlayIndex]; + if (ToPlayIndex < _words.Count) + { + ToPlayIndex++; + } + } + + private async void OnReplayAsync(object sender, RoutedEventArgs args) => + await Teachers.SpeakAsync(_words[ToPlayIndex - 1]); + + private async void OnEnterPressedAsync(object sender, KeyRoutedEventArgs args) + { + if (args.Key == VirtualKey.Enter) + { + if (Checker.State != InputValidationState.Unvalidated) + { + await PlayNextAsync(); + } + else + { + ((SpellChecker)sender).TriggerValidation(); + } + } + } + + private void OnReset(object sender, RoutedEventArgs e) => SelectWords(); + + private void OnSettingClick(object sender, RoutedEventArgs e) + { + Root.IsPaneOpen = !Root.IsPaneOpen; + } + + private void OnPaneClose(object sender, RoutedEventArgs e) + { + Root.IsPaneOpen = false; + } + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Pages/RootPage.xaml b/AkiraVoid.WordBook/Pages/RootPage.xaml new file mode 100644 index 0000000..a8935c3 Binary files /dev/null and b/AkiraVoid.WordBook/Pages/RootPage.xaml differ diff --git a/AkiraVoid.WordBook/Pages/RootPage.xaml.cs b/AkiraVoid.WordBook/Pages/RootPage.xaml.cs new file mode 100644 index 0000000..014debe --- /dev/null +++ b/AkiraVoid.WordBook/Pages/RootPage.xaml.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using Microsoft.UI.Xaml.Controls; +using System; +using System.Linq; +using AkiraVoid.WordBook.Helpers; +using AkiraVoid.WordBook.Models; +using AkiraVoid.WordBook.Utilities; +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.Pages +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class RootPage : Page + { + private readonly Navigator _navigator; + private bool _isContinuousSettingsInvoking = false; + + public RootPage() + { + this.InitializeComponent(); + if (!Global.UIHelper.TryGetWindow("MainWindow", out var mainWindow)) + { + throw new("No window was rendered."); + } + + mainWindow.SetTitleBar(AppTitleBar); + + _navigator = new(MainFrame, Navigation); + Global.Navigator = _navigator; + Navigation.ItemInvoked += OnSettingsClick; + + int selectedTheme = ThemeSwitcher.GetTheme() switch + { + ElementTheme.Dark => 2, + ElementTheme.Light => 1, + ElementTheme.Default => 0, + _ => throw new ArgumentException("theme") + }; + ThemeSelector.SelectedIndex = selectedTheme; + ThemeSwitcher.SwitchTheme(this); + } + + public void OnSettingsClick(object sender, NavigationViewItemInvokedEventArgs args) + { + if (!args.IsSettingsInvoked) return; + if (!_isContinuousSettingsInvoking) + { + DrawerContainer.IsPaneOpen = !DrawerContainer.IsPaneOpen; + _isContinuousSettingsInvoking = true; + } + else + { + _isContinuousSettingsInvoking = false; + } + } + + public void OnThemeChange(object sender, SelectionChangedEventArgs args) + { + var selectedIndex = (sender as RadioButtons)!.SelectedIndex; + var theme = selectedIndex == 2 ? ElementTheme.Dark : + selectedIndex == 1 ? ElementTheme.Light : ElementTheme.Default; + ThemeSwitcher.SwitchTheme(theme, this); + } + + private void OnSearching(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) + { + if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) + { + var suggestion = Global.WordList + .Where(w => w.Spell.Contains(sender.Text, StringComparison.InvariantCultureIgnoreCase)) + .ToList(); + sender.ItemsSource = suggestion; + } + } + + private void OnSuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args) + { + if (args.SelectedItem is Word word) + { + _navigator.Navigate(_navigator.Navigations.FirstOrDefault(nav => nav.PageTag == "WordDetail"), word); + } + } + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Pages/WordBookPage.xaml b/AkiraVoid.WordBook/Pages/WordBookPage.xaml new file mode 100644 index 0000000..3f0ebc5 --- /dev/null +++ b/AkiraVoid.WordBook/Pages/WordBookPage.xaml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Auto + Auto + * + + + + + + + + + + + + + + + 已选中 + 项 + + + + + + + + + + + + + + + + + Auto + * + + + + + 添加或编辑词汇 + + + + + + + + + \ No newline at end of file diff --git a/AkiraVoid.WordBook/Pages/WordBookPage.xaml.cs b/AkiraVoid.WordBook/Pages/WordBookPage.xaml.cs new file mode 100644 index 0000000..8afc8f0 --- /dev/null +++ b/AkiraVoid.WordBook/Pages/WordBookPage.xaml.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using Microsoft.UI.Xaml.Controls; +using System.Linq; +using AkiraVoid.WordBook.Models; +using AkiraVoid.WordBook.Utilities; +using AkiraVoid.WordBook.ViewModels; +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.Pages +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class WordBookPage : Page + { + public SelectedItemCollection SelectedWords { get; set; } = new(); + + public WordBookPage() + { + this.InitializeComponent(); + SelectedWords.CountChanged += OnSelectionChanged; + EditWordButton.IsEnabled = SelectedWords.IsBetween(1, 1); + DeleteWordButton.IsEnabled = SelectedWords.HasSelectedItems(); + ReadMoreButton.IsEnabled = SelectedWords.IsBetween(1, 1); + } + + private void OnSelectionChanged(object sender, CountChangedEventArgs e) + { + EditWordButton.IsEnabled = SelectedWords.IsBetween(1, 1); + DeleteWordButton.IsEnabled = SelectedWords.HasSelectedItems(); + ReadMoreButton.IsEnabled = SelectedWords.IsBetween(1, 1); + } + + private void OnPaneClose(object sender, RoutedEventArgs args) + { + PageRoot.IsPaneOpen = false; + } + + private void OnAdditionPageOpen(object sender, RoutedEventArgs args) + { + Actions.Navigate(typeof(WordEditionPage)); + PageRoot.IsPaneOpen = true; + } + + private void OnEditionPageOpen(object sender, RoutedEventArgs args) + { + Actions.Navigate(typeof(WordEditionPage), SelectedWords[0]); + PageRoot.IsPaneOpen = true; + } + + private void OnWordListSelectionChanged(object sender, SelectionChangedEventArgs e) + { + foreach (object removedItem in e.RemovedItems) + { + SelectedWords.Remove((Word)removedItem); + } + + foreach (object addedItem in e.AddedItems) + { + SelectedWords.Add((Word)addedItem); + } + } + + private void OnImportantToggled(object sender, WordListRoutedEventArgs e) + { + var word = Global.WordList.FirstOrDefault(w => w.Id == e.WordId); + if (word != null) + { + word.IsImportant = !word.IsImportant; + Global.WordBank.Entry(word).State = EntityState.Modified; + Global.WordBank.SaveChanges(); + } + } + + private void OnDeleted(object sender, RoutedEventArgs e) => DeleteSelection(); + + private void DeleteSelection() + { + Global.WordBank.Words.RemoveRange(SelectedWords); + foreach (var selectedWord in SelectedWords.ToList()) + { + Global.WordList.Remove(selectedWord); + } + + SelectedWords.Clear(); + Global.WordBank.SaveChanges(); + } + + private void OnMemorizedToggle(object sender, WordListRoutedEventArgs e) + { + var word = Global.WordList.FirstOrDefault(w => w.Id == e.WordId); + if (word != null) + { + word.HasMemorized = !word.HasMemorized; + Global.WordBank.Entry(word).State = EntityState.Modified; + Global.WordBank.SaveChanges(); + } + } + + private void OnReadMoreRequested(object sender, RoutedEventArgs e) + { + Global.Navigator.Navigate( + Global.Navigator.Navigations.Find(nav => nav.PageTag == "WordDetail"), + SelectedWords[0]); + } + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Pages/WordDetailPage.xaml b/AkiraVoid.WordBook/Pages/WordDetailPage.xaml new file mode 100644 index 0000000..c1f42b2 --- /dev/null +++ b/AkiraVoid.WordBook/Pages/WordDetailPage.xaml @@ -0,0 +1,68 @@ + + + + + + + + + + Auto + * + + + + + + + + + Auto + * + + + Auto + Auto + + + + + + + + + + + + + * + * + + + + + + + + + + \ No newline at end of file diff --git a/AkiraVoid.WordBook/Pages/WordDetailPage.xaml.cs b/AkiraVoid.WordBook/Pages/WordDetailPage.xaml.cs new file mode 100644 index 0000000..a71c666 --- /dev/null +++ b/AkiraVoid.WordBook/Pages/WordDetailPage.xaml.cs @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using AkiraVoid.WordBook.Models; +using AkiraVoid.WordBook.Utilities; + +// 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.Pages +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class WordDetailPage : Page + { + public WordDetailPage() + { + this.InitializeComponent(); + } + + private Word Word { get; set; } + + private readonly ObservableCollection> _groupedExplanations = new(); + + public object MemorizeButtonContent + { + get => GetValue(MemorizeButtonContentProperty); + set => SetValue(MemorizeButtonContentProperty, value); + } + + public static readonly DependencyProperty MemorizeButtonContentProperty = DependencyProperty.Register( + nameof(MemorizeButtonContent), + typeof(object), + typeof(WordDetailPage), + new("标记为已记住")); + + public FontIcon ImportantButtonIcon + { + get => (FontIcon)GetValue(ImportantButtonIconProperty); + set => SetValue(ImportantButtonIconProperty, value); + } + + public static readonly DependencyProperty ImportantButtonIconProperty = DependencyProperty.Register( + nameof(ImportantButtonIcon), + typeof(FontIcon), + typeof(WordDetailPage), + new(null)); + + public string ImportantButtonToolTip + { + get => (string)GetValue(ImportantButtonToolTipProperty); + set => SetValue(ImportantButtonToolTipProperty, value); + } + + public static readonly DependencyProperty ImportantButtonToolTipProperty = DependencyProperty.Register( + nameof(ImportantButtonToolTip), + typeof(string), + typeof(WordDetailPage), + new("标记为重要")); + + /// + protected override void OnNavigatedTo(NavigationEventArgs args) + { + base.OnNavigatedTo(args); + Word = args.Parameter is not null ? args.Parameter as Word : new() { Explanations = new() }; + _groupedExplanations.Clear(); + _groupedExplanations.AddRange(Word?.Explanations.GroupBy(e => e.PartOfSpeech)); + Word!.PropertyChanged += OnWordPropertyChange; + var memorizedContent = new StackPanel { Orientation = Orientation.Horizontal, }; + memorizedContent.Children.Add( + new FontIcon + { + FontFamily = (FontFamily)Resources["SymbolThemeFontFamily"], + Glyph = "\xe73e", + Margin = new( + 0, + 0, + 4, + 0) + }); + memorizedContent.Children.Add(new TextBlock { Text = "已记住" }); + + MemorizeButtonContent = Word.HasMemorized ? memorizedContent : "标记为已记住"; + ImportantButtonIcon = Word.IsImportant + ? new() { FontFamily = (FontFamily)Resources["SymbolThemeFontFamily"], Glyph = "\xe735" } + : new() { FontFamily = (FontFamily)Resources["SymbolThemeFontFamily"], Glyph = "\xe734" }; + ImportantButtonToolTip = Word.IsImportant ? "重要" : "标记为重要"; + } + + private void OnWordPropertyChange(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == nameof(Word.HasMemorized)) + { + var memorizedContent = new StackPanel { Orientation = Orientation.Horizontal, }; + memorizedContent.Children.Add( + new FontIcon + { + FontFamily = (FontFamily)Resources["SymbolThemeFontFamily"], + Glyph = "\xe73e", + Margin = new( + 0, + 0, + 4, + 0) + }); + memorizedContent.Children.Add(new TextBlock { Text = "已记住" }); + + MemorizeButtonContent = Word.HasMemorized ? memorizedContent : "标记为已记住"; + } + else if (args.PropertyName == nameof(Word.IsImportant)) + { + ImportantButtonIcon = Word.IsImportant + ? new() { FontFamily = (FontFamily)Resources["SymbolThemeFontFamily"], Glyph = "\xe735" } + : new() { FontFamily = (FontFamily)Resources["SymbolThemeFontFamily"], Glyph = "\xe734" }; + ImportantButtonToolTip = Word.IsImportant ? "重要" : "标记为重要"; + } + } + + private void OnMemorize(object sender, RoutedEventArgs e) + { + Word.HasMemorized = !Word.HasMemorized; + Global.WordBank.SaveChanges(); + } + + private void OnSetImportant(object sender, RoutedEventArgs e) + { + Word.IsImportant = !Word.IsImportant; + Global.WordBank.SaveChanges(); + } + } + + public class ExplanationGroup : IGrouping + { + /// + public IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + public PartOfSpeech Key { get; } + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Pages/WordEditionPage.xaml b/AkiraVoid.WordBook/Pages/WordEditionPage.xaml new file mode 100644 index 0000000..420c830 --- /dev/null +++ b/AkiraVoid.WordBook/Pages/WordEditionPage.xaml @@ -0,0 +1,59 @@ + + + + + + + + + 基本信息 + + + + + English + 日本語 + + + 词汇解释 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AkiraVoid.WordBook/Pages/WordEditionPage.xaml.cs b/AkiraVoid.WordBook/Pages/WordEditionPage.xaml.cs new file mode 100644 index 0000000..c70b208 --- /dev/null +++ b/AkiraVoid.WordBook/Pages/WordEditionPage.xaml.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation and Contributors. +// Licensed under the MIT License. + +using System.Collections.ObjectModel; +using System.ComponentModel; +using AkiraVoid.WordBook.Enums; +using AkiraVoid.WordBook.Models; +using AkiraVoid.WordBook.Utilities; +using AkiraVoid.WordBook.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Navigation; + +// 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.Pages +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class WordEditionPage : Page + { + private bool _isEditing; + + public Word Word + { + get => (Word)GetValue(WordProperty); + set + { + NewExplanations.Clear(); + + if (NewExplanations.Count <= 0 && value.Explanations is not { Count: > 0 }) + { + NewExplanations.Add(new(value.Id)); + } + + SetValue(WordProperty, value); + } + } + + public static readonly DependencyProperty WordProperty = DependencyProperty.Register( + nameof(Word), + typeof(Word), + typeof(WordEditionPage), + new(new())); + + public ObservableCollection NewExplanations { get; set; } = new(); + + public WordEditionPage() + { + this.InitializeComponent(); + + LanguagePicker.SelectionChanged += (sender, args) => + { + Word.Language = LanguagePicker.SelectedIndex == 0 ? WordLanguage.English : WordLanguage.Japanese; + }; + } + + /// + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + Word = e.Parameter is not null ? e.Parameter as Word : new() { Explanations = new() }; + _isEditing = e.Parameter is not null; + Word!.PropertyChanged += OnWordLanguageChange; + LanguagePicker.SelectedIndex = Word.Language == WordLanguage.English ? 0 : 1; + } + + private void OnExplanationAdded(object sender, ExplanationEditorRoutedEventArgs args) + { + NewExplanations.Add(new(Word.Id)); + } + + private void OnNewExplanationRemoved(object sender, ExplanationEditorRoutedEventArgs args) + { + NewExplanations.Remove(args.Explanation); + if (NewExplanations.Count <= 0 && Word.Explanations.Count <= 0) + { + NewExplanations.Add(new(Word.Id)); + } + } + + private void OnExplanationRemoved(object sender, ExplanationEditorRoutedEventArgs args) + { + Word.Explanations.Remove(args.Explanation); + if (NewExplanations.Count <= 0 && Word.Explanations.Count <= 0) + { + NewExplanations.Add(new(Word.Id)); + } + } + + private void OnWordLanguageChange(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == nameof(Word.Language)) + { + LanguagePicker.SelectedIndex = Word.Language == WordLanguage.English ? 0 : 1; + } + } + + private void OnConfirm(object sender, RoutedEventArgs e) + { + ConfirmButton.IsEnabled = false; + foreach (var explanation in NewExplanations) + { + if (!explanation.IsEmpty()) + { + Word.Explanations.Add(explanation); + } + } + + if (_isEditing) + { + Global.WordBank.SaveChanges(); + NewExplanations.Clear(); + if (Word.Explanations.Count <= 0 && NewExplanations.Count <= 0) + { + NewExplanations.Add(new(Word.Id)); + } + } + else + { + Global.WordBank.Add(Word); + Global.WordBank.SaveChanges(); + + + Global.WordList.Add(Word); + + Word = new() { Explanations = new() }; + var binding = new Binding { Source = Word.Explanations }; + OriginalExplanationEditors.SetBinding(ItemsRepeater.ItemsSourceProperty, binding); + } + + ConfirmButton.IsEnabled = true; + } + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Properties/launchSettings.json b/AkiraVoid.WordBook/Properties/launchSettings.json new file mode 100644 index 0000000..30e7d6e --- /dev/null +++ b/AkiraVoid.WordBook/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "AkiraVoid.WordBook (Package)": { + "commandName": "MsixPackage" + }, + "AkiraVoid.WordBook (Unpackaged)": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Themes/Generic.xaml b/AkiraVoid.WordBook/Themes/Generic.xaml new file mode 100644 index 0000000..4c8b07e --- /dev/null +++ b/AkiraVoid.WordBook/Themes/Generic.xaml @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/AkiraVoid.WordBook/Utilities/Configuration.cs b/AkiraVoid.WordBook/Utilities/Configuration.cs new file mode 100644 index 0000000..67faa1b --- /dev/null +++ b/AkiraVoid.WordBook/Utilities/Configuration.cs @@ -0,0 +1,44 @@ +using System.Configuration; + +namespace AkiraVoid.WordBook.Utilities; + +/// +/// 提供一系列简化配置操作的方法。 +/// +public static class Configuration +{ + /// + /// 获取配置项。 + /// + /// 指向配置项的键。 + /// 返回获取的配置项。配置项不存在则返回 + public static string GetConfiguration(string key) + { + return ConfigurationManager.AppSettings[key]; + } + + /// + /// 设置某项配置项。该方法使用 UPSERT 语义。 + /// + /// 配置项的键。 + /// 配置项的值。 + /// + /// UPSERT 语义通常用于数据库操作,指代存在该实体则更新之(UPDATE),否则插入新的实体(INSERT)。 + /// + public static void SetConfiguration(string key, string value) + { + var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); + var settings = config.AppSettings.Settings; + if (settings[key] == null) + { + settings.Add(key, value); + } + else + { + settings[key].Value = value; + } + + config.Save(ConfigurationSaveMode.Modified); + ConfigurationManager.RefreshSection(config.AppSettings.SectionInformation.SectionName); + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Utilities/Extensions.cs b/AkiraVoid.WordBook/Utilities/Extensions.cs new file mode 100644 index 0000000..24ead0b --- /dev/null +++ b/AkiraVoid.WordBook/Utilities/Extensions.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Security.Cryptography; + +namespace AkiraVoid.WordBook.Utilities; + +/// +/// 包含一系列扩展方法。 +/// +public static class Extensions +{ + /// + /// 将字符串的首字母大写。 + /// + /// + /// 首字母大写后的字符串。 + public static string Capitalize(this string str) + { + return str.Length > 1 ? str[0].ToString().ToUpperInvariant() + str[1..] : str.ToUpperInvariant(); + } + + /// + /// 提供类似 JavaScript 中 array.map 函数相似的功能。 + /// + /// 原集合中元素的类型。 + /// 新集合中元素的类型。 + /// + /// 要执行的操作。 + /// 基于 构建的新集合。 + /// + /// JavaScript 中 array.map 函数会在数组上执行指定的操作,并将该操作的返回值作为元素构建一个新数组。 + /// + public static IEnumerable Map( + this IEnumerable source, + Func conversion) + { + var list = new List(); + foreach (var item in source) + { + list.Add(conversion(item)); + } + + return list.AsEnumerable(); + } + + /// + /// 将集合随机打乱。该方法只能用于那些支持随机访问的集合。 + /// + /// 集合中元素的类型。 + /// + /// 打乱后的该集合本身。 + public static IList Shuffle(this IList source) + { + // Knuth-Durstenfeld Shuffle 算法 + // 时间 O(n),空间 O(1) + for (var i = source.Count - 1; i > 0; i--) + { + var j = RandomNumberGenerator.GetInt32(i + 1); // toExclusive 参数不包含临界点,需要 +1 + (source[i], source[j]) = (source[j], source[i]); + } + + return source; + } + + /// + /// 向集合中添加复数个元素。 + /// + /// 集合中元素的类型。 + /// + /// 要添加的元素。 + public static void AddRange(this ObservableCollection source, IEnumerable targets) + { + foreach (var target in targets) + { + source.Add(target); + } + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Utilities/Global.cs b/AkiraVoid.WordBook/Utilities/Global.cs new file mode 100644 index 0000000..fb45bac --- /dev/null +++ b/AkiraVoid.WordBook/Utilities/Global.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using AkiraVoid.WordBook.Helpers; +using AkiraVoid.WordBook.Models; + +namespace AkiraVoid.WordBook.Utilities; + +public static class Global +{ + /// + /// 全局数据库实例。 + /// + public static readonly WordBankContext WordBank = new(); + +#pragma warning disable CA2211 + // ReSharper disable once InconsistentNaming + public static UIHelper UIHelper = new(); + + /// + /// 全局词汇列表。 + /// + public static ObservableCollection WordList = new(); + + /// + /// 全局词性列表。 + /// + public static List PartsOfSpeech = new(); + + /// + /// RootPage 所使用的导航器。 + /// + public static Navigator Navigator; +#pragma warning restore CA2211 +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Utilities/Teachers.cs b/AkiraVoid.WordBook/Utilities/Teachers.cs new file mode 100644 index 0000000..d038d45 --- /dev/null +++ b/AkiraVoid.WordBook/Utilities/Teachers.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Windows.Media.Playback; +using Windows.Media.SpeechSynthesis; +using AkiraVoid.WordBook.Enums; +using AkiraVoid.WordBook.Models; +using AkiraVoid.WordBook.ViewModels; + +namespace AkiraVoid.WordBook.Utilities; + +/// +/// 提供与听写有关的一系列方法。 +/// +public static class Teachers +{ + /// + /// 合成语音并播放。 + /// + /// 单词的拼写。 + /// 单词的语言。 + /// 媒体播放器。 + /// 异步任务。 + private static async Task SynthesizeAndPlayAsync(string spell, WordLanguage language, MediaPlayer player) + { + var teacher = new SpeechSynthesizer(); + SetVoice(language, ref teacher); + var source = await teacher.SynthesizeTextToStreamAsync(spell); + player.SetStreamSource(source); + teacher.Dispose(); + player.Play(); + } + + /// + /// 获取一次性使用的 实例。 + /// + /// 一次性使用的 实例。 + private static MediaPlayer GetOneTimeMediaPlayerPlayer() + { + var mediaPlayer = new MediaPlayer(); + mediaPlayer.MediaEnded += (player, _) => player.Dispose(); + return mediaPlayer; + } + + /// + /// 设置语音合成器使用的人声。 + /// + /// 人声语言。 + /// 语音合成器。 + private static void SetVoice(WordLanguage language, ref SpeechSynthesizer synthesizer) + { + var teacher = GetConfiguredTeacher(language)?.ToVoiceInformation(); + + synthesizer.Voice = teacher; + } + + /// + /// 播放合成语言。 + /// + /// 要合成语音的单词。 + /// 异步任务。 + public static async Task SpeakAsync(Word word) + { + await SynthesizeAndPlayAsync(word.Spell, word.Language, GetOneTimeMediaPlayerPlayer()); + } + + /// + /// 播放合成语言。 + /// + /// 要合成语音的文本。 + /// 文本语言。 + /// 异步任务。 + public static async Task SpeakAsync(string text, WordLanguage language) + { + await SynthesizeAndPlayAsync(text, language, GetOneTimeMediaPlayerPlayer()); + } + + /// + /// 设置对应语言的讲述人,并将该设置保存至配置项。 + /// + /// 讲述人语言。 + /// 讲述人。 + public static void SetTeacher(WordLanguage language, Teacher teacher) + { + Configuration.SetConfiguration($"{language.ToString().ToLowerInvariant()}TeacherId", teacher.Id); + } + + /// + /// 获取所有可用讲述人。 + /// + /// 所有可用讲述人。 + public static IEnumerable GetTeachers() + { + return SpeechSynthesizer.AllVoices.Map((v) => new Teacher(v)); + } + + /// + /// 获取所有指定语言下的可用讲述人。 + /// + /// 讲述人语言,使用标准化 BCP-47 语言代码表示。 + /// 所有指定语言下的可用讲述人。 + public static List GetTeachers(WordLanguage language) + { + var languageCode = language == WordLanguage.English ? "en-US" : "ja-JP"; + return SpeechSynthesizer.AllVoices.Where(v => v.Language == languageCode).Map(v => new Teacher(v)).ToList(); + } + + /// + /// 获取指定语言的保存在配置项中的讲述人。 + /// + /// 讲述人语言。 + /// 指定语言的保存在配置项中的讲述人。 + public static Teacher GetConfiguredTeacher(WordLanguage language) + { + var defaultTeacher = language == WordLanguage.English + ? SpeechSynthesizer.AllVoices.FirstOrDefault(v => v.Language == "en-US") + : SpeechSynthesizer.AllVoices.FirstOrDefault(v => v.Language == "ja-JP"); + if (defaultTeacher == null) + { + defaultTeacher = SpeechSynthesizer.DefaultVoice; + } + + var configuredTeacherId = Configuration.GetConfiguration($"{language.ToString().ToLowerInvariant()}TeacherId"); + if (string.IsNullOrEmpty(configuredTeacherId) || configuredTeacherId == "null") + { + return new(defaultTeacher); + } + + return new(SpeechSynthesizer.AllVoices.FirstOrDefault(v => v.Id == configuredTeacherId) ?? defaultTeacher); + } + + /// + /// 根据讲述人 ID 获取讲述人。 + /// + /// 讲述人 ID。 + /// 对应的讲述人。 + public static Teacher GetTeacher(string id) + { + return GetTeachers().FirstOrDefault(v => v.Id == id); + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Utilities/ThemeSwitcher.cs b/AkiraVoid.WordBook/Utilities/ThemeSwitcher.cs new file mode 100644 index 0000000..ccd4646 --- /dev/null +++ b/AkiraVoid.WordBook/Utilities/ThemeSwitcher.cs @@ -0,0 +1,74 @@ +using System; +using AkiraVoid.WordBook.Helpers; +using Microsoft.UI; +using Microsoft.UI.Xaml; + +namespace AkiraVoid.WordBook.Utilities; + +/// +/// 提供一系列用于操作应用主题的方法。 +/// +public static class ThemeSwitcher +{ + /// + /// 切换应用主题到目标主题。 + /// + /// 目标主题。 + /// 要应用主题的元素。 + /// 的值不在 的可能值范围内。 + public static void SwitchTheme(ElementTheme theme, FrameworkElement root) + { + root.RequestedTheme = theme; + var resources = root.Resources; + var captionButtonColor = theme switch + { + ElementTheme.Default => Win32.ShouldSystemUseDarkMode() ? Colors.White : Colors.Black, + ElementTheme.Light => Colors.Black, + ElementTheme.Dark => Colors.White, + _ => throw new ArgumentOutOfRangeException(nameof(theme), theme, null) + }; + + resources["WindowCaptionForeground"] = captionButtonColor; + UIHelper.TriggerTitleBarRepaint(Global.UIHelper.GetWindow("MainWindow")); + + Configuration.SetConfiguration("theme", theme.ToString()); + } + + /// + /// 切换应用主题到配置项中配置的主题。 + /// + /// 要应用主题的元素。 + /// + /// 如果配置项中没有配置主题,则默认跟随系统主题,并将其记录在配置项中。 + /// + public static void SwitchTheme(FrameworkElement root) + { + var themeSetting = Configuration.GetConfiguration("theme"); + if (!Enum.TryParse(themeSetting, out var theme)) + { + theme = ElementTheme.Default; + Configuration.SetConfiguration("theme", theme.ToString()); + } + + SwitchTheme(theme, root); + } + + /// + /// 获取现在配置项中配置的主题。 + /// + /// 配置项中配置的主题。 + /// + /// 如果配置项中没有配置主题,则默认跟随系统主题,并将其记录在配置项中。 + /// + public static ElementTheme GetTheme() + { + var themeSetting = Configuration.GetConfiguration("theme"); + if (!Enum.TryParse(themeSetting, out var theme)) + { + theme = ElementTheme.Default; + Configuration.SetConfiguration("theme", theme.ToString()); + } + + return theme; + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/Utilities/Win32.cs b/AkiraVoid.WordBook/Utilities/Win32.cs new file mode 100644 index 0000000..07b8df2 --- /dev/null +++ b/AkiraVoid.WordBook/Utilities/Win32.cs @@ -0,0 +1,36 @@ +using System; +using System.Runtime.InteropServices; + +namespace AkiraVoid.WordBook.Utilities +{ + /// + /// 提供一系列与 Win32 接口操作的方法。 + /// + internal static class Win32 + { + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage( + IntPtr hWnd, + int Msg, + int wParam, + IntPtr lParam); + + [DllImport("user32.dll")] public static extern IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName); + + [DllImport("user32.dll")] public static extern IntPtr GetActiveWindow(); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr GetModuleHandle(IntPtr moduleName); + + [DllImport("UXTheme.dll", SetLastError = true, EntryPoint = "#138")] + public static extern bool ShouldSystemUseDarkMode(); + + public const int WM_ACTIVATE = 0x0006; + public const int WA_ACTIVE = 0x01; + public const int WA_INACTIVE = 0x00; + + public const int WM_SETICON = 0x0080; + public const int ICON_SMALL = 0; + public const int ICON_BIG = 1; + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/ViewModels/CountChangedEventArgs.cs b/AkiraVoid.WordBook/ViewModels/CountChangedEventArgs.cs new file mode 100644 index 0000000..b7db951 --- /dev/null +++ b/AkiraVoid.WordBook/ViewModels/CountChangedEventArgs.cs @@ -0,0 +1,16 @@ +using System; + +namespace AkiraVoid.WordBook.ViewModels; + +public class CountChangedEventArgs : EventArgs +{ + /// + /// 先前的数量。 + /// + public int Previous { get; set; } + + /// + /// 现在的数量。 + /// + public int Now { get; set; } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/ViewModels/ExplanationEditorEditedEventArgs.cs b/AkiraVoid.WordBook/ViewModels/ExplanationEditorEditedEventArgs.cs new file mode 100644 index 0000000..6546828 --- /dev/null +++ b/AkiraVoid.WordBook/ViewModels/ExplanationEditorEditedEventArgs.cs @@ -0,0 +1,27 @@ +using AkiraVoid.WordBook.Models; + +namespace AkiraVoid.WordBook.ViewModels; + +public class ExplanationEditorEditedEventArgs +{ + /// + /// 被编辑的属性名称。 + /// + public string EditedProperty { get; set; } + + /// + /// 被编辑的解释。 + /// + public WordExplanation Explanation { get; set; } + + /// + /// 初始化一个 实例。 + /// + /// 被编辑的属性名称。 + /// 被编辑的解释。 + public ExplanationEditorEditedEventArgs(WordExplanation explanation, string editedProperty = null) + { + EditedProperty = editedProperty; + Explanation = explanation; + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/ViewModels/ExplanationEditorRoutedEventArgs.cs b/AkiraVoid.WordBook/ViewModels/ExplanationEditorRoutedEventArgs.cs new file mode 100644 index 0000000..9ee1851 --- /dev/null +++ b/AkiraVoid.WordBook/ViewModels/ExplanationEditorRoutedEventArgs.cs @@ -0,0 +1,23 @@ +using AkiraVoid.WordBook.Models; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace AkiraVoid.WordBook.ViewModels; + +public class ExplanationEditorRoutedEventArgs : RoutedEventArgs +{ + /// + public new object OriginalSource { get; } = null; + + public AppBarButton ActionButton { get; set; } + public WordExplanation Explanation { get; set; } + + public ExplanationEditorRoutedEventArgs() + { + } + + public ExplanationEditorRoutedEventArgs(RoutedEventArgs args) + { + OriginalSource = args.OriginalSource; + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/ViewModels/Navigation.cs b/AkiraVoid.WordBook/ViewModels/Navigation.cs new file mode 100644 index 0000000..11044aa --- /dev/null +++ b/AkiraVoid.WordBook/ViewModels/Navigation.cs @@ -0,0 +1,11 @@ +using System; + +namespace AkiraVoid.WordBook.ViewModels; + +public class Navigation +{ + public Type PageType { get; set; } + public Action CallBack { get; set; } + public string PageTitle { get; set; } + public string PageTag { get; set; } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/ViewModels/RadioButtonItem.cs b/AkiraVoid.WordBook/ViewModels/RadioButtonItem.cs new file mode 100644 index 0000000..bf20733 --- /dev/null +++ b/AkiraVoid.WordBook/ViewModels/RadioButtonItem.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace AkiraVoid.WordBook.ViewModels; + +public class RadioButtonItem : INotifyPropertyChanged +{ + private object _originalItem; + private bool _isChecked; + private string _displayContent; + + public object OriginalItem + { + get => _originalItem; + set => SetField(ref _originalItem, value); + } + + public bool IsChecked + { + get => _isChecked; + set => SetField(ref _isChecked, value); + } + + public string DisplayContent + { + get => _displayContent; + set => SetField(ref _displayContent, value); + } + + public RadioButtonItem() + { + } + + public RadioButtonItem(object originalItem) + { + OriginalItem = originalItem; + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected bool SetField(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) return false; + field = value; + OnPropertyChanged(propertyName); + return true; + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/ViewModels/SelectedItemCollection.cs b/AkiraVoid.WordBook/ViewModels/SelectedItemCollection.cs new file mode 100644 index 0000000..2fa024f --- /dev/null +++ b/AkiraVoid.WordBook/ViewModels/SelectedItemCollection.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace AkiraVoid.WordBook.ViewModels; + +public class SelectedItemCollection : ObservableCollection +{ + public SelectedItemCollection() + { + _previousCount = Count; + CollectionChanged += OnCollectionChanged; + } + + private int _previousCount; + public event EventHandler CountChanged; + + public bool IsMoreThan(int count) => Count > count; + public bool IsLessThan(int count) => Count < count; + public bool HasSelectedItems() => Count > 0; + public bool NoSelectedItems() => Count <= 0; + public bool IsBetween(int atLeast, int atMost) => Count >= atLeast && Count <= atMost; + + private void OnCountChanged() + { + CountChanged?.Invoke(this, new() { Now = Count, Previous = _previousCount }); + _previousCount = Count; + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (Count != _previousCount) + { + OnCountChanged(); + } + } + + /// + public sealed override event NotifyCollectionChangedEventHandler CollectionChanged + { + add => base.CollectionChanged += value; + remove => base.CollectionChanged -= value; + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/ViewModels/Teacher.cs b/AkiraVoid.WordBook/ViewModels/Teacher.cs new file mode 100644 index 0000000..57b6a83 --- /dev/null +++ b/AkiraVoid.WordBook/ViewModels/Teacher.cs @@ -0,0 +1,37 @@ +using System.Linq; +using Windows.Media.SpeechSynthesis; + +namespace AkiraVoid.WordBook.ViewModels; + +public class Teacher +{ + public string Id { get; set; } + public string Name { get; set; } + + public Teacher() + { + } + + public Teacher(VoiceInformation voiceInformation) + { + Id = voiceInformation.Id; + Name = voiceInformation.DisplayName; + } + + public VoiceInformation ToVoiceInformation() + { + return SpeechSynthesizer.AllVoices.FirstOrDefault(v => v.Id == Id); + } + + /// + public override string ToString() + { + return Name; + } + + /// + public bool Equals(Teacher teacher) + { + return Id == teacher?.Id; + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/ViewModels/ValidationEventArgs.cs b/AkiraVoid.WordBook/ViewModels/ValidationEventArgs.cs new file mode 100644 index 0000000..5f98d04 --- /dev/null +++ b/AkiraVoid.WordBook/ViewModels/ValidationEventArgs.cs @@ -0,0 +1,9 @@ +using System; +using AkiraVoid.WordBook.Enums; + +namespace AkiraVoid.WordBook.ViewModels; + +public class ValidationEventArgs : EventArgs +{ + public InputValidationState State { get; set; } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/ViewModels/ValueChangedEventArgs.cs b/AkiraVoid.WordBook/ViewModels/ValueChangedEventArgs.cs new file mode 100644 index 0000000..f3e96f2 --- /dev/null +++ b/AkiraVoid.WordBook/ViewModels/ValueChangedEventArgs.cs @@ -0,0 +1,13 @@ +using System; + +namespace AkiraVoid.WordBook.ViewModels; + +public class ValueChangedEventArgs : EventArgs +{ + public readonly T Value; + + public ValueChangedEventArgs(T value) + { + Value = value; + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/ViewModels/WordListRoutedEventArgs.cs b/AkiraVoid.WordBook/ViewModels/WordListRoutedEventArgs.cs new file mode 100644 index 0000000..b64c086 --- /dev/null +++ b/AkiraVoid.WordBook/ViewModels/WordListRoutedEventArgs.cs @@ -0,0 +1,15 @@ +using System; +using Microsoft.UI.Xaml; + +namespace AkiraVoid.WordBook.ViewModels; + +public class WordListRoutedEventArgs : RoutedEventArgs +{ + public new object OriginalSource { get; } + public Guid WordId { get; set; } + + public WordListRoutedEventArgs(RoutedEventArgs args) + { + OriginalSource = args.OriginalSource; + } +} \ No newline at end of file diff --git a/AkiraVoid.WordBook/app.manifest b/AkiraVoid.WordBook/app.manifest new file mode 100644 index 0000000..1eef3fc --- /dev/null +++ b/AkiraVoid.WordBook/app.manifest @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..52622cc --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ + GLWT(Good Luck With That) Public License + Copyright (c) Everyone, except Author + +Everyone is permitted to copy, distribute, modify, merge, sell, publish, +sublicense or whatever they want with this software but at their OWN RISK. + + Preamble + +The author has absolutely no clue what the code in this project does. +It might just work or not, there is no third option. + + + GOOD LUCK WITH THAT PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION, AND MODIFICATION + + 0. You just DO WHATEVER YOU WANT TO as long as you NEVER LEAVE A +TRACE TO TRACK THE AUTHOR of the original product to blame for or hold +responsible. + +IN NO EVENT SHALL THE AUTHORS 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. + +Good luck and Godspeed. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1dfe262 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# AkiraVoid.WordBook + +纯自用,发出来给朋友参考的。有 Issue 可以发,但是发出来我多半没时间改,急的可以提交 Pull Request,Review 的时间还是有的。 + +目前支持日语和英语,因为这是我自己要学的语言,后期有时间了可能会加多语言支持。 + +该源码在 [GLWT(Good Luck With That,祝你好运)公共许可证](https://github.com/me-shaon/GLWTPL/blob/master/translations/LICENSE_zh-CN)下开源,所以如果你要用请自求多福。 + +This is made for self-using, I published it here is mainly for sharing with friends. You can create issues but I mostly have no time to fix, create pull request instead if you are impatient. + +This app now support Japanese and English, because these are languages I am currently learning. Maybe I will add multi-language support later when I have time. + +This source code is published under the [GLWT(Good Luck With That) Public License](https://github.com/me-shaon/GLWTPL/blob/master/LICENSE), so good luck with that. \ No newline at end of file